Visualise Java structured concurrency task trees, lifetimes, and cancellation propagation.
View a live example report → (no install needed — nested scopes, a cancelled task, and critical-path highlighting)
Wrap your StructuredTaskScope code with TracedScope, run the program under a JFR
recording, feed the .jfr file to the analyzer, and open the resulting self-contained HTML
report. The report shows a per-scope Gantt timeline and task table so you can see what ran
in parallel, which tasks were cancelled, and how nested scopes relate to their parent task.
Targets JDK 26+ with --enable-preview (StructuredTaskScope is a preview API).
| Tool | Version |
|---|---|
| Java | 26+ (with --enable-preview) |
| Maven | 3.9+ |
<dependency>
<groupId>com.ionutbanu</groupId>
<artifactId>scope-tracer-core</artifactId>
<version>0.1.0</version>
</dependency>Until 0.1.0 lands on Maven Central, build from source (see below) and install into your local Maven repository before adding the dependency.
Replace StructuredTaskScope with TracedScope. The API is the same: fork(), join(),
try-with-resources.
import com.ionutbanu.scopetracer.core.TracedScope;
try (var scope = new TracedScope("checkout-flow")) {
Subtask<Quote> pricing = scope.fork(() -> pricingService.quote(cart));
Subtask<Reservation> inventory = scope.fork(() -> inventoryService.reserve(cart));
scope.join();
return new Checkout(pricing.get(), inventory.get());
}TracedScope emits six JFR events at every lifecycle moment (scope opened/closed, task
forked/succeeded/failed/cancelled). No other configuration is required.
java -XX:StartFlightRecording=filename=myapp.jfr,dumponexit=true \
--enable-preview \
-cp <your-classpath> \
com.example.MyAppVERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
java --enable-preview \
-jar scope-tracer-analyzer/target/scope-tracer-analyzer-${VERSION}-executable.jar \
myapp.jfr report.htmlThe -executable jar is a self-contained fat-jar produced by mvn package. It bundles
all runtime dependencies so no classpath assembly is needed.
Pass --format=json as a third argument to write the parsed trace as JSON instead of an
HTML report (schema documented in TraceModelJson's Javadoc) — intended for external
tools, such as an IDE plugin, that want the parsed trace without loading any scope-tracer
classes in-process:
java --enable-preview \
-jar scope-tracer-analyzer/target/scope-tracer-analyzer-${VERSION}-executable.jar \
myapp.jfr trace.json --format=jsonOr use the programmatic API:
import com.ionutbanu.scopetracer.analyzer.JfrParser;
import com.ionutbanu.scopetracer.analyzer.HtmlRenderer;
var model = JfrParser.parse(Path.of("myapp.jfr"));
String html = HtmlRenderer.render(model);
Files.writeString(Path.of("report.html"), html);Open report.html in any browser. No server required — the file is fully self-contained.
Each TracedScope gets its own section containing:
- Metadata line — owner thread, open time (UTC), total duration, task count.
- Task table — one row per forked task: task ID, task name (explicit, derived from
the
Callableclass, orSimpleClass#method:linefrom the call site), thread name, fork offset from scope open, duration, and outcome. Failed tasks show the exception type. Tasks that opened a nested scope show the child scope name. In all-success scopes the slowest task is annotated← critical path +Xms. - SVG Gantt timeline — a blue bar for the scope lifetime, colour-coded bars for each task (green = success, amber = critical path, red = failed, orange = cancelled, grey = incomplete), with the task ID and name labelled inside each bar; narrow bars drop the class-name prefix and, if still too narrow, truncate with an ellipsis — the full name is always available in the hover tooltip. Hover for details. The amber critical-path bar is the task that determined the scope's total duration.
Nested scopes are rendered indented beneath the parent task that opened them, with a breadcrumb showing which task spawned them.
Don't want to change source code? Use the agent. It instruments StructuredTaskScope at
the bytecode level — any JDK 26+ application is traced without touching its source.
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
java --enable-preview \
-javaagent:scope-tracer-agent/target/scope-tracer-agent-${VERSION}-agent.jar \
-XX:StartFlightRecording=filename=myapp.jfr,dumponexit=true \
-cp <your-classpath> \
com.example.MyAppThe agent:
- Uses the name supplied to
Config.withName("my-scope")when set. When no name is configured the name is derived from the call-site stack frame (format:SimpleClassName#methodName). - Emits the same six JFR events as
TracedScope, so the analyzer pipeline is identical. - Do not combine with
TracedScope— each scope would emit duplicate events.
For production deployments where you don't want to capture every scope, the agent accepts filter and sampling arguments. Filtered or sampled-out scopes produce zero JFR events for their entire lifetime — no open, no fork, no completion, no close.
| Argument | Meaning |
|---|---|
include.name=<glob> |
Only capture scopes whose name matches the glob. |
exclude.name=<glob> |
Drop scopes whose name matches. Exclude wins over include. |
include.package=<glob> |
Only capture scopes opened from a class in a matching package. Fails closed when the stack walk yields no user frame. |
exclude.package=<glob> |
Drop scopes opened from a class in a matching package. |
sample.rate=<0.0-1.0> |
Capture only this fraction of scopes that survive include/exclude. 1.0 (default) keeps all; 0.0 drops everything; 0.01 keeps ~1%. |
Globs support *, ** (both equivalent to .*), and ?. Patterns are not path-segment
aware: com.acme.* matches both com.acme.foo and com.acme.foo.bar. Use a literal pattern
if you need a strict prefix.
Example — trace only the checkout subsystem at a 10% sample rate:
java --enable-preview \
-javaagent:scope-tracer-agent.jar=include.package=com.acme.checkout.**,sample.rate=0.1 \
-XX:StartFlightRecording=filename=checkout.jfr,dumponexit=true \
-cp <your-classpath> com.acme.MainNote: The agent jar is self-bootstrapped via
Boot-Class-Pathin its manifest. No extra JVM flags are needed for the bootstrap classloader setup.
Seven runnable examples are included in scope-tracer-demos:
| Demo | What it shows |
|---|---|
ParallelFetchDemo |
Happy path — three tasks run in parallel, all succeed |
FailFastDemo |
Cancellation — one task fails, its sibling is interrupted |
NestedScopesDemo |
Nesting — a task inside the outer scope opens an inner scope |
AgentDemo |
Zero-code-change — plain StructuredTaskScope, traced by the agent |
LiveServiceDemo |
On-demand monitoring — long-running service; use jcmd to turn tracing on/off without restarting |
OrderProcessingDemo |
Multi-level nesting — e-commerce pipeline with payment and inventory sub-scopes; fraud failures; critical-path highlighting |
LiveOrderProcessingDemo |
Live version of OrderProcessingDemo — runs until Ctrl+C; use jcmd to capture windows of the nested pipeline |
The first four demos write a .jfr and .html file to target/ and exit. The live demos
run until Ctrl+C and print ready-to-paste jcmd commands at startup.
Run a demo:
mvn install -DskipTests
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
mvn -pl scope-tracer-demos -q dependency:build-classpath -Dmdep.outputFile=/tmp/scope-tracer-cp.txt
CP=$(cat /tmp/scope-tracer-cp.txt)
JARS="scope-tracer-core/target/scope-tracer-core-${VERSION}.jar:\
scope-tracer-analyzer/target/scope-tracer-analyzer-${VERSION}.jar:\
scope-tracer-demos/target/scope-tracer-demos-${VERSION}.jar:$CP"
java --enable-preview -cp "$JARS" com.ionutbanu.scopetracer.demos.ParallelFetchDemo
java --enable-preview -cp "$JARS" com.ionutbanu.scopetracer.demos.FailFastDemo
java --enable-preview -cp "$JARS" com.ionutbanu.scopetracer.demos.NestedScopesDemo
# AgentDemo uses the agent — no TracedScope in source
java --enable-preview \
-javaagent:scope-tracer-agent/target/scope-tracer-agent-${VERSION}-agent.jar \
-cp "$JARS" com.ionutbanu.scopetracer.demos.AgentDemo
# OrderProcessingDemo — multi-level nesting; writes .jfr and .html then exits
java --enable-preview -cp "$JARS" com.ionutbanu.scopetracer.demos.OrderProcessingDemo
# LiveServiceDemo — long-running; copy the jcmd commands it prints, then Ctrl+C to stop
java --enable-preview \
-javaagent:scope-tracer-agent/target/scope-tracer-agent-${VERSION}-agent.jar \
-cp "$JARS" com.ionutbanu.scopetracer.demos.LiveServiceDemo
# LiveOrderProcessingDemo — live nested pipeline; copy the jcmd commands it prints, then Ctrl+C to stop
java --enable-preview -cp "$JARS" com.ionutbanu.scopetracer.demos.LiveOrderProcessingDemoUsing LiveServiceDemo (in a second terminal while the service is running):
# capture a window of activity
jcmd <pid> JFR.start name=trace filename=/tmp/scope-trace.jfr
# ... wait for a few orders to process ...
# stop and dump
jcmd <pid> JFR.stop name=trace
# analyze
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
java --enable-preview \
-jar scope-tracer-analyzer/target/scope-tracer-analyzer-${VERSION}-executable.jar \
/tmp/scope-trace.jfr /tmp/report.html
open /tmp/report.htmlYou can repeat JFR.start / JFR.stop as many times as you like without restarting the
service. Each recording captures only the orders that ran during that window.
git clone <repo-url>
cd scope-tracer
mvn -q verify # compile, test, style check
mvn spotless:apply # auto-fix formatting if neededSee CLAUDE.md for architecture details, coding conventions, and contributor guidelines.
| Module | Artifact | Purpose |
|---|---|---|
scope-tracer-core |
com.ionutbanu:scope-tracer-core |
TracedScope wrapper; emits JFR events |
scope-tracer-analyzer |
com.ionutbanu:scope-tracer-analyzer |
Parses .jfr files; renders HTML/SVG reports |
scope-tracer-agent |
com.ionutbanu:scope-tracer-agent |
Java agent; instruments StructuredTaskScope at bytecode level |
scope-tracer-demos |
com.ionutbanu:scope-tracer-demos |
Runnable example programs |
The six events emitted by TracedScope and the agent are documented in
docs/jfr-events.md. That file lists every field, its type, and
when each event is emitted — useful when writing custom consumers of the raw .jfr file.
Apache License 2.0. Free for any use, including commercial — see NOTICE for attribution requirements.