The runtime implements the WHATWG/WinterTC-standard Performance surface: High
Resolution Time, User Timing Level
3 and the Performance
Timeline with
PerformanceObserver.
Globals (own, writable, enumerable, configurable properties of globalThis,
in main and worker isolates alike): performance, Performance,
PerformanceEntry, PerformanceMark, PerformanceMeasure,
PerformanceObserver, PerformanceObserverEntryList.
performance.now()— double milliseconds since the isolate's time origin, monotonic (V8's platform clock,CLOCK_MONOTONIC-based: it does not tick while the device is suspended), full double precision with no coarsening.performance.timeOrigin— readonly accessor; wall-clock milliseconds since the Unix epoch, sampled once when the isolate's runtime is created. Each worker gets its own time origin at worker-thread start, sotimeOrigin + now()approximatesDate.now()per isolate while the device stays awake; it drifts behind after device suspend (the monotonic clock does not tick then) and diverges under wall-clock adjustments.performance.toJSON(),Symbol.toStringTag, andPerformance extends EventTargetper spec;performance,PerformanceEntry,PerformanceMeasureandPerformanceObserverEntryListare not user-constructible (newthrowsTypeError);new PerformanceMark(name, options)is constructible per spec but does not buffer the entry.- User timing:
mark(name, {startTime, detail}),measure(name, startOrOptions, endMark)with the full Level 3 options algebra ({start, end, duration, detail}, mark names or timestamps, over-/under-constraint errors),clearMarks(name?),clearMeasures(name?). - Timeline:
getEntries(),getEntriesByType(type),getEntriesByName(name, type?)return copies sorted chronologically bystartTime(stable for ties). - Observers:
new PerformanceObserver(cb),observe({entryTypes})orobserve({type, buffered}),disconnect(),takeRecords(), static frozenPerformanceObserver.supportedEntryTypes, which is["mark", "measure"].
All spec logic lives in the internal/performance.js builtin
(test-app/runtime/src/main/cpp/js/performance.js), shared with the iOS
runtime: the native side hands it only { now(), timeOrigin }, so the same
file runs unchanged on both runtimes and should be kept in sync with iOS's
copy.
The native clock is owned by Runtime (Runtime::PerformanceNowMillis(),
Runtime::TimeOriginMillis(), Runtime::TimeOriginMonotonicMillis(),
captured in Runtime::PrepareV8Runtime) and exposed to native callers through
tns::Performance::NowMillis(isolate)
(test-app/runtime/src/main/cpp/Performance.h). Any native producer of
JS-visible timestamps must read the clock through that hook rather than
sampling its own, so every timestamp shares performance.timeOrigin as its
base.
__postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn)
(test-app/runtime/src/main/cpp/FrameCallbacks.h) schedule fn for the next
display frame. fn receives two arguments:
__postFrameCallback((frameTimeNanos, performanceMillis) => { … });frameTimeNanos— the platform's raw frame time:CLOCK_MONOTONICnanoseconds, theSystem.nanoTime()base. Unchanged from earlier runtimes, which passed it as the only argument.performanceMillis— the same instant on this isolate's performance timeline, so it compares directly withperformance.now(). Converted natively throughPerformance::MonotonicNanosToTimelineMillis(), which subtractsRuntime::TimeOriginMonotonicMillis()— Choreographer stamps frames on the very clock the time origin is captured on, so the mapping is exact rather than an approximation resampled in JS.
Two implementations sit behind that one surface: the NDK's AChoreographer
(API 24+, resolved with dlsym) and android.view.Choreographer through
com.tns.FrameCallbacks for API 21–23, where the NDK API does not exist.
Scheduling is per calling thread, so a worker schedules against its own looper.
Both paths produce the same two arguments with the same exactness.
- Buffers are unbounded. Per spec for user timing.
detailis structured-cloned at entry creation (per spec — an uncloneabledetailthrows theDataCloneError-named error, see structuredClone), so entries hold snapshots, but a long-lived app marking in a loop should still clear entries periodically. - Observer callbacks run from a microtask, not a queued task: delivery is
asynchronous relative to
mark()/measure()but precedes timer callbacks scheduled in the same turn. Callback exceptions are routed toreportError, so one throwing observer does not starve the others. - No
DOMException. Errors the specs express asDOMException— theSyntaxErrorfor a missing mark name, theInvalidModificationErrorfor switching an observer between theentryTypesandtypeforms — areErrorinstances withnamepatched.err.namechecks work;instanceof DOMExceptiondoes not. - Browser-only surface is absent: no resource/navigation timing, no
eventCounts, and noPerformanceTiming-attribute resolution inmeasure().