Skip to content

Commit 1651088

Browse files
authored
Optimize plot re-rendering (#3337)
* Optimize rendering time of plots by making sure snap points do not trigger re-rendering * Use a type for snappoints * Optimize rendering of plots * Do not drill down vega plot content * Fix checkpoint plots * Add linting rule for TODOs * Made a few variables clearer
1 parent db50f42 commit 1651088

File tree

16 files changed

+285
-263
lines changed

16 files changed

+285
-263
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ module.exports = {
148148
// https://github.com/typescript-eslint/typescript-eslint/issues/2540#issuecomment-692505191
149149
'no-use-before-define': 'off',
150150
'no-void': ['error', { allowAsStatement: true }],
151+
'no-warning-comments': 'error',
151152
quotes: ['error', 'single', { avoidEscape: true }],
152153
'react-hooks/exhaustive-deps': 'error',
153154
'react-hooks/rules-of-hooks': 'error',

webview/src/plots/components/App.test.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ import {
2727
Revision,
2828
Section,
2929
TemplatePlotGroup,
30-
TemplatePlotsData,
31-
TemplatePlotSection
30+
TemplatePlotsData
3231
} from 'dvc/src/plots/webview/contract'
3332
import {
3433
MessageFromWebviewType,
@@ -41,8 +40,12 @@ import { VisualizationSpec } from 'react-vega'
4140
import { App } from './App'
4241
import { NewSectionBlock } from './templatePlots/TemplatePlots'
4342
import { SectionDescription } from './PlotsContainer'
44-
import { CheckpointPlotsById, plotDataStore } from './plotDataStore'
45-
import { setMaxPlotSize } from './webviewSlice'
43+
import {
44+
CheckpointPlotsById,
45+
plotDataStore,
46+
TemplatePlotsById
47+
} from './plotDataStore'
48+
import { setSnapPoints } from './webviewSlice'
4649
import { plotsReducers, plotsStore } from '../store'
4750
import { vsCodeApi } from '../../shared/api'
4851
import {
@@ -126,7 +129,7 @@ describe('App', () => {
126129

127130
const setWrapperSize = (store: typeof plotsStore) =>
128131
act(() => {
129-
store.dispatch(setMaxPlotSize(1000))
132+
store.dispatch(setSnapPoints(1000))
130133
})
131134

132135
const templatePlot = templatePlotsFixture.plots[0].entries[0]
@@ -201,8 +204,8 @@ describe('App', () => {
201204
jest
202205
.spyOn(HTMLElement.prototype, 'clientHeight', 'get')
203206
.mockImplementation(() => heightToSuppressVegaError)
204-
plotDataStore.checkpoint = {} as CheckpointPlotsById
205-
plotDataStore.template = [] as TemplatePlotSection[]
207+
plotDataStore[Section.CHECKPOINT_PLOTS] = {} as CheckpointPlotsById
208+
plotDataStore[Section.TEMPLATE_PLOTS] = {} as TemplatePlotsById
206209
})
207210

208211
afterEach(() => {
@@ -1427,7 +1430,7 @@ describe('App', () => {
14271430

14281431
const resizeScreen = (width: number, store: typeof plotsStore) => {
14291432
act(() => {
1430-
store.dispatch(setMaxPlotSize(width))
1433+
store.dispatch(setSnapPoints(width))
14311434
})
14321435
act(() => {
14331436
global.innerWidth = width

webview/src/plots/components/Plots.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CheckpointPlotsWrapper } from './checkpointPlots/CheckpointPlotsWrapper
66
import { TemplatePlotsWrapper } from './templatePlots/TemplatePlotsWrapper'
77
import { ComparisonTableWrapper } from './comparisonTable/ComparisonTableWrapper'
88
import { Ribbon } from './ribbon/Ribbon'
9-
import { setMaxPlotSize, setZoomedInPlot } from './webviewSlice'
9+
import { setSnapPoints, setZoomedInPlot } from './webviewSlice'
1010
import { EmptyState } from '../../shared/components/emptyState/EmptyState'
1111
import { Modal } from '../../shared/components/modal/Modal'
1212
import { WebviewWrapper } from '../../shared/components/webviewWrapper/WebviewWrapper'
@@ -38,7 +38,7 @@ const PlotsContent = () => {
3838
const onResize = () => {
3939
wrapperRef.current &&
4040
dispatch(
41-
setMaxPlotSize(wrapperRef.current.getBoundingClientRect().width - 100)
41+
setSnapPoints(wrapperRef.current.getBoundingClientRect().width - 100)
4242
)
4343
}
4444
window.addEventListener('resize', onResize)

webview/src/plots/components/ZoomablePlot.tsx

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,54 @@
1+
import { AnyAction } from '@reduxjs/toolkit'
12
import cx from 'classnames'
2-
import React, { useEffect, useRef, useState } from 'react'
3-
import { useDispatch } from 'react-redux'
4-
import { PlainObject, VisualizationSpec } from 'react-vega'
3+
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
4+
import { Section } from 'dvc/src/plots/webview/contract'
5+
import React, { useCallback, useEffect, useRef, useState } from 'react'
6+
import { useDispatch, useSelector } from 'react-redux'
7+
import { VisualizationSpec } from 'react-vega'
58
import { Renderers } from 'vega'
69
import VegaLite, { VegaLiteProps } from 'react-vega/lib/VegaLite'
710
import { setZoomedInPlot } from './webviewSlice'
811
import styles from './styles.module.scss'
912
import { Resizer } from './Resizer'
1013
import { config } from './constants'
14+
import { PlotsState } from '../store'
15+
import { useGetPlot } from '../hooks/useGetPlot'
1116
import { GripIcon } from '../../shared/components/dragDrop/GripIcon'
17+
import { sendMessage } from '../../shared/vscode'
1218

1319
interface ZoomablePlotProps {
14-
spec: VisualizationSpec
15-
data?: PlainObject
20+
spec?: VisualizationSpec
1621
id: string
1722
onViewReady?: () => void
18-
toggleDrag: (enabled: boolean) => void
19-
onResize: (diff: number) => void
20-
snapPoints: number[]
23+
toggleDrag: (enabled: boolean, id: string) => void
24+
changeSize: (size: number) => AnyAction
2125
currentSnapPoint: number
22-
size: number
26+
section: Section
27+
shouldNotResize?: boolean
2328
}
2429

2530
export const ZoomablePlot: React.FC<ZoomablePlotProps> = ({
26-
spec,
27-
data,
31+
spec: createdSpec,
2832
id,
2933
onViewReady,
3034
toggleDrag,
31-
onResize,
32-
snapPoints,
35+
changeSize,
3336
currentSnapPoint,
34-
size
37+
section,
38+
shouldNotResize
3539
}) => {
40+
const snapPoints = useSelector(
41+
(state: PlotsState) => state.webview.snapPoints
42+
)
43+
const { data, content: spec } = useGetPlot(section, id, createdSpec)
3644
const dispatch = useDispatch()
3745
const previousSpecsAndData = useRef(JSON.stringify({ data, spec }))
3846
const currentPlotProps = useRef<VegaLiteProps>()
3947
const clickDisabled = useRef(false)
4048
const enableClickTimeout = useRef(0)
4149
const [isExpanding, setIsExpanding] = useState(false)
4250
const newSpecsAndData = JSON.stringify({ data, spec })
51+
const size = snapPoints[currentSnapPoint - 1]
4352

4453
const plotProps: VegaLiteProps = {
4554
actions: false,
@@ -69,20 +78,34 @@ export const ZoomablePlot: React.FC<ZoomablePlotProps> = ({
6978
const handleOnClick = () =>
7079
!clickDisabled.current && dispatch(setZoomedInPlot({ id, plot: plotProps }))
7180

81+
const onResize = useCallback(
82+
(newSnapPoint: number) => {
83+
dispatch(changeSize(newSnapPoint))
84+
sendMessage({
85+
payload: { section, size: newSnapPoint },
86+
type: MessageFromWebviewType.RESIZE_PLOTS
87+
})
88+
},
89+
[dispatch, changeSize, section]
90+
)
91+
7292
const commonResizerProps = {
7393
onGrab: () => {
7494
clickDisabled.current = true
75-
toggleDrag(false)
95+
toggleDrag(false, id)
7696
},
7797
onRelease: () => {
78-
toggleDrag(true)
98+
toggleDrag(true, id)
7999
enableClickTimeout.current = window.setTimeout(
80100
() => (clickDisabled.current = false),
81101
0
82102
)
83103
},
84104
onResize
85105
}
106+
if (!data && !spec) {
107+
return null
108+
}
86109
return (
87110
<button
88111
className={cx(styles.zoomablePlot, {
@@ -94,7 +117,7 @@ export const ZoomablePlot: React.FC<ZoomablePlotProps> = ({
94117
{currentPlotProps.current && (
95118
<VegaLite {...plotProps} onNewView={onViewReady} />
96119
)}
97-
{snapPoints.length > 0 && (
120+
{!shouldNotResize && (
98121
<Resizer
99122
className={styles.plotVerticalResizer}
100123
{...commonResizerProps}

webview/src/plots/components/checkpointPlots/CheckpointPlot.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import styles from '../styles.module.scss'
88
import { withScale } from '../../../util/styles'
99
import { plotDataStore } from '../plotDataStore'
1010
import { PlotsState } from '../../store'
11-
import { useResize } from '../../hooks/useResize'
1211

1312
interface CheckpointPlotProps {
1413
id: string
@@ -23,12 +22,9 @@ export const CheckpointPlot: React.FC<CheckpointPlotProps> = ({
2322
const plotSnapshot = useSelector(
2423
(state: PlotsState) => state.checkpoint.plotsSnapshots[id]
2524
)
26-
const [plot, setPlot] = useState(plotDataStore.checkpoint[id])
25+
const [plot, setPlot] = useState(plotDataStore[Section.CHECKPOINT_PLOTS][id])
2726
const currentSize = useSelector((state: PlotsState) => state.checkpoint.size)
28-
const { onResize: handleResize, snapPoints } = useResize(
29-
Section.CHECKPOINT_PLOTS,
30-
changeSize
31-
)
27+
3228
const spec = useMemo(() => {
3329
const title = plot?.title
3430
if (!title) {
@@ -38,15 +34,13 @@ export const CheckpointPlot: React.FC<CheckpointPlotProps> = ({
3834
}, [plot?.title, colors])
3935

4036
useEffect(() => {
41-
setPlot(plotDataStore.checkpoint[id])
37+
setPlot(plotDataStore[Section.CHECKPOINT_PLOTS][id])
4238
}, [plotSnapshot, id])
4339

4440
if (!plot) {
4541
return null
4642
}
4743

48-
const { values } = plot
49-
5044
const key = `plot-${id}`
5145

5246
const toggleDrag = (enabled: boolean) => {
@@ -57,13 +51,11 @@ export const CheckpointPlot: React.FC<CheckpointPlotProps> = ({
5751
<div className={styles.plot} data-testid={key} id={id} style={withScale(1)}>
5852
<ZoomablePlot
5953
spec={spec}
60-
data={{ values }}
61-
id={key}
54+
id={id}
6255
toggleDrag={toggleDrag}
63-
onResize={handleResize}
64-
snapPoints={snapPoints}
56+
changeSize={changeSize}
6557
currentSnapPoint={currentSize}
66-
size={snapPoints[currentSize - 1]}
58+
section={Section.CHECKPOINT_PLOTS}
6759
/>
6860
</div>
6961
)

webview/src/plots/components/checkpointPlots/checkpointPlotsSlice.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
DEFAULT_SECTION_SIZES,
66
Section
77
} from 'dvc/src/plots/webview/contract'
8-
import {
9-
addCheckpointPlotsWithSnapshots,
10-
removeCheckpointPlots
11-
} from '../plotDataStore'
8+
import { addPlotsWithSnapshots, removePlots } from '../plotDataStore'
129
export interface CheckpointPlotsState
1310
extends Omit<CheckpointPlotsData, 'plots'> {
1411
isCollapsed: boolean
@@ -48,8 +45,8 @@ export const checkpointPlotsSlice = createSlice({
4845
}
4946
const { plots, ...statePayload } = action.payload
5047
const plotsIds = plots?.map(plot => plot.id) || []
51-
const snapShots = addCheckpointPlotsWithSnapshots(plots)
52-
removeCheckpointPlots(plotsIds)
48+
const snapShots = addPlotsWithSnapshots(plots, Section.CHECKPOINT_PLOTS)
49+
removePlots(plotsIds, Section.CHECKPOINT_PLOTS)
5350
return {
5451
...state,
5552
...statePayload,
Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
import {
22
CheckpointPlotData,
3-
TemplatePlotSection
3+
Section,
4+
TemplatePlotEntry
45
} from 'dvc/src/plots/webview/contract'
56

67
export type CheckpointPlotsById = { [key: string]: CheckpointPlotData }
8+
export type TemplatePlotsById = { [key: string]: TemplatePlotEntry }
79

810
export const plotDataStore = {
9-
checkpoint: {} as CheckpointPlotsById,
10-
template: [] as TemplatePlotSection[]
11+
[Section.CHECKPOINT_PLOTS]: {} as CheckpointPlotsById,
12+
[Section.TEMPLATE_PLOTS]: {} as TemplatePlotsById,
13+
[Section.COMPARISON_TABLE]: {} as CheckpointPlotsById // This category is unused but exists only to make typings easier
1114
}
1215

13-
export const addCheckpointPlotsWithSnapshots = (
14-
plots: CheckpointPlotData[]
16+
export const addPlotsWithSnapshots = (
17+
plots: (CheckpointPlotData | TemplatePlotEntry)[],
18+
section: Section
1519
) => {
1620
const snapShots: { [key: string]: string } = {}
1721
for (const plot of plots || []) {
18-
plotDataStore.checkpoint[plot.id] = plot
22+
plotDataStore[section][plot.id] = plot
1923
snapShots[plot.id] = JSON.stringify(plot)
2024
}
2125
return snapShots
2226
}
2327

24-
export const removeCheckpointPlots = (currentIds: string[]) => {
25-
for (const plotId of Object.keys(plotDataStore.checkpoint)) {
28+
export const removePlots = (currentIds: string[], section: Section) => {
29+
for (const plotId of Object.keys(plotDataStore[section])) {
2630
if (!currentIds.includes(plotId)) {
27-
delete plotDataStore.checkpoint[plotId]
31+
delete plotDataStore[section][plotId]
2832
}
2933
}
3034
}

webview/src/plots/components/templatePlots/AddedSection.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { DragEvent } from 'react'
22
import { useSelector } from 'react-redux'
33
import cx from 'classnames'
4-
import { TemplatePlotSection } from 'dvc/src/plots/webview/contract'
4+
import { PlotGroup } from './templatePlotsSlice'
55
import styles from '../styles.module.scss'
6-
import { getIDWithoutIndex } from '../../../util/ids'
76
import { PlotsState } from '../../store'
7+
import { getIDWithoutIndex } from '../../../util/ids'
88
import { Icon } from '../../../shared/components/Icon'
99
import { GraphLine } from '../../../shared/components/icons'
1010
import { useDeferedDragLeave } from '../../../shared/hooks/useDeferedDragLeave'
@@ -14,7 +14,7 @@ interface AddedSectionProps {
1414
hoveredSection: string
1515
setHoveredSection: (section: string) => void
1616
onDrop: (e: DragEvent<HTMLElement>) => void
17-
closestSection: TemplatePlotSection
17+
closestSection: PlotGroup
1818
acceptedGroups: string[]
1919
}
2020

0 commit comments

Comments
 (0)