-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[DebuggerV2] Add basic ng impl of actions, store, data_source & new Alerts view #3075
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
Conversation
tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container.ts
Outdated
Show resolved
Hide resolved
| constructor(private readonly store: Store<State>) {} | ||
|
|
||
| ngOnInit(): void { | ||
| this.store.dispatch(debuggerLoaded()); |
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.
Optional but consider this. There are rare cases when you want to use this. We should avoid that certain view is initialized if possible since that can lead to proliferation of bad patterns. Since view is a reflection of state in the store, effects and reducers should be able to update correctly without this.
For instance, whatever you do here can probably be replaced with PluginChanged action, right?
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.
Whatever you do here can probably be replaced with PluginChanged action, right?
I think that's correct. But part of me worry about separation of responsibility as well. It feels a little strange to me that the PluginsChange action, which is at a higher level than the individual plugin (DebuggerV2 specifically), should know something about the internal workings of the plugin.
Also, why do you say these are rare cases? The DebuggerContainer being loaded (and hence its ngOnInit() method being called) when a user switches to the plugin is an essential step is a common workflow for the plugin.
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.
Also, why do you say these are rare cases? The DebuggerContainer being loaded
It is rare because this information is easily attainable from store's perspective and view really should not fire an action with intention of telling the store.
which is at a higher level than the individual plugin (DebuggerV2 specifically), should know something about the internal workings of the plugin.
I am not sure if I agree with this. One of the point of building on the same framework and same instance of the store is better "integration". Being part of the greater system is not a wrong thing.
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 a TODO item here to explore consolidate this effect into the integrated webapp.
|
|
||
| fetchRuns() { | ||
| // TODO(cais): Once the backend uses an DataProvider that unifies tfdbg and | ||
| // non-tfdbg plugins, switch to using `tf_backend.runStore.refresh()`. |
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 referring to fact that the ngrx store does not contain the runs right now? Would it be possible to add it there if so? Maybe runs here is different than that in general sense?
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, it is not entirely clear what "runs" will mean for DebuggerV2 yet. Currently each logdir may contain up to only 1 DebuggerV2 run (an error will be thrown by the backend Python code is >1 runs is present in a logdir). So the run for DebuggerV2 has a fixed, hardcoded run name right now. This is related to the fact that we have a special route to load the DebuggerV2 run here. It's possible that we'll allow multiple DebuggerV2 runs in the same logdir in the future, at that time, the run fetching mechanism can become more consistent with other plugins.
| ], | ||
| ) | ||
|
|
||
| # TODO(cais): After TestBed.inject() is figured out in OSS, add |
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 you explain here in GitHub comments what this would mean? You seem to have written a good test with TestBed.get which is more or less equal to inject
tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts
Show resolved
Hide resolved
| export const DEBUGGER_FEATURE_KEY = 'debugger'; | ||
|
|
||
| // TODO(cais): Maybe deduplicate this with tensorboard/webapp. | ||
| export enum DataLoadState { |
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.
You should certainly dedup ;) also this code, as discussed in offline, should move to webapp so that would be quite natural.
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.
Done. Please note that I made some changes to the types in tensorboard/webapp as well:
- The
LoadStatein tensorboard/webapp/types/api.ts is now renamed asDataLoadState. - The
LoadStatein tensorboard/core/store/core_types.ts is now moved into tensorboard/webapp/types/api.ts. This is justified by the fact that it is a sufficiently general interface that can be used not only the webapp/core, but also by other components and plugins.
This makes the type system slightly cleaner and more intuitive IMO.
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.
tensorboard/webapp/types/api.ts
I don't think the LoadState belongs in there. I would love it if it gets moved to tensorboard/webapp/types/data.ts or something to that effect.
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.
Done. I created a new file in the same directory (tensorboard/webapp/types/data.ts) and moved the DataLoadState and LoadState interfaces to that file. Changes are made accordingly in tensorboard/webapp and in tensorboard/plugins/debugger_v2.
caisq
left a comment
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.
Thanks for the review so far.
Can you explain here in GitHub comments what this would mean? You seem to have written a good test with TestBed.get which is more or less equal to inject
Sorry for the confusion. This now-out-of-date TODO item was about adding debugger_effects_test. Done.
|
|
||
| fetchRuns() { | ||
| // TODO(cais): Once the backend uses an DataProvider that unifies tfdbg and | ||
| // non-tfdbg plugins, switch to using `tf_backend.runStore.refresh()`. |
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, it is not entirely clear what "runs" will mean for DebuggerV2 yet. Currently each logdir may contain up to only 1 DebuggerV2 run (an error will be thrown by the backend Python code is >1 runs is present in a logdir). So the run for DebuggerV2 has a fixed, hardcoded run name right now. This is related to the fact that we have a special route to load the DebuggerV2 run here. It's possible that we'll allow multiple DebuggerV2 runs in the same logdir in the future, at that time, the run fetching mechanism can become more consistent with other plugins.
| constructor(private readonly store: Store<State>) {} | ||
|
|
||
| ngOnInit(): void { | ||
| this.store.dispatch(debuggerLoaded()); |
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.
Whatever you do here can probably be replaced with PluginChanged action, right?
I think that's correct. But part of me worry about separation of responsibility as well. It feels a little strange to me that the PluginsChange action, which is at a higher level than the individual plugin (DebuggerV2 specifically), should know something about the internal workings of the plugin.
Also, why do you say these are rare cases? The DebuggerContainer being loaded (and hence its ngOnInit() method being called) when a user switches to the plugin is an essential step is a common workflow for the plugin.
tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts
Show resolved
Hide resolved
| export const DEBUGGER_FEATURE_KEY = 'debugger'; | ||
|
|
||
| // TODO(cais): Maybe deduplicate this with tensorboard/webapp. | ||
| export enum DataLoadState { |
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.
Done. Please note that I made some changes to the types in tensorboard/webapp as well:
- The
LoadStatein tensorboard/webapp/types/api.ts is now renamed asDataLoadState. - The
LoadStatein tensorboard/core/store/core_types.ts is now moved into tensorboard/webapp/types/api.ts. This is justified by the fact that it is a sufficiently general interface that can be used not only the webapp/core, but also by other components and plugins.
This makes the type system slightly cleaner and more intuitive IMO.
| /** | ||
| * Actions for the Alerts Component. | ||
| */ | ||
| export const alertsViewLoaded = createAction('[Alerts] Alerts View Loaded'); |
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.
[Debugger] seems more apt but it really does not matter what is in this 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.
Done.
|
|
||
| /** @typehack */ import * as _typeHackRxjs from 'rxjs'; | ||
|
|
||
| export const TFDBG2_DATA_SOURCE = new InjectionToken<Tfdbg2DataSource>( |
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.
How is this being used? It may be unused since you are providing the Tfdbg2HttpServerDataSource and injecting it in the places it is being used.
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.
Right. It's currently unused. I removed it.
| export class DebuggerComponent {} | ||
| export class DebuggerComponent { | ||
| @Input() | ||
| runs: DebuggerRunListing = {}; |
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.
optional: if this component does not make sense without runs, make this input a required field by not providing a default value.
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 agree. Made it a required field.
| export const getDebuggerRuns = createSelector( | ||
| selectDebuggerState, | ||
| (state: DebuggerState): DebuggerRunListing => { | ||
| return state.runs; |
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.
since it is returning a DebuggerRunListing, consider renaming the selector to getDebuggerRunListing (getDebuggerRun is has room for confusion since it can mean list of run IDs).
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.
Done.
| const fixture = TestBed.createComponent(DebuggerContainer); | ||
| fixture.detectChanges(); | ||
|
|
||
| store.setState( |
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.
Feel free to disagree: when writing views, we try to abstract out what is contained inside the store via semantically meaningful selectors. I think it makes more sense to mock out selectors without re-creating the store structure like this. If you agree with what I said, you can use store.overrideSelector(selector, value).
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 see your point. I think this pattern (namely setting store state) is more aligned with the testing principle of "test behavior, not internal implementation". For one thing, it's more resistant against changes in how the details of the selectors change. So I prefer to keep the current pattern. (Also, as you may know, the component tests in tensorboard/webapp are based on this pattern, instead of the overrideSelector() pattern.
|
|
||
| const initialState = createState(createDebuggerState()); | ||
| await TestBed.configureTestingModule({ | ||
| imports: [HttpClientTestingModule], |
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.
since we are using the DataSource here, we may not actually need this especially since we should all stub data source methods out. Also, it may be a good idea to create a testing version of the DataSource.
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 is actually needed. Without it, there will be a NullInjectorError, because Tfdbg2HttpServerDataSource requires an HttpClient injection.
I don't feel the urgent need to create a testing version of DataSource yet. Currently I find spyOn to be sufficient. I may add a testing version in the future when the need arises.
| deps = [ | ||
| "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions", | ||
| "@npm//@ngrx/store", | ||
| "@npm//rxjs", |
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.
unused dep (I think)
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.
Right. Removed.
| expect(nextState.runsLoaded.lastLoadedTimeInMs).toBeNull(); | ||
| }); | ||
|
|
||
| it('sets runsLoaded and runs on successful runs loading', () => { |
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.
write a test for overwriting existing key please.
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.
Done. Thanks for the suggestion.
| actions.debuggerRunsLoaded, | ||
| (state: DebuggerState, {runs}): DebuggerState => { | ||
| return { | ||
| runs, |
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.
you may want to spread state so it is more resistant to changes in the state.
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.
Good point. Done.
| } | ||
|
|
||
| export interface State { | ||
| [DEBUGGER_FEATURE_KEY]?: DebuggerState; |
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.
Why is the DebuggerState optional? I don't think it should be optional.
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 agree. Done.
caisq
left a comment
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.
Thanks again.
| /** | ||
| * Actions for the Alerts Component. | ||
| */ | ||
| export const alertsViewLoaded = createAction('[Alerts] Alerts View Loaded'); |
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.
Done.
|
|
||
| /** @typehack */ import * as _typeHackRxjs from 'rxjs'; | ||
|
|
||
| export const TFDBG2_DATA_SOURCE = new InjectionToken<Tfdbg2DataSource>( |
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.
Right. It's currently unused. I removed it.
| export class DebuggerComponent {} | ||
| export class DebuggerComponent { | ||
| @Input() | ||
| runs: DebuggerRunListing = {}; |
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 agree. Made it a required field.
|
|
||
| const initialState = createState(createDebuggerState()); | ||
| await TestBed.configureTestingModule({ | ||
| imports: [HttpClientTestingModule], |
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 is actually needed. Without it, there will be a NullInjectorError, because Tfdbg2HttpServerDataSource requires an HttpClient injection.
I don't feel the urgent need to create a testing version of DataSource yet. Currently I find spyOn to be sufficient. I may add a testing version in the future when the need arises.
| deps = [ | ||
| "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions", | ||
| "@npm//@ngrx/store", | ||
| "@npm//rxjs", |
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.
Right. Removed.
| actions.debuggerRunsLoaded, | ||
| (state: DebuggerState, {runs}): DebuggerState => { | ||
| return { | ||
| runs, |
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.
Good point. Done.
| expect(nextState.runsLoaded.lastLoadedTimeInMs).toBeNull(); | ||
| }); | ||
|
|
||
| it('sets runsLoaded and runs on successful runs loading', () => { |
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.
Done. Thanks for the suggestion.
| } | ||
|
|
||
| export interface State { | ||
| [DEBUGGER_FEATURE_KEY]?: DebuggerState; |
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 agree. Done.
| export const getDebuggerRuns = createSelector( | ||
| selectDebuggerState, | ||
| (state: DebuggerState): DebuggerRunListing => { | ||
| return state.runs; |
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.
Done.
| const fixture = TestBed.createComponent(DebuggerContainer); | ||
| fixture.detectChanges(); | ||
|
|
||
| store.setState( |
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 see your point. I think this pattern (namely setting store state) is more aligned with the testing principle of "test behavior, not internal implementation". For one thing, it's more resistant against changes in how the details of the selectors change. So I prefer to keep the current pattern. (Also, as you may know, the component tests in tensorboard/webapp are based on this pattern, instead of the overrideSelector() pattern.
Remove `as Observable<Action>` Remove unused dep
…nto dbg2-alert-component
|
Gentle ping :) |
debuggerLoaded.tf-debugger-v2-alertselement (AlertsContainer) is rendered conditionally in lieu of the existingInactiveContainerwhen there is at least 1 debugger run available via the data source.TestingDebuggerModulein testing/ and using it in /tensorboard/webapp.The code pattern is largely based on the work of @stephanwlee