mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement native console object in RuntimeTarget (#43456)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43456 Changelog: [Internal] Implements the [WHATWG `console` spec](https://console.spec.whatwg.org/) directly in `RuntimeTarget`, based on the Hermes-powered `addConsoleMessage` method first used in D54494298. Benefits: * This allows the console API to work independently of the polyfill shipped in RN, including very early during JS execution. * It also opens the door to better stack traces (once we start reporting those) and richer functionality in the `console` object itself. Reviewed By: robhogan Differential Revision: D54826073 fbshipit-source-id: d5b0bd004bf35c2fce91742ae84ea86225ec1c61
This commit is contained in:
committed by
Facebook GitHub Bot
parent
eca4d76d81
commit
305c2811cf
+3
@@ -125,6 +125,9 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
|
||||
case ConsoleAPIType::kTimeEnd:
|
||||
type = HermesConsoleAPIType::kTimeEnd;
|
||||
break;
|
||||
case ConsoleAPIType::kCount:
|
||||
type = HermesConsoleAPIType::kCount;
|
||||
break;
|
||||
default:
|
||||
throw std::logic_error{"Unknown console message type"};
|
||||
}
|
||||
|
||||
@@ -34,77 +34,10 @@ RuntimeTarget::RuntimeTarget(
|
||||
jsExecutor_(jsExecutor) {}
|
||||
|
||||
void RuntimeTarget::installGlobals() {
|
||||
// NOTE: RuntimeTarget::installConsoleHandler is in RuntimeTargetConsole.cpp
|
||||
installConsoleHandler();
|
||||
}
|
||||
|
||||
void RuntimeTarget::installConsoleHandler() {
|
||||
jsExecutor_([selfWeak = weak_from_this(),
|
||||
selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
|
||||
// TODO(moti): Switch from implementing __inspectorLog to directly
|
||||
// installing a `console` object.
|
||||
runtime.global().setProperty(
|
||||
runtime,
|
||||
"__inspectorLog",
|
||||
jsi::Function::createFromHostFunction(
|
||||
runtime,
|
||||
jsi::PropNameID::forAscii(runtime, "__inspectorLog"),
|
||||
4,
|
||||
[selfWeak, selfExecutor](
|
||||
jsi::Runtime& rt,
|
||||
const jsi::Value& /*thisVal*/,
|
||||
const jsi::Value* args,
|
||||
size_t count) {
|
||||
if (count < 4) {
|
||||
throw jsi::JSError(
|
||||
rt,
|
||||
"__inspectorLog requires at least 4 arguments: logLevel, str, args, framesToSkip");
|
||||
}
|
||||
std::chrono::time_point<std::chrono::system_clock> timestamp =
|
||||
std::chrono::system_clock::now();
|
||||
std::string level = args[0].asString(rt).utf8(rt);
|
||||
ConsoleAPIType type = ConsoleAPIType::kLog;
|
||||
if (level == "debug") {
|
||||
type = ConsoleAPIType::kDebug;
|
||||
} else if (level == "log") {
|
||||
type = ConsoleAPIType::kLog;
|
||||
} else if (level == "warning") {
|
||||
type = ConsoleAPIType::kWarning;
|
||||
} else if (level == "error") {
|
||||
type = ConsoleAPIType::kError;
|
||||
}
|
||||
// NOTE: args[1] is the processed string message - ignore it.
|
||||
jsi::Array argsArray = args[2].asObject(rt).asArray(rt);
|
||||
std::vector<jsi::Value> argsVec;
|
||||
for (size_t i = 0, length = argsArray.length(rt); i != length;
|
||||
++i) {
|
||||
argsVec.emplace_back(argsArray.getValueAtIndex(rt, i));
|
||||
}
|
||||
// TODO(moti): Handle framesToSkip in some way. Note that the
|
||||
// runtime doesn't even capture a stack trace at the moment.
|
||||
ConsoleMessage consoleMessage{
|
||||
std::chrono::duration_cast<
|
||||
std::chrono::duration<double, std::milli>>(
|
||||
timestamp.time_since_epoch())
|
||||
.count(),
|
||||
type,
|
||||
std::move(argsVec)};
|
||||
if (auto self = selfWeak.lock()) {
|
||||
// Q: Why is it safe to use self->delegate_ here?
|
||||
// A: Because the caller of InspectorTarget::registerRuntime
|
||||
// is explicitly required to guarantee that the delegate not
|
||||
// only outlives the target, but also outlives all JS code
|
||||
// execution that occurs on the JS thread.
|
||||
self->delegate_.addConsoleMessage(
|
||||
rt, std::move(consoleMessage));
|
||||
// To ensure we never destroy `self` on the JS thread, send
|
||||
// our shared_ptr back to the inspector thread.
|
||||
selfExecutor([self = std::move(self)](auto&) { (void)self; });
|
||||
}
|
||||
return jsi::Value::undefined();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(
|
||||
FrontendChannel channel,
|
||||
SessionState& sessionState) {
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* 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 <jsinspector-modern/RuntimeTarget.h>
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
using namespace facebook::jsi;
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ConsoleState {
|
||||
/**
|
||||
* https://console.spec.whatwg.org/#counting
|
||||
*/
|
||||
std::unordered_map<std::string, int> countMap;
|
||||
|
||||
/**
|
||||
* https://console.spec.whatwg.org/#timing
|
||||
*/
|
||||
std::unordered_map<std::string, double> timerTable;
|
||||
|
||||
ConsoleState() = default;
|
||||
ConsoleState(const ConsoleState&) = delete;
|
||||
ConsoleState& operator=(const ConsoleState&) = delete;
|
||||
ConsoleState(ConsoleState&&) = delete;
|
||||
ConsoleState& operator=(ConsoleState&&) = delete;
|
||||
~ConsoleState() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* `console` methods that have no behaviour other than emitting a
|
||||
* Runtime.consoleAPICalled message.
|
||||
*/
|
||||
constexpr const std::pair<const char*, ConsoleAPIType>
|
||||
kForwardingConsoleMethods[] = {
|
||||
{"clear", ConsoleAPIType::kClear},
|
||||
{"debug", ConsoleAPIType::kDebug},
|
||||
{"dir", ConsoleAPIType::kDir},
|
||||
{"dirxml", ConsoleAPIType::kDirXML},
|
||||
{"error", ConsoleAPIType::kError},
|
||||
{"group", ConsoleAPIType::kStartGroup},
|
||||
{"groupCollapsed", ConsoleAPIType::kStartGroupCollapsed},
|
||||
{"groupEnd", ConsoleAPIType::kEndGroup},
|
||||
{"info", ConsoleAPIType::kInfo},
|
||||
{"log", ConsoleAPIType::kLog},
|
||||
{"table", ConsoleAPIType::kTable},
|
||||
{"trace", ConsoleAPIType::kTrace},
|
||||
{"warn", ConsoleAPIType::kWarning},
|
||||
};
|
||||
|
||||
/**
|
||||
* JS `Object.create()`
|
||||
*/
|
||||
jsi::Object objectCreate(jsi::Runtime& runtime, jsi::Value prototype) {
|
||||
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
|
||||
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
|
||||
return createFn.callWithThis(runtime, objectGlobal, prototype)
|
||||
.getObject(runtime);
|
||||
}
|
||||
|
||||
bool toBoolean(jsi::Runtime& runtime, const jsi::Value& val) {
|
||||
// Based on Operations.cpp:toBoolean in the Hermes VM.
|
||||
if (val.isUndefined() || val.isNull()) {
|
||||
return false;
|
||||
}
|
||||
if (val.isBool()) {
|
||||
return val.getBool();
|
||||
}
|
||||
if (val.isNumber()) {
|
||||
double m = val.getNumber();
|
||||
return m != 0 && !std::isnan(m);
|
||||
}
|
||||
if (val.isSymbol() || val.isObject()) {
|
||||
return true;
|
||||
}
|
||||
if (val.isString()) {
|
||||
std::string s = val.getString(runtime).utf8(runtime);
|
||||
return !s.empty();
|
||||
}
|
||||
assert(false && "All cases should be covered");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time in milliseconds as a double.
|
||||
*/
|
||||
double getTimestampMs() {
|
||||
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RuntimeTarget::installConsoleHandler() {
|
||||
jsExecutor_([selfWeak = weak_from_this(),
|
||||
selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
|
||||
jsi::Value consolePrototype = jsi::Value::null();
|
||||
auto originalConsoleVal = runtime.global().getProperty(runtime, "console");
|
||||
std::shared_ptr<jsi::Object> originalConsole;
|
||||
if (originalConsoleVal.isObject()) {
|
||||
originalConsole =
|
||||
std::make_shared<jsi::Object>(originalConsoleVal.getObject(runtime));
|
||||
consolePrototype = std::move(originalConsoleVal);
|
||||
} else {
|
||||
consolePrototype = jsi::Object(runtime);
|
||||
}
|
||||
auto console = objectCreate(runtime, std::move(consolePrototype));
|
||||
auto state = std::make_shared<ConsoleState>();
|
||||
|
||||
/**
|
||||
* An executor that runs synchronously and provides a safe reference to our
|
||||
* RuntimeTargetDelegate for use on the JS thread.
|
||||
* \see RuntimeTargetDelegate for information on which methods are safe to
|
||||
* call on the JS thread.
|
||||
* \warning The callback will not run if the RuntimeTarget has been
|
||||
* destroyed.
|
||||
*/
|
||||
auto delegateExecutorSync = [selfWeak, selfExecutor](auto&& func) {
|
||||
if (auto self = selfWeak.lock()) {
|
||||
// Q: Why is it safe to use self->delegate_ here?
|
||||
// A: Because the caller of InspectorTarget::registerRuntime
|
||||
// is explicitly required to guarantee that the delegate not
|
||||
// only outlives the target, but also outlives all JS code
|
||||
// execution that occurs on the JS thread.
|
||||
func(self->delegate_);
|
||||
// To ensure we never destroy `self` on the JS thread, send
|
||||
// our shared_ptr back to the inspector thread.
|
||||
selfExecutor([self = std::move(self)](auto&) { (void)self; });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call \param innerFn and forward any arguments to the original console
|
||||
* method named \param methodName, if possible.
|
||||
*/
|
||||
auto forwardToOriginalConsole = [originalConsole, delegateExecutorSync](
|
||||
const char* methodName,
|
||||
auto&& innerFn) {
|
||||
return [originalConsole,
|
||||
delegateExecutorSync,
|
||||
innerFn = std::forward<decltype(innerFn)>(innerFn),
|
||||
methodName](
|
||||
jsi::Runtime& runtime,
|
||||
const jsi::Value& thisVal,
|
||||
const jsi::Value* args,
|
||||
size_t count) mutable {
|
||||
jsi::Value retVal = innerFn(runtime, thisVal, args, count);
|
||||
if (originalConsole) {
|
||||
auto val = originalConsole->getProperty(runtime, methodName);
|
||||
if (val.isObject()) {
|
||||
auto obj = val.getObject(runtime);
|
||||
if (obj.isFunction(runtime)) {
|
||||
auto func = obj.getFunction(runtime);
|
||||
func.callWithThis(runtime, *originalConsole, args, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Install a console method with the given name and body. The body receives
|
||||
* the usual JSI host function parameters plus a ConsoleState reference, a
|
||||
* reference to the RuntimeTargetDelegate for sending messages to the
|
||||
* client, and the timestamp of the call. After the body runs (or is skipped
|
||||
* due to RuntimeTarget having been destroyed), the method of the same name
|
||||
* is also called on originalConsole (if it exists).
|
||||
*/
|
||||
auto installConsoleMethod =
|
||||
[&](const char* methodName,
|
||||
std::function<void(
|
||||
jsi::Runtime & runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
double timestampMs)>&& body) {
|
||||
console.setProperty(
|
||||
runtime,
|
||||
methodName,
|
||||
jsi::Function::createFromHostFunction(
|
||||
runtime,
|
||||
jsi::PropNameID::forAscii(runtime, methodName),
|
||||
0,
|
||||
forwardToOriginalConsole(
|
||||
methodName,
|
||||
[body = std::move(body), state, delegateExecutorSync](
|
||||
jsi::Runtime& runtime,
|
||||
const jsi::Value& /*thisVal*/,
|
||||
const jsi::Value* args,
|
||||
size_t count) mutable {
|
||||
auto timestampMs = getTimestampMs();
|
||||
delegateExecutorSync(
|
||||
[&runtime,
|
||||
args,
|
||||
count,
|
||||
body = std::move(body),
|
||||
state,
|
||||
timestampMs](auto& runtimeTargetDelegate) {
|
||||
body(
|
||||
runtime,
|
||||
args,
|
||||
count,
|
||||
runtimeTargetDelegate,
|
||||
*state,
|
||||
timestampMs);
|
||||
});
|
||||
return jsi::Value::undefined();
|
||||
})));
|
||||
};
|
||||
|
||||
/**
|
||||
* console.count
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"count",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
auto timestampMs) {
|
||||
std::string label = "default";
|
||||
if (count > 0 && !args[0].isUndefined()) {
|
||||
label = args[0].toString(runtime).utf8(runtime);
|
||||
}
|
||||
auto it = state.countMap.find(label);
|
||||
if (it == state.countMap.end()) {
|
||||
it = state.countMap.insert({label, 1}).first;
|
||||
} else {
|
||||
it->second++;
|
||||
}
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime, label + ": "s + std::to_string(it->second)));
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime, {timestampMs, ConsoleAPIType::kCount, std::move(vec)});
|
||||
});
|
||||
|
||||
/**
|
||||
* console.countReset
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"countReset",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
auto timestampMs) {
|
||||
std::string label = "default";
|
||||
if (count > 0 && !args[0].isUndefined()) {
|
||||
label = args[0].toString(runtime).utf8(runtime);
|
||||
}
|
||||
auto it = state.countMap.find(label);
|
||||
if (it == state.countMap.end()) {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime, "Count for '"s + label + "' does not exist"));
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs, ConsoleAPIType::kWarning, std::move(vec)});
|
||||
} else {
|
||||
it->second = 0;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* console.time
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"time",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
auto timestampMs) {
|
||||
std::string label = "default";
|
||||
if (count > 0 && !args[0].isUndefined()) {
|
||||
label = args[0].toString(runtime).utf8(runtime);
|
||||
}
|
||||
auto it = state.timerTable.find(label);
|
||||
if (it == state.timerTable.end()) {
|
||||
state.timerTable.insert({label, timestampMs});
|
||||
} else {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime, "Timer '"s + label + "' already exists"));
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs, ConsoleAPIType::kWarning, std::move(vec)});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* console.timeEnd
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"timeEnd",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
auto timestampMs) {
|
||||
std::string label = "default";
|
||||
if (count > 0 && !args[0].isUndefined()) {
|
||||
label = args[0].toString(runtime).utf8(runtime);
|
||||
}
|
||||
auto it = state.timerTable.find(label);
|
||||
if (it == state.timerTable.end()) {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime, "Timer '"s + label + "' does not exist"));
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs, ConsoleAPIType::kWarning, std::move(vec)});
|
||||
} else {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime,
|
||||
label + ": "s + std::to_string(timestampMs - it->second) +
|
||||
" ms"));
|
||||
state.timerTable.erase(it);
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs, ConsoleAPIType::kTimeEnd, std::move(vec)});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* console.timeLog
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"timeLog",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& state,
|
||||
auto timestampMs) {
|
||||
std::string label = "default";
|
||||
if (count > 0 && !args[0].isUndefined()) {
|
||||
label = args[0].toString(runtime).utf8(runtime);
|
||||
}
|
||||
auto it = state.timerTable.find(label);
|
||||
if (it == state.timerTable.end()) {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime, "Timer '"s + label + "' does not exist"));
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs, ConsoleAPIType::kWarning, std::move(vec)});
|
||||
} else {
|
||||
std::vector<jsi::Value> vec;
|
||||
vec.emplace_back(jsi::String::createFromUtf8(
|
||||
runtime,
|
||||
label + ": "s + std::to_string(timestampMs - it->second) +
|
||||
" ms"));
|
||||
if (count > 1) {
|
||||
for (size_t i = 1; i != count; ++i) {
|
||||
vec.emplace_back(runtime, args[i]);
|
||||
}
|
||||
}
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime, {timestampMs, ConsoleAPIType::kLog, std::move(vec)});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* console.assert
|
||||
*/
|
||||
installConsoleMethod(
|
||||
"assert",
|
||||
[](jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& /*state*/,
|
||||
auto timestampMs) {
|
||||
if (count >= 1 && toBoolean(runtime, args[0])) {
|
||||
return;
|
||||
}
|
||||
std::deque<jsi::Value> data;
|
||||
|
||||
if (count > 1) {
|
||||
for (size_t i = 1; i != count; ++i) {
|
||||
data.emplace_back(runtime, args[i]);
|
||||
}
|
||||
}
|
||||
if (data.empty()) {
|
||||
data.emplace_back(
|
||||
jsi::String::createFromUtf8(runtime, "Assertion failed"));
|
||||
} else if (data.front().isString()) {
|
||||
data.front() = jsi::String::createFromUtf8(
|
||||
runtime,
|
||||
"Assertion failed: "s +
|
||||
data.front().asString(runtime).utf8(runtime));
|
||||
} else {
|
||||
data.emplace_front(
|
||||
jsi::String::createFromUtf8(runtime, "Assertion failed"));
|
||||
}
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime,
|
||||
{timestampMs,
|
||||
ConsoleAPIType::kAssert,
|
||||
std::vector<jsi::Value>(
|
||||
make_move_iterator(data.begin()),
|
||||
make_move_iterator(data.end()))});
|
||||
});
|
||||
|
||||
for (auto& [name, type] : kForwardingConsoleMethods) {
|
||||
installConsoleMethod(
|
||||
name,
|
||||
[type = type](
|
||||
jsi::Runtime& runtime,
|
||||
const jsi::Value* args,
|
||||
size_t count,
|
||||
RuntimeTargetDelegate& runtimeTargetDelegate,
|
||||
ConsoleState& /*state*/,
|
||||
auto timestampMs) {
|
||||
std::vector<jsi::Value> argsVec;
|
||||
for (size_t i = 0; i != count; ++i) {
|
||||
argsVec.emplace_back(runtime, args[i]);
|
||||
}
|
||||
runtimeTargetDelegate.addConsoleMessage(
|
||||
runtime, {timestampMs, type, std::move(argsVec)});
|
||||
});
|
||||
}
|
||||
|
||||
runtime.global().setProperty(runtime, "console", console);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
Reference in New Issue
Block a user