This specification describes an API that allows web applications to control a sampling profiler for measuring client JavaScript execution times.
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.
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
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.
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:
stopped, and return.To get a stack ID given an execution context stack bound to stack, perform the following steps:
undefined.undefined, return 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.
To get an element ID for an item in a list, run the following steps:
[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.
RangeError."js-profiling-mode" in the Document. Let |profilingMode| be the result.
"eager" or "lazy":
"js-profiling" in the Document. Let |jsProfilingEnabled : boolean| be the result.
"NotAllowedError" DOMException.
«[{{ProfilerTrace/resources}} → «», {{ProfilerTrace/frames}} → «», {{ProfilerTrace/stacks}} → «», {{ProfilerTrace/samples}} → «»]».Stops the profiler and returns a trace. This method MUST run these steps:
stopped, return [= a promise rejected with =] an "InvalidStateError" DOMException.
stopped.Any samples taken after stop() is invoked SHOULD NOT be included by the profiling session.
typedef 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.
dictionary ProfilerSample {
required DOMHighResTimeStamp timestamp;
unsigned long long stackId;
};
timestamp MUST return the value it was initialized to.
stackId MUST return the value it was initialized to.
dictionary 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.
dictionary 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.
dictionary ProfilerInitOptions {
required DOMHighResTimeStamp sampleInterval;
required unsigned long maxBufferSize;
};
ProfilerInitOptions MUST support the following fields:
This spec defines configuration points in Document Policy to control profiling capability and initialization behavior.
js-profiling-mode
This spec defines a configuration point with name js-profiling-mode. Its type is enum with allowed values "eager" and "lazy". The default value is the empty string.
When set, this policy authorizes scripts to use the JS Self-Profiling API.
eager
When js-profiling-mode is set to "eager", it signals to the UA that profiling during page load is expected.
UAs can use this hint to perform early initialization of profiling infrastructure, storing required metadata and warming up profiling components as early as possible during document load. However, this may introduce non-trivial performance overhead even when profiling is not actively used. This overhead may negatively impact metrics such as First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
lazy
When js-profiling-mode is set to "lazy", it signals to the UA that profiling will be used conditionally.
UAs can use this hint to defer profiling-related initialization overhead until the first Profiler is instantiated, avoiding performance costs during critical rendering periods when profiling is not actively used. However, if initialization occurs during user interaction processing, it may negatively impact Interaction to Next Paint (INP). This mode is particularly suitable for documents that conditionally enable profiling based on sampling decisions.
js-profiling (deprecated)
This spec also defines a configuration point with name js-profiling. Its type is boolean with default value false.
When enabled, this policy authorizes scripts to use the JS Self-Profiling API and signals to the UA to perform early initialization of profiling infrastructure (semantically equivalent to js-profiling-mode=eager).
The js-profiling boolean configuration point is deprecated in favor of js-profiling-mode. If both are specified, js-profiling-mode takes precedence and js-profiling MUST be ignored. Implementations SHOULD support js-profiling for backwards compatibility but MAY remove support in the future.
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.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.