From cdb9f35d40c8259455320870d9b727f9f007b137 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 30 Oct 2023 12:47:53 +0300 Subject: [PATCH] feat(D3 plugin): add chartPerfomance data to onRender callback --- package-lock.json | 6 ++ package.json | 1 + .../bar-x/PerformanceIssue.stories.tsx | 75 +++++++++++++++++++ .../line/PerformanceIssue.stories.tsx | 72 ++++++++++++++++++ .../scatter/PerformanceIssue.stories.tsx | 59 ++++++++++----- src/plugins/d3/renderer/D3Widget.tsx | 35 ++++++--- src/utils/index.ts | 2 +- src/utils/performance.ts | 10 +++ 8 files changed, 230 insertions(+), 30 deletions(-) create mode 100644 src/plugins/d3/__stories__/bar-x/PerformanceIssue.stories.tsx create mode 100644 src/plugins/d3/__stories__/line/PerformanceIssue.stories.tsx diff --git a/package-lock.json b/package-lock.json index dbcebaf1..5f57c8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@bem-react/classname": "^1.6.0", "@gravity-ui/date-utils": "^1.4.1", "@gravity-ui/yagr": "^3.11.0", + "afterframe": "^1.0.2", "d3": "^7.8.5", "lodash": "^4.17.21", "react-split-pane": "^0.1.92" @@ -7837,6 +7838,11 @@ "node": ">= 10.0.0" } }, + "node_modules/afterframe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/afterframe/-/afterframe-1.0.2.tgz", + "integrity": "sha512-0JeMZI7dIfVs5guqLgidQNV7c6jBC2HO0QNSekAUB82Hr7PdU9QXNAF3kpFkvATvHYDDTGto7FPsRu1ey+aKJQ==" + }, "node_modules/agent-base": { "version": "6.0.2", "dev": true, diff --git a/package.json b/package.json index 2dcacc30..d15d4fc5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@bem-react/classname": "^1.6.0", "@gravity-ui/date-utils": "^1.4.1", "@gravity-ui/yagr": "^3.11.0", + "afterframe": "^1.0.2", "d3": "^7.8.5", "lodash": "^4.17.21", "react-split-pane": "^0.1.92" diff --git a/src/plugins/d3/__stories__/bar-x/PerformanceIssue.stories.tsx b/src/plugins/d3/__stories__/bar-x/PerformanceIssue.stories.tsx new file mode 100644 index 00000000..8d8ff5bb --- /dev/null +++ b/src/plugins/d3/__stories__/bar-x/PerformanceIssue.stories.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {StoryObj} from '@storybook/react'; +import {action} from '@storybook/addon-actions'; +import {Button} from '@gravity-ui/uikit'; +import {settings} from '../../../../libs'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitRef, ChartKitWidgetData} from '../../../../types'; +import {D3Plugin} from '../..'; +import {randomNormal} from 'd3'; +import {randomString} from '../../../../utils'; + +const randomFn = randomNormal(0, 10); +const randomStr = () => randomString(Math.random() * 10, 'absdEFGHIJklmnopqrsTUvWxyz'); + +const ChartStory = (args: {pointsCount: number; seriesCount: number}) => { + const [shown, setShown] = React.useState(false); + const chartkitRef = React.useRef(); + + const widgetData: ChartKitWidgetData = React.useMemo(() => { + const points = Array.from({length: args.pointsCount}).map(() => + Math.ceil(Math.abs(randomFn())), + ); + const series = Array.from({length: args.seriesCount}).map(randomStr); + + return { + series: { + data: series.map((s) => ({ + type: 'bar-x', + stacking: 'normal', + name: s, + data: points.map((p, i) => ({ + x: i, + y: p, + })), + })), + }, + }; + }, [args]); + + if (!shown) { + settings.set({plugins: [D3Plugin]}); + return ; + } + + return ( +
+ +
+ ); +}; + +export const PerformanceIssueScatter: StoryObj = { + name: 'Performance issue', + args: { + pointsCount: 1000, + seriesCount: 10, + }, + argTypes: { + pointsCount: { + control: 'number', + }, + }, +}; + +export default { + title: 'Plugins/D3/Bar-X', + component: ChartStory, +}; diff --git a/src/plugins/d3/__stories__/line/PerformanceIssue.stories.tsx b/src/plugins/d3/__stories__/line/PerformanceIssue.stories.tsx new file mode 100644 index 00000000..15ccf443 --- /dev/null +++ b/src/plugins/d3/__stories__/line/PerformanceIssue.stories.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import {StoryObj} from '@storybook/react'; +import {action} from '@storybook/addon-actions'; +import {Button} from '@gravity-ui/uikit'; +import {settings} from '../../../../libs'; +import {ChartKit} from '../../../../components/ChartKit'; +import type {ChartKitRef, ChartKitWidgetData} from '../../../../types'; +import {D3Plugin} from '../..'; +import {randomNormal} from 'd3'; +import {randomString} from '../../../../utils'; + +const randomFn = randomNormal(0, 10); +const randomStr = () => randomString(Math.random() * 10, 'absdEFGHIJklmnopqrsTUvWxyz'); + +const ChartStory = (args: {pointsCount: number; seriesCount: number}) => { + const [shown, setShown] = React.useState(false); + const chartkitRef = React.useRef(); + + const widgetData: ChartKitWidgetData = React.useMemo(() => { + const points = Array.from({length: args.pointsCount}).map(() => Math.abs(randomFn())); + const series = Array.from({length: args.seriesCount}).map(randomStr); + + return { + series: { + data: series.map((s) => ({ + type: 'line', + name: s, + data: points.map((_, i) => ({ + x: i, + y: randomFn(), + })), + })), + }, + }; + }, [args]); + + if (!shown) { + settings.set({plugins: [D3Plugin]}); + return ; + } + + return ( +
+ +
+ ); +}; + +export const PerformanceIssueScatter: StoryObj = { + name: 'Performance issue', + args: { + pointsCount: 5000, + seriesCount: 2, + }, + argTypes: { + pointsCount: { + control: 'number', + }, + }, +}; + +export default { + title: 'Plugins/D3/Line', + component: ChartStory, +}; diff --git a/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx b/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx index df34f94b..2a4c1db2 100644 --- a/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx +++ b/src/plugins/d3/__stories__/scatter/PerformanceIssue.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Meta, Story} from '@storybook/react'; +import {StoryObj} from '@storybook/react'; import {action} from '@storybook/addon-actions'; import {Button} from '@gravity-ui/uikit'; import {settings} from '../../../../libs'; @@ -7,14 +7,18 @@ import {ChartKit} from '../../../../components/ChartKit'; import type {ChartKitRef, ChartKitWidgetData} from '../../../../types'; import {D3Plugin} from '../..'; import {randomNormal} from 'd3'; +import {randomString} from '../../../../utils'; -const Template: Story = () => { +const randomFn = randomNormal(0, 10); +const randomStr = () => randomString(Math.random() * 10, 'absdEFGHIJklmnopqrsTUvWxyz'); + +const ChartStory = (args: {categoriesCount: number; seriesCount: number}) => { const [shown, setShown] = React.useState(false); const chartkitRef = React.useRef(); const widgetData: ChartKitWidgetData = React.useMemo(() => { - const categories = Array.from({length: 5000}).map((_, i) => String(i)); - const randomFn = randomNormal(0, 10); + const categories = Array.from({length: args.categoriesCount}).map(randomStr); + const series = Array.from({length: args.seriesCount}).map(randomStr); return { xAxis: { @@ -22,19 +26,17 @@ const Template: Story = () => { categories: categories, }, series: { - data: [ - { - type: 'scatter', - name: 'Series 1', - data: categories.map((_, i) => ({ - x: i, - y: randomFn(), - })), - }, - ], + data: series.map((s) => ({ + type: 'scatter', + name: s, + data: categories.map((_, i) => ({ + x: i, + y: randomFn(), + })), + })), }, }; - }, []); + }, [args]); if (!shown) { settings.set({plugins: [D3Plugin]}); @@ -43,15 +45,32 @@ const Template: Story = () => { return (
- +
); }; -export const PerformanceIssue = Template.bind({}); +export const PerformanceIssueScatter: StoryObj = { + name: 'Performance issue', + args: { + categoriesCount: 5000, + seriesCount: 2, + }, + argTypes: { + categoriesCount: { + control: 'number', + }, + }, +}; -const meta: Meta = { +export default { title: 'Plugins/D3/Scatter', + component: ChartStory, }; - -export default meta; diff --git a/src/plugins/d3/renderer/D3Widget.tsx b/src/plugins/d3/renderer/D3Widget.tsx index acaed1d8..9277f7ce 100644 --- a/src/plugins/d3/renderer/D3Widget.tsx +++ b/src/plugins/d3/renderer/D3Widget.tsx @@ -2,10 +2,10 @@ import React from 'react'; import {select} from 'd3'; import debounce from 'lodash/debounce'; import type {DebouncedFunc} from 'lodash'; +import afterFrame from 'afterframe'; import type {ChartKitProps, ChartKitWidgetRef} from '../../../types'; -import {getRandomCKId} from '../../../utils'; - +import {getRandomCKId, measurePerformance} from '../../../utils'; import {Chart} from './components'; type ChartDimensions = { @@ -15,21 +15,38 @@ type ChartDimensions = { const D3Widget = React.forwardRef>( function D3Widget(props, forwardedRef) { - const {data, onLoad, onRender} = props; + const {data, onLoad, onRender, onChartLoad} = props; const ref = React.useRef(null); const debounced = React.useRef void> | undefined>(); const [dimensions, setDimensions] = React.useState>(); + const performanceMeasure = React.useRef | null>( + measurePerformance(), + ); - //FIXME: add chartPerfomance data to callbacks; React.useLayoutEffect(() => { - if (onLoad) { - onLoad({}); + if (onChartLoad) { + onChartLoad({}); } + }, [onChartLoad]); - if (onRender) { - onRender({}); + React.useLayoutEffect(() => { + if (dimensions?.width) { + if (!performanceMeasure.current) { + performanceMeasure.current = measurePerformance(); + } + + afterFrame(() => { + const renderTime = performanceMeasure.current?.end(); + onRender?.({ + renderTime, + }); + onLoad?.({ + widgetRendering: renderTime, + }); + performanceMeasure.current = null; + }); } - }, []); + }, [data, onRender, onLoad, dimensions]); const handleResize = React.useCallback(() => { const parentElement = ref.current?.parentElement; diff --git a/src/utils/index.ts b/src/utils/index.ts index 9dc051f6..5eec3262 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,3 @@ export {getRandomCKId, randomString} from './common'; export {typedMemo} from './react'; -export {getChartPerformanceDuration, markChartPerformance} from './performance'; +export * from './performance'; diff --git a/src/utils/performance.ts b/src/utils/performance.ts index 3d6e9efa..55dfb20a 100644 --- a/src/utils/performance.ts +++ b/src/utils/performance.ts @@ -15,3 +15,13 @@ export const getChartPerformanceDuration = (name: string) => { return undefined; }; + +export function measurePerformance() { + const timestamp = performance.now(); + + return { + end() { + return performance.now() - timestamp; + }, + }; +}