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/. */
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.
11 var Sequencer = class {
12 // Return the current mutator (of class AllocationLoad).
14 throw new Error("unimplemented");
17 start(now = gHost.now()) {
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()) {
27 throw new Error("tick() called on completed sequencer");
30 return this.do_tick(now);
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");
39 // Returns whether this sequencer is done running trials.
41 throw new Error("unimplemented");
44 restart(now = gHost.now()) {
49 // Returns how long the current load has been running.
50 currentLoadElapsed(now = gHost.now()) {
51 return now - this.started;
55 // Run a single trial of a mutator and be done.
56 var SingleMutatorSequencer = class extends Sequencer {
57 constructor(mutator, perf, duration_sec) {
59 this.mutator = mutator;
61 if (!(duration_sec > 0)) {
62 throw new Error(`invalid duration '${duration_sec}'`);
64 this.duration = duration_sec * 1000;
65 this.state = 'init'; // init -> running -> done
66 this.lastResult = undefined;
70 return this.state === 'done' ? undefined : this.mutator;
77 start(now = gHost.now()) {
78 if (this.state !== 'init') {
79 throw new Error("cannot restart a single-mutator sequencer");
82 this.state = 'running';
83 this.perf.on_load_start(this.current, now);
87 if (this.currentLoadElapsed(now) < this.duration) {
91 const load = this.current;
93 return this.perf.on_load_end(load, now);
97 return this.state === 'done';
101 // For each of series of sequencers, run until done.
102 var ChainSequencer = class extends Sequencer {
103 constructor(sequencers) {
105 this.sequencers = sequencers;
107 this.state = sequencers.length ? 'init' : 'done'; // init -> running -> done
111 return this.idx >= 0 ? this.sequencers[this.idx].current : undefined;
119 start(now = gHost.now()) {
121 if (this.sequencers.length === 0) {
127 this.sequencers[0].start(now);
128 this.state = 'running';
132 const sequencer = this.sequencers[this.idx];
133 const trial_result = sequencer.do_tick(now);
135 return false; // Trial is still going.
138 if (!sequencer.done()) {
139 // A single trial has completed, but the sequencer is not yet done.
144 if (this.idx < this.sequencers.length) {
145 this.sequencers[this.idx].start();
155 return this.state === 'done';
159 var RunUntilSequencer = class extends Sequencer {
160 constructor(sequencer, loadMgr) {
162 this.loadMgr = loadMgr;
163 this.sequencer = sequencer;
165 // init -> running -> done
166 this.state = sequencer.done() ? 'done' : 'init';
170 return this.sequencer?.current;
174 this.sequencer.reset();
180 this.sequencer.start(now);
181 this.initSearch(now);
182 this.state = 'running';
188 return this.state === 'done';
192 const trial_result = this.sequencer.do_tick(now);
194 if (this.searchComplete(trial_result)) {
197 this.sequencer.restart(now);
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
206 searchComplete(result) {
207 throw new Error("must implement in subclass");
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);
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;
221 this.high_range = high_range;
222 this.persistence = 3;
233 this.garbagePerFrame = undefined;
235 this.good = undefined;
236 this.goodAt = undefined;
237 this.bad = undefined;
238 this.badAt = undefined;
246 this.garbagePerFrame = this.sequencer.current.garbagePerFrame;
250 searchComplete(result) {
252 `Saw ${percent(result.dropped_60fps_fraction)} with garbagePerFrame=${this.garbagePerFrame}`
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;
262 if (this.badAt !== undefined) {
263 this.garbagePerFrame = Math.trunc(
264 (this.garbagePerFrame + this.badAt) / 2
267 this.garbagePerFrame *= 2;
270 if (this.badAt === undefined || this.badAt > this.garbagePerFrame) {
271 this.badAt = this.garbagePerFrame;
272 this.bad = result.dropped_60fps_fraction;
274 if (this.goodAt !== undefined) {
275 this.garbagePerFrame = Math.trunc(
276 (this.garbagePerFrame + this.goodAt) / 2
279 this.garbagePerFrame = Math.trunc(this.garbagePerFrame / 2);
284 this.low_range < result.dropped_60fps_fraction &&
285 result.dropped_60fps_fraction < this.high_range
288 if (this.numInRange >= this.persistence) {
293 print(`next run with ${this.garbagePerFrame}`);
294 this.loadMgr.change_garbagePerFrame(this.garbagePerFrame);