Skip to content

Commit 3a3ab49

Browse files
authored
Add mechanism to prevent state to be persisted in local storage. (#6267)
For Google-internal versions of TensorBoard, there are some scenarios where we do not want to read or write user settings to local storage. This PR adds a mechanism to facilitate turning persistence off, if desired. The PR introduces persistent_settings State with one property, shouldPersistSettings, and one selector, getShouldPersistSettings. In this codebase the value of this is always true. Google-internal versions of TensorBoard, however, may have their own reducers that set this property to false. The PR also modifies persistent_settings effects to read the value of shouldPersistSettings before determining whether to read and write to the persistent data source. The effects now wait until the first navigating() event before attempting to read from the data source, in order to give other layers a small amount of time to set the shouldPersistSettings state before the first read attempt. There's nothing particularly special about using the navigating() event for this, other than it is fired late enough for our current use cases. We could consider using a different, later, action in the future. Googlers, see how one internal TensorBoard overrides this property in cl/518939981.
1 parent 89475fc commit 3a3ab49

12 files changed

+304
-26
lines changed

tensorboard/webapp/persistent_settings/BUILD

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ tf_ng_module(
1212
":config_module",
1313
"//tensorboard/webapp/persistent_settings/_data_source",
1414
"//tensorboard/webapp/persistent_settings/_data_source:types",
15+
"//tensorboard/webapp/persistent_settings/_redux",
1516
"//tensorboard/webapp/persistent_settings/_redux:persistent_settings_actions",
16-
"//tensorboard/webapp/persistent_settings/_redux:persistent_settings_effects",
17+
"//tensorboard/webapp/persistent_settings/_redux:types",
1718
"@npm//@angular/core",
1819
"@npm//@ngrx/effects",
1920
"@npm//@ngrx/store",
@@ -66,6 +67,6 @@ tf_ng_web_test_suite(
6667
deps = [
6768
":config_module_test_lib",
6869
"//tensorboard/webapp/persistent_settings/_data_source:_data_source_test_lib",
69-
"//tensorboard/webapp/persistent_settings/_redux:persistent_settings_effects_test_lib",
70+
"//tensorboard/webapp/persistent_settings/_redux:_redux_test_lib",
7071
],
7172
)

tensorboard/webapp/persistent_settings/_redux/BUILD

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tensorboard/defs:defs.bzl", "tf_ng_module")
1+
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_ts_library")
22

33
package(default_visibility = ["//tensorboard:internal"])
44

@@ -14,12 +14,16 @@ tf_ng_module(
1414
)
1515

1616
tf_ng_module(
17-
name = "persistent_settings_effects",
17+
name = "_redux",
1818
srcs = [
1919
"persistent_settings_effects.ts",
20+
"persistent_settings_reducers.ts",
21+
"persistent_settings_selectors.ts",
2022
],
2123
deps = [
2224
":persistent_settings_actions",
25+
":types",
26+
"//tensorboard/webapp/app_routing/actions",
2327
"//tensorboard/webapp/persistent_settings:config_module",
2428
"//tensorboard/webapp/persistent_settings/_data_source",
2529
"//tensorboard/webapp/persistent_settings/_data_source:types",
@@ -30,17 +34,39 @@ tf_ng_module(
3034
],
3135
)
3236

37+
tf_ts_library(
38+
name = "types",
39+
srcs = [
40+
"persistent_settings_types.ts",
41+
],
42+
)
43+
44+
tf_ts_library(
45+
name = "testing",
46+
testonly = True,
47+
srcs = [
48+
"testing.ts",
49+
],
50+
deps = [
51+
":types",
52+
],
53+
)
54+
3355
tf_ng_module(
34-
name = "persistent_settings_effects_test_lib",
56+
name = "_redux_test_lib",
3557
testonly = True,
3658
srcs = [
3759
"persistent_settings_effects_test.ts",
60+
"persistent_settings_selectors_test.ts",
3861
],
3962
deps = [
63+
":_redux",
4064
":persistent_settings_actions",
41-
":persistent_settings_effects",
65+
":testing",
4266
"//tensorboard/webapp/angular:expect_angular_core_testing",
4367
"//tensorboard/webapp/angular:expect_ngrx_store_testing",
68+
"//tensorboard/webapp/app_routing:testing",
69+
"//tensorboard/webapp/app_routing/actions",
4470
"//tensorboard/webapp/persistent_settings:config_module",
4571
"//tensorboard/webapp/persistent_settings/_data_source:testing",
4672
"//tensorboard/webapp/testing:utils",

tensorboard/webapp/persistent_settings/_redux/persistent_settings_effects.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,27 @@ limitations under the License.
1515
import {Injectable} from '@angular/core';
1616
import {Actions, createEffect, ofType, OnInitEffects} from '@ngrx/effects';
1717
import {Action, createAction, Store} from '@ngrx/store';
18-
import {merge, Observable} from 'rxjs';
18+
import {EMPTY, merge, Observable} from 'rxjs';
1919
import {
2020
buffer,
2121
debounceTime,
2222
delay,
2323
distinctUntilChanged,
24+
filter,
2425
mergeMap,
2526
share,
2627
skip,
28+
take,
2729
tap,
30+
withLatestFrom,
2831
} from 'rxjs/operators';
2932
import {PersistentSettingsConfigModule} from '../persistent_settings_config_module';
3033
import {PersistentSettingsDataSource} from '../_data_source/persistent_settings_data_source';
3134
import {PersistableSettings} from '../_data_source/types';
3235
import {globalSettingsLoaded} from './persistent_settings_actions';
36+
import {getShouldPersistSettings} from './persistent_settings_selectors';
37+
import * as appRoutingActions from '../../app_routing/actions';
3338

34-
const initAction = createAction('[Persistent Settings] Effects Init');
3539
const DEBOUNCE_PERIOD_IN_MS = 500;
3640

3741
/**
@@ -40,12 +44,15 @@ const DEBOUNCE_PERIOD_IN_MS = 500;
4044
* emit.
4145
*/
4246
@Injectable()
43-
export class PersistentSettingsEffects implements OnInitEffects {
47+
export class PersistentSettingsEffects {
4448
/** @export */
4549
readonly initializeAndUpdateSettings$: Observable<void> = createEffect(
4650
() => {
4751
const selectorsEmit$ = this.actions$.pipe(
48-
ofType(initAction),
52+
ofType(appRoutingActions.navigating),
53+
take(1),
54+
withLatestFrom(this.store.select(getShouldPersistSettings)),
55+
filter(([, shouldPersistSettings]) => shouldPersistSettings),
4956
mergeMap(() => this.dataSource.getSettings()),
5057
tap((partialSettings) => {
5158
this.store.dispatch(globalSettingsLoaded({partialSettings}));
@@ -85,14 +92,18 @@ export class PersistentSettingsEffects implements OnInitEffects {
8592
// Buffers changes from all selectors and only emit when debounce period
8693
// is over.
8794
buffer(selectorsEmit$.pipe(debounceTime(DEBOUNCE_PERIOD_IN_MS))),
88-
mergeMap((partialSettings) => {
89-
const partialSetting: Partial<PersistableSettings> = {};
95+
mergeMap((stateSettings) => {
96+
if (stateSettings.length === 0) {
97+
return EMPTY;
98+
}
99+
100+
const dataSourceSettings: Partial<PersistableSettings> = {};
90101
// Combine buffered setting changes. Last settings change would
91102
// overwrite earlier changes.
92-
for (const setting of partialSettings) {
93-
Object.assign(partialSetting, setting);
103+
for (const setting of stateSettings) {
104+
Object.assign(dataSourceSettings, setting);
94105
}
95-
return this.dataSource.setSettings(partialSetting);
106+
return this.dataSource.setSettings(dataSourceSettings);
96107
})
97108
);
98109
},
@@ -108,11 +119,6 @@ export class PersistentSettingsEffects implements OnInitEffects {
108119
>,
109120
private readonly dataSource: PersistentSettingsDataSource<PersistableSettings>
110121
) {}
111-
112-
/** @export */
113-
ngrxOnInitEffects(): Action {
114-
return initAction();
115-
}
116122
}
117123

118-
export const TEST_ONLY = {initAction, DEBOUNCE_PERIOD_IN_MS};
124+
export const TEST_ONLY = {DEBOUNCE_PERIOD_IN_MS};

tensorboard/webapp/persistent_settings/_redux/persistent_settings_effects_test.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import {
3333
PersistentSettingsEffects,
3434
TEST_ONLY,
3535
} from './persistent_settings_effects';
36+
import {getShouldPersistSettings} from './persistent_settings_selectors';
37+
import * as appRoutingActions from '../../app_routing/actions';
38+
import {buildRoute} from '../../app_routing/testing';
3639

3740
describe('persistent_settings effects test', () => {
3841
let action: ReplaySubject<Action>;
@@ -95,13 +98,13 @@ describe('persistent_settings effects test', () => {
9598
});
9699

97100
describe('on init', () => {
98-
it('fetches user settings and emits an aciton', () => {
101+
it('fetches user settings and emits an action', () => {
99102
getSettingsSpy.and.returnValue(
100103
of({
101104
ignoreOutliers: false,
102105
})
103106
);
104-
action.next(TEST_ONLY.initAction());
107+
action.next(appRoutingActions.navigating({after: buildRoute()}));
105108

106109
expect(actualActions).toEqual([
107110
globalSettingsLoaded({
@@ -112,12 +115,47 @@ describe('persistent_settings effects test', () => {
112115
]);
113116
});
114117

118+
it('does not fetch user settings if it should not', () => {
119+
store.overrideSelector(getShouldPersistSettings, false);
120+
store.refreshState();
121+
getSettingsSpy.and.returnValue(
122+
of({
123+
ignoreOutliers: false,
124+
})
125+
);
126+
action.next(appRoutingActions.navigating({after: buildRoute()}));
127+
128+
expect(actualActions).toEqual([]);
129+
expect(getSettingsSpy).not.toHaveBeenCalled();
130+
});
131+
132+
it('does not fetch again after first navigating event', () => {
133+
getSettingsSpy.and.returnValue(
134+
of({
135+
ignoreOutliers: false,
136+
})
137+
);
138+
139+
action.next(appRoutingActions.navigating({after: buildRoute()}));
140+
expect(actualActions).toEqual([
141+
globalSettingsLoaded({
142+
partialSettings: {
143+
ignoreOutliers: false,
144+
},
145+
}),
146+
]);
147+
148+
actualActions = [];
149+
action.next(appRoutingActions.navigating({after: buildRoute()}));
150+
expect(actualActions).toEqual([]);
151+
});
152+
115153
it('subscribes to selector changes after initial setting is read', fakeAsync(() => {
116154
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.1});
117155
const getSettingsSubject = new ReplaySubject(1);
118156

119157
getSettingsSpy.and.returnValue(getSettingsSubject);
120-
action.next(TEST_ONLY.initAction());
158+
action.next(appRoutingActions.navigating({after: buildRoute()}));
121159

122160
tick();
123161
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.3});
@@ -140,7 +178,7 @@ describe('persistent_settings effects test', () => {
140178
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.1});
141179

142180
getSettingsSpy.and.returnValue(of({}));
143-
action.next(TEST_ONLY.initAction());
181+
action.next(appRoutingActions.navigating({after: buildRoute()}));
144182

145183
tick(TEST_ONLY.DEBOUNCE_PERIOD_IN_MS * 2);
146184
expect(setSettingsSpy).not.toHaveBeenCalled();
@@ -151,8 +189,9 @@ describe('persistent_settings effects test', () => {
151189
function initializeAndSubscribe() {
152190
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.1});
153191
store.overrideSelector(setIgnoreOutliers, {ignoreOutliers: false});
192+
store.refreshState();
154193
getSettingsSpy.and.returnValue(of({}));
155-
action.next(TEST_ONLY.initAction());
194+
action.next(appRoutingActions.navigating({after: buildRoute()}));
156195
tick();
157196
}
158197

@@ -208,6 +247,30 @@ describe('persistent_settings effects test', () => {
208247
ignoreOutliers: true,
209248
});
210249
}));
250+
251+
it('persists settings when it should', fakeAsync(() => {
252+
store.overrideSelector(getShouldPersistSettings, true);
253+
initializeAndSubscribe();
254+
255+
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.3});
256+
store.refreshState();
257+
258+
tick(TEST_ONLY.DEBOUNCE_PERIOD_IN_MS);
259+
expect(setSettingsSpy).toHaveBeenCalledOnceWith({
260+
scalarSmoothing: 0.3,
261+
});
262+
}));
263+
264+
it('does not persist settings when it should not', fakeAsync(() => {
265+
store.overrideSelector(getShouldPersistSettings, false);
266+
initializeAndSubscribe();
267+
268+
store.overrideSelector(setSmoothingSelector, {scalarSmoothing: 0.3});
269+
store.refreshState();
270+
271+
tick(TEST_ONLY.DEBOUNCE_PERIOD_IN_MS);
272+
expect(setSettingsSpy).not.toHaveBeenCalled();
273+
}));
211274
});
212275
});
213276
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {Action, createReducer} from '@ngrx/store';
16+
17+
import {PersistentSettingsState} from './persistent_settings_types';
18+
19+
const initialState: PersistentSettingsState = {
20+
// In the OSS code base this value is always true but Google-internal
21+
// extensions may override it in certain circumstances.
22+
shouldPersistSettings: true,
23+
};
24+
25+
const reducer = createReducer(initialState);
26+
27+
export function reducers(
28+
state: PersistentSettingsState | undefined,
29+
action: Action
30+
) {
31+
return reducer(state, action);
32+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {createFeatureSelector, createSelector} from '@ngrx/store';
16+
17+
import {
18+
PERSISTENT_SETTINGS_FEATURE_KEY,
19+
PersistentSettingsState,
20+
} from './persistent_settings_types';
21+
22+
const selectPersistentSettings = createFeatureSelector<PersistentSettingsState>(
23+
PERSISTENT_SETTINGS_FEATURE_KEY
24+
);
25+
26+
export const getShouldPersistSettings = createSelector(
27+
selectPersistentSettings,
28+
(state: PersistentSettingsState): boolean => {
29+
return state.shouldPersistSettings;
30+
}
31+
);

0 commit comments

Comments
 (0)