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

Add WebGL escape hatch #5494

Merged
merged 9 commits into from Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 25 additions & 2 deletions tensorboard/webapp/feature_flag/BUILD
Expand Up @@ -8,6 +8,7 @@ tf_ng_module(
"feature_flag_module.ts",
],
deps = [
":force_svg_data_source",
"//tensorboard/webapp/feature_flag/effects",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
Expand All @@ -19,6 +20,18 @@ tf_ng_module(
],
)

tf_ng_module(
name = "force_svg_data_source",
srcs = [
"force_svg_data_source.ts",
"force_svg_data_source_module.ts",
],
deps = [
"//tensorboard/webapp/webapp_data_source:feature_flag_types",
"@npm//@angular/core",
],
)

tf_ts_library(
name = "types",
srcs = [
Expand All @@ -29,6 +42,16 @@ tf_ts_library(
tf_ts_library(
name = "testing",
testonly = True,
srcs = ["testing.ts"],
deps = [":types"],
srcs = [
"force_svg_data_source_test.ts",
"testing.ts",
],
deps = [
":force_svg_data_source",
":types",
"//tensorboard/webapp/angular:expect_angular_core_testing",
"//tensorboard/webapp/util:local_storage_testing",
"//tensorboard/webapp/webapp_data_source:feature_flag_types",
"@npm//@types/jasmine",
],
)
4 changes: 4 additions & 0 deletions tensorboard/webapp/feature_flag/effects/BUILD
Expand Up @@ -8,9 +8,11 @@ tf_ng_module(
"feature_flag_effects.ts",
],
deps = [
"//tensorboard/webapp/feature_flag:force_svg_data_source",
"//tensorboard/webapp/feature_flag/actions",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
"//tensorboard/webapp/webapp_data_source:feature_flag",
"//tensorboard/webapp/webapp_data_source:feature_flag_types",
"@npm//@angular/core",
"@npm//@ngrx/effects",
Expand All @@ -29,7 +31,9 @@ tf_ng_module(
":effects",
"//tensorboard/webapp/angular:expect_angular_core_testing",
"//tensorboard/webapp/angular:expect_ngrx_store_testing",
"//tensorboard/webapp/feature_flag:force_svg_data_source",
"//tensorboard/webapp/feature_flag:testing",
"//tensorboard/webapp/feature_flag:types",
"//tensorboard/webapp/feature_flag/actions",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/feature_flag/store:types",
Expand Down
11 changes: 10 additions & 1 deletion tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts
Expand Up @@ -19,6 +19,7 @@ import {Action, createAction, Store} from '@ngrx/store';
import {combineLatestWith, map} from 'rxjs/operators';
import {TBFeatureFlagDataSource} from '../../webapp_data_source/tb_feature_flag_data_source_types';
import {partialFeatureFlagsLoaded} from '../actions/feature_flag_actions';
import {ForceSvgDataSource} from '../force_svg_data_source';
import {getIsAutoDarkModeAllowed} from '../store/feature_flag_selectors';
import {State} from '../store/feature_flag_types';

Expand All @@ -33,6 +34,13 @@ export class FeatureFlagEffects {
combineLatestWith(this.store.select(getIsAutoDarkModeAllowed)),
map(([, isDarkModeAllowed]) => {
const features = this.dataSource.getFeatures(isDarkModeAllowed);

if (features.forceSvg != null) {
this.forceSvgDataSource.updateForceSvgFlag(features.forceSvg);
} else {
features.forceSvg = this.forceSvgDataSource.getForceSvgFlag();
}

return partialFeatureFlagsLoaded({features});
})
)
Expand All @@ -41,7 +49,8 @@ export class FeatureFlagEffects {
constructor(
private readonly actions$: Actions,
private readonly store: Store<State>,
private readonly dataSource: TBFeatureFlagDataSource
private readonly dataSource: TBFeatureFlagDataSource,
private readonly forceSvgDataSource: ForceSvgDataSource
) {}

/** @export */
Expand Down
Expand Up @@ -23,21 +23,25 @@ import {
TestingTBFeatureFlagDataSource,
} from '../../webapp_data_source/tb_feature_flag_testing';
import {partialFeatureFlagsLoaded} from '../actions/feature_flag_actions';
import {ForceSvgDataSource} from '../force_svg_data_source';
import {ForceSvgDataSourceModule} from '../force_svg_data_source_module';
import {getIsAutoDarkModeAllowed} from '../store/feature_flag_selectors';
import {State} from '../store/feature_flag_types';
import {buildFeatureFlag} from '../testing';
import {FeatureFlags} from '../types';
import {FeatureFlagEffects} from './feature_flag_effects';

describe('feature_flag_effects', () => {
let actions: ReplaySubject<Action>;
let store: MockStore<State>;
let dataSource: TestingTBFeatureFlagDataSource;
let forceSvgDataSource: ForceSvgDataSource;
let effects: FeatureFlagEffects;

beforeEach(async () => {
actions = new ReplaySubject<Action>(1);
await TestBed.configureTestingModule({
imports: [TBFeatureFlagTestingModule],
imports: [TBFeatureFlagTestingModule, ForceSvgDataSourceModule],
providers: [
provideMockActions(actions),
FeatureFlagEffects,
Expand All @@ -47,6 +51,7 @@ describe('feature_flag_effects', () => {
effects = TestBed.inject(FeatureFlagEffects);
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
dataSource = TestBed.inject(TestingTBFeatureFlagDataSource);
forceSvgDataSource = TestBed.inject(ForceSvgDataSource);
store.overrideSelector(getIsAutoDarkModeAllowed, false);
});

Expand Down Expand Up @@ -79,5 +84,55 @@ describe('feature_flag_effects', () => {
}),
]);
});

it('calls updateForceSvgFlag when getFeatures returns a value for forceSvg', () => {
spyOn(dataSource, 'getFeatures').and.returnValue(
buildFeatureFlag({
forceSvg: true,
})
);
let updateSpy = spyOn(
forceSvgDataSource,
'updateForceSvgFlag'
).and.stub();

actions.next(effects.ngrxOnInitEffects());

expect(recordedActions).toEqual([
partialFeatureFlagsLoaded({
features: buildFeatureFlag({
forceSvg: true,
}),
}),
]);

expect(updateSpy).toHaveBeenCalledOnceWith(true);
});

it('gets forceSVG flag from ForceSvgDataSource when getFeatures returns no value for forceSvg', () => {
let featureFlags: Partial<FeatureFlags> = buildFeatureFlag();
delete featureFlags.forceSvg;
spyOn(dataSource, 'getFeatures').and.returnValue(featureFlags);
let updateSpy = spyOn(
forceSvgDataSource,
'updateForceSvgFlag'
).and.stub();
let getSpy = spyOn(forceSvgDataSource, 'getForceSvgFlag').and.returnValue(
true
);

actions.next(effects.ngrxOnInitEffects());

expect(recordedActions).toEqual([
partialFeatureFlagsLoaded({
features: buildFeatureFlag({
forceSvg: true,
}),
}),
]);

expect(getSpy).toHaveBeenCalledOnceWith();
expect(updateSpy).toHaveBeenCalledTimes(0);
});
});
});
2 changes: 2 additions & 0 deletions tensorboard/webapp/feature_flag/feature_flag_module.ts
Expand Up @@ -23,6 +23,7 @@ import {
} from '../persistent_settings';
import {TBFeatureFlagModule} from '../webapp_data_source/tb_feature_flag_module';
import {FeatureFlagEffects} from './effects/feature_flag_effects';
import {ForceSvgDataSourceModule} from './force_svg_data_source_module';
import {reducers} from './store/feature_flag_reducers';
import {getEnableDarkModeOverride} from './store/feature_flag_selectors';
import {
Expand All @@ -44,6 +45,7 @@ export function getThemeSettingSelector() {

@NgModule({
imports: [
ForceSvgDataSourceModule,
TBFeatureFlagModule,
StoreModule.forFeature(
FEATURE_FLAG_FEATURE_KEY,
Expand Down
41 changes: 41 additions & 0 deletions tensorboard/webapp/feature_flag/force_svg_data_source.ts
@@ -0,0 +1,41 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import {Injectable} from '@angular/core';

const FORCE_SVG_RENDERER_KEY = '_tb_force_svg';

@Injectable()
export class ForceSvgDataSource {
constructor() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not using the LocalStorage abstraction that is used by PersistentSettingsDataSource:

https://cs.opensource.google/tensorflow/tensorboard/+/master:tensorboard/webapp/persistent_settings/_data_source/persistent_settings_data_source.ts;l=198;drc=b0f27571ce3e6ce322b9f1b91a4e8773f9ca47e0

I'm ok with that if you are -- I would lean towards LocalStorage being a bit of an overkill abstraction. But it would be nice to align PersistentSettingsDataSource and ForceSvgDataSource one way or the other. Mind sending a followup PR to remove LocalStorage abstraction use completely?

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


getForceSvgFlag(): boolean {
if (localStorage.getItem(FORCE_SVG_RENDERER_KEY)) {
return true;
}
return false;
}

updateForceSvgFlag(forceSvgFlag: boolean) {
if (forceSvgFlag) {
localStorage.setItem(FORCE_SVG_RENDERER_KEY, 'present');
} else {
localStorage.removeItem(FORCE_SVG_RENDERER_KEY);
}
}
}

export const TEST_ONLY = {
FORCE_SVG_RENDERER_KEY,
};
22 changes: 22 additions & 0 deletions tensorboard/webapp/feature_flag/force_svg_data_source_module.ts
@@ -0,0 +1,22 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {NgModule} from '@angular/core';
import {ForceSvgDataSource} from './force_svg_data_source';

@NgModule({
providers: [ForceSvgDataSource],
})
export class ForceSvgDataSourceModule {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating its own module I think you could just provide ForceSvgDataSource module in FeatureFlagModule. That eliminates a little bit of the overhead.

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 tried that at first. But including that in the effects build creates a circular dependency.

77 changes: 77 additions & 0 deletions tensorboard/webapp/feature_flag/force_svg_data_source_test.ts
@@ -0,0 +1,77 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import {TestBed} from '@angular/core/testing';
import {LocalStorageTestingModule} from '../util/local_storage_testing';
import {ForceSvgDataSource, TEST_ONLY} from './force_svg_data_source';

describe('feature_flag/force_svg_util test', () => {
let dataSource: ForceSvgDataSource;
let getItemReturnValue: string | null;
let getItemSpy: jasmine.Spy;
let setItemSpy: jasmine.Spy;
let removeItemSpy: jasmine.Spy;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LocalStorageTestingModule],
providers: [ForceSvgDataSource],
}).compileComponents();

getItemSpy = spyOn(window.localStorage, 'getItem').and.callFake(
(key: string) => {
return getItemReturnValue;
}
);
setItemSpy = spyOn(window.localStorage, 'setItem').and.stub();
removeItemSpy = spyOn(window.localStorage, 'removeItem').and.stub();
dataSource = TestBed.inject(ForceSvgDataSource);
});

describe('#getForceSVG', () => {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

it('returns false if localStorage.getItem returns null', () => {
getItemReturnValue = null;
const actual = dataSource.getForceSvgFlag();
expect(getItemSpy).toHaveBeenCalledOnceWith(
TEST_ONLY.FORCE_SVG_RENDERER_KEY
);
expect(actual).toBeFalse();
});

it('returns true if there is a value returned by localstorage.getItem with the key "forceSVG"', () => {
getItemReturnValue = 'this should not matter';
const actual = dataSource.getForceSvgFlag();
expect(getItemSpy).toHaveBeenCalledOnceWith(
TEST_ONLY.FORCE_SVG_RENDERER_KEY
);
expect(actual).toBeTruthy();
});
});

describe('updateForceSVG', () => {
it('creates localStorage entry with key forceSVG when passed truthy', () => {
dataSource.updateForceSvgFlag(true);
expect(setItemSpy).toHaveBeenCalledOnceWith(
TEST_ONLY.FORCE_SVG_RENDERER_KEY,
jasmine.any(String)
);
});
it('calls localStorage.removeItem with key forceSVG', () => {
dataSource.updateForceSvgFlag(false);
expect(removeItemSpy).toHaveBeenCalledOnceWith(
TEST_ONLY.FORCE_SVG_RENDERER_KEY
);
});
});
});
Expand Up @@ -129,3 +129,10 @@ export const getEnabledTimeNamespacedState = createSelector(
return flags.enabledTimeNamespacedState;
}
);

export const getForceSvgFeatureFlag = createSelector(
getFeatureFlags,
(flags: FeatureFlags): boolean => {
return flags.forceSvg;
}
);
Expand Up @@ -32,6 +32,7 @@ export const initialState: FeatureFlagState = {
enableTimeSeriesPromotion: false,
enabledCardWidthSetting: true,
enabledTimeNamespacedState: false,
forceSvg: false,
},
flagOverrides: {},
};
Expand Down
1 change: 1 addition & 0 deletions tensorboard/webapp/feature_flag/testing.ts
Expand Up @@ -32,6 +32,7 @@ export function buildFeatureFlag(
enableTimeSeriesPromotion: false,
enabledCardWidthSetting: false,
enabledTimeNamespacedState: false,
forceSvg: false,
...override,
};
}
3 changes: 3 additions & 0 deletions tensorboard/webapp/feature_flag/types.ts
Expand Up @@ -50,4 +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
// Scalar cards.
forceSvg: boolean;
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

}
1 change: 1 addition & 0 deletions tensorboard/webapp/metrics/views/card_renderer/BUILD
Expand Up @@ -268,6 +268,7 @@ tf_ng_module(
"//tensorboard/webapp/angular:expect_angular_material_menu",
"//tensorboard/webapp/angular:expect_angular_material_progress_spinner",
"//tensorboard/webapp/experiments:types",
"//tensorboard/webapp/feature_flag/store",
"//tensorboard/webapp/metrics:types",
"//tensorboard/webapp/metrics/data_source",
"//tensorboard/webapp/metrics/store",
Expand Down