-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Bug Fix: Stop making duplicate time series requests #6529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
cc1bbaf
f79b718
94b102a
5814618
20bf2f0
4d264a5
3eea398
0d741af
02ee0fd
bcd88ae
11e7729
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import {forkJoin, merge, Observable, of} from 'rxjs'; | |
import { | ||
catchError, | ||
throttleTime, | ||
combineLatestWith, | ||
filter, | ||
map, | ||
mergeMap, | ||
|
@@ -68,6 +69,12 @@ const getCardFetchInfo = createSelector( | |
|
||
const initAction = createAction('[Metrics Effects] Init'); | ||
|
||
function parseRunIdFromSampledRunInfoName(eidRun: string): string { | ||
if (!eidRun) return ''; | ||
const [, ...runIdChunks] = eidRun.split('/'); | ||
return runIdChunks.join('/'); | ||
} | ||
|
||
@Injectable() | ||
export class MetricsEffects implements OnInitEffects { | ||
constructor( | ||
|
@@ -76,6 +83,40 @@ export class MetricsEffects implements OnInitEffects { | |
private readonly dataSource: MetricsDataSource | ||
) {} | ||
|
||
readonly tagToEid$: Observable<Record<string, Set<string>>> = this.store | ||
.select(selectors.getMetricsTagMetadata) | ||
.pipe( | ||
combineLatestWith(this.store.select(selectors.getRunIdToExperimentId)), | ||
map(([tagMetadata, runToEid]) => { | ||
const imageTagToRuns = Object.fromEntries( | ||
Object.entries(tagMetadata.images.tagRunSampledInfo).map( | ||
|
||
([tag, sampledRunInfo]) => { | ||
const runIds = Object.keys(sampledRunInfo).map((runInfoKey) => | ||
parseRunIdFromSampledRunInfoName(runInfoKey) | ||
); | ||
return [tag, runIds]; | ||
} | ||
) | ||
); | ||
|
||
const tagToEid: Record<string, Set<string>> = {}; | ||
function mapTagsToEid(tagToRun: Record<string, readonly string[]>) { | ||
Object.entries(tagToRun).forEach(([tag, runIds]) => { | ||
if (!tagToEid[tag]) { | ||
tagToEid[tag] = new Set(); | ||
} | ||
runIds.forEach((runId) => tagToEid[tag].add(runToEid[runId])); | ||
}); | ||
} | ||
|
||
mapTagsToEid(tagMetadata.scalars.tagToRuns); | ||
mapTagsToEid(tagMetadata.histograms.tagToRuns); | ||
mapTagsToEid(imageTagToRuns); | ||
|
||
return tagToEid; | ||
}) | ||
); | ||
|
||
/** @export */ | ||
ngrxOnInitEffects(): Action { | ||
return initAction(); | ||
|
@@ -195,23 +236,31 @@ export class MetricsEffects implements OnInitEffects { | |
fetchInfos: CardFetchInfo[], | ||
experimentIds: string[] | ||
) { | ||
/** | ||
* TODO(psybuzz): if 2 cards require the same data, we should dedupe instead of | ||
* making 2 identical requests. | ||
*/ | ||
const requests: TimeSeriesRequest[] = fetchInfos.map((fetchInfo) => { | ||
const {plugin, tag, runId, sample} = fetchInfo; | ||
const partialRequest: TimeSeriesRequest = isSingleRunPlugin(plugin) | ||
? {plugin, tag, runId: runId!} | ||
: {plugin, tag, experimentIds}; | ||
if (sample !== undefined) { | ||
partialRequest.sample = sample; | ||
} | ||
return partialRequest; | ||
}); | ||
|
||
// Fetch and handle responses. | ||
return of(requests).pipe( | ||
return this.tagToEid$.pipe( | ||
|
||
map((tagToEid): TimeSeriesRequest[] => { | ||
const requests = fetchInfos.map((fetchInfo) => { | ||
const {plugin, tag, runId, sample} = fetchInfo; | ||
const filteredEids = experimentIds.filter((eid) => | ||
tagToEid[tag]?.has(eid) | ||
); | ||
|
||
const partialRequest: TimeSeriesRequest = isSingleRunPlugin(plugin) | ||
? {plugin, tag, runId: runId!} | ||
: {plugin, tag, experimentIds: filteredEids}; | ||
if (sample !== undefined) { | ||
partialRequest.sample = sample; | ||
} | ||
return partialRequest; | ||
}); | ||
const uniqueRequests = new Set( | ||
|
||
requests.map((request) => JSON.stringify(request)) | ||
); | ||
|
||
return Array.from(uniqueRequests).map( | ||
(serialized) => JSON.parse(serialized) as TimeSeriesRequest | ||
); | ||
}), | ||
tap((requests) => { | ||
this.store.dispatch(actions.multipleTimeSeriesRequested({requests})); | ||
}), | ||
|
@@ -302,4 +351,5 @@ export class MetricsEffects implements OnInitEffects { | |
export const TEST_ONLY = { | ||
getCardFetchInfo, | ||
initAction, | ||
parseRunIdFromSampledRunInfoName, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,8 +89,52 @@ describe('metrics effects', () => { | |
selectors.getMetricsTooltipSort, | ||
TooltipSort.ALPHABETICAL | ||
); | ||
|
||
overrideTagMetadata(); | ||
overrideRunToEid(); | ||
}); | ||
|
||
function overrideTagMetadata() { | ||
store.overrideSelector(selectors.getMetricsTagMetadata, { | ||
scalars: { | ||
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], | ||
bmd3k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tagB: ['run2', 'run3'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are no references to 'run2' thru 'run6' or 'tagC' and 'tagD' or 'defaultExperimentId' anywhere else in this test as far as I can tell. Maybe just mock the minimum amount of data you need for existing tests to pass and override at a more detailed level only for your new tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added some additional data to the state to ensure it did not result in additional requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add some comments acknowledging which data is unnecessary and why you include it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After your last comment I added some additional tests and ensured that all of the data is being used. In particular the test |
||
tagC: ['run4', 'run5'], | ||
tagD: ['run6'], | ||
}, | ||
}, | ||
histograms: { | ||
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it valid for there to be duplicate tags across scalars/histograms? Doesn't seem to make sense to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there anything that prohibits this? Tags can appear in multiple experiments and they could have different data being logged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ya, fair enough. That makes sense. |
||
tagB: ['run4'], | ||
}, | ||
}, | ||
images: { | ||
tagDescriptions: {}, | ||
tagRunSampledInfo: { | ||
tagC: { | ||
'defaultExperimentId/run1': {} as any, | ||
'exp1/run3': {} as any, | ||
}, | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
function overrideRunToEid() { | ||
store.overrideSelector(selectors.getRunIdToExperimentId, { | ||
run1: 'exp1', | ||
run2: 'exp1', | ||
run3: 'exp2', | ||
run4: 'defaultExperimentId', | ||
run5: 'defaultExperimentId', | ||
run6: 'defaultExperimentId', | ||
}); | ||
} | ||
|
||
afterEach(() => { | ||
store?.resetSelectors(); | ||
}); | ||
|
@@ -365,35 +409,25 @@ describe('metrics effects', () => { | |
actions$.next(reloadAction()); | ||
|
||
expect(fetchTagMetadataSpy).toHaveBeenCalled(); | ||
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(2); | ||
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(1); | ||
expect(actualActions).toEqual([ | ||
actions.metricsTagMetadataRequested(), | ||
actions.metricsTagMetadataLoaded({ | ||
tagMetadata: buildDataSourceTagMetadata(), | ||
}), | ||
|
||
// Currently we expect 2x the same requests if the cards are the same. | ||
// Ideally we should dedupe requests for the same info. | ||
actions.multipleTimeSeriesRequested({ | ||
requests: [ | ||
{ | ||
plugin: PluginType.SCALARS as MultiRunPluginType, | ||
tag: 'tagA', | ||
experimentIds: ['exp1'], | ||
}, | ||
{ | ||
plugin: PluginType.SCALARS as MultiRunPluginType, | ||
tag: 'tagA', | ||
experimentIds: ['exp1'], | ||
}, | ||
], | ||
}), | ||
actions.fetchTimeSeriesLoaded({ | ||
response: buildTimeSeriesResponse(), | ||
}), | ||
actions.fetchTimeSeriesLoaded({ | ||
response: buildTimeSeriesResponse(), | ||
}), | ||
]); | ||
}); | ||
|
||
|
@@ -487,6 +521,8 @@ describe('metrics effects', () => { | |
it('does not re-fetch time series, until a valid experiment id', () => { | ||
// Reset any `getExperimentIdsFromRoute` overrides above. | ||
store.resetSelectors(); | ||
overrideTagMetadata(); | ||
overrideRunToEid(); | ||
store.overrideSelector(getActivePlugin, METRICS_PLUGIN_ID); | ||
store.overrideSelector( | ||
selectors.getVisibleCardIdSet, | ||
|
@@ -510,6 +546,43 @@ describe('metrics effects', () => { | |
|
||
expect(fetchTimeSeriesSpy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('does not send requests to experiments lacking a cards tag', () => { | ||
store.overrideSelector(getActivePlugin, METRICS_PLUGIN_ID); | ||
store.overrideSelector(selectors.getExperimentIdsFromRoute, [ | ||
'exp1', | ||
'exp2', | ||
]); | ||
store.overrideSelector( | ||
selectors.getVisibleCardIdSet, | ||
new Set(['card1', 'card2']) | ||
); | ||
provideCardFetchInfo([ | ||
{id: 'card1', tag: 'tagA'}, | ||
{id: 'card2', tag: 'tagB'}, | ||
]); | ||
store.refreshState(); | ||
|
||
const effectFetchTimeSeriesSpy = spyOn( | ||
effects as any, | ||
'fetchTimeSeries' | ||
).and.stub(); | ||
|
||
actions$.next(coreActions.manualReload()); | ||
|
||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledTimes(2); | ||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({ | ||
|
||
plugin: 'scalars', | ||
|
||
tag: 'tagA', | ||
experimentIds: ['exp1'], | ||
}); | ||
|
||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({ | ||
plugin: 'scalars', | ||
|
||
tag: 'tagB', | ||
experimentIds: ['exp1', 'exp2'], | ||
}); | ||
}); | ||
}); | ||
|
||
describe('loadTimeSeriesForVisibleCardsWithoutData', () => { | ||
|
@@ -778,4 +851,24 @@ describe('metrics effects', () => { | |
} | ||
}); | ||
}); | ||
|
||
describe('#utilities', () => { | ||
describe('parseRunIdFromSampledRunInfoName', () => { | ||
it('removes prefixed experiment id', () => { | ||
expect( | ||
TEST_ONLY.parseRunIdFromSampledRunInfoName('experimentId/someRun') | ||
).toEqual('someRun'); | ||
}); | ||
|
||
it('preserves "/" characters in run names', () => { | ||
expect( | ||
TEST_ONLY.parseRunIdFromSampledRunInfoName('experimentId/some/run') | ||
).toEqual('some/run'); | ||
}); | ||
|
||
it('returns an empty string when an empty string is provided', () => { | ||
expect(TEST_ONLY.parseRunIdFromSampledRunInfoName('')).toEqual(''); | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what you are trying to parse here. A comment would be helpful.
Is it this part highlighted in red:

Is this the same format as the run names for tagMetadata.scalars.tagToRuns and tagMetadata.histograms.tagToRuns? If so, why handle it differently for this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've found a way to avoid doing this parsing but the structure still needs to be different.
I'll add a block comment explaining this.
The structure of
SampledTagMetadata
is quite different from non sampledThe NonSampledPlugins map from
run
totag
while the SampledPlugin(s) map fromtag
torun
Sampled
Non Sampled
Rough Sketch
Here is a rough sketch with only the relevant parts