|
| 1 | +# Performance API |
| 2 | + |
| 3 | +The runtime implements the WHATWG/WinterTC-standard Performance surface: [High |
| 4 | +Resolution Time](https://w3c.github.io/hr-time/), [User Timing Level |
| 5 | +3](https://w3c.github.io/user-timing/) and the [Performance |
| 6 | +Timeline](https://w3c.github.io/performance-timeline/) with |
| 7 | +`PerformanceObserver`. |
| 8 | + |
| 9 | +## Surface |
| 10 | + |
| 11 | +Globals (own, writable, enumerable, configurable properties of `globalThis`, |
| 12 | +in main and worker isolates alike): `performance`, `Performance`, |
| 13 | +`PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, |
| 14 | +`PerformanceObserver`, `PerformanceObserverEntryList`. |
| 15 | + |
| 16 | +- `performance.now()` — double milliseconds since the isolate's time origin, |
| 17 | + monotonic (V8's platform clock, `CLOCK_MONOTONIC`-based: it does not tick |
| 18 | + while the device is suspended), full double precision with no coarsening. |
| 19 | +- `performance.timeOrigin` — readonly accessor; wall-clock milliseconds since |
| 20 | + the Unix epoch, sampled once when the isolate's runtime is created. Each |
| 21 | + worker gets its own time origin at worker-thread start, so |
| 22 | + `timeOrigin + now()` approximates `Date.now()` per isolate while the device |
| 23 | + stays awake; it drifts behind after device suspend (the monotonic clock does |
| 24 | + not tick then) and diverges under wall-clock adjustments. |
| 25 | +- `performance.toJSON()`, `Symbol.toStringTag`, and `Performance extends |
| 26 | + EventTarget` per spec; `performance`, `PerformanceEntry`, |
| 27 | + `PerformanceMeasure` and `PerformanceObserverEntryList` are not |
| 28 | + user-constructible (`new` throws `TypeError`); `new PerformanceMark(name, |
| 29 | + options)` is constructible per spec but does not buffer the entry. |
| 30 | +- User timing: `mark(name, {startTime, detail})`, `measure(name, |
| 31 | + startOrOptions, endMark)` with the full Level 3 options algebra (`{start, |
| 32 | + end, duration, detail}`, mark names or timestamps, over-/under-constraint |
| 33 | + errors), `clearMarks(name?)`, `clearMeasures(name?)`. |
| 34 | +- Timeline: `getEntries()`, `getEntriesByType(type)`, `getEntriesByName(name, |
| 35 | + type?)` return copies sorted chronologically by `startTime` (stable for |
| 36 | + ties). |
| 37 | +- Observers: `new PerformanceObserver(cb)`, `observe({entryTypes})` or |
| 38 | + `observe({type, buffered})`, `disconnect()`, `takeRecords()`, static frozen |
| 39 | + `PerformanceObserver.supportedEntryTypes`, which is `["mark", "measure"]`. |
| 40 | + |
| 41 | +## Architecture |
| 42 | + |
| 43 | +All spec logic lives in the `internal/performance.js` builtin |
| 44 | +(`test-app/runtime/src/main/cpp/js/performance.js`), shared with the iOS |
| 45 | +runtime: the native side hands it only `{ now(), timeOrigin }`, so the same |
| 46 | +file runs unchanged on both runtimes and should be kept in sync with iOS's |
| 47 | +copy. |
| 48 | + |
| 49 | +The native clock is owned by `Runtime` (`Runtime::PerformanceNowMillis()`, |
| 50 | +`Runtime::TimeOriginMillis()`, `Runtime::TimeOriginMonotonicMillis()`, |
| 51 | +captured in `Runtime::PrepareV8Runtime`) and exposed to native callers through |
| 52 | +`tns::Performance::NowMillis(isolate)` |
| 53 | +(`test-app/runtime/src/main/cpp/Performance.h`). Any native producer of |
| 54 | +JS-visible timestamps must read the clock through that hook rather than |
| 55 | +sampling its own, so every timestamp shares `performance.timeOrigin` as its |
| 56 | +base. |
| 57 | + |
| 58 | +## Frame callbacks |
| 59 | + |
| 60 | +`__postFrameCallback(fn[, delayMillis])` / `__removeFrameCallback(fn)` |
| 61 | +(`test-app/runtime/src/main/cpp/FrameCallbacks.h`) schedule `fn` for the next |
| 62 | +display frame. `fn` receives **two** arguments: |
| 63 | + |
| 64 | +```js |
| 65 | +__postFrameCallback((frameTimeNanos, performanceMillis) => { … }); |
| 66 | +``` |
| 67 | + |
| 68 | +- `frameTimeNanos` — the platform's raw frame time: `CLOCK_MONOTONIC` |
| 69 | + nanoseconds, the `System.nanoTime()` base. Unchanged from earlier runtimes, |
| 70 | + which passed it as the only argument. |
| 71 | +- `performanceMillis` — the same instant on this isolate's performance |
| 72 | + timeline, so it compares directly with `performance.now()`. Converted |
| 73 | + natively through `Performance::MonotonicNanosToTimelineMillis()`, which |
| 74 | + subtracts `Runtime::TimeOriginMonotonicMillis()` — Choreographer stamps |
| 75 | + frames on the very clock the time origin is captured on, so the mapping is |
| 76 | + exact rather than an approximation resampled in JS. |
| 77 | + |
| 78 | +Two implementations sit behind that one surface: the NDK's `AChoreographer` |
| 79 | +(API 24+, resolved with `dlsym`) and `android.view.Choreographer` through |
| 80 | +`com.tns.FrameCallbacks` for API 21–23, where the NDK API does not exist. |
| 81 | +Scheduling is per calling thread, so a worker schedules against its own looper. |
| 82 | +Both paths produce the same two arguments with the same exactness. |
| 83 | + |
| 84 | +## Deviations from the specs |
| 85 | + |
| 86 | +- **Buffers are unbounded.** Per spec for user timing. `detail` is |
| 87 | + structured-cloned at entry creation (per spec — an uncloneable `detail` |
| 88 | + throws the `DataCloneError`-named error, see |
| 89 | + [structuredClone](structured-clone.md)), so entries hold snapshots, but a |
| 90 | + long-lived app marking in a loop should still clear entries periodically. |
| 91 | +- **Observer callbacks run from a microtask**, not a queued task: delivery is |
| 92 | + asynchronous relative to `mark()`/`measure()` but precedes timer callbacks |
| 93 | + scheduled in the same turn. Callback exceptions are routed to |
| 94 | + `reportError`, so one throwing observer does not starve the others. |
| 95 | +- **No `DOMException`.** Errors the specs express as `DOMException` — the |
| 96 | + `SyntaxError` for a missing mark name, the `InvalidModificationError` for |
| 97 | + switching an observer between the `entryTypes` and `type` forms — are |
| 98 | + `Error` instances with `name` patched. `err.name` checks work; |
| 99 | + `instanceof DOMException` does not. |
| 100 | +- Browser-only surface is absent: no resource/navigation timing, no |
| 101 | + `eventCounts`, and no `PerformanceTiming`-attribute resolution in |
| 102 | + `measure()`. |
0 commit comments