Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
9c09d082fc fix(profiler): update computeLineColumnWithSourcemap to use String& instead of String*
Match the updated WebKit callback signature that uses a reference
parameter instead of a pointer for the remapped URL output.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 01:14:39 +00:00
Claude Bot
09f5a0fac0 fix(test): use platform-aware binary name for Windows compatibility
Use process.platform check to append .exe on Windows when building
and running the compiled test binary.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 00:43:28 +00:00
Claude Bot
3f96830637 fix(profiler): update computeLineColumnWithSourcemap to support URL remapping
Update the callback signature to match the WebKit-side change
(oven-sh/WebKit#166) that adds an optional String* parameter for
returning the remapped source URL through the sourcemap callback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 00:29:05 +00:00
Claude Bot
ae88edef82 fix(profiler): resolve sourcemapped paths in CPU profile output for compiled binaries
The CPU profiler was using `vm.computeLineColumnWithSourcemap()` which
only remaps line/column but not the source URL. For `bun build --compile
--sourcemap` binaries, this meant profiles showed internal `/$bunfs/root/`
paths instead of original source file paths.

Replace the callback with direct calls to `Bun__remapStackFramePositions`
which remaps both the URL and line/column through sourcemaps, matching
the behavior of error stack traces.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 10:23:57 +00:00
4 changed files with 191 additions and 89 deletions

View File

@@ -3,6 +3,7 @@
#include "ZigGlobalObject.h"
#include "helpers.h"
#include "BunString.h"
#include "headers-handwritten.h"
#include <JavaScriptCore/SamplingProfiler.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/JSGlobalObject.h>
@@ -72,6 +73,35 @@ struct ProfileNode {
WTF::Vector<int> children;
};
// Remap a source URL and line/column through sourcemaps.
// This handles both `bun build --sourcemap` and `bun build --compile --sourcemap`,
// resolving bundled paths like `/$bunfs/root/chunk-xyz.js:123` back to the original
// source file and line number (e.g., `src/myfile.ts:45`).
// lineNumber and columnNumber use 1-based convention on both input and output.
static void remapSourceLocation(JSC::VM& vm, WTF::String& url, int& lineNumber, int& columnNumber)
{
if (url.isEmpty() || lineNumber < 0)
return;
ZigStackFrame frame = {};
frame.source_url = Bun::toStringRef(url);
// Convert from 1-based to 0-based for ZigStackFrame
frame.position.line_zero_based = lineNumber > 0 ? lineNumber - 1 : 0;
frame.position.column_zero_based = columnNumber > 0 ? columnNumber - 1 : 0;
frame.remapped = false;
Bun__remapStackFramePositions(Bun::vm(vm), &frame, 1);
if (frame.remapped) {
WTF::String remappedUrl = frame.source_url.toWTFString();
if (!remappedUrl.isEmpty())
url = remappedUrl;
// Convert back to 1-based
lineNumber = frame.position.line().oneBasedInt();
columnNumber = frame.position.column().oneBasedInt();
}
}
WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm)
{
s_isProfilerRunning = false;
@@ -172,48 +202,30 @@ WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm)
if (provider) {
url = provider->sourceURL();
scriptId = static_cast<int>(provider->asID());
// Convert absolute paths to file:// URLs
// Check for:
// - Unix absolute path: /path/to/file
// - Windows drive letter: C:\path or C:/path
// - Windows UNC path: \\server\share
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/') {
// Unix absolute path
isAbsolutePath = true;
} else if (url.length() >= 2 && url[1] == ':') {
// Windows drive letter (e.g., C:\)
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z')) {
isAbsolutePath = true;
}
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\') {
// Windows UNC path (e.g., \\server\share)
isAbsolutePath = true;
}
}
if (isAbsolutePath) {
url = WTF::URL::fileURLWithFileSystemPath(url).string();
}
}
if (frame.hasExpressionInfo()) {
// Apply sourcemap if available
JSC::LineColumn sourceMappedLineColumn = frame.semanticLocation.lineColumn;
if (provider) {
#if USE(BUN_JSC_ADDITIONS)
auto& fn = vm.computeLineColumnWithSourcemap();
if (fn) {
fn(vm, provider, sourceMappedLineColumn);
}
#endif
}
lineNumber = static_cast<int>(sourceMappedLineColumn.line);
columnNumber = static_cast<int>(sourceMappedLineColumn.column);
lineNumber = static_cast<int>(frame.semanticLocation.lineColumn.line);
columnNumber = static_cast<int>(frame.semanticLocation.lineColumn.column);
// Remap through sourcemaps (updates url, lineNumber, columnNumber)
remapSourceLocation(vm, url, lineNumber, columnNumber);
}
// Convert absolute paths to file:// URLs (after sourcemap remapping)
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/')
isAbsolutePath = true;
else if (url.length() >= 2 && url[1] == ':') {
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z'))
isAbsolutePath = true;
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\')
isAbsolutePath = true;
}
if (isAbsolutePath)
url = WTF::URL::fileURLWithFileSystemPath(url).string();
}
// Create a unique key for this frame based on parent + callFrame
@@ -647,35 +659,30 @@ void stopCPUProfiler(JSC::VM& vm, WTF::String* outJSON, WTF::String* outText)
if (provider) {
url = provider->sourceURL();
scriptId = static_cast<int>(provider->asID());
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/')
isAbsolutePath = true;
else if (url.length() >= 2 && url[1] == ':') {
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z'))
isAbsolutePath = true;
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\')
isAbsolutePath = true;
}
if (isAbsolutePath)
url = WTF::URL::fileURLWithFileSystemPath(url).string();
}
if (frame.hasExpressionInfo()) {
JSC::LineColumn sourceMappedLineColumn = frame.semanticLocation.lineColumn;
if (provider) {
#if USE(BUN_JSC_ADDITIONS)
auto& fn = vm.computeLineColumnWithSourcemap();
if (fn)
fn(vm, provider, sourceMappedLineColumn);
#endif
}
lineNumber = static_cast<int>(sourceMappedLineColumn.line);
columnNumber = static_cast<int>(sourceMappedLineColumn.column);
lineNumber = static_cast<int>(frame.semanticLocation.lineColumn.line);
columnNumber = static_cast<int>(frame.semanticLocation.lineColumn.column);
// Remap through sourcemaps (updates url, lineNumber, columnNumber)
remapSourceLocation(vm, url, lineNumber, columnNumber);
}
// Convert absolute paths to file:// URLs (after sourcemap remapping)
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/')
isAbsolutePath = true;
else if (url.length() >= 2 && url[1] == ':') {
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z'))
isAbsolutePath = true;
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\')
isAbsolutePath = true;
}
if (isAbsolutePath)
url = WTF::URL::fileURLWithFileSystemPath(url).string();
}
WTF::StringBuilder keyBuilder;
@@ -817,35 +824,31 @@ void stopCPUProfiler(JSC::VM& vm, WTF::String* outJSON, WTF::String* outText)
if (frame.frameType == JSC::SamplingProfiler::FrameType::Executable && frame.executable) {
auto sourceProviderAndID = frame.sourceProviderAndID();
auto* provider = std::get<0>(sourceProviderAndID);
if (provider) {
if (provider)
url = provider->sourceURL();
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/')
isAbsolutePath = true;
else if (url.length() >= 2 && url[1] == ':') {
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z'))
isAbsolutePath = true;
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\')
isAbsolutePath = true;
}
if (isAbsolutePath)
url = WTF::URL::fileURLWithFileSystemPath(url).string();
if (frame.hasExpressionInfo()) {
int columnNumber = -1;
lineNumber = static_cast<int>(frame.semanticLocation.lineColumn.line);
columnNumber = static_cast<int>(frame.semanticLocation.lineColumn.column);
// Remap through sourcemaps (updates url, lineNumber, columnNumber)
remapSourceLocation(vm, url, lineNumber, columnNumber);
}
if (frame.hasExpressionInfo()) {
JSC::LineColumn sourceMappedLineColumn = frame.semanticLocation.lineColumn;
if (provider) {
#if USE(BUN_JSC_ADDITIONS)
auto& fn = vm.computeLineColumnWithSourcemap();
if (fn)
fn(vm, provider, sourceMappedLineColumn);
#endif
}
lineNumber = static_cast<int>(sourceMappedLineColumn.line);
// Convert absolute paths to file:// URLs (after sourcemap remapping)
bool isAbsolutePath = false;
if (!url.isEmpty()) {
if (url[0] == '/')
isAbsolutePath = true;
else if (url.length() >= 2 && url[1] == ':') {
char firstChar = url[0];
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z'))
isAbsolutePath = true;
} else if (url.length() >= 2 && url[0] == '\\' && url[1] == '\\')
isAbsolutePath = true;
}
if (isAbsolutePath)
url = WTF::URL::fileURLWithFileSystemPath(url).string();
}
WTF::String location = formatLocation(url, lineNumber);

View File

@@ -541,7 +541,7 @@ WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, Vector<StackFrame>& sta
return result;
}
void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn)
void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn, WTF::String& remappedURL)
{
auto sourceURL = sourceProvider->sourceURL();
if (sourceURL.isEmpty()) {
@@ -561,6 +561,9 @@ void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull s
if (frame.remapped) {
lineColumn.line = frame.position.line().oneBasedInt();
lineColumn.column = frame.position.column().oneBasedInt();
WTF::String newURL = frame.source_url.toWTFString();
if (!newURL.isEmpty() && newURL != sourceURL)
remappedURL = newURL;
}
}

View File

@@ -82,7 +82,7 @@ JSC_DECLARE_CUSTOM_SETTER(errorInstanceLazyStackCustomSetter);
// Internal wrapper functions for JSC error info callbacks
WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL, void* bunErrorData);
JSC::JSValue computeErrorInfoWrapperToJSValue(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL, JSC::JSObject* errorInstance, void* bunErrorData);
void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn);
void computeLineColumnWithSourcemap(JSC::VM& vm, JSC::SourceProvider* _Nonnull sourceProvider, JSC::LineColumn& lineColumn, WTF::String& remappedURL);
} // namespace Bun
namespace Zig {

View File

@@ -363,6 +363,102 @@ describe.concurrent("--cpu-prof", () => {
expect(profileContent).toContain("# CPU Profile");
});
test("--cpu-prof with --compile --sourcemap shows sourcemapped paths", async () => {
using dir = tempDir("cpu-prof-compile-sourcemap", {
"src/index.ts": `
import { heavyWork } from "./worker";
function main() {
const now = performance.now();
while (now + 100 > performance.now()) {
heavyWork();
}
}
main();
`,
"src/worker.ts": `
export function heavyWork() {
let sum = 0;
for (let i = 0; i < 10000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
`,
});
const appName = process.platform === "win32" ? "app.exe" : "app";
// Build with --compile --sourcemap
await using buildProc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--sourcemap",
"--outfile",
join(String(dir), appName),
join(String(dir), "src/index.ts"),
],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const buildStderr = await buildProc.stderr.text();
const buildExitCode = await buildProc.exited;
expect(buildStderr).not.toContain("error");
expect(buildExitCode).toBe(0);
// Run the compiled binary with cpu-prof flags via BUN_OPTIONS env var
// (standalone binaries don't parse runtime args, but do parse BUN_OPTIONS)
await using runProc = Bun.spawn({
cmd: [join(String(dir), appName)],
cwd: String(dir),
env: { ...bunEnv, BUN_OPTIONS: "--cpu-prof --cpu-prof-md" },
stderr: "pipe",
});
const runStderr = await runProc.stderr.text();
const runExitCode = await runProc.exited;
expect(runExitCode).toBe(0);
// Check JSON profile (.cpuprofile)
const files = readdirSync(String(dir));
const profileFiles = files.filter(f => f.endsWith(".cpuprofile"));
expect(profileFiles.length).toBeGreaterThan(0);
const profile = JSON.parse(readFileSync(join(String(dir), profileFiles[0]), "utf-8"));
// Collect all URLs with line numbers from profile nodes (frames with expression info)
const framesWithLocation: { url: string; line: number }[] = profile.nodes
.map((n: any) => ({ url: n.callFrame.url, line: n.callFrame.lineNumber }))
.filter((f: { url: string; line: number }) => f.url.length > 0 && f.line >= 0);
// Frames WITH line info should NOT contain /$bunfs/ paths or chunk- names
// (frames without line info may still show the binary name, which is expected)
const bunfsPaths = framesWithLocation.filter(
(f: { url: string }) => f.url.includes("$bunfs") || f.url.includes("chunk-"),
);
expect(bunfsPaths).toEqual([]);
// Should contain original source file names
const allUrls = framesWithLocation.map((f: { url: string }) => f.url);
const hasWorkerTs = allUrls.some((u: string) => u.includes("worker.ts"));
const hasIndexTs = allUrls.some((u: string) => u.includes("index.ts"));
expect(hasWorkerTs || hasIndexTs).toBe(true);
// Check Markdown profile (.md)
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
const mdContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
// Should contain original source filenames in markdown
expect(mdContent.includes("worker.ts") || mdContent.includes("index.ts")).toBe(true);
});
test("--cpu-prof and --cpu-prof-md together creates both files", async () => {
using dir = tempDir("cpu-prof-both-formats", {
"test.js": `