LibJS: Add source locations to console.trace()

LibJS+DevTools: Implement console.trace() with source locations

- Add Console::TraceFrame struct with source location data
- Implement Console::trace() to gather stack information
- Add WebView::StackFrame and ConsoleTrace for IPC
- Implement DevToolsConsoleClient::printer() for traces
- Update FrameActor to format traces for DevTools
- Update WorkerDebugConsoleClient trace handling
- Update ReplConsoleClient to format trace output
This commit is contained in:
Adam Colvin 2026-01-16 06:26:42 +00:00 committed by Sam Atkins
parent 0b346d1952
commit 2df5a7bb31
10 changed files with 145 additions and 24 deletions

View file

@ -286,6 +286,39 @@ void FrameActor::on_console_message(WebView::ConsoleOutput console_output)
console_messages.must_append(move(message));
},
[&](WebView::ConsoleTrace const& trace) {
message.set("level"sv, "trace"sv);
message.set("timeStamp"sv, console_output.timestamp.milliseconds_since_epoch());
JsonArray arguments;
if (!trace.label.is_empty())
arguments.must_append(trace.label);
message.set("arguments"sv, move(arguments));
JsonArray stack_array;
for (auto const& frame : trace.stack) {
JsonObject frame_object;
frame_object.set("functionName"sv, frame.function.value_or("<anonymous>"_string));
frame_object.set("filename"sv, frame.file.value_or("unknown"_string));
frame_object.set("lineNumber"sv, static_cast<i64>(frame.line.value_or(0)));
frame_object.set("columnNumber"sv, static_cast<i64>(frame.column.value_or(0)));
stack_array.must_append(move(frame_object));
}
message.set("stacktrace"sv, move(stack_array));
if (trace.stack.is_empty()) {
message.set("filename"sv, "unknown"sv);
message.set("lineNumber"sv, 0);
message.set("columnNumber"sv, 0);
} else {
auto const& first_frame = trace.stack.first();
message.set("filename"sv, first_frame.file.value_or("unknown"_string));
message.set("lineNumber"sv, static_cast<i64>(first_frame.line.value_or(0)));
message.set("columnNumber"sv, static_cast<i64>(first_frame.column.value_or(0)));
}
console_messages.must_append(move(message));
},
[&](WebView::ConsoleError const& error) {
StringBuilder stack;

View file

@ -15,8 +15,11 @@
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/ExecutionContext.h>
#include <LibJS/Runtime/StringConstructor.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibJS/SourceRange.h>
namespace JS {
@ -347,13 +350,28 @@ ThrowCompletionOr<Value> Console::trace()
// 1. Let trace be some implementation-defined, potentially-interactive representation of the callstack from where this function was called.
Console::Trace trace;
auto& execution_context_stack = vm.execution_context_stack();
// NOTE: -2 to skip the console.trace() execution context
for (ssize_t i = execution_context_stack.size() - 2; i >= 0; --i) {
auto function_name = execution_context_stack[i]->function ? execution_context_stack[i]->function->name_for_call_stack() : ""_utf16;
trace.stack.append(function_name.is_empty()
? "<anonymous>"_string
: function_name.to_utf8());
auto stack_trace = vm.stack_trace();
// NOTE: Skip the first frame (console.trace() itself)
for (size_t i = 1; i < stack_trace.size(); ++i) {
auto const& element = stack_trace[i];
auto* context = element.execution_context;
Console::TraceFrame frame;
auto function_name = (context && context->function) ? context->function->name_for_call_stack() : ""_utf16;
frame.function_name = function_name.is_empty() ? "<anonymous>"_string : function_name.to_utf8();
if (element.source_range) {
auto const& source_range = element.source_range->realize_source_range();
if (!source_range.filename().is_empty()) {
frame.source_file = MUST(String::from_byte_string(source_range.filename()));
frame.line = source_range.start.line;
frame.column = source_range.start.column;
}
}
trace.stack.append(move(frame));
}
// 2. Optionally, let formattedData be the result of Formatter(data), and incorporate formattedData as a label for trace.

View file

@ -55,9 +55,16 @@ public:
String label;
};
struct TraceFrame {
String function_name;
Optional<String> source_file;
Optional<size_t> line;
Optional<size_t> column;
};
struct Trace {
String label;
Vector<String> stack;
Vector<TraceFrame> stack;
};
void set_client(ConsoleClient& client) { m_client = &client; }

View file

@ -24,15 +24,7 @@ SourceRange const& TracebackFrame::source_range() const
{
if (!cached_source_range)
return dummy_source_range;
if (auto* unrealized = cached_source_range->source_range.get_pointer<UnrealizedSourceRange>()) {
auto source_range = [&] {
if (!unrealized->source_code)
return dummy_source_range;
return unrealized->realize();
}();
cached_source_range->source_range = move(source_range);
}
return cached_source_range->source_range.get<SourceRange>();
return cached_source_range->realize_source_range();
}
GC::Ref<Error> Error::create(Realm& realm)

View file

@ -33,6 +33,20 @@ public:
{
}
SourceRange const& realize_source_range()
{
static SourceRange dummy_source_range { SourceCode::create({}, {}), {}, {} };
if (auto* unrealized = source_range.get_pointer<UnrealizedSourceRange>()) {
if (unrealized->source_code) {
source_range = unrealized->realize();
} else {
source_range = dummy_source_range;
}
}
return source_range.get<SourceRange>();
}
size_t program_counter { 0 };
Variant<UnrealizedSourceRange, SourceRange> source_range;
};

View file

@ -46,8 +46,8 @@ JS::ThrowCompletionOr<JS::Value> WorkerDebugConsoleClient::printer(JS::Console::
if (!trace.label.is_empty())
builder.appendff("{}\033[36;1m{}\033[0m\n", indent, trace.label);
for (auto& function_name : trace.stack)
builder.appendff("{}-> {}\n", indent, function_name);
for (auto& frame : trace.stack)
builder.appendff("{}-> {}\n", indent, frame.function_name);
dbgln("{}", builder.string_view());
return JS::js_undefined();

View file

@ -70,6 +70,24 @@ ErrorOr<WebView::ConsoleError> IPC::decode(Decoder& decoder)
return WebView::ConsoleError { move(name), move(message), move(trace), inside_promise };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::ConsoleTrace const& trace)
{
TRY(encoder.encode(trace.label));
TRY(encoder.encode(trace.stack));
return {};
}
template<>
ErrorOr<WebView::ConsoleTrace> IPC::decode(Decoder& decoder)
{
auto label = TRY(decoder.decode<String>());
auto stack = TRY(decoder.decode<Vector<WebView::StackFrame>>());
return WebView::ConsoleTrace { move(label), move(stack) };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::ConsoleOutput const& output)
{
@ -83,7 +101,7 @@ template<>
ErrorOr<WebView::ConsoleOutput> IPC::decode(Decoder& decoder)
{
auto timestamp = TRY(decoder.decode<UnixDateTime>());
auto output = TRY(decoder.decode<Variant<WebView::ConsoleLog, WebView::ConsoleError>>());
auto output = TRY(decoder.decode<Variant<WebView::ConsoleLog, WebView::ConsoleError, WebView::ConsoleTrace>>());
return WebView::ConsoleOutput { timestamp, move(output) };
}

View file

@ -36,9 +36,14 @@ struct WEBVIEW_API ConsoleError {
bool inside_promise { false };
};
struct WEBVIEW_API ConsoleTrace {
String label;
Vector<StackFrame> stack;
};
struct WEBVIEW_API ConsoleOutput {
UnixDateTime timestamp;
Variant<ConsoleLog, ConsoleError> output;
Variant<ConsoleLog, ConsoleError, ConsoleTrace> output;
};
}
@ -63,6 +68,12 @@ ErrorOr<void> encode(Encoder&, WebView::ConsoleError const&);
template<>
ErrorOr<WebView::ConsoleError> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, WebView::ConsoleTrace const&);
template<>
ErrorOr<WebView::ConsoleTrace> decode(Decoder&);
template<>
WEBVIEW_API ErrorOr<void> encode(Encoder&, WebView::ConsoleOutput const&);

View file

@ -145,8 +145,36 @@ void DevToolsConsoleClient::send_console_output(WebView::ConsoleOutput console_o
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
JS::ThrowCompletionOr<JS::Value> DevToolsConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
{
if (log_level == JS::Console::LogLevel::Trace) {
auto const& trace = arguments.get<JS::Console::Trace>();
m_console->output_debug_message(log_level, trace.label);
Vector<WebView::StackFrame> stack_frames;
stack_frames.ensure_capacity(trace.stack.size());
for (auto const& frame : trace.stack) {
stack_frames.unchecked_append(WebView::StackFrame {
.function = frame.function_name,
.file = frame.source_file,
.line = frame.line,
.column = frame.column,
});
}
send_console_output({
.timestamp = UnixDateTime::now(),
.output = WebView::ConsoleTrace {
.label = trace.label,
.stack = move(stack_frames),
},
});
return JS::js_undefined();
}
// FIXME: Implement these.
if (first_is_one_of(log_level, JS::Console::LogLevel::Table, JS::Console::LogLevel::Trace, JS::Console::LogLevel::Group, JS::Console::LogLevel::GroupCollapsed))
if (first_is_one_of(log_level, JS::Console::LogLevel::Table, JS::Console::LogLevel::Group, JS::Console::LogLevel::GroupCollapsed))
return JS::js_undefined();
auto const& argument_values = arguments.get<GC::RootVector<JS::Value>>();

View file

@ -446,8 +446,8 @@ public:
if (!trace.label.is_empty())
builder.appendff("{}\033[36;1m{}\033[0m\n", indent, trace.label);
for (auto& function_name : trace.stack)
builder.appendff("{}-> {}\n", indent, function_name);
for (auto& frame : trace.stack)
builder.appendff("{}-> {}\n", indent, frame.function_name);
outln("{}", builder.string_view());
return JS::js_undefined();