chrome-devtools-frontend
Version:
Chrome DevTools UI
283 lines (241 loc) • 10.1 kB
text/typescript
// 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.
import * as Common from '../../core/common/common.js';
import * as i18n from '../i18n/i18n.js';
import {EmulationModel} from './EmulationModel.js';
import {type SDKModelObserver, TargetManager} from './TargetManager.js';
const UIStrings = {
/**
* @description Text label for a menu item indicating that no throttling is applied.
*/
noThrottling: 'No throttling',
/**
* @description Text label for a menu item indicating that a specific slowdown multiplier is applied.
* @example {2} PH1
*/
dSlowdown: '{PH1}× slowdown',
/**
* @description Text label for a menu item indicating an average mobile device.
*/
calibratedMidTierMobile: 'Mid-tier mobile',
/**
* @description Text label for a menu item indicating a below-average mobile device.
*/
calibratedLowTierMobile: 'Low-tier mobile',
/**
* @description Text label indicating why an option is not available, because the user's device is not fast enough to emulate a device.
*/
calibrationErrorDeviceTooWeak: 'Device is not powerful enough',
} as const;
const str_ = i18n.i18n.registerUIStrings('core/sdk/CPUThrottlingManager.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
let throttlingManagerInstance: CPUThrottlingManager;
export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
SDKModelObserver<EmulationModel> {
#cpuThrottlingOption: CPUThrottlingOption;
#calibratedThrottlingSetting: Common.Settings.Setting<CalibratedCPUThrottling>;
#hardwareConcurrency?: number;
#pendingMainTargetPromise?: (r: number) => void;
private constructor() {
super();
this.#cpuThrottlingOption = NoThrottlingOption;
this.#calibratedThrottlingSetting = Common.Settings.Settings.instance().createSetting<CalibratedCPUThrottling>(
'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL);
this.#calibratedThrottlingSetting.addChangeListener(this.#onCalibratedSettingChanged, this);
TargetManager.instance().observeModels(EmulationModel, this);
}
static instance(opts: {forceNew: boolean|null} = {forceNew: null}): CPUThrottlingManager {
const {forceNew} = opts;
if (!throttlingManagerInstance || forceNew) {
throttlingManagerInstance = new CPUThrottlingManager();
}
return throttlingManagerInstance;
}
cpuThrottlingRate(): number {
return this.#cpuThrottlingOption.rate();
}
cpuThrottlingOption(): CPUThrottlingOption {
return this.#cpuThrottlingOption;
}
#onCalibratedSettingChanged(): void {
// If a calibrated option is selected, need to propagate new rate.
const currentOption = this.#cpuThrottlingOption;
if (!currentOption.calibratedDeviceType) {
return;
}
const rate = this.#cpuThrottlingOption.rate();
if (rate === 0) {
// This calibrated option is no longer valid.
this.setCPUThrottlingOption(NoThrottlingOption);
return;
}
for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
void emulationModel.setCPUThrottlingRate(rate);
}
this.dispatchEventToListeners(Events.RATE_CHANGED, rate);
}
setCPUThrottlingOption(option: CPUThrottlingOption): void {
if (option === this.#cpuThrottlingOption) {
return;
}
this.#cpuThrottlingOption = option;
for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingOption.rate());
}
this.dispatchEventToListeners(Events.RATE_CHANGED, this.#cpuThrottlingOption.rate());
}
setHardwareConcurrency(concurrency: number): void {
this.#hardwareConcurrency = concurrency;
for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
void emulationModel.setHardwareConcurrency(concurrency);
}
this.dispatchEventToListeners(Events.HARDWARE_CONCURRENCY_CHANGED, this.#hardwareConcurrency);
}
hasPrimaryPageTargetSet(): boolean {
// In some environments, such as Node, trying to check if we have a page
// target may error. So if we get any errors here at all, assume that we do
// not have a target.
try {
return TargetManager.instance().primaryPageTarget() !== null;
} catch {
return false;
}
}
async getHardwareConcurrency(): Promise<number> {
const target = TargetManager.instance().primaryPageTarget();
const existingCallback = this.#pendingMainTargetPromise;
// If the main target hasn't attached yet, block callers until it appears.
if (!target) {
if (existingCallback) {
return await new Promise(r => {
this.#pendingMainTargetPromise = (result: number) => {
r(result);
existingCallback(result);
};
});
}
return await new Promise(r => {
this.#pendingMainTargetPromise = r;
});
}
const evalResult = await target.runtimeAgent().invoke_evaluate(
{expression: 'navigator.hardwareConcurrency', returnByValue: true, silent: true, throwOnSideEffect: true});
const error = evalResult.getError();
if (error) {
throw new Error(error);
}
const {result, exceptionDetails} = evalResult;
if (exceptionDetails) {
throw new Error(exceptionDetails.text);
}
return result.value;
}
modelAdded(emulationModel: EmulationModel): void {
if (this.#cpuThrottlingOption !== NoThrottlingOption) {
void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingOption.rate());
}
if (this.#hardwareConcurrency !== undefined) {
void emulationModel.setHardwareConcurrency(this.#hardwareConcurrency);
}
// If there are any callers blocked on a getHardwareConcurrency call, let's wake them now.
if (this.#pendingMainTargetPromise) {
const existingCallback = this.#pendingMainTargetPromise;
this.#pendingMainTargetPromise = undefined;
void this.getHardwareConcurrency().then(existingCallback);
}
}
modelRemoved(_emulationModel: EmulationModel): void {
// Implemented as a requirement for being a SDKModelObserver.
}
}
export const enum Events {
RATE_CHANGED = 'RateChanged',
HARDWARE_CONCURRENCY_CHANGED = 'HardwareConcurrencyChanged',
}
export interface EventTypes {
[Events.RATE_CHANGED]: number;
[Events.HARDWARE_CONCURRENCY_CHANGED]: number;
}
export function throttlingManager(): CPUThrottlingManager {
return CPUThrottlingManager.instance();
}
export enum CPUThrottlingRates {
NO_THROTTLING = 1,
MID_TIER_MOBILE = 4,
LOW_TIER_MOBILE = 6,
EXTRA_SLOW = 20,
// eslint-disable-next-line @typescript-eslint/naming-convention -- Used by web_tests.
MidTierMobile = MID_TIER_MOBILE,
// eslint-disable-next-line @typescript-eslint/naming-convention -- Used by web_tests.
LowEndMobile = LOW_TIER_MOBILE,
}
export type CalibratedDeviceType = 'low-tier-mobile'|'mid-tier-mobile';
export interface CPUThrottlingOption {
title: () => string;
rate: () => number;
calibratedDeviceType?: CalibratedDeviceType;
jslogContext: string;
}
function makeFixedPresetThrottlingOption(rate: CPUThrottlingRates): CPUThrottlingOption {
return {
title: rate === 1 ? i18nLazyString(UIStrings.noThrottling) : i18nLazyString(UIStrings.dSlowdown, {PH1: rate}),
rate: () => rate,
jslogContext: rate === 1 ? 'cpu-no-throttling' : `cpu-throttled-${rate}`,
};
}
export const NoThrottlingOption = makeFixedPresetThrottlingOption(CPUThrottlingRates.NO_THROTTLING);
export const MidTierThrottlingOption = makeFixedPresetThrottlingOption(CPUThrottlingRates.MID_TIER_MOBILE);
export const LowTierThrottlingOption = makeFixedPresetThrottlingOption(CPUThrottlingRates.LOW_TIER_MOBILE);
export const ExtraSlowThrottlingOption = makeFixedPresetThrottlingOption(CPUThrottlingRates.EXTRA_SLOW);
function makeCalibratedThrottlingOption(calibratedDeviceType: CalibratedDeviceType): CPUThrottlingOption {
const getSettingValue = (): number|CalibrationError|null => {
const setting = Common.Settings.Settings.instance().createSetting<CalibratedCPUThrottling>(
'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL);
const value = setting.get();
if (calibratedDeviceType === 'low-tier-mobile') {
return value.low ?? null;
}
if (calibratedDeviceType === 'mid-tier-mobile') {
return value.mid ?? null;
}
return null;
};
return {
title(): string {
const typeString = calibratedDeviceType === 'low-tier-mobile' ? i18nString(UIStrings.calibratedLowTierMobile) :
i18nString(UIStrings.calibratedMidTierMobile);
const value = getSettingValue();
if (typeof value === 'number') {
return `${typeString} – ${value.toFixed(1)}×`;
}
return typeString;
},
rate(): number {
const value = getSettingValue();
if (typeof value === 'number') {
return value;
}
return 0;
},
calibratedDeviceType,
jslogContext: `cpu-throttled-calibrated-${calibratedDeviceType}`,
};
}
export const CalibratedLowTierMobileThrottlingOption = makeCalibratedThrottlingOption('low-tier-mobile');
export const CalibratedMidTierMobileThrottlingOption = makeCalibratedThrottlingOption('mid-tier-mobile');
export interface CalibratedCPUThrottling {
/** Either the CPU multiplier, or an error code for why it could not be determined. */
low?: number|CalibrationError;
mid?: number|CalibrationError;
}
export enum CalibrationError {
DEVICE_TOO_WEAK = 'DEVICE_TOO_WEAK',
}
export function calibrationErrorToString(error: CalibrationError): string {
if (error === CalibrationError.DEVICE_TOO_WEAK) {
return i18nString(UIStrings.calibrationErrorDeviceTooWeak);
}
return error;
}