This specification describes an API that allows web applications to control a sampling profiler for measuring client JavaScript execution times.

Introduction

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.

Examples

        const profiler = await performance.profile({ sampleInterval: 10 });
        for (let i = 0; i < 1000000; i++) {
             doWork();
        }
        const trace = await profiler.stop();
        sendTrace(trace);
        

Definitions

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.

Profiling Sessions

A profiling session is an abstract producer of samples. Each session has:

  1. A global object as defined in [[HTML]].
  2. A state, which is one of {started, paused, stopped}.
  3. A sample interval, defined as the periodicity at which the session obtains samples.

    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.

  4. An origin.
  5. A time origin that samples' timestamps are measured relative to.
  6. A sample buffer storing captured samples, with a finite sample buffer size limit.

Multiple profiling sessions on the same page SHOULD be supported.

States

In the started state, the UA SHOULD make a best-effort to capture samples by executing the appropriate sampling algorithm 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.

The UA MAY move a session to stopped from any state.

A stopped session MUST NOT move to the started or paused states.

If the number of samples captured by a started session reaches the sample buffer size limit, the session MUST move to the stopped state and a new event of type samplebufferfull MUST be sent to the event handler associated with the session's associated Profiler.

Sampling Algorithm

This section is non-normative for now, the specifics currently being driven by results from a prototype implementation.

We define the sampling algorithm as follows:

  1. Let env be the script execution environment of the profiling session.
  2. Let stack be the stack of execution contexts associated with env.
  3. For each execution context ec on stack:
    1. Let ecs be the script associated with ec.
    2. If the realm associated with ecs is not equal to the realm of the global object associated with the profiling session, skip.
    3. If the origin of the script associated with ecs is not equal to the origin of the browsing context, and its associated script tag (if present) does not pass a CORS check, skip.
    4. Otherwise, include the stack frame.
  4. Record a new sample with all stack frames recorded in the algorithm, associated with the current timestamp relative to the browsing context's time origin.

Note on script origins

A origin of an execution context's script (pending formalization) can be mapped into the following cases in lieu of a formal algorithm:

The Profiler Interface

      [Exposed=(Window,Worker)]
      interface Profiler {
        readonly attribute DOMHighResTimeStamp sampleInterval;
        readonly attribute boolean stopped;

        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.

stop() method

Stops the profiler and returns a trace. This method MUST run these steps:

  1. If the associated profiling session's state is stopped, return the ProfilerTrace captured by the session.
  2. Set the profiling session's state to stopped.
  3. Return a ProfilerTrace from the profiling session.

Any samples taken after stop() is invoked SHOULD NOT be included by the profiling session.

The ProfilerTrace Dictionary

      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 contain exactly all unique URIs captured during the profiling session.

The frames attribute MUST contain all frames recorded during the profiling session.

The stacks attribute MUST contain all stacks recorded during the profiling session.

The samples attribute MUST return all samples recorded during the profiling session, ordered from earliest to latest recorded.

Inspired by the V8 trace event format and Gecko profile format, this representation is designed to be easily and efficiently serializable.

The ProfilerSample Dictionary

        dictionary ProfilerSample {
          required DOMHighResTimeStamp timestamp;
          unsigned long long stackId;
        };
        

The timestamp attribute MUST be the current high resolution time relative to the profiling session's time origin when the sample was recorded.

If the sample captured a stack, the stackId attribute MUST reference a valid index into the parent ProfilerTrace.stacks, where ProfilerTrace.stacks[stackId] is the leaf stack associated with this sample. Otherwise, it MUST be undefined.

The ProfilerStack Dictionary

        dictionary ProfilerStack {
          unsigned long long parentId;
          required unsigned long long frameId;
        };
        

The parentId attribute MUST be either null if the associated frame is at the bottom of the stack, OR a valid index into the parent ProfilerTrace.stacks, where ProfilerTrace.stacks[parentId] is a parent substack containing outer frames.

The frameId attribute MUST reference a valid index into the parent ProfilerTrace.frames, where ProfilerTrace.frames[frameId] is the frame associated with this stack's top.

The ProfilerFrame Dictionary

        dictionary ProfilerFrame {
          DOMString name;
          required unsigned long long resourceId;
          required unsigned long long line;
          required unsigned long long column;
        };
        

Let ec be the execution context that the frame models.

Let instance be the function instance associated with ec, or null.

A more rigorous definition needs to be applied to the line and column attributes, particularly to define them for inline, eval, and toplevel scripts.

Extensions to the Performance Interface

      dictionary ProfilerInitOptions {
        required DOMHighResTimeStamp sampleInterval;
        required unsigned long maxBufferSize;
      };

      [Exposed=(Window,Worker)]
      partial interface Performance {
        Promise<Profiler> profile(ProfilerInitOptions options);
      };
      

ProfilerInitOptions MUST support the following fields:

profile() method

Creates a new Profiler associated with a newly started profiling session. It MUST run these steps:

  1. If options' ProfilerInitOptions.sampleInterval is less than 0, reject the promise with a RangeError.
  2. If the requesting document's origin is not allowed to use the "js-profiling" feature policy, reject the promise with a "SecurityError" DOMException.
  3. Create a new profiling session where:
    1. The associated global object is set to the current global object.
    2. The associated sample interval is set to either ProfilerInitOptions.sampleInterval OR the next lowest interval supported by the UA.
    3. The associated origin is equal to the origin of the global object's associated environment settings context.
    4. The associated time origin is equal to the time origin of the global object.
    5. The associated sample buffer is created with size ProfilerInitOptions.maxBufferSize.
  4. Return a Promise that yields a Profiler instance once the profiling session has started.

Feature Policy

This specification defines a new policy-controlled feature that controls whether the Performance.profile method may be invoked.

The feature identifier for this feature is "js-profiling".

The default allowlist for this feature is "none".

Feature policy is leveraged here to give embedders the ability to control whether or not embedded frames may start profilers, as well as hint to the UA that the page may wish to initialize profiling. The default policy is false to allow JS engines to avoid storing any profiling metadata for documents that do not intend to profile themselves.

Privacy and Security

The primary concern with profiling JavaScript running on an event loop shared by multiple browsing contexts is ensuring that stack frames from cross-origin browsing contexts are not leaked. The spec aims to avoid the leakage of such frames by utilizing a ECMA-262 realm-based filtering approach, allowing isolation of frames in profiling sessions to a single realm / frame to comply with the same-origin policy.

Another concern is the leaking of function names from foreign-origin scripts, which wouldn't normally be accessible without the resource participating in CORS. The spec aims to mitigate this by only including such foreign-origin functions if the resource that contains them passes a CORS check. For classic scripts, this means that functions are only included in traces if the crossorigin bit traditionally used for uncensoring Error.stack contents is present in the associated script tag.

As with any API that exposes accurate timing information, timing side-channel attacks are a risk. We are not aware of any new timing attacks enabled by this spec, but further analysis is necessary.