Skip to content

Commit cc1bbaf

Browse files
committed
stop making duplicate time series requests
1 parent fb714c9 commit cc1bbaf

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
@@ -19,6 +19,7 @@ import {forkJoin, merge, Observable, of} from 'rxjs';
1919
import {
2020
catchError,
2121
throttleTime,
22+
combineLatestWith,
2223
filter,
2324
map,
2425
mergeMap,
@@ -68,6 +69,12 @@ const getCardFetchInfo = createSelector(
6869

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

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

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

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)