Bug 1867190 - Add prefs for PHC probablities r=glandium
[gecko.git] / js / src / devtools / gc-ubench / sequencer.js
blob0271140c04ac6de5c24cae90f4323dba1f48abcd
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).
13   get current() {
14     throw new Error("unimplemented");
15   }
17   start(now = gHost.now()) {
18     this.started = now;
19   }
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     }
30     return this.do_tick(now);
31   }
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   }
39   // Returns whether this sequencer is done running trials.
40   done() {
41     throw new Error("unimplemented");
42   }
44   restart(now = gHost.now()) {
45     this.reset();
46     this.start(now);
47   }
49   // Returns how long the current load has been running.
50   currentLoadElapsed(now = gHost.now()) {
51     return now - this.started;
52   }
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   }
69   get current() {
70     return this.state === 'done' ? undefined : this.mutator;
71   }
73   reset() {
74     this.state = 'init';
75   }
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   }
86   do_tick(now) {
87     if (this.currentLoadElapsed(now) < this.duration) {
88       return false;
89     }
91     const load = this.current;
92     this.state = 'done';
93     return this.perf.on_load_end(load, now);
94   }
96   done() {
97     return this.state === 'done';
98   }
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   }
110   get current() {
111     return this.idx >= 0 ? this.sequencers[this.idx].current : undefined;
112   }
114   reset() {
115     this.state = 'init';
116     this.idx = -1;
117   }
119   start(now = gHost.now()) {
120     super.start(now);
121     if (this.sequencers.length === 0) {
122       this.state = 'done';
123       return;
124     }
126     this.idx = 0;
127     this.sequencers[0].start(now);
128     this.state = 'running';
129   }
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     }
138     if (!sequencer.done()) {
139       // A single trial has completed, but the sequencer is not yet done.
140       return trial_result;
141     }
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     }
151     return trial_result;
152   }
154   done() {
155     return this.state === 'done';
156   }
159 var RunUntilSequencer = class extends Sequencer {
160   constructor(sequencer, loadMgr) {
161     super();
162     this.loadMgr = loadMgr;
163     this.sequencer = sequencer;
165     // init -> running -> done
166     this.state = sequencer.done() ? 'done' : 'init';
167   }
169   get current() {
170     return this.sequencer?.current;
171   }
173   reset() {
174     this.sequencer.reset();
175     this.state = 'init';
176   }
178   start(now) {
179     super.start(now);
180     this.sequencer.start(now);
181     this.initSearch(now);
182     this.state = 'running';
183   }
185   initSearch(now) {}
187   done() {
188     return this.state === 'done';
189   }
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   }
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   }
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;
220     this.goal = goal;
221     this.high_range = high_range;
222     this.persistence = 3;
224     this.clear();
225   }
227   reset() {
228     super.reset();
229     this.clear();
230   }
232   clear() {
233     this.garbagePerFrame = undefined;
235     this.good = undefined;
236     this.goodAt = undefined;
237     this.bad = undefined;
238     this.badAt = undefined;
240     this.numInRange = 0;
241   }
243   start(now) {
244     super.start(now);
245     if (!this.done()) {
246       this.garbagePerFrame = this.sequencer.current.garbagePerFrame;
247     }
248   }
250   searchComplete(result) {
251     print(
252       `Saw ${percent(result.dropped_60fps_fraction)} with garbagePerFrame=${this.garbagePerFrame}`
253     );
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     }
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     }
293     print(`next run with ${this.garbagePerFrame}`);
294     this.loadMgr.change_garbagePerFrame(this.garbagePerFrame);
296     return false;
297   }