diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index 04439593c96a0e..9777f930586eca 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -571,6 +571,11 @@ function consoleAssertPolyfill(expression, label) { function stub() {} +// https://developer.chrome.com/docs/devtools/console/api#createtask +function consoleCreateTaskStub() { + return {run: cb => cb()}; +} + if (global.nativeLoggingHook) { const originalConsole = global.console; // Preserve the original `console` as `originalConsole` @@ -587,6 +592,7 @@ if (global.nativeLoggingHook) { timeStamp: stub, count: stub, countReset: stub, + createTask: consoleCreateTaskStub, ...(originalConsole ?? {}), error: getNativeLogFunction(LOG_LEVELS.error), info: getNativeLogFunction(LOG_LEVELS.info), @@ -705,6 +711,7 @@ if (global.nativeLoggingHook) { time: stub, timeEnd: stub, timeStamp: stub, + createTask: consoleCreateTaskStub, }; Object.defineProperty(console, '_isPolyfilled', { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.cpp b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.cpp new file mode 100644 index 00000000000000..371d628f968997 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.cpp @@ -0,0 +1,27 @@ +/* + * 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 "ConsoleTask.h" +#include "ConsoleTaskOrchestrator.h" + +namespace facebook::react::jsinspector_modern { + +ConsoleTask::ConsoleTask(std::shared_ptr taskContext) + : taskContext_(std::move(taskContext)), + orchestrator_(ConsoleTaskOrchestrator::getInstance()) { + if (taskContext_) { + orchestrator_.startTask(taskContext_->id()); + } +} + +ConsoleTask::~ConsoleTask() { + if (taskContext_) { + orchestrator_.finishTask(taskContext_->id()); + } +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.h b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.h new file mode 100644 index 00000000000000..599396ca614f72 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTask.h @@ -0,0 +1,38 @@ +/* + * 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 + +namespace facebook::react::jsinspector_modern { + +class ConsoleTaskContext; +class RuntimeTargetDelegate; +class ConsoleTaskOrchestrator; + +class ConsoleTask { + public: + /** + * \param runtimeTargetDelegate The delegate to the corresponding runtime. + * \param taskContext The context that tracks the task. + */ + explicit ConsoleTask(std::shared_ptr taskContext); + ~ConsoleTask(); + + ConsoleTask(const ConsoleTask &) = default; + ConsoleTask &operator=(const ConsoleTask &) = delete; + + ConsoleTask(ConsoleTask &&) = default; + ConsoleTask &operator=(ConsoleTask &&) = delete; + + private: + std::shared_ptr taskContext_; + ConsoleTaskOrchestrator &orchestrator_; +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.cpp b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.cpp new file mode 100644 index 00000000000000..53ba29297836ba --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.cpp @@ -0,0 +1,57 @@ +/* + * 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 "ConsoleTaskContext.h" +#include "ConsoleTaskOrchestrator.h" +#include "RuntimeTarget.h" + +namespace facebook::react::jsinspector_modern { + +ConsoleTaskContext::ConsoleTaskContext( + jsi::Runtime& runtime, + RuntimeTargetDelegate& runtimeTargetDelegate, + std::string name) + : runtimeTargetDelegate_(runtimeTargetDelegate), + name_(std::move(name)), + orchestrator_(ConsoleTaskOrchestrator::getInstance()) { + stackTrace_ = runtimeTargetDelegate_.captureStackTrace(runtime); +} + +ConsoleTaskContext::~ConsoleTaskContext() { + orchestrator_.cancelTask(id()); +} + +ConsoleTaskId ConsoleTaskContext::id() const { + return ConsoleTaskId{(void*)this}; +} + +std::optional ConsoleTaskContext::getSerializedStackTrace() + const { + auto maybeValue = runtimeTargetDelegate_.serializeStackTrace(*stackTrace_); + if (maybeValue) { + maybeValue.value()["description"] = name_; + } + + return maybeValue; +} + +std::function()> +ConsoleTaskContext::getSerializedStackTraceProvider() const { + return [selfWeak = weak_from_this()]() -> std::optional { + if (auto self = selfWeak.lock()) { + return self->getSerializedStackTrace(); + } + + return std::nullopt; + }; +} + +void ConsoleTaskContext::schedule() { + orchestrator_.scheduleTask(id(), weak_from_this()); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.h b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.h new file mode 100644 index 00000000000000..e651168661bc37 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskContext.h @@ -0,0 +1,107 @@ +/* + * 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 "StackTrace.h" + +#include +#include + +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +class ConsoleTaskOrchestrator; +class RuntimeTargetDelegate; + +class ConsoleTaskId { + public: + ConsoleTaskId() = default; + ~ConsoleTaskId() = default; + + ConsoleTaskId(const ConsoleTaskId &) = default; + ConsoleTaskId &operator=(const ConsoleTaskId &) = default; + + ConsoleTaskId(ConsoleTaskId &&) = default; + ConsoleTaskId &operator=(ConsoleTaskId &&) = default; + + bool operator==(const ConsoleTaskId &) const = default; + inline operator bool() const + { + return (bool)id_; + } + + explicit inline operator void *() const + { + return id_; + } + + private: + explicit inline ConsoleTaskId(void *id) : id_(id) + { + assert(id_ != nullptr); + } + + void *id_{nullptr}; + + friend class ConsoleTaskContext; +}; + +class ConsoleTaskContext : public std::enable_shared_from_this { + public: + ConsoleTaskContext(jsi::Runtime &runtime, RuntimeTargetDelegate &runtimeTargetDelegate, std::string name); + ~ConsoleTaskContext(); + + // Can't be moved or copied: the address of `ConsoleTaskContext` is used to + // identify this task and all corresponding invocations. + ConsoleTaskContext(const ConsoleTaskContext &) = delete; + ConsoleTaskContext &operator=(const ConsoleTaskContext &) = delete; + + ConsoleTaskContext(ConsoleTaskContext &&) = delete; + ConsoleTaskContext &operator=(ConsoleTaskContext &&) = delete; + + /** + * Unique identifier that is calculated based on the address of + * ConsoleTaskContext. + */ + ConsoleTaskId id() const; + + /** + * Returns the serialized stack trace that was captured during the allocation + * of ConsoleTaskContext. + */ + std::optional getSerializedStackTrace() const; + + /** + * Returns a function that returns the serialized stack trace, if available. + */ + std::function()> getSerializedStackTraceProvider() const; + + void schedule(); + + private: + RuntimeTargetDelegate &runtimeTargetDelegate_; + std::string name_; + ConsoleTaskOrchestrator &orchestrator_; + std::unique_ptr stackTrace_; +}; + +} // namespace facebook::react::jsinspector_modern + +namespace std { +template <> +struct hash { + size_t operator()(const facebook::react::jsinspector_modern::ConsoleTaskId &id) const + { + return std::hash{}(static_cast(id)); + } +}; +} // namespace std diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.cpp b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.cpp new file mode 100644 index 00000000000000..17baa16a19ea81 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.cpp @@ -0,0 +1,55 @@ +/* + * 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 "ConsoleTaskOrchestrator.h" + +namespace facebook::react::jsinspector_modern { + +/* static */ ConsoleTaskOrchestrator& ConsoleTaskOrchestrator::getInstance() { + static ConsoleTaskOrchestrator instance; + return instance; +} + +void ConsoleTaskOrchestrator::scheduleTask( + ConsoleTaskId taskId, + std::weak_ptr taskContext) { + std::lock_guard lock(mutex_); + tasks_.emplace(taskId, taskContext); +} + +void ConsoleTaskOrchestrator::cancelTask(ConsoleTaskId id) { + std::lock_guard lock(mutex_); + tasks_.erase(id); +} + +void ConsoleTaskOrchestrator::startTask(ConsoleTaskId id) { + std::lock_guard lock(mutex_); + stack_.push(id); +} + +void ConsoleTaskOrchestrator::finishTask(ConsoleTaskId id) { + std::lock_guard lock(mutex_); + assert(stack_.top() == id); + + stack_.pop(); +} + +std::shared_ptr ConsoleTaskOrchestrator::top() const { + std::lock_guard lock(mutex_); + if (stack_.empty()) { + return nullptr; + } + + auto it = tasks_.find(stack_.top()); + if (it == tasks_.end()) { + return nullptr; + } + + return it->second.lock(); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.h b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.h new file mode 100644 index 00000000000000..79d645baeda983 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/ConsoleTaskOrchestrator.h @@ -0,0 +1,48 @@ +/* + * 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 +#include +#include + +#include "ConsoleTaskContext.h" + +namespace facebook::react::jsinspector_modern { + +class ConsoleTaskOrchestrator { + public: + static ConsoleTaskOrchestrator &getInstance(); + + ~ConsoleTaskOrchestrator() = default; + + ConsoleTaskOrchestrator(const ConsoleTaskOrchestrator &) = delete; + ConsoleTaskOrchestrator &operator=(const ConsoleTaskOrchestrator &) = delete; + + ConsoleTaskOrchestrator(ConsoleTaskOrchestrator &&) = delete; + ConsoleTaskOrchestrator &operator=(ConsoleTaskOrchestrator &&) = delete; + + void scheduleTask(ConsoleTaskId taskId, std::weak_ptr taskContext); + void cancelTask(ConsoleTaskId taskId); + + void startTask(ConsoleTaskId taskId); + void finishTask(ConsoleTaskId taskId); + std::shared_ptr top() const; + + private: + ConsoleTaskOrchestrator() = default; + + std::stack stack_; + std::unordered_map> tasks_; + /** + * Protects the stack_ and tasks_ members. + */ + mutable std::mutex mutex_; +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h index 7e0b62b442e7ff..74d5c935a5716b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h @@ -99,7 +99,7 @@ class RuntimeAgent final { * Lifetime of this agent is bound to the lifetime of the Tracing session - * HostTargetTraceRecording and to the lifetime of the RuntimeTarget. */ -class RuntimeTracingAgent : tracing::TargetTracingAgent { +class RuntimeTracingAgent : public tracing::TargetTracingAgent { public: explicit RuntimeTracingAgent(tracing::TraceRecordingState &state, RuntimeTargetController &targetController); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 0f84ecd0e17987..2fe2f51552bcf2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -248,6 +248,18 @@ bool RuntimeTarget::isDomainEnabled(Domain domain) const { return threadSafeDomainStatus_[domain]; } +bool RuntimeTarget::isConsoleCreateTaskEnabled() const { + if (isDomainEnabled(Domain::Runtime)) { + return true; + } + + if (auto tracingAgent = tracingAgent_.lock()) { + return tracingAgent->isRunningInBackgroundMode(); + } + + return false; +} + RuntimeTargetController::RuntimeTargetController(RuntimeTarget& target) : target_(target) {} diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h index 236cf83e7de381..7312baac0dbc3e 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h @@ -207,19 +207,6 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis createTracingAgent(tracing::TraceRecordingState &state); - /** - * Start sampling profiler for a particular JavaScript runtime. - */ - void enableSamplingProfiler(); - /** - * Stop sampling profiler for a particular JavaScript runtime. - */ - void disableSamplingProfiler(); - /** - * Return recorded sampling profile for the previous sampling session. - */ - tracing::RuntimeSamplingProfile collectSamplingProfile(); - private: using Domain = RuntimeTargetController::Domain; @@ -275,6 +262,18 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis tracingAgent_; + /** + * Start sampling profiler for a particular JavaScript runtime. + */ + void enableSamplingProfiler(); + /** + * Stop sampling profiler for a particular JavaScript runtime. + */ + void disableSamplingProfiler(); + /** + * Return recorded sampling profile for the previous sampling session. + */ + tracing::RuntimeSamplingProfile collectSamplingProfile(); /** * Adds a function with the given name on the runtime's global object, that @@ -293,6 +292,10 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis #include @@ -477,8 +481,17 @@ void consoleTimeStamp( } if (performanceTracer.isTracing()) { + auto taskContext = ConsoleTaskOrchestrator::getInstance().top(); + performanceTracer.reportTimeStamp( - label, start, end, trackName, trackGroup, color, std::move(detail)); + label, + start, + end, + trackName, + trackGroup, + color, + std::move(detail), + taskContext ? taskContext->getSerializedStackTraceProvider() : nullptr); } if (ReactPerfettoLogger::isTracing()) { @@ -521,6 +534,71 @@ void installConsoleTimeStamp( }))); } +/** + * run method of the task object returned from console.createTask(). + */ +jsi::Value consoleTaskRun( + jsi::Runtime& runtime, + const jsi::Value* args, + size_t count, + std::shared_ptr taskContext) { + if (count < 1 || !args[0].isObject()) { + throw JSError(runtime, "First argument must be a function"); + } + auto fnObj = args[0].getObject(runtime); + if (!fnObj.isFunction(runtime)) { + throw JSError(runtime, "First argument must be a function"); + } + + ConsoleTask consoleTask{taskContext}; + + auto fn = fnObj.getFunction(runtime); + return fn.call(runtime); +} + +/** + * console.createTask. Non-standardized. + * https://developer.chrome.com/docs/devtools/console/api#createtask + */ +jsi::Value consoleCreateTask( + jsi::Runtime& runtime, + const jsi::Value* args, + size_t count, + RuntimeTargetDelegate& runtimeTargetDelegate, + bool enabled) { + if (count < 1 || !args[0].isString()) { + throw JSError(runtime, "First argument must be a non-empty string"); + } + auto name = args[0].asString(runtime).utf8(runtime); + if (name.empty()) { + throw JSError(runtime, "First argument must be a non-empty string"); + } + + jsi::Object task{runtime}; + std::shared_ptr taskContext = nullptr; + if (enabled) { + taskContext = std::make_shared( + runtime, runtimeTargetDelegate, name); + taskContext->schedule(); + } + + task.setProperty( + runtime, + "run", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "run"), + 0, + [taskContext]( + jsi::Runtime& runtime, + const jsi::Value& /*thisVal*/, + const jsi::Value* args, + size_t count) { + return consoleTaskRun(runtime, args, count, taskContext); + })); + return task; +} + } // namespace void RuntimeTarget::installConsoleHandler() { @@ -624,6 +702,33 @@ void RuntimeTarget::installConsoleHandler() { */ installConsoleTimeStamp(runtime, originalConsole, console); + /** + * console.createTask + */ + console.setProperty( + runtime, + "createTask", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "createTask"), + 0, + [state, selfWeak]( + jsi::Runtime& runtime, + const jsi::Value& /*thisVal*/, + const jsi::Value* args, + size_t count) { + jsi::Value task; + tryExecuteSync(selfWeak, [&](auto& self) { + task = consoleCreateTask( + runtime, + args, + count, + self.delegate_, + self.isConsoleCreateTaskEnabled()); + }); + return task; + })); + // Install forwarding console methods. #define FORWARDING_CONSOLE_METHOD(name, type) \ installConsoleMethod(#name, console_##name); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/ConsoleCreateTaskTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/ConsoleCreateTaskTest.cpp new file mode 100644 index 00000000000000..79fbf844a67d3c --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/ConsoleCreateTaskTest.cpp @@ -0,0 +1,131 @@ +/* + * 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 + +#include "JsiIntegrationTest.h" +#include "engines/JsiIntegrationTestHermesEngineAdapter.h" + +#include + +using namespace ::testing; + +namespace facebook::react::jsinspector_modern { + +/** + * A test fixture for the console.createTask API. + */ +class ConsoleCreateTaskTest : public JsiIntegrationPortableTestBase< + JsiIntegrationTestHermesEngineAdapter, + folly::QueuedImmediateExecutor> {}; + +TEST_F(ConsoleCreateTaskTest, Installed) { + auto result = eval("typeof console.createTask"); + auto& runtime = engineAdapter_->getRuntime(); + EXPECT_EQ(result.asString(runtime).utf8(runtime), "function"); +} + +TEST_F(ConsoleCreateTaskTest, ReturnsTaskObject) { + auto result = eval("typeof console.createTask('test-task')"); + auto& runtime = engineAdapter_->getRuntime(); + EXPECT_EQ(result.asString(runtime).utf8(runtime), "object"); +} + +TEST_F(ConsoleCreateTaskTest, TaskObjectHasRunMethod) { + auto result = eval("typeof console.createTask('test-task').run"); + auto& runtime = engineAdapter_->getRuntime(); + EXPECT_EQ(result.asString(runtime).utf8(runtime), "function"); +} + +TEST_F(ConsoleCreateTaskTest, RunMethodExecutesFunction) { + auto result = eval(R"( + let executed = false; + const task = console.createTask('test-task'); + task.run(() => { executed = true; }); + executed; + )"); + EXPECT_TRUE(result.getBool()); +} + +TEST_F(ConsoleCreateTaskTest, RunMethodReturnsValue) { + auto result = eval(R"( + const task = console.createTask('test-task'); + task.run(() => 42); + )"); + EXPECT_EQ(result.getNumber(), 42); +} + +TEST_F(ConsoleCreateTaskTest, ThrowsOnNoArguments) { + EXPECT_THROW(eval("console.createTask()"), facebook::jsi::JSError); +} + +TEST_F(ConsoleCreateTaskTest, ThrowsOnEmptyString) { + EXPECT_THROW(eval("console.createTask('')"), facebook::jsi::JSError); +} + +TEST_F(ConsoleCreateTaskTest, ThrowsOnNonStringArgument) { + EXPECT_THROW(eval("console.createTask(123)"), facebook::jsi::JSError); + EXPECT_THROW(eval("console.createTask(null)"), facebook::jsi::JSError); + EXPECT_THROW(eval("console.createTask(undefined)"), facebook::jsi::JSError); + EXPECT_THROW(eval("console.createTask({})"), facebook::jsi::JSError); +} + +TEST_F(ConsoleCreateTaskTest, RunMethodThrowsOnNoArguments) { + EXPECT_THROW( + eval(R"( + const task = console.createTask('test-task'); + task.run(); + )"), + facebook::jsi::JSError); +} + +TEST_F(ConsoleCreateTaskTest, RunMethodThrowsOnNonFunction) { + EXPECT_THROW( + eval(R"( + const task = console.createTask('test-task'); + task.run(123); + )"), + facebook::jsi::JSError); + EXPECT_THROW( + eval(R"( + const task = console.createTask('test-task'); + task.run('not a function'); + )"), + facebook::jsi::JSError); + EXPECT_THROW( + eval(R"( + const task = console.createTask('test-task'); + task.run({}); + )"), + facebook::jsi::JSError); +} + +TEST_F(ConsoleCreateTaskTest, MultipleTasksCanBeCreated) { + auto result = eval(R"( + const task1 = console.createTask('task-1'); + const task2 = console.createTask('task-2'); + let count = 0; + task1.run(() => { count++; }); + task2.run(() => { count++; }); + count; + )"); + EXPECT_EQ(result.getNumber(), 2); +} + +TEST_F(ConsoleCreateTaskTest, TaskCanBeRunMultipleTimes) { + auto result = eval(R"( + const task = console.createTask('test-task'); + let count = 0; + task.run(() => { count++; }); + task.run(() => { count++; }); + task.run(() => { count++; }); + count; + )"); + EXPECT_EQ(result.getNumber(), 3); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp index d596decb121ac4..cf4b443df9bb89 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp @@ -187,7 +187,8 @@ void PerformanceTracer::reportMeasure( const std::string& name, HighResTimeStamp start, HighResDuration duration, - folly::dynamic&& detail) { + folly::dynamic&& detail, + std::function()>&& stackTraceProvider) { if (!tracingAtomic_) { return; } @@ -204,6 +205,7 @@ void PerformanceTracer::reportMeasure( .duration = duration, .detail = std::move(detail), .threadId = getCurrentThreadId(), + .stackTraceProvider = std::move(stackTraceProvider), }); } @@ -214,7 +216,8 @@ void PerformanceTracer::reportTimeStamp( std::optional trackName, std::optional trackGroup, std::optional color, - std::optional detail) { + std::optional detail, + std::function()>&& stackTraceProvider) { if (!tracingAtomic_) { return; } @@ -233,6 +236,7 @@ void PerformanceTracer::reportTimeStamp( .trackGroup = std::move(trackGroup), .color = std::move(color), .detail = std::move(detail), + .stackTraceProvider = std::move(stackTraceProvider), .threadId = getCurrentThreadId(), }); } @@ -606,6 +610,12 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( beginEventArgs = folly::dynamic::object("detail", folly::toJson(event.detail)); } + if (event.stackTraceProvider) { + if (auto maybeStackTrace = event.stackTraceProvider()) { + beginEventArgs["data"] = folly::dynamic::object( + "rnStackTrace", std::move(*maybeStackTrace)); + } + } auto eventId = ++performanceMeasureCount_; @@ -695,6 +705,11 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( } data["devtools"] = folly::toJson(devtoolsDetail); } + if (event.stackTraceProvider) { + if (auto maybeStackTrace = event.stackTraceProvider()) { + data["rnStackTrace"] = std::move(*maybeStackTrace); + } + } events.emplace_back( TraceEvent{ diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h index f763cc8597baaf..96bf715ab3f167 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h @@ -81,7 +81,8 @@ class PerformanceTracer { const std::string &name, HighResTimeStamp start, HighResDuration duration, - folly::dynamic &&detail = nullptr); + folly::dynamic &&detail = nullptr, + std::function()> &&stackTraceProvider = nullptr); /** * Record a "TimeStamp" Trace Event - a labelled entry on Performance @@ -97,7 +98,8 @@ class PerformanceTracer { std::optional trackName = std::nullopt, std::optional trackGroup = std::nullopt, std::optional color = std::nullopt, - std::optional detail = std::nullopt); + std::optional detail = std::nullopt, + std::function()> &&stackTraceProvider = nullptr); /** * Record an Event Loop tick, which will be represented as an Event Loop task @@ -256,6 +258,7 @@ class PerformanceTracer { HighResDuration duration; folly::dynamic detail; ThreadId threadId; + std::function()> stackTraceProvider; HighResTimeStamp createdAt = HighResTimeStamp::now(); }; @@ -267,6 +270,7 @@ class PerformanceTracer { std::optional trackGroup; std::optional color; std::optional detail; + std::function()> stackTraceProvider; ThreadId threadId; HighResTimeStamp createdAt = HighResTimeStamp::now(); }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TargetTracingAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TargetTracingAgent.h index f92ffe7b263156..a1a1260c649437 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TargetTracingAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TargetTracingAgent.h @@ -27,6 +27,11 @@ class TargetTracingAgent { (void)state_; } + bool isRunningInBackgroundMode() + { + return state_.mode == tracing::Mode::Background; + } + protected: TraceRecordingState &state_; }; diff --git a/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt b/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt index 742f6f9fefe3e4..38475e30766027 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/performance/timeline/CMakeLists.txt @@ -16,6 +16,7 @@ target_compile_options(react_performance_timeline PRIVATE -Wpedantic) target_include_directories(react_performance_timeline PUBLIC ${REACT_COMMON_DIR}) target_link_libraries(react_performance_timeline + jsinspector jsinspector_tracing reactperflogger react_featureflags diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp index 17b3bbecd5a270..ed33d46d69d69c 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp @@ -7,6 +7,7 @@ #include "PerformanceEntryReporter.h" +#include #include #include #include @@ -381,8 +382,16 @@ void PerformanceEntryReporter::traceMeasure( } if (performanceTracer.isTracing()) { + auto taskContext = + jsinspector_modern::ConsoleTaskOrchestrator::getInstance().top(); + performanceTracer.reportMeasure( - entry.name, entry.startTime, entry.duration, std::move(detail)); + entry.name, + entry.startTime, + entry.duration, + std::move(detail), + taskContext ? taskContext->getSerializedStackTraceProvider() + : nullptr); } } } diff --git a/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec b/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec index 6e190bbfe6d9c6..31c88d97abe951 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec +++ b/packages/react-native/ReactCommon/react/performance/timeline/React-performancetimeline.podspec @@ -41,6 +41,7 @@ Pod::Spec.new do |s| resolve_use_frameworks(s, header_mappings_dir: "../../..", module_name: "React_performancetimeline") s.dependency "React-featureflags" + add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern') add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing') s.dependency "React-timing" s.dependency "React-perflogger" diff --git a/packages/react-native/flow/bom.js.flow b/packages/react-native/flow/bom.js.flow index 896253932a4777..fe78011fa4cb1b 100644 --- a/packages/react-native/flow/bom.js.flow +++ b/packages/react-native/flow/bom.js.flow @@ -25,6 +25,10 @@ type DevToolsColor = | 'warning' | 'error'; +declare interface ConsoleTask { + run(f: () => T): T; +} + // $FlowExpectedError[libdef-override] Flow core definitions are incomplete. declare var console: { // Logging @@ -75,6 +79,9 @@ declare var console: { detail?: {[string]: mixed}, ): void, + // Stack tagging + createTask(label: string): ConsoleTask, + ... }; diff --git a/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-benchmark-itest.js b/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-benchmark-itest.js new file mode 100644 index 00000000000000..7ba03b1f42c744 --- /dev/null +++ b/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-benchmark-itest.js @@ -0,0 +1,35 @@ +/** + * 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. + * + * @flow strict-local + * @format + * @fantom_mode dev + */ + +/** + * We force the DEV mode, because Fusebox infra is not installed in production builds. + * We want to benchmark the implementation, not the polyfill. + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; + +const fn = () => {}; + +Fantom.unstable_benchmark + .suite('console.createTask', { + minIterations: 50000, + disableOptimizedBuildCheck: true, + }) + .test('JavaScript shim', () => { + const task: ConsoleTask = {run: cb => cb()}; + task.run(fn); + }) + .test('implementation', () => { + const task = console.createTask('task'); + task.run(fn); + }); diff --git a/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-jsx-benchmark-itest.js b/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-jsx-benchmark-itest.js new file mode 100644 index 00000000000000..a840438c2b25c5 --- /dev/null +++ b/packages/react-native/src/private/webapis/console/__tests__/consoleCreateTask-jsx-benchmark-itest.js @@ -0,0 +1,79 @@ +/** + * 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. + * + * @flow strict-local + * @format + * @fantom_mode dev + */ + +/** + * We force the DEV mode, because React only uses console.createTask in DEV builds. + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; +import {View} from 'react-native'; + +let root; + +function Node(props: {depth: number}): React.Node { + if (props.depth === 500) { + return ; + } + + return ( + + + + ); +} +function Root(props: {prop: boolean}): React.Node { + return ; +} + +Fantom.unstable_benchmark + .suite( + `console.createTask ${typeof console.createTask === 'function' ? 'installed' : 'removed'}`, + { + minIterations: 100, + disableOptimizedBuildCheck: true, + }, + ) + .test( + 'Rendering 1000 views', + () => { + let recursiveViews: React.MixedElement; + for (let i = 0; i < 1000; ++i) { + recursiveViews = {recursiveViews}; + } + + Fantom.runTask(() => root.render(recursiveViews)); + }, + { + beforeEach: () => { + root = Fantom.createRoot(); + }, + afterEach: () => { + root.destroy(); + }, + }, + ) + .test( + 'Updating a subtree of 500 nodes', + () => { + Fantom.runTask(() => root.render()); + }, + { + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => root.render()); + }, + afterEach: () => { + root.destroy(); + }, + }, + ); diff --git a/packages/react-native/src/private/webapis/console/__tests__/no-consoleCreateTask-jsx-benchmark-itest.js b/packages/react-native/src/private/webapis/console/__tests__/no-consoleCreateTask-jsx-benchmark-itest.js new file mode 100644 index 00000000000000..eb5f7a7db1dc00 --- /dev/null +++ b/packages/react-native/src/private/webapis/console/__tests__/no-consoleCreateTask-jsx-benchmark-itest.js @@ -0,0 +1,19 @@ +/** + * 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. + * + * @flow strict-local + * @format + * @fantom_mode dev + */ + +/** + * We force the DEV mode, because React only uses console.createTask in DEV builds. + */ + +//$FlowExpectedError[cannot-write] +delete console.createTask; + +require('./consoleCreateTask-jsx-benchmark-itest.js');