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