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 87 let flags = [...this.flags.values()]; 88 flags = flags.filter((flag) => includeDevFlags || !flag.devOnly); 89 flags.sort((a, b) => a.name.localeCompare(b.name)); 90 return flags; 91 } 92 93 resetAll() { 94 for (const flag of this.flags.values()) { 95 flag.state = OverrideState.DEFAULT; 96 } 97 this.save(); 98 } 99 100 load(): void { 101 const o = this.store.load(); 102 if (isFlagOverrides(o)) { 103 this.overrides = o; 104 } 105 } 106 107 save(): void { 108 for (const flag of this.flags.values()) { 109 if (flag.isOverridden()) { 110 this.overrides[flag.id] = flag.state; 111 } else { 112 delete this.overrides[flag.id]; 113 } 114 } 115 116 this.store.save(this.overrides); 117 } 118} 119 120export interface Flag { 121 // A unique identifier for this flag ("magicSorting") 122 readonly id: string; 123 124 // The name of the flag the user sees ("New track sorting algorithm") 125 readonly name: string; 126 127 // A longer description which is displayed to the user. 128 // "Sort tracks using an embedded tfLite model based on your expression 129 // while waiting for the trace to load." 130 readonly description: string; 131 132 // Whether the flag defaults to true or false. 133 // If !flag.isOverridden() then flag.get() === flag.defaultValue 134 readonly defaultValue: boolean; 135 136 // Get the current value of the flag. 137 get(): boolean; 138 139 // Override the flag and persist the new value. 140 set(value: boolean): void; 141 142 // If the flag has been overridden. 143 // Note: A flag can be overridden to its default value. 144 isOverridden(): boolean; 145 146 // Reset the flag to its default setting. 147 reset(): void; 148 149 // Get the current state of the flag. 150 overriddenState(): OverrideState; 151} 152 153class FlagImpl implements Flag { 154 registry: Flags; 155 state: OverrideState; 156 157 readonly id: string; 158 readonly name: string; 159 readonly description: string; 160 readonly defaultValue: boolean; 161 readonly devOnly: boolean; 162 163 constructor(registry: Flags, state: OverrideState, settings: FlagSettings) { 164 this.registry = registry; 165 this.id = settings.id; 166 this.state = state; 167 this.description = settings.description; 168 this.defaultValue = settings.defaultValue; 169 this.name = settings.name || settings.id; 170 this.devOnly = settings.devOnly || false; 171 } 172 173 get(): boolean { 174 switch (this.state) { 175 case OverrideState.TRUE: 176 return true; 177 case OverrideState.FALSE: 178 return false; 179 case OverrideState.DEFAULT: 180 default: 181 return this.defaultValue; 182 } 183 } 184 185 set(value: boolean): void { 186 const next = value ? OverrideState.TRUE : OverrideState.FALSE; 187 if (this.state === next) { 188 return; 189 } 190 this.state = next; 191 this.registry.save(); 192 } 193 194 overriddenState(): OverrideState { 195 return this.state; 196 } 197 198 reset() { 199 this.state = OverrideState.DEFAULT; 200 this.registry.save(); 201 } 202 203 isOverridden(): boolean { 204 return this.state !== OverrideState.DEFAULT; 205 } 206} 207 208class LocalStorageStore implements FlagStore { 209 static KEY = 'perfettoFeatureFlags'; 210 211 load(): object { 212 const s = localStorage.getItem(LocalStorageStore.KEY); 213 let parsed: object; 214 try { 215 parsed = JSON.parse(s || '{}'); 216 } catch (e) { 217 return {}; 218 } 219 if (typeof parsed !== 'object' || parsed === null) { 220 return {}; 221 } 222 return parsed; 223 } 224 225 save(o: object): void { 226 const s = JSON.stringify(o); 227 localStorage.setItem(LocalStorageStore.KEY, s); 228 } 229} 230 231export const FlagsForTesting = Flags; 232export const featureFlags = new Flags(new LocalStorageStore()); 233 234export const PERF_SAMPLE_FLAG = featureFlags.register({ 235 id: 'perfSampleFlamegraph', 236 name: 'Perf Sample Flamegraph', 237 description: 'Show flamegraph generated by a perf sample.', 238 defaultValue: true, 239}); 240 241export const RECORDING_V2_FLAG = featureFlags.register({ 242 id: 'recordingv2', 243 name: 'Recording V2', 244 description: 'Record using V2 interface', 245 defaultValue: false, 246}); 247