Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit c955f39

Browse files
feat(doc): Add component perf charts (#1239)
* feat(doc): Add component perf charts * Address PR comments * Improved type * Address more PR comments
1 parent 8a47d6e commit c955f39

File tree

24 files changed

+483
-23
lines changed

24 files changed

+483
-23
lines changed

docs/src/app.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Provider, themes } from '@stardust-ui/react'
55
import { mergeThemes } from 'src/lib'
66
import { ThemeContext, ThemeContextData, themeContextDefaults } from './context/ThemeContext'
77
import Router from './routes'
8+
import { PerfDataProvider } from './components/ComponentDoc/PerfChart'
89

910
class App extends React.Component<any, ThemeContextData> {
1011
// State also contains the updater function so it will
@@ -31,7 +32,9 @@ class App extends React.Component<any, ThemeContextData> {
3132
],
3233
})}
3334
>
34-
<Router />
35+
<PerfDataProvider>
36+
<Router />
37+
</PerfDataProvider>
3538
</Provider>
3639
</ThemeContext.Provider>
3740
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Box, Flex, Text } from '@stardust-ui/react'
2+
import { PerfChart, usePerfData } from 'docs/src/components/ComponentDoc/PerfChart'
3+
import * as React from 'react'
4+
5+
export const ComponentPerfChart = ({ perfTestName }) => {
6+
const { loading, error, data } = usePerfData(perfTestName)
7+
8+
return (
9+
<Box
10+
styles={{
11+
'::before': {
12+
paddingTop: '50%',
13+
content: '""',
14+
display: 'block',
15+
},
16+
position: 'relative',
17+
}}
18+
>
19+
<Flex
20+
hAlign="center"
21+
vAlign="center"
22+
styles={{
23+
position: 'absolute',
24+
top: 0,
25+
bottom: 0,
26+
left: 0,
27+
right: 0,
28+
}}
29+
>
30+
{loading ? (
31+
<Text content="Loading..." />
32+
) : error ? (
33+
<Text error content={`Error: ${error.message}`} />
34+
) : (
35+
<PerfChart perfData={data} />
36+
)}
37+
</Flex>
38+
</Box>
39+
)
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as React from 'react'
2+
import * as _ from 'lodash'
3+
4+
import ComponentExampleTitle from '../ComponentExample/ComponentExampleTitle'
5+
import { Accordion, Segment } from '@stardust-ui/react'
6+
import ComponentExample from '../ComponentExample'
7+
import { ComponentPerfChart } from './ComponentPerfChart'
8+
9+
export interface ComponentPerfExampleProps {
10+
title: React.ReactNode
11+
description?: React.ReactNode
12+
examplePath: string
13+
}
14+
15+
const ComponentPerfExample: React.FC<ComponentPerfExampleProps> = props => {
16+
const { title, description, examplePath } = props
17+
// FIXME: find a better way
18+
// "components/Divider/Performance/Divider.perf" -> dividerPerfTsx
19+
const perfTestName = `${_.camelCase(_.last(examplePath.split('/')))}Tsx`
20+
21+
return (
22+
<>
23+
<Segment variables={{ padding: 0 }}>
24+
<Segment variables={{ boxShadowColor: undefined }}>
25+
<ComponentExampleTitle title={title} description={description} />
26+
<ComponentPerfChart perfTestName={perfTestName} />
27+
</Segment>
28+
<Accordion
29+
panels={
30+
[
31+
{
32+
title: {
33+
key: 't',
34+
content: 'Show example',
35+
styles: ({ theme }) => {
36+
return {
37+
fontSize: theme.siteVariables.fontSizes.smaller,
38+
}
39+
},
40+
},
41+
content: {
42+
key: 'c',
43+
content: <ComponentExample {..._.omit(props, 'title', 'description')} />, // FIXME: defer rendering until opened
44+
},
45+
},
46+
] as any[]
47+
}
48+
/>
49+
</Segment>
50+
</>
51+
)
52+
}
53+
54+
export default ComponentPerfExample
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ComponentPerfExample'
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as React from 'react'
2+
import * as _ from 'lodash'
3+
import { FlexibleXYPlot, HorizontalGridLines, LineSeries, XAxis, YAxis } from 'react-vis'
4+
import PerfChartTooltip from './PerfChartTooltip'
5+
import { PerfData } from './PerfDataContext'
6+
7+
/**
8+
* Draws a performance chart for all items in perfData.performance.
9+
* Shows tooltip with details for selected build on mouse hover.
10+
* x-axis is a build number
11+
* y-axis is a render time
12+
*/
13+
const PerfChart: React.FC<{ perfData: PerfData }> = ({ perfData }) => {
14+
const availableCharts: string[] = perfData
15+
.reduce(
16+
(acc, next) => {
17+
return Array.from(new Set([...acc, ...Object.keys(next.performance)]))
18+
},
19+
[] as string[],
20+
)
21+
.sort()
22+
23+
const [nearestX, setNearestX] = React.useState<number>()
24+
25+
return (
26+
<FlexibleXYPlot
27+
onMouseLeave={() => {
28+
setNearestX(undefined)
29+
}}
30+
>
31+
<YAxis title="ms" />
32+
<XAxis title="build" />
33+
34+
<HorizontalGridLines />
35+
{availableCharts.map((chartName, index) => (
36+
<LineSeries
37+
key={chartName}
38+
data={perfData.map(entry => ({
39+
x: entry.build,
40+
y: _.get(entry, `performance.${chartName}.actualTime.median`, 0),
41+
}))}
42+
{...(index === 0
43+
? {
44+
onNearestX: (d: { x: number }) => {
45+
setNearestX(d.x)
46+
},
47+
}
48+
: undefined)}
49+
/>
50+
))}
51+
52+
{nearestX ? (
53+
<PerfChartTooltip x={nearestX} data={perfData.find(entry => entry.build === nearestX)} />
54+
) : (
55+
undefined
56+
)}
57+
</FlexibleXYPlot>
58+
)
59+
}
60+
61+
export default PerfChart
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as _ from 'lodash'
2+
import * as React from 'react'
3+
import { Crosshair } from 'react-vis'
4+
import { PerfSample } from './PerfDataContext'
5+
6+
const PerfChartTooltip = ({ x, data, ...rest }: { x: number; data: PerfSample }) => {
7+
return (
8+
<Crosshair {...rest} values={[{ x, y: 20 }]}>
9+
<div style={{ background: '#555', color: 'white', padding: '.5em' }}>
10+
<div>Build:&nbsp;{x}</div>
11+
<div>Date:&nbsp;{data.ts}</div>
12+
<table className="tooltip">
13+
<thead>
14+
<tr>
15+
<th>Example</th>
16+
<th>Min</th>
17+
<th>Median</th>
18+
<th>Max</th>
19+
</tr>
20+
</thead>
21+
<tbody>
22+
{Object.keys(data.performance)
23+
.sort()
24+
.map(chartName => (
25+
<tr key={chartName}>
26+
<td>{chartName}</td>
27+
<td>{_.get(data, `performance.${chartName}.actualTime.min`, '-')}</td>
28+
<td>{_.get(data, `performance.${chartName}.actualTime.median`, '-')}</td>
29+
<td>{_.get(data, `performance.${chartName}.actualTime.max`, '-')}</td>
30+
</tr>
31+
))}
32+
</tbody>
33+
</table>
34+
</div>
35+
</Crosshair>
36+
)
37+
}
38+
39+
export default PerfChartTooltip
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react'
2+
3+
export type PerfSample = {
4+
build: number
5+
ts: string
6+
performance: Record<
7+
string,
8+
{
9+
actualTime: {
10+
min: number
11+
median: number
12+
max: number
13+
}
14+
}
15+
>
16+
}
17+
18+
export type PerfData = PerfSample[]
19+
20+
export type PerfDataContextValue = {
21+
loading: boolean
22+
error: Error
23+
data: PerfData
24+
}
25+
26+
const PerfDataContext = React.createContext<PerfDataContextValue>({
27+
loading: true,
28+
error: undefined,
29+
data: undefined,
30+
})
31+
32+
export default PerfDataContext
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from 'react'
2+
import PerfDataContext from './PerfDataContext'
3+
import config from '../../../config'
4+
5+
/**
6+
* Fetches data from network and stores them to context.
7+
* In production build, returns error instead of fetching the data.
8+
* TODO:
9+
* [ ] cache data in Local Storage
10+
* [ ] optionally fetch more data
11+
* [ ] refresh support
12+
* [ ] serve static data in public builds
13+
*/
14+
const PerfDataProvider: React.FC = ({ children }) => {
15+
const [loading, setLoading] = React.useState(true)
16+
const [error, setError] = React.useState()
17+
const [data, setData] = React.useState()
18+
19+
React.useEffect(() => {
20+
if (process.env.NODE_ENV === 'production') {
21+
setError(new Error('Stats data not available in production'))
22+
setLoading(false)
23+
} else {
24+
fetch(config.getStatsUri)
25+
.then(response => response.json())
26+
.then(responseJson => {
27+
setData(responseJson)
28+
setLoading(false)
29+
})
30+
.catch(error => {
31+
setError(error)
32+
setLoading(false)
33+
})
34+
}
35+
}, [])
36+
37+
return (
38+
<PerfDataContext.Provider value={{ loading, error, data }}>{children}</PerfDataContext.Provider>
39+
)
40+
}
41+
42+
export default PerfDataProvider
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as PerfChart } from './PerfChart'
2+
export { default as PerfDataProvider } from './PerfDataProvider'
3+
export { default as usePerfData } from './usePerfData'
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react'
2+
import PerfDataContext from '../PerfChart/PerfDataContext'
3+
import * as _ from 'lodash'
4+
5+
const usePerfData = (filter?: string) => {
6+
const { loading, error, data = [] } = React.useContext(PerfDataContext)
7+
8+
if (loading || error) {
9+
return { loading, error, data }
10+
}
11+
12+
const filteredData = filter
13+
? data
14+
.filter(entry => _.get(entry, `performance.${filter}`))
15+
.map(entry => ({
16+
...entry,
17+
performance: { [filter]: entry.performance[filter] },
18+
}))
19+
: data
20+
21+
if (filteredData.length === 0) {
22+
return { loading, error: new Error('No data'), data: filteredData }
23+
}
24+
25+
return {
26+
loading,
27+
error,
28+
data: filteredData,
29+
}
30+
}
31+
32+
export default usePerfData

docs/src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const config = {
2+
getStatsUri: 'https://stardust.azurewebsites.net/api/GetPerfStats',
3+
}
4+
5+
export default config

docs/src/examples/components/Attachment/Performance/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as React from 'react'
22

3-
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
3+
import ComponentPerfExample from 'docs/src/components/ComponentDoc/ComponentPerfExample'
44
import NonPublicSection from 'docs/src/components/ComponentDoc/NonPublicSection'
55

66
const Performance = () => (
77
<NonPublicSection title="Performance">
8-
<ComponentExample
8+
<ComponentPerfExample
99
title="Default"
1010
description="A default test."
1111
examplePath="components/Attachment/Performance/Attachment.perf"

docs/src/examples/components/Button/Performance/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as React from 'react'
22

3-
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
3+
import ComponentPerfExample from 'docs/src/components/ComponentDoc/ComponentPerfExample'
44
import NonPublicSection from 'docs/src/components/ComponentDoc/NonPublicSection'
55

66
const Performance = () => (
77
<NonPublicSection title="Performance">
8-
<ComponentExample
8+
<ComponentPerfExample
99
title="Default"
1010
description="A default test."
1111
examplePath="components/Button/Performance/Button.perf"

docs/src/examples/components/Chat/Performance/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as React from 'react'
22

3-
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
3+
import ComponentPerfExample from 'docs/src/components/ComponentDoc/ComponentPerfExample'
44
import NonPublicSection from 'docs/src/components/ComponentDoc/NonPublicSection'
55

66
const Performance = () => (
77
<NonPublicSection title="Performance">
8-
<ComponentExample
8+
<ComponentPerfExample
99
title="Default"
1010
description="A default test."
1111
examplePath="components/Chat/Performance/Chat.perf"
1212
/>
13-
<ComponentExample
13+
<ComponentPerfExample
1414
title="Chat with popover"
1515
description="Chat with actions menu in a popover"
1616
examplePath="components/Chat/Performance/ChatWithPopover.perf"

docs/src/examples/components/Divider/Performance/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as React from 'react'
22

3-
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
3+
import ComponentPerfExample from 'docs/src/components/ComponentDoc/ComponentPerfExample'
44
import NonPublicSection from 'docs/src/components/ComponentDoc/NonPublicSection'
55

66
const Performance = () => (
77
<NonPublicSection title="Performance">
8-
<ComponentExample
8+
<ComponentPerfExample
99
title="Default"
1010
description="A default test."
1111
examplePath="components/Divider/Performance/Divider.perf"

0 commit comments

Comments
 (0)