summaryrefslogtreecommitdiffstats
path: root/devtools/shared/test-helpers/browser_allocation_tracker.js
blob: aedefcb2ba40a24fa6f70f89807648416d6ad567 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Load the tracker in a dedicated loader using invisibleToDebugger and freshCompartment
// so that it can inspect any other module/compartment, even DevTools, chrome,
// and this script!
const { DevToolsLoader } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);
const loader = new DevToolsLoader({
  invisibleToDebugger: true,
  freshCompartment: true,
});
const { allocationTracker } = loader.require(
  "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker"
);
const TrackedObjects = loader.require(
  "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
);

// This test record multiple times complete heap snapshot,
// so that it can take a little bit to complete.
requestLongerTimeout(2);

add_task(async function () {
  // Use a sandbox to allocate test javascript object in order to avoid any
  // external noise
  const global = Cu.Sandbox("http://example.com");

  const tracker = allocationTracker({ watchGlobal: global });
  const before = tracker.stillAllocatedObjects();

  /* eslint-disable no-undef */
  // This will allocation 1001 objects. The array and 1000 elements in it.
  Cu.evalInSandbox(
    "let list; new " +
      function () {
        list = [];
        for (let i = 0; i < 1000; i++) {
          list.push({});
        }
      },
    global,
    undefined,
    "test-file.js",
    1,
    /* enforceFilenameRestrictions */ false
  );
  /* eslint-enable no-undef */

  const allocations = tracker.countAllocations();
  ok(
    allocations >= 1001,
    `At least 1001 objects are reported as created (${allocations})`
  );

  // Uncomment this and comment the call to `countAllocations` to debug the allocations.
  // The call to `countAllocations` will reset the allocation record.
  // tracker.logAllocationSites();

  const afterCreation = tracker.stillAllocatedObjects();
  is(
    afterCreation.objectsWithStack - before.objectsWithStack,
    1001,
    "We got exactly the expected number of objects recorded with an allocation site"
  );
  ok(
    afterCreation.objectsWithStack > before.objectsWithStack,
    "We got some random number of objects without an allocation site"
  );

  Cu.evalInSandbox(
    "list = null;",
    global,
    undefined,
    "test-file.js",
    7,
    /* enforceFilenameRestrictions */ false
  );

  Cu.forceGC();
  Cu.forceCC();

  const afterGC = tracker.stillAllocatedObjects();
  is(
    afterCreation.objectsWithStack - afterGC.objectsWithStack,
    1001,
    "All the expected objects were reported freed in the count with allocation sites"
  );
  ok(
    afterGC.objectsWithoutStack < afterCreation.objectsWithoutStack,
    "And we released some random number of objects without an allocation site"
  );

  tracker.stop();
});

add_task(async function () {
  const leaked = {};
  TrackedObjects.track(leaked);
  let transient = {};
  TrackedObjects.track(transient);

  is(TrackedObjects.getAllNodeIds().length, 2, "The two objects are reported");

  info("Free the transient object");
  transient = null;
  Cu.forceGC();

  is(
    TrackedObjects.getAllNodeIds().length,
    1,
    "We now only have the leaked object"
  );
  TrackedObjects.clear();
});

add_task(async function () {
  info("Test start and stop recording without any debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations();
  await tracker.stopRecordingAllocations();
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with 'allocations' debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations("allocations");
  await tracker.stopRecordingAllocations("allocations");
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with 'leaks' debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations("leaks");
  await tracker.stopRecordingAllocations("leaks");
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with tracked objects");

  const leaked = {};
  TrackedObjects.track(leaked);

  const tracker = allocationTracker({ watchAllGlobals: true });
  await tracker.startRecordingAllocations();
  await tracker.stopRecordingAllocations();
  tracker.stop();

  TrackedObjects.clear();
});

add_task(async function () {
  info("Test start and stop recording with tracked objects");

  const sandbox = Cu.Sandbox(window);
  const tracker = allocationTracker({ watchGlobal: sandbox });
  await tracker.startRecordingAllocations("leaks");

  Cu.evalInSandbox("this.foo = {};", sandbox, null, "sandbox.js", 1);

  const record = await tracker.stopRecordingAllocations("leaks");
  is(
    record.objectsWithStack,
    1,
    "We get only one leaked objects, the foo object of the sandbox."
  );
  ok(
    record.objectsWithoutStack > 10,
    "We get an handful of objects without stacks. Most likely created by Memory API itself."
  );

  is(
    record.leaks.length,
    2,
    "We get the one leak and the objects with missing stacks"
  );
  is(
    record.leaks[0].src,
    "UNKNOWN",
    "First item is the objects with missing stacks"
  );
  // In theory the two following values should be equal,
  // but they aren't always because of some dark matter around objects with missing stacks.
  // `count` is computed out of `takeCensus`, while `objectsWithoutStack` uses `drainAllocationsLog`
  // While the first go through the current GC graph, the second is a record of allocations over time,
  // this probably explain why there is some subtle difference
  ok(
    record.leaks[0].count <= record.objectsWithoutStack,
    "For now, the leak report intermittently assume there is less leaked objects than the summary"
  );
  is(record.leaks[1].src, "sandbox.js", "Second item if about our 'foo' leak");
  is(record.leaks[1].count, 1, "We leak one object on this file");
  is(record.leaks[1].lines.length, 1, "We leak from only one line");
  is(record.leaks[1].lines[0], "1: 1", "On first line, we leak one object");
  tracker.stop();

  TrackedObjects.clear();
});

add_task(async function () {
  info("Test that transient globals are not leaked");

  const tracker = allocationTracker({ watchAllGlobals: true });

  let sandboxBefore = Cu.Sandbox(window);
  // We need to allocate at least one object from the global to reproduce the leak
  Cu.evalInSandbox(
    "this.foo = {};",
    sandboxBefore,
    null,
    "sandbox-before.js",
    1
  );
  const weakBefore = Cu.getWeakReference(sandboxBefore);
  sandboxBefore = null;

  await tracker.startRecordingAllocations();

  ok(
    !weakBefore.get(),
    "Sandbox created before the record should have been freed by GCs done by startRecordingAllocations"
  );

  let sandboxDuring = Cu.Sandbox(window);
  // We need to allocate at least one object from the global to reproduce the leak
  Cu.evalInSandbox(
    "this.bar = {};",
    sandboxDuring,
    null,
    "sandbox-during.js",
    1
  );
  const weakDuring = Cu.getWeakReference(sandboxDuring);
  sandboxDuring = null;

  await tracker.stopRecordingAllocations();

  ok(
    !weakDuring.get(),
    "Sandbox should have been freed by GCs done by stopRecordingAllocations"
  );

  tracker.stop();
});