1 function BenchmarkState(testInterval)
3 this._currentTimeOffset = 0;
4 this._stageInterval = testInterval / BenchmarkState.stages.FINISHED;
7 // The enum values and the messages should be in the same order
8 BenchmarkState.stages = {
14 BenchmarkState.prototype =
16 _timeOffset: function(stage)
18 return stage * this._stageInterval;
21 _message: function(stage, timeOffset)
23 if (stage == BenchmarkState.stages.FINISHED)
24 return BenchmarkState.stages.messages[stage];
26 return BenchmarkState.stages.messages[stage] + "... ("
27 + Math.floor((timeOffset - this._timeOffset(stage)) / 1000) + "/"
28 + Math.floor((this._timeOffset(stage + 1) - this._timeOffset(stage)) / 1000) + ")";
31 update: function(currentTimeOffset)
33 this._currentTimeOffset = currentTimeOffset;
36 samplingTimeOffset: function()
38 return this._timeOffset(BenchmarkState.stages.SAMPLING);
41 currentStage: function()
43 for (var stage = BenchmarkState.stages.WARMING; stage < BenchmarkState.stages.FINISHED; ++stage) {
44 if (this._currentTimeOffset < this._timeOffset(stage + 1))
47 return BenchmarkState.stages.FINISHED;
51 Stage = Utilities.createClass(
56 initialize: function(benchmark)
58 this._benchmark = benchmark;
59 this._element = document.getElementById("stage");
60 this._element.setAttribute("width", document.body.offsetWidth);
61 this._element.setAttribute("height", document.body.offsetHeight);
62 this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
75 complexity: function()
82 throw "Not implemented";
87 throw "Not implemented";
92 return this.tune(-this.tune(0));
96 Utilities.extendObject(Stage, {
97 random: function(min, max)
99 return (Math.random() * (max - min)) + min;
102 randomBool: function()
104 return !!Math.round(this.random(0, 1));
107 randomInt: function(min, max)
109 return Math.round(this.random(min, max));
112 randomPosition: function(maxPosition)
114 return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
117 randomSquareSize: function(min, max)
119 var side = this.random(min, max);
120 return new Point(side, side);
123 randomVelocity: function(maxVelocity)
125 return this.random(maxVelocity / 8, maxVelocity);
128 randomAngle: function()
130 return this.random(0, Math.PI * 2);
133 randomColor: function()
138 + this.randomInt(min, max).toString(16)
139 + this.randomInt(min, max).toString(16)
140 + this.randomInt(min, max).toString(16);
143 randomRotater: function()
145 return new Rotater(this.random(1000, 10000));
149 Rotater = Utilities.createClass(
150 function(rotateInterval)
153 this._rotateInterval = rotateInterval;
154 this._isSampling = false;
159 return this._rotateInterval;
162 next: function(timeDelta)
164 this._timeDelta = (this._timeDelta + timeDelta) % this._rotateInterval;
169 return (360 * this._timeDelta) / this._rotateInterval;
174 return "rotateZ(" + Math.floor(this.degree()) + "deg)";
177 rotate: function(center)
179 return "rotate(" + Math.floor(this.degree()) + ", " + center.x + "," + center.y + ")";
183 Animator = Utilities.createClass(
186 this._intervalFrameCount = 0;
187 this._numberOfFramesToMeasurePerInterval = 3;
188 this._referenceTime = 0;
189 this._currentTimeOffset = 0;
192 initialize: function(benchmark)
194 this._benchmark = benchmark;
196 // Use Kalman filter to get a more non-fluctuating frame rate.
197 if (benchmark.options["estimated-frame-rate"])
198 this._estimator = new KalmanEstimator(60);
200 this._estimator = new IdentityEstimator;
205 return this._benchmark;
208 _intervalTimeDelta: function()
210 return this._currentTimeOffset - this._startTimeOffset;
213 _shouldRequestAnotherFrame: function()
215 // Cadence is number of frames to measure, then one more frame to adjust the scene, and drop
216 var currentTime = performance.now();
218 if (!this._referenceTime)
219 this._referenceTime = currentTime;
221 this._currentTimeOffset = currentTime - this._referenceTime;
223 if (!this._intervalFrameCount)
224 this._startTimeOffset = this._currentTimeOffset;
226 // Start the work for the next frame.
227 ++this._intervalFrameCount;
229 // Drop _dropFrameCount frames and measure the average of _measureFrameCount frames.
230 if (this._intervalFrameCount <= this._numberOfFramesToMeasurePerInterval) {
231 this._benchmark.record(this._currentTimeOffset, -1, -1);
235 // Get the average FPS of _measureFrameCount frames over intervalTimeDelta.
236 var intervalTimeDelta = this._intervalTimeDelta();
237 var intervalFrameRate = 1000 / (intervalTimeDelta / this._numberOfFramesToMeasurePerInterval);
238 var estimatedIntervalFrameRate = this._estimator.estimate(intervalFrameRate);
239 // Record the complexity of the frame we just rendered. The next frame's time respresents the adjusted
241 this._benchmark.record(this._currentTimeOffset, estimatedIntervalFrameRate, intervalFrameRate);
243 // Adjust the test to reach the desired FPS.
244 var shouldContinueRunning = this._benchmark.update(this._currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate);
246 // Start the next drop/measure cycle.
247 this._intervalFrameCount = 0;
249 // If result is false, no more requestAnimationFrame() will be invoked.
250 return shouldContinueRunning;
253 animateLoop: function()
255 if (this._shouldRequestAnotherFrame()) {
256 this._benchmark.stage.animate(this._intervalTimeDelta());
257 requestAnimationFrame(this.animateLoop.bind(this));
262 Benchmark = Utilities.createClass(
263 function(stage, options)
265 this._options = options;
268 this._stage.initialize(this);
269 this._animator = new Animator();
270 this._animator.initialize(this);
272 this._recordInterval = 200;
273 this._isSampling = false;
274 this._controller = new PIDController(this._options["frame-rate"]);
275 this._sampler = new Sampler(4, 60 * this._options["test-interval"], this);
276 this._state = new BenchmarkState(this._options["test-interval"] * 1000);
281 return this._options;
291 return this._animator;
294 // Called from the load event listener or from this.run().
297 this._animator.animateLoop();
300 // Called from the animator to adjust the complexity of the test.
301 update: function(currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate)
303 this._state.update(currentTimeOffset);
305 var stage = this._state.currentStage();
306 if (stage == BenchmarkState.stages.FINISHED) {
311 if (stage == BenchmarkState.stages.SAMPLING && !this._isSampling) {
312 this._sampler.mark(Strings.json.samplingTimeOffset, {
313 time: this._state.samplingTimeOffset() / 1000
315 this._isSampling = true;
319 if (this._options["adjustment"] == "fixed") {
320 if (this._options["complexity"]) {
321 // this._stage.tune(0) returns the current complexity of the test.
322 tuneValue = this._options["complexity"] - this._stage.tune(0);
325 else if (!(this._isSampling && this._options["adjustment"] == "fixed-after-warmup")) {
326 // The relationship between frameRate and test complexity is inverse-proportional so we
327 // need to use the negative of PIDController.tune() to change the complexity of the test.
328 tuneValue = -this._controller.tune(currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate);
329 tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
332 this._stage.tune(tuneValue);
334 if (typeof this._recordTimeOffset == "undefined")
335 this._recordTimeOffset = currentTimeOffset;
337 var stage = this._state.currentStage();
338 if (stage != BenchmarkState.stages.FINISHED && currentTimeOffset < this._recordTimeOffset + this._recordInterval)
341 this._recordTimeOffset = currentTimeOffset;
345 record: function(currentTimeOffset, estimatedFrameRate, intervalFrameRate)
347 // If the frame rate is -1 it means we are still recording for this sample
348 this._sampler.record(currentTimeOffset, this.stage.complexity(), estimatedFrameRate, intervalFrameRate);
353 return this.waitUntilReady().then(function() {
355 var promise = new SimplePromise;
356 var resolveWhenFinished = function() {
357 if (typeof this._state != "undefined" && (this._state.currentStage() == BenchmarkState.stages.FINISHED))
358 return promise.resolve(this._sampler);
359 setTimeout(resolveWhenFinished, 50);
362 resolveWhenFinished();
367 waitUntilReady: function()
369 var promise = new SimplePromise;
374 processSamples: function(results)
376 var complexity = new Experiment;
377 var smoothedFPS = new Experiment;
378 var samplingIndex = 0;
380 var samplingMark = this._sampler.marks[Strings.json.samplingTimeOffset];
382 samplingIndex = samplingMark.index;
383 results[Strings.json.samplingTimeOffset] = samplingMark.time;
386 results[Strings.json.samples] = this._sampler.samples[0].map(function(d, i) {
388 // time offsets represented as seconds
390 complexity: this._sampler.samples[1][i]
393 // time offsets represented as FPS
397 result.fps = 1000 / (d - this._sampler.samples[0][i - 1]);
399 var smoothedFPSresult = this._sampler.samples[2][i];
400 if (smoothedFPSresult != -1) {
401 result.smoothedFPS = smoothedFPSresult;
402 result.intervalFPS = this._sampler.samples[3][i];
405 if (i >= samplingIndex) {
406 complexity.sample(result.complexity);
407 if (smoothedFPSresult != -1) {
408 smoothedFPS.sample(smoothedFPSresult);
415 results[Strings.json.score] = complexity.score(Experiment.defaults.CONCERN);
416 [complexity, smoothedFPS].forEach(function(experiment, index) {
417 var jsonExperiment = !index ? Strings.json.experiments.complexity : Strings.json.experiments.frameRate;
418 results[jsonExperiment] = {};
419 results[jsonExperiment][Strings.json.measurements.average] = experiment.mean();
420 results[jsonExperiment][Strings.json.measurements.concern] = experiment.concern(Experiment.defaults.CONCERN);
421 results[jsonExperiment][Strings.json.measurements.stdev] = experiment.standardDeviation();
422 results[jsonExperiment][Strings.json.measurements.percent] = experiment.percentage();