[go: nahoru, domu]

blob: 9b31d3ac8e8344096f86cdbb6e71f64e5b759447 [file] [log] [blame]
// 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',
}