未验证 提交 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';
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<HighDimensionalChartProps> = ({
reduction,
dimension
}) => {
const {data, error} = useRequest<Data>(
const {t} = useTranslation('common');
const {data, error, loading} = useRunningRequest<Data>(
`/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<HighDimensionalChartProps> = ({
];
}, [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;
......@@ -14,7 +14,10 @@ const Image: FunctionComponent<ImageProps> = ({src}) => {
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
useLayoutEffect(() => {
......
......@@ -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
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;
......
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<RunningToggleProps> = ({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 <StyledButton onClick={onClick}>{t(state ? 'running' : 'stopped')}</StyledButton>;
return <StyledButton onClick={() => setState(s => !s)}>{t(state ? 'running' : 'stopped')}</StyledButton>;
};
export default RunningToggle;
......@@ -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<SampleChartProps> = ({run, tag, fit, running}) => {
const {t} = useTranslation('common');
const {data, error, loading} = useRequest<ImageData[]>(
const {data, error, loading} = useRunningRequest<ImageData[]>(
`/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<SampleChartProps> = ({run, tag, fit, runnin
if (loading) {
return <GridLoader color={primaryColor} size="10px" />;
}
if (error) {
if (!data && error) {
return <span>{t('error')}</span>;
}
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 {em, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
......@@ -23,7 +23,30 @@ type StepSliderProps = {
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => {
const {t} = useTranslation('samples');
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 (
<>
......@@ -34,7 +57,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}
step={1}
value={step}
onChange={setStep}
onChangeComplete={() => onChange?.(step)}
onChangeComplete={changeComplate}
/>
</>
);
......
......@@ -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<ScalarChartProps> = ({
}) => {
const {t, i18n} = useTranslation(['scalars', 'common']);
// TODO: maybe we can create a custom hook here
const {data: datasets, error, loading} = useRequest<Dataset[]>(
const {data: datasets, error, loading} = useRunningRequest<Dataset[]>(
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<ScalarChartProps> = ({
[smoothedDatasets, runs, sortingMethod, i18n]
);
if (error) {
return <span>{t('common:error')}</span>;
// display error only on first fetch
if (!data && error) {
return <Error>{t('common:error')}</Error>;
}
return (
......
......@@ -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;
......
......@@ -10,9 +10,9 @@ const useECharts = <T extends HTMLElement>(options: {
ref: MutableRefObject<T | null>;
echart: MutableRefObject<ECharts | null> | 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<T | null>(null);
const echartInstance = useRef<ECharts | null>(null);
const [echart, setEchart] = useState<typeof echartInstance | null>(null);
const createChart = useCallback(() => {
(async () => {
......
......@@ -6,10 +6,10 @@ const useHeavyWork = <T = unknown, P = unknown>(
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<ReturnType<NonNullable<typeof createWasm>>>(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]);
......
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<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetch
function useRequest<D = unknown, E = unknown>(
key: keyInterface,
fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E>
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E>;
function useRequest<D = unknown, E = unknown>(
key: keyInterface,
......@@ -23,4 +23,45 @@ function useRequest<D = unknown, E = unknown>(
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 {useRunningRequest};
{
"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": [
......
......@@ -46,13 +46,13 @@ const Samples: NextI18NextPage = () => {
);
const ungroupedSelectedTags = useMemo(
() =>
selectedTags.reduce((prev, {runs, ...item}) => {
selectedTags.reduce<Item[]>((prev, {runs, ...item}) => {
Array.prototype.push.apply(
prev,
runs.map(run => ({...item, run}))
);
return prev;
}, [] as Item[]),
}, []),
[selectedTags]
);
......
......@@ -3,5 +3,5 @@
"audio": "音频",
"text": "文本",
"show-actual-size": "按真实大小展示",
"step": ""
"step": "Step"
}
{
"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": "图表缩放时忽略极端值"
}
......@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册