tor-browser

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

ui.js (18635B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 var stroke = {
      6  gcslice: "rgb(255,100,0)",
      7  minor: "rgb(0,255,100)",
      8  initialMajor: "rgb(180,60,255)",
      9 };
     10 
     11 var numSamples = 500;
     12 
     13 var tests = new Map();
     14 
     15 var gHistogram = new Map(); // {ms: count}
     16 var gHistory = new FrameHistory(numSamples);
     17 var gPerf = new PerfTracker();
     18 
     19 var latencyGraph;
     20 var memoryGraph;
     21 var ctx;
     22 var memoryCtx;
     23 
     24 var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)'
     25 var testState = "idle"; // One of 'idle' or 'running'.
     26 var enabled = { trackingSizes: false };
     27 
     28 var gMemory = performance.mozMemory?.gc || performance.mozMemory || {};
     29 
     30 var Firefox = class extends Host {
     31  start_turn() {
     32    // Handled by Gecko.
     33  }
     34 
     35  end_turn() {
     36    // Handled by Gecko.
     37  }
     38 
     39  suspend(duration) {
     40    // Not used; requestAnimationFrame takes its place.
     41    throw new Error("unimplemented");
     42  }
     43 
     44  get minorGCCount() {
     45    return gMemory.minorGCCount;
     46  }
     47  get majorGCCount() {
     48    return gMemory.majorGCCount;
     49  }
     50  get GCSliceCount() {
     51    return gMemory.sliceCount;
     52  }
     53  get gcBytes() {
     54    return gMemory.zone.gcBytes;
     55  }
     56  get mallocBytes() {
     57    return gMemory.zone.mallocBytes;
     58  }
     59  get gcAllocTrigger() {
     60    return gMemory.zone.gcAllocTrigger;
     61  }
     62  get mallocTrigger() {
     63    return gMemory.zone.mallocTriggerBytes;
     64  }
     65 
     66  features = {
     67    haveMemorySizes: 'gcBytes' in gMemory,
     68    haveGCCounts: 'majorGCCount' in gMemory,
     69  };
     70 };
     71 
     72 var gHost = new Firefox();
     73 
     74 function parse_units(v) {
     75  if (!v.length) {
     76    return NaN;
     77  }
     78  var lastChar = v[v.length - 1].toLowerCase();
     79  if (!isNaN(parseFloat(lastChar))) {
     80    return parseFloat(v);
     81  }
     82  var units = parseFloat(v.substr(0, v.length - 1));
     83  if (lastChar == "k") {
     84    return units * 1e3;
     85  }
     86  if (lastChar == "m") {
     87    return units * 1e6;
     88  }
     89  if (lastChar == "g") {
     90    return units * 1e9;
     91  }
     92  return NaN;
     93 }
     94 
     95 var Graph = class {
     96  constructor(canvas) {
     97    this.ctx = canvas.getContext('2d');
     98 
     99    // Adjust scale for high-DPI displays.
    100    this.scale = window.devicePixelRatio || 1;
    101    let rect = canvas.getBoundingClientRect();
    102    canvas.width = Math.floor(rect.width * this.scale);
    103    canvas.height = Math.floor(rect.height * this.scale);
    104    canvas.style.width = rect.width;
    105    canvas.style.height = rect.height;
    106 
    107    // Record canvas size to draw into.
    108    this.width = canvas.width;
    109    this.height = canvas.height;
    110 
    111    this.layout = {
    112      xAxisLabel_Y: this.height - 20 * this.scale,
    113    };
    114  }
    115 
    116  xpos(index) {
    117    return (index / numSamples) * (this.width - 100 * this.scale);
    118  }
    119 
    120  clear() {
    121    this.ctx.clearRect(0, 0, this.width, this.height);
    122  }
    123 
    124  drawScale(delay) {
    125    this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)");
    126  }
    127 
    128  draw60fps() {
    129    this.drawHBar(1000 / 60, "60fps", "#00cf61", 25);
    130  }
    131 
    132  draw30fps() {
    133    this.drawHBar(1000 / 30, "30fps", "#cf0061", 25);
    134  }
    135 
    136  drawAxisLabels(x_label, y_label) {
    137    const ctx = this.ctx;
    138 
    139    ctx.font = `${10 * this.scale}px sans-serif`;
    140 
    141    ctx.fillText(x_label, this.width / 2, this.layout.xAxisLabel_Y);
    142 
    143    ctx.save();
    144    ctx.rotate(Math.PI / 2);
    145    var start = this.height / 2 - ctx.measureText(y_label).width / 2;
    146    ctx.fillText(y_label, start, -this.width + 20 * this.scale);
    147    ctx.restore();
    148  }
    149 
    150  drawFrame() {
    151    const ctx = this.ctx;
    152    const width = this.width;
    153    const height = this.height;
    154 
    155    // Draw frame to show size
    156    ctx.strokeStyle = "rgb(0,0,0)";
    157    ctx.fillStyle = "rgb(0,0,0)";
    158    ctx.beginPath();
    159    ctx.moveTo(0, 0);
    160    ctx.lineTo(width, 0);
    161    ctx.lineTo(width, height);
    162    ctx.lineTo(0, height);
    163    ctx.closePath();
    164    ctx.stroke();
    165  }
    166 };
    167 
    168 var LatencyGraph = class extends Graph {
    169  constructor(ctx) {
    170    super(ctx);
    171  }
    172 
    173  ypos(delay) {
    174    return this.height + this.scale * (100 - Math.log(delay) * 64);
    175  }
    176 
    177  drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) {
    178    const ctx = this.ctx;
    179 
    180    let y = this.ypos(delay);
    181 
    182    ctx.fillStyle = color;
    183    ctx.strokeStyle = color;
    184    ctx.fillText(
    185      label,
    186      this.xpos(numSamples) + 4 + label_offset,
    187      this.ypos(delay) + 3
    188    );
    189 
    190    ctx.beginPath();
    191    ctx.moveTo(this.xpos(0), this.ypos(delay));
    192    ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay));
    193    ctx.stroke();
    194    ctx.strokeStyle = "rgb(0,0,0)";
    195    ctx.fillStyle = "rgb(0,0,0)";
    196  }
    197 
    198  draw() {
    199    const ctx = this.ctx;
    200 
    201    this.clear();
    202    this.drawFrame();
    203 
    204    for (var delay of [10, 20, 30, 50, 100, 200, 400, 800]) {
    205      this.drawScale(delay);
    206    }
    207    this.draw60fps();
    208    this.draw30fps();
    209 
    210    var worst = 0,
    211      worstpos = 0;
    212    ctx.beginPath();
    213    for (let i = 0; i < numSamples; i++) {
    214      ctx.lineTo(this.xpos(i), this.ypos(gHistory.delays[i]));
    215      if (gHistory.delays[i] >= worst) {
    216        worst = gHistory.delays[i];
    217        worstpos = i;
    218      }
    219    }
    220    ctx.stroke();
    221 
    222    // Draw vertical lines marking minor and major GCs
    223    if (gHost.features.haveGCCounts) {
    224      ctx.strokeStyle = stroke.gcslice;
    225      let idx = sampleIndex % numSamples;
    226      const count = {
    227        major: gHistory.majorGCs[idx],
    228        minor: 0,
    229        slice: gHistory.slices[idx],
    230      };
    231      for (let i = 0; i < numSamples; i++) {
    232        idx = (sampleIndex + i) % numSamples;
    233        const isMajorStart = count.major < gHistory.majorGCs[idx];
    234        if (count.slice < gHistory.slices[idx]) {
    235          if (isMajorStart) {
    236            ctx.strokeStyle = stroke.initialMajor;
    237          }
    238          ctx.beginPath();
    239          ctx.moveTo(this.xpos(idx), 0);
    240          ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y);
    241          ctx.stroke();
    242          if (isMajorStart) {
    243            ctx.strokeStyle = stroke.gcslice;
    244          }
    245        }
    246        count.major = gHistory.majorGCs[idx];
    247        count.slice = gHistory.slices[idx];
    248      }
    249 
    250      ctx.strokeStyle = stroke.minor;
    251      idx = sampleIndex % numSamples;
    252      count.minor = gHistory.minorGCs[idx];
    253      for (let i = 0; i < numSamples; i++) {
    254        idx = (sampleIndex + i) % numSamples;
    255        if (count.minor < gHistory.minorGCs[idx]) {
    256          ctx.beginPath();
    257          ctx.moveTo(this.xpos(idx), 0);
    258          ctx.lineTo(this.xpos(idx), 20);
    259          ctx.stroke();
    260        }
    261        count.minor = gHistory.minorGCs[idx];
    262      }
    263    }
    264 
    265    ctx.fillStyle = "rgb(255,0,0)";
    266    if (worst) {
    267      ctx.fillText(
    268        `${worst.toFixed(2)}ms`,
    269        this.xpos(worstpos) - 10,
    270        this.ypos(worst) - 14
    271      );
    272    }
    273 
    274    // Mark and label the slowest frame
    275    ctx.beginPath();
    276    var where = sampleIndex % numSamples;
    277    ctx.arc(
    278      this.xpos(where),
    279      this.ypos(gHistory.delays[where]),
    280      5,
    281      0,
    282      Math.PI * 2,
    283      true
    284    );
    285    ctx.fill();
    286    ctx.fillStyle = "rgb(0,0,0)";
    287 
    288    this.drawAxisLabels("Time", "Pause between frames (log scale)");
    289  }
    290 };
    291 
    292 var MemoryGraph = class extends Graph {
    293  constructor(ctx) {
    294    super(ctx);
    295    this.range = 1;
    296  }
    297 
    298  ypos(size) {
    299    const percent = size / this.range;
    300    return (1 - percent) * this.height * 0.9 + this.scale * 20;
    301  }
    302 
    303  drawHBarForBytes(size, name, color) {
    304    this.drawHBar(size, `${format_bytes(size)} ${name}`, color)
    305  }
    306 
    307  drawHBar(size, label, color) {
    308    const ctx = this.ctx;
    309 
    310    const y = this.ypos(size);
    311 
    312    ctx.fillStyle = color;
    313    ctx.strokeStyle = color;
    314    ctx.fillText(label, this.xpos(numSamples) + 4, y + 3);
    315 
    316    ctx.beginPath();
    317    ctx.moveTo(this.xpos(0), y);
    318    ctx.lineTo(this.xpos(numSamples), y);
    319    ctx.stroke();
    320    ctx.strokeStyle = "rgb(0,0,0)";
    321    ctx.fillStyle = "rgb(0,0,0)";
    322  }
    323 
    324  draw() {
    325    const ctx = this.ctx;
    326 
    327    this.clear();
    328    this.drawFrame();
    329 
    330    let gcMaxPos = 0;
    331    let mallocMaxPos = 0;
    332    let gcMax = 0;
    333    let mallocMax = 0;
    334    for (let i = 0; i < numSamples; i++) {
    335      if (gHistory.gcBytes[i] >= gcMax) {
    336        gcMax = gHistory.gcBytes[i];
    337        gcMaxPos = i;
    338      }
    339      if (gHistory.mallocBytes[i] >= mallocMax) {
    340        mallocMax = gHistory.mallocBytes[i];
    341        mallocMaxPos = i;
    342      }
    343    }
    344 
    345    this.range = Math.max(gcMax, mallocMax, gHost.gcAllocTrigger, gHost.mallocTrigger);
    346 
    347    this.drawHBarForBytes(gcMax, "GC max", "#00cf61");
    348    this.drawHBarForBytes(mallocMax, "Malloc max", "#cc1111");
    349    this.drawHBarForBytes(gHost.gcAllocTrigger, "GC trigger", "#cc11cc");
    350    this.drawHBarForBytes(gHost.mallocTrigger, "Malloc trigger", "#cc11cc");
    351 
    352    ctx.fillStyle = "rgb(255,0,0)";
    353 
    354    if (gcMax !== 0) {
    355      ctx.fillText(
    356        format_bytes(gcMax),
    357        this.xpos(gcMaxPos) - 10,
    358        this.ypos(gcMax) - 14
    359      );
    360    }
    361    if (mallocMax !== 0) {
    362      ctx.fillText(
    363        format_bytes(mallocMax),
    364        this.xpos(mallocMaxPos) - 10,
    365        this.ypos(mallocMax) - 14
    366      );
    367    }
    368 
    369    const where = sampleIndex % numSamples;
    370 
    371    ctx.beginPath();
    372    ctx.arc(
    373      this.xpos(where),
    374      this.ypos(gHistory.gcBytes[where]),
    375      5,
    376      0,
    377      Math.PI * 2,
    378      true
    379    );
    380    ctx.fill();
    381    ctx.beginPath();
    382    ctx.arc(
    383      this.xpos(where),
    384      this.ypos(gHistory.mallocBytes[where]),
    385      5,
    386      0,
    387      Math.PI * 2,
    388      true
    389    );
    390    ctx.fill();
    391 
    392    ctx.beginPath();
    393    for (let i = 0; i < numSamples; i++) {
    394      let x = this.xpos(i);
    395      let y = this.ypos(gHistory.gcBytes[i]);
    396      if (i == (sampleIndex + 1) % numSamples) {
    397        ctx.moveTo(x, y);
    398      } else {
    399        ctx.lineTo(x, y);
    400      }
    401      if (i == where) {
    402        ctx.stroke();
    403      }
    404    }
    405    ctx.stroke();
    406 
    407    ctx.beginPath();
    408    for (let i = 0; i < numSamples; i++) {
    409      let x = this.xpos(i);
    410      let y = this.ypos(gHistory.mallocBytes[i]);
    411      if (i == (sampleIndex + 1) % numSamples) {
    412        ctx.moveTo(x, y);
    413      } else {
    414        ctx.lineTo(x, y);
    415      }
    416      if (i == where) {
    417        ctx.stroke();
    418      }
    419    }
    420    ctx.stroke();
    421 
    422    ctx.fillStyle = "rgb(0,0,0)";
    423 
    424    this.drawAxisLabels("Time", "Heap Memory Usage");
    425  }
    426 };
    427 
    428 function onUpdateDisplayChanged() {
    429  const do_graph = document.getElementById("do-graph");
    430  if (do_graph.checked) {
    431    window.requestAnimationFrame(handler);
    432    gHistory.resume();
    433  } else {
    434    gHistory.pause();
    435  }
    436  update_load_state_indicator();
    437 }
    438 
    439 function onDoLoadChange() {
    440  const do_load = document.getElementById("do-load");
    441  gLoadMgr.paused = !do_load.checked;
    442  console.log(`load paused: ${gLoadMgr.paused}`);
    443  update_load_state_indicator();
    444 }
    445 
    446 var previous = 0;
    447 function handler(timestamp) {
    448  if (gHistory.is_stopped()) {
    449    return;
    450  }
    451 
    452  const completed = gLoadMgr.tick(timestamp);
    453  if (completed) {
    454    end_test(timestamp, gLoadMgr.lastActive);
    455    if (!gLoadMgr.stopped()) {
    456      start_test();
    457    }
    458    update_load_display();
    459  }
    460 
    461  if (testState == "running") {
    462    document.getElementById("test-progress").textContent =
    463      (gLoadMgr.currentLoadRemaining(timestamp) / 1000).toFixed(1) + " sec";
    464  }
    465 
    466  const delay = gHistory.on_frame(timestamp);
    467 
    468  update_histogram(gHistogram, delay);
    469 
    470  latencyGraph.draw();
    471  if (memoryGraph) {
    472    memoryGraph.draw();
    473  }
    474  window.requestAnimationFrame(handler);
    475 }
    476 
    477 // For interactive debugging.
    478 //
    479 // ['a', 'b', 'b', 'b', 'c', 'c'] => ['a', 'b x 3', 'c x 2']
    480 function summarize(arr) {
    481  if (!arr.length) {
    482    return [];
    483  }
    484 
    485  var result = [];
    486  var run_start = 0;
    487  var prev = arr[0];
    488  for (let i = 1; i <= arr.length; i++) {
    489    if (i == arr.length || arr[i] != prev) {
    490      if (i == run_start + 1) {
    491        result.push(arr[i]);
    492      } else {
    493        result.push(prev + " x " + (i - run_start));
    494      }
    495      run_start = i;
    496    }
    497    if (i != arr.length) {
    498      prev = arr[i];
    499    }
    500  }
    501 
    502  return result;
    503 }
    504 
    505 function reset_draw_state() {
    506  gHistory.reset();
    507 }
    508 
    509 function onunload() {
    510  if (gLoadMgr) {
    511    gLoadMgr.deactivateLoad();
    512  }
    513 }
    514 
    515 async function onload() {
    516  // Collect all test loads into the `tests` Map.
    517  let imports = [];
    518  foreach_test_file(path => imports.push(import("./" + path)));
    519  await Promise.all(imports);
    520 
    521  // The order of `tests` is currently based on their asynchronous load
    522  // order, rather than the listed order. Rearrange by extracting the test
    523  // names from their filenames, which is kind of gross.
    524  _tests = tests;
    525  tests = new Map();
    526  foreach_test_file(fn => {
    527    // "benchmarks/foo.js" => "foo"
    528    const name = fn.split(/\//)[1].split(/\./)[0];
    529    tests.set(name, _tests.get(name));
    530  });
    531  _tests = undefined;
    532 
    533  gLoadMgr = new AllocationLoadManager(tests);
    534 
    535  // Load initial test duration.
    536  duration_changed();
    537 
    538  // Load initial garbage size.
    539  garbage_piles_changed();
    540  garbage_per_frame_changed();
    541 
    542  // Populate the test selection dropdown.
    543  var select = document.getElementById("test-selection");
    544  for (var [name, test] of tests) {
    545    test.name = name;
    546    var option = document.createElement("option");
    547    option.id = name;
    548    option.text = name;
    549    option.title = test.description;
    550    select.add(option);
    551  }
    552 
    553  // Load the initial test.
    554  gLoadMgr.setActiveLoad(gLoadMgr.getByName("noAllocation"));
    555  update_load_display();
    556  document.getElementById("test-selection").value = "noAllocation";
    557 
    558  // Polyfill rAF.
    559  var requestAnimationFrame =
    560    window.requestAnimationFrame ||
    561    window.mozRequestAnimationFrame ||
    562    window.webkitRequestAnimationFrame ||
    563    window.msRequestAnimationFrame;
    564  window.requestAnimationFrame = requestAnimationFrame;
    565 
    566  // Acquire our canvas.
    567  var canvas = document.getElementById("graph");
    568  latencyGraph = new LatencyGraph(canvas);
    569 
    570  if (!gHost.features.haveMemorySizes) {
    571    document.getElementById("memgraph-disabled").style.display = "block";
    572    document.getElementById("track-sizes-div").style.display = "none";
    573  }
    574 
    575  trackHeapSizes(document.getElementById("track-sizes").checked);
    576 
    577  update_load_state_indicator();
    578  gHistory.start();
    579 
    580  // Start drawing.
    581  reset_draw_state();
    582  window.requestAnimationFrame(handler);
    583 }
    584 
    585 function run_one_test() {
    586  start_test_cycle([gLoadMgr.activeLoad().name]);
    587 }
    588 
    589 function run_all_tests() {
    590  start_test_cycle([...tests.keys()]);
    591 }
    592 
    593 function start_test_cycle(tests_to_run) {
    594  // Convert from an iterable to an array for pop.
    595  const duration = gLoadMgr.testDurationMS / 1000;
    596  const mutators = tests_to_run.map(name => new SingleMutatorSequencer(gLoadMgr.getByName(name), gPerf, duration));
    597  const sequencer = new ChainSequencer(mutators);
    598  gLoadMgr.startSequencer(sequencer);
    599  testState = "running";
    600  gHistogram.clear();
    601  reset_draw_state();
    602 }
    603 
    604 function update_load_state_indicator() {
    605  if (
    606    !gLoadMgr.load_running() ||
    607    gLoadMgr.activeLoad().name == "noAllocation"
    608  ) {
    609    loadState = "(none)";
    610  } else if (gHistory.is_stopped() || gLoadMgr.paused) {
    611    loadState = "(inactive)";
    612  } else {
    613    loadState = "(active)";
    614  }
    615  document.getElementById("load-running").textContent = loadState;
    616 }
    617 
    618 function start_test() {
    619  console.log(`Running test: ${gLoadMgr.activeLoad().name}`);
    620  document.getElementById("test-selection").value = gLoadMgr.activeLoad().name;
    621  update_load_state_indicator();
    622 }
    623 
    624 function end_test(timestamp, load) {
    625  document.getElementById("test-progress").textContent = "(not running)";
    626  report_test_result(load, gHistogram);
    627  gHistogram.clear();
    628  console.log(`Ending test ${load.name}`);
    629  if (gLoadMgr.stopped()) {
    630    testState = "idle";
    631  }
    632  update_load_state_indicator();
    633  reset_draw_state();
    634 }
    635 
    636 function compute_test_spark_histogram(histogram) {
    637  const percents = compute_spark_histogram_percents(histogram);
    638 
    639  var sparks = "▁▂▃▄▅▆▇█";
    640  var colors = [
    641    "#aaaa00",
    642    "#007700",
    643    "#dd0000",
    644    "#ff0000",
    645    "#ff0000",
    646    "#ff0000",
    647    "#ff0000",
    648    "#ff0000",
    649  ];
    650  var line = "";
    651  for (let i = 0; i < percents.length; ++i) {
    652    var spark = sparks.charAt(parseInt(percents[i] * sparks.length));
    653    line += `<span style="color:${colors[i]}">${spark}</span>`;
    654  }
    655  return line;
    656 }
    657 
    658 function report_test_result(load, histogram) {
    659  var resultList = document.getElementById("results-display");
    660  var resultElem = document.createElement("div");
    661  var score = compute_test_score(histogram);
    662  var sparks = compute_test_spark_histogram(histogram);
    663  var params = `(${format_num(load.garbagePerFrame)},${format_num(
    664    load.garbagePiles
    665  )})`;
    666  resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${
    667    load.name
    668  }${params} - ${load.description}`;
    669  resultList.appendChild(resultElem);
    670 }
    671 
    672 function update_load_display() {
    673  const garbage = gLoadMgr.activeLoad()
    674    ? gLoadMgr.activeLoad().garbagePerFrame
    675    : parse_units(gDefaultGarbagePerFrame);
    676  document.getElementById("garbage-per-frame").value = format_num(garbage);
    677  const piles = gLoadMgr.activeLoad()
    678    ? gLoadMgr.activeLoad().garbagePiles
    679    : parse_units(gDefaultGarbagePiles);
    680  document.getElementById("garbage-piles").value = format_num(piles);
    681  update_load_state_indicator();
    682 }
    683 
    684 function duration_changed() {
    685  var durationInput = document.getElementById("test-duration");
    686  gLoadMgr.testDurationMS = parseInt(durationInput.value) * 1000;
    687  console.log(
    688    `Updated test duration to: ${gLoadMgr.testDurationMS / 1000} seconds`
    689  );
    690 }
    691 
    692 function onLoadChange() {
    693  var select = document.getElementById("test-selection");
    694  console.log(`Switching to test: ${select.value}`);
    695  gLoadMgr.setActiveLoad(gLoadMgr.getByName(select.value));
    696  update_load_display();
    697  gHistogram.clear();
    698  reset_draw_state();
    699 }
    700 
    701 function garbage_piles_changed() {
    702  const input = document.getElementById("garbage-piles");
    703  const value = parse_units(input.value);
    704  if (isNaN(value)) {
    705    update_load_display();
    706    return;
    707  }
    708 
    709  if (gLoadMgr.load_running()) {
    710    gLoadMgr.change_garbagePiles(value);
    711    console.log(
    712      `Updated garbage-piles to ${gLoadMgr.activeLoad().garbagePiles} items`
    713    );
    714  }
    715  gHistogram.clear();
    716  reset_draw_state();
    717 }
    718 
    719 function garbage_per_frame_changed() {
    720  const input = document.getElementById("garbage-per-frame");
    721  var value = parse_units(input.value);
    722  if (isNaN(value)) {
    723    update_load_display();
    724    return;
    725  }
    726  if (gLoadMgr.load_running()) {
    727    gLoadMgr.change_garbagePerFrame(value);
    728    console.log(
    729      `Updated garbage-per-frame to ${
    730        gLoadMgr.activeLoad().garbagePerFrame
    731      } items`
    732    );
    733  }
    734 }
    735 
    736 function trackHeapSizes(track) {
    737  enabled.trackingSizes = track && gHost.features.haveMemorySizes;
    738 
    739  var canvas = document.getElementById("memgraph");
    740 
    741  if (enabled.trackingSizes) {
    742    canvas.style.display = "block";
    743    memoryGraph = new MemoryGraph(canvas);
    744  } else {
    745    canvas.style.display = "none";
    746    memoryGraph = null;
    747  }
    748 }