diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp index 97de0d8b58d..c5994609bd1 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include @@ -324,6 +325,16 @@ void consoleAssert( #include "ForwardingConsoleMethods.def" #undef FORWARDING_CONSOLE_METHOD +/* + * Attempt to call String() on the given value. + */ +std::optional stringifyJsiValue( + const jsi::Value& value, + jsi::Runtime& runtime) { + auto String = runtime.global().getPropertyAsFunction(runtime, "String"); + return String.call(runtime, value).asString(runtime).utf8(runtime); +}; + /** * Call innerFn and forward any arguments to the original console method * named methodName, if possible. @@ -354,6 +365,116 @@ auto forwardToOriginalConsole( }; }; +/** + * Recording a marker on a timeline of the Performance instrumentation. + * No actual logging is provided by definition. + * https://developer.mozilla.org/en-US/docs/Web/API/console/timeStamp_static + * https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp + */ +void consoleTimeStamp( + jsi::Runtime& runtime, + const jsi::Value* arguments, + size_t argumentsCount) { + auto& performanceTracer = tracing::PerformanceTracer::getInstance(); + if (!performanceTracer.isTracing() || argumentsCount == 0) { + // If not tracing, just early return to avoid the cost of parsing. + return; + } + + const jsi::Value& labelArgument = arguments[0]; + std::string label; + if (labelArgument.isString()) { + label = labelArgument.asString(runtime).utf8(runtime); + } else { + auto maybeStringifiedLabel = stringifyJsiValue(labelArgument, runtime); + if (maybeStringifiedLabel) { + label = std::move(*maybeStringifiedLabel); + } else { + // Do not record this entry: unable to reliably stringify the label. + return; + } + } + + auto now = HighResTimeStamp::now(); + std::optional start; + if (argumentsCount >= 2) { + const jsi::Value& startArgument = arguments[1]; + if (startArgument.isNumber()) { + start = + HighResTimeStamp::fromDOMHighResTimeStamp(startArgument.asNumber()); + } else if (startArgument.isString()) { + start = startArgument.asString(runtime).utf8(runtime); + } else if (startArgument.isUndefined()) { + start = now; + } + } + + std::optional end; + if (argumentsCount >= 3) { + const jsi::Value& endArgument = arguments[2]; + if (endArgument.isNumber()) { + end = HighResTimeStamp::fromDOMHighResTimeStamp(endArgument.asNumber()); + } else if (endArgument.isString()) { + end = endArgument.asString(runtime).utf8(runtime); + } else if (endArgument.isUndefined()) { + end = now; + } + } + + std::optional trackName; + std::optional trackGroup; + std::optional color; + if (argumentsCount >= 4) { + const jsi::Value& trackNameArgument = arguments[3]; + if (trackNameArgument.isString()) { + trackName = trackNameArgument.asString(runtime).utf8(runtime); + } + } + if (argumentsCount >= 5) { + const jsi::Value& trackGroupArgument = arguments[4]; + if (trackGroupArgument.isString()) { + trackGroup = trackGroupArgument.asString(runtime).utf8(runtime); + } + } + if (argumentsCount >= 6) { + const jsi::Value& colorArgument = arguments[5]; + if (colorArgument.isString()) { + color = tracing::getConsoleTimeStampColorFromString( + colorArgument.asString(runtime).utf8(runtime)); + } + } + + performanceTracer.reportTimeStamp( + label, start, end, trackName, trackGroup, color); +} + +/* + * Installs console.timeStamp and manages the forwarding to the original console + * object, if available. + */ +void installConsoleTimeStamp( + jsi::Runtime& runtime, + std::shared_ptr originalConsole, + jsi::Object& consoleObject) { + consoleObject.setProperty( + runtime, + "timeStamp", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "timeStamp"), + 0, + forwardToOriginalConsole( + originalConsole, + "timeStamp", + [](jsi::Runtime& runtime, + const jsi::Value& /*thisVal*/, + const jsi::Value* args, + size_t count) { + consoleTimeStamp(runtime, args, count); + return jsi::Value::undefined(); + }))); +} + } // namespace void RuntimeTarget::installConsoleHandler() { @@ -470,6 +591,11 @@ void RuntimeTarget::installConsoleHandler() { */ installConsoleMethod("assert", consoleAssert); + /** + * console.timeStamp + */ + installConsoleTimeStamp(runtime, originalConsole, console); + // Install forwarding console methods. #define FORWARDING_CONSOLE_METHOD(name, type) \ installConsoleMethod(#name, console_##name); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/__docs__/consoleTimeStamp.md b/packages/react-native/ReactCommon/jsinspector-modern/__docs__/consoleTimeStamp.md new file mode 100644 index 00000000000..fbfea626894 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/__docs__/consoleTimeStamp.md @@ -0,0 +1,96 @@ +# console.timeStamp API + +[🏠 Home](../../../../../README.md) + +The console.timeStamp API provides a way to record markers on the timeline of +the Performance instrumentation. + +The idea is to have a highly performant API for instrumenting React Native +applications and surfacing the recorded timing data to performance tooling: +Performance panel or Perfetto. This API should be explicitly designed to have +minimal runtime overhead and expected to be used for instrumenting hot paths and +profiling (production) builds. + +See also: + +- https://developer.mozilla.org/en-US/docs/Web/API/console/timeStamp_static +- https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp + +## πŸš€ Usage + +The `console.timeStamp()` method can be called with various parameters to record +markers on the performance timeline: + +```javascript +// Basic usage with just a label +console.timeStamp('Click'); + +// Advanced usage with start and end markers +console.timeStamp('Animation', performance.now(), performance.now() + 500); + +// Full usage with all parameters +console.timeStamp( + 'Animation', // label + performance.now(), // start time + performance.now() + 500, // end time + 'UI Animations', // track name + 'Main Thread', // track group + 'primary', // color +); +``` + +More examples are available on +[Chrome DevTools website](https://developer.chrome.com/docs/devtools/performance/extension#consoletimestamp_api_examples). + +### βš™οΈ Parameters + +1. `label` (required): A string label for the timestamp marker +2. `start` (optional): A + [DOMHighResTimeStamp](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) + or string marker for the start time +3. `end` (optional): A + [DOMHighResTimeStamp](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) + or string marker for the end time +4. `trackName` (optional): A string specifying the track name +5. `trackGroup` (optional): A string specifying the track group +6. `color` (optional): A string specifying the color of the timestamp marker. + Supported values depend on Performance instrumentation, for React Native + DevTools, the supported values are listed in ConsoleTimeStamp.h. + +## πŸ“ Design + +The console.timeStamp API is a JSI Host Function that is installed as part of +the Console interface when JavaScript Runtime target is registered with the +jsinspector-modern stack. + +To achieve minimal runtime costs: + +- This API will not add or buffer Performance Entries that can be accessed or + observed with PerformanceObserver. +- This API won’t have any buffering implemented by design. The buffering may be + implemented by the performance tooling, if needed. +- This API will have limited validation and user feedback. All incorrect timing + entries are expected to be ignored. + +## 🩻 Visualization + +With Chrome DevTools Performance panel integration: + +- Marks (only `label` specified) are not reported to custom tracks. They are + only reported as `"TimeStamp: