Re-land Implement console.timeStamp

Summary:
# Changelog: [Internal]

Adds support for experimental non-standardized `console.timeStamp` API for capturing performance entries on a timeline. The main idea of the API is to be highly performant. More details in the corresponding RCP [1].

NOTE: Because of the `jsinspector-modern` stack gating logic, this won't be installed in production builds. `console.timeStamp` will be polyfilled with a stub - D76987507.

Reviewed By: rubennorte

Differential Revision: D77374707

fbshipit-source-id: cb66b9fda06168f4b13af764afe95a63a0a8d5a0
This commit is contained in:
Ruslan Lesiutin
2025-06-26 09:57:32 -07:00
committed by Facebook GitHub Bot
parent 0386b9bd51
commit ff97ca3134
6 changed files with 530 additions and 0 deletions
@@ -6,6 +6,7 @@
*/
#include <jsinspector-modern/RuntimeTarget.h>
#include <jsinspector-modern/tracing/PerformanceTracer.h>
#include <concepts>
#include <deque>
@@ -324,6 +325,16 @@ void consoleAssert(
#include "ForwardingConsoleMethods.def"
#undef FORWARDING_CONSOLE_METHOD
/*
* Attempt to call String() on the given value.
*/
std::optional<std::string> 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<tracing::ConsoleTimeStampEntry> 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<tracing::ConsoleTimeStampEntry> 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<std::string> trackName;
std::optional<std::string> trackGroup;
std::optional<tracing::ConsoleTimeStampColor> 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<jsi::Object> 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);
@@ -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 wont 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: <label>"` entries on the Timings track.
- Measures (both start and end are defined) are reported to custom tracks, if
specified.
## 🔗 Relationship with other systems
### Part of
- jsinspector-modern: The JavaScript debugger stack for React Native DevTools,
which controls the lifetime of the Console interface for the targeted
JavaScript Runtime.
### Used by this
- Performance Tracing System: The console.timeStamp API reports markers to the
PerformanceTracer singleton, which is a Trace Event engine responsible for
collection, storage and serialization of Performance timeline events.
@@ -0,0 +1,146 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <folly/executors/QueuedImmediateExecutor.h>
#include "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
#include <jsinspector-modern/tracing/PerformanceTracer.h>
using namespace ::testing;
namespace facebook::react::jsinspector_modern {
/**
* A test fixture for the console.timeStamp API.
*/
class ConsoleTimeStampTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor> {
protected:
size_t countNumberOfTimeStampEvents(
tracing::PerformanceTracer& performanceTracer) {
size_t count = 0;
performanceTracer.collectEvents(
[&count](const folly::dynamic& chunk) {
auto event = chunk[0];
if (event["name"] == "TimeStamp") {
count++;
}
},
1);
return count;
}
};
TEST_F(ConsoleTimeStampTest, Installed) {
auto result = eval("typeof console.timeStamp");
auto& runtime = engineAdapter_->getRuntime();
EXPECT_EQ(result.asString(runtime).utf8(runtime), "function");
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithJustLabel) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-label')");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithSpecifiedTimestamps) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test-range', 100, 200)");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, RecordsEntriesWithMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, KeepsEntriesWithUnknownMarkNames) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval(
"console.timeStamp('test-string-timestamps', 'start-marker', 'end-marker')");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, DoesNotThrowIfNotTracing) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_FALSE(tracer.stopTracing());
// Call console.timeStamp - should be a no-op when not tracing
eval("console.timeStamp('test-no-tracing')");
}
TEST_F(ConsoleTimeStampTest, SurvivesInvalidArguments) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
// Won't be logged, no label specified.
eval("console.timeStamp()");
// Will be logged - undefined will be stringified.
eval("console.timeStamp(undefined)");
// Will be logged - function will be stringified.
eval("console.timeStamp(() => {})");
// Will be logged - 123 will be stringified.
eval("console.timeStamp(123)");
// Will be logged - start will default to an empty string.
eval("console.timeStamp('label', {})");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 4);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackNameIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, {})");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidTrackGroupIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('label', 0, 1, 'trackName', {})");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
TEST_F(ConsoleTimeStampTest, InvalidColorIsIgnored) {
auto& tracer = tracing::PerformanceTracer::getInstance();
EXPECT_TRUE(tracer.startTracing());
eval("console.timeStamp('test', 100, 200, 'FooTrack', 'BarGroup', 'red')");
EXPECT_TRUE(tracer.stopTracing());
EXPECT_EQ(countNumberOfTimeStampEvents(tracer), 1);
}
} // namespace facebook::react::jsinspector_modern
@@ -0,0 +1,91 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/timing/primitives.h>
#include <cassert>
#include <optional>
#include <stdexcept>
#include <string>
#include <variant>
namespace facebook::react::jsinspector_modern::tracing {
// https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp
using ConsoleTimeStampEntry = std::variant<HighResTimeStamp, std::string>;
// https://developer.chrome.com/docs/devtools/performance/extension#devtools_object
enum class ConsoleTimeStampColor {
Primary,
PrimaryLight,
PrimaryDark,
Secondary,
SecondaryLight,
SecondaryDark,
Tertiary,
TertiaryLight,
TertiaryDark,
Error,
};
inline std::string consoleTimeStampColorToString(ConsoleTimeStampColor color) {
switch (color) {
case ConsoleTimeStampColor::Primary:
return "primary";
case ConsoleTimeStampColor::PrimaryLight:
return "primary-light";
case ConsoleTimeStampColor::PrimaryDark:
return "primary-dark";
case ConsoleTimeStampColor::Secondary:
return "secondary";
case ConsoleTimeStampColor::SecondaryLight:
return "secondary-light";
case ConsoleTimeStampColor::SecondaryDark:
return "secondary-dark";
case ConsoleTimeStampColor::Tertiary:
return "tertiary";
case ConsoleTimeStampColor::TertiaryLight:
return "tertiary-light";
case ConsoleTimeStampColor::TertiaryDark:
return "tertiary-dark";
case ConsoleTimeStampColor::Error:
return "error";
default:
throw std::runtime_error("Unknown ConsoleTimeStampColor");
}
};
inline std::optional<ConsoleTimeStampColor> getConsoleTimeStampColorFromString(
const std::string& str) {
if (str == "primary") {
return ConsoleTimeStampColor::Primary;
} else if (str == "primary-light") {
return ConsoleTimeStampColor::PrimaryLight;
} else if (str == "primary-dark") {
return ConsoleTimeStampColor::PrimaryDark;
} else if (str == "secondary") {
return ConsoleTimeStampColor::Secondary;
} else if (str == "secondary-light") {
return ConsoleTimeStampColor::SecondaryLight;
} else if (str == "secondary-dark") {
return ConsoleTimeStampColor::SecondaryDark;
} else if (str == "tertiary") {
return ConsoleTimeStampColor::Tertiary;
} else if (str == "tertiary-light") {
return ConsoleTimeStampColor::TertiaryLight;
} else if (str == "tertiary-dark") {
return ConsoleTimeStampColor::TertiaryDark;
} else if (str == "error") {
return ConsoleTimeStampColor::Error;
} else {
return std::nullopt;
}
};
}; // namespace facebook::react::jsinspector_modern::tracing
@@ -180,6 +180,61 @@ void PerformanceTracer::reportMeasure(
});
}
void PerformanceTracer::reportTimeStamp(
std::string name,
std::optional<ConsoleTimeStampEntry> start,
std::optional<ConsoleTimeStampEntry> end,
std::optional<std::string> trackName,
std::optional<std::string> trackGroup,
std::optional<ConsoleTimeStampColor> color) {
if (!tracingAtomic_) {
return;
}
// `name` takes precedence over `message` in Chrome DevTools Frontend, no need
// to record both.
folly::dynamic data = folly::dynamic::object("name", std::move(name));
if (start) {
if (std::holds_alternative<HighResTimeStamp>(*start)) {
data["start"] = highResTimeStampToTracingClockTimeStamp(
std::get<HighResTimeStamp>(*start));
} else {
data["start"] = std::move(std::get<std::string>(*start));
}
}
if (end) {
if (std::holds_alternative<HighResTimeStamp>(*end)) {
data["end"] = highResTimeStampToTracingClockTimeStamp(
std::get<HighResTimeStamp>(*end));
} else {
data["end"] = std::move(std::get<std::string>(*end));
}
}
if (trackName) {
data["track"] = std::move(*trackName);
}
if (trackGroup) {
data["trackGroup"] = std::move(*trackGroup);
}
if (color) {
data["color"] = consoleTimeStampColorToString(*color);
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
buffer_.emplace_back(TraceEvent{
.name = "TimeStamp",
.cat = "devtools.timeline",
.ph = 'I',
.ts = HighResTimeStamp::now(),
.pid = processId_,
.tid = oscompat::getCurrentThreadId(),
.args = folly::dynamic::object("data", std::move(data)),
});
}
void PerformanceTracer::reportProcess(uint64_t id, const std::string& name) {
if (!tracingAtomic_) {
return;
@@ -8,6 +8,7 @@
#pragma once
#include "CdpTracing.h"
#include "ConsoleTimeStamp.h"
#include "TraceEvent.h"
#include "TraceEventProfile.h"
@@ -79,6 +80,21 @@ class PerformanceTracer {
HighResDuration duration,
const std::optional<DevToolsTrackEntryPayload>& trackMetadata);
/**
* Record a "TimeStamp" Trace Event - a labelled entry on Performance
* timeline. The only required argument is `name`. Optional arguments, if not
* provided, won't be recorded in the serialized Trace Event.
* @see
https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp
*/
void reportTimeStamp(
std::string name,
std::optional<ConsoleTimeStampEntry> start = std::nullopt,
std::optional<ConsoleTimeStampEntry> end = std::nullopt,
std::optional<std::string> trackName = std::nullopt,
std::optional<std::string> trackGroup = std::nullopt,
std::optional<ConsoleTimeStampColor> color = std::nullopt);
/**
* Record a corresponding Trace Event for OS-level process.
*/