• 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(), OverrideState.FALSE.toString()];
50  for (const v of Object.values(o)) {
51    if (typeof v !== 'string' || !states.includes(v)) {
52      return false;
53    }
54  }
55  return true;
56}
57
58class Flags {
59  private store: FlagStore;
60  private flags: Map<string, FlagImpl>;
61  private overrides: FlagOverrides;
62
63  constructor(store: FlagStore) {
64    this.store = store;
65    this.flags = new Map();
66    this.overrides = {};
67    this.load();
68  }
69
70  register(settings: FlagSettings): Flag {
71    const id = settings.id;
72    if (this.flags.has(id)) {
73      throw new Error(`Flag with id "${id}" is already registered.`);
74    }
75
76    const saved = this.overrides[id];
77    const state = saved === undefined ? OverrideState.DEFAULT : saved;
78    const flag = new FlagImpl(this, state, settings);
79    this.flags.set(id, flag);
80    return flag;
81  }
82
83  allFlags(): Flag[] {
84    const includeDevFlags =
85        ['127.0.0.1', '::1', 'localhost'].includes(window.location.hostname);
86    return [...this.flags.values()].filter(
87        flag => includeDevFlags || !flag.devOnly);
88  }
89
90  resetAll() {
91    for (const flag of this.flags.values()) {
92      flag.state = OverrideState.DEFAULT;
93    }
94    this.save();
95  }
96
97  load(): void {
98    const o = this.store.load();
99    if (isFlagOverrides(o)) {
100      this.overrides = o;
101    }
102  }
103
104  save(): void {
105    for (const flag of this.flags.values()) {
106      if (flag.isOverridden()) {
107        this.overrides[flag.id] = flag.state;
108      } else {
109        delete this.overrides[flag.id];
110      }
111    }
112
113    this.store.save(this.overrides);
114  }
115}
116
117export interface Flag {
118  // A unique identifier for this flag ("magicSorting")
119  readonly id: string;
120
121  // The name of the flag the user sees ("New track sorting algorithm")
122  readonly name: string;
123
124  // A longer description which is displayed to the user.
125  // "Sort tracks using an embedded tfLite model based on your expression
126  // while waiting for the trace to load."
127  readonly description: string;
128
129  // Whether the flag defaults to true or false.
130  // If !flag.isOverridden() then flag.get() === flag.defaultValue
131  readonly defaultValue: boolean;
132
133  // Get the current value of the flag.
134  get(): boolean;
135
136  // Override the flag and persist the new value.
137  set(value: boolean): void;
138
139  // If the flag has been overridden.
140  // Note: A flag can be overridden to its default value.
141  isOverridden(): boolean;
142
143  // Reset the flag to its default setting.
144  reset(): void;
145
146  // Get the current state of the flag.
147  overriddenState(): OverrideState;
148}
149
150class FlagImpl implements Flag {
151  registry: Flags;
152  state: OverrideState;
153
154  readonly id: string;
155  readonly name: string;
156  readonly description: string;
157  readonly defaultValue: boolean;
158  readonly devOnly: boolean;
159
160  constructor(registry: Flags, state: OverrideState, settings: FlagSettings) {
161    this.registry = registry;
162    this.id = settings.id;
163    this.state = state;
164    this.description = settings.description;
165    this.defaultValue = settings.defaultValue;
166    this.name = settings.name || settings.id;
167    this.devOnly = settings.devOnly || false;
168  }
169
170  get(): boolean {
171    switch (this.state) {
172      case OverrideState.TRUE:
173        return true;
174      case OverrideState.FALSE:
175        return false;
176      case OverrideState.DEFAULT:
177      default:
178        return this.defaultValue;
179    }
180  }
181
182  set(value: boolean): void {
183    const next = value ? OverrideState.TRUE : OverrideState.FALSE;
184    if (this.state === next) {
185      return;
186    }
187    this.state = next;
188    this.registry.save();
189  }
190
191  overriddenState(): OverrideState {
192    return this.state;
193  }
194
195  reset() {
196    this.state = OverrideState.DEFAULT;
197    this.registry.save();
198  }
199
200  isOverridden(): boolean {
201    return this.state !== OverrideState.DEFAULT;
202  }
203}
204
205class LocalStorageStore implements FlagStore {
206  static KEY = 'perfettoFeatureFlags';
207
208  load(): object {
209    const s = localStorage.getItem(LocalStorageStore.KEY);
210    let parsed: object;
211    try {
212      parsed = JSON.parse(s || '{}');
213    } catch (e) {
214      return {};
215    }
216    if (typeof parsed !== 'object' || parsed === null) {
217      return {};
218    }
219    return parsed;
220  }
221
222  save(o: object): void {
223    const s = JSON.stringify(o);
224    localStorage.setItem(LocalStorageStore.KEY, s);
225  }
226}
227
228export const FlagsForTesting = Flags;
229export const featureFlags = new Flags(new LocalStorageStore());
230
231export const PERF_SAMPLE_FLAG = featureFlags.register({
232  id: 'perfSampleFlamegraph',
233  name: 'Perf Sample Flamegraph',
234  description: 'Show flamegraph generated by a perf sample.',
235  defaultValue: true
236});
237