-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Bug Fix: Stop making duplicate time series requests #6529
base: master
Are you sure you want to change the base?
Changes from 8 commits
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,13 +19,15 @@ import {forkJoin, merge, Observable, of} from 'rxjs'; | |
import { | ||
catchError, | ||
throttleTime, | ||
combineLatestWith, | ||
filter, | ||
map, | ||
mergeMap, | ||
switchMap, | ||
take, | ||
tap, | ||
withLatestFrom, | ||
shareReplay, | ||
} from 'rxjs/operators'; | ||
import * as routingActions from '../../app_routing/actions'; | ||
import {State} from '../../app_state'; | ||
|
@@ -42,13 +44,16 @@ import { | |
TagMetadata, | ||
TimeSeriesRequest, | ||
TimeSeriesResponse, | ||
NonSampledPluginType, | ||
} from '../data_source/index'; | ||
import { | ||
getCardLoadState, | ||
getCardMetadata, | ||
getMetricsTagMetadataLoadState, | ||
TagMetadata as StoreTagMetadata, | ||
} from '../store'; | ||
import {CardId, CardMetadata} from '../types'; | ||
import {CardId, CardMetadata, PluginType} from '../types'; | ||
import {DeepReadonly} from '../../util/types'; | ||
|
||
export type CardFetchInfo = CardMetadata & { | ||
id: CardId; | ||
|
@@ -68,6 +73,31 @@ const getCardFetchInfo = createSelector( | |
|
||
const initAction = createAction('[Metrics Effects] Init'); | ||
|
||
function generateMultiRunTagsToEidMapping( | ||
tagMetadata: DeepReadonly<StoreTagMetadata>, | ||
runToEid: Record<string, string> | ||
): Record<string, Set<string>> { | ||
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])); | ||
}); | ||
} | ||
|
||
for (const pluginType in tagMetadata) { | ||
if (isSingleRunPlugin(pluginType as PluginType)) { | ||
continue; | ||
} | ||
|
||
mapTagsToEid(tagMetadata[pluginType as NonSampledPluginType].tagToRuns); | ||
} | ||
|
||
return tagToEid; | ||
} | ||
|
||
@Injectable() | ||
export class MetricsEffects implements OnInitEffects { | ||
constructor( | ||
|
@@ -76,6 +106,22 @@ export class MetricsEffects implements OnInitEffects { | |
private readonly dataSource: MetricsDataSource | ||
) {} | ||
|
||
/** | ||
* Computes a record of tag to the experiments it appears in. | ||
* | ||
* The computation is done by translating Plugin -> Tag -> Run -> ExpId | ||
* | ||
* Sampled plugins are ignored because they are associated with runs, not experiments. | ||
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. Sampled plugins are not the only things ignored here. Really it's any single-run plugins that are ignored. Would be good to fix that in the documentation. I think its also worth explaining the real motivation for the change here - otherwise it is hard to understand why we would bother doing this for scalars and why we wouldn't do this for the others: We want to eliminate unnecessary requests for experiment+tag combinations where the experiment does not actually contain the tag. In case of single-run plugins we assume that every given request for expeirment+run+tag is already valid, since they originate from cards for that experiment+run+tag combination. 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've updated this comment and added a detail description of the problem and how observable is being used to solve it. |
||
*/ | ||
readonly multiRunTagsToEid$: Observable<Record<string, Set<string>>> = | ||
this.store.select(selectors.getMetricsTagMetadata).pipe( | ||
combineLatestWith(this.store.select(selectors.getRunIdToExperimentId)), | ||
map(([tagMetadata, runToEid]) => { | ||
return generateMultiRunTagsToEidMapping(tagMetadata, runToEid); | ||
}), | ||
shareReplay(1) | ||
); | ||
|
||
/** @export */ | ||
ngrxOnInitEffects(): Action { | ||
return initAction(); | ||
|
@@ -195,23 +241,44 @@ 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.multiRunTagsToEid$.pipe( | ||
take(1), | ||
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 we use withLatestFrom instead? The caller of this function uses withLatestFrom so that might be a natural place to tie in this observable? 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. Yeah, that works. |
||
map((tagToEid): TimeSeriesRequest[] => { | ||
const requests = fetchInfos | ||
.map((fetchInfo) => { | ||
const {plugin, tag, runId, sample} = fetchInfo; | ||
|
||
if (isSingleRunPlugin(plugin)) { | ||
if (!runId) { | ||
return; | ||
} | ||
return { | ||
plugin, | ||
tag, | ||
runId, | ||
sample, | ||
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. This assumes that sample only exists for single run plugins. Is that guaranteed by the data model? - single run vs sampled are theoretically orthoganal considerations. (I realize in practice that there are no multi-run, sampled plugins but the old code handles this case fine so I assume that is intentional). 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've added sample to the multi run plugin request. It shouldn't ever come up, but the typing does allow for it so it's close to free to include. |
||
}; | ||
} | ||
|
||
const filteredEids = experimentIds.filter((eid) => | ||
tagToEid[tag]?.has(eid) | ||
); | ||
if (!filteredEids.length) { | ||
return; | ||
} | ||
|
||
return {plugin, tag, experimentIds: filteredEids}; | ||
}) | ||
.filter(Boolean); | ||
const uniqueRequests = new Set( | ||
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 an actual problem you are trying to solve here? I don't see a test for this and I didn't see anything about it in the PR description. 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 am attempting to address the TODO left by psybuzz 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. Does "if 2 cards require the same data" happen in practice? If it does, is it a source of problems? If not, especially given that this code is critical, do we need to be making unnecessary changes? Also, it's not clear to me that you wrote a test for this particular change? 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 was an existing test which verified this was doing the wrong thing which I updated. See the comment that I removed from line 375 of metrics_effects_test |
||
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 +369,5 @@ export class MetricsEffects implements OnInitEffects { | |
export const TEST_ONLY = { | ||
getCardFetchInfo, | ||
initAction, | ||
generateMultiRunTagsToEidMapping, | ||
}; |
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.
Nit: I think it would be clearer if you just inlined the contents of this function in the for loop (at L95). You would possibly even save some lines of code. You only use it once, after all.