Repository navigation
BenchmarkRunner test measurement refactoring #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
78827ea
6d228fb
55c24b6
88f0aea
1918d35
5332231
4fa3eea
3fefc6d
c98d533
79985e7
30f6e07
c086c4e
9d8e3c1
e5fd3b9
983242e
9a45be9
57066c9
69e92c7
2c20010
d03dbca
fa7d90b
376675b
8a997f3
eda8635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -163,6 +163,166 @@ const WarmupSuite = { | |
| ], | ||
| }; | ||
|
|
||
| class TimerCallbacks { | ||
| constructor(firstCallback, secondCallback) { | ||
| this._firstCallback = firstCallback; | ||
| this._secondCallback = secondCallback; | ||
| this._setTimeoutCallback = this._setTimeout.bind(this); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I'd find it easier to follow these classes if property name was always |
||
| } | ||
|
|
||
| run() { | ||
| setTimeout(this._setTimeoutCallback, 0); | ||
| } | ||
|
|
||
| _setTimeout() { | ||
| this._firstCallback(); | ||
| setTimeout(this._secondCallback, 0); | ||
| } | ||
| } | ||
|
|
||
| class RAFBracketedCallbacks { | ||
| constructor(firstCallback, secondCallback) { | ||
| this._firstCallback = firstCallback; | ||
| this._secondCallback = secondCallback; | ||
| this._setTimeoutCallback = this._setTimeout.bind(this); | ||
| } | ||
|
|
||
| run() { | ||
| requestAnimationFrame(this._firstCallback); | ||
| requestAnimationFrame(this._setTimeoutCallback); | ||
| } | ||
|
|
||
| _setTimeout() { | ||
| setTimeout(this._secondCallback, 0); | ||
| } | ||
| } | ||
|
|
||
| class MeasureStep { | ||
| constructor(runner, suite, test, page, frame) { | ||
| this._runner = runner; | ||
| this.suite = suite; | ||
| this.test = test; | ||
| this._page = page; | ||
| this._frame = frame; | ||
|
|
||
| this._unusedHeight = undefined; | ||
|
|
||
| this._wasRun = false; | ||
|
|
||
| this._asyncMeasurePromise = new Promise((resolve) => { | ||
| this._asyncDoneCallback = resolve; | ||
| }); | ||
| this._rAFMeasurement = new RAFBracketedCallbacks(this._measureRafStart.bind(this), this._measureRafEnd.bind(this)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something here is rather confusing. Using RAFBracketedCallbacks in two places with different callbacks. |
||
|
|
||
| this.syncTime = 0; | ||
| this.asyncTime = 0; | ||
| this.layoutTime = 0; | ||
| this.rafTime = 0; | ||
|
|
||
| this._asyncStartTime = 0; | ||
|
|
||
| this._label = `${suite.name}.${test.name}`; | ||
| this._startLabel = `${this._label}-start`; | ||
| this._syncEndLabel = `${this._label}-sync-end`; | ||
| this._asyncStartLabel = `${this._label}-async-start`; | ||
| this._asyncEndLabel = `${this._label}-async-end`; | ||
| this._forcedLayoutStartLabel = `${this._label}-layout-start`; | ||
| this._forcedLayoutEndLabel = `${this._label}-layout-end`; | ||
| this._rAFStartLabel= `${this._label}-raf-start`; | ||
| this._rAFEndLabel= `${this._label}-raf-end`; | ||
| this._rAFLabel = `${this._label}-raf`; | ||
|
|
||
| this._asyncMetricMode = params.asyncMetric; | ||
| } | ||
|
|
||
| async run() { | ||
| if (this._wasRun) | ||
| throw Error("MeasureStep can only run once."); | ||
| if (params.stepWaitTime) { | ||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, params.stepWaitTime); | ||
| }); | ||
| } | ||
|
camillobruni marked this conversation as resolved.
|
||
| if (this._asyncMetricMode === "timeout") | ||
| new TimerCallbacks(this._measureSync.bind(this), this._measureAsyncTimeout.bind(this)).run(); | ||
| else if (this._asyncMetricMode === "raf") | ||
| new RAFBracketedCallbacks(this._measureSync.bind(this), this._measureAsyncEnd.bind(this)).run(); | ||
| else | ||
| throw new Error(`Unknown async measure mode: ${this._asyncMetricMode}`); | ||
| await this._done(); | ||
| this._wasRun = true; | ||
| } | ||
|
|
||
| _measureSync() { | ||
| // This function ought be as simple as possible. Don't even use Promise. | ||
| performance.mark(this._startLabel); | ||
| const syncStartTime = performance.now(); | ||
| this.test.run(this._page); | ||
| const syncEndTime = performance.now(); | ||
| performance.mark(this._syncEndLabel); | ||
|
|
||
| this.syncTime = syncEndTime - syncStartTime; | ||
|
|
||
| // Measure test work scheduled via rAF. | ||
| if (this._asyncMetricMode === "raf") | ||
| this._rAFMeasurement.run(); | ||
|
|
||
| performance.mark(this._asyncStartLabel); | ||
| this._asyncStartTime = performance.now(); | ||
| } | ||
|
|
||
| _measureForceLayout() { | ||
| performance.mark(this._forcedLayoutStartLabel); | ||
| const startTime = performance.now(); | ||
| this._unusedHeight = this._frame.contentDocument.body.getBoundingClientRect().height; | ||
| const endTime = performance.now(); | ||
| performance.mark(this._forcedLayoutEndLabel); | ||
| this.layoutTime = endTime - startTime; | ||
| // Prevent dead code elimination. | ||
| this._frame.contentWindow._unusedHeightValue = this._unusedHeight; | ||
| } | ||
|
|
||
| _measureAsyncTimeout() { | ||
| this._measureForceLayout(); | ||
| this._measureAsyncEnd(); | ||
| this._asyncDoneCallback(); | ||
| } | ||
|
|
||
| _measureAsyncEnd() { | ||
| const endTime = performance.now(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this called as the second rAF callback. This wouldn't capture setTimeout(, 0).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think we need to start setTimeout(~, 0) here and measure that as the end of the async time.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conceptually, two rAF callbacks runs back to back. We want to do this so that any async tasks scheduled during the sync step would run before our timer fires. rAF1 -> measure sync, this can schedule async tasks with setTimeout(~ , 0), postMessage, etc... This guarantees that end-timer runs after all other async tasks scheduled in rAF1. |
||
| performance.mark(this._asyncEndLabel); | ||
| this.asyncTime = endTime - this._asyncStartTime; | ||
| } | ||
|
|
||
| _measureRafStart() { | ||
| performance.mark(this._rAFStartLabel); | ||
| this._rAFStartTime = performance.now(); | ||
| } | ||
|
|
||
| _measureRafEnd() { | ||
| const endTime = performance.now(); | ||
| performance.mark(this._rAFEndLabel); | ||
| performance.measure(this._rAFLabel, this._rAFStartLabel, this._rAFEndLabel); | ||
| const delta = endTime - this._rAFStartTime; | ||
| this.rafTime += delta; | ||
| // Keep accumulating RAF time until there is no more work. | ||
| // FIXME: Figure out a better way to do this (maybe a configurable iteration count?). | ||
| if (delta >= 1) | ||
| this._rAFMeasurement.run(); | ||
| else | ||
| this._asyncDoneCallback(); | ||
| } | ||
|
|
||
| async _done() { | ||
| await this._asyncMeasurePromise; | ||
| performance.measure(`${this._label}-sync`, this._startLabel, this._syncEndLabel); | ||
| performance.measure(`${this._label}-async`, this._asyncStartLabel, this._asyncEndLabel); | ||
| if (this._asyncMetricMode === "timeout") | ||
| performance.measure(`${this._label}-layout`, this._forcedLayoutStartLabel, this._forcedLayoutEndLabel); | ||
| await new Promise(requestAnimationFrame); | ||
| } | ||
| } | ||
|
|
||
| export class BenchmarkRunner { | ||
| constructor(suites, client) { | ||
| this._suites = suites; | ||
|
|
@@ -266,67 +426,32 @@ export class BenchmarkRunner { | |
| } | ||
|
|
||
| async _runTestAndRecordResults(suite, test) { | ||
| /* eslint-disable-next-line no-async-promise-executor */ | ||
| return new Promise(async (resolve) => { | ||
| if (this._client?.willRunTest) | ||
| await this._client.willRunTest(suite, test); | ||
|
|
||
| setTimeout(() => { | ||
| this._runTest(suite, test, this._page, resolve); | ||
| }, 0); | ||
| }); | ||
| } | ||
|
|
||
| // This function ought be as simple as possible. Don't even use Promise. | ||
| _runTest(suite, test, page, testDoneCallback) { | ||
| // Prepare all mark labels outside the measuring loop. | ||
| const startLabel = `${suite.name}.${test.name}-start`; | ||
| const syncEndLabel = `${suite.name}.${test.name}-sync-end`; | ||
| const asyncStartLabel = `${suite.name}.${test.name}-async-start`; | ||
| const asyncEndLabel = `${suite.name}.${test.name}-async-end`; | ||
|
|
||
| performance.mark(startLabel); | ||
| const syncStartTime = performance.now(); | ||
| test.run(page); | ||
| const syncEndTime = performance.now(); | ||
| performance.mark(syncEndLabel); | ||
|
|
||
| const syncTime = syncEndTime - syncStartTime; | ||
|
|
||
| performance.mark(asyncStartLabel); | ||
| const asyncStartTime = performance.now(); | ||
| setTimeout(() => { | ||
| // Some browsers don't immediately update the layout for paint. | ||
| // Force the layout here to ensure we're measuring the layout time. | ||
| const height = this._frame.contentDocument.body.getBoundingClientRect().height; | ||
| const asyncEndTime = performance.now(); | ||
| const asyncTime = asyncEndTime - asyncStartTime; | ||
| this._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination. | ||
| performance.mark(asyncEndLabel); | ||
| performance.measure(`${suite.name}.${test.name}-sync`, startLabel, syncEndLabel); | ||
| performance.measure(`${suite.name}.${test.name}-async`, asyncStartLabel, asyncEndLabel); | ||
| window.requestAnimationFrame(() => { | ||
| this._recordTestResults(suite, test, syncTime, asyncTime, height, testDoneCallback); | ||
| }); | ||
| }, 0); | ||
| if (this._client?.willRunTest) | ||
| await this._client.willRunTest(suite, test); | ||
| const step = new MeasureStep(this, suite, test, this._page, this._frame); | ||
| await step.run(); | ||
| await this._recordTestResults(step); | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have before/after numbers among browsers for this change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've finally had time to run this change against the previous version. I do see a 1% to 2% slowdown. I will have to investigate and see whether I can repro this a bit better to pinpoint the main culprit.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have we gotten around to do any analysis on this? |
||
| async _recordTestResults(suite, test, syncTime, asyncTime, unused_height, testDoneCallback) { | ||
| // Skip reporting updates for the warmup suite. | ||
| if (suite === WarmupSuite) { | ||
| testDoneCallback(); | ||
| return; | ||
| } | ||
| async _recordTestResults(step) { | ||
| const suite = step.suite; | ||
| const test = step.test; | ||
| const suiteResults = this._measuredValues.tests[suite.name] || { tests: {}, total: 0 }; | ||
| const total = syncTime + asyncTime; | ||
| const total = step.syncTime + step.asyncTime + step.rafTime; | ||
| this._measuredValues.tests[suite.name] = suiteResults; | ||
| suiteResults.tests[test.name] = { tests: { Sync: syncTime, Async: asyncTime }, total: total }; | ||
| suiteResults.tests[test.name] = { | ||
| tests: { | ||
| Sync: step.syncTime, | ||
| Layout: step.layoutTime, | ||
| Async: step.asyncTime, | ||
| RAF: step.rafTime, | ||
| }, | ||
| total: total, | ||
| }; | ||
| suiteResults.total += total; | ||
|
|
||
| if (this._client?.didRunTest) | ||
| await this._client.didRunTest(suite, test); | ||
|
|
||
| testDoneCallback(); | ||
| } | ||
|
|
||
| async _finalize() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call these as first & second are rather confusing.
We should probably call them as syncCallback & asyncCallback.