Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 5f31ca0

Browse files
committed
Bug 1539502 - [devtools] Prevent leaking all transient global while recording memory leaks. r=jdescottes
This is two reason for leaking the globals: * The JIT/CachedIR that are introduced by trackingAllocationSites=true and can be fixed by calling minimizeMemoryUsage to purge CachedIR objects. * The Debugger::allocationsLog which relates to Debugger.Memory.drainAllocationLog and can be fixed by calling drainAllocationLog to clear allocationsLog. We also have to be careful about disable allocation site recording while doing the GCs. On start and on stop. Both were keeping strong references to the globals. In this patch, I'm also extending the coverage of the AllocationTracker to better assert what precise leaks are reported. Differential Revision: https://phabricator.services.mozilla.com/D127777
1 parent d9c74c8 commit 5f31ca0

File tree

4 files changed

+129
-2
lines changed

4 files changed

+129
-2
lines changed

devtools/client/framework/test/allocations/head.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ async function startRecordingAllocations({
106106
await tracker.startRecordingAllocations(debug_allocations);
107107
}
108108
);
109+
// Trigger a GC in the parent process as this additional ContentTask
110+
// seems to make harder to release objects created before we start recording.
111+
await tracker.doGC();
109112
}
110113

111114
await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS);

devtools/shared/test-helpers/allocation-tracker.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,15 @@ exports.allocationTracker = function({
146146
async startRecordingAllocations(debug_allocations) {
147147
// Do a first pass of GC, to ensure all to-be-freed objects from the first run
148148
// are really freed.
149+
// We have to temporarily disable allocation-site recording in order to ensure
150+
// freeing everything and especially avoid retaining objects in the allocation-log
151+
// related to `drainAllocationLog` feature.
152+
dbg.memory.allocationSamplingProbability = 0.0;
153+
// Also force clearing the allocation log in order to prevent holding alive globals
154+
// which have been destroyed before we start recording
155+
this.flushAllocations();
149156
await this.doGC();
157+
dbg.memory.allocationSamplingProbability = 1.0;
150158

151159
// Measure the current process memory usage
152160
const memory = this.getAllocatedMemory();
@@ -172,16 +180,23 @@ exports.allocationTracker = function({
172180
},
173181

174182
async stopRecordingAllocations(debug_allocations) {
183+
// We have to flush the allocation log in order to prevent leaking some objects
184+
// being hold in memory solely by their allocation-site (i.e. `SavedFrame` in `Debugger::allocationsLog`)
185+
if (debug_allocations != "allocations") {
186+
this.flushAllocations();
187+
}
188+
175189
// Before computing allocations, re-do some GCs in order to free all what is to-be-freed.
176190
await this.doGC();
177191

178192
const memory = this.getAllocatedMemory();
179193
const objects = this.stillAllocatedObjects();
180194

195+
let leaks;
181196
if (debug_allocations == "allocations") {
182197
this.logAllocationLog();
183198
} else if (debug_allocations == "leaks") {
184-
this.logAllocationSitesDiff(this.data.allocations);
199+
leaks = this.logAllocationSitesDiff(this.data.allocations);
185200
}
186201

187202
return {
@@ -190,6 +205,7 @@ exports.allocationTracker = function({
190205
objectsWithStack:
191206
objects.objectsWithStack - this.data.objects.objectsWithStack,
192207
memory: memory - this.data.memory,
208+
leaks,
193209
};
194210
},
195211

@@ -307,6 +323,7 @@ exports.allocationTracker = function({
307323
JSON.stringify(allocationList, null, 2) +
308324
"\n"
309325
);
326+
return allocationList;
310327
},
311328

312329
/**
@@ -436,6 +453,15 @@ exports.allocationTracker = function({
436453
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
437454
await new Promise(resolve => setTimeout(resolve, 1000));
438455
}
456+
457+
// Also call minimizeMemoryUsage as that's the only way to purge JIT cache.
458+
// CachedIR objects (JIT related objects) are ultimately leading to keep
459+
// all transient globals in memory. For some reason, when enabling trackingAllocationSites=true
460+
// we compute stack traces (SavedFrame) for each object being allocated.
461+
// This either create new CachedIR -or- force holding alive existing CachedIR
462+
// and CachedIR itself hold strong references to the transient globals.
463+
// See bug 1733480.
464+
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
439465
},
440466

441467
/**

devtools/shared/test-helpers/browser.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ support-files =
55
allocation-tracker.js
66

77
[browser_allocation_tracker.js]
8-
skip-if = verify # Bug 1730507 - objects without stacks get allocated during the GC of the first test when running multiple times
8+
skip-if = debug | verify # Bug 1730507 - objects without stacks get allocated during the GC of the first test when running multiple times. Also avoid running in debug as we don't try to track memory from debug builds.

devtools/shared/test-helpers/browser_allocation_tracker.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const TrackedObjects = loader.require(
2020
"devtools/shared/test-helpers/tracked-objects.jsm"
2121
);
2222

23+
// This test record multiple times complete heap snapshot,
24+
// so that it can take a little bit to complete.
25+
requestLongerTimeout(2);
26+
2327
add_task(async function() {
2428
// Use a sandbox to allocate test javascript object in order to avoid any
2529
// external noise
@@ -150,3 +154,97 @@ add_task(async function() {
150154

151155
TrackedObjects.clear();
152156
});
157+
158+
add_task(async function() {
159+
info("Test start and stop recording with tracked objects");
160+
161+
const sandbox = Cu.Sandbox(window);
162+
const tracker = allocationTracker({ watchGlobal: sandbox });
163+
await tracker.startRecordingAllocations("leaks");
164+
165+
Cu.evalInSandbox("this.foo = {};", sandbox, null, "sandbox.js", 1);
166+
167+
const record = await tracker.stopRecordingAllocations("leaks");
168+
is(
169+
record.objectsWithStack,
170+
1,
171+
"We get only one leaked objects, the foo object of the sandbox."
172+
);
173+
ok(
174+
record.objectsWithoutStack > 10,
175+
"We get an handful of objects without stacks. Most likely created by Memory API itself."
176+
);
177+
178+
is(
179+
record.leaks.length,
180+
2,
181+
"We get the one leak and the objects with missing stacks"
182+
);
183+
is(
184+
record.leaks[0].src,
185+
"UNKNOWN",
186+
"First item is the objects with missing stacks"
187+
);
188+
// In theory the two following values should be equal,
189+
// but they aren't always because of some dark matter around objects with missing stacks.
190+
// `count` is computed out of `takeCensus`, while `objectsWithoutStack` uses `drainAllocationsLog`
191+
// While the first go through the current GC graph, the second is a record of allocations over time,
192+
// this probably explain why there is some subtle difference
193+
ok(
194+
record.leaks[0].count <= record.objectsWithoutStack,
195+
"For now, the leak report intermittently assume there is less leaked objects than the summary"
196+
);
197+
is(record.leaks[1].src, "sandbox.js", "Second item if about our 'foo' leak");
198+
is(record.leaks[1].count, 1, "We leak one object on this file");
199+
is(record.leaks[1].lines.length, 1, "We leak from only one line");
200+
is(record.leaks[1].lines[0], "1: 1", "On first line, we leak one object");
201+
tracker.stop();
202+
203+
TrackedObjects.clear();
204+
});
205+
206+
add_task(async function() {
207+
info("Test that transient globals are not leaked");
208+
209+
const tracker = allocationTracker({ watchAllGlobals: true });
210+
211+
let sandboxBefore = Cu.Sandbox(window);
212+
// We need to allocate at least one object from the global to reproduce the leak
213+
Cu.evalInSandbox(
214+
"this.foo = {};",
215+
sandboxBefore,
216+
null,
217+
"sandbox-before.js",
218+
1
219+
);
220+
const weakBefore = Cu.getWeakReference(sandboxBefore);
221+
sandboxBefore = null;
222+
223+
await tracker.startRecordingAllocations();
224+
225+
ok(
226+
!weakBefore.get(),
227+
"Sandbox created before the record should have been freed by GCs done by startRecordingAllocations"
228+
);
229+
230+
let sandboxDuring = Cu.Sandbox(window);
231+
// We need to allocate at least one object from the global to reproduce the leak
232+
Cu.evalInSandbox(
233+
"this.bar = {};",
234+
sandboxDuring,
235+
null,
236+
"sandbox-during.js",
237+
1
238+
);
239+
const weakDuring = Cu.getWeakReference(sandboxDuring);
240+
sandboxDuring = null;
241+
242+
await tracker.stopRecordingAllocations();
243+
244+
ok(
245+
!weakDuring.get(),
246+
"Sandbox should have been freed by GCs done by stopRecordingAllocations"
247+
);
248+
249+
tracker.stop();
250+
});

0 commit comments

Comments
 (0)