-
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?
Bug Fix: Stop making duplicate time series requests #6529
Conversation
5e18132
to
cc1bbaf
Compare
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.
There is a lot about this PR that is confusing. I'm worried you've introduced a bunch of new subtle bugs.
This is a critical piece of code so please take a deep look at what you've written and rigourously test it (manually and with unit tests).
combineLatestWith(this.store.select(selectors.getRunIdToExperimentId)), | ||
map(([tagMetadata, runToEid]) => { | ||
const imageTagToRuns = Object.fromEntries( | ||
Object.entries(tagMetadata.images.tagRunSampledInfo).map( |
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.
Can we handle plugins and sampled vs non-sampled more generically?
Ideally the code is unaware of the set of plugin types (it doesn't know about images, scalars, or histograms). Ideally the code is unaware of which plugin types are sampled and which are not.
There is isSampledPlugin
function to help with this, too.
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.
Alright, I thought the additional loop that approach required harmed readability a bit but I've gone ahead and refactored to use it.
@@ -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('/'); |
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.
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 sampled
The NonSampledPlugins map from run
to tag
while the SampledPlugin(s) map from tag
to run
Sampled
Non Sampled
Rough Sketch
Here is a rough sketch with only the relevant parts
{
tagMetadata: {
scalars: {
runTagInfo: {
runId: ['tag1', 'tag2',]
},
},
images: {
tagRunSampledInfo: {
tag: {
runId: {maxSamplesPerStep: number}
}
}
},
}
}
// Fetch and handle responses. | ||
return of(requests).pipe( | ||
return this.tagToEid$.pipe( |
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.
Rather than piping this.tagToEid$ can we just get the latest value? I'm a little worried about subtle bugs when tagToEid$ changes for whatever unpredicatable reason and the pipe kicks off a new set of requests.
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 added a take(1)
I could use a subject instead or maybe a subscription? Let me know if you'd prefer something else.
} | ||
return partialRequest; | ||
}); | ||
const uniqueRequests = new Set( |
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.
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 comment
The 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 comment
The 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 comment
The 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
histograms: { | ||
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], |
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.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ya, fair enough. That makes sense.
actions$.next(coreActions.manualReload()); | ||
|
||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledTimes(2); | ||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({ |
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.
It's extremely hard to reason why the test concludes that these should be the requests sent. Some of the key test data (like in overrideTagMetadata and overrideRunToEid) are setup far from here. Maybe it would be helpful to leave a comment about all the requests that could have been made and identify why certain requests were filtered out.
|
||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledTimes(2); | ||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({ | ||
plugin: 'scalars', |
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.
Should we also be fetching plugin: 'histograms', tag: 'tagA', experimentIds: ['exp1']?
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], | ||
tagB: ['run2', 'run3'], |
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.
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 comment
The 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 comment
The 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 comment
The 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 does not send requests to experiments lacking a cards tag
references every tag.
}); | ||
|
||
expect(effectFetchTimeSeriesSpy).toHaveBeenCalledWith({ | ||
plugin: 'scalars', |
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.
Given the complexity of the logic you've written it would be good to see more rigourous testing. A single test case doesn't really seem to cut it.
A couple that pop up in my head:
- A test case where some image requests make it through the new filter.
- A test case where some histogram requests make it through the new filter.
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'll add some additional tests to this but wanted to leave some preliminary comments.
combineLatestWith(this.store.select(selectors.getRunIdToExperimentId)), | ||
map(([tagMetadata, runToEid]) => { | ||
const imageTagToRuns = Object.fromEntries( | ||
Object.entries(tagMetadata.images.tagRunSampledInfo).map( |
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.
Alright, I thought the additional loop that approach required harmed readability a bit but I've gone ahead and refactored to use it.
@@ -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('/'); |
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 sampled
The NonSampledPlugins map from run
to tag
while the SampledPlugin(s) map from tag
to run
Sampled
Non Sampled
Rough Sketch
Here is a rough sketch with only the relevant parts
{
tagMetadata: {
scalars: {
runTagInfo: {
runId: ['tag1', 'tag2',]
},
},
images: {
tagRunSampledInfo: {
tag: {
runId: {maxSamplesPerStep: number}
}
}
},
}
}
// Fetch and handle responses. | ||
return of(requests).pipe( | ||
return this.tagToEid$.pipe( |
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 added a take(1)
I could use a subject instead or maybe a subscription? Let me know if you'd prefer something else.
} | ||
return partialRequest; | ||
}); | ||
const uniqueRequests = new Set( |
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 am attempting to address the TODO left by psybuzz
histograms: { | ||
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], |
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.
Is there anything that prohibits this? Tags can appear in multiple experiments and they could have different data being logged.
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], | ||
tagB: ['run2', 'run3'], |
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 added some additional data to the state to ensure it did not result in additional requests.
… by run not eid and should be handled differently
…ograms and scalars should not be entangled
runToEid: Record<string, string> | ||
): Record<string, Set<string>> { | ||
const tagToEid: Record<string, Set<string>> = {}; | ||
function mapTagsToEid(tagToRun: Record<string, readonly string[]>) { |
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.
} | ||
return partialRequest; | ||
}); | ||
const uniqueRequests = new Set( |
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.
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?
* | ||
* 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 comment
The 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 comment
The 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.
// 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that works.
plugin, | ||
tag, | ||
runId, | ||
sample, |
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.
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 comment
The 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.
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], | ||
tagB: ['run2', 'run3'], |
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.
Could you add some comments acknowledging which data is unnecessary and why you include it?
histograms: { | ||
tagDescriptions: {} as any, | ||
tagToRuns: { | ||
tagA: ['run1'], |
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.
Ah ya, fair enough. That makes sense.
).toEqual({}); | ||
}); | ||
|
||
it('maps scalar data', () => { |
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.
Would it be worth adding one additional test that includes tagMetadata for all of scalars, histograms, and images?
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 think it's a little redundant but I've added one to be safe.
Motivation for features / changes
Whenever a card appears on the time series dashboard we make a request to the fetch data for the card for each experiment being viewed. Because not all cards contain data from all experiments being viewed this sometimes results in unnecessary requests being dispatched.