• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 The Android Open Source Project
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// This file should not import anything else. Since the flags will be used from
16// ~everywhere and the are "statically" initialized (i.e. files construct Flags
17// at import time) if this file starts importing anything we will quickly run
18// into issues with initialization order which will be a pain.
19
20interface FlagSettings {
21  id: string;
22  defaultValue: boolean;
23  description: string;
24  name?: string;
25  devOnly?: boolean;
26}
27
28export enum OverrideState {
29  DEFAULT = 'DEFAULT',
30  TRUE = 'OVERRIDE_TRUE',
31  FALSE = 'OVERRIDE_FALSE',
32}
33
34export interface FlagStore {
35  load(): object;
36  save(o: object): void;
37}
38
39// Stored state for a number of flags.
40interface FlagOverrides {
41  [id: string]: OverrideState;
42}
43
44// Check if the given object is a valid FlagOverrides.
45// This is necessary since someone could modify the persisted flags
46// behind our backs.
47function isFlagOverrides(o: object): o is FlagOverrides {
48  const states = [
49    OverrideState.TRUE.toString(),
50    OverrideState.FALSE.toString(),
51  ];
52  for (const v of Object.values(o)) {
53    if (typeof v !== 'string' || !states.includes(v)) {
54      return false;
55    }
56  }
57  return true;
58}
59
60class Flags {
61  private store: FlagStore;
62  private flags: Map<string, FlagImpl>;
63  private overrides: FlagOverrides;
64
65  constructor(store: FlagStore) {
66    this.store = store;
67    this.flags = new Map();
68    this.overrides = {};
69    this.load();
70  }
71
72  register(settings: FlagSettings): Flag {
73    const id = settings.id;
74    if (this.flags.has(id)) {
75      throw new Error(`Flag with id "${id}" is already registered.`);
76    }
77
78    const saved = this.overrides[id];
79    const state = saved === undefined ? OverrideState.DEFAULT : saved;
80    const flag = new FlagImpl(this, state, settings);
81    this.flags.set(id, flag);
82    return flag;
83  }
84
85  allFlags(): Flag[] {
86    const includeDevFlags = ['127.0.0.1', '::1', 'localhost'].includes(
87      window.location.hostname,
88    );
89
90    let flags = [...this.flags.values()];
91    flags = flags.filter((flag) => includeDevFlags || !flag.devOnly);
92    flags.sort((a, b) => a.name.localeCompare(b.name));
93    return flags;
94  }
95
96  resetAll() {
97    for (const flag of this.flags.values()) {
98      flag.state = OverrideState.DEFAULT;
99    }
100    this.save();
101  }
102
103  load(): void {
104    const o = this.store.load();
105    if (isFlagOverrides(o)) {
106      this.overrides = o;
107    }
108  }
109
110  save(): void {
111    for (const flag of this.flags.values()) {
112      if (flag.isOverridden()) {
113        this.overrides[flag.id] = flag.state;
114      } else {
115        delete this.overrides[flag.id];
116      }
117    }
118
119    this.store.save(this.overrides);
120  }
121}
122
123export interface Flag {
124  // A unique identifier for this flag ("magicSorting")
125  readonly id: string;
126
127  // The name of the flag the user sees ("New track sorting algorithm")
128  readonly name: string;
129
130  // A longer description which is displayed to the user.
131  // "Sort tracks using an embedded tfLite model based on your expression
132  // while waiting for the trace to load."
133  readonly description: string;
134
135  // Whether the flag defaults to true or false.
136  // If !flag.isOverridden() then flag.get() === flag.defaultValue
137  readonly defaultValue: boolean;
138
139  // Get the current value of the flag.
140  get(): boolean;
141
142  // Override the flag and persist the new value.
143  set(value: boolean): void;
144
145  // If the flag has been overridden.
146  // Note: A flag can be overridden to its default value.
147  isOverridden(): boolean;
148
149  // Reset the flag to its default setting.
150  reset(): void;
151
152  // Get the current state of the flag.
153  overriddenState(): OverrideState;
154}
155
156class FlagImpl implements Flag {
157  registry: Flags;
158  state: OverrideState;
159
160  readonly id: string;
161  readonly name: string;
162  readonly description: string;
163  readonly defaultValue: boolean;
164  readonly devOnly: boolean;
165
166  constructor(registry: Flags, state: OverrideState, settings: FlagSettings) {
167    this.registry = registry;
168    this.id = settings.id;
169    this.state = state;
170    this.description = settings.description;
171    this.defaultValue = settings.defaultValue;
172    this.name = settings.name || settings.id;
173    this.devOnly = settings.devOnly || false;
174  }
175
176  get(): boolean {
177    switch (this.state) {
178      case OverrideState.TRUE:
179        return true;
180      case OverrideState.FALSE:
181        return false;
182      case OverrideState.DEFAULT:
183      default:
184        return this.defaultValue;
185    }
186  }
187
188  set(value: boolean): void {
189    const next = value ? OverrideState.TRUE : OverrideState.FALSE;
190    if (this.state === next) {
191      return;
192    }
193    this.state = next;
194    this.registry.save();
195  }
196
197  overriddenState(): OverrideState {
198    return this.state;
199  }
200
201  reset() {
202    this.state = OverrideState.DEFAULT;
203    this.registry.save();
204  }
205
206  isOverridden(): boolean {
207    return this.state !== OverrideState.DEFAULT;
208  }
209}
210
211class LocalStorageStore implements FlagStore {
212  static KEY = 'perfettoFeatureFlags';
213
214  load(): object {
215    const s = localStorage.getItem(LocalStorageStore.KEY);
216    let parsed: object;
217    try {
218      parsed = JSON.parse(s || '{}');
219    } catch (e) {
220      return {};
221    }
222    if (typeof parsed !== 'object' || parsed === null) {
223      return {};
224    }
225    return parsed;
226  }
227
228  save(o: object): void {
229    const s = JSON.stringify(o);
230    localStorage.setItem(LocalStorageStore.KEY, s);
231  }
232}
233
234export const FlagsForTesting = Flags;
235export const featureFlags = new Flags(new LocalStorageStore());
236
237export const PERF_SAMPLE_FLAG = featureFlags.register({
238  id: 'perfSampleFlamegraph',
239  name: 'Perf Sample Flamegraph',
240  description: 'Show flamegraph generated by a perf sample.',
241  defaultValue: true,
242});
243
244export const RECORDING_V2_FLAG = featureFlags.register({
245  id: 'recordingv2',
246  name: 'Recording V2',
247  description: 'Record using V2 interface',
248  defaultValue: false,
249});
250