From 6a8d29f06becba5b06e4e99c1b59f2ef274db996 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Wed, 11 Mar 2020 20:04:22 +0800 Subject: [PATCH] perf: images loading performance optimized (#587) * remove unnecessary fields * performance: add cache to images * performance: add debounce to step slider * v2.0.0-beta.16 * style: better type reference * fix: better implementation of revalidation & minor bug fix * v2.0.0-beta.17 * feat: chinese translation adaptive * fix: select height limitation * chore: better error handling on revalidation * v2.0.0-beta.18 --- .../HighDimensionalChart.tsx | 30 ++++++++++--- frontend/components/Image.tsx | 5 ++- frontend/components/RadioGroup.tsx | 4 +- frontend/components/RunningToggle.tsx | 10 ++--- .../components/SamplesPage/SampleChart.tsx | 11 ++--- .../components/SamplesPage/StepSlider.tsx | 29 ++++++++++-- .../components/ScalarsPage/ScalarChart.tsx | 23 ++++++---- frontend/components/Select.tsx | 4 ++ frontend/hooks/useECharts.ts | 6 +-- frontend/hooks/useHeavyWork.ts | 6 +-- frontend/hooks/useRequest.ts | 45 ++++++++++++++++++- frontend/package.json | 2 +- frontend/pages/samples.tsx | 4 +- frontend/public/locales/zh/samples.json | 2 +- frontend/public/locales/zh/scalars.json | 17 +++---- visualdl/server/lib.py | 26 +---------- 16 files changed, 143 insertions(+), 81 deletions(-) diff --git a/frontend/components/HighDimensionalPage/HighDimensionalChart.tsx b/frontend/components/HighDimensionalPage/HighDimensionalChart.tsx index e7dfe4b1..ae123011 100644 --- a/frontend/components/HighDimensionalPage/HighDimensionalChart.tsx +++ b/frontend/components/HighDimensionalPage/HighDimensionalChart.tsx @@ -2,7 +2,8 @@ import React, {FunctionComponent, useMemo} from 'react'; import styled from 'styled-components'; import queryString from 'query-string'; import {rem, primaryColor} from '~/utils/style'; -import useRequest from '~/hooks/useRequest'; +import {useTranslation} from '~/utils/i18n'; +import {useRunningRequest} from '~/hooks/useRequest'; import useHeavyWork from '~/hooks/useHeavyWork'; import {divide, Dimension, Reduction, DivideParams, Point} from '~/resource/high-dimensional'; import ScatterChart from '~/components/ScatterChart'; @@ -24,6 +25,14 @@ const StyledScatterChart = styled(ScatterChart)` height: ${height}; `; +const Empty = styled.div` + display: flex; + justify-content: center; + align-items: center; + font-size: ${rem(20)}; + height: ${height}; +`; + const label = { show: true, position: 'top', @@ -52,16 +61,15 @@ const HighDimensionalChart: FunctionComponent = ({ reduction, dimension }) => { - const {data, error} = useRequest( + const {t} = useTranslation('common'); + + const {data, error, loading} = useRunningRequest( `/embeddings/embedding?${queryString.stringify({ run: run ?? '', dimension: Number.parseInt(dimension), reduction })}`, - undefined, - { - refreshInterval: running ? 15 * 1000 : 0 - } + !!running ); const divideParams = useMemo( @@ -91,7 +99,15 @@ const HighDimensionalChart: FunctionComponent = ({ ]; }, [points]); - return ; + if (!data && error) { + return {t('error')}; + } + + if (!data && !loading) { + return {t('empty')}; + } + + return ; }; export default HighDimensionalChart; diff --git a/frontend/components/Image.tsx b/frontend/components/Image.tsx index a74cb83d..28954c48 100644 --- a/frontend/components/Image.tsx +++ b/frontend/components/Image.tsx @@ -14,7 +14,10 @@ const Image: FunctionComponent = ({src}) => { const [url, setUrl] = useState(''); - const {data, error, loading} = useRequest(src ?? null, blobFetcher); + const {data, error, loading} = useRequest(src ?? null, blobFetcher, { + // cache image for 5 minutes + dedupingInterval: 5 * 60 * 1000 + }); // use useLayoutEffect hook to prevent image render after url revoked useLayoutEffect(() => { diff --git a/frontend/components/RadioGroup.tsx b/frontend/components/RadioGroup.tsx index 3577d6a3..5fa4e393 100644 --- a/frontend/components/RadioGroup.tsx +++ b/frontend/components/RadioGroup.tsx @@ -11,9 +11,9 @@ const Wrapper = styled.div` } `; -export const ValueContext = createContext(null as string | number | symbol | undefined | null); +export const ValueContext = createContext(null); // eslint-disable-next-line @typescript-eslint/no-empty-function -export const EventContext = createContext((() => {}) as ((value: string | number | symbol) => unknown) | undefined); +export const EventContext = createContext<((value: string | number | symbol) => unknown) | undefined>(() => {}); type RadioGroupProps = { value?: string | number | symbol; diff --git a/frontend/components/RunningToggle.tsx b/frontend/components/RunningToggle.tsx index 56b50ece..c8b52db0 100644 --- a/frontend/components/RunningToggle.tsx +++ b/frontend/components/RunningToggle.tsx @@ -1,4 +1,4 @@ -import React, {FunctionComponent, useState, useCallback} from 'react'; +import React, {FunctionComponent, useState, useEffect} from 'react'; import styled from 'styled-components'; import {rem} from '~/utils/style'; import {useTranslation} from '~/utils/i18n'; @@ -19,12 +19,12 @@ const RunningToggle: FunctionComponent = ({running, onToggle const {t} = useTranslation('common'); const [state, setState] = useState(!!running); - const onClick = useCallback(() => { - setState(s => !s); + + useEffect(() => { onToggle?.(state); - }, [state, onToggle]); + }, [onToggle, state]); - return {t(state ? 'running' : 'stopped')}; + return setState(s => !s)}>{t(state ? 'running' : 'stopped')}; }; export default RunningToggle; diff --git a/frontend/components/SamplesPage/SampleChart.tsx b/frontend/components/SamplesPage/SampleChart.tsx index 4028a61c..17bf3fd9 100644 --- a/frontend/components/SamplesPage/SampleChart.tsx +++ b/frontend/components/SamplesPage/SampleChart.tsx @@ -5,7 +5,7 @@ import isEmpty from 'lodash/isEmpty'; import GridLoader from 'react-spinners/GridLoader'; import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style'; import {useTranslation} from '~/utils/i18n'; -import useRequest from '~/hooks/useRequest'; +import {useRunningRequest} from '~/hooks/useRequest'; import Image from '~/components/Image'; import StepSlider from '~/components/SamplesPage/StepSlider'; @@ -85,12 +85,9 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number): const SampleChart: FunctionComponent = ({run, tag, fit, running}) => { const {t} = useTranslation('common'); - const {data, error, loading} = useRequest( + const {data, error, loading} = useRunningRequest( `/images/list?${queryString.stringify({run, tag})}`, - undefined, - { - refreshInterval: running ? 15 * 1000 : 0 - } + !!running ); const [step, setStep] = useState(0); @@ -99,7 +96,7 @@ const SampleChart: FunctionComponent = ({run, tag, fit, runnin if (loading) { return ; } - if (error) { + if (!data && error) { return {t('error')}; } if (isEmpty(data)) { diff --git a/frontend/components/SamplesPage/StepSlider.tsx b/frontend/components/SamplesPage/StepSlider.tsx index a8dbf18d..5225857a 100644 --- a/frontend/components/SamplesPage/StepSlider.tsx +++ b/frontend/components/SamplesPage/StepSlider.tsx @@ -1,4 +1,4 @@ -import React, {FunctionComponent, useState, useEffect} from 'react'; +import React, {FunctionComponent, useState, useEffect, useCallback, useRef} from 'react'; import styled from 'styled-components'; import {em, textLightColor} from '~/utils/style'; import {useTranslation} from '~/utils/i18n'; @@ -23,7 +23,30 @@ type StepSliderProps = { const StepSlider: FunctionComponent = ({onChange, value, steps}) => { const {t} = useTranslation('samples'); const [step, setStep] = useState(value); - useEffect(() => setStep(value), [setStep, value]); + const timer = useRef(null); + + useEffect(() => setStep(value), [value]); + + const clearTimer = useCallback(() => { + if (timer.current) { + clearTimeout(timer.current); + timer.current = null; + } + }, []); + + // debounce for backend performance consideration + useEffect(() => { + timer.current = setTimeout(() => { + onChange?.(step); + }, 500); + return clearTimer; + }, [step, onChange, clearTimer]); + + // trigger immediately when mouse up + const changeComplate = useCallback(() => { + clearTimer(); + onChange?.(step); + }, [onChange, clearTimer, step]); return ( <> @@ -34,7 +57,7 @@ const StepSlider: FunctionComponent = ({onChange, value, steps} step={1} value={step} onChange={setStep} - onChangeComplete={() => onChange?.(step)} + onChangeComplete={changeComplate} /> ); diff --git a/frontend/components/ScalarsPage/ScalarChart.tsx b/frontend/components/ScalarsPage/ScalarChart.tsx index 9aea447f..c5dfda6f 100644 --- a/frontend/components/ScalarsPage/ScalarChart.tsx +++ b/frontend/components/ScalarsPage/ScalarChart.tsx @@ -4,7 +4,7 @@ import queryString from 'query-string'; import {EChartOption} from 'echarts'; import {em, size} from '~/utils/style'; import {useTranslation} from '~/utils/i18n'; -import useRequest from '~/hooks/useRequest'; +import {useRunningRequest} from '~/hooks/useRequest'; import useHeavyWork from '~/hooks/useHeavyWork'; import {cycleFetcher} from '~/utils/fetch'; import { @@ -44,6 +44,13 @@ const StyledLineChart = styled(LineChart)` ${size(height, width)} `; +const Error = styled.div` + ${size(height, width)} + display: flex; + justify-content: center; + align-items: center; +`; + type ScalarChartProps = { runs: string[]; tag: string; @@ -65,13 +72,10 @@ const ScalarChart: FunctionComponent = ({ }) => { const {t, i18n} = useTranslation(['scalars', 'common']); - // TODO: maybe we can create a custom hook here - const {data: datasets, error, loading} = useRequest( + const {data: datasets, error, loading} = useRunningRequest( runs.map(run => `/scalars/list?${queryString.stringify({run, tag})}`), - (...urls) => cycleFetcher(urls), - { - refreshInterval: running ? 15 * 1000 : 0 - } + !!running, + (...urls) => cycleFetcher(urls) ); const type = xAxis === 'wall' ? 'time' : 'value'; @@ -142,8 +146,9 @@ const ScalarChart: FunctionComponent = ({ [smoothedDatasets, runs, sortingMethod, i18n] ); - if (error) { - return {t('common:error')}; + // display error only on first fetch + if (!data && error) { + return {t('common:error')}; } return ( diff --git a/frontend/components/Select.tsx b/frontend/components/Select.tsx index bec44796..b99f0803 100644 --- a/frontend/components/Select.tsx +++ b/frontend/components/Select.tsx @@ -17,6 +17,7 @@ import { easing, ellipsis, transitions, + math, css } from '~/utils/style'; import Checkbox from '~/components/Checkbox'; @@ -76,6 +77,9 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>` position: absolute; top: 100%; width: calc(100% + 2px); + max-height: ${math(`4.35 * ${height} + 2 * ${padding}`)}; + overflow-x: hidden; + overflow-y: auto; left: -1px; padding: ${padding} 0; border: inherit; diff --git a/frontend/hooks/useECharts.ts b/frontend/hooks/useECharts.ts index b6129ba3..1ea2098c 100644 --- a/frontend/hooks/useECharts.ts +++ b/frontend/hooks/useECharts.ts @@ -10,9 +10,9 @@ const useECharts = (options: { ref: MutableRefObject; echart: MutableRefObject | null; } => { - const ref = useRef(null as T | null); - const echartInstance = useRef(null as ECharts | null); - const [echart, setEchart] = useState(null as typeof echartInstance | null); + const ref = useRef(null); + const echartInstance = useRef(null); + const [echart, setEchart] = useState(null); const createChart = useCallback(() => { (async () => { diff --git a/frontend/hooks/useHeavyWork.ts b/frontend/hooks/useHeavyWork.ts index bd96a16d..685681a3 100644 --- a/frontend/hooks/useHeavyWork.ts +++ b/frontend/hooks/useHeavyWork.ts @@ -6,10 +6,10 @@ const useHeavyWork = ( fallback: ((arg: P) => T) | null, params: P ) => { - const wasm = useRef(null as Promise<(arg: P) => T> | null); - const worker = useRef(null as Worker | null); + const wasm = useRef>>(null); + const worker = useRef>>(null); - const [result, setResult] = useState(undefined as T | undefined); + const [result, setResult] = useState(undefined); const runFallback = useCallback((p: P) => fallback && setResult(fallback(p)), [fallback]); diff --git a/frontend/hooks/useRequest.ts b/frontend/hooks/useRequest.ts index 5f82591e..efc03ae9 100644 --- a/frontend/hooks/useRequest.ts +++ b/frontend/hooks/useRequest.ts @@ -1,4 +1,4 @@ -import {useMemo} from 'react'; +import {useMemo, useEffect} from 'react'; import useSWR, {responseInterface, keyInterface, ConfigInterface} from 'swr'; import {fetcherFn} from 'swr/dist/types'; @@ -11,7 +11,7 @@ function useRequest(key: keyInterface, fetcher?: fetch function useRequest( key: keyInterface, fetcher?: fetcherFn, - config?: ConfigInterface + config?: ConfigInterface> ): Response; function useRequest( key: keyInterface, @@ -23,4 +23,45 @@ function useRequest( return {data, error, loading, ...other}; } +function useRunningRequest(key: keyInterface, running: boolean): Response; +function useRunningRequest( + key: keyInterface, + running: boolean, + fetcher?: fetcherFn +): Response; +function useRunningRequest( + key: keyInterface, + running: boolean, + fetcher?: fetcherFn, + config?: Omit>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'> +): Response; +function useRunningRequest( + key: keyInterface, + running: boolean, + fetcher?: fetcherFn, + config?: Omit>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'> +) { + const c = useMemo>>( + () => ({ + ...config, + refreshInterval: running ? 15 * 1000 : 0, + dedupingInterval: 15 * 1000, + errorRetryInterval: 15 * 1000 + }), + [running, config] + ); + + const {mutate, ...others} = useRequest(key, fetcher, c); + + // revalidate immediately when running is set to true + useEffect(() => { + if (running) { + mutate(); + } + }, [running, mutate]); + + return {mutate, ...others}; +} + export default useRequest; +export {useRunningRequest}; diff --git a/frontend/package.json b/frontend/package.json index 9042803c..2c616a45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "visualdl", - "version": "2.0.0-beta.15", + "version": "2.0.0-beta.18", "title": "VisualDL", "description": "A platform to visualize the deep learning process and result.", "keywords": [ diff --git a/frontend/pages/samples.tsx b/frontend/pages/samples.tsx index 752c3d70..17882564 100644 --- a/frontend/pages/samples.tsx +++ b/frontend/pages/samples.tsx @@ -46,13 +46,13 @@ const Samples: NextI18NextPage = () => { ); const ungroupedSelectedTags = useMemo( () => - selectedTags.reduce((prev, {runs, ...item}) => { + selectedTags.reduce((prev, {runs, ...item}) => { Array.prototype.push.apply( prev, runs.map(run => ({...item, run})) ); return prev; - }, [] as Item[]), + }, []), [selectedTags] ); diff --git a/frontend/public/locales/zh/samples.json b/frontend/public/locales/zh/samples.json index 999f4e52..462d53c1 100644 --- a/frontend/public/locales/zh/samples.json +++ b/frontend/public/locales/zh/samples.json @@ -3,5 +3,5 @@ "audio": "音频", "text": "文本", "show-actual-size": "按真实大小展示", - "step": "步" + "step": "Step" } diff --git a/frontend/public/locales/zh/scalars.json b/frontend/public/locales/zh/scalars.json index d748fc36..b3563107 100644 --- a/frontend/public/locales/zh/scalars.json +++ b/frontend/public/locales/zh/scalars.json @@ -1,12 +1,12 @@ { "smoothing": "平滑度", - "value": "值", - "smoothed": "平滑值", + "value": "Value", + "smoothed": "Smoothed", "x-axis": "X轴", "x-axis-value": { - "step": "步", - "relative": "相对值", - "wall": "时间" + "step": "Step", + "relative": "Relative", + "wall": "Wall Time" }, "tooltip-sorting": "标签排序方法", "tooltip-sorting-value": { @@ -15,10 +15,5 @@ "ascending": "升序", "nearest": "最近" }, - "ignore-outliers": "图表缩放时忽略极端值", - "mode": "模式", - "mode-value": { - "overlay": "概览", - "offset": "偏移" - } + "ignore-outliers": "图表缩放时忽略极端值" } diff --git a/visualdl/server/lib.py b/visualdl/server/lib.py index ab054d05..8fe4f411 100644 --- a/visualdl/server/lib.py +++ b/visualdl/server/lib.py @@ -103,7 +103,6 @@ def get_image_tag_steps(storage, mode, tag): # remove suffix '/x' res = re.search(r".*/([0-9]+$)", tag) sample_index = 0 - origin_tag = tag if res: tag = tag[:tag.rfind('/')] sample_index = int(res.groups()[0]) @@ -114,23 +113,10 @@ def get_image_tag_steps(storage, mode, tag): for step_index in range(image.num_records()): record = image.record(step_index, sample_index) - shape = record.shape() - # TODO(ChunweiYan) remove this trick, some shape will be empty - if not shape: - continue try: - query = urlencode({ - 'sample': 0, - 'index': step_index, - 'tag': origin_tag, - 'run': mode, - }) res.append({ - 'height': shape[0], - 'width': shape[1], 'step': record.step_id(), - 'wall_time': image.timestamp(step_index), - 'query': query, + 'wallTime': image.timestamp(step_index), }) except Exception: logger.error("image sample out of range") @@ -184,7 +170,6 @@ def get_audio_tag_steps(storage, mode, tag): # remove suffix '/x' res = re.search(r".*/([0-9]+$)", tag) sample_index = 0 - origin_tag = tag if res: tag = tag[:tag.rfind('/')] sample_index = int(res.groups()[0]) @@ -196,16 +181,9 @@ def get_audio_tag_steps(storage, mode, tag): for step_index in range(audio.num_records()): record = audio.record(step_index, sample_index) - query = urlencode({ - 'sample': 0, - 'index': step_index, - 'tag': origin_tag, - 'run': mode, - }) res.append({ 'step': record.step_id(), - 'wall_time': audio.timestamp(step_index), - 'query': query, + 'wallTime': audio.timestamp(step_index), }) return res -- GitLab