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 };