| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Platform from '../platform/platform.js'; |
| |
| const queryParamsObject = new URLSearchParams(location.search); |
| |
| let runtimePlatform = ''; |
| |
| let runtimeInstance: Runtime|undefined; |
| |
| export function getRemoteBase(location: string = self.location.toString()): { |
| base: string, |
| version: string, |
| }|null { |
| const url = new URL(location); |
| const remoteBase = url.searchParams.get('remoteBase'); |
| if (!remoteBase) { |
| return null; |
| } |
| |
| const version = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(remoteBase); |
| if (!version) { |
| return null; |
| } |
| |
| return {base: `devtools://devtools/remote/serve_file/${version[1]}/`, version: version[1]}; |
| } |
| |
| export class Runtime { |
| private constructor() { |
| } |
| |
| static instance(opts: { |
| forceNew: boolean|null, |
| }|undefined = {forceNew: null}): Runtime { |
| const {forceNew} = opts; |
| if (!runtimeInstance || forceNew) { |
| runtimeInstance = new Runtime(); |
| } |
| |
| return runtimeInstance; |
| } |
| |
| static removeInstance(): void { |
| runtimeInstance = undefined; |
| } |
| |
| static queryParam(name: string): string|null { |
| return queryParamsObject.get(name); |
| } |
| |
| static setQueryParamForTesting(name: string, value: string): void { |
| queryParamsObject.set(name, value); |
| } |
| |
| static experimentsSetting(): { |
| [x: string]: boolean, |
| } { |
| try { |
| return JSON.parse( |
| self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}') as { |
| [x: string]: boolean, |
| }; |
| } catch (e) { |
| console.error('Failed to parse localStorage[\'experiments\']'); |
| return {}; |
| } |
| } |
| |
| static setPlatform(platform: string): void { |
| runtimePlatform = platform; |
| } |
| |
| static platform(): string { |
| return runtimePlatform; |
| } |
| |
| static isDescriptorEnabled(descriptor: { |
| experiment: ((string | undefined)|null), |
| condition: ((string | undefined)|null), |
| }): boolean { |
| const activatorExperiment = descriptor['experiment']; |
| if (activatorExperiment === '*') { |
| return true; |
| } |
| if (activatorExperiment && activatorExperiment.startsWith('!') && |
| experiments.isEnabled(activatorExperiment.substring(1))) { |
| return false; |
| } |
| if (activatorExperiment && !activatorExperiment.startsWith('!') && !experiments.isEnabled(activatorExperiment)) { |
| return false; |
| } |
| const condition = descriptor['condition']; |
| if (condition && !condition.startsWith('!') && !Runtime.queryParam(condition)) { |
| return false; |
| } |
| if (condition && condition.startsWith('!') && Runtime.queryParam(condition.substring(1))) { |
| return false; |
| } |
| return true; |
| } |
| |
| loadLegacyModule(modulePath: string): Promise<void> { |
| return import(`../../${modulePath}`); |
| } |
| } |
| |
| export interface Option { |
| title: string; |
| value: string|boolean; |
| raw?: boolean; |
| text?: string; |
| } |
| |
| export class ExperimentsSupport { |
| #experiments: Experiment[]; |
| #experimentNames: Set<string>; |
| #enabledTransiently: Set<string>; |
| readonly #enabledByDefault: Set<string>; |
| readonly #serverEnabled: Set<string>; |
| // Experiments in this set won't be shown to the user |
| readonly #nonConfigurable: Set<string>; |
| constructor() { |
| this.#experiments = []; |
| this.#experimentNames = new Set(); |
| this.#enabledTransiently = new Set(); |
| this.#enabledByDefault = new Set(); |
| this.#serverEnabled = new Set(); |
| this.#nonConfigurable = new Set(); |
| } |
| |
| allConfigurableExperiments(): Experiment[] { |
| const result = []; |
| for (const experiment of this.#experiments) { |
| if (!this.#enabledTransiently.has(experiment.name) && !this.#nonConfigurable.has(experiment.name)) { |
| result.push(experiment); |
| } |
| } |
| return result; |
| } |
| |
| enabledExperiments(): Experiment[] { |
| return this.#experiments.filter(experiment => experiment.isEnabled()); |
| } |
| |
| private setExperimentsSetting(value: Object): void { |
| if (!self.localStorage) { |
| return; |
| } |
| self.localStorage['experiments'] = JSON.stringify(value); |
| } |
| |
| register( |
| experimentName: string, experimentTitle: string, unstable?: boolean, docLink?: string, |
| feedbackLink?: string): void { |
| Platform.DCHECK( |
| () => !this.#experimentNames.has(experimentName), 'Duplicate registration of experiment ' + experimentName); |
| this.#experimentNames.add(experimentName); |
| this.#experiments.push(new Experiment( |
| this, experimentName, experimentTitle, Boolean(unstable), |
| docLink as Platform.DevToolsPath.UrlString ?? Platform.DevToolsPath.EmptyUrlString, |
| feedbackLink as Platform.DevToolsPath.UrlString ?? Platform.DevToolsPath.EmptyUrlString)); |
| } |
| |
| isEnabled(experimentName: string): boolean { |
| this.checkExperiment(experimentName); |
| // Check for explicitly disabled #experiments first - the code could call setEnable(false) on the experiment enabled |
| // by default and we should respect that. |
| if (Runtime.experimentsSetting()[experimentName] === false) { |
| return false; |
| } |
| if (this.#enabledTransiently.has(experimentName) || this.#enabledByDefault.has(experimentName)) { |
| return true; |
| } |
| if (this.#serverEnabled.has(experimentName)) { |
| return true; |
| } |
| |
| return Boolean(Runtime.experimentsSetting()[experimentName]); |
| } |
| |
| setEnabled(experimentName: string, enabled: boolean): void { |
| this.checkExperiment(experimentName); |
| const experimentsSetting = Runtime.experimentsSetting(); |
| experimentsSetting[experimentName] = enabled; |
| this.setExperimentsSetting(experimentsSetting); |
| } |
| |
| enableExperimentsTransiently(experimentNames: string[]): void { |
| for (const experimentName of experimentNames) { |
| this.checkExperiment(experimentName); |
| this.#enabledTransiently.add(experimentName); |
| } |
| } |
| |
| enableExperimentsByDefault(experimentNames: string[]): void { |
| for (const experimentName of experimentNames) { |
| this.checkExperiment(experimentName); |
| this.#enabledByDefault.add(experimentName); |
| } |
| } |
| |
| setServerEnabledExperiments(experimentNames: string[]): void { |
| for (const experiment of experimentNames) { |
| this.checkExperiment(experiment); |
| this.#serverEnabled.add(experiment); |
| } |
| } |
| |
| setNonConfigurableExperiments(experimentNames: string[]): void { |
| for (const experiment of experimentNames) { |
| this.checkExperiment(experiment); |
| this.#nonConfigurable.add(experiment); |
| } |
| } |
| |
| enableForTest(experimentName: string): void { |
| this.checkExperiment(experimentName); |
| this.#enabledTransiently.add(experimentName); |
| } |
| |
| disableForTest(experimentName: string): void { |
| this.checkExperiment(experimentName); |
| this.#enabledTransiently.delete(experimentName); |
| } |
| |
| clearForTest(): void { |
| this.#experiments = []; |
| this.#experimentNames.clear(); |
| this.#enabledTransiently.clear(); |
| this.#enabledByDefault.clear(); |
| this.#serverEnabled.clear(); |
| } |
| |
| cleanUpStaleExperiments(): void { |
| const experimentsSetting = Runtime.experimentsSetting(); |
| const cleanedUpExperimentSetting: { |
| [x: string]: boolean, |
| } = {}; |
| for (const {name: experimentName} of this.#experiments) { |
| if (experimentsSetting.hasOwnProperty(experimentName)) { |
| const isEnabled = experimentsSetting[experimentName]; |
| if (isEnabled || this.#enabledByDefault.has(experimentName)) { |
| cleanedUpExperimentSetting[experimentName] = isEnabled; |
| } |
| } |
| } |
| this.setExperimentsSetting(cleanedUpExperimentSetting); |
| } |
| |
| private checkExperiment(experimentName: string): void { |
| Platform.DCHECK(() => this.#experimentNames.has(experimentName), 'Unknown experiment ' + experimentName); |
| } |
| } |
| |
| export class Experiment { |
| name: string; |
| title: string; |
| unstable: boolean; |
| docLink?: Platform.DevToolsPath.UrlString; |
| readonly feedbackLink?: Platform.DevToolsPath.UrlString; |
| readonly #experiments: ExperimentsSupport; |
| constructor( |
| experiments: ExperimentsSupport, name: string, title: string, unstable: boolean, |
| docLink: Platform.DevToolsPath.UrlString, feedbackLink: Platform.DevToolsPath.UrlString) { |
| this.name = name; |
| this.title = title; |
| this.unstable = unstable; |
| this.docLink = docLink; |
| this.feedbackLink = feedbackLink; |
| this.#experiments = experiments; |
| } |
| |
| isEnabled(): boolean { |
| return this.#experiments.isEnabled(this.name); |
| } |
| |
| setEnabled(enabled: boolean): void { |
| this.#experiments.setEnabled(this.name, enabled); |
| } |
| } |
| |
| // This must be constructed after the query parameters have been parsed. |
| export const experiments = new ExperimentsSupport(); |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum ExperimentName { |
| CAPTURE_NODE_CREATION_STACKS = 'captureNodeCreationStacks', |
| CSS_OVERVIEW = 'cssOverview', |
| LIVE_HEAP_PROFILE = 'liveHeapProfile', |
| DEVELOPER_RESOURCES_VIEW = 'developerResourcesView', |
| CSP_VIOLATIONS_VIEW = 'cspViolationsView', |
| WASM_DWARF_DEBUGGING = 'wasmDWARFDebugging', |
| ALL = '*', |
| PROTOCOL_MONITOR = 'protocolMonitor', |
| WEBAUTHN_PANE = 'webauthnPane', |
| FULL_ACCESSIBILITY_TREE = 'fullAccessibilityTree', |
| PRECISE_CHANGES = 'preciseChanges', |
| STYLES_PANE_CSS_CHANGES = 'stylesPaneCSSChanges', |
| HEADER_OVERRIDES = 'headerOverrides', |
| EYEDROPPER_COLOR_PICKER = 'eyedropperColorPicker', |
| INSTRUMENTATION_BREAKPOINTS = 'instrumentationBreakpoints', |
| CSS_AUTHORING_HINTS = 'cssAuthoringHints', |
| AUTHORED_DEPLOYED_GROUPING = 'authoredDeployedGrouping', |
| IMPORTANT_DOM_PROPERTIES = 'importantDOMProperties', |
| JUST_MY_CODE = 'justMyCode', |
| BREAKPOINT_VIEW = 'breakpointView', |
| PRELOADING_STATUS_PANEL = 'preloadingStatusPanel', |
| DISABLE_COLOR_FORMAT_SETTING = 'disableColorFormatSetting', |
| TIMELINE_AS_CONSOLE_PROFILE_RESULT_PANEL = 'timelineAsConsoleProfileResultPanel', |
| OUTERMOST_TARGET_SELECTOR = 'outermostTargetSelector', |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum ConditionName { |
| CAN_DOCK = 'can_dock', |
| NOT_SOURCES_HIDE_ADD_FOLDER = '!sources.hide_add_folder', |
| } |