tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_gc_schedule.js (10781B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const TEST_PAGE =
      8  "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html";
      9 
     10 async function waitForGCBegin() {
     11  var waitTopic = "garbage-collector-begin";
     12  var observer = {};
     13 
     14  info("Waiting for " + waitTopic);
     15  // This fixes a ReferenceError for Date, it's weird.
     16  ok(Date.now(), "Date.now()");
     17  var when = await new Promise(resolve => {
     18    observer.observe = function () {
     19      resolve(Date.now());
     20    };
     21 
     22    Services.obs.addObserver(observer, waitTopic);
     23  });
     24 
     25  Services.obs.removeObserver(observer, waitTopic);
     26 
     27  // This delay attempts to make the time stamps unique.
     28  do {
     29    var now = Date.now();
     30  } while (when + 5 > now);
     31 
     32  return when;
     33 }
     34 
     35 async function waitForGCEnd() {
     36  var waitTopic = "garbage-collector-end";
     37  var observer = {};
     38 
     39  info("Waiting for " + waitTopic);
     40  // This fixes a ReferenceError for Date, it's weird.
     41  ok(Date.now(), "Date.now()");
     42  let when = await new Promise(resolve => {
     43    observer.observe = function () {
     44      resolve(Date.now());
     45    };
     46 
     47    Services.obs.addObserver(observer, waitTopic);
     48  });
     49 
     50  Services.obs.removeObserver(observer, waitTopic);
     51 
     52  do {
     53    var now = Date.now();
     54  } while (when + 5 > now);
     55 
     56  return when;
     57 }
     58 
     59 function getProcessID() {
     60  return Services.appinfo.processID;
     61 }
     62 
     63 async function resolveInOrder(promisesAndStates) {
     64  var order = [];
     65  var promises = [];
     66 
     67  for (let p of promisesAndStates) {
     68    promises.push(
     69      p.promise.then(when => {
     70        info(`Tab: ${p.tab} did ${p.state}`);
     71        order.push({ tab: p.tab, state: p.state, when });
     72      })
     73    );
     74  }
     75 
     76  await Promise.all(promises);
     77 
     78  return order;
     79 }
     80 
     81 // Check that the list of events returned by resolveInOrder are in a
     82 // sensible order.
     83 function checkOneAtATime(events) {
     84  var cur = null;
     85  var lastWhen = null;
     86 
     87  info("Checking order of events");
     88  for (const e of events) {
     89    ok(e.state === "begin" || e.state === "end", "event.state is good");
     90    Assert.notStrictEqual(e.tab, undefined, "event.tab exists");
     91 
     92    if (lastWhen) {
     93      // We need these in sorted order so that the other checks here make
     94      // sense.
     95      Assert.lessOrEqual(
     96        lastWhen,
     97        e.when,
     98        `Unsorted events, last: ${lastWhen}, this: ${e.when}`
     99      );
    100    }
    101    lastWhen = e.when;
    102 
    103    if (e.state === "begin") {
    104      is(cur, null, `GC can begin on tab ${e.tab}`);
    105      cur = e.tab;
    106    } else {
    107      is(e.tab, cur, `GC can end on tab ${e.tab}`);
    108      cur = null;
    109    }
    110  }
    111 
    112  is(cur, null, "No GC left running");
    113 }
    114 
    115 function checkAllCompleted(events, expectTabsCompleted) {
    116  var tabsCompleted = events.filter(e => e.state === "end").map(e => e.tab);
    117 
    118  for (var t of expectTabsCompleted) {
    119    ok(tabsCompleted.includes(t), `Tab ${t} did a GC`);
    120  }
    121 }
    122 
    123 async function setupTabsAndOneForForeground(num_tabs) {
    124  ++num_tabs;
    125  var pids = [];
    126 
    127  const parent_pid = getProcessID();
    128  info("Parent process PID is " + parent_pid);
    129 
    130  const tabs = await Promise.all(
    131    Array(num_tabs)
    132      .fill()
    133      .map(_ => {
    134        return BrowserTestUtils.openNewForegroundTab({
    135          gBrowser,
    136          opening: TEST_PAGE,
    137          forceNewProcess: true,
    138        });
    139      })
    140  );
    141 
    142  for (const [i, tab] of Object.entries(tabs)) {
    143    const tab_pid = await SpecialPowers.spawn(
    144      tab.linkedBrowser,
    145      [],
    146      getProcessID
    147    );
    148 
    149    info(`Tab ${i} pid is ${tab_pid}`);
    150    isnot(parent_pid, tab_pid, `Tab ${i} is in content process`);
    151    ok(!pids.includes(tab_pid), `Tab ${i} is in a distinct process`);
    152 
    153    pids.push(tab_pid);
    154  }
    155 
    156  // Since calling openNewForegroundTab several times in a row doesn't update
    157  // process priorities correctly, we need to explicitly switch tabs.
    158  for (let tab of tabs) {
    159    await BrowserTestUtils.switchTab(gBrowser, tab);
    160  }
    161 
    162  return tabs;
    163 }
    164 
    165 function doContentRunNextCollectionTimer() {
    166  content.windowUtils.pokeGC("PAGE_HIDE");
    167  content.windowUtils.runNextCollectorTimer("PAGE_HIDE");
    168 }
    169 
    170 function startNextCollection(
    171  tab,
    172  tab_num,
    173  waits,
    174  fn = doContentRunNextCollectionTimer
    175 ) {
    176  var browser = tab.linkedBrowser;
    177 
    178  // Finish any currently running GC.
    179  SpecialPowers.spawn(browser, [], () => {
    180    SpecialPowers.Cu.getJSTestingFunctions().finishgc();
    181  });
    182 
    183  if (tab.selected) {
    184    // One isn't expected to use the return value with foreground tab!
    185    return {};
    186  }
    187 
    188  var waitBegin = SpecialPowers.spawn(browser, [], waitForGCBegin);
    189  var waitEnd = SpecialPowers.spawn(browser, [], waitForGCEnd);
    190  waits.push({ promise: waitBegin, tab: tab_num, state: "begin" });
    191  waits.push({ promise: waitEnd, tab: tab_num, state: "end" });
    192 
    193  SpecialPowers.spawn(browser, [], fn);
    194 
    195  // Return these so that the abort GC test can wait for the begin.
    196  return { waitBegin, waitEnd };
    197 }
    198 
    199 add_task(async function gcOneAtATime() {
    200  SpecialPowers.pushPrefEnv({
    201    set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
    202  });
    203 
    204  const num_tabs = 12;
    205  var tabs = await setupTabsAndOneForForeground(num_tabs);
    206 
    207  info("Tabs ready, Asking for GCs");
    208  var waits = [];
    209  for (var i = 0; i < num_tabs; i++) {
    210    startNextCollection(tabs[i], i, waits);
    211  }
    212 
    213  let order = await resolveInOrder(waits);
    214  // We need these in the order they actually occurred, so far that's how
    215  // they're returned, but we'll sort them to be sure.
    216  order.sort((e1, e2) => e1.when - e2.when);
    217  checkOneAtATime(order);
    218  checkAllCompleted(
    219    order,
    220    Array.from({ length: num_tabs }, (_, n) => n)
    221  );
    222 
    223  for (var tab of tabs) {
    224    BrowserTestUtils.removeTab(tab);
    225  }
    226 
    227  SpecialPowers.popPrefEnv();
    228 });
    229 
    230 add_task(async function gcAbort() {
    231  SpecialPowers.pushPrefEnv({
    232    set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
    233  });
    234 
    235  const num_tabs = 2;
    236  var tabs = await setupTabsAndOneForForeground(num_tabs);
    237 
    238  info("Tabs ready, Asking for GCs");
    239  var waits = [];
    240 
    241  var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
    242    SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
    243  });
    244  await tab0Waits.waitBegin;
    245 
    246  // Tab 0 has started a GC. Now we schedule a GC in tab one.  It must not
    247  // begin yet (but we don't check that, gcOneAtATime is assumed to check
    248  // this.
    249  startNextCollection(tabs[1], 1, waits);
    250 
    251  // Request that tab 0 abort, this test checks that tab 1 can now begin.
    252  SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
    253    SpecialPowers.Cu.getJSTestingFunctions().abortgc();
    254  });
    255 
    256  let order = await resolveInOrder(waits);
    257  // We need these in the order they actually occurred, so far that's how
    258  // they're returned, but we'll sort them to be sure.
    259  order.sort((e1, e2) => e1.when - e2.when);
    260  checkOneAtATime(order);
    261  checkAllCompleted(
    262    order,
    263    Array.from({ length: num_tabs }, (_, n) => n)
    264  );
    265 
    266  for (var tab of tabs) {
    267    BrowserTestUtils.removeTab(tab);
    268  }
    269 
    270  SpecialPowers.popPrefEnv();
    271 });
    272 
    273 add_task(async function gcJSInitiatedDuring() {
    274  SpecialPowers.pushPrefEnv({
    275    set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
    276  });
    277 
    278  const num_tabs = 3;
    279  var tabs = await setupTabsAndOneForForeground(num_tabs);
    280 
    281  let completed;
    282  let retry = 0;
    283  const maxRetries = 3;
    284  do {
    285    completed = true;
    286    retry++;
    287 
    288    info("Tabs ready, Asking for GCs");
    289    var waits = [];
    290 
    291    // Start a GC on tab 0 to consume the scheduler's "token".
    292    var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
    293      SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
    294    });
    295    await tab0Waits.waitBegin;
    296    info("GC on tab 0 has begun");
    297 
    298    // Request a GC in tab 1, this will be blocked by the ongoing GC in tab 0.
    299    var tab1Waits = startNextCollection(tabs[1], 1, waits);
    300 
    301    // Force a GC to start in tab 1.  This won't wait for tab 0.
    302    SpecialPowers.spawn(tabs[1].linkedBrowser, [], () => {
    303      SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
    304    });
    305 
    306    await tab1Waits.waitBegin;
    307    info("GC on tab 1 has begun");
    308 
    309    // The GC in tab 0 should still be running.
    310    var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
    311      return SpecialPowers.Cu.getJSTestingFunctions().gcstate();
    312    });
    313 
    314    info("State of Tab 0 GC is " + state);
    315    if (state == "NotActive") {
    316      // The GC finished earlier than expected. The test must be retried.
    317      info("GC finished in tab 0");
    318      completed = false;
    319    }
    320 
    321    // Let the GCs complete, verify that a GC in a 3rd tab can acquire a token.
    322    startNextCollection(tabs[2], 2, waits);
    323 
    324    let order = await resolveInOrder(waits);
    325    info("All GCs finished");
    326    checkAllCompleted(
    327      order,
    328      Array.from({ length: num_tabs }, (_, n) => n)
    329    );
    330  } while (!completed && retry <= maxRetries);
    331 
    332  ok(completed, "GC in tab 0 finished sooner than expected");
    333 
    334  for (var tab of tabs) {
    335    BrowserTestUtils.removeTab(tab);
    336  }
    337 
    338  SpecialPowers.popPrefEnv();
    339 });
    340 
    341 add_task(async function gcJSInitiatedBefore() {
    342  SpecialPowers.pushPrefEnv({
    343    set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
    344  });
    345 
    346  const num_tabs = 8;
    347  var tabs = await setupTabsAndOneForForeground(num_tabs);
    348 
    349  let completed;
    350  let retry = 0;
    351  const maxRetries = 3;
    352  do {
    353    completed = true;
    354    retry++;
    355 
    356    info("Tabs ready");
    357    var waits = [];
    358 
    359    // Start a GC on tab 0 to consume the scheduler's first "token".
    360    info("Force a JS-initiated GC in tab 0");
    361    var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
    362      SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
    363    });
    364    await tab0Waits.waitBegin;
    365 
    366    info("Request GCs in remaining tabs");
    367    for (var i = 1; i < num_tabs; i++) {
    368      startNextCollection(tabs[i], i, waits);
    369    }
    370 
    371    // The GC in tab 0 should still be running.
    372    var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
    373      return SpecialPowers.Cu.getJSTestingFunctions().gcstate();
    374    });
    375 
    376    info("State is " + state);
    377    if (state == "NotActive") {
    378      // The GC finished earlier than expected. The test must be retried.
    379      info("GC finished in tab 0");
    380      completed = false;
    381    }
    382 
    383    let order = await resolveInOrder(waits);
    384    // We need these in the order they actually occurred, so far that's how
    385    // they're returned, but we'll sort them to be sure.
    386    order.sort((e1, e2) => e1.when - e2.when);
    387    checkOneAtATime(order);
    388    checkAllCompleted(
    389      order,
    390      Array.from({ length: num_tabs }, (_, n) => n)
    391    );
    392  } while (!completed && retry <= maxRetries);
    393 
    394  ok(completed, "GC in tab 0 finished sooner than expected");
    395 
    396  for (var tab of tabs) {
    397    BrowserTestUtils.removeTab(tab);
    398  }
    399 
    400  SpecialPowers.popPrefEnv();
    401 });