未验证 提交 6a8d29f0 编写于 作者: P Peter Pan 提交者: GitHub

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
上级 b5bc72f4
...@@ -2,7 +2,8 @@ import React, {FunctionComponent, useMemo} from 'react'; ...@@ -2,7 +2,8 @@ import React, {FunctionComponent, useMemo} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import queryString from 'query-string'; import queryString from 'query-string';
import {rem, primaryColor} from '~/utils/style'; 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 useHeavyWork from '~/hooks/useHeavyWork';
import {divide, Dimension, Reduction, DivideParams, Point} from '~/resource/high-dimensional'; import {divide, Dimension, Reduction, DivideParams, Point} from '~/resource/high-dimensional';
import ScatterChart from '~/components/ScatterChart'; import ScatterChart from '~/components/ScatterChart';
...@@ -24,6 +25,14 @@ const StyledScatterChart = styled(ScatterChart)` ...@@ -24,6 +25,14 @@ const StyledScatterChart = styled(ScatterChart)`
height: ${height}; height: ${height};
`; `;
const Empty = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
height: ${height};
`;
const label = { const label = {
show: true, show: true,
position: 'top', position: 'top',
...@@ -52,16 +61,15 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({ ...@@ -52,16 +61,15 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({
reduction, reduction,
dimension dimension
}) => { }) => {
const {data, error} = useRequest<Data>( const {t} = useTranslation('common');
const {data, error, loading} = useRunningRequest<Data>(
`/embeddings/embedding?${queryString.stringify({ `/embeddings/embedding?${queryString.stringify({
run: run ?? '', run: run ?? '',
dimension: Number.parseInt(dimension), dimension: Number.parseInt(dimension),
reduction reduction
})}`, })}`,
undefined, !!running
{
refreshInterval: running ? 15 * 1000 : 0
}
); );
const divideParams = useMemo( const divideParams = useMemo(
...@@ -91,7 +99,15 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({ ...@@ -91,7 +99,15 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({
]; ];
}, [points]); }, [points]);
return <StyledScatterChart loading={!data && !error} data={chartData} gl={dimension === '3d'} />; if (!data && error) {
return <Empty>{t('error')}</Empty>;
}
if (!data && !loading) {
return <Empty>{t('empty')}</Empty>;
}
return <StyledScatterChart loading={loading} data={chartData} gl={dimension === '3d'} />;
}; };
export default HighDimensionalChart; export default HighDimensionalChart;
...@@ -14,7 +14,10 @@ const Image: FunctionComponent<ImageProps> = ({src}) => { ...@@ -14,7 +14,10 @@ const Image: FunctionComponent<ImageProps> = ({src}) => {
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const {data, error, loading} = useRequest<Blob>(src ?? null, blobFetcher); const {data, error, loading} = useRequest<Blob>(src ?? null, blobFetcher, {
// cache image for 5 minutes
dedupingInterval: 5 * 60 * 1000
});
// use useLayoutEffect hook to prevent image render after url revoked // use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => { useLayoutEffect(() => {
......
...@@ -11,9 +11,9 @@ const Wrapper = styled.div` ...@@ -11,9 +11,9 @@ const Wrapper = styled.div`
} }
`; `;
export const ValueContext = createContext(null as string | number | symbol | undefined | null); export const ValueContext = createContext<string | number | symbol | undefined | null>(null);
// eslint-disable-next-line @typescript-eslint/no-empty-function // 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 = { type RadioGroupProps = {
value?: string | number | symbol; value?: string | number | symbol;
......
import React, {FunctionComponent, useState, useCallback} from 'react'; import React, {FunctionComponent, useState, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
...@@ -19,12 +19,12 @@ const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle ...@@ -19,12 +19,12 @@ const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const [state, setState] = useState(!!running); const [state, setState] = useState(!!running);
const onClick = useCallback(() => {
setState(s => !s); useEffect(() => {
onToggle?.(state); onToggle?.(state);
}, [state, onToggle]); }, [onToggle, state]);
return <StyledButton onClick={onClick}>{t(state ? 'running' : 'stopped')}</StyledButton>; return <StyledButton onClick={() => setState(s => !s)}>{t(state ? 'running' : 'stopped')}</StyledButton>;
}; };
export default RunningToggle; export default RunningToggle;
...@@ -5,7 +5,7 @@ import isEmpty from 'lodash/isEmpty'; ...@@ -5,7 +5,7 @@ import isEmpty from 'lodash/isEmpty';
import GridLoader from 'react-spinners/GridLoader'; import GridLoader from 'react-spinners/GridLoader';
import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style'; import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
import useRequest from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import Image from '~/components/Image'; import Image from '~/components/Image';
import StepSlider from '~/components/SamplesPage/StepSlider'; import StepSlider from '~/components/SamplesPage/StepSlider';
...@@ -85,12 +85,9 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number): ...@@ -85,12 +85,9 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number):
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => { const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const {data, error, loading} = useRequest<ImageData[]>( const {data, error, loading} = useRunningRequest<ImageData[]>(
`/images/list?${queryString.stringify({run, tag})}`, `/images/list?${queryString.stringify({run, tag})}`,
undefined, !!running
{
refreshInterval: running ? 15 * 1000 : 0
}
); );
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
...@@ -99,7 +96,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -99,7 +96,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
if (loading) { if (loading) {
return <GridLoader color={primaryColor} size="10px" />; return <GridLoader color={primaryColor} size="10px" />;
} }
if (error) { if (!data && error) {
return <span>{t('error')}</span>; return <span>{t('error')}</span>;
} }
if (isEmpty(data)) { if (isEmpty(data)) {
......
import React, {FunctionComponent, useState, useEffect} from 'react'; import React, {FunctionComponent, useState, useEffect, useCallback, useRef} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {em, textLightColor} from '~/utils/style'; import {em, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
...@@ -23,7 +23,30 @@ type StepSliderProps = { ...@@ -23,7 +23,30 @@ type StepSliderProps = {
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => { const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => {
const {t} = useTranslation('samples'); const {t} = useTranslation('samples');
const [step, setStep] = useState(value); const [step, setStep] = useState(value);
useEffect(() => setStep(value), [setStep, value]); const timer = useRef<NodeJS.Timeout | null>(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 ( return (
<> <>
...@@ -34,7 +57,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps} ...@@ -34,7 +57,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}
step={1} step={1}
value={step} value={step}
onChange={setStep} onChange={setStep}
onChangeComplete={() => onChange?.(step)} onChangeComplete={changeComplate}
/> />
</> </>
); );
......
...@@ -4,7 +4,7 @@ import queryString from 'query-string'; ...@@ -4,7 +4,7 @@ import queryString from 'query-string';
import {EChartOption} from 'echarts'; import {EChartOption} from 'echarts';
import {em, size} from '~/utils/style'; import {em, size} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
import useRequest from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import useHeavyWork from '~/hooks/useHeavyWork'; import useHeavyWork from '~/hooks/useHeavyWork';
import {cycleFetcher} from '~/utils/fetch'; import {cycleFetcher} from '~/utils/fetch';
import { import {
...@@ -44,6 +44,13 @@ const StyledLineChart = styled(LineChart)` ...@@ -44,6 +44,13 @@ const StyledLineChart = styled(LineChart)`
${size(height, width)} ${size(height, width)}
`; `;
const Error = styled.div`
${size(height, width)}
display: flex;
justify-content: center;
align-items: center;
`;
type ScalarChartProps = { type ScalarChartProps = {
runs: string[]; runs: string[];
tag: string; tag: string;
...@@ -65,13 +72,10 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -65,13 +72,10 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}) => { }) => {
const {t, i18n} = useTranslation(['scalars', 'common']); const {t, i18n} = useTranslation(['scalars', 'common']);
// TODO: maybe we can create a custom hook here const {data: datasets, error, loading} = useRunningRequest<Dataset[]>(
const {data: datasets, error, loading} = useRequest<Dataset[]>(
runs.map(run => `/scalars/list?${queryString.stringify({run, tag})}`), runs.map(run => `/scalars/list?${queryString.stringify({run, tag})}`),
(...urls) => cycleFetcher(urls), !!running,
{ (...urls) => cycleFetcher(urls)
refreshInterval: running ? 15 * 1000 : 0
}
); );
const type = xAxis === 'wall' ? 'time' : 'value'; const type = xAxis === 'wall' ? 'time' : 'value';
...@@ -142,8 +146,9 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -142,8 +146,9 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
[smoothedDatasets, runs, sortingMethod, i18n] [smoothedDatasets, runs, sortingMethod, i18n]
); );
if (error) { // display error only on first fetch
return <span>{t('common:error')}</span>; if (!data && error) {
return <Error>{t('common:error')}</Error>;
} }
return ( return (
......
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
easing, easing,
ellipsis, ellipsis,
transitions, transitions,
math,
css css
} from '~/utils/style'; } from '~/utils/style';
import Checkbox from '~/components/Checkbox'; import Checkbox from '~/components/Checkbox';
...@@ -76,6 +77,9 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>` ...@@ -76,6 +77,9 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>`
position: absolute; position: absolute;
top: 100%; top: 100%;
width: calc(100% + 2px); width: calc(100% + 2px);
max-height: ${math(`4.35 * ${height} + 2 * ${padding}`)};
overflow-x: hidden;
overflow-y: auto;
left: -1px; left: -1px;
padding: ${padding} 0; padding: ${padding} 0;
border: inherit; border: inherit;
......
...@@ -10,9 +10,9 @@ const useECharts = <T extends HTMLElement>(options: { ...@@ -10,9 +10,9 @@ const useECharts = <T extends HTMLElement>(options: {
ref: MutableRefObject<T | null>; ref: MutableRefObject<T | null>;
echart: MutableRefObject<ECharts | null> | null; echart: MutableRefObject<ECharts | null> | null;
} => { } => {
const ref = useRef(null as T | null); const ref = useRef<T | null>(null);
const echartInstance = useRef(null as ECharts | null); const echartInstance = useRef<ECharts | null>(null);
const [echart, setEchart] = useState(null as typeof echartInstance | null); const [echart, setEchart] = useState<typeof echartInstance | null>(null);
const createChart = useCallback(() => { const createChart = useCallback(() => {
(async () => { (async () => {
......
...@@ -6,10 +6,10 @@ const useHeavyWork = <T = unknown, P = unknown>( ...@@ -6,10 +6,10 @@ const useHeavyWork = <T = unknown, P = unknown>(
fallback: ((arg: P) => T) | null, fallback: ((arg: P) => T) | null,
params: P params: P
) => { ) => {
const wasm = useRef(null as Promise<(arg: P) => T> | null); const wasm = useRef<ReturnType<NonNullable<typeof createWasm>>>(null);
const worker = useRef(null as Worker | null); const worker = useRef<ReturnType<NonNullable<typeof createWorker>>>(null);
const [result, setResult] = useState(undefined as T | undefined); const [result, setResult] = useState<T | undefined>(undefined);
const runFallback = useCallback((p: P) => fallback && setResult(fallback(p)), [fallback]); const runFallback = useCallback((p: P) => fallback && setResult(fallback(p)), [fallback]);
......
import {useMemo} from 'react'; import {useMemo, useEffect} from 'react';
import useSWR, {responseInterface, keyInterface, ConfigInterface} from 'swr'; import useSWR, {responseInterface, keyInterface, ConfigInterface} from 'swr';
import {fetcherFn} from 'swr/dist/types'; import {fetcherFn} from 'swr/dist/types';
...@@ -11,7 +11,7 @@ function useRequest<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetch ...@@ -11,7 +11,7 @@ function useRequest<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetch
function useRequest<D = unknown, E = unknown>( function useRequest<D = unknown, E = unknown>(
key: keyInterface, key: keyInterface,
fetcher?: fetcherFn<D>, fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E> config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E>; ): Response<D, E>;
function useRequest<D = unknown, E = unknown>( function useRequest<D = unknown, E = unknown>(
key: keyInterface, key: keyInterface,
...@@ -23,4 +23,45 @@ function useRequest<D = unknown, E = unknown>( ...@@ -23,4 +23,45 @@ function useRequest<D = unknown, E = unknown>(
return {data, error, loading, ...other}; return {data, error, loading, ...other};
} }
function useRunningRequest<D = unknown, E = unknown>(key: keyInterface, running: boolean): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>
): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'>
): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'>
) {
const c = useMemo<ConfigInterface<D, E, fetcherFn<D>>>(
() => ({
...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 default useRequest;
export {useRunningRequest};
{ {
"name": "visualdl", "name": "visualdl",
"version": "2.0.0-beta.15", "version": "2.0.0-beta.18",
"title": "VisualDL", "title": "VisualDL",
"description": "A platform to visualize the deep learning process and result.", "description": "A platform to visualize the deep learning process and result.",
"keywords": [ "keywords": [
......
...@@ -46,13 +46,13 @@ const Samples: NextI18NextPage = () => { ...@@ -46,13 +46,13 @@ const Samples: NextI18NextPage = () => {
); );
const ungroupedSelectedTags = useMemo( const ungroupedSelectedTags = useMemo(
() => () =>
selectedTags.reduce((prev, {runs, ...item}) => { selectedTags.reduce<Item[]>((prev, {runs, ...item}) => {
Array.prototype.push.apply( Array.prototype.push.apply(
prev, prev,
runs.map(run => ({...item, run})) runs.map(run => ({...item, run}))
); );
return prev; return prev;
}, [] as Item[]), }, []),
[selectedTags] [selectedTags]
); );
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
"audio": "音频", "audio": "音频",
"text": "文本", "text": "文本",
"show-actual-size": "按真实大小展示", "show-actual-size": "按真实大小展示",
"step": "" "step": "Step"
} }
{ {
"smoothing": "平滑度", "smoothing": "平滑度",
"value": "", "value": "Value",
"smoothed": "平滑值", "smoothed": "Smoothed",
"x-axis": "X轴", "x-axis": "X轴",
"x-axis-value": { "x-axis-value": {
"step": "", "step": "Step",
"relative": "相对值", "relative": "Relative",
"wall": "时间" "wall": "Wall Time"
}, },
"tooltip-sorting": "标签排序方法", "tooltip-sorting": "标签排序方法",
"tooltip-sorting-value": { "tooltip-sorting-value": {
...@@ -15,10 +15,5 @@ ...@@ -15,10 +15,5 @@
"ascending": "升序", "ascending": "升序",
"nearest": "最近" "nearest": "最近"
}, },
"ignore-outliers": "图表缩放时忽略极端值", "ignore-outliers": "图表缩放时忽略极端值"
"mode": "模式",
"mode-value": {
"overlay": "概览",
"offset": "偏移"
}
} }
...@@ -103,7 +103,6 @@ def get_image_tag_steps(storage, mode, tag): ...@@ -103,7 +103,6 @@ def get_image_tag_steps(storage, mode, tag):
# remove suffix '/x' # remove suffix '/x'
res = re.search(r".*/([0-9]+$)", tag) res = re.search(r".*/([0-9]+$)", tag)
sample_index = 0 sample_index = 0
origin_tag = tag
if res: if res:
tag = tag[:tag.rfind('/')] tag = tag[:tag.rfind('/')]
sample_index = int(res.groups()[0]) sample_index = int(res.groups()[0])
...@@ -114,23 +113,10 @@ def get_image_tag_steps(storage, mode, tag): ...@@ -114,23 +113,10 @@ def get_image_tag_steps(storage, mode, tag):
for step_index in range(image.num_records()): for step_index in range(image.num_records()):
record = image.record(step_index, sample_index) 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: try:
query = urlencode({
'sample': 0,
'index': step_index,
'tag': origin_tag,
'run': mode,
})
res.append({ res.append({
'height': shape[0],
'width': shape[1],
'step': record.step_id(), 'step': record.step_id(),
'wall_time': image.timestamp(step_index), 'wallTime': image.timestamp(step_index),
'query': query,
}) })
except Exception: except Exception:
logger.error("image sample out of range") logger.error("image sample out of range")
...@@ -184,7 +170,6 @@ def get_audio_tag_steps(storage, mode, tag): ...@@ -184,7 +170,6 @@ def get_audio_tag_steps(storage, mode, tag):
# remove suffix '/x' # remove suffix '/x'
res = re.search(r".*/([0-9]+$)", tag) res = re.search(r".*/([0-9]+$)", tag)
sample_index = 0 sample_index = 0
origin_tag = tag
if res: if res:
tag = tag[:tag.rfind('/')] tag = tag[:tag.rfind('/')]
sample_index = int(res.groups()[0]) sample_index = int(res.groups()[0])
...@@ -196,16 +181,9 @@ def get_audio_tag_steps(storage, mode, tag): ...@@ -196,16 +181,9 @@ def get_audio_tag_steps(storage, mode, tag):
for step_index in range(audio.num_records()): for step_index in range(audio.num_records()):
record = audio.record(step_index, sample_index) record = audio.record(step_index, sample_index)
query = urlencode({
'sample': 0,
'index': step_index,
'tag': origin_tag,
'run': mode,
})
res.append({ res.append({
'step': record.step_id(), 'step': record.step_id(),
'wall_time': audio.timestamp(step_index), 'wallTime': audio.timestamp(step_index),
'query': query,
}) })
return res return res
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册