harness.js (8041B)
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 // Global defaults 6 7 // Allocate this much "garbage" per frame. This might correspond exactly to a 8 // number of objects/values, or it might be some set of objects, depending on 9 // the mutator in question. 10 var gDefaultGarbagePerFrame = "8K"; 11 12 // In order to avoid a performance cliff between when the per-frame garbage 13 // fits in the nursery and when it doesn't, most mutators will collect multiple 14 // "piles" of garbage and round-robin through them, so that the per-frame 15 // garbage stays alive for some number of frames. There will still be some 16 // internal temporary allocations that don't end up in the piles; presumably, 17 // the nursery will take care of those. 18 // 19 // If the per-frame garbage is K and the number of piles is P, then some of the 20 // garbage will start getting tenured as long as P*K > size(nursery). 21 var gDefaultGarbagePiles = "8"; 22 23 var gDefaultTestDuration = 8.0; 24 25 // The Host interface that provides functionality needed by the test harnesses 26 // (web + various shells). Subclasses should override with the appropriate 27 // functionality. The methods that throw an error must be implemented. The ones 28 // that return undefined are optional. 29 // 30 // Note that currently the web UI doesn't really use the scheduling pieces of 31 // this. 32 var Host = class { 33 constructor() {} 34 start_turn() { 35 throw new Error("unimplemented"); 36 } 37 end_turn() { 38 throw new Error("unimplemented"); 39 } 40 suspend(duration) { 41 throw new Error("unimplemented"); 42 } // Shell driver only 43 now() { 44 return performance.now(); 45 } 46 47 minorGCCount() { 48 return undefined; 49 } 50 majorGCCount() { 51 return undefined; 52 } 53 GCSliceCount() { 54 return undefined; 55 } 56 57 features = { 58 haveMemorySizes: false, 59 haveGCCounts: false, 60 }; 61 }; 62 63 function percent(x) { 64 return `${(x*100).toFixed(2)}%`; 65 } 66 67 function parse_units(v) { 68 if (!v.length) { 69 return NaN; 70 } 71 var lastChar = v[v.length - 1].toLowerCase(); 72 if (!isNaN(parseFloat(lastChar))) { 73 return parseFloat(v); 74 } 75 var units = parseFloat(v.substr(0, v.length - 1)); 76 if (lastChar == "k") { 77 return units * 1e3; 78 } 79 if (lastChar == "m") { 80 return units * 1e6; 81 } 82 if (lastChar == "g") { 83 return units * 1e9; 84 } 85 return NaN; 86 } 87 88 var AllocationLoad = class { 89 constructor(info, name) { 90 this.load = info; 91 this.load.name = this.load.name ?? name; 92 93 this._garbagePerFrame = 94 info.garbagePerFrame || 95 parse_units(info.defaultGarbagePerFrame || gDefaultGarbagePerFrame); 96 this._garbagePiles = 97 info.garbagePiles || 98 parse_units(info.defaultGarbagePiles || gDefaultGarbagePiles); 99 } 100 101 get name() { 102 return this.load.name; 103 } 104 get description() { 105 return this.load.description; 106 } 107 get garbagePerFrame() { 108 return this._garbagePerFrame; 109 } 110 set garbagePerFrame(amount) { 111 this._garbagePerFrame = amount; 112 } 113 get garbagePiles() { 114 return this._garbagePiles; 115 } 116 set garbagePiles(amount) { 117 this._garbagePiles = amount; 118 } 119 120 start() { 121 this.load.load(this._garbagePiles); 122 } 123 124 stop() { 125 this.load.unload(); 126 } 127 128 reload() { 129 this.stop(); 130 this.start(); 131 } 132 133 tick() { 134 this.load.makeGarbage(this._garbagePerFrame); 135 } 136 137 is_dummy_load() { 138 return this.load.name == "noAllocation"; 139 } 140 }; 141 142 var AllocationLoadManager = class { 143 constructor(tests) { 144 this._loads = new Map(); 145 for (const [name, info] of tests.entries()) { 146 this._loads.set(name, new AllocationLoad(info, name)); 147 } 148 this._active = undefined; 149 this._paused = false; 150 151 // Public API 152 this.sequencer = null; 153 this.testDurationMS = gDefaultTestDuration * 1000; 154 } 155 156 getByName(name) { 157 const mutator = this._loads.get(name); 158 if (!mutator) { 159 throw new Error(`invalid mutator '${name}'`); 160 } 161 return mutator; 162 } 163 164 activeLoad() { 165 return this._active; 166 } 167 168 setActiveLoad(mutator) { 169 if (this._active) { 170 this._active.stop(); 171 } 172 this._active = mutator; 173 this._active.start(); 174 } 175 176 deactivateLoad() { 177 this._active.stop(); 178 this._active = undefined; 179 } 180 181 get paused() { 182 return this._paused; 183 } 184 set paused(pause) { 185 this._paused = pause; 186 } 187 188 load_running() { 189 return this._active; 190 } 191 192 change_garbagePiles(amount) { 193 if (this._active) { 194 this._active.garbagePiles = amount; 195 this._active.reload(); 196 } 197 } 198 199 change_garbagePerFrame(amount) { 200 if (this._active) { 201 this._active.garbagePerFrame = amount; 202 } 203 } 204 205 tick(now = gHost.now()) { 206 this.lastActive = this._active; 207 let completed = false; 208 209 if (this.sequencer) { 210 if (this.sequencer.tick(now)) { 211 completed = true; 212 if (this.sequencer.current) { 213 this.setActiveLoad(this.sequencer.current); 214 } else { 215 this.deactivateLoad(); 216 } 217 if (this.sequencer.done()) { 218 this.sequencer = null; 219 } 220 } 221 } 222 223 if (this._active && !this._paused) { 224 this._active.tick(); 225 } 226 227 return completed; 228 } 229 230 startSequencer(sequencer, now = gHost.now()) { 231 this.sequencer = sequencer; 232 this.sequencer.start(now); 233 this.setActiveLoad(this.sequencer.current); 234 } 235 236 stopped() { 237 return !this.sequencer || this.sequencer.done(); 238 } 239 240 currentLoadRemaining(now = gHost.now()) { 241 if (this.stopped()) { 242 return 0; 243 } 244 245 // TODO: The web UI displays a countdown to the end of the current mutator. 246 // This won't work for potential future things like "run until 3 major GCs 247 // have been seen", so the API will need to be modified to provide 248 // information in that case. 249 return this.testDurationMS - this.sequencer.currentLoadElapsed(now); 250 } 251 }; 252 253 // Current test state. 254 var gLoadMgr = undefined; 255 256 function format_with_units(n, label, shortlabel, kbase) { 257 function format(n, prefix, unit) { 258 let s = Number.isInteger(n) ? n.toString() : n.toFixed(2); 259 return `${s}${prefix}${unit}`; 260 } 261 262 if (n < kbase * 4) { 263 return `${n} ${label}`; 264 } else if (n < kbase ** 2 * 4) { 265 return format(n / kbase, 'K', shortlabel); 266 } else if (n < kbase ** 3 * 4) { 267 return format(n / kbase ** 2, 'M', shortlabel); 268 } 269 return format(n / kbase ** 3, 'G', shortlabel); 270 } 271 272 function format_bytes(bytes) { 273 return format_with_units(bytes, "bytes", "B", 1024); 274 } 275 276 function format_num(n) { 277 return format_with_units(n, "", "", 1000); 278 } 279 280 function update_histogram(histogram, delay) { 281 // Round to a whole number of 10us intervals to provide enough resolution to 282 // capture a 16ms target with adequate accuracy. 283 delay = Math.round(delay * 100) / 100; 284 var current = histogram.has(delay) ? histogram.get(delay) : 0; 285 histogram.set(delay, ++current); 286 } 287 288 // Compute a score based on the total ms we missed frames by per second. 289 function compute_test_score(histogram) { 290 var score = 0; 291 for (let [delay, count] of histogram) { 292 score += Math.abs((delay - 1000 / 60) * count); 293 } 294 score = score / (gLoadMgr.testDurationMS / 1000); 295 return Math.round(score * 1000) / 1000; 296 } 297 298 // Build a spark-lines histogram for the test results to show with the aggregate score. 299 function compute_spark_histogram_percents(histogram) { 300 var ranges = [ 301 [-99999999, 16.6], 302 [16.6, 16.8], 303 [16.8, 25], 304 [25, 33.4], 305 [33.4, 60], 306 [60, 100], 307 [100, 300], 308 [300, 99999999], 309 ]; 310 var rescaled = new Map(); 311 for (let [delay] of histogram) { 312 for (var i = 0; i < ranges.length; ++i) { 313 var low = ranges[i][0]; 314 var high = ranges[i][1]; 315 if (low <= delay && delay < high) { 316 update_histogram(rescaled, i); 317 break; 318 } 319 } 320 } 321 var total = 0; 322 for (const [, count] of rescaled) { 323 total += count; 324 } 325 326 var spark = []; 327 for (let i = 0; i < ranges.length; ++i) { 328 const amt = rescaled.has(i) ? rescaled.get(i) : 0; 329 spark.push(amt / total); 330 } 331 332 return spark; 333 }