UNPKG

chrome-devtools-frontend

Version:
493 lines (434 loc) 20.2 kB
// Copyright 2017 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import {Icon} from '../../ui/kit/kit.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {MobileThrottlingSelector} from './MobileThrottlingSelector.js'; import { type Conditions, type ConditionsList, type MobileThrottlingConditionsGroup, ThrottlingPresets, } from './ThrottlingPresets.js'; export interface CPUThrottlingSelectorWrapper { control: UI.Toolbar.ToolbarComboBox; updateRecommendedOption(recommendedOption: SDK.CPUThrottlingManager.CPUThrottlingOption|null): void; } const UIStrings = { /** *@description Text to indicate the network connectivity is offline */ offline: 'Offline', /** *@description Text in Throttling Manager of the Network panel */ forceDisconnectedFromNetwork: 'Force disconnected from network', /** * @description Text for throttling the network */ throttling: 'Throttling', /** * @description Icon title in Throttling Manager of the Network panel */ cpuThrottlingIsEnabled: 'CPU throttling is enabled', /** * @description Screen reader label for a select box that chooses the CPU throttling speed in the Performance panel */ cpuThrottling: 'CPU throttling', /** * @description Tooltip text in Throttling Manager of the Performance panel */ excessConcurrency: 'Exceeding the default value may degrade system performance.', /** * @description Tooltip text in Throttling Manager of the Performance panel */ resetConcurrency: 'Reset to the default value', /** * @description Label for an check box that neables overriding navigator.hardwareConcurrency */ hardwareConcurrency: 'Hardware concurrency', /** * @description Tooltip text for an input box that overrides navigator.hardwareConcurrency on the page */ hardwareConcurrencySettingLabel: 'Override the value reported by navigator.hardwareConcurrency', /** * @description Text label for a selection box showing that a specific option is recommended for CPU or Network throttling. * @example {Fast 4G} PH1 * @example {4x slowdown} PH1 */ recommendedThrottling: '{PH1} – recommended', /** * @description Text to prompt the user to run the CPU calibration process. */ calibrate: 'Calibrate…', /** * @description Text to prompt the user to re-run the CPU calibration process. */ recalibrate: 'Recalibrate…', /** * @description Text to indicate Save-Data override is not set. */ noSaveDataOverride: '\'Save-Data\': default', /** * @description Text to indicate Save-Data override is set to Enabled. */ saveDataOn: '\'Save-Data\': on', /** * @description Text to indicate Save-Data override is set to Disabled. */ saveDataOff: '\'Save-Data\': off', /** * @description Tooltip text for an select element that overrides navigator.connection.saveData on the page */ saveDataSettingTooltip: 'Override the value reported by navigator.connection.saveData on the page', } as const; const str_ = i18n.i18n.registerUIStrings('panels/mobile_throttling/ThrottlingManager.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let throttlingManagerInstance: ThrottlingManager; class PromiseQueue<T> { #promise = Promise.resolve(); push(promise: Promise<T>): Promise<T> { return new Promise(r => { this.#promise = this.#promise.then(async () => r(await promise)); }); } } export class ThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<ThrottlingManager.EventTypes> { private readonly cpuThrottlingControls: Set<UI.Toolbar.ToolbarComboBox>; private readonly cpuThrottlingOptions: SDK.CPUThrottlingManager.CPUThrottlingOption[]; private readonly customNetworkConditionsSetting: Common.Settings.Setting<SDK.NetworkManager.Conditions[]>; private readonly currentNetworkThrottlingConditionKeySetting: Common.Settings.Setting<SDK.NetworkManager.ThrottlingConditionKey>; private readonly calibratedCpuThrottlingSetting: Common.Settings.Setting<SDK.CPUThrottlingManager.CalibratedCPUThrottling>; private lastNetworkThrottlingConditions!: SDK.NetworkManager.Conditions; private readonly cpuThrottlingManager: SDK.CPUThrottlingManager.CPUThrottlingManager; #hardwareConcurrencyOverrideEnabled = false; readonly #emulationQueue = new PromiseQueue<void>(); get hardwareConcurrencyOverrideEnabled(): boolean { return this.#hardwareConcurrencyOverrideEnabled; } private constructor() { super(); this.cpuThrottlingManager = SDK.CPUThrottlingManager.CPUThrottlingManager.instance(); this.cpuThrottlingManager.addEventListener( SDK.CPUThrottlingManager.Events.RATE_CHANGED, (event: Common.EventTarget.EventTargetEvent<number>) => this.onCPUThrottlingRateChangedOnSDK(event.data)); this.cpuThrottlingControls = new Set(); this.cpuThrottlingOptions = ThrottlingPresets.cpuThrottlingPresets; this.customNetworkConditionsSetting = SDK.NetworkManager.customUserNetworkConditionsSetting(); this.currentNetworkThrottlingConditionKeySetting = SDK.NetworkManager.activeNetworkThrottlingKeySetting(); this.calibratedCpuThrottlingSetting = Common.Settings.Settings.instance().createSetting<SDK.CPUThrottlingManager.CalibratedCPUThrottling>( 'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL); SDK.NetworkManager.MultitargetNetworkManager.instance().addEventListener( SDK.NetworkManager.MultitargetNetworkManager.Events.CONDITIONS_CHANGED, () => { this.lastNetworkThrottlingConditions = this.#getCurrentNetworkConditions(); const conditions = SDK.NetworkManager.MultitargetNetworkManager.instance().networkConditions(); this.currentNetworkThrottlingConditionKeySetting.set(conditions.key); }); if (this.isDirty()) { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions(this.#getCurrentNetworkConditions()); } } #getCurrentNetworkConditions(): SDK.NetworkManager.Conditions { const activeKey = this.currentNetworkThrottlingConditionKeySetting.get(); const definition = SDK.NetworkManager.getPredefinedCondition(activeKey); if (definition) { return definition; } const custom = this.customNetworkConditionsSetting.get().find(conditions => conditions.key === activeKey); // Fall back to NoThrottling if we failed to find a match. return custom ?? SDK.NetworkManager.NoThrottlingConditions; } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ThrottlingManager { const {forceNew} = opts; if (!throttlingManagerInstance || forceNew) { throttlingManagerInstance = new ThrottlingManager(); } return throttlingManagerInstance; } createOfflineToolbarCheckbox(): UI.Toolbar.ToolbarCheckbox { const checkbox = new UI.Toolbar.ToolbarCheckbox( i18nString(UIStrings.offline), i18nString(UIStrings.forceDisconnectedFromNetwork), forceOffline.bind(this)); checkbox.element.setAttribute('jslog', `${VisualLogging.toggle('disconnect-from-network').track({click: true})}`); SDK.NetworkManager.MultitargetNetworkManager.instance().addEventListener( SDK.NetworkManager.MultitargetNetworkManager.Events.CONDITIONS_CHANGED, networkConditionsChanged); checkbox.setChecked(SDK.NetworkManager.MultitargetNetworkManager.instance().isOffline()); function forceOffline(this: ThrottlingManager): void { if (checkbox.checked()) { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions( SDK.NetworkManager.OfflineConditions); } else { const newConditions = (!this.lastNetworkThrottlingConditions.download && !this.lastNetworkThrottlingConditions.upload) ? SDK.NetworkManager.NoThrottlingConditions : this.lastNetworkThrottlingConditions; SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions(newConditions); } } function networkConditionsChanged(): void { checkbox.setChecked(SDK.NetworkManager.MultitargetNetworkManager.instance().isOffline()); } return checkbox; } createMobileThrottlingButton(): UI.Toolbar.ToolbarMenuButton { const button = new UI.Toolbar.ToolbarMenuButton(appendItems, undefined, undefined, 'mobile-throttling'); button.setTitle(i18nString(UIStrings.throttling)); button.setDarkText(); let options: ConditionsList = []; let selectedIndex = -1; const selector = new MobileThrottlingSelector(populate, select); return button; function appendItems(contextMenu: UI.ContextMenu.ContextMenu): void { for (let index = 0; index < options.length; ++index) { const conditions = options[index]; if (!conditions) { continue; } if (conditions.title === ThrottlingPresets.getCustomConditions().title && conditions.description === ThrottlingPresets.getCustomConditions().description) { continue; } contextMenu.defaultSection().appendCheckboxItem( conditions.title, selector.optionSelected.bind(selector, conditions as Conditions), {checked: selectedIndex === index, jslogContext: conditions.jslogContext}); } } function populate(groups: MobileThrottlingConditionsGroup[]): ConditionsList { options = []; for (const group of groups) { for (const conditions of group.items) { options.push(conditions); } options.push(null); } return options; } function select(index: number): void { selectedIndex = index; const option = options[index]; if (option) { button.setText(option.title); button.setTitle(`${option.title}: ${option.description}`); } } } private updatePanelIcon(): void { const warnings = []; if (this.cpuThrottlingManager.cpuThrottlingRate() !== SDK.CPUThrottlingManager.CPUThrottlingRates.NO_THROTTLING) { warnings.push(i18nString(UIStrings.cpuThrottlingIsEnabled)); } UI.InspectorView.InspectorView.instance().setPanelWarnings('timeline', warnings); } setCPUThrottlingOption(option: SDK.CPUThrottlingManager.CPUThrottlingOption): void { // This will transitively call onCPUThrottlingRateChangedOnSDK. this.cpuThrottlingManager.setCPUThrottlingOption(option); } onCPUThrottlingRateChangedOnSDK(rate: number): void { if (rate !== SDK.CPUThrottlingManager.CPUThrottlingRates.NO_THROTTLING) { Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuThrottlingEnabled); } const index = this.cpuThrottlingOptions.indexOf(this.cpuThrottlingManager.cpuThrottlingOption()); for (const control of this.cpuThrottlingControls) { control.setSelectedIndex(index); } this.updatePanelIcon(); } createCPUThrottlingSelector(): CPUThrottlingSelectorWrapper { const getCalibrationString = (): Common.UIString.LocalizedString => { const value = this.calibratedCpuThrottlingSetting.get(); const hasCalibrated = value.low || value.mid; return hasCalibrated ? i18nString(UIStrings.recalibrate) : i18nString(UIStrings.calibrate); }; const optionSelected = (): void => { if (control.selectedIndex() === control.options().length - 1) { const index = this.cpuThrottlingOptions.indexOf(this.cpuThrottlingManager.cpuThrottlingOption()); control.setSelectedIndex(index); void Common.Revealer.reveal(this.calibratedCpuThrottlingSetting); } else { this.setCPUThrottlingOption(this.cpuThrottlingOptions[control.selectedIndex()]); } }; const control = new UI.Toolbar.ToolbarComboBox(optionSelected, i18nString(UIStrings.cpuThrottling), '', 'cpu-throttling'); this.cpuThrottlingControls.add(control); const currentOption = this.cpuThrottlingManager.cpuThrottlingOption(); const optionEls: HTMLOptionElement[] = []; const options = this.cpuThrottlingOptions; for (let i = 0; i < this.cpuThrottlingOptions.length; ++i) { const option = this.cpuThrottlingOptions[i]; const title = option.title(); const value = option.jslogContext; const optionEl = control.createOption(title, value); control.addOption(optionEl); if (currentOption === option) { control.setSelectedIndex(i); } optionEls.push(optionEl); } const optionEl = control.createOption(getCalibrationString(), ''); control.addOption(optionEl); optionEls.push(optionEl); return { control, updateRecommendedOption(recommendedOption: SDK.CPUThrottlingManager.CPUThrottlingOption|null) { for (let i = 0; i < optionEls.length - 1; i++) { const option = options[i]; optionEls[i].text = option === recommendedOption ? i18nString(UIStrings.recommendedThrottling, {PH1: option.title()}) : option.title(); optionEls[i].disabled = option.rate() === 0; } optionEls[optionEls.length - 1].textContent = getCalibrationString(); }, }; } createSaveDataOverrideSelector(className?: string): UI.Toolbar.ToolbarComboBox { const reset = new Option(i18nString(UIStrings.noSaveDataOverride), undefined, true, true); const enable = new Option(i18nString(UIStrings.saveDataOn)); const disable = new Option(i18nString(UIStrings.saveDataOff)); const handler = (e: Event): void => { const select = e.target as HTMLSelectElement; switch (select.selectedOptions.item(0)) { case reset: for (const emulationModel of SDK.TargetManager.TargetManager.instance().models( SDK.EmulationModel.EmulationModel)) { void this.#emulationQueue.push( emulationModel.setDataSaverOverride(SDK.EmulationModel.DataSaverOverride.UNSET)); } break; case enable: for (const emulationModel of SDK.TargetManager.TargetManager.instance().models( SDK.EmulationModel.EmulationModel)) { void this.#emulationQueue.push( emulationModel.setDataSaverOverride(SDK.EmulationModel.DataSaverOverride.ENABLED)); } break; case disable: for (const emulationModel of SDK.TargetManager.TargetManager.instance().models( SDK.EmulationModel.EmulationModel)) { void this.#emulationQueue.push( emulationModel.setDataSaverOverride(SDK.EmulationModel.DataSaverOverride.DISABLED)); } break; } this.dispatchEventToListeners(ThrottlingManager.Events.SAVE_DATA_OVERRIDE_CHANGED, select.selectedIndex); }; const select = new UI.Toolbar.ToolbarComboBox(handler, i18nString(UIStrings.saveDataSettingTooltip), className); select.addOption(reset); select.addOption(enable); select.addOption(disable); this.addEventListener( ThrottlingManager.Events.SAVE_DATA_OVERRIDE_CHANGED, ({data}) => select.setSelectedIndex(data)); return select; } /** Hardware Concurrency doesn't store state in a setting. */ createHardwareConcurrencySelector(): { numericInput: UI.Toolbar.ToolbarItem, reset: UI.Toolbar.ToolbarButton, warning: UI.Toolbar.ToolbarItem, checkbox: UI.UIUtils.CheckboxLabel, } { const numericInput = new UI.Toolbar.ToolbarItem(UI.UIUtils.createInput('devtools-text-input', 'number', 'hardware-concurrency')); numericInput.setTitle(i18nString(UIStrings.hardwareConcurrencySettingLabel)); const inputElement = numericInput.element; inputElement.min = '1'; numericInput.setEnabled(false); const checkbox = UI.UIUtils.CheckboxLabel.create( i18nString(UIStrings.hardwareConcurrency), false, i18nString(UIStrings.hardwareConcurrencySettingLabel), 'hardware-concurrency'); const reset = new UI.Toolbar.ToolbarButton('Reset concurrency', 'undo', undefined, 'hardware-concurrency-reset'); reset.setTitle(i18nString(UIStrings.resetConcurrency)); const icon = new Icon(); icon.name = 'warning-filled'; icon.classList.add('small'); const warning = new UI.Toolbar.ToolbarItem(icon); warning.setTitle(i18nString(UIStrings.excessConcurrency)); checkbox.disabled = true; // Prevent modification while still wiring things up asynchronously below reset.element.classList.add('concurrency-hidden'); warning.element.classList.add('concurrency-hidden'); void this.cpuThrottlingManager.getHardwareConcurrency().then(defaultValue => { if (defaultValue === undefined) { return; } const setHardwareConcurrency = (value: number): void => { if (value >= 1) { this.cpuThrottlingManager.setHardwareConcurrency(value); } if (value > defaultValue) { warning.element.classList.remove('concurrency-hidden'); } else { warning.element.classList.add('concurrency-hidden'); } if (value === defaultValue) { reset.element.classList.add('concurrency-hidden'); } else { reset.element.classList.remove('concurrency-hidden'); } }; inputElement.value = `${defaultValue}`; inputElement.oninput = () => setHardwareConcurrency(Number(inputElement.value)); checkbox.disabled = false; checkbox.addEventListener('change', () => { this.#hardwareConcurrencyOverrideEnabled = checkbox.checked; numericInput.setEnabled(this.hardwareConcurrencyOverrideEnabled); setHardwareConcurrency(this.hardwareConcurrencyOverrideEnabled ? Number(inputElement.value) : defaultValue); }); reset.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => { inputElement.value = `${defaultValue}`; setHardwareConcurrency(defaultValue); }); }); return {numericInput, reset, warning, checkbox}; } setHardwareConcurrency(concurrency: number): void { this.cpuThrottlingManager.setHardwareConcurrency(concurrency); } private isDirty(): boolean { const networkConditions = SDK.NetworkManager.MultitargetNetworkManager.instance().networkConditions(); const knownCurrentConditions = this.#getCurrentNetworkConditions(); return !SDK.NetworkManager.networkConditionsEqual(networkConditions, knownCurrentConditions); } } export namespace ThrottlingManager { export const enum Events { SAVE_DATA_OVERRIDE_CHANGED = 'SaveDataOverrideChanged', } export interface EventTypes { [Events.SAVE_DATA_OVERRIDE_CHANGED]: number; } } export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { handleAction(_context: UI.Context.Context, actionId: string): boolean { if (actionId === 'network-conditions.network-online') { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions( SDK.NetworkManager.NoThrottlingConditions); return true; } if (actionId === 'network-conditions.network-low-end-mobile') { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions(SDK.NetworkManager.Slow3GConditions); return true; } if (actionId === 'network-conditions.network-mid-tier-mobile') { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions(SDK.NetworkManager.Slow4GConditions); return true; } if (actionId === 'network-conditions.network-offline') { SDK.NetworkManager.MultitargetNetworkManager.instance().setNetworkConditions( SDK.NetworkManager.OfflineConditions); return true; } return false; } } export function throttlingManager(): ThrottlingManager { return ThrottlingManager.instance(); }