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"
in the Document. If the result is false, throw a "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 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
.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.