Copyright © 2025 the Contributors to the JS Self-Profiling API Specification, published by the Web Platform Incubator Community Group under the W3C Community Contributor License Agreement (CLA). A human-readable summary is available.
This specification describes an API that allows web applications to control a sampling profiler for measuring client JavaScript execution times.
This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.
GitHub Issues are preferred for discussion of this specification.
This section is non-normative.
Complex web applications currently have limited visibility into where JS execution time is spent on clients. Without the ability to efficiently collect stack samples, applications are forced to instrument their code with profiling hooks that are imprecise and can significantly slow down execution. By providing an API to manipulate a sampling profiler, applications can gather rich execution data for aggregation and analysis with minimal overhead.
This section is non-normative.
The following example demonstrates how a user may profile an expensive operation, gathering JS execution samples every 10ms. The trace can be sent to a server for analysis to debug outliers and JS execution characteristics in aggregate.
const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });
const start = performance.now();
for (let i = 0; i < 1000000; i++) {
doWork();
}
const duration = performance.now() - start;
const trace = await profiler.stop();
const traceJson = JSON.stringify({
duration,
trace,
});
sendTrace(traceJson);
Another common real-world scenario is profiling JS across a pageload. This example profiles the onload event, sending performance timing data along with the trace.
const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });
window.addEventListener('load', async () => {
const trace = await profiler.stop();
const traceJson = JSON.stringify({
timing: performance.timing,
trace,
});
sendTrace(traceJson);
});
// Rest of the page's JS initialization logic
As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.
The key words MAY, MUST, MUST NOT, NOT REQUIRED, RECOMMENDED, SHOULD, and SHOULD NOT in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
A sample is a descriptor of the instantaneous state of execution at a given point in time. Each sample is associated with a stack.
A stack is a list of frames that MUST be ordered sequentially from outermost to innermost frame.
A frame is an element in the context of a stack containing information about the current execution state.
A profiling session is an abstract producer of samples. Each session has:
{started, paused, stopped}
.The UA is NOT REQUIRED to take samples at this rate. However, it is RECOMMENDED that sampling is prioritized to take samples at this rate to produce higher quality traces.
ProfilerTrace
storing captured samples.Multiple profiling sessions on the same page SHOULD be supported.
In the started
state, the UA SHOULD make a best-effort to
capture samples by executing the take a sample algorithm in parallel each time the sample interval has elapsed.
In the paused
and stopped
states, the UA SHOULD NOT capture samples.
Profiling sessions MUST begin in the started
state.
The UA MAY move a session from started
to paused
, and from paused
to started
.
The user agent is RECOMMENDED to pause the sampling of a profiling session if the browsing context is not in the foreground.
A stopped
session MUST NOT move to the started
or paused
states.
To take a sample given a profiling session, perform the following steps:
Profiler
, move the state to stopped
, and return.ProfilerSample
.ProfilerTrace
.To get a stack ID given an execution context stack bound to stack, perform the following steps:
undefined
.undefined
, return parentId.ProfilerStack
with ProfilerStack.frameId equal to frameId, and ProfilerStack.parentId equal to parentId.
To get a frame ID given an execution context bound to context, perform the following steps:
undefined
.ScriptOrModule
associated with context.ScriptOrModule
containing the function that invoked instance.
The purpose of the above logic is to ensure that built-in functions invoked by inaccessible scripts are not exposed in traces, by using the ScriptOrModule
that invoked them for attribution.
"[...] the ScriptOrModule
containing the function that invoked instance" should be defined more rigorously. We could leverage the top-most execution context on the stack that defines a ScriptOrModule
to provide this, but it's not ideal -- there may (theoretically) be other mechanisms for a builtin to be enqueued on the execution context stack, in which case the attribution would be invalid.
undefined
.true
, return undefined
.
This check ensures that we avoid including stack frames from cross-origin scripts served in a CORS-cross-origin response. We may want to consider renaming muted errors to better reflect this use case.
ProfilerFrame
.To get an element ID for an item in a list, run the following steps:
WebIDL[Exposed=Window]
interface Profiler
: EventTarget {
readonly attribute DOMHighResTimeStamp sampleInterval
;
readonly attribute boolean stopped
;
constructor
(ProfilerInitOptions
options);
Promise<ProfilerTrace
> stop
();
};
Each Profiler
MUST be associated with exactly one profiling session.
The sampleInterval
attribute MUST reflect the sample interval of the associated profiling session expressed as a DOMHighResTimeStamp.
The stopped
attribute MUST be true if and only if the profiling session has state stopped
.
Profiler
is only exposed on Window
until consensus is reached on [Permissions-Policy] and Worker
integration.
new Profiler(options)
runs the following steps given an object options of type ProfilerInitOptions
:
sampleInterval
is less than 0, throw a RangeError
."js-profiling"
in the Document. If the result is false, throw a "NotAllowedError"
DOMException.maxBufferSize
.ProfilerTrace
is set to «[resources
→ «», frames
→ «», stacks
→ «», samples
→ «»]»
.Profiler
associated with the newly created profiling session.Stops the profiler and returns a trace. This method MUST run these steps:
stopped
, return a promise rejected with an "InvalidStateError"
DOMException.
stopped
.ProfilerTrace
associated with the profiler's profiling session.
Any samples taken after stop()
is invoked SHOULD NOT be included by the profiling session.
WebIDLtypedef DOMString ProfilerResource
;
dictionary ProfilerTrace
{
required sequence<ProfilerResource
> resources
;
required sequence<ProfilerFrame
> frames
;
required sequence<ProfilerStack
> stacks
;
required sequence<ProfilerSample
> samples
;
};
The resources
attribute MUST return the ProfilerResource
list set by the take a sample algorithm.
The frames
attribute MUST return the ProfilerFrame
list set by the take a sample algorithm.
The stacks
attribute MUST return the ProfilerStack
list set by the take a sample algorithm.
The samples
attribute MUST return the ProfilerSample
list set by the take a sample algorithm.
Inspired by the V8 trace event format and Gecko profile format, this representation is designed to be easily and efficiently serializable.
WebIDLenum ProfilerMarker
{ "script
", "gc
", "style
", "layout
", "paint
", "other
" };
dictionary ProfilerSample
{
required DOMHighResTimeStamp timestamp
;
unsigned long long stackId
;
ProfilerMarker
? marker
;
};
timestamp
MUST return the value it was initialized to.
stackId
MUST return the value it was initialized to.
marker
MUST return the value it was initialized to, if present. The availability of markers depends on the context's cross-origin isolation status.
In cross-origin isolated contexts, all marker types are available. In non-isolated contexts, only style
and layout
markers are exposed for security reasons.
WebIDLdictionary ProfilerStack
{
unsigned long long parentId
;
required unsigned long long frameId
;
};
parentId
MUST return the value it was initialized to.
frameId
MUST return the value it was iniitalized to.
WebIDLdictionary ProfilerFrame
{
required DOMString name
;
unsigned long long resourceId
;
unsigned long long line
;
unsigned long long column
;
};
name
MUST return the value it was initialized to.
resourceId
MUST return the value it was initialized to.
line
MUST return the value it was initialized to.
column
MUST return the value it was initialized to.
WebIDLdictionary ProfilerInitOptions
{
required DOMHighResTimeStamp sampleInterval
;
required unsigned long maxBufferSize
;
};
ProfilerInitOptions
MUST support the following fields:
sampleInterval
is the application's desired sample interval. This value MUST be greater than or equal to zero.maxBufferSize
is the desired sample buffer size limit, in samples.
This spec defines a configuration point in Document Policy with name js-profiling
. Its type is boolean
with default value false
.
Document policy is leveraged to give UAs the ability to avoid storing
required metadata for profiling when the document does not explicitly
request it. While this metadata could conceivably be generated in
response to a profiler being started, we store this bit on the document
in order to signal to the engine as early as possible (as profiling early
in page load is a common use case). This overhead may be non-trivial
depending on the implementation, and therefore we default to
false
.
For the purposes of user-agent automation and application testing, this document defines the following [WebDriver] extension command.
HTTP Method | URI Template |
---|---|
POST | /session/{session id}/forcesample |
The Force Sample extension command forces all profiling sessions to take a sample for the purpose of enabling more deterministic testing.
The remote end steps are:
started
, take a sample with session.null
.This section is non-normative.
The following sections detail some of the privacy and security choices of the API, illustrating protection strategies against various types of attacks.
The API avoids exposing contents of cross-origin scripts by requiring all functions included via the take a sample algorithm to be defined in a script served with CORS-same-origin through the muted errors property. Browser builtins (such as performance.now()
) must also only be included when invoked from CORS-same-origin script.
As a result, the API does not expose any new insight into the contents or execution characteristics of cross-origin script, beyond what is already possible through manual instrumentation. UAs are encouraged to verify this holds if they choose to support extremely low sample interval values (e.g. less than one millisecond).
Cross-origin execution contexts should not be observable by the API through the realm check in the take a sample algorithm. Cross-origin iframes
and other execution contexts that share an agent with a profiler will therefore not have their execution observable through this API.
Timing attacks remain a concern for any API that could introduce a new source of high-resolution timing information. Timestamps gathered in traces should be obtained from the same source as [HR-Time]'s current high resolution time to avoid exposing a new vector for side-channel attacks.
See [HR-Time]'s discussion on clock resolution.
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in: