tor-browser

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

sequencer.js (7616B)


      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 // A Sequencer handles transitioning between different mutators. Typically, it
      6 // will base the decision to transition on things like elapsed time, number of
      7 // GCs observed, or similar. However, they might also implement a search for
      8 // some result value by running for some time while measuring, tweaking
      9 // parameters, and re-running until an in-range result is found.
     10 
     11 var Sequencer = class {
     12  // Return the current mutator (of class AllocationLoad).
     13  get current() {
     14    throw new Error("unimplemented");
     15  }
     16 
     17  start(now = gHost.now()) {
     18    this.started = now;
     19  }
     20 
     21  // Called by user to handle advancing time. Subclasses will normally override
     22  // do_tick() instead. Returns the results of a trial if complete (the mutator
     23  // reached its allotted time or otherwise determined that its timing data
     24  // should be valid), and falsy otherwise.
     25  tick(now = gHost.now()) {
     26    if (this.done()) {
     27      throw new Error("tick() called on completed sequencer");
     28    }
     29 
     30    return this.do_tick(now);
     31  }
     32 
     33  // Implement in subclass to handle time advancing. Must return trial's result
     34  // if complete. Called by tick(), above.
     35  do_tick(now = gHost.now()) {
     36    throw new Error("unimplemented");
     37  }
     38 
     39  // Returns whether this sequencer is done running trials.
     40  done() {
     41    throw new Error("unimplemented");
     42  }
     43 
     44  restart(now = gHost.now()) {
     45    this.reset();
     46    this.start(now);
     47  }
     48 
     49  // Returns how long the current load has been running.
     50  currentLoadElapsed(now = gHost.now()) {
     51    return now - this.started;
     52  }
     53 };
     54 
     55 // Run a single trial of a mutator and be done.
     56 var SingleMutatorSequencer = class extends Sequencer {
     57  constructor(mutator, perf, duration_sec) {
     58    super();
     59    this.mutator = mutator;
     60    this.perf = perf;
     61    if (!(duration_sec > 0)) {
     62      throw new Error(`invalid duration '${duration_sec}'`);
     63    }
     64    this.duration = duration_sec * 1000;
     65    this.state = 'init'; // init -> running -> done
     66    this.lastResult = undefined;
     67  }
     68 
     69  get current() {
     70    return this.state === 'done' ? undefined : this.mutator;
     71  }
     72 
     73  reset() {
     74    this.state = 'init';
     75  }
     76 
     77  start(now = gHost.now()) {
     78    if (this.state !== 'init') {
     79      throw new Error("cannot restart a single-mutator sequencer");
     80    }
     81    super.start(now);
     82    this.state = 'running';
     83    this.perf.on_load_start(this.current, now);
     84  }
     85 
     86  do_tick(now) {
     87    if (this.currentLoadElapsed(now) < this.duration) {
     88      return false;
     89    }
     90 
     91    const load = this.current;
     92    this.state = 'done';
     93    return this.perf.on_load_end(load, now);
     94  }
     95 
     96  done() {
     97    return this.state === 'done';
     98  }
     99 };
    100 
    101 // For each of series of sequencers, run until done.
    102 var ChainSequencer = class extends Sequencer {
    103  constructor(sequencers) {
    104    super();
    105    this.sequencers = sequencers;
    106    this.idx = -1;
    107    this.state = sequencers.length ? 'init' : 'done'; // init -> running -> done
    108  }
    109 
    110  get current() {
    111    return this.idx >= 0 ? this.sequencers[this.idx].current : undefined;
    112  }
    113 
    114  reset() {
    115    this.state = 'init';
    116    this.idx = -1;
    117  }
    118 
    119  start(now = gHost.now()) {
    120    super.start(now);
    121    if (this.sequencers.length === 0) {
    122      this.state = 'done';
    123      return;
    124    }
    125 
    126    this.idx = 0;
    127    this.sequencers[0].start(now);
    128    this.state = 'running';
    129  }
    130 
    131  do_tick(now) {
    132    const sequencer = this.sequencers[this.idx];
    133    const trial_result = sequencer.do_tick(now);
    134    if (!trial_result) {
    135      return false; // Trial is still going.
    136    }
    137 
    138    if (!sequencer.done()) {
    139      // A single trial has completed, but the sequencer is not yet done.
    140      return trial_result;
    141    }
    142 
    143    this.idx++;
    144    if (this.idx < this.sequencers.length) {
    145      this.sequencers[this.idx].start();
    146    } else {
    147      this.idx = -1;
    148      this.state = 'done';
    149    }
    150 
    151    return trial_result;
    152  }
    153 
    154  done() {
    155    return this.state === 'done';
    156  }
    157 };
    158 
    159 var RunUntilSequencer = class extends Sequencer {
    160  constructor(sequencer, loadMgr) {
    161    super();
    162    this.loadMgr = loadMgr;
    163    this.sequencer = sequencer;
    164 
    165    // init -> running -> done
    166    this.state = sequencer.done() ? 'done' : 'init';
    167  }
    168 
    169  get current() {
    170    return this.sequencer?.current;
    171  }
    172 
    173  reset() {
    174    this.sequencer.reset();
    175    this.state = 'init';
    176  }
    177 
    178  start(now) {
    179    super.start(now);
    180    this.sequencer.start(now);
    181    this.initSearch(now);
    182    this.state = 'running';
    183  }
    184 
    185  initSearch(now) {}
    186 
    187  done() {
    188    return this.state === 'done';
    189  }
    190 
    191  do_tick(now) {
    192    const trial_result = this.sequencer.do_tick(now);
    193    if (trial_result) {
    194      if (this.searchComplete(trial_result)) {
    195        this.state = 'done';
    196      } else {
    197        this.sequencer.restart(now);
    198      }
    199    }
    200    return trial_result;
    201  }
    202 
    203  // Take the result of the last mutator run into account (only notified after
    204  // a mutator is complete, so cannot be used to decide when to end the
    205  // mutator.)
    206  searchComplete(result) {
    207    throw new Error("must implement in subclass");
    208  }
    209 };
    210 
    211 // Run trials, adjusting garbagePerFrame, until 50% of the frames are dropped.
    212 var Find50Sequencer = class extends RunUntilSequencer {
    213  constructor(sequencer, loadMgr, goal=0.5, low_range=0.45, high_range=0.55) {
    214    super(sequencer, loadMgr);
    215 
    216    // Run trials with varying garbagePerFrame, looking for a setting that
    217    // drops 50% of the frames, until we have been searching in the range for
    218    // `persistence` times.
    219    this.low_range = low_range;
    220    this.goal = goal;
    221    this.high_range = high_range;
    222    this.persistence = 3;
    223 
    224    this.clear();
    225  }
    226 
    227  reset() {
    228    super.reset();
    229    this.clear();
    230  }
    231 
    232  clear() {
    233    this.garbagePerFrame = undefined;
    234 
    235    this.good = undefined;
    236    this.goodAt = undefined;
    237    this.bad = undefined;
    238    this.badAt = undefined;
    239 
    240    this.numInRange = 0;
    241  }
    242 
    243  start(now) {
    244    super.start(now);
    245    if (!this.done()) {
    246      this.garbagePerFrame = this.sequencer.current.garbagePerFrame;
    247    }
    248  }
    249 
    250  searchComplete(result) {
    251    print(
    252      `Saw ${percent(result.dropped_60fps_fraction)} with garbagePerFrame=${this.garbagePerFrame}`
    253    );
    254 
    255    // This is brittle with respect to noise. It might be better to do a linear
    256    // regression and stop at an error threshold.
    257    if (result.dropped_60fps_fraction < this.goal) {
    258      if (this.goodAt === undefined || this.goodAt < this.garbagePerFrame) {
    259        this.goodAt = this.garbagePerFrame;
    260        this.good = result.dropped_60fps_fraction;
    261      }
    262      if (this.badAt !== undefined) {
    263        this.garbagePerFrame = Math.trunc(
    264          (this.garbagePerFrame + this.badAt) / 2
    265        );
    266      } else {
    267        this.garbagePerFrame *= 2;
    268      }
    269    } else {
    270      if (this.badAt === undefined || this.badAt > this.garbagePerFrame) {
    271        this.badAt = this.garbagePerFrame;
    272        this.bad = result.dropped_60fps_fraction;
    273      }
    274      if (this.goodAt !== undefined) {
    275        this.garbagePerFrame = Math.trunc(
    276          (this.garbagePerFrame + this.goodAt) / 2
    277        );
    278      } else {
    279        this.garbagePerFrame = Math.trunc(this.garbagePerFrame / 2);
    280      }
    281    }
    282 
    283    if (
    284      this.low_range < result.dropped_60fps_fraction &&
    285      result.dropped_60fps_fraction < this.high_range
    286    ) {
    287      this.numInRange++;
    288      if (this.numInRange >= this.persistence) {
    289        return true;
    290      }
    291    }
    292 
    293    print(`next run with ${this.garbagePerFrame}`);
    294    this.loadMgr.change_garbagePerFrame(this.garbagePerFrame);
    295 
    296    return false;
    297  }
    298 };