Skip to content

Proposal: Expose engine-defined compute timing for WebNN (MLComputeTiming) #963

Open

Description

@shubhamg13

Problem

  • WebNN has no performance surface: no timing, no attribution.
  • dispatch() has no completion signal: "callers can await the results of reading back the
    output tensors."
  • The only way for a page to learn that a run finished is a readback — so the readback becomes
    part of the number, and the bracket differs per engine. Page-timed numbers are not interoperable.

Scenario

A page runs a face detector and wants to pick a smaller model on a slow device. It cannot tell
whether the slow device is limited by the model, the hardware, or its own input preparation.
The only measurement it can take is this:

const start = performance.now();
context.dispatch(graph, inputs, outputs);
await context.readTensor(a);          // the only completion signal
await context.readTensor(b);
console.log(`Inference: ${(performance.now() - start).toFixed(2)}ms`);

On a reference mobile device (NPU backend):

measurement how it was taken value
actual graph execution engine timestamps around the backend run 5.69 ms
dispatch round trip page timer around dispatch() only 5.85 ms
the code above dispatch() + two awaited readTensor() calls 209.23 ms

The page's "inference" is 209 ms against 5.7 ms of computation, and it cannot see the difference.

Proposal

MLComputeTiming: a PerformanceEntry interface with two engine-created entry types.

[Exposed=(Window,Worker)]
interface MLComputeTiming : PerformanceEntry {
  readonly attribute DOMHighResTimeStamp computeDuration;
  readonly attribute boolean accelerated;
  [Default] object toJSON();
};
entryType created when
ml-compile compilation completes, incl. initialization (build())
ml-compute one graph execution completes (dispatch())
  • One entry per execution, never per operator.
  • Observer-only: availableFromTimeline: false, delivered through PerformanceObserver.
  • duration = submission → completion (queueing included); computeDuration = the engine's own
    compute interval inside it.
  • graphId (engine-assigned, per context) correlates compile and run entries; accelerated is
    one bit.

Relations

  • #934 (effective compute policy):
    complementary — this contributes measurements and the privacy framing, not a device surface.
  • #923 (compute policy rename):
    decides the long-term shape of the accelerated field.
  • Model Loader (draft): the same entries are
    designed to cover its load() / compute() once that API stabilises.
  • WebPerf: the entry types need a row in the timing entry types registry. WebNN owns the
    semantics; WebPerf owns the registration.

Measurements, alternatives, privacy and the open questions (naming, graphId vs MLGraph.id,
accelerated shape, computeDuration normativity) are in the explainer: link when published.

Activity

  1. vmukhachev commented on Oct 20, 2026

    @vmukhachev

    How to measure or estimate execution time of some nodes or groups of nodes? Split the graph into multiple individual graphs?

  2. shubhamg13 commented on Oct 21, 2026

    @shubhamg13
    ContributorAuthor

    Yes — split the graph into subgraphs.

  3. changed the title [-]Expose engine-defined compute timing for WebNN (`MLComputeTiming`)[/-] [+]Proposal: Expose engine-defined compute timing for WebNN (`MLComputeTiming`)[/+] on Oct 22, 2026
  4. shubhamg13 commented on Oct 22, 2026

    @shubhamg13
    ContributorAuthor
  5. shubhamg13 commented on Oct 26, 2026

    @shubhamg13
    ContributorAuthor
    Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions