summaryrefslogtreecommitdiffstats
path: root/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js
blob: 14573fd8a2b3fd10159506eaaa46399a8f19c8f2 (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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * Return a web-based URL for a given file based on the testing directory.
 * @param {String} fileName
 *        file that caller wants its web-based url
 */
function GetTestWebBasedURL(fileName) {
  return (
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "http://example.org"
    ) + fileName
  );
}

const kNS_PER_MS = 1000000;

function printProcInfo(procInfo) {
  info(
    `  pid: ${procInfo.pid}, type = parent, cpu time = ${
      procInfo.cpuTime / kNS_PER_MS
    }ms`
  );
  for (let child of procInfo.children) {
    info(
      `  pid: ${child.pid}, type = ${child.type}, cpu time = ${
        child.cpuTime / kNS_PER_MS
      }ms`
    );
  }
}

// It would be nice to have an API to list all the statically defined labels of
// a labeled_counter. Hopefully bug 1672273 will help with this.
const kGleanProcessTypeLabels = [
  "parent.active",
  "parent.active.playing-audio",
  "parent.active.playing-video",
  "parent.inactive",
  "parent.inactive.playing-audio",
  "parent.inactive.playing-video",
  "prealloc",
  "privilegedabout",
  "rdd",
  "socket",
  "web.background",
  "web.background-perceivable",
  "web.foreground",
  "extension",
  "gpu",
  "gmplugin",
  "utility",
  "__other__",
];

async function getChildCpuTime(pid) {
  return (await ChromeUtils.requestProcInfo()).children.find(p => p.pid == pid)
    .cpuTime;
}

add_task(async () => {
  // Temporarily open a new foreground tab to make the current tab a background
  // tab, and burn some CPU time in it while it's in the background.
  const kBusyWaitForMs = 50;
  let cpuTimeSpentOnBackgroundTab;
  let firstBrowser = gBrowser.selectedTab.linkedBrowser;
  let processPriorityChangedPromise = BrowserTestUtils.contentTopicObserved(
    firstBrowser.browsingContext,
    "ipc:process-priority-changed"
  );
  await BrowserTestUtils.withNewTab(
    GetTestWebBasedURL("dummy.html"),
    async () => {
      await processPriorityChangedPromise;
      // We can't be sure that a busy loop lasting for a specific duration
      // will use the same amount of CPU time, as that would require a core
      // to be fully available for our busy loop, which is unlikely on single
      // core hardware.
      // To be able to have a predictable amount of CPU time used, we need to
      // check using ChromeUtils.requestProcInfo how much CPU time has actually
      // been spent.
      let pid = firstBrowser.frameLoader.remoteTab.osPid;
      let initalCpuTime = await getChildCpuTime(pid);
      let afterCpuTime;
      do {
        await SpecialPowers.spawn(
          firstBrowser,
          [kBusyWaitForMs],
          async kBusyWaitForMs => {
            let startTime = Date.now();
            while (Date.now() - startTime < 10) {
              // Burn CPU time...
            }
          }
        );
        afterCpuTime = await getChildCpuTime(pid);
      } while (afterCpuTime - initalCpuTime < kBusyWaitForMs * kNS_PER_MS);
      cpuTimeSpentOnBackgroundTab = Math.floor(
        (afterCpuTime - initalCpuTime) / kNS_PER_MS
      );
    }
  );

  let beforeProcInfo = await ChromeUtils.requestProcInfo();
  await Services.fog.testFlushAllChildren();

  let cpuTimeByType = {},
    gpuTimeByType = {};
  for (let label of kGleanProcessTypeLabels) {
    cpuTimeByType[label] =
      Glean.power.cpuTimePerProcessTypeMs[label].testGetValue();
    gpuTimeByType[label] =
      Glean.power.gpuTimePerProcessTypeMs[label].testGetValue();
  }
  let totalCpuTime = Glean.power.totalCpuTimeMs.testGetValue();
  let totalGpuTime = Glean.power.totalGpuTimeMs.testGetValue();

  let afterProcInfo = await ChromeUtils.requestProcInfo();

  info("CPU time from ProcInfo before calling testFlushAllChildren:");
  printProcInfo(beforeProcInfo);

  info("CPU time for each label:");
  let totalCpuTimeByType = 0;
  for (let label of kGleanProcessTypeLabels) {
    totalCpuTimeByType += cpuTimeByType[label] ?? 0;
    info(`  ${label} = ${cpuTimeByType[label]}`);
  }

  info("CPU time from ProcInfo after calling testFlushAllChildren:");
  printProcInfo(afterProcInfo);

  Assert.equal(
    totalCpuTimeByType,
    totalCpuTime,
    "The sum of CPU time used by all process types should match totalCpuTimeMs"
  );

  // In infra the parent process time will be reported as parent.inactive,
  // but when running the test locally the user might move the mouse a bit.
  let parentTime =
    (cpuTimeByType["parent.active"] || 0) +
    (cpuTimeByType["parent.inactive"] || 0);
  Assert.greaterOrEqual(
    parentTime,
    Math.floor(beforeProcInfo.cpuTime / kNS_PER_MS),
    "reported parent cpu time should be at least what the first requestProcInfo returned"
  );
  Assert.lessOrEqual(
    parentTime,
    Math.ceil(afterProcInfo.cpuTime / kNS_PER_MS),
    "reported parent cpu time should be at most what the second requestProcInfo returned"
  );

  kGleanProcessTypeLabels
    .filter(label => label.startsWith("parent.") && label.includes(".playing-"))
    .forEach(label => {
      Assert.strictEqual(
        cpuTimeByType[label],
        undefined,
        `no media was played so the CPU time for ${label} should be undefined`
      );
    });

  if (beforeProcInfo.children.some(p => p.type == "preallocated")) {
    Assert.greaterOrEqual(
      cpuTimeByType.prealloc,
      beforeProcInfo.children.reduce(
        (time, p) =>
          time +
          (p.type == "preallocated" ? Math.floor(p.cpuTime / kNS_PER_MS) : 0),
        0
      ),
      "reported cpu time for preallocated content processes should be at least the sum of what the first requestProcInfo returned."
    );
    // We can't compare with the values returned by the second requestProcInfo
    // call because one preallocated content processes has been turned into
    // a normal content process when we opened a tab.
  } else {
    info(
      "no preallocated content process existed when we started our test, but some might have existed before"
    );
  }

  if (beforeProcInfo.children.some(p => p.type == "privilegedabout")) {
    Assert.greaterOrEqual(
      cpuTimeByType.privilegedabout,
      1,
      "we used some CPU time in a foreground tab, but don't know how much as the process might have started as preallocated"
    );
  }

  for (let label of [
    "rdd",
    "socket",
    "extension",
    "gpu",
    "gmplugin",
    "utility",
  ]) {
    if (!kGleanProcessTypeLabels.includes(label)) {
      Assert.ok(
        false,
        `coding error in the test, ${label} isn't part of ${kGleanProcessTypeLabels.join(
          ", "
        )}`
      );
    }
    if (beforeProcInfo.children.some(p => p.type == label)) {
      Assert.greaterOrEqual(
        cpuTimeByType[label],
        Math.floor(
          beforeProcInfo.children.find(p => p.type == label).cpuTime /
            kNS_PER_MS
        ),
        "reported cpu time for " +
          label +
          " process should be at least what the first requestProcInfo returned."
      );
      Assert.lessOrEqual(
        cpuTimeByType[label],
        Math.ceil(
          afterProcInfo.children.find(p => p.type == label).cpuTime / kNS_PER_MS
        ),
        "reported cpu time for " +
          label +
          " process should be at most what the second requestProcInfo returned."
      );
    } else {
      info(
        "no " +
          label +
          " process existed when we started our test, but some might have existed before"
      );
    }
  }

  Assert.greaterOrEqual(
    cpuTimeByType["web.background"],
    cpuTimeSpentOnBackgroundTab,
    "web.background should be at least the time we spent."
  );

  Assert.greaterOrEqual(
    cpuTimeByType["web.foreground"],
    1,
    "we used some CPU time in a foreground tab, but don't know how much"
  );

  // We only have web.background-perceivable CPU time if a muted video was
  // played in a background tab.
  Assert.strictEqual(
    cpuTimeByType["web.background-perceivable"],
    undefined,
    "CPU time should only be recorded in the web.background-perceivable label"
  );

  // __other__ should be undefined, if it is not, we have a missing label in the metrics.yaml file.
  Assert.strictEqual(
    cpuTimeByType.__other__,
    undefined,
    "no CPU time should be recorded in the __other__ label"
  );

  info("GPU time for each label:");
  let totalGpuTimeByType = undefined;
  for (let label of kGleanProcessTypeLabels) {
    if (gpuTimeByType[label] !== undefined) {
      totalGpuTimeByType = (totalGpuTimeByType || 0) + gpuTimeByType[label];
    }
    info(`  ${label} = ${gpuTimeByType[label]}`);
  }

  Assert.equal(
    totalGpuTimeByType,
    totalGpuTime,
    "The sum of GPU time used by all process types should match totalGpuTimeMs"
  );

  // __other__ should be undefined, if it is not, we have a missing label in the metrics.yaml file.
  Assert.strictEqual(
    gpuTimeByType.__other__,
    undefined,
    "no GPU time should be recorded in the __other__ label"
  );

  // Now test per-thread CPU time.
  // We don't test parentActive as the user is not marked active on infra.
  let processTypes = [
    "parentInactive",
    "contentBackground",
    "contentForeground",
  ];
  if (beforeProcInfo.children.some(p => p.type == "gpu")) {
    processTypes.push("gpuProcess");
  }
  // The list of accepted labels is not accessible to the JS code, so test only the main thread.
  const kThreadName = "geckomain";
  if (AppConstants.NIGHTLY_BUILD) {
    for (let processType of processTypes) {
      Assert.greater(
        Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
        0,
        `some CPU time should have been recorded for the ${processType} main thread`
      );
      Assert.greater(
        Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
        0,
        `some thread wake ups should have been recorded for the ${processType} main thread`
      );
    }
  } else {
    // We are not recording per thread CPU use outside of the Nightly channel.
    for (let processType of processTypes) {
      Assert.equal(
        Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
        undefined,
        `no CPU time should have been recorded for the ${processType} main thread`
      );
      Assert.equal(
        Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
        undefined,
        `no thread wake ups should have been recorded for the ${processType} main thread`
      );
    }
  }
});

add_task(async function test_tracker_power() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["privacy.trackingprotection.enabled", false],
      ["privacy.trackingprotection.annotate_channels", true],
    ],
  });
  let initialValues = [];
  for (let trackerType of [
    "ad",
    "analytics",
    "cryptomining",
    "fingerprinting",
    "social",
    "unknown",
  ]) {
    initialValues[trackerType] =
      Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0;
  }

  await BrowserTestUtils.withNewTab(
    GetTestWebBasedURL("dummy.html"),
    async () => {
      // Load a tracker in a subframe, as we only record CPU time used by third party trackers.
      await SpecialPowers.spawn(
        gBrowser.selectedTab.linkedBrowser,
        [
          GetTestWebBasedURL("dummy.html").replace(
            "example.org",
            "trackertest.org"
          ),
        ],
        async frameUrl => {
          let iframe = content.document.createElement("iframe");
          iframe.setAttribute("src", frameUrl);
          await new content.Promise(resolve => {
            iframe.onload = resolve;
            content.document.body.appendChild(iframe);
          });
        }
      );
    }
  );

  await Services.fog.testFlushAllChildren();

  let unknownTrackerCPUTime =
    Glean.power.cpuTimePerTrackerTypeMs.unknown.testGetValue() || 0;
  Assert.greater(
    unknownTrackerCPUTime,
    initialValues.unknown,
    "The CPU time of unknown trackers should have increased"
  );

  for (let trackerType of [
    "ad",
    "analytics",
    "cryptomining",
    "fingerprinting",
    "social",
  ]) {
    Assert.equal(
      Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0,
      initialValues[trackerType],
      `no new CPU time should have been recorded for ${trackerType} trackers`
    );
  }
});