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