The Compute Pressure API provides a way for websites to react to changes in the CPU consumption of the target device, such that websites can trade off CPU resources for an improved user experience.

Introduction

Modern applications often need to balance the trade offs and advantages of fully utilizing the system CPU resources, in order to provide a modern and improved user experience.

As an example, many applications can render video effects with varying degrees of sophistication. These applications aim to provide the best user experience, while avoiding driving the user's device in a high CPU utilization regime.

High CPU utilization is undesirable because it strongly degrades the user experience. Many smartphones, tablets and laptops can become uncomfortably hot to the touch. The fans in laptops and desktops can become so loud that they disrupt conversations or the users’ ability to focus. In many cases, a device under high CPU utilization appears to be unresponsive, as the operating system may fail to schedule the threads advancing the task that the user is waiting for. See also Compute Pressure: Use Cases.

Concepts

This specification defines the following concepts:

CPU utilization

The CPU utilization of the user's device is the average of the utilization of all the device's CPU cores.

A CPU core's utilization is the fraction of time that the core has been executing code belonging to a thread, as opposed to being in an idle state.

A CPU utilization close to 1.0 is very likely to lead to a bad user experience. The device is likely at its thermal limits, and noise from CPU cooling fans is very noticeable. Applications can help avoid bad user experiences by reducing their compute demands when the CPU utilization is high.

CPU clock speed

Modern CPU cores support a set of clock speeds. The device's firmware or operating system can set the core clock speed, in order to trade off the available CPU computational resources with power consumption.

From a user experience standpoint, the following are the most interesting clock speed levels:

When a device's CPU utilization gets high, the device increases clock speeds across its CPU cores, in an attempt to meet the CPU compute demand. As the speeds exceed the base clock speed, the elevated power consumption increases the CPU's temperature. At some point, the device enters a thermal throttling regime, where the CPU clock speed is reduced, in order to bring the temperature down.

By the time thermal throttling kicks in, the user is having a bad experience. Applications can help avoid thermal throttling by reducing their demands for CPU compute right as the CPU clock speeds approaches / exceeds the base speed.

Internal slots

A construction {{ComputePressureObserver}} object, has the following internal slots.

Internal slot Initial value Description
[[\Callback]] {{undefined}} An optional function of type {{ComputePressureUpdateCallback}}.
[[\CPUUtilizationThresholds]] An empty [=list=]. A [=list=] of {{double}}s between 0 and 1, representing the user requested CPU utilization thresholds.
[[\CPUSpeedThresholds]] An empty [=list=]. A [=list=] of {{double}}s between 0 and 1, representing the user requested CPU clock speed thresholds.

Compute Pressure Observer

The Compute Pressure Observer API enables developers to understand the utilization characteristics of a CPU.

The ComputePressureUpdateCallback callback

    callback ComputePressureUpdateCallback = undefined (
      ComputePressureObserverUpdate update,
      ComputePressureObserver observer
    );
  
This callback will be invoked when CPU clock speed and/or CPU utilization crosses the user set thresholds.

The ComputePressureObserver object

The {{ComputePressureObserver}} can be used to observe changes in the CPU clock speed and CPU utilization, following user set thresholds.

Providing a list of thresholds effectively separates the resource usage into buckets. For example, the thresholds list `[0.5, 0.75, 0.9]` defines a 4-bucket scheme, where the buckets cover the ranges `[0-0.5[`, `[0.5-0.75[`, `[0.75-0.9[`, and `[0.9-1.0]`.

    [Exposed=Window]
    interface ComputePressureObserver {
      constructor(
        ComputePressureUpdateCallback callback,
        optional ComputePressureObserverOptions options = {}
      );
      undefined observe();
      undefined unobserve();
    };
  

The ComputePressureObserver interface represents a {{ComputePressureObserver}}.

The constructor() method

The {{ComputePressureObserver/constructor()}} method, when invoked, MUST run the following step, given the arguments |callback:ComputePressureUpdateCallback| and |options:ComputePressureObserverOptions|:

  1. Let |this:ComputePressureObserver| be a new the {{ComputePressureObserver}} object.
  2. Set |this|.{{[[Callback]]}} to |callback|.
  3. If |options|.|cpuUtilizationThresholds:list| exists, set |this|.{{[[CPUUtilizationThresholds]]}} be a [=list=] equal to that.
  4. If any value in |this|.{{[[CPUUtilizationThresholds]]}} is less than 0.0 or greater than 1.0, throw a {{RangeError}} exception.
  5. Sort |this|.{{[[CPUUtilizationThresholds]]}} in ascending order.
  6. If |options|.|cpuSpeedThresholds:list| exists, set |this|.{{[[CPUSpeedThresholds]]}} be a [=list=] equal to that.
  7. If any value in |this|.{{[[CPUSpeedThresholds]]}} is less than 0.0 or greater than 1.0, throw a {{RangeError}} exception.
  8. Sort |this|.{{[[CPUSpeedThresholds]]}} in ascending order.
  9. Return |this|.

The observe() method

The {{ComputePressureObserver/observe()}} method, when invoked, MUST run the following step:

The unobserve() method

The {{ComputePressureObserver/unobserve()}} method, when invoked, MUST run the following step:

The ComputePressureObserverUpdate dictionary

    dictionary ComputePressureObserverUpdate {
      double cpuSpeed;
      double cpuUtilization;
      ComputePressureObserverOptions options;
    };
  

The cpuSpeed attribute

The {{ComputePressureObserverUpdate/cpuSpeed}} attribute represents the current CPU clock speed as a {{double}} value between `0` and `1`.

The cpuUtilization attribute

The {{ComputePressureObserverUpdate/cpuUtilization}} attribute represents the current CPU utilization as a {{double}} value between `0` and `1`.

The options attribute

The {{ComputePressureObserverUpdate/options}} attribute represents the {{ComputePressureObserverOptions}} dictionary that {{ComputePressureObserver}} was constructed with.

The ComputePressureObserverOptions dictionary

    dictionary ComputePressureObserverOptions {
      sequence<double> cpuUtilizationThresholds = [];
      sequence<double> cpuSpeedThresholds = [];
    };
  

The cpuUtilizationThresholds member

The {{ComputePressureObserverOptions/cpuUtilizationThresholds}} member represents the current CPU utilization thresholds, as a sequence of {{double}}s values between `0` and `1`.

The cpuSpeedThresholds member

The {{ComputePressureObserverOptions/cpuSpeedThresholds}} member represents the current CPU clock speed thresholds, as a sequence of {{double}}s values between `0` and `1`.

Security and privacy considerations

Minimizing information exposure

Exposing hardware related events related to CPU utilization increases the risk of harming the user's privacy.

To minimize this risk, only the absolute minimal amount of information needed to to support the use-cases is exposed.

The subsections below describe the processing model. At a high level, the information exposed is reduced by the following steps:

  1. Normalization - Per-core information reported by the operating system is normalized to a number between 0.0 and 1.0. This removes variability across CPU models and operating systems.
  2. Aggregation - Normalized per-core information is aggregated into one overall number.
  3. Quantization (a.k.a. bucketing) - Each application (origin) must declare a small number of value ranges (buckets) where it wants to behave differently. The application doesn't receive the exact aggregation results, and instead only gets to learn the range (bucket) that each aggregated number falls within to.
  4. Rate-limiting - The user agent notifies the application of changes in the information it can learn (buckets that each aggregated number). Change notifications are rate-limited.

Normalizing CPU utilization

The user agent will normalize CPU core utilization information reported by the operating system to a number between 0.0 and 1.0.

0.0 maps to 0% utilization, meaning the CPU core was always idle during the observed time window. 1.0 maps to 100% utilization, meaning the CPU core was never idle during the observed time window.

Aggregating CPU utilization

CPU utilization is averaged over all enabled CPU cores.

Under normal circumstances, all of a system's cores are enabled. However, mitigating some recent micro-architectural attacks on some devices may require completely disabling some CPU cores. For example, some Intel systems require disabling hyperthreading.

We recommend that user agents aggregate CPU utilization over a time window of 1 second. Smaller windows increase the risk of facilitating a side-channel attack. Larger windows reduce the application's ability to make timely decisions that avoid bad user experiences.

Normalizing CPU clock speed

This API normalizes each CPU core's clock speed to a number between `0.0` and `1.0`. The proposal intends to enable the decisions we set out to support, without exposing the clock speeds.

We recommend the following principles for normalizing a CPU core's clock speed.

  • The minimum clock speed is always reported as `0.0`.
  • The base clock speed is always reported as `0.5`.
  • The maximum clock speed is always reported as `1.0`.
  • Speeds outside these values are clamped (to `0.0` or `1.0`).
  • Speeds between these values are linearly interpolated.

Aggregating CPU clock speed

TODO: Aggregating is an average of the current speed across all cores. No aggregation over a time window. Proposal for aggregating clock speeds across systems with heterogeneous CPU cores

Quantizing values (a.k.a. Bucketing)

Quantizing the aggregated CPU utilization and clock speed reduces the amount of information exposed by the API.

Having applications designate the quantization ranges (buckets) reduces the quantization resolution that user agents must support in order to enable the decisions used in a multitude of applications.

Applications communicate their desired quantization scheme by passing in a list of thresholds. For example, the thresholds list `[0.5, 0.75, 0.9]` defines a 4-bucket scheme, where the buckets cover the value ranges `0`-`0.5`, `0.5`-`0.75`, `0.75`-`0.9`, and `0.9`-`1.0`. We propose representing a bucket using the middle value in its range.

Suppose an application used the threshold list above, and the user agent measured a CPU utilization of `0.87`. This would fall under the `0.75`-`0.9` bucket, and would be reported as `0.825` (the average of `0.75` and `0.9`).

We recommend that user agents allow at most 5 buckets (4 thresholds) for CPU utilization, and 2 buckets (1 threshold) for CPU speed.

Rate-limiting change notifications

We propose exposing the quantized CPU utilization and clock speed via rate-limited change notifications. This aims to remove the ability to observe the precise time when a value transitions between two buckets.

More precisely, once the compute pressure observer is installed, it will be called once with initial quantized values, and then be called when the quantized values change. The subsequent calls will be rate-limited. When the callback is called, the most recent quantized value is reported.

The specification will recommend a rate limit of at most one call per second for the active window, and one call per 10 seconds for all other windows. We will also recommend that the call timings are jittered across origins.

These measures benefit the user's privacy, by reducing the risk of identifying a device across multiple origins. The rate-limiting also benefits the user's security, by making it difficult to use this API for timing attacks. Last, rate-limiting change callbacks places an upper bound on the performance overhead of this API.

Third-party contexts

This API will only be available in frames served from the same origin as the top-level frame. This requirement is necessary for preserving the privacy benefits of the API's quantizing scheme.

The same-origin requirement above implies that the API is only available in first-party contexts.

Examples

    const observer = new ComputePressureObserver(
      computePressureCallback,
      {
        cpuUtilizationThresholds: [0.75, 0.9, 0.5],
        cpuSpeedThresholds: [0.5],
      }
    );
  
    observer.observe();
  
    function computePressureCallback(update) {
      // The CPU base clock speed is represented as 0.5.
      if (update.cpuSpeed >= 0.5) {
        // Dramatically cut down compute requirements to avoid overheating.
        limitVideoStreams(2);
        return;
      }
    
      if (update.cpuUtilization >= 0.9) {
        limitVideoStreams(2);
      } else if (update.cpuUtilization >= 0.75) {
        limitVideoStreams(4);
      } else if (update.cpuUtilization >= 0.5) {
        limitVideoStreams(8);
      } else {
        // The system is in great shape. Show all meeting participants.
        showAllVideoStreams();
      }
    }
  

This is required for specifications that contain normative material.

Acknowledgments

Many thanks for valuable feedback and advice from Chen Xing, Evan Shrubsole, Jesse Barnes, Kamila Hasanbega, Jan Gora, Joshua Bell, Matt Menke, Nicolás Peña Moreno, Opal Voravootivat, Paul Jensen, Peter Djeu, Reilly Grant, Ulan Degenbaev, Victor Miura, and Zhenyao Mo