Compute Pressure Level 1
The Compute Pressure API provides a way for websites to react to changes
in the CPU pressure of the target device, such that websites can trade off
resources for an improved user experience.
A Note on Feature Detection
This section is non-normative.
Feature detection is an established web development best practice. Resources on the topic are plentiful on- and
offline and the purpose of this section is not to discuss it further, but rather to put it in the context of
detecting hardware-dependent features.
Consider the below feature detection examples:
Concepts
This specification defines the following concepts:
Processing Units
Computing devices consist of a multitude of different processing units such as the Central
Processing Unit (CPU), the Graphics Processing Unit (GPU) and many specialized
processing units. The latter are becoming popular such as ones designed to accelerate specific
tasks like machine learning or computer vision.
Supported sources
The specification currently defines the supported source types as
global system thermals and the central [=processing unit=], also know as the CPU.
Future levels of this specification MAY introduce additional [=source types=].
enum PressureSource { "thermals", "cpu" };
The PressureSource enum represents the [=supported source types=]:
-
{{PressureSource/"thermals"}} represents the global thermal state of the system.
-
{{PressureSource/"cpu"}} represents the average pressure of the central [=processing unit=]
across all its cores.
Sampling and Reporting Rate
The sampling rate for a [=platform collector=] is defined as a rate
at which the [=user agent=] obtains telemetry readings from the underlying platform,
and it might differ from the pressure observers' [=requested sampling rates=].
The reporting rate for a pressure observer is the rate at which it runs
the [=data delivery=] steps.
The [=sampling rate=] differs from the [=requested sampling rate=] when the
[=requested sampling rate=] exceeds upper or lower sampling rate bounds
supported or accepted by the underlying platform and [=user agent=]†.
†It is recommended that the [=user agent=] limits the [=reporting rate=]
as outlined in [[[#rate-limiting-change-notifications]]].
In case the user didn't request a [=sampling rate=], the [=sampling rate=]
is [=implementation-defined=].
Platform primitives
The [=platform collector=] refers to a platform interface, with which the [=user agent=] interacts to
obtain the telemetry readings required by this specification.
A [=platform collector=] can be defined by the underlying platform (e.g. in a native telemetry
framework) or by the [=user agent=], if it has a direct access to hardware counters.
A [=platform collector=] can support telemetry for different source types of computing
devices defined by {{PressureSource}}, or there can be multiple [=platform collectors=].
From the implementation perspective [=platform collector=] can be treated as a software proxy for the
corresponding hardware counters. It is possible to have multiple [=platform collector=] simultaneously
interacting with the same underlying hardware if the underlying platform supports it.
In simple cases, a [=platform collector=] represents individual hardware counters, but if the provided
counter readings are a product of data fusion performed in software, the [=platform collector=]
represents the results of the data fusion process. This may happen in user space or in kernel space.
As collecting telemetry data often means polling hardware counters, it is not a free operation and thus,
it should not happen if there are no one observing the data. See [[[#life-cycle]]] for more information.
A [=platform collector=] samples data at a specific rate. A [=user agent=] may modify this rate
(if possible) for privacy reasons, or ignore and fuse certain readings.
User notifications
It is RECOMMENDED that a [=user agent=] show some form of user-visible
notification that informs the user when a pressure observer is active,
as well as provides the user with the means to block the ongoing operation,
or simply dismiss the notification.
Policy control
The Compute Pressure API defines a [=policy-controlled feature=]
identified by the token "compute-pressure".
Its [=policy-controlled feature/default allowlist=] is `["self"]`.
Internal Slot Definitions
Each [=global object=] has:
-
a current pressure state (a string), initialized to the empty string.
-
a pressure observer task queued (a boolean), which is initially false.
-
a registered observer list per supported [=source type=], which is initially empty.
-
a reference to an underlying platform collector as detailed in [[[#platform-primitives]]].
A registered observer consists of an observer (a {{PressureObserver}} object).
The [=user agent=] additionally has a max queued records integer, which
is set to an [=implementation-defined=] value, greater than 0.
A constructed {{PressureObserver}} object has the following internal slots:
-
a [[\Callback]] of type {{PressureUpdateCallback}} set on creation.
-
a [[\SampleRate]] double set on creation.
-
a [[\PendingObservePromises]] [=list=] of zero or more source-promise [=tuples=], initially empty,
where source holds a {{PressureSource}} string and promise holds a {{Promise}} object.
-
a [[\QueuedRecords]] [=queue=] of zero or more {{PressureRecord}}
objects, which is initially empty.
-
a [[\LastRecordMap]] [=ordered map=], [=map/keyed=] on a {{PressureSource}},
representing the [=source type=] to which the last record belongs.
The [=ordered map=]'s [=map/value=] is a {{PressureRecord}}.
For the [=rate obfuscation=] mitigation the constructed {{PressureObserver}} object additionally
has the following internal slots:
-
an [[\ObservationWindow]] integer set as part of the [=reset observation window=] steps.
-
a [[\MaxChangesThreshold]] integer set as part of the [=reset observation window=] steps.
-
a [[\PenaltyDuration]] integer set as part of the [=reset observation window=] steps.
-
a [[\ChangesCountMap]] [=ordered map=], [=map/keyed=] on a {{PressureSource}},
representing the [=source type=] that triggered transition to the [=current pressure state=].
The [=ordered map=]'s [=map/value=] is an integer representing the number of state changes in the
current observation window timeframe.
-
a [[\AfterPenaltyRecordMap]] [=ordered map=], [=map/keyed=] on a {{PressureSource}},
representing the [=source type=] of the last {{PressureRecord}}.
The [=ordered map=]'s [=map/value=] is a {{PressureRecord}}.
Pressure States
Pressure states represents the minimal set of useful states that allows websites
to react to changes in compute and system pressure with minimal degration in quality or service,
or user experience.
enum PressureState { "nominal", "fair", "serious", "critical" };
The PressureState enum represents the [=pressure state=] with the following states:
-
{{PressureState/"nominal"}}: The conditions of the target device are at an acceptable level with no noticeable
adverse effects on the user.
-
{{PressureState/"fair"}}: Target device pressure, temperature and/or energy usage are slightly elevated, potentially
resulting in reduced battery-life, as well as fans (or systems with fans) becoming active and audible.
Apart from that the target device is running flawlessly and can take on additional work.
-
{{PressureState/"serious"}}: Target device pressure, temperature and/or energy usage is consistently highly elevated.
The system may be throttling as a countermeasure to reduce thermals.
-
{{PressureState/"critical"}}: The temperature of the target device or system is significantly elevated and it requires
cooling down to avoid any potential issues.
Contributing Factors
Contributing factors represent the underlying hardware metrics contributing to the [=current pressure state=]
and can be [=implementation-defined=].
The adjusted pressure state is a [=pressure state=] determined by an [=implementation-defined=]
algorithm that takes as input [=source type=] and any other [=implementation-defined=] data from
[=contributing factors=]. This algorithm MUST not be deterministic to ensure [=break calibration=] mitigation effectiveness.
The change in contributing factors is substantial steps are as follows:
-
If [=implementation-defined=] low-level hardware metrics that contribute to the
[=current pressure state=] drop below or exceed an, per metric,
[=implementation-defined=] threshold
for the [=current pressure state=], return true.
-
Return false.
Pressure Observer
The Compute Pressure API enables developers to understand the pressure
of system resources such as the CPU.
callback PressureUpdateCallback = undefined (
sequence<PressureRecord> changes,
PressureObserver observer
);
This callback will be invoked when the [=pressure state=] changes.
The {{PressureObserver}} can be used to observe changes in the [=pressure states=].
[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
interface PressureObserver {
constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {});
Promise<undefined> observe(PressureSource source);
undefined unobserve(PressureSource source);
undefined disconnect();
sequence<PressureRecord> takeRecords();
[SameObject] static readonly attribute FrozenArray<PressureSource> supportedSources;
};
The PressureObserver interface represents a {{PressureObserver}}.
The constructor() method
The `new` {{PressureObserver(callback, options)}} constructor steps are:
-
Set |this|.{{PressureObserver/[[Callback]]}} to |callback:PressureUpdateCallback|.
-
If |options|["sampleRate"] is less than or equal to 0, throw a {{RangeError}}.
-
Set |this:PressureObserver|.{{PressureObserver/[[SampleRate]]}} to |options|["sampleRate"].
The observe() method
The {{PressureObserver/observe(source)}} method steps are:
-
Let |document:Document| be [=this=]'s [=relevant settings
object=]'s [=associated Document=].
-
If |document| is not null and is not [=allowed to use=] the [=policy-controlled
feature=] token "compute-pressure", return [=a promise rejected with=] {{NotAllowedError}}.
-
Let |promise:Promise| be [=a new promise=].
-
Let |pendingPromiseTuple| be (|source|, |promise|).
-
[=list/Append=] |pendingPromiseTuple| to [=this=].{{PressureObserver/[[PendingObservePromises]]}}.
-
[=promise/React=] to |promise|:
-
If |promise| was [=resolved|fulfilled=] or [=rejected=], then:
-
[=list/Remove=] |tuple| from [=this=].{{PressureObserver/[[PendingObservePromises]]}}.
-
Run the following steps [=in parallel=]:
-
If |source:PressureSource| is not a [=supported source type=],
[=queue a global task=] on the [=PressureObserver task source=]
given |document|'s [=relevant global object=] |relevantGlobal|
to reject |promise| {{NotSupportedError}} and abort these steps.
-
Activate [=data delivery=] of |source| data to |relevantGlobal|.
-
[=Queue a global task=] on the [=PressureObserver task source=] given |document|'s
[=relevant global object=] |relevantGlobal| to run these steps:
-
If |promise| was rejected, run the following substeps:
-
If |relevantGlobal|'s [=registered observer list=] for |source| is [=list/empty=],
deactivate [=data delivery=] of |source| data to |relevantGlobal|.
-
Return.
-
[=list/Append=] a new [=registered observer=] whose [=observer=] is [=this=]
to |relevantGlobal|'s [=registered observer list=] for |source|.
-
Resolve |promise|.
-
Return |promise|.
The unobserve() method
The {{PressureObserver/unobserve(source)}} method steps are:
-
If |source:PressureSource| is not a [=supported source type=], throw {{"NotSupportedError"}}.
-
[=list/Remove=] from |this|.{{PressureObserver/[[QueuedRecords]]}} all
|records| associated with |source|.
-
[=map/Remove=] |this|.{{PressureObserver/[[LastRecordMap]]}}[|source|].
-
[=map/Remove=] |this|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[|source|].
-
[=list/For each=] (|promiseSource|, |pendingPromise|) of [=this=].{{PressureObserver/[[PendingObservePromises]]}},
if |source| is equal to |promiseSource|, [=reject=] |pendingPromise| with an {{AbortError}}.
-
Let |relevantGlobal| be [=this=]'s [=relevant global object=].
-
Remove any [=registered observer=] from |relevantGlobal|'s [=registered observer list=] for
|source| for which [=this=] is the [=registered observer=].
-
If the above [=registered observer list=] is [=list/empty=],
deactivate [=data delivery=] of |source| data to |relevantGlobal|.
The disconnect() method
The {{PressureObserver/disconnect()}} method steps are:
-
[=list/Empty=] |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
[=map/Clear=] |this|.{{PressureObserver/[[LastRecordMap]]}}.
-
[=map/Clear=] |this|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}.
-
[=list/For each=] (|promiseSource|, |pendingPromise|) of [=this=].{{PressureObserver/[[PendingObservePromises]]}},
[=reject=] |pendingPromise| with an {{AbortError}}.
-
Let |relevantGlobal| be [=this=]'s [=relevant global object=].
-
Remove any [=registered observer=] from |relevantGlobal|'s' [=registered observer list=] for
all supported [=source types=] for which [=this=] is the [=observer=].
-
If the above [=registered observer list=] is [=list/empty=],
deactivate [=data delivery=] of |source| data to |relevantGlobal|.
The takeRecords() method
The {{PressureObserver/takeRecords()}} method steps are:
-
Let |records| be a [=list/clone=] of |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
[=list/Empty=] |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
Return |records|.
The supportedSources attribute
The {{PressureObserver/supportedSources}} attribute is informing on the [=supported source type=] by the [=platform collector=].
The {{PressureObserver/supportedSources}} getter steps are:
-
Let |sources| be a [=list=] of |source:PressureSource|.
-
Return |observer|'s frozen array of supported [=source types=].
The PressureRecord interface
[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
interface PressureRecord {
readonly attribute PressureSource source;
readonly attribute PressureState state;
readonly attribute DOMHighResTimeStamp time;
[Default] object toJSON();
};
A constructed {{PressureRecord}} object has the following internal slots:
-
a [[\Source]] value of type {{PressureSource}}, which represents the current [=source type=].
-
a [[\State]] value of type {{PressureState}}, which represents the [=current pressure state=].
-
a [[\Time]] value of type {{DOMHighResTimeStamp}},
which corresponds to the
time the data was obtained from the system, relative to the [=environment settings object/time origin=] of the global object associated with
the {{PressureObserver}} instance that generated the notification.
The source attribute
The {{PressureRecord/source}} [=getter steps=] are to return its {{PressureRecord/[[Source]]}} internal slot.
The state attribute
The {{PressureRecord/state}} [=getter steps=] are to return its {{PressureRecord/[[State]]}} internal slot.
The time attribute
The {{PressureRecord/time}} [=getter steps=] are to return its {{PressureRecord/[[Time]]}} internal slot.
The toJSON member
When {{PressureRecord.toJSON}} is called, run [[[WebIDL]]]'s [=default toJSON steps=].
The PressureObserverOptions dictionary
dictionary PressureObserverOptions {
double sampleRate = 1.0;
};
The sampleRate member
The {{PressureObserverOptions/sampleRate}} member represents the requested sampling
rate expressed in Hz, ie. it represents the number of samples requested to be obtained
from the hardware per second. The [=reporting rate=] will never exceed the [=requested sampling rate=].
Life-cycle and garbage collection
Each [=global object=] has a
strong reference to [=registered observers=] in their [=registered observer list=]
(one per source).
Processing Model
This section outlines the steps the user agent must take when implementing the specification.
Supporting algorithms
The reset observation window steps given the argument |observer:PressureObserver|, are as follows:
-
set |observer|.{{PressureObserver/[[ObservationWindow]]}} to an [=implementation-defined=] randomized integer value in
milliseconds within an [=implementation-defined=] range, e.g., random between 300000 and 600000 (5 and 10 minutes).
-
set |observer|.{{PressureObserver/[[MaxChangesThreshold]]}} to an [=implementation-defined=] randomized integer
value of maximum allowed changes within the |observationWindow| within an [=implementation-defined=] range.
-
set |observer|.{{PressureObserver/[[PenaltyDuration]]}} to an [=implementation-defined=] randomized integer value
in milliseconds, within an [=implementation-defined=] range.
-
[=list/Empty=] the observer.{{PressureObserver/[[ChangesCountMap]]}} map.
Run the [=reset observation window=] steps and start a timer to re-run the steps when the observer.{{PressureObserver/[[ObservationWindow]]}}
time has passed, using different randomized values.
The passes privacy test steps given the argument |observer:PressureObserver| and
its [=relevant global object=] |relevantGlobal|, are as follows:
-
If |relevantGlobal| is a {{WorkerGlobalScope}} object:
-
If |relevantGlobal|'s relevant worker is not a
active needed worker, return false.
-
Otherwise, return true.
-
If |relevantGlobal| is a {{Window}} object:
-
If |relevantGlobal|'s [=associated document=] is not [=Document/fully active=], return false.
-
[=list/For each=] |origin| in
initiators of active Picture-in-Picture sessions:
-
If |relevantGlobal|'s [=relevant settings object=]'s [=origin=] is [=same origin-domain=] with |origin|, return true.
-
If |relevantGlobal|'s [=browsing context=] is [=context is capturing|capturing=], return true.
-
Let |topLevelBC| be |relevantGlobal|'s [=browsing context=]'s [=top-level browsing context=].
-
If |topLevelBC| does not have [=top-level traversable/system focus=], return false.
-
Let |focusedDocument| be the |topLevelBC|'s
currently focused area's [=Node/node document=].
-
If |relevantGlobal|'s [=relevant settings object=]'s [=origin=] is [=same origin-domain=] with
|focusedDocument|'s [=origin=], return true.
-
Otherwise, return false.
The passes rate test steps given the argument |observer:PressureObserver|,
|source:PressureSource| and |timestamp:DOMHighResTimeStamp|, are as follows:
-
If |observer|.{{PressureObserver/[[LastRecordMap]]}}[|source|] does not [=map/exist=], return true.
-
Let |record:PressureRecord| be |observer|.{{PressureObserver/[[LastRecordMap]]}}[|source|].
-
Let |sampleRate| be |observer|.{{PressureObserver/[[SampleRate]]}}.
-
Let |timeDeltaMilliseconds:DOMHighResTimeStamp| = |timestamp| - |record|.{{PressureRecord/[[Time]]}}.
-
Let |intervalSeconds| = 1 / |sampleRate|.
-
If (|timeDeltaMilliseconds| / 1000) ≥ |intervalSeconds|, return true, otherwise return false.
The has change in data steps given the argument |observer:PressureObserver|, |source:PressureSource|,
|state:PressureState|, are as follows:
-
If |observer|.{{PressureObserver/[[LastRecordMap]]}}[|source|] does not [=map/exist=], return true.
-
Let |record:PressureRecord| be |observer|.{{PressureObserver/[[LastRecordMap]]}}[|source|].
-
If |record|.{{PressureRecord/[[State]]}} is not equal to |state| and [=change in contributing factors is substantial=]
returns true, return true.
-
Return false.
The passes rate obfuscation test steps given the argument |observer:PressureObserver|,
|source:PressureSource|, are as follows:
-
Increment observer.{{PressureObserver/[[ChangesCountMap]]}}[|source|].
-
Return observer.{{PressureObserver/[[ChangesCountMap]]}}[|source|]
≤ observer.{{PressureObserver/[[MaxChangesThreshold]]}}.
Data delivery
[=Data delivery=] from a [=platform collector=] can be activate and deactivated in an
[=implementation-defined=] manner per [=source type=] and [=global object=].
The data delivery steps that are run when
an [=implementation-defined=] |data| sample of [=source type=] |source:PressureSource| is
obtained from [=global object=] |relevantGlobal|'s [=platform collector=],
are as follows:
-
Let |source:PressureSource| be the [=source type=] of the |data| sample.
-
Let |state:PressureState| be an [=adjusted pressure state=] given |data| and |source|.
-
Let |timestamp:DOMHighResTimeStamp| be a timestamp representing the time the |data| was
obtained from the |relevantGlobal|'s [=platform collector=].
-
[=list/For each=] |observer:PressureObserver| in |relevantGlobal|'s
[=registered observer list=] for |source|:
-
If running [=passes privacy test=] with |observer|
returns false, [=iteration/continue=].
-
If running [=passes rate test=] with |observer|, |source| and |timestamp|
returns false, [=iteration/continue=].
-
If running [=has change in data=] with |observer|, |source| and |state|
returns false, [=iteration/continue=].
-
Let |record:PressureRecord| be a new {{PressureRecord}} object with its
{{PressureRecord/[[Source]]}} set to |source|,
{{PressureRecord/[[State]]}} set to |state|
and {{PressureRecord/[[Time]]}} set to |timestamp|.
-
If |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[source] [=map/exists=]:
-
Set |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[|source|] to |record|.
-
[=iteration/Continue=].
-
If running [=passes rate obfuscation test=] with |observer| and |source| returns false:
-
Set |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[|source|] to |record|.
-
Set |observer|.{{PressureObserver/[[ChangesCountMap]]}}[|source|] to 0.
-
Create timer of |observer|.{{PressureObserver/[[PenaltyDuration]]}} duration with the following callback:
-
If |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[source] [=map/exists=]:
-
Let |record| be |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[|source|].
-
[=map/Remove=] |observer|.{{PressureObserver/[[AfterPenaltyRecordMap]]}}[|source|].
-
Run [=queue a record=] with |observer|, |source|, |record|.
-
[=iteration/Continue=].
-
Run [=queue a record=] with |observer|, |source|, |record|.
Queue a PressureRecord
To queue a record given the arguments |observer:PressureObserver|,
|source:PressureSource|, |record:PressureRecord|,
run these steps:
-
If [=list/size=] of |observer|.{{PressureObserver/[[QueuedRecords]]}} is greater than
[=max queued records=], then [=list/remove=] the first [=list/item=].
-
[=list/Append=] |record| to |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
Set |observer|.{{PressureObserver/[[LastRecordMap]]}}[|source|] to |record|.
-
[=Queue a pressure observer task=] with |observer|'s [=relevant global object=].
Queue a Pressure Observer Task
The PressureObserver task source is a [=task source=] used for scheduling tasks to [[[#notify-observers]]].
To queue a pressure observer task given |relevantGlobal| as input, run these steps:
-
If the |relevantGlobal|'s [=pressure observer task queued=] is true, then return.
-
Set the |relevantGlobal|'s [=pressure observer task queued=] to true.
-
[=Queue a global task=] on [=PressureObserver task source=] with |relevantGlobal| to [=notify pressure observers=].
Notify Pressure Observers
To notify pressure observers given |relevantGlobal| as input, run these steps:
-
Set |relevantGlobal|'s [=pressure observer task queued=] to false.
-
Let |notifySet| be a new [=set=] of all [=observers=] in
|relevantGlobal|’s [=registered observer lists=].
-
[=list/For each=] |observer:PressureObserver| of |notifySet|:
-
Let |records| be a [=list/clone=] of |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
[=list/Empty=] |observer|.{{PressureObserver/[[QueuedRecords]]}}.
-
If |records| is not [=list/empty=], then invoke |observer|.{{PressureObserver/[[Callback]]}}
with |records| and |observer|. If this throws an exception, catch it, and [=report the exception=].
Handling change of fully active
When a {{Document}} |document| is no longer [=Document/fully active=],
deactivate [=data delivery=] of data of all [=supported source types=] to |document|'s [=relevant global object=].
When a worker with associated {{WorkerGlobalScope}} |relevantGlobal| is no longer
an
active needed workers,
deactivate [=data delivery=] of data of all [=supported source types=] to |relevantGlobal|.
When a {{Document}} |document| becomes [=Document/fully active=],
for each non-[=list/empty=] [=registered observer list=] associated the [=source type=] |source|,
activate [=data delivery=] of |source| data to |document|'s [=relevant global object=].
When a worker with associated {{WorkerGlobalScope}} |relevantGlobal| becomes
an
active needed workers,
for each non-[=list/empty=] [=registered observer list=] associated the [=source type=] |source|,
activate [=data delivery=] of |source| data to |document|'s [=relevant global object=].
Handle unloading document and closing of workers
When a worker with associated {{WorkerGlobalScope}} |relevantGlobal|,
once |relevantGlobal|'s [=WorkerGlobalScope/closing=] flag is set to true,
deactivate [=data delivery=] for all [=supported source types=] to |relevantGlobal|.
As one of the [=unloading document cleanup steps=] given {{Document}} |document|,
deactivate [=data delivery=] for all [=supported source types=] to |document|'s [=relevant global object=].
Security and privacy considerations
Types of privacy and security threats
The Working Group will list any known attack vectors, both theoretical and real-world,
in this section.
Timing attacks
It may be possible to identify users across non-[=same origin=] sites if unique
or very precise values can be accessed at the same time by sites not sharing
origin.
This attack is mitigated by [[[#data-minimization]]], [[[#rate-limiting-change-notifications]]],
and [[[#same-origin-restriction]]].
Cross-site covert channel
In computer security a covert channel creates a capability to transfer information between processes
that are not supposed to be allowed to communicate. In modern multi-process web engines in the generic
case each window or tab resides in its own process (documents that have the [=same origin=] or sites that
have the [=same site=] typically share the same process). Using this API it may be possible to create a
cross-site covert channel C where a site A on one tab first broadcasts to the channel C after having
manipulated the state of the CPU. Next a site B (that is not same site with site A) on another tab reads
the broadcasted data from the channel C by using this API to learn when the state of the CPU has changed.
This process is repeated as long as the scripts run on both the sites A and B.
This attack is mitigated by [[[#rate-limiting-change-notifications]]], [[[#rate-obfuscation]]] and
[[[#break-calibration]]]. Implementers are advised to consider all these mitigations for long-running scripts.
The longer the scripts run the more information can be transmitted using the proposed cross-site covert channel.
For example, if a user is on a video conferencing site and another long-running site that allows for more
information to be transferred compared to a regular browsing scenario. On the other hand, a workload such as
a video conferencing session will typically exert sustained pressure on the CPU that makes it harder to
manipulate the pressure state in a predictive manner.
Mitigation strategies
This section gives a high-level view into mitigation strategies applicable to this specification.
The normative definitions of these mitigations are integrated into the respective algorithms of this specification.
Implementers are advised to consider the
TAG guidance
on private browsing modes when implementing the mitigations defined in this specification.
Data minimization
This specification adheres to the generic
data minimization principles
to limit exposure of data related to low-level details of the underlying platform to the minimum required to
address its high-value use cases. This includes consideration for limiting exposure of
identifying information about devices.
The specific application of data minimization principles in the context of this specification
are discussed in [[[#rate-limiting-change-notifications]]] and [[[#same-origin-restriction]]].
Rate-limiting change notifications
By rate-limiting the delivery of the pressure state information we remove the
attacker's ability to observe the precise time when a value transitions between two states.
More precisely, once the pressure observer is activated, it will be
called once with initial values, and then is called when the values change.
The subsequent calls will be rate-limited. When the callback is
called, the most recent 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.
Rate limiting can be implemented in the user agent, but it might also be
possible to simply change the polling/sampling rate of the underlying hardware
counters, if not accessed via a higher level framework.
Rate obfuscation
The specification requires implementing the rate obfuscation mitigation
to keep track of the number of pressure changes over an [=implementation-defined=]
sliding observation window and
set a flag if an [=implementation-defined=] threshold for the number of pressure
changes is exceeded. Similarly, it is also recommended for the implementation to
observe any abnormal activity such as a high number of pressure state changes
spanning across multiple states, and set this flag similarly.
If this flag is set, the implementation is recommended to give the pressure observer
a penalty during which it will not be able to inform scripts of changes in its
pressure state as it normally would. The duration of this penalty is
[=implementation-defined=] and it is recommended to be randomized.
When [=notify pressure observers=] resumes operation after the penalty, it only
reports the latest pressure state and disregards any interim state information
received from the platform collector during this penalty.
Break calibration
In a calibration process an attacker tries to manipulate the CPU so that this
API would report a transition into a certain pressure state with the highest
probability in response to the pressure exerted by the fabricated workload.
This break calibration mitigation solution can slow down or prevent this calibration process
from succeeding by slightly changing at runtime the [=implementation-defined=]
low-level hardware metrics that contribute to these pressure state transitions.
Even if the initial calibration would succeed, its results will be invalidated
at runtime when this mitigation is running continuously. Any attempts to recalibrate
will similarly be mitigated against.
Modern browsers throttle background tabs using [=implementation-defined=]
heuristics in order to reduce resource usage. For example, after a period of
no user interaction a background tab can be throttled that will influence
the global pressure state of the system. This built-in feature of modern
browsers further improves the effectiveness of the break calibration
mitigation.
Same-origin restriction
By default data delivery is restricted to documents served from the same-origin as an
initiator of an active picture-in-picture-session,
documents [=context is capturing|capturing=]
or the document with [=top-level traversable/system focus=], if any.
The documents qualifying for data delivery, under the above rules, can delegate it to documents in [=child navigables=].
The feature can be extended to third-party contexts such as iframes only by a
declared policy.
Examples
const samples = [];
function pressureChange(records, observer) {
for (const record of records) {
samples.push(record.state);
// We only want 20 samples.
if (samples.length == 20) {
observer.disconnect();
return;
}
}
}
const observer = new PressureObserver(pressureChange);
observer.observe("cpu");
In the following example we want to lower the number of concurrent video streams when the
pressure becomes critical. For the sake of simplicity we only consider this one state.
As lowering the amount of streams might not result in exiting the critical state,
or at least not immediately, we use a strategy where we lower one stream at the time
every 30 seconds while still in the critical state.
We accomplish this by making sure the callback is called at least once every 30 seconds,
or when the state actually changes. When the state changes we reset the interval timer.
let timerId = -1;
function pressureChange(records) {
// Clear timer every time we are called, either by an actual state change,
// or when called by setTimeout (see below).
if (timerId > 0) {
clearTimeout(timerId);
}
// When entering critical state, we want to recheck every 30sec if we are
// still in critical state and if so, further reduce our concurrent streams.
// For this reason we create a timer for 30 seconds that will call us back
// with the last result in there were no change.
const lastRecordArray = [records.at(records.length - 1)];
timerId = setTimeout(pressureChange.bind(this, lastRecordArray), 30_000);
for (const record of records) {
if (record.state == "critical") {
let streamsCount = getStreamsCount();
setStreamsCount(streamsCount--);
}
}
}
const observer = new PressureObserver(pressureChange);
observer.observe("cpu");
In the following example, we want to demonstrate the usage of {{PressureObserver/takeRecords()}},
by retrieving the remaining |records| accumulated since the the callback was last
invoked.
It is recommended to do so before {{PressureObserver/disconnect()}},
otherwise {{PressureObserver/disconnect()}} will clear them and they will be lost forever.
For example, we might want to measure the pressure during a benchmarking workload, and thus
want pressure telemetry for the exact duration of the workload. This means disconnecting all
observers immediately when the task is completed, and manually requesting any pending pressure
telemetry up to this point that might not have been delivered yet as part of the event loop cycle.
function logWorkloadStatistics(records) {
// do something with records.
}
const observer = new PressureObserver(logWorkloadStatistics);
observer.observe("cpu");
// Read pending state change records, otherwise they will be cleared
// when we disconnect.
const records = observer.takeRecords();
logWorkloadStatistics(records);
observer.disconnect();
In the following example, we show how to tell the observer to stop watching a specific
|source:PressureSource| by invoking {{PressureObserver/unobserve()}}
with |source|.
const observer = new PressureObserver(records => { /* do something with records. */ });
observer.observe("cpu");
observer.observe("gpu");
// Callback now gets called whenever the pressure state changes for 'cpu' or 'gpu'.
observer.unobserve("gpu");
// Callback now only gets called whenever the pressure state changes for 'cpu'.
In the following example, we show how to tell the observer to stop watching for any
state changes by calling {{PressureObserver/disconnect()}}. Calling
{{PressureObserver/disconnect()}} will stop observing all sources observed
by previous {{PressureObserver/observe()}} calls.
Additionally it will clear all pending records collected since the last callback was invoked.
const observer = new PressureObserver(records => { // do something with records. });
observer.observe("cpu");
observer.observe("gpu");
// some time later...
observer.disconnect();
// records will be an empty array, because of the previous disconnect().
const records = observer.takeRecords();