Skip to content

Commit 5e18132

Browse files
committed
stop making duplicate time series requests
1 parent 29e5352 commit 5e18132

File tree

2 files changed

+170
-27
lines changed

2 files changed

+170
-27
lines changed

tensorboard/webapp/metrics/effects/index.ts

+66-16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {Action, createAction, createSelector, Store} from '@ngrx/store';
1818
import {forkJoin, merge, Observable, of} from 'rxjs';
1919
import {
2020
catchError,
21+
combineLatestWith,
2122
filter,
2223
map,
2324
mergeMap,
@@ -67,6 +68,12 @@ const getCardFetchInfo = createSelector(
6768

6869
const initAction = createAction('[Metrics Effects] Init');
6970

71+
function parseRunIdFromSampledRunInfoName(eidRun: string): string {
72+
if (!eidRun) return '';
73+
const [, ...runIdChunks] = eidRun.split('/');
74+
return runIdChunks.join('/');
75+
}
76+
7077
@Injectable()
7178
export class MetricsEffects implements OnInitEffects {
7279
constructor(
@@ -75,6 +82,40 @@ export class MetricsEffects implements OnInitEffects {
7582
private readonly dataSource: MetricsDataSource
7683
) {}
7784

85+
readonly tagToEid$: Observable<Record<string, Set<string>>> = this.store
86+
.select(selectors.getMetricsTagMetadata)
87+
.pipe(
88+
combineLatestWith(this.store.select(selectors.getRunIdToExperimentId)),
89+
map(([tagMetadata, runToEid]) => {
90+
const imageTagToRuns = Object.fromEntries(
91+
Object.entries(tagMetadata.images.tagRunSampledInfo).map(
92+
([tag, sampledRunInfo]) => {
93+
const runIds = Object.keys(sampledRunInfo).map((runInfoKey) =>
94+
parseRunIdFromSampledRunInfoName(runInfoKey)
95+
);
96+
return [tag, runIds];
97+
}
98+
)
99+
);
100+
101+
const tagToEid: Record<string, Set<string>> = {};
102+
function mapTagsToEid(tagToRun: Record<string, readonly string[]>) {
103+
Object.entries(tagToRun).forEach(([tag, runIds]) => {
104+
if (!tagToEid[tag]) {
105+
tagToEid[tag] = new Set();
106+
}
107+
runIds.forEach((runId) => tagToEid[tag].add(runToEid[runId]));
108+
});
109+
}
110+
111+
mapTagsToEid(tagMetadata.scalars.tagToRuns);
112+
mapTagsToEid(tagMetadata.histograms.tagToRuns);
113+
mapTagsToEid(imageTagToRuns);
114+
115+
return tagToEid;
116+
})
117+
);
118+
78119
/** @export */
79120
ngrxOnInitEffects(): Action {
80121
return initAction();
@@ -193,23 +234,31 @@ export class MetricsEffects implements OnInitEffects {
193234
fetchInfos: CardFetchInfo[],
194235
experimentIds: string[]
195236
) {
196-
/**
197-
* TODO(psybuzz): if 2 cards require the same data, we should dedupe instead of
198-
* making 2 identical requests.
199-
*/
200-
const requests: TimeSeriesRequest[] = fetchInfos.map((fetchInfo) => {
201-
const {plugin, tag, runId, sample} = fetchInfo;
202-
const partialRequest: TimeSeriesRequest = isSingleRunPlugin(plugin)
203-
? {plugin, tag, runId: runId!}
204-
: {plugin, tag, experimentIds};
205-
if (sample !== undefined) {
206-
partialRequest.sample = sample;
207-
}
208-
return partialRequest;
209-
});
210-
211237
// Fetch and handle responses.
212-
return of(requests).pipe(
238+
return this.tagToEid$.pipe(
239+
map((tagToEid): TimeSeriesRequest[] => {
240+
const requests = fetchInfos.map((fetchInfo) => {
241+
const {plugin, tag, runId, sample} = fetchInfo;
242+
const filteredEids = experimentIds.filter((eid) =>
243+
tagToEid[tag]?.has(eid)
244+
);
245+
246+
const partialRequest: TimeSeriesRequest = isSingleRunPlugin(plugin)
247+
? {plugin, tag, runId: runId!}
248+
: {plugin, tag, experimentIds: filteredEids};
249+
if (sample !== undefined) {
250+
partialRequest.sample = sample;
251+
}
252+
return partialRequest;
253+
});
254+
const uniqueRequests = new Set(
255+
requests.map((request) => JSON.stringify(request))
256+
);
257+
258+
return Array.from(uniqueRequests).map(
259+
(serialized) => JSON.parse(serialized) as TimeSeriesRequest
260+
);
261+
}),
213262
tap((requests) => {
214263
this.store.dispatch(actions.multipleTimeSeriesRequested({requests}));
215264
}),
@@ -300,4 +349,5 @@ export class MetricsEffects implements OnInitEffects {
300349
export const TEST_ONLY = {
301350
getCardFetchInfo,
302351
initAction,
352+
parseRunIdFromSampledRunInfoName,
303353
};

tensorboard/webapp/metrics/effects/metrics_effects_test.ts

+104-11
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,52 @@ describe('metrics effects', () => {
8989
selectors.getMetricsTooltipSort,
9090
TooltipSort.ALPHABETICAL
9191
);
92+
93+
overrideTagMetadata();
94+
overrideRunToEid();
9295
});
9396

97+
function overrideTagMetadata() {
98+
store.overrideSelector(selectors.getMetricsTagMetadata, {
99+
scalars: {
100+
tagDescriptions: {} as any,
101+
tagToRuns: {
102+
tagA: ['run1'],
103+
tagB: ['run2', 'run3'],
104+
tagC: ['run4', 'run5'],
105+
tagD: ['run6'],
106+
},
107+
},
108+
histograms: {
109+
tagDescriptions: {} as any,
110+
tagToRuns: {
111+
tagA: ['run1'],
112+
tagB: ['run4'],
113+
},
114+
},
115+
images: {
116+
tagDescriptions: {},
117+
tagRunSampledInfo: {
118+
tagC: {
119+
'defaultExperimentId/run1': {} as any,
120+
'exp1/run3': {} as any,
121+
},
122+
},
123+
},
124+
});
125+
}
126+
127+
function overrideRunToEid() {
128+
store.overrideSelector(selectors.getRunIdToExperimentId, {
129+
run1: 'exp1',
130+
run2: 'exp1',
131+
run3: 'exp2',
132+
run4: 'defaultExperimentId',
133+
run5: 'defaultExperimentId',
134+
run6: 'defaultExperimentId',
135+
});
136+
}
137+
94138
afterEach(() => {
95139
store?.resetSelectors();
96140
});
@@ -365,35 +409,25 @@ describe('metrics effects', () => {
365409
actions$.next(reloadAction());
366410

367411
expect(fetchTagMetadataSpy).toHaveBeenCalled();
368-
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(2);
412+
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(1);
369413
expect(actualActions).toEqual([
370414
actions.metricsTagMetadataRequested(),
371415
actions.metricsTagMetadataLoaded({
372416
tagMetadata: buildDataSourceTagMetadata(),
373417
}),
374418

375-
// Currently we expect 2x the same requests if the cards are the same.
376-
// Ideally we should dedupe requests for the same info.
377419
actions.multipleTimeSeriesRequested({
378420
requests: [
379421
{
380422
plugin: PluginType.SCALARS as MultiRunPluginType,
381423
tag: 'tagA',
382424
experimentIds: ['exp1'],
383425
},
384-
{
385-
plugin: PluginType.SCALARS as MultiRunPluginType,
386-
tag: 'tagA',
387-
experimentIds: ['exp1'],
388-
},
389426
],
390427
}),
391428
actions.fetchTimeSeriesLoaded({
392429
response: buildTimeSeriesResponse(),
393430
}),
394-
actions.fetchTimeSeriesLoaded({
395-
response: buildTimeSeriesResponse(),
396-
}),
397431
]);
398432
});
399433

@@ -487,6 +521,8 @@ describe('metrics effects', () => {
487521
it('does not re-fetch time series, until a valid experiment id', () => {
488522
// Reset any `getExperimentIdsFromRoute` overrides above.
489523
store.resetSelectors();
524+
overrideTagMetadata();
525+
overrideRunToEid();
490526
store.overrideSelector(getActivePlugin, METRICS_PLUGIN_ID);
491527
store.overrideSelector(
492528
selectors.getVisibleCardIdSet,
@@ -510,6 +546,43 @@ describe('metrics effects', () => {
510546

511547
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(2);
512548
});
549+
550+
it('does not send requests to experiments lacking a cards tag', () => {
551+
store.overrideSelector(getActivePlugin, METRICS_PLUGIN_ID);
552+
store.overrideSelector(selectors.getExperimentIdsFromRoute, [
553+
'exp1',
554+
'exp2',
555+
]);
556+
store.overrideSelector(
557+
selectors.getVisibleCardIdSet,
558+
new Set(['card1', 'card2'])
559+
);
560+
provideCardFetchInfo([
561+
{id: 'card1', tag: 'tagA'},
562+
{id: 'card2', tag: 'tagB'},
563+
]);
564+
store.refreshState();
565+
566+
const effectFetchTimeSeriesSpy = spyOn(
567+
effects as any,
568+
'fetchTimeSeries'
569+
).and.stub();
570+
571+
actions$.next(coreActions.manualReload());
572+
573+
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledTimes(2);
574+
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({
575+
plugin: 'scalars',
576+
tag: 'tagA',
577+
experimentIds: ['exp1'],
578+
});
579+
580+
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({
581+
plugin: 'scalars',
582+
tag: 'tagB',
583+
experimentIds: ['exp1', 'exp2'],
584+
});
585+
});
513586
});
514587

515588
describe('loadTimeSeriesForVisibleCardsWithoutData', () => {
@@ -778,4 +851,24 @@ describe('metrics effects', () => {
778851
}
779852
});
780853
});
854+
855+
describe('#utilities', () => {
856+
describe('parseRunIdFromSampledRunInfoName', () => {
857+
it('removes prefixed experiment id', () => {
858+
expect(
859+
TEST_ONLY.parseRunIdFromSampledRunInfoName('experimentId/someRun')
860+
).toEqual('someRun');
861+
});
862+
863+
it('preserves "/" characters in run names', () => {
864+
expect(
865+
TEST_ONLY.parseRunIdFromSampledRunInfoName('experimentId/some/run')
866+
).toEqual('some/run');
867+
});
868+
869+
it('returns an empty string when an empty string is provided', () => {
870+
expect(TEST_ONLY.parseRunIdFromSampledRunInfoName('')).toEqual('');
871+
});
872+
});
873+
});
781874
});

0 commit comments

Comments
 (0)