From 4edc33e4e141bbd6469b488e6ce1bc8b4547296e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 16 Jul 2025 02:48:43 -0700 Subject: [PATCH] Refactor implementation of performance.mark and performance.measure (#52586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52586 Changelog: [internal] This fixes a "bug" (or spec-compliance issue) in the `performance.measure` method where `end` and `duration` couldn't be used together (only `start` and `duration`, and `start` and `end` could be used). This also refactors the API to be "JS-first", preparing the native module methods to support passing instances of entries to fix other issues. Verified performance impact: `mark` is slightly regressed (~7% slower) due to an additional JSI call to get the default start time and `measure` is significantly optimized (25/30% faster) due to simplified parameter handling in JSI calls. * Before | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------- | -------------------- | ------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'mark (default)' | '1621.03 ± 1.17%' | '1562.00' | '636986 ± 0.01%' | '640205' | 616890 | | 1 | 'mark (with custom startTime)' | '1756.88 ± 1.15%' | '1693.00' | '586826 ± 0.01%' | '590667' | 569190 | | 2 | 'measure (default)' | '2424.66 ± 1.35%' | '2333.00' | '426122 ± 0.02%' | '428633' | 412429 | | 3 | 'measure (with start and end timestamps)' | '2679.96 ± 1.23%' | '2574.00' | '385266 ± 0.02%' | '388500' | 373140 | | 4 | 'measure (with mark names)' | '2713.49 ± 0.50%' | '2644.00' | '375383 ± 0.02%' | '378215' | 368530 | | 5 | 'clearMarks' | '691.13 ± 0.07%' | '681.00' | '1467016 ± 0.01%' | '1468429' | 1446900 | | 6 | 'clearMeasures' | '706.00 ± 0.05%' | '691.00' | '1431489 ± 0.01%' | '1447178' | 1416435 | | 7 | 'mark + clearMarks' | '2083.21 ± 1.14%' | '2003.00' | '497974 ± 0.01%' | '499251' | 480028 | | 8 | 'measure + clearMeasures (with start and end timestamps)' | '3085.14 ± 0.88%' | '2974.00' | '334337 ± 0.02%' | '336247' | 324135 | | 9 | 'measure + clearMeasures (with mark names)' | '2949.45 ± 0.62%' | '2884.00' | '345335 ± 0.02%' | '346741' | 339046 | * After | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------- | -------------------- | ------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'mark (default)' | '1740.06 ± 1.01%' | '1692.00' | '587400 ± 0.01%' | '591017' | 574695 | | 1 | 'mark (with custom startTime)' | '1661.64 ± 1.16%' | '1612.00' | '617453 ± 0.01%' | '620347' | 601815 | | 2 | 'measure (default)' | '1808.71 ± 1.28%' | '1753.00' | '566516 ± 0.01%' | '570451' | 552882 | | 3 | 'measure (with start and end timestamps)' | '1869.21 ± 1.00%' | '1823.00' | '546571 ± 0.01%' | '548546' | 534987 | | 4 | 'measure (with mark names)' | '2016.40 ± 0.74%' | '1983.00' | '502987 ± 0.01%' | '504286' | 496075 | | 5 | 'clearMarks' | '682.18 ± 0.03%' | '671.00' | '1476364 ± 0.01%' | '1490313' | 1465899 | | 6 | 'clearMeasures' | '686.78 ± 0.03%' | '681.00' | '1467264 ± 0.01%' | '1468429' | 1456081 | | 7 | 'mark + clearMarks' | '2148.90 ± 1.28%' | '2073.00' | '480925 ± 0.01%' | '482393' | 465356 | | 8 | 'measure + clearMeasures (with start and end timestamps)' | '2277.26 ± 1.10%' | '2204.00' | '451016 ± 0.01%' | '453721' | 439125 | | 9 | 'measure + clearMeasures (with mark names)' | '2277.82 ± 0.51%' | '2243.00' | '443853 ± 0.01%' | '445831' | 439016 | Reviewed By: hoxyq Differential Revision: D78193072 fbshipit-source-id: 03b52927a1999f19a2baf0d02335a959525d7add --- .../webperformance/NativePerformance.cpp | 112 ++------ .../webperformance/NativePerformance.h | 28 +- .../timeline/PerformanceEntryReporter.cpp | 22 +- .../timeline/PerformanceEntryReporter.h | 10 +- .../tests/PerformanceEntryReporterTest.cpp | 44 ++- .../tests/PerformanceObserverTest.cpp | 6 +- .../webapis/performance/Performance.js | 267 ++++++++++-------- .../performance/__tests__/UserTiming-itest.js | 8 +- .../performance/specs/NativePerformance.js | 25 +- 9 files changed, 217 insertions(+), 305 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp index 34b2cc21e84..cb833ab134c 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp @@ -120,108 +120,30 @@ HighResTimeStamp NativePerformance::now(jsi::Runtime& /*rt*/) { return forcedCurrentTimeStamp_.value_or(HighResTimeStamp::now()); } -HighResTimeStamp NativePerformance::markWithResult( +void NativePerformance::reportMark( jsi::Runtime& rt, std::string name, - std::optional startTime) { - auto entry = PerformanceEntryReporter::getInstance()->reportMark( - name, startTime.value_or(now(rt))); - return entry.startTime; + HighResTimeStamp startTime, + jsi::Value /*entry*/) { + PerformanceEntryReporter::getInstance()->reportMark(name, startTime); } -std::tuple NativePerformance::measure( - jsi::Runtime& runtime, - std::string name, - std::optional startTime, - std::optional endTime, - std::optional duration, - std::optional startMark, - std::optional endMark) { - auto reporter = PerformanceEntryReporter::getInstance(); - - HighResTimeStamp startTimeValue; - - // If the start time mark name is specified, it takes precedence over the - // startTime parameter, which can be set to 0 by default from JavaScript. - if (startTime) { - startTimeValue = *startTime; - } else if (startMark) { - if (auto startMarkBufferedTime = reporter->getMarkTime(*startMark)) { - startTimeValue = *startMarkBufferedTime; - } else { - throw jsi::JSError( - runtime, "The mark '" + *startMark + "' does not exist."); - } - } else { - startTimeValue = HighResTimeStamp::fromDOMHighResTimeStamp(0); - } - - HighResTimeStamp endTimeValue; - - if (endTime) { - endTimeValue = *endTime; - } else if (duration) { - endTimeValue = startTimeValue + *duration; - } else if (endMark) { - if (auto endMarkBufferedTime = reporter->getMarkTime(*endMark)) { - endTimeValue = *endMarkBufferedTime; - } else { - throw jsi::JSError( - runtime, "The mark '" + *endMark + "' does not exist."); - } - } else { - // The end time is not specified, take the current time, according to the - // standard - endTimeValue = now(runtime); - } - - auto entry = reporter->reportMeasure(name, startTimeValue, endTimeValue); - return std::tuple{entry.startTime, entry.duration}; -} - -std::tuple -NativePerformance::measureWithResult( - jsi::Runtime& runtime, +void NativePerformance::reportMeasure( + jsi::Runtime& rt, std::string name, HighResTimeStamp startTime, - HighResTimeStamp endTime, - std::optional duration, - std::optional startMark, - std::optional endMark) { - auto reporter = PerformanceEntryReporter::getInstance(); + HighResDuration duration, + jsi::Value /*entry*/) { + PerformanceEntryReporter::getInstance()->reportMeasure( + name, startTime, duration); +} - HighResTimeStamp startTimeValue = startTime; - // If the start time mark name is specified, it takes precedence over the - // startTime parameter, which can be set to 0 by default from JavaScript. - if (startMark) { - if (auto startMarkBufferedTime = reporter->getMarkTime(*startMark)) { - startTimeValue = *startMarkBufferedTime; - } else { - throw jsi::JSError( - runtime, "The mark '" + *startMark + "' does not exist."); - } - } - - HighResTimeStamp endTimeValue = endTime; - // If the end time mark name is specified, it takes precedence over the - // startTime parameter, which can be set to 0 by default from JavaScript. - if (endMark) { - if (auto endMarkBufferedTime = reporter->getMarkTime(*endMark)) { - endTimeValue = *endMarkBufferedTime; - } else { - throw jsi::JSError( - runtime, "The mark '" + *endMark + "' does not exist."); - } - } else if (duration) { - endTimeValue = startTimeValue + *duration; - } else if (endTimeValue < startTimeValue) { - // The end time is not specified, take the current time, according to the - // standard - endTimeValue = now(runtime); - } - - auto entry = reporter->reportMeasure(name, startTime, endTime); - return std::tuple{entry.startTime, entry.duration}; +std::optional NativePerformance::getMarkTime( + jsi::Runtime& rt, + std::string name) { + auto markTime = PerformanceEntryReporter::getInstance()->getMarkTime(name); + return markTime ? std::optional{(*markTime).toDOMHighResTimeStamp()} + : std::nullopt; } void NativePerformance::clearMarks( diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h index a1e05cf66c3..27971617d11 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h @@ -90,32 +90,20 @@ class NativePerformance : public NativePerformanceCxxSpec { #pragma mark - User Timing Level 3 functions (https://w3c.github.io/user-timing/) - // https://w3c.github.io/user-timing/#mark-method - HighResTimeStamp markWithResult( + void reportMark( jsi::Runtime& rt, std::string name, - std::optional startTime); + HighResTimeStamp time, + jsi::Value entry); - // https://w3c.github.io/user-timing/#measure-method - std::tuple measure( - jsi::Runtime& rt, - std::string name, - std::optional startTime, - std::optional endTime, - std::optional duration, - std::optional startMark, - std::optional endMark); - - // https://w3c.github.io/user-timing/#measure-method - [[deprecated("This method is deprecated. Use the measure method instead.")]] - std::tuple measureWithResult( + void reportMeasure( jsi::Runtime& rt, std::string name, HighResTimeStamp startTime, - HighResTimeStamp endTime, - std::optional duration, - std::optional startMark, - std::optional endMark); + HighResDuration duration, + jsi::Value entry); + + std::optional getMarkTime(jsi::Runtime& rt, std::string name); // https://w3c.github.io/user-timing/#clearmarks-method void clearMarks( diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp index 4bb9f8e44d2..2aa284f3eb9 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp @@ -167,12 +167,10 @@ void PerformanceEntryReporter::clearEntries( getBufferRef(entryType).clear(entryName); } -PerformanceMark PerformanceEntryReporter::reportMark( +void PerformanceEntryReporter::reportMark( const std::string& name, - const std::optional& startTime) { - // Resolve timings - auto startTimeVal = startTime ? *startTime : getCurrentTimeStamp(); - const auto entry = PerformanceMark{{.name = name, .startTime = startTimeVal}}; + const HighResTimeStamp startTime) { + const auto entry = PerformanceMark{{.name = name, .startTime = startTime}}; traceMark(entry); @@ -183,18 +181,14 @@ PerformanceMark PerformanceEntryReporter::reportMark( } observerRegistry_->queuePerformanceEntry(entry); - - return entry; } -PerformanceMeasure PerformanceEntryReporter::reportMeasure( +void PerformanceEntryReporter::reportMeasure( const std::string& name, HighResTimeStamp startTime, - HighResTimeStamp endTime, + HighResDuration duration, const std::optional& trackMetadata) { - HighResDuration duration = endTime - startTime; - const auto entry = PerformanceMeasure{ {.name = std::string(name), .startTime = startTime, @@ -209,8 +203,6 @@ PerformanceMeasure PerformanceEntryReporter::reportMeasure( } observerRegistry_->queuePerformanceEntry(entry); - - return entry; } void PerformanceEntryReporter::clearEventCounts() { @@ -275,7 +267,7 @@ void PerformanceEntryReporter::reportLongTask( observerRegistry_->queuePerformanceEntry(entry); } -PerformanceResourceTiming PerformanceEntryReporter::reportResourceTiming( +void PerformanceEntryReporter::reportResourceTiming( const std::string& url, HighResTimeStamp fetchStart, HighResTimeStamp requestStart, @@ -302,8 +294,6 @@ PerformanceResourceTiming PerformanceEntryReporter::reportResourceTiming( } observerRegistry_->queuePerformanceEntry(entry); - - return entry; } void PerformanceEntryReporter::traceMark(const PerformanceMark& entry) const { diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h index 1bacd16e1d9..434625bdf8f 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h @@ -86,14 +86,12 @@ class PerformanceEntryReporter { std::optional getMarkTime( const std::string& markName) const; - PerformanceMark reportMark( - const std::string& name, - const std::optional& startTime = std::nullopt); + void reportMark(const std::string& name, HighResTimeStamp startTime); - PerformanceMeasure reportMeasure( + void reportMeasure( const std::string& name, HighResTimeStamp startTime, - HighResTimeStamp endTime, + HighResDuration duration, const std::optional& trackMetadata = std::nullopt); @@ -107,7 +105,7 @@ class PerformanceEntryReporter { void reportLongTask(HighResTimeStamp startTime, HighResDuration duration); - PerformanceResourceTiming reportResourceTiming( + void reportResourceTiming( const std::string& url, HighResTimeStamp fetchStart, HighResTimeStamp requestStart, diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp index 330eb185f44..bf8aaf06dba 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp @@ -129,20 +129,16 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { "mark2", timeOrigin + HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "measure0", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(2)); + "measure0", timeOrigin, HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "measure1", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(3)); + "measure1", timeOrigin, HighResDuration::fromMilliseconds(3)); reporter->reportMark( "mark3", timeOrigin + HighResDuration::fromNanoseconds(2.5 * 1e6)); reporter->reportMeasure( "measure2", timeOrigin + HighResDuration::fromMilliseconds(2), - timeOrigin + HighResDuration::fromMilliseconds(2)); + HighResDuration::fromMilliseconds(2)); reporter->reportMark( "mark4", timeOrigin + HighResDuration::fromMilliseconds(3)); @@ -172,7 +168,7 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { PerformanceMeasure{ {.name = "measure2", .startTime = timeOrigin + HighResDuration::fromMilliseconds(2), - .duration = HighResDuration::zero()}}, + .duration = HighResDuration::fromMilliseconds(2)}}, PerformanceMark{ {.name = "mark3", .startTime = @@ -203,21 +199,17 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { "mark2", timeOrigin + HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "common_name", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(2)); + "common_name", timeOrigin, HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "measure1", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(3)); + "measure1", timeOrigin, HighResDuration::fromMilliseconds(3)); reporter->reportMeasure( "measure2", timeOrigin + HighResDuration::fromMilliseconds(1), - timeOrigin + HighResDuration::fromMilliseconds(6)); + HighResDuration::fromMilliseconds(6)); reporter->reportMeasure( "measure3", timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6), - timeOrigin + HighResDuration::fromMilliseconds(2)); + HighResDuration::fromMilliseconds(2)); { const auto allEntries = toSorted(reporter->getEntries()); @@ -241,12 +233,12 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { PerformanceMeasure{ {.name = "measure2", .startTime = timeOrigin + HighResDuration::fromMilliseconds(1), - .duration = HighResDuration::fromMilliseconds(5)}}, + .duration = HighResDuration::fromMilliseconds(6)}}, PerformanceMeasure{ {.name = "measure3", .startTime = timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6), - .duration = HighResDuration::fromNanoseconds(0.5 * 1e6)}}, + .duration = HighResDuration::fromMilliseconds(2)}}, PerformanceMark{ {.name = "mark2", .startTime = timeOrigin + HighResDuration::fromMilliseconds(2), @@ -288,12 +280,12 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { PerformanceMeasure{ {.name = "measure2", .startTime = timeOrigin + HighResDuration::fromMilliseconds(1), - .duration = HighResDuration::fromMilliseconds(5)}}, + .duration = HighResDuration::fromMilliseconds(6)}}, PerformanceMeasure{ {.name = "measure3", .startTime = timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6), - .duration = HighResDuration::fromNanoseconds(0.5 * 1e6)}}}; + .duration = HighResDuration::fromMilliseconds(2)}}}; ASSERT_EQ(expected, measures); } @@ -330,21 +322,17 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearMarks) { "mark2", timeOrigin + HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "common_name", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(2)); + "common_name", timeOrigin, HighResDuration::fromMilliseconds(2)); reporter->reportMeasure( - "measure1", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(3)); + "measure1", timeOrigin, HighResDuration::fromMilliseconds(3)); reporter->reportMeasure( "measure2", timeOrigin + HighResDuration::fromMilliseconds(1), - timeOrigin + HighResDuration::fromMilliseconds(6)); + HighResDuration::fromMilliseconds(6)); reporter->reportMeasure( "measure3", timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6), - timeOrigin + HighResDuration::fromMilliseconds(2)); + HighResDuration::fromMilliseconds(2)); reporter->clearEntries(PerformanceEntryType::MARK, "common_name"); diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp index 0a954958288..8bb6a9163af 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp @@ -190,7 +190,7 @@ TEST(PerformanceObserver, PerformanceObserverTestObserveTakeRecords) { reporter->reportMeasure( "off", timeOrigin + HighResDuration::fromMilliseconds(10), - timeOrigin + HighResDuration::fromMilliseconds(20)); + HighResDuration::fromMilliseconds(20)); reporter->reportMark( "test2", timeOrigin + HighResDuration::fromMilliseconds(20)); reporter->reportMark( @@ -371,9 +371,7 @@ TEST(PerformanceObserver, PerformanceObserverTestMultiple) { {.durationThreshold = HighResDuration::fromMilliseconds(80)}); reporter->reportMeasure( - "measure", - timeOrigin, - timeOrigin + HighResDuration::fromMilliseconds(50)); + "measure", timeOrigin, HighResDuration::fromMilliseconds(50)); reporter->reportEvent( "event1", timeOrigin, diff --git a/packages/react-native/src/private/webapis/performance/Performance.js b/packages/react-native/src/private/webapis/performance/Performance.js index 968882ec2ec..789e901a14e 100644 --- a/packages/react-native/src/private/webapis/performance/Performance.js +++ b/packages/react-native/src/private/webapis/performance/Performance.js @@ -58,8 +58,9 @@ export type PerformanceMeasureOptions = const ENTRY_TYPES_AVAILABLE_FROM_TIMELINE: $ReadOnlyArray = ['mark', 'measure']; -const cachedNativeMark = NativePerformance?.markWithResult; -const cachedNativeMeasure = NativePerformance?.measure; +const cachedReportMark = NativePerformance?.reportMark; +const cachedReportMeasure = NativePerformance?.reportMeasure; +const cachedGetMarkTime = NativePerformance?.getMarkTime; const cachedNativeClearMarks = NativePerformance?.clearMarks; const cachedNativeClearMeasures = NativePerformance?.clearMeasures; @@ -74,6 +75,19 @@ const MEASURE_OPTIONS_REUSABLE_OBJECT: {...PerformanceMeasureInit} = { detail: undefined, }; +const getMarkTimeForMeasure = cachedGetMarkTime + ? (markName: string): number => { + const markTime = cachedGetMarkTime(markName); + if (markTime == null) { + throw new DOMException( + `Failed to execute 'measure' on 'Performance': The mark '${markName}' does not exist.`, + 'SyntaxError', + ); + } + return markTime; + } + : undefined; + /** * Partial implementation of the Performance interface for RN, * corresponding to the standard in @@ -141,6 +155,11 @@ export default class Performance { // Please run the benchmarks in `Performance-benchmarks-itest` to ensure // changes do not regress performance. + if (cachedReportMark === undefined) { + warnNoNativePerformance(); + return new PerformanceMark(markName, {startTime: 0}); + } + if (markName === undefined) { throw new TypeError( `Failed to execute 'mark' on 'Performance': 1 argument required, but only 0 present.`, @@ -150,46 +169,51 @@ export default class Performance { const resolvedMarkName = typeof markName === 'string' ? markName : String(markName); + let resolvedStartTime; let resolvedDetail; - const detail = markOptions?.detail; + + let startTime; + let detail; + if (markOptions != null) { + ({startTime, detail} = markOptions); + } + + if (startTime !== undefined) { + resolvedStartTime = + typeof startTime === 'number' ? startTime : Number(startTime); + if (resolvedStartTime < 0) { + throw new TypeError( + `Failed to execute 'mark' on 'Performance': '${resolvedMarkName}' cannot have a negative start time.`, + ); + } else if ( + // This is faster than calling Number.isFinite() + // eslint-disable-next-line no-self-compare + resolvedStartTime !== resolvedStartTime || + resolvedStartTime === Infinity + ) { + throw new TypeError( + `Failed to execute 'mark' on 'Performance': Failed to read the 'startTime' property from 'PerformanceMarkOptions': The provided double value is non-finite.`, + ); + } + } else { + resolvedStartTime = getCurrentTimeStamp(); + } + if (detail !== undefined) { resolvedDetail = structuredClone(detail); } - let computedStartTime; - if (cachedNativeMark) { - let resolvedStartTime; - - const startTime = markOptions?.startTime; - if (startTime !== undefined) { - resolvedStartTime = - typeof startTime === 'number' ? startTime : Number(startTime); - if (resolvedStartTime < 0) { - throw new TypeError( - `Failed to execute 'mark' on 'Performance': '${resolvedMarkName}' cannot have a negative start time.`, - ); - } else if ( - // This is faster than calling Number.isFinite() - // eslint-disable-next-line no-self-compare - resolvedStartTime !== resolvedStartTime || - resolvedStartTime === Infinity - ) { - throw new TypeError( - `Failed to execute 'mark' on 'Performance': Failed to read the 'startTime' property from 'PerformanceMarkOptions': The provided double value is non-finite.`, - ); - } - } - - computedStartTime = cachedNativeMark(resolvedMarkName, resolvedStartTime); - } else { - warnNoNativePerformance(); - computedStartTime = getCurrentTimeStamp(); - } - - MARK_OPTIONS_REUSABLE_OBJECT.startTime = computedStartTime; + MARK_OPTIONS_REUSABLE_OBJECT.startTime = resolvedStartTime; MARK_OPTIONS_REUSABLE_OBJECT.detail = resolvedDetail; - return new PerformanceMark(resolvedMarkName, MARK_OPTIONS_REUSABLE_OBJECT); + const entry = new PerformanceMark( + resolvedMarkName, + MARK_OPTIONS_REUSABLE_OBJECT, + ); + + cachedReportMark(resolvedMarkName, resolvedStartTime, entry); + + return entry; } clearMarks(markName?: string): void { @@ -210,20 +234,27 @@ export default class Performance { // Please run the benchmarks in `Performance-benchmarks-itest` to ensure // changes do not regress performance. + if ( + getMarkTimeForMeasure === undefined || + cachedReportMeasure === undefined + ) { + warnNoNativePerformance(); + return new PerformanceMeasure(measureName, {startTime: 0, duration: 0}); + } + + let resolvedMeasureName: string; + let resolvedStartTime: number; + let resolvedDuration: number; + let resolvedDetail: mixed; + if (measureName === undefined) { throw new TypeError( `Failed to execute 'measure' on 'Performance': 1 argument required, but only 0 present.`, ); } - let resolvedMeasureName = - typeof measureName === 'string' ? measureName : String(measureName); - let resolvedStartTime: number | void; - let resolvedStartMark: string | void; - let resolvedEndTime: number | void; - let resolvedEndMark: string | void; - let resolvedDuration: number | void; - let resolvedDetail: mixed; + resolvedMeasureName = + measureName === 'string' ? measureName : String(measureName); if (startMarkOrOptions != null) { switch (typeof startMarkOrOptions) { @@ -234,50 +265,65 @@ export default class Performance { ); } - const start = startMarkOrOptions.start; + const {start, end, duration, detail} = startMarkOrOptions; + + let resolvedEndTime; + + if ( + start !== undefined && + end !== undefined && + duration !== undefined + ) { + throw new TypeError( + `Failed to execute 'measure' on 'Performance': If a non-empty PerformanceMeasureOptions object was passed, it must not have all of its 'start', 'duration', and 'end' properties defined`, + ); + } + switch (typeof start) { + case 'undefined': { + // This will be handled after all options have been processed. + break; + } case 'number': { resolvedStartTime = start; break; } case 'string': { - resolvedStartMark = start; - break; - } - case 'undefined': { + resolvedStartTime = getMarkTimeForMeasure(start); break; } default: { - resolvedStartMark = String(start); + resolvedStartTime = getMarkTimeForMeasure(String(start)); } } - const end = startMarkOrOptions.end; switch (typeof end) { + case 'undefined': { + // This will be handled after all options have been processed. + break; + } case 'number': { resolvedEndTime = end; break; } case 'string': { - resolvedEndMark = end; - break; - } - case 'undefined': { + resolvedEndTime = getMarkTimeForMeasure(end); break; } default: { - resolvedEndMark = String(end); + resolvedEndTime = getMarkTimeForMeasure(String(end)); } } - const duration = startMarkOrOptions.duration; switch (typeof duration) { + case 'undefined': { + // This will be handled after all options have been processed. + break; + } case 'number': { resolvedDuration = duration; break; } - case 'undefined': - break; default: { resolvedDuration = Number(duration); if (!Number.isFinite(resolvedDuration)) { @@ -288,16 +334,28 @@ export default class Performance { } } - if ( - resolvedDuration !== undefined && - (resolvedEndMark !== undefined || resolvedEndTime !== undefined) - ) { - throw new TypeError( - `Failed to execute 'measure' on 'Performance': If a non-empty PerformanceMeasureOptions object was passed, it must not have all of its 'start', 'duration', and 'end' properties defined`, - ); + if (resolvedStartTime === undefined) { + if ( + resolvedEndTime !== undefined && + resolvedDuration !== undefined + ) { + resolvedStartTime = resolvedEndTime - resolvedDuration; + } else { + resolvedStartTime = 0; + } + } + + if (resolvedDuration === undefined) { + if ( + resolvedStartTime !== undefined && + resolvedEndTime !== undefined + ) { + resolvedDuration = resolvedEndTime - resolvedStartTime; + } else { + resolvedDuration = getCurrentTimeStamp() - resolvedStartTime; + } } - const detail = startMarkOrOptions.detail; if (detail !== undefined) { resolvedDetail = structuredClone(detail); } @@ -305,67 +363,54 @@ export default class Performance { break; } case 'string': { - resolvedStartMark = startMarkOrOptions; + resolvedStartTime = getMarkTimeForMeasure(startMarkOrOptions); if (endMark !== undefined) { - resolvedEndMark = String(endMark); + resolvedDuration = + getMarkTimeForMeasure(endMark) - resolvedStartTime; + } else { + resolvedDuration = getCurrentTimeStamp() - resolvedStartTime; } break; } default: { - resolvedStartMark = String(startMarkOrOptions); + resolvedStartTime = getMarkTimeForMeasure(String(startMarkOrOptions)); + + if (endMark !== undefined) { + resolvedDuration = + getMarkTimeForMeasure(endMark) - resolvedStartTime; + } else { + resolvedDuration = getCurrentTimeStamp() - resolvedStartTime; + } } } - } - - let computedStartTime = 0; - let computedDuration = 0; - - if (cachedNativeMeasure) { - try { - [computedStartTime, computedDuration] = cachedNativeMeasure( - resolvedMeasureName, - resolvedStartTime, - resolvedEndTime, - resolvedDuration, - resolvedStartMark, - resolvedEndMark, - ); - } catch (error) { - throw new DOMException( - "Failed to execute 'measure' on 'Performance': " + error.message, - 'SyntaxError', - ); - } - } else if (NativePerformance?.measureWithResult) { - try { - [computedStartTime, computedDuration] = - NativePerformance.measureWithResult( - resolvedMeasureName, - resolvedStartTime ?? 0, - resolvedEndTime ?? 0, - resolvedDuration, - resolvedStartMark, - resolvedEndMark, - ); - } catch (error) { - throw new DOMException( - "Failed to execute 'measure' on 'Performance': " + error.message, - 'SyntaxError', - ); - } } else { - warnNoNativePerformance(); + resolvedStartTime = 0; + + if (endMark !== undefined) { + resolvedDuration = getMarkTimeForMeasure(endMark) - resolvedStartTime; + } else { + resolvedDuration = getCurrentTimeStamp() - resolvedStartTime; + } } - MEASURE_OPTIONS_REUSABLE_OBJECT.startTime = computedStartTime; - MEASURE_OPTIONS_REUSABLE_OBJECT.duration = computedDuration ?? 0; + MEASURE_OPTIONS_REUSABLE_OBJECT.startTime = resolvedStartTime; + MEASURE_OPTIONS_REUSABLE_OBJECT.duration = resolvedDuration; MEASURE_OPTIONS_REUSABLE_OBJECT.detail = resolvedDetail; - return new PerformanceMeasure( + const entry = new PerformanceMeasure( resolvedMeasureName, MEASURE_OPTIONS_REUSABLE_OBJECT, ); + + cachedReportMeasure( + resolvedMeasureName, + resolvedStartTime, + resolvedDuration, + entry, + ); + + return entry; } clearMeasures(measureName?: string): void { diff --git a/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js b/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js index f25dad51cd9..85fa6443510 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js @@ -293,9 +293,7 @@ describe('User Timing', () => { expect(measure.detail).toBe(null); }); - // TODO fix case - // eslint-disable-next-line jest/no-disabled-tests - it.skip('works with an end timestamp and a duration', () => { + it('works with an end timestamp and a duration', () => { const measure = performance.measure( 'measure-with-end-timestamp-and-duration', { @@ -312,9 +310,7 @@ describe('User Timing', () => { expect(measure.detail).toBe(null); }); - // TODO fix case - // eslint-disable-next-line jest/no-disabled-tests - it.skip('works with an end mark and a duration', () => { + it('works with an end mark and a duration', () => { performance.mark('end-mark', { startTime: 40, }); diff --git a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js index 39014040cf0..eeb08d788e0 100644 --- a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js +++ b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js @@ -54,27 +54,14 @@ export type PerformanceObserverInit = { export interface Spec extends TurboModule { +now?: () => number; - +markWithResult?: ( - name: string, - startTime?: number, - ) => NativePerformanceMarkResult; - +measure?: ( - name: string, - startTime?: number, - endTime?: number, - duration?: number, - startMark?: string, - endMark?: string, - ) => NativePerformanceMeasureResult; - // DEPRECATED: Use measure instead. - +measureWithResult?: ( + +reportMark?: (name: string, startTime: number, entry: mixed) => void; + +reportMeasure?: ( name: string, startTime: number, - endTime: number, - duration?: number, - startMark?: string, - endMark?: string, - ) => NativePerformanceMeasureResult; + duration: number, + entry: mixed, + ) => void; + +getMarkTime?: (name: string) => ?number; +clearMarks?: (entryName?: string) => void; +clearMeasures?: (entryName?: string) => void; +getEntries?: () => $ReadOnlyArray;