Skip to content
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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions tensorboard/webapp/metrics/effects/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ tf_ng_module(
"//tensorboard/webapp/metrics/data_source",
"//tensorboard/webapp/metrics/store",
"//tensorboard/webapp/types",
"//tensorboard/webapp/util:types",
"@npm//@angular/core",
"@npm//@ngrx/effects",
"@npm//@ngrx/store",
Expand Down
102 changes: 85 additions & 17 deletions tensorboard/webapp/metrics/effects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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[]>) {
Copy link
Contributor

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.

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(
Expand All @@ -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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.

*/
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();
Expand Down Expand Up @@ -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),
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
Copy link
Contributor

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).

Copy link
Contributor Author

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.

};
}

const filteredEids = experimentIds.filter((eid) =>
tagToEid[tag]?.has(eid)
);
if (!filteredEids.length) {
return;
}

return {plugin, tag, experimentIds: filteredEids};
})
.filter(Boolean);
const uniqueRequests = new Set(
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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

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}));
}),
Expand Down Expand Up @@ -302,4 +369,5 @@ export class MetricsEffects implements OnInitEffects {
export const TEST_ONLY = {
getCardFetchInfo,
initAction,
generateMultiRunTagsToEidMapping,
};
Loading