summaryrefslogtreecommitdiffstats
path: root/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
blob: f04fefb339d75a25ca49390ebaf16e020707e930 (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
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * Test that we see jank that takes place in a webpage,
 * and that jank from several iframes are actually charged
 * to the top window.
 */
Cu.import("resource://gre/modules/PerformanceStats.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://testing-common/ContentTask.jsm", this);


const URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random();
const PARENT_TITLE = `Main frame for test browser_compartments.js ${Math.random()}`;
const FRAME_TITLE = `Subframe for test browser_compartments.js ${Math.random()}`;

const PARENT_PID = Services.appinfo.processID;

// This function is injected as source as a frameScript
function frameScript() {
  try {
    "use strict";

    const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
    Cu.import("resource://gre/modules/PerformanceStats.jsm");
    Cu.import("resource://gre/modules/Services.jsm");

    // Make sure that the stopwatch is now active.
    let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks", "compartments"]);

    addMessageListener("compartments-test:getStatistics", () => {
      try {
        monitor.promiseSnapshot().then(snapshot => {
          sendAsyncMessage("compartments-test:getStatistics", {snapshot, pid: Services.appinfo.processID});
        });
      } catch (ex) {
        Cu.reportError("Error in content (getStatistics): " + ex);
        Cu.reportError(ex.stack);
      }
    });

    addMessageListener("compartments-test:setTitles", titles => {
      try {
        content.document.title = titles.data.parent;
        for (let i = 0; i < content.frames.length; ++i) {
          content.frames[i].postMessage({title: titles.data.frames}, "*");
        }
        console.log("content", "Done setting titles", content.document.title);
        sendAsyncMessage("compartments-test:setTitles");
      } catch (ex) {
        Cu.reportError("Error in content (setTitles): " + ex);
        Cu.reportError(ex.stack);
      }
    });
  } catch (ex) {
    Cu.reportError("Error in content (setup): " + ex);
    Cu.reportError(ex.stack);
  }
}

// A variant of `Assert` that doesn't spam the logs
// in case of success.
var SilentAssert = {
  equal: function(a, b, msg) {
    if (a == b) {
      return;
    }
    Assert.equal(a, b, msg);
  },
  notEqual: function(a, b, msg) {
    if (a != b) {
      return;
    }
    Assert.notEqual(a, b, msg);
  },
  ok: function(a, msg) {
    if (a) {
      return;
    }
    Assert.ok(a, msg);
  },
  leq: function(a, b, msg) {
    this.ok(a <= b, `${msg}: ${a} <= ${b}`);
  }
};

var isShuttingDown = false;
function monotinicity_tester(source, testName) {
  // In the background, check invariants:
  // - numeric data can only ever increase;
  // - the name, addonId, isSystem of a component never changes;
  // - the name, addonId, isSystem of the process data;
  // - there is at most one component with a combination of `name` and `addonId`;
  // - types, etc.
  let previous = {
    processData: null,
    componentsMap: new Map(),
  };

  let sanityCheck = function(prev, next) {
    if (prev == null) {
      return;
    }
    for (let k of ["groupId", "addonId", "isSystem"]) {
      SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed (${prev.name}).`);
    }
    for (let [probe, k] of [
      ["jank", "totalUserTime"],
      ["jank", "totalSystemTime"],
      ["cpow", "totalCPOWTime"],
      ["ticks", "ticks"]
    ]) {
      SilentAssert.equal(typeof next[probe][k], "number", `Sanity check (${testName}): ${k} is a number.`);
      SilentAssert.leq(prev[probe][k], next[probe][k], `Sanity check (${testName}): ${k} is monotonic.`);
      SilentAssert.leq(0, next[probe][k], `Sanity check (${testName}): ${k} is >= 0.`)
    }
    SilentAssert.equal(prev.jank.durations.length, next.jank.durations.length);
    for (let i = 0; i < next.jank.durations.length; ++i) {
      SilentAssert.ok(typeof next.jank.durations[i] == "number" && next.jank.durations[i] >= 0,
        `Sanity check (${testName}): durations[${i}] is a non-negative number.`);
      SilentAssert.leq(prev.jank.durations[i], next.jank.durations[i],
        `Sanity check (${testName}): durations[${i}] is monotonic.`);
    }
    for (let i = 0; i < next.jank.durations.length - 1; ++i) {
      SilentAssert.leq(next.jank.durations[i + 1], next.jank.durations[i],
        `Sanity check (${testName}): durations[${i}] >= durations[${i + 1}].`)
    }
  };
  let iteration = 0;
  let frameCheck = Task.async(function*() {
    if (isShuttingDown) {
      window.clearInterval(interval);
      return;
    }
    let name = `${testName}: ${iteration++}`;
    let result = yield source();
    if (!result) {
      // This can happen at the end of the test when we attempt
      // to communicate too late with the content process.
      window.clearInterval(interval);
      return;
    }
    let {pid, snapshot} = result;

    // Sanity check on the process data.
    sanityCheck(previous.processData, snapshot.processData);
    SilentAssert.equal(snapshot.processData.isSystem, true);
    SilentAssert.equal(snapshot.processData.name, "<process>");
    SilentAssert.equal(snapshot.processData.addonId, "");
    SilentAssert.equal(snapshot.processData.processId, pid);
    previous.procesData = snapshot.processData;

    // Sanity check on components data.
    let map = new Map();
    for (let item of snapshot.componentsData) {
      for (let [probe, k] of [
        ["jank", "totalUserTime"],
        ["jank", "totalSystemTime"],
        ["cpow", "totalCPOWTime"]
      ]) {
        // Note that we cannot expect components data to be always smaller
        // than process data, as `getrusage` & co are not monotonic.
        SilentAssert.leq(item[probe][k], 3 * snapshot.processData[probe][k],
          `Sanity check (${name}): ${k} of component is not impossibly larger than that of process`);
      }

      let isCorrectPid = (item.processId == pid && !item.isChildProcess)
        || (item.processId != pid && item.isChildProcess);
      SilentAssert.ok(isCorrectPid, `Pid check (${name}): the item comes from the right process`);

      let key = item.groupId;
      if (map.has(key)) {
        let old = map.get(key);
        Assert.ok(false, `Component ${key} has already been seen. Latest: ${item.addonId||item.name}, previous: ${old.addonId||old.name}`);
      }
      map.set(key, item);
    }
    for (let item of snapshot.componentsData) {
      if (!item.parentId) {
        continue;
      }
      let parent = map.get(item.parentId);
      SilentAssert.ok(parent, `The parent exists ${item.parentId}`);

      for (let [probe, k] of [
        ["jank", "totalUserTime"],
        ["jank", "totalSystemTime"],
        ["cpow", "totalCPOWTime"]
      ]) {
        // Note that we cannot expect components data to be always smaller
        // than parent data, as `getrusage` & co are not monotonic.
        SilentAssert.leq(item[probe][k], 2 * parent[probe][k],
          `Sanity check (${testName}): ${k} of component is not impossibly larger than that of parent`);
      }
    }
    for (let [key, item] of map) {
      sanityCheck(previous.componentsMap.get(key), item);
      previous.componentsMap.set(key, item);
    }
  });
  let interval = window.setInterval(frameCheck, 300);
  registerCleanupFunction(() => {
    window.clearInterval(interval);
  });
}

add_task(function* test() {
  let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks"]);

  info("Extracting initial state");
  let stats0 = yield monitor.promiseSnapshot();
  Assert.notEqual(stats0.componentsData.length, 0, "There is more than one component");
  Assert.ok(!stats0.componentsData.find(stat => stat.name.indexOf(URL) != -1),
    "The url doesn't appear yet");

  let newTab = gBrowser.addTab();
  let browser = newTab.linkedBrowser;
  // Setup monitoring in the tab
  info("Setting up monitoring in the tab");
  yield ContentTask.spawn(newTab.linkedBrowser, null, frameScript);

  info("Opening URL");
  newTab.linkedBrowser.loadURI(URL);

  if (Services.sysinfo.getPropertyAsAString("name") == "Windows_NT") {
    info("Deactivating sanity checks under Windows (bug 1151240)");
  } else {
    info("Setting up sanity checks");
    monotinicity_tester(() => monitor.promiseSnapshot().then(snapshot => ({snapshot, pid: PARENT_PID})), "parent process");
    monotinicity_tester(() => promiseContentResponseOrNull(browser, "compartments-test:getStatistics", null), "content process" );
  }

  let skipTotalUserTime = hasLowPrecision();


  while (true) {
    yield new Promise(resolve => setTimeout(resolve, 100));

    // We may have race conditions with DOM loading.
    // Don't waste too much brainpower here, let's just ask
    // repeatedly for the title to be changed, until this works.
    info("Setting titles");
    yield promiseContentResponse(browser, "compartments-test:setTitles", {
      parent: PARENT_TITLE,
      frames: FRAME_TITLE
    });
    info("Titles set");

    let {snapshot: stats} = (yield promiseContentResponse(browser, "compartments-test:getStatistics", null));

    // Attach titles to components.
    let titles = [];
    let map = new Map();
    let windows = Services.wm.getEnumerator("navigator:browser");
    while (windows.hasMoreElements()) {
      let window = windows.getNext();
      let tabbrowser = window.gBrowser;
      for (let browser of tabbrowser.browsers) {
        let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
        if (id != null) {
          map.set(id, browser);
        }
      }
    }
    for (let stat of stats.componentsData) {
      if (!stat.windowId) {
        continue;
      }
      let browser = map.get(stat.windowId);
      if (!browser) {
        continue;
      }
      let title = browser.contentTitle;
      if (title) {
        stat.title = title;
        titles.push(title);
      }
    }

    // While the webpage consists in three compartments, we should see only
    // one `PerformanceData` in `componentsData`. Its `name` is undefined
    // (could be either the main frame or one of its subframes), but its
    // `title` should be the title of the main frame.
    info(`Searching for frame title '${FRAME_TITLE}' in ${JSON.stringify(titles)} (I hope not to find it)`);
    Assert.ok(!titles.includes(FRAME_TITLE), "Searching by title, the frames don't show up in the list of components");

    info(`Searching for window title '${PARENT_TITLE}' in ${JSON.stringify(titles)} (I hope to find it)`);
    let parent = stats.componentsData.find(x => x.title == PARENT_TITLE);
    if (!parent) {
      info("Searching by title, we didn't find the main frame");
      continue;
    }
    info("Found the main frame");

    if (skipTotalUserTime) {
      info("Not looking for total user time on this platform, we're done");
      break;
    } else if (parent.jank.totalUserTime > 1000) {
      info("Enough CPU time detected, we're done");
      break;
    } else {
      info(`Not enough CPU time detected: ${parent.jank.totalUserTime}`);
    }
  }
  isShuttingDown = true;

  // Cleanup
  gBrowser.removeTab(newTab, {skipPermitUnload: true});
});