Benchmarks include forced layout time

This change to 'benchmarks' reports the time taken to perform a forced
layout after mounting the tree. Adding a forced layout to the stress
tests can surface how different approaches to styling may affect browser
render timings.

The total time displayed is now the sum of "scripting time" (previously
total time) and "layout time". The layout time is a reflection of the
time the browser takes to perform a style recalculation and relayout of
the document.

The Benchmark component now has a 'forceLayout' prop. When it is 'true'
a forced layout is triggered on componentDidUpdate. The time taken is
added to the sample's timing data.
This commit is contained in:
Nicolas Gallagher
2018-01-22 17:28:03 -08:00
parent a403244e67
commit efeaea70a9
7 changed files with 73 additions and 48 deletions
-1
View File
@@ -3,7 +3,6 @@
[ignore] [ignore]
<PROJECT_ROOT>/.*/__tests__/.* <PROJECT_ROOT>/.*/__tests__/.*
<PROJECT_ROOT>/packages/benchmarks/.*
<PROJECT_ROOT>/packages/.*/dist/.* <PROJECT_ROOT>/packages/.*/dist/.*
<PROJECT_ROOT>/website/.* <PROJECT_ROOT>/website/.*
.*/node_modules/babel-plugin-transform-react-remove-prop-types/* .*/node_modules/babel-plugin-transform-react-remove-prop-types/*
+3
View File
@@ -100,6 +100,8 @@ export default class App extends Component {
libraryName={r.libraryName} libraryName={r.libraryName}
libraryVersion={r.libraryVersion} libraryVersion={r.libraryVersion}
mean={r.mean} mean={r.mean}
meanLayout={r.meanLayout}
meanScripting={r.meanScripting}
runTime={r.runTime} runTime={r.runTime}
sampleCount={r.sampleCount} sampleCount={r.sampleCount}
stdDev={r.stdDev} stdDev={r.stdDev}
@@ -130,6 +132,7 @@ export default class App extends Component {
<View ref={this._setBenchWrapperRef}> <View ref={this._setBenchWrapperRef}>
<Benchmark <Benchmark
component={Component} component={Component}
forceLayout={true}
getComponentProps={getComponentProps} getComponentProps={getComponentProps}
onComplete={this._createHandleComplete({ onComplete={this._createHandleComplete({
sampleCount, sampleCount,
+54 -28
View File
@@ -1,5 +1,7 @@
// @flow
/* global $Values */ /* global $Values */
/**
* @flow
*/
import * as Timing from './timing'; import * as Timing from './timing';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { getMean, getMedian, getStdDev } from './math'; import { getMean, getMedian, getStdDev } from './math';
@@ -12,8 +14,6 @@ export const BenchmarkType = {
UNMOUNT: 'unmount' UNMOUNT: 'unmount'
}; };
const emptyObject = {};
const shouldRender = (cycle: number, type: $Values<typeof BenchmarkType>): boolean => { const shouldRender = (cycle: number, type: $Values<typeof BenchmarkType>): boolean => {
switch (type) { switch (type) {
// Render every odd iteration (first, third, etc) // Render every odd iteration (first, third, etc)
@@ -66,7 +66,8 @@ const sortNumbers = (a: number, b: number): number => a - b;
type BenchmarkPropsType = { type BenchmarkPropsType = {
component: typeof React.Component, component: typeof React.Component,
getComponentProps?: Function, forceLayout?: boolean,
getComponentProps: Function,
onComplete: (x: BenchResultsType) => void, onComplete: (x: BenchResultsType) => void,
sampleCount: number, sampleCount: number,
timeout: number, timeout: number,
@@ -84,13 +85,13 @@ type BenchmarkStateType = {
* TODO: documentation * TODO: documentation
*/ */
export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkStateType> { export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkStateType> {
_raf: ?Function;
_startTime: number; _startTime: number;
_samples: Array<SampleTimingType>; _samples: Array<SampleTimingType>;
static displayName = 'Benchmark'; static displayName = 'Benchmark';
static defaultProps = { static defaultProps = {
getComponentProps: () => emptyObject,
sampleCount: 50, sampleCount: 50,
timeout: 10000, // 10 seconds timeout: 10000, // 10 seconds
type: BenchmarkType.MOUNT type: BenchmarkType.MOUNT
@@ -101,8 +102,9 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
constructor(props: BenchmarkPropsType, context?: {}) { constructor(props: BenchmarkPropsType, context?: {}) {
super(props, context); super(props, context);
const cycle = 0; const cycle = 0;
const componentProps = props.getComponentProps({ cycle });
this.state = { this.state = {
componentProps: props.getComponentProps({ cycle }), componentProps,
cycle, cycle,
running: false running: false
}; };
@@ -111,7 +113,9 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
} }
componentWillReceiveProps(nextProps: BenchmarkPropsType) { componentWillReceiveProps(nextProps: BenchmarkPropsType) {
this.setState(state => ({ componentProps: nextProps.getComponentProps(state.cycle) })); if (nextProps) {
this.setState(state => ({ componentProps: nextProps.getComponentProps(state.cycle) }));
}
} }
componentWillUpdate(nextProps: BenchmarkPropsType, nextState: BenchmarkStateType) { componentWillUpdate(nextProps: BenchmarkPropsType, nextState: BenchmarkStateType) {
@@ -121,11 +125,20 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
} }
componentDidUpdate() { componentDidUpdate() {
const { sampleCount, timeout, type } = this.props; const { forceLayout, sampleCount, timeout, type } = this.props;
const { cycle, running } = this.state; const { cycle, running } = this.state;
if (running && shouldRecord(cycle, type)) { if (running && shouldRecord(cycle, type)) {
this._samples[cycle].end = Timing.now(); this._samples[cycle].scriptingEnd = Timing.now();
// force style recalc that would otherwise happen before the next frame
if (forceLayout) {
this._samples[cycle].layoutStart = Timing.now();
if (document.body) {
document.body.offsetWidth;
}
this._samples[cycle].layoutEnd = Timing.now();
}
} }
if (running) { if (running) {
@@ -139,8 +152,8 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
} }
componentWillUnmount() { componentWillUnmount() {
if (this.raf) { if (this._raf) {
window.cancelAnimationFrame(this.raf); window.cancelAnimationFrame(this._raf);
} }
} }
@@ -148,7 +161,7 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
const { component: Component, type } = this.props; const { component: Component, type } = this.props;
const { componentProps, cycle, running } = this.state; const { componentProps, cycle, running } = this.state;
if (running && shouldRecord(cycle, type)) { if (running && shouldRecord(cycle, type)) {
this._samples[cycle] = { start: Timing.now() }; this._samples[cycle] = { scriptingStart: Timing.now() };
} }
return running && shouldRender(cycle, type) ? <Component {...componentProps} /> : null; return running && shouldRender(cycle, type) ? <Component {...componentProps} /> : null;
} }
@@ -162,15 +175,18 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
const { getComponentProps, type } = this.props; const { getComponentProps, type } = this.props;
const { cycle } = this.state; const { cycle } = this.state;
// Calculate the component props outside of the time recording (render) let componentProps;
// so that it doesn't skew results if (getComponentProps) {
const componentProps = getComponentProps({ cycle }); // Calculate the component props outside of the time recording (render)
// make sure props always change for update tests // so that it doesn't skew results
if (type === BenchmarkType.UPDATE) { componentProps = getComponentProps({ cycle });
componentProps['data-test'] = cycle; // make sure props always change for update tests
if (type === BenchmarkType.UPDATE) {
componentProps['data-test'] = cycle;
}
} }
this.raf = window.requestAnimationFrame(() => { this._raf = window.requestAnimationFrame(() => {
this.setState((state: BenchmarkStateType) => ({ this.setState((state: BenchmarkStateType) => ({
cycle: state.cycle + 1, cycle: state.cycle + 1,
componentProps componentProps
@@ -182,10 +198,16 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
return this._samples.reduce( return this._samples.reduce(
( (
memo: Array<FullSampleTimingType>, memo: Array<FullSampleTimingType>,
{ start, end: endTime }: SampleTimingType { scriptingStart, scriptingEnd, layoutStart, layoutEnd }: SampleTimingType
): Array<FullSampleTimingType> => { ): Array<FullSampleTimingType> => {
const end = endTime || 0; memo.push({
memo.push({ start, end, elapsed: end - start }); start: scriptingStart,
end: layoutEnd || scriptingEnd || 0,
scriptingStart,
scriptingEnd: scriptingEnd || 0,
layoutStart,
layoutEnd
});
return memo; return memo;
}, },
[] []
@@ -199,11 +221,13 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
this.setState(() => ({ running: false, cycle: 0 })); this.setState(() => ({ running: false, cycle: 0 }));
const runTime = endTime - this._startTime; const runTime = endTime - this._startTime;
const sortedElapsedTimes = samples const sortedElapsedTimes = samples.map(({ start, end }) => end - start).sort(sortNumbers);
.map(({ elapsed }: { elapsed: number }): number => elapsed) const sortedScriptingElapsedTimes = samples
.map(({ scriptingStart, scriptingEnd }) => scriptingEnd - scriptingStart)
.sort(sortNumbers);
const sortedLayoutElapsedTimes = samples
.map(({ layoutStart, layoutEnd }) => (layoutEnd || 0) - (layoutStart || 0))
.sort(sortNumbers); .sort(sortNumbers);
const mean = getMean(sortedElapsedTimes);
const stdDev = getStdDev(sortedElapsedTimes);
onComplete({ onComplete({
startTime: this._startTime, startTime: this._startTime,
@@ -214,8 +238,10 @@ export default class Benchmark extends Component<BenchmarkPropsType, BenchmarkSt
max: sortedElapsedTimes[sortedElapsedTimes.length - 1], max: sortedElapsedTimes[sortedElapsedTimes.length - 1],
min: sortedElapsedTimes[0], min: sortedElapsedTimes[0],
median: getMedian(sortedElapsedTimes), median: getMedian(sortedElapsedTimes),
mean, mean: getMean(sortedElapsedTimes),
stdDev stdDev: getStdDev(sortedElapsedTimes),
meanLayout: getMean(sortedLayoutElapsedTimes),
meanScripting: getMean(sortedScriptingElapsedTimes)
}); });
} }
} }
+12 -9
View File
@@ -1,4 +1,6 @@
// @flow /**
* @flow
*/
export type BenchResultsType = { export type BenchResultsType = {
startTime: number, startTime: number,
endTime: number, endTime: number,
@@ -9,20 +11,21 @@ export type BenchResultsType = {
min: number, min: number,
median: number, median: number,
mean: number, mean: number,
stdDev: number, stdDev: number
p70: number,
p95: number,
p99: number
}; };
export type SampleTimingType = { export type SampleTimingType = {
start: number, scriptingStart: number,
end?: number, scriptingEnd?: number,
elapsed?: number layoutStart?: number,
layoutEnd?: number
}; };
export type FullSampleTimingType = { export type FullSampleTimingType = {
start: number, start: number,
end: number, end: number,
elapsed: number scriptingStart: number,
scriptingEnd: number,
layoutStart?: number,
layoutEnd?: number
}; };
+4 -4
View File
@@ -15,8 +15,9 @@ class ReportCard extends React.PureComponent {
libraryName, libraryName,
sampleCount, sampleCount,
mean, mean,
meanLayout,
meanScripting,
stdDev, stdDev,
runTime,
libraryVersion libraryVersion
} = this.props; } = this.props;
@@ -38,9 +39,8 @@ class ReportCard extends React.PureComponent {
<Text style={[styles.bold, styles.monoFont]}> <Text style={[styles.bold, styles.monoFont]}>
{fmt(mean)} ±{fmt(stdDev)} ms {fmt(mean)} ±{fmt(stdDev)} ms
</Text> </Text>
<Text style={[styles.monoFont, styles.centerText]}> <Text style={[styles.smallText, styles.monoFont]}>
<Text style={styles.smallText}>Σ = </Text> (S/L) {fmt(meanScripting)} / {fmt(meanLayout)} ms
<Text>{Math.round(runTime)} ms</Text>
</Text> </Text>
</Fragment> </Fragment>
) : ( ) : (
-4
View File
@@ -1,9 +1,5 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/**
* @flow
*/
import { bool } from 'prop-types'; import { bool } from 'prop-types';
import React from 'react'; import React from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
-2
View File
@@ -1,5 +1,3 @@
/* @flow */
import { type Component } from 'react'; import { type Component } from 'react';
import packageJson from '../package.json'; import packageJson from '../package.json';