Skip to content

Commit a3b130f

Browse files
perf(hparams): keep the live pane cheap and quiet between refreshes
A refresh replaces the window content every second, and every derived value in the pane keyed off those new array identities: buildColumns and the whole filter/spec chain re-ran each tick, the Metrics tab re-parsed every selected run history, and Plotly redrew. useKeyedMemo holds the previous value while a content key is unchanged, so the column set, the colour index and the fetched histories keep their identity when nothing about them actually changed. The Metrics tab also revalidates quietly now. A background re-fetch keeps the current curves on screen instead of flashing the loading overlay every tick, and a failed one keeps them too, reporting the failure in the toolbar rather than replacing a working chart. An explicit Refresh still shows the overlay, since the user asked. Fixes the updated marker under continuous logging: it is keyed by content id so the animation restarts per update, and fades out only, so a run logging every second reads as steady rather than blinking. Recolours it to the running-status blue; the green it used appeared nowhere else in the pane.
1 parent 9d87b3d commit a3b130f

5 files changed

Lines changed: 105 additions & 29 deletions

File tree

js/panes/HParamsPane.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
filterRecords,
3232
} from './hparams/hparamsUtils';
3333
import useHParamsColumns from './hparams/useHParamsColumns';
34+
import useKeyedMemo, { listKey } from './hparams/useKeyedMemo';
3435
import Pane from './Pane';
3536

3637
const VIEWS = [
@@ -107,9 +108,15 @@ var HParamsPane = (props) => {
107108
}, [props.contentID]);
108109

109110
const records = data ? data.records : NO_RECORDS;
110-
const paramKeys = data ? data.paramKeys : NO_KEYS;
111-
const metricKeys = data ? data.metricKeys : NO_KEYS;
112-
const tagKeys = data ? data.tagKeys : NO_KEYS;
111+
const nextParamKeys = data ? data.paramKeys : NO_KEYS;
112+
const nextMetricKeys = data ? data.metricKeys : NO_KEYS;
113+
const nextTagKeys = data ? data.tagKeys : NO_KEYS;
114+
const paramKeys = useKeyedMemo(() => nextParamKeys, listKey(nextParamKeys));
115+
const metricKeys = useKeyedMemo(
116+
() => nextMetricKeys,
117+
listKey(nextMetricKeys)
118+
);
119+
const tagKeys = useKeyedMemo(() => nextTagKeys, listKey(nextTagKeys));
113120
const columns = useHParamsColumns(paramKeys, metricKeys, tagKeys);
114121
const specs = useMemo(
115122
() => buildFilterSpecs(records, columns),
@@ -325,7 +332,12 @@ var HParamsPane = (props) => {
325332
</span>
326333
) : null}
327334
{justUpdated ? (
328-
<span className="hparams-stat hparams-stat-live">updated</span>
335+
<span
336+
key={props.contentID}
337+
className="hparams-stat hparams-stat-live"
338+
>
339+
updated
340+
</span>
329341
) : null}
330342
</div>
331343
<div className="hparams-views">

js/panes/hparams/HParamsMetrics.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './hparamsPlot';
2121
import { selectMetricSeries } from './hparamsUtils';
2222
import useExperimentMetrics from './useExperimentMetrics';
23+
import useKeyedMemo, { listKey } from './useKeyedMemo';
2324

2425
const MAX_MISSING_NAMED = 3;
2526

@@ -39,13 +40,16 @@ var HParamsMetrics = (props) => {
3940
cacheRef
4041
);
4142

42-
const colorIndex = useMemo(() => {
43-
const index = new Map();
44-
(columnRecords || []).forEach((record, i) => {
45-
if (record && record.env_id) index.set(record.env_id, i);
46-
});
47-
return index;
48-
}, [columnRecords]);
43+
const colorIndex = useKeyedMemo(
44+
() => {
45+
const index = new Map();
46+
(columnRecords || []).forEach((record, i) => {
47+
if (record && record.env_id) index.set(record.env_id, i);
48+
});
49+
return index;
50+
},
51+
listKey((columnRecords || []).map((record) => record && record.env_id))
52+
);
4953

5054
const activeMetric = useMemo(() => {
5155
if (metric && metricKeys.indexOf(metric) > -1) return metric;
@@ -124,7 +128,7 @@ var HParamsMetrics = (props) => {
124128
);
125129
}
126130

127-
if (status === 'error') {
131+
if (status === 'error' && runs.length === 0) {
128132
return (
129133
<HParamsMessage wrapClass="hparams-metrics-wrap" tone="error">
130134
{error}{' '}
@@ -165,9 +169,17 @@ var HParamsMetrics = (props) => {
165169
</select>
166170
</label>
167171
<span className="hparams-plot-note">
168-
{plotted.length} of {records.length}{' '}
169-
{records.length === 1 ? 'run' : 'runs'}
170-
{note ? ' · ' + note : ''}
172+
{status === 'error' ? (
173+
<span className="hparams-plot-stale">
174+
{error} Showing the last history loaded.
175+
</span>
176+
) : (
177+
<>
178+
{plotted.length} of {records.length}{' '}
179+
{records.length === 1 ? 'run' : 'runs'}
180+
{note ? ' · ' + note : ''}
181+
</>
182+
)}
171183
</span>
172184
<button type="button" className="hparams-link-btn" onClick={refresh}>
173185
Refresh

js/panes/hparams/useExperimentMetrics.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
*
88
*/
99

10-
import { useCallback, useEffect, useMemo, useState } from 'react';
10+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1111

1212
import { fetchExperimentComparison } from '../../api/experimentsApi';
1313
import { buildMetricSeries } from './hparamsUtils';
1414

1515
const NO_EXPERIMENTS = [];
1616

17+
function sameExperiments(a, b) {
18+
return a.length === b.length && a.every((exp, i) => exp === b[i]);
19+
}
20+
1721
export default function useExperimentMetrics(records, cacheRef) {
1822
const [nonce, setNonce] = useState(0);
23+
const askedRef = useRef(false);
1924
const [state, setState] = useState({
2025
status: 'idle',
2126
error: null,
@@ -38,13 +43,20 @@ export default function useExperimentMetrics(records, cacheRef) {
3843
const refresh = useCallback(() => {
3944
const cache = cacheRef.current;
4045
if (cache) envIds.forEach((id) => cache.delete(id));
46+
askedRef.current = true;
4147
setNonce((n) => n + 1);
4248
}, [cacheRef, envIds]);
4349

4450
useEffect(() => {
4551
const cache = cacheRef.current;
52+
const asked = askedRef.current;
53+
askedRef.current = false;
4654
if (envIds.length === 0) {
47-
setState({ status: 'idle', error: null, experiments: NO_EXPERIMENTS });
55+
setState((prev) =>
56+
prev.status === 'idle'
57+
? prev
58+
: { status: 'idle', error: null, experiments: NO_EXPERIMENTS }
59+
);
4860
return undefined;
4961
}
5062

@@ -58,15 +70,27 @@ export default function useExperimentMetrics(records, cacheRef) {
5870
.map((id) => cache.get(id))
5971
.filter(Boolean)
6072
.map((entry) => entry.experiment);
73+
const settle = (experiments) =>
74+
setState((prev) =>
75+
prev.status === 'ready' &&
76+
sameExperiments(prev.experiments, experiments)
77+
? prev
78+
: { status: 'ready', error: null, experiments }
79+
);
80+
6181
const wanted = envIds.filter((id) => !cache.has(id));
6282
if (wanted.length === 0) {
63-
setState({ status: 'ready', error: null, experiments: readCache() });
83+
settle(readCache());
6484
return undefined;
6585
}
6686

6787
let cancelled = false;
6888
const controller = new AbortController();
69-
setState((prev) => ({ ...prev, status: 'loading', error: null }));
89+
setState((prev) =>
90+
asked || prev.experiments.length === 0
91+
? { ...prev, status: 'loading', error: null }
92+
: prev
93+
);
7094

7195
fetchExperimentComparison(wanted, controller.signal)
7296
.then((reply) => {
@@ -80,15 +104,15 @@ export default function useExperimentMetrics(records, cacheRef) {
80104
});
81105
}
82106
});
83-
setState({ status: 'ready', error: null, experiments: readCache() });
107+
settle(readCache());
84108
})
85109
.catch((err) => {
86110
if (cancelled || (err && err.name === 'AbortError')) return;
87-
setState({
111+
setState((prev) => ({
88112
status: 'error',
89113
error: (err && err.message) || 'Could not load metric history.',
90-
experiments: NO_EXPERIMENTS,
91-
});
114+
experiments: prev.experiments,
115+
}));
92116
});
93117

94118
return () => {

js/panes/hparams/useKeyedMemo.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright 2017-present, The Visdom Authors
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
import { useRef } from 'react';
11+
12+
const SEPARATOR = '\u001f';
13+
14+
export function listKey(list) {
15+
return (list || []).join(SEPARATOR);
16+
}
17+
18+
export default function useKeyedMemo(factory, key) {
19+
const held = useRef(null);
20+
if (held.current === null || held.current.key !== key) {
21+
held.current = { key, value: factory() };
22+
}
23+
return held.current.value;
24+
}

py/visdom/static/css/hparams.css

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,10 @@
516516
pointer-events: none;
517517
}
518518

519+
.hparams-plot-stale {
520+
color: #b94a48;
521+
}
522+
519523
.hparams-plot-note {
520524
margin-left: auto;
521525
color: #888;
@@ -872,28 +876,28 @@
872876
}
873877

874878
/* A live refresh swaps the content in place, so nothing else on screen says it
875-
happened; the chip fades out on its own rather than sitting there blinking. */
879+
happened; the marker fades out on its own rather than sitting there blinking.
880+
The 4s here is paired with UPDATED_FLASH_MS in HParamsPane.js. */
876881
.hparams-stat-live {
877882
display: inline-flex;
878883
align-items: center;
879884
gap: 5px;
880-
color: #4a7a4a;
885+
color: #6389d8;
881886
animation: hparams-live-fade 4s ease-out forwards;
882887
}
883888

884889
.hparams-stat-live::before {
885890
content: '';
886891
width: 6px;
887892
height: 6px;
888-
background-color: #5ea75e;
893+
background-color: #6389d8;
889894
border-radius: 50%;
890895
}
891896

897+
/* Fades out only. A run logging every second remounts this element every
898+
second, and a fade-in would read as a blink on each restart. */
892899
@keyframes hparams-live-fade {
893900
0% {
894-
opacity: 0;
895-
}
896-
8% {
897901
opacity: 1;
898902
}
899903
70% {

0 commit comments

Comments
 (0)