chrome-devtools-frontend
Version:
Chrome DevTools UI
345 lines (304 loc) • 12.5 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 Protocol from '../../generated/protocol.js';
import * as Root from '../root/root.js';
import {EmulationModel} from './EmulationModel.js';
import {cpuPerformanceSettingDescriptor} from './SDKSettings.js';
import {type SDKModelObserver, TargetManager} from './TargetManager.js';
export enum CalibrationError {
DEVICE_TOO_WEAK = 'DEVICE_TOO_WEAK',
}
export interface CalibratedCPUThrottling {
low?: number|CalibrationError;
mid?: number|CalibrationError;
actualScore?: number;
}
export import CPUPerformanceTier = Protocol.Emulation.SetCPUPerformanceOverrideRequestPerformanceTier;
export function numberToTier(value: number): CPUPerformanceTier {
switch (value) {
case 1:
return CPUPerformanceTier.Low;
case 2:
return CPUPerformanceTier.Mid;
case 3:
return CPUPerformanceTier.High;
case 4:
return CPUPerformanceTier.Ultra;
default:
// This defaults to UNKNOWN (0) if the value is out of range.
return CPUPerformanceTier.Unknown;
}
}
export function tierToNumber(tier: CPUPerformanceTier): number {
switch (tier) {
case CPUPerformanceTier.Unknown:
return 0;
case CPUPerformanceTier.Low:
return 1;
case CPUPerformanceTier.Mid:
return 2;
case CPUPerformanceTier.High:
return 3;
case CPUPerformanceTier.Ultra:
return 4;
}
}
export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
SDKModelObserver<EmulationModel> {
readonly #calibratedCpuThrottlingSetting: Common.Settings.Setting<CalibratedCPUThrottling>;
readonly #cpuPerformanceSetting: Common.Settings.Setting<string>;
readonly #targetManager: TargetManager;
#cpuThrottlingRate: number;
#hardwareConcurrency?: number;
#hostDefaultCPUPerformanceTier?: CPUPerformanceTier;
#manualCPUPerformanceOverride?: CPUPerformanceTier;
#pendingMainTargetPromise?: (r: number) => void;
constructor(settings: Common.Settings.Settings, targetManager: TargetManager) {
super();
this.#targetManager = targetManager;
this.#cpuThrottlingRate = 1; // No throttling
this.#calibratedCpuThrottlingSetting = settings.createSetting<CalibratedCPUThrottling>(
'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL);
this.#cpuPerformanceSetting = settings.resolve(cpuPerformanceSettingDescriptor);
}
initialize(): void {
this.#targetManager.observeModels(EmulationModel, this);
this.#cpuPerformanceSetting.addChangeListener(this.#onCPUPerformanceSettingChanged, this);
this.#onCPUPerformanceSettingChanged();
}
static instance(opts: {
forceNew?: boolean|null,
settings?: Common.Settings.Settings,
targetManager?: TargetManager,
} = {forceNew: null}): CPUThrottlingManager {
const {forceNew} = opts;
if (!Root.DevToolsContext.globalInstance().has(CPUThrottlingManager) || forceNew) {
/* eslint-disable @devtools/no-instance-of-migrated-singletons */
const manager = new CPUThrottlingManager(opts.settings ?? Common.Settings.Settings.instance(),
opts.targetManager ?? TargetManager.instance());
/* eslint-enable @devtools/no-instance-of-migrated-singletons */
manager.initialize();
Root.DevToolsContext.globalInstance().set(CPUThrottlingManager, manager);
}
return Root.DevToolsContext.globalInstance().get(CPUThrottlingManager);
}
static removeInstance(): void {
Root.DevToolsContext.globalInstance().delete(CPUThrottlingManager);
}
cpuThrottlingRate(): number {
return this.#cpuThrottlingRate;
}
calculatedCPUPerformanceTier(): CPUPerformanceTier|undefined {
// Return the calculated tier, based on the host hardware baseline and any active CPU throttling multiplier,
// ignoring any manual override via the sensors panel.
// https://docs.google.com/document/d/1cVUQxigT9GMJweyR4c5Gmj16DEE7GnMTWy-FFPMkl14
const NOMINAL_SCORES = {
LOW: 264,
MID: 1000,
HIGH: 2350,
ULTRA: 3000,
};
const calibratedSetting = this.#calibratedCpuThrottlingSetting.get();
const isCalibrated = typeof calibratedSetting.actualScore === 'number' && calibratedSetting.actualScore > 0;
let rLow: number;
let rMid: number;
let rHigh: number;
let rUltra: number;
if (isCalibrated) {
const sHost = calibratedSetting.actualScore as number;
rLow = typeof calibratedSetting.low === 'number' ? calibratedSetting.low : 1.0;
rMid = typeof calibratedSetting.mid === 'number' ? calibratedSetting.mid : 1.0;
rHigh = Math.min(rMid, Math.max(sHost / NOMINAL_SCORES.HIGH, 1.0));
rUltra = Math.min(rHigh, Math.max(sHost / NOMINAL_SCORES.ULTRA, 1.0));
} else {
// If the host default tier is undefined or unknown, return that.
const hostTier = this.#hostDefaultCPUPerformanceTier;
if (!hostTier || hostTier === CPUPerformanceTier.Unknown) {
return hostTier;
}
let sHost;
switch (hostTier) {
case CPUPerformanceTier.Low:
sHost = NOMINAL_SCORES.LOW;
break;
case CPUPerformanceTier.Mid:
sHost = NOMINAL_SCORES.MID;
break;
case CPUPerformanceTier.High:
sHost = NOMINAL_SCORES.HIGH;
break;
default:
sHost = NOMINAL_SCORES.ULTRA;
break;
}
rLow = Math.max(sHost / NOMINAL_SCORES.LOW, 1.0);
rMid = Math.max(sHost / NOMINAL_SCORES.MID, 1.0);
rHigh = Math.max(sHost / NOMINAL_SCORES.HIGH, 1.0);
rUltra = Math.max(sHost / NOMINAL_SCORES.ULTRA, 1.0);
}
const thetaLow = Math.sqrt(rLow * rMid);
const thetaMid = Math.sqrt(rMid * rHigh);
const thetaHigh = Math.sqrt(rHigh * rUltra);
if (this.#cpuThrottlingRate >= thetaLow) {
return CPUPerformanceTier.Low;
}
if (this.#cpuThrottlingRate >= thetaMid) {
return CPUPerformanceTier.Mid;
}
if (this.#cpuThrottlingRate >= thetaHigh) {
return CPUPerformanceTier.High;
}
return CPUPerformanceTier.Ultra;
}
effectiveCPUPerformanceTier(): CPUPerformanceTier|undefined {
// Return the tier, applying the manual override, if any.
if (this.#manualCPUPerformanceOverride !== undefined) {
return this.#manualCPUPerformanceOverride;
}
return this.calculatedCPUPerformanceTier();
}
#onCPUPerformanceSettingChanged(): void {
const val = this.#cpuPerformanceSetting.get();
this.#manualCPUPerformanceOverride = val === 'no-override' ? undefined : val as CPUPerformanceTier;
this.#syncCPUPerformanceTier();
if (this.#hostDefaultCPUPerformanceTier === undefined) {
void this.updateHostDefaultCPUPerformanceTier();
}
}
#isCPUPerformanceOverrideActive(): boolean {
return this.#manualCPUPerformanceOverride !== undefined || this.#cpuThrottlingRate !== 1;
}
#syncCPUPerformanceTier(): void {
const effectiveTier = this.effectiveCPUPerformanceTier();
// Synchronize with Chromium backend, via CDP.
const activeOverride = this.#isCPUPerformanceOverrideActive() ? effectiveTier : undefined;
for (const emulationModel of this.#targetManager.models(EmulationModel)) {
void emulationModel.setCPUPerformanceOverride(activeOverride);
}
// Notify UI and other listeners.
this.dispatchEventToListeners(Events.CPU_PERFORMANCE_TIER_CHANGED, effectiveTier);
}
setCPUThrottlingRate(rate: number): void {
if (rate === this.#cpuThrottlingRate) {
return;
}
this.#cpuThrottlingRate = rate;
for (const emulationModel of this.#targetManager.models(EmulationModel)) {
void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingRate);
}
this.dispatchEventToListeners(Events.RATE_CHANGED, this.#cpuThrottlingRate);
// Propagate changes to the effective tier only if not manually overridden.
if (this.#manualCPUPerformanceOverride === undefined) {
this.#syncCPUPerformanceTier();
if (this.#hostDefaultCPUPerformanceTier === undefined) {
void this.updateHostDefaultCPUPerformanceTier();
}
}
}
setHardwareConcurrency(concurrency: number): void {
this.#hardwareConcurrency = concurrency;
for (const emulationModel of this.#targetManager.models(EmulationModel)) {
void emulationModel.setHardwareConcurrency(concurrency);
}
this.dispatchEventToListeners(Events.HARDWARE_CONCURRENCY_CHANGED, this.#hardwareConcurrency);
}
setCPUPerformanceTier(tier?: CPUPerformanceTier): void {
this.#cpuPerformanceSetting.set(tier ?? 'no-override');
}
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 this.#targetManager.primaryPageTarget() !== null;
} catch {
return false;
}
}
async getHardwareConcurrency(): Promise<number> {
const target = this.#targetManager.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;
}
async updateHostDefaultCPUPerformanceTier(): Promise<void> {
if (this.#manualCPUPerformanceOverride !== undefined) {
// We do not want to update the host default tier when it is manually overridden
// via the sensors panel (in which case, `navigator.cpuPerformance` would return the override).
return;
}
const target = this.#targetManager.primaryPageTarget();
if (!target) {
return;
}
const evalResult = await target.runtimeAgent().invoke_evaluate(
{expression: 'navigator.cpuPerformance', returnByValue: true, silent: true, throwOnSideEffect: true});
if (evalResult.getError()) {
return;
}
const {result, exceptionDetails} = evalResult;
if (exceptionDetails || typeof result.value !== 'number') {
return;
}
const detectedTier = numberToTier(result.value);
if (this.#hostDefaultCPUPerformanceTier !== detectedTier) {
this.#hostDefaultCPUPerformanceTier = detectedTier;
this.#syncCPUPerformanceTier();
}
}
modelAdded(emulationModel: EmulationModel): void {
if (this.#cpuThrottlingRate !== 1) {
void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingRate);
}
if (this.#hardwareConcurrency !== undefined) {
void emulationModel.setHardwareConcurrency(this.#hardwareConcurrency);
}
if (this.#isCPUPerformanceOverrideActive()) {
void emulationModel.setCPUPerformanceOverride(this.effectiveCPUPerformanceTier());
}
// 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',
CPU_PERFORMANCE_TIER_CHANGED = 'CpuPerformanceTierChanged',
}
export interface EventTypes {
[Events.RATE_CHANGED]: number;
[Events.HARDWARE_CONCURRENCY_CHANGED]: number;
[Events.CPU_PERFORMANCE_TIER_CHANGED]: CPUPerformanceTier|undefined;
}