-
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
Add WebGL escape hatch #5494
Add WebGL escape hatch #5494
Conversation
Ultimately, like all state in our Angular app, the source of truth for the user's decision to use WebGL vs SVG should be the in-memory ngrx state. On app load we would construct the ngrx-specific value taking into consideration the value from the query parameters and from local settings. Normally we would represent this sort of state as a "feature flag":
That line, specifically, shows how we read "feature flags" from "persistent settings", which is the abstraction on top of local storage: https://github.com/tensorflow/tensorboard/tree/master/tensorboard/webapp/persistent_settings This is where we read some feature flag overrides from query parameters: And, finally, I believe this is an example of how you could take the value from the ngrx state and update the local storage via (via persistent settings):
|
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 ForceSVGDataSource is missing from these changes. Can you add it?
@@ -33,6 +34,7 @@ export class FeatureFlagEffects { | |||
combineLatestWith(this.store.select(getIsAutoDarkModeAllowed)), | |||
map(([, isDarkModeAllowed]) => { | |||
const features = this.dataSource.getFeatures(isDarkModeAllowed); | |||
features.forceSVG = this.ForceSVGDataSource.getAndUpdateForceSVGFlag(); |
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.
Data source layer is ideally sort of dumb and just reads/writes data without trying to make any decisions. The decision making (aka business logic) is ideally in the effects and reducers layers.
My recommendation:
-
Rely on TBFeatureFlagDataSource.getFeatures() to read the value of the forceSVG query parameter. If it follows the pattern of other query parameters then it should return empty value for forceSVG if it is not specified in the URL.
-
Introduce ForceSVGDataSource.getForceSVG() that returns the value from local storage or null if there is none.
-
Write logic in the Effects layer to determine the actual value of ForceSVG based on (1) and (2). The value is possibly empty (in which case, in practice, the app will rely on the default in the ngrx state). Include this value in the partialFeatureFlagsLoaded.
-
Introduce ForceSVGDataSoruce.updateForceSVG() to write the value to local storage, if appropriate.
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 all sounds good. However, if we are only pulling from localStorage for the ForceSVGDataSource it does not need all the overhead of an injectable class. I am going to change it to force_svg_util and just have a getForceSvg and updateForceSvg functions as standalone functions. Does that sound good to you?
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.
In this particular case, I can empathise with the temptation to avoid using Angular's Dependency Injection (DI). Why you might think this way:
- force_svg_util itself currently has no DI-able dependencies (although I think it's hard to be certain that will be true forever)
- we don't foresee wanting to to DI some other implementation of ForceSVGDataSource -- we think it will always be a local storage thing, unlike persistent settings which might one day be stored on the server.
Having said that, I think it's good habit/hygiene to make this a DI-able object like any other object in our system that reads/writes to a data source. I agree there is some overhead but it is pretty standard Angular stuff. It will also give you standard options/patterns when you write tests - how to Mock, how to Fake/Stub.
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 made it injectable again.
I also added tests.
PTAL.
tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html
Outdated
Show resolved
Hide resolved
@@ -91,6 +91,7 @@ tf_ng_module( | |||
tf_ng_module( | |||
name = "feature_flag", | |||
srcs = [ | |||
"force_SVG_data_source.ts", |
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.
Over time we've been trying to move things out of webapp_data_source and into feature-specific folders. Ideally force_svg_data_source and tb_feature_flag_data_source and tb_feature_flag_module would be in the feature_flag folder.
Could we put force_svg_data_source there immediately?
Would you be interested in following up with cleanup PRs to move tb_feature_flag_data_source and tb_feature_flag_module there as well? (disclosure: this may be a bit difficult because of the fact that classes in our internal repo reference these classes)
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.
Moved and renamed. Yea I can probably do that cleanup. I will look into what it will take to make that happen. Maybe make a copy of it temporarily until we can switch internal code over then remove.
@@ -90,6 +91,10 @@ export class QueryParamsFeatureFlagDataSource | |||
params.get(ENABLE_TIME_NAMESPACED_STATE) !== 'false'; | |||
} | |||
|
|||
if (params.has(FORCE_SVG_RENDERER)) { |
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 add tests for this?
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.
Added.
@@ -102,7 +102,7 @@ | |||
></mat-spinner> | |||
<line-chart | |||
[disableUpdate]="!isCardVisible" | |||
[preferredRendererType]="RendererType.SVG" | |||
[preferredRendererType]="forceSvg ? RendererType.SVG : RendererType.WEBGL" |
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 add tests for this?
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.
Added.
@@ -50,4 +50,6 @@ export interface FeatureFlags { | |||
// Whether to enable time-namespaced state and how it impacts how user | |||
// settings are kept during navigation. | |||
enabledTimeNamespacedState: boolean; | |||
|
|||
forceSvg: boolean; |
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.
Document? Worth mentioning this is for TimeSeries Scalars cards only (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.
Added.
import {FeatureFlagEffects} from './feature_flag_effects'; | ||
|
||
describe('feature_flag_effects', () => { | ||
fdescribe('feature_flag_effects', () => { |
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.
Dangling fdescribe here.
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.
doh! removed.
@@ -0,0 +1,22 @@ | |||
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. |
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.
2022
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.
Doh!
removeItemSpy = spyOn(window.localStorage, 'removeItem').and.stub(); | ||
dataSource = TestBed.inject(ForceSvgDataSource); | ||
}); | ||
describe('#getForceSVG', () => { |
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: Newline before this line.
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.
Added.
limitations under the License. | ||
==============================================================================*/ | ||
import {Injectable} from '@angular/core'; | ||
import {FORCE_SVG_RENDERER} from '../webapp_data_source/tb_feature_flag_data_source_types'; |
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 sharing the query parameter key perhaps we should use a name more like the one used by PersistentSettingsDataSource:
Prefixed with '_tb_'.
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.
That makes more sense. Changed.
}); | ||
|
||
describe('updateForceSVG', () => { | ||
it('Creates localStorage entry with key forceSVG when passed truthy', () => { |
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 'Creates' => 'creates'
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.
|
||
describe('updateForceSVG', () => { | ||
it('Creates localStorage entry with key forceSVG when passed truthy', () => { | ||
let dataSource = new ForceSvgDataSource(); |
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.
Use dataSource you get from the TestBed instead?
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 did not mean to leave this line in there. Removed.
); | ||
}); | ||
it('calls localStorage.removeItem with key forceSVG', () => { | ||
let dataSource = new ForceSvgDataSource(); |
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.
Use dataSource you get from the TestBed instead?
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.
removed.
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 Brian. Let me know if there is anything else you can see to improve this.
import {FeatureFlagEffects} from './feature_flag_effects'; | ||
|
||
describe('feature_flag_effects', () => { | ||
fdescribe('feature_flag_effects', () => { |
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.
doh! removed.
expect(updateSpy).toHaveBeenCalledOnceWith(true); | ||
}); | ||
|
||
fit('gets forceSVG flag from ForceSvgDataSource when getFeatures returns no value for forceSvg', () => { |
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.
removed.
expect(updateSpy).toHaveBeenCalledOnceWith(true); | ||
}); | ||
|
||
fit('gets forceSVG flag from ForceSvgDataSource when getFeatures returns no value for forceSvg', () => { |
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.
Added.
|
||
@Injectable() | ||
export class ForceSvgDataSource { | ||
constructor() {} |
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 would love to do that. I see it as a useless abstraction. I am not a fan of useless abstractions. The basic localStorage API works great by itself(I might be a bit biased because my last team owned that API :P)
@@ -0,0 +1,22 @@ | |||
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. |
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.
Doh!
@@ -50,4 +50,6 @@ export interface FeatureFlags { | |||
// Whether to enable time-namespaced state and how it impacts how user | |||
// settings are kept during navigation. | |||
enabledTimeNamespacedState: boolean; | |||
|
|||
forceSvg: boolean; |
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.
Added.
@@ -102,7 +102,7 @@ | |||
></mat-spinner> | |||
<line-chart | |||
[disableUpdate]="!isCardVisible" | |||
[preferredRendererType]="RendererType.SVG" | |||
[preferredRendererType]="forceSvg ? RendererType.SVG : RendererType.WEBGL" |
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.
Added.
@@ -90,6 +91,10 @@ export class QueryParamsFeatureFlagDataSource | |||
params.get(ENABLE_TIME_NAMESPACED_STATE) !== 'false'; | |||
} | |||
|
|||
if (params.has(FORCE_SVG_RENDERER)) { |
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.
Added.
|
||
updateForceSvgFlag(forceSvgFlag: boolean) { | ||
if (forceSvgFlag) { | ||
localStorage.setItem(FORCE_SVG_RENDERER, 'true'); |
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 like that. Changed.
limitations under the License. | ||
==============================================================================*/ | ||
import {Injectable} from '@angular/core'; | ||
import {FORCE_SVG_RENDERER} from '../webapp_data_source/tb_feature_flag_data_source_types'; |
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.
That makes more sense. Changed.
@@ -50,6 +50,7 @@ export interface FeatureFlags { | |||
// Whether to enable time-namespaced state and how it impacts how user | |||
// settings are kept during navigation. | |||
enabledTimeNamespacedState: boolean; | |||
|
|||
// Flag for the escape hatch from webGL. This only effects the TimeSeries |
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: webGL => WebGL
31f70e5
to
8f8472a
Compare
There is logic to fallback to SVG if we receive an error from WebGL. However, if some users are having trouble with WebGL and we are not catching the error we wanted an escape hatch for users to force the use of SVG. This PR adds that escape hatch as a hidden query parameter that users can add. Once added it will be stored locally until turned off which can be done with another query parameter.
There is logic to fallback to SVG if we receive an error from WebGL. However, if some users are having trouble with WebGL and we are not catching the error we wanted an escape hatch for users to force the use of SVG. This PR adds that escape hatch as a hidden query parameter that users can add. Once added it will be stored locally until turned off which can be done with another query parameter.
While we hope our error catching is sufficient to automate the fallback to SVG when WebGL fails we still want to give users the ability to force the use of SVG. This is not something we want most users to do. This should only be used if WebGL is failing and out fallback is not working.
This CL adds a secret query parameter that will allow users in the know to force the use of SVG. It also adds a secret parameter to stop forcing SVG and return to the default behavior.