Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
80ff462
DO NOT LAND -- Bubbleprof visualization demo
jasnell Jan 7, 2026
aff8fbb
fixup! DO NOT LAND -- Bubbleprof visualization demo
jasnell Jan 7, 2026
6d9ffe0
Add critical path visualization
jasnell Jan 8, 2026
564fba2
Expand visualizations and usability of the viewer
jasnell Jan 8, 2026
065c464
More visualization tweaks
jasnell Jan 8, 2026
d0e1feb
Example samples/demos
jasnell Jan 8, 2026
c608688
Further refinements/improvements to visualization
jasnell Jan 8, 2026
ede3e2c
Streams operations async context instrumentation
jasnell Jan 8, 2026
ba0d63b
Further enhacements to async trace viewer
jasnell Jan 8, 2026
6c3c419
Fix bugs and add a cf pages config
jasnell Jan 8, 2026
a1ca263
Added reset/redraw hot key
jasnell Jan 8, 2026
ca98058
Expand more analysis patterns
jasnell Jan 8, 2026
924aba1
Expand docs more
jasnell Jan 8, 2026
dff87e0
Improve tooltip context
jasnell Jan 8, 2026
8c9fa56
Add runtime overhead metric
jasnell Jan 8, 2026
61ce9b6
Document some additional future metrics
jasnell Jan 8, 2026
3a21f7e
Add critical path details to tooltips
jasnell Jan 8, 2026
746891b
Highlight critical path in replay
jasnell Jan 8, 2026
deb0421
Enhance visual indicators in replay view
jasnell Jan 8, 2026
4138a2f
Highlight unresolved promises in replay view
jasnell Jan 8, 2026
a94730e
More help text
jasnell Jan 8, 2026
e69c9d0
More visual replay tweaks
jasnell Jan 8, 2026
7616a60
More visual tweaks to replay
jasnell Jan 8, 2026
9dcdc8d
Remember settings through refresh
jasnell Jan 8, 2026
06d487d
Add evolving user vs runtime display to replay
jasnell Jan 8, 2026
84416f3
More visual tweaks in replay view
jasnell Jan 9, 2026
56c164a
More visual tweaks to replay view
jasnell Jan 9, 2026
a582a20
Fix timer instrumentation
jasnell Jan 9, 2026
86daf99
Improve waterfall view details
jasnell Jan 9, 2026
45eb1ba
Waterfall view improvements
jasnell Jan 9, 2026
d5d43ae
Update progress notes
jasnell Jan 9, 2026
5a5fb86
More enhancements to waterfall view
jasnell Jan 9, 2026
85324e8
Improvements to the DAG view
jasnell Jan 9, 2026
dfcfb78
Doc and guide updates
jasnell Jan 9, 2026
88ac830
Consolidate the bubble and dag views into a single view with toggle
jasnell Jan 9, 2026
0e38636
Tweak the UI a bit more
jasnell Jan 9, 2026
fb0c7ff
More tweaks for replay view, better rendering
jasnell Jan 9, 2026
fdc23f6
Additional replay tweaks
jasnell Jan 9, 2026
e6bb8d3
Improve parallelism view
jasnell Jan 9, 2026
30d73a2
Enhance the breakdown view
jasnell Jan 9, 2026
735e89a
Enhance latency and gaps visualizations
jasnell Jan 9, 2026
002bda3
Heatmap and doc improvements
jasnell Jan 9, 2026
f013cd9
more instrumentation and sample traces
jasnell Jan 10, 2026
09c2607
More replay tweaks
jasnell Jan 12, 2026
d514ee4
Experimenting with alternative replay forms
jasnell Jan 12, 2026
edb7a29
Update with some future possible view concepts
jasnell Jan 12, 2026
09ad4d9
Refine internal classification of events
jasnell Jan 12, 2026
7c9cf58
Add sibling grouping in bubble view
jasnell Jan 13, 2026
70400a9
Improve sibling/cousin grouping options in graph
jasnell Jan 13, 2026
f8a4123
Update future maybe do list
jasnell Jan 13, 2026
02467da
Add zoom controls to graph view
jasnell Jan 13, 2026
5645e2d
Refinements and fixes to latency calculation
jasnell Jan 13, 2026
28d1371
Expand latency plots
jasnell Jan 13, 2026
a72f8f7
Refine latency plots more
jasnell Jan 13, 2026
a0084e5
Instrument scheduler.wait
jasnell Jan 13, 2026
969eb24
Remove the breakdown view... it wasn't useful for diagnostics
jasnell Jan 13, 2026
37f4547
Remove heatmap view
jasnell Jan 13, 2026
31abefb
Design doc and cleanups
jasnell Jan 13, 2026
1a662a6
Add streams to async trace viewer samples
jasnell Jan 13, 2026
6e9a753
Update the web streams sample traces
jasnell Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions samples/web-streams/config.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ const webStreams :Workerd.Worker = (
(name = "streams-util", esModule = embed "streams-util.js")
],
compatibilityDate = "2025-12-31",
compatibilityFlags = [ "streams_no_default_auto_allocate_chunk_size", "experimental" ],
);
33 changes: 31 additions & 2 deletions src/workerd/api/basics.c++
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "actor-state.h"
#include "global-scope.h"

#include <workerd/io/async-trace.h>
#include <workerd/io/features.h>
#include <workerd/io/io-context.h>

Expand Down Expand Up @@ -1062,6 +1063,17 @@ kj::Promise<void> Scheduler::wait(
}
}

auto& ioContext = IoContext::current();

// Create async trace resource for this scheduler.wait operation
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = ioContext.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kTimer, js.v8Isolate);
asyncTrace->annotate(asyncTraceId, "delay"_kj, kj::str(delay));
asyncTrace->annotate(asyncTraceId, "type"_kj, "scheduler.wait"_kj);
}

// TODO(cleanup): Use jsg promise and resolver to avoid an unlock/relock. However, we need
// the abort signal to support wrapping jsg promises.
auto paf = kj::newPromiseAndFulfiller<void>();
Expand All @@ -1070,8 +1082,25 @@ kj::Promise<void> Scheduler::wait(

auto& global =
jsg::extractInternalPointer<ServiceWorkerGlobalScope, true>(context, context->Global());
global.setTimeoutInternal([fulfiller = IoContext::current().addObject(kj::mv(paf.fulfiller))](
jsg::Lock& lock) mutable { fulfiller->fulfill(); },
global.setTimeoutInternal(
[fulfiller = ioContext.addObject(kj::mv(paf.fulfiller)), asyncTraceId](
jsg::Lock& lock) mutable {
// Enter async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->enterCallback(asyncTraceId);
}
}
KJ_DEFER({
// Exit async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->exitCallback();
}
}
});
fulfiller->fulfill();
},
delay);

auto promise = kj::mv(paf.promise);
Expand Down
21 changes: 21 additions & 0 deletions src/workerd/api/cache.c++
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "util.h"

#include <workerd/io/async-trace.h>
#include <workerd/io/io-context.h>
#include <workerd/util/own-util.h>

Expand Down Expand Up @@ -81,6 +82,12 @@ jsg::Promise<jsg::Optional<jsg::Ref<Response>>> Cache::match(jsg::Lock& js,
auto userSpan = context.makeUserTraceSpan("cache_match"_kjc);
TraceContext traceContext(kj::mv(traceSpan), kj::mv(userSpan));

// Create async trace resource for cache.match
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTrace->createResource(AsyncTraceContext::ResourceType::kCacheGet, js.v8Isolate);
// Annotation will be added inside evalNow where we have the URL
}

KJ_IF_SOME(o, options) {
KJ_IF_SOME(ignoreMethod, o.ignoreMethod) {
traceContext.userSpan.setTag("cache.request.ignore_method"_kjc, ignoreMethod);
Expand Down Expand Up @@ -288,6 +295,13 @@ jsg::Promise<void> Cache::put(jsg::Lock& js,
auto userSpan = context.makeUserTraceSpan("cache_put"_kjc);
TraceContext traceContext(kj::mv(traceSpan), kj::mv(userSpan));

// Create async trace resource for cache.put
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
auto asyncId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kCachePut, js.v8Isolate);
asyncTrace->annotate(asyncId, "url"_kj, jsRequest->getUrl());
}

traceContext.userSpan.setTag("cache.request.url"_kjc, jsRequest->getUrl());
traceContext.userSpan.setTag("cache.request.method"_kjc, kj::str(jsRequest->getMethodEnum()));
traceContext.userSpan.setTag(
Expand Down Expand Up @@ -562,6 +576,13 @@ jsg::Promise<bool> Cache::delete_(jsg::Lock& js,
auto userSpan = context.makeUserTraceSpan("cache_delete"_kjc);
TraceContext traceContext(kj::mv(traceSpan), kj::mv(userSpan));

// Create async trace resource for cache.delete
// Note: URL annotation is added inside evalNow where we have the request
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTrace->createResource(
AsyncTraceContext::ResourceType::kCacheGet, js.v8Isolate); // Using kCacheGet as stand-in
}

KJ_IF_SOME(o, options) {
KJ_IF_SOME(ignoreMethod, o.ignoreMethod) {
traceContext.userSpan.setTag("cache.request.ignore_method"_kjc, ignoreMethod);
Expand Down
119 changes: 110 additions & 9 deletions src/workerd/api/global-scope.c++
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <workerd/api/system-streams.h>
#include <workerd/api/trace.h>
#include <workerd/api/util.h>
#include <workerd/io/async-trace.h>
#include <workerd/io/compatibility-date.h>
#include <workerd/io/features.h>
#include <workerd/io/io-context.h>
Expand Down Expand Up @@ -776,9 +777,32 @@ jsg::JsString ServiceWorkerGlobalScope::atob(jsg::Lock& js, kj::String data) {
}

void ServiceWorkerGlobalScope::queueMicrotask(jsg::Lock& js, jsg::Function<void()> task) {
auto& context = IoContext::current();

// Create async trace resource for this microtask
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kMicrotask, js.v8Isolate);
}

auto fn = js.wrapSimpleFunction(js.v8Context(),
JSG_VISITABLE_LAMBDA((this, fn = kj::mv(task)), (fn),
JSG_VISITABLE_LAMBDA((this, fn = kj::mv(task), asyncTraceId), (fn),
(jsg::Lock& js, const v8::FunctionCallbackInfo<v8::Value>& args) {
// Enter async trace callback scope for microtask
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->enterCallback(asyncTraceId);
}
}
KJ_DEFER({
// Exit async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->exitCallback();
}
}
});
js.tryCatch([&] {
// The function won't be called with any arguments, so we can
// safely ignore anything passed in to args.
Expand Down Expand Up @@ -834,13 +858,39 @@ TimeoutId::NumberType ServiceWorkerGlobalScope::setTimeout(jsg::Lock& js,
jsg::Function<void(jsg::Arguments<jsg::Value>)> function,
jsg::Optional<double> msDelay,
jsg::Arguments<jsg::Value> args) {
auto& context = IoContext::current();

// Create async trace resource for this timer
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kTimer, js.v8Isolate);
asyncTrace->annotate(asyncTraceId, "delay"_kj, kj::str(msDelay.orDefault(0)));
asyncTrace->annotate(asyncTraceId, "type"_kj, "setTimeout"_kj);
}

function.setReceiver(js.v8Ref<v8::Value>(js.v8Context()->Global()));
auto fn = [function = kj::mv(function), args = kj::mv(args),
context = jsg::AsyncContextFrame::currentRef(js)](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, context);
asyncContextFrame = jsg::AsyncContextFrame::currentRef(js),
asyncTraceId](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, asyncContextFrame);
// Enter async trace callback scope for timer
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->enterCallback(asyncTraceId);
}
}
KJ_DEFER({
// Exit async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->exitCallback();
}
}
});
function(js, kj::mv(args));
};
auto timeoutId = IoContext::current().setTimeoutImpl(timeoutIdGenerator,
auto timeoutId = context.setTimeoutImpl(timeoutIdGenerator,
/* repeat */ false, [function = kj::mv(fn)](jsg::Lock& js) mutable { function(js); },
msDelay.orDefault(0));
return timeoutId.toNumber();
Expand All @@ -860,15 +910,41 @@ TimeoutId::NumberType ServiceWorkerGlobalScope::setInterval(jsg::Lock& js,
jsg::Function<void(jsg::Arguments<jsg::Value>)> function,
jsg::Optional<double> msDelay,
jsg::Arguments<jsg::Value> args) {
auto& context = IoContext::current();

// Create async trace resource for this timer
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kTimer, js.v8Isolate);
asyncTrace->annotate(asyncTraceId, "delay"_kj, kj::str(msDelay.orDefault(0)));
asyncTrace->annotate(asyncTraceId, "type"_kj, "setInterval"_kj);
}

function.setReceiver(js.v8Ref<v8::Value>(js.v8Context()->Global()));
auto fn = [function = kj::mv(function), args = kj::mv(args),
context = jsg::AsyncContextFrame::currentRef(js)](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, context);
asyncContextFrame = jsg::AsyncContextFrame::currentRef(js),
asyncTraceId](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, asyncContextFrame);
// Enter async trace callback scope for timer
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->enterCallback(asyncTraceId);
}
}
KJ_DEFER({
// Exit async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->exitCallback();
}
}
});
// Because the fn is called multiple times, we will clone the args on each call.
auto argv = KJ_MAP(i, args) { return i.addRef(js); };
function(js, jsg::Arguments(kj::mv(argv)));
};
auto timeoutId = IoContext::current().setTimeoutImpl(timeoutIdGenerator,
auto timeoutId = context.setTimeoutImpl(timeoutIdGenerator,
/* repeat */ true, [function = kj::mv(fn)](jsg::Lock& js) mutable { function(js); },
msDelay.orDefault(0));
return timeoutId.toNumber();
Expand Down Expand Up @@ -1035,9 +1111,34 @@ jsg::Ref<Immediate> ServiceWorkerGlobalScope::setImmediate(jsg::Lock& js,
// would require a compat flag... but that's OK for now?

auto& context = IoContext::current();

// Create async trace resource for this immediate
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kTimer, js.v8Isolate);
asyncTrace->annotate(asyncTraceId, "delay"_kj, "0"_kj);
asyncTrace->annotate(asyncTraceId, "type"_kj, "setImmediate"_kj);
}

auto fn = [function = kj::mv(function), args = kj::mv(args),
context = jsg::AsyncContextFrame::currentRef(js)](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, context);
asyncContextFrame = jsg::AsyncContextFrame::currentRef(js),
asyncTraceId](jsg::Lock& js) mutable {
jsg::AsyncContextFrame::Scope scope(js, asyncContextFrame);
// Enter async trace callback scope for immediate
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->enterCallback(asyncTraceId);
}
}
KJ_DEFER({
// Exit async trace callback scope
if (asyncTraceId != AsyncTraceContext::INVALID_ID) {
if (auto* trace = IoContext::current().getAsyncTrace()) {
trace->exitCallback();
}
}
});
function(js, kj::mv(args));
};
auto timeoutId = context.setTimeoutImpl(timeoutIdGenerator,
Expand Down
11 changes: 11 additions & 0 deletions src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "worker-rpc.h"
#include "workerd/jsg/jsvalue.h"

#include <workerd/io/async-trace.h>
#include <workerd/io/features.h>
#include <workerd/io/io-context.h>
#include <workerd/jsg/ser.h>
Expand Down Expand Up @@ -1506,6 +1507,16 @@ jsg::Promise<jsg::Ref<Response>> fetchImplNoOutputLock(jsg::Lock& js,

auto& ioContext = IoContext::current();

// Create async trace resource for this fetch operation
AsyncTraceContext::AsyncId asyncTraceId = AsyncTraceContext::INVALID_ID;
if (auto* asyncTrace = ioContext.getAsyncTrace(); asyncTrace != nullptr) {
asyncTraceId = asyncTrace->createResource(
AsyncTraceContext::ResourceType::kFetch, js.v8Isolate);
asyncTrace->annotate(asyncTraceId, "url"_kj, jsRequest->getUrl());
asyncTrace->annotate(asyncTraceId, "method"_kj,
kj::str(jsRequest->getMethodEnum()));
}

auto signal = jsRequest->getSignal();
KJ_IF_SOME(s, signal) {
// If the AbortSignal has already been triggered, then we need to stop here.
Expand Down
37 changes: 37 additions & 0 deletions src/workerd/api/sockets.c++
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "streams/standard.h"
#include "system-streams.h"

#include <workerd/io/async-trace.h>
#include <workerd/io/io-context.h>
#include <workerd/io/worker-interface.h>
#include <workerd/jsg/exception.h>
#include <workerd/jsg/url.h>
Expand Down Expand Up @@ -255,6 +257,25 @@ jsg::Ref<Socket> connectImplNoOutputLock(jsg::Lock& js,
}
kj::Own<kj::TlsStarterCallback> tlsStarter = kj::heap<kj::TlsStarterCallback>();
httpConnectSettings.tlsStarter = tlsStarter;

// Create async trace resource for socket connect
if (auto* asyncTrace = ioContext.getAsyncTrace(); asyncTrace != nullptr) {
auto asyncId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kSocketConnect, js.v8Isolate);
asyncTrace->annotate(asyncId, "address"_kj, addressStr);
switch (secureTransport) {
case SecureTransportKind::OFF:
asyncTrace->annotate(asyncId, "secureTransport"_kj, "off"_kj);
break;
case SecureTransportKind::ON:
asyncTrace->annotate(asyncId, "secureTransport"_kj, "on"_kj);
break;
case SecureTransportKind::STARTTLS:
asyncTrace->annotate(asyncId, "secureTransport"_kj, "starttls"_kj);
break;
}
}

auto request = httpClient->connect(addressStr, *headers, httpConnectSettings);
request.connection = request.connection.attach(kj::mv(httpClient));

Expand Down Expand Up @@ -283,6 +304,15 @@ jsg::Promise<void> Socket::close(jsg::Lock& js) {
}

isClosing = true;

// Create async trace resource for socket close
auto& context = IoContext::current();
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
auto asyncId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kSocketClose, js.v8Isolate);
asyncTrace->annotate(asyncId, "address"_kj, remoteAddress);
}

writable->getController().setPendingClosure();
readable->getController().setPendingClosure();

Expand Down Expand Up @@ -339,6 +369,13 @@ jsg::Ref<Socket> Socket::startTls(jsg::Lock& js, jsg::Optional<TlsOptions> tlsOp
//
// Detach the AsyncIoStream from the Writable/Readable streams and make them unusable.
auto& context = IoContext::current();

// Create async trace resource for startTls
if (auto* asyncTrace = context.getAsyncTrace(); asyncTrace != nullptr) {
auto asyncId =
asyncTrace->createResource(AsyncTraceContext::ResourceType::kSocketStartTls, js.v8Isolate);
asyncTrace->annotate(asyncId, "address"_kj, remoteAddress);
}
auto openedPrPair = js.newPromiseAndResolver<SocketInfo>();
auto secureStreamPromise = context.awaitJs(js,
writable->flush(js).then(js,
Expand Down
Loading
Loading