https://github.com/btraceio/jafar
Experimental JFR parser
https://github.com/btraceio/jafar
Last synced: 2 months ago
JSON representation
Experimental JFR parser
- Host: GitHub
- URL: https://github.com/btraceio/jafar
- Owner: btraceio
- License: apache-2.0
- Created: 2024-08-08T00:45:37.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2026-04-23T23:06:38.000Z (2 months ago)
- Last Synced: 2026-04-24T01:38:14.516Z (2 months ago)
- Language: Java
- Homepage:
- Size: 5.34 MB
- Stars: 49
- Watchers: 3
- Forks: 1
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# JAFAR
Fast, modern JFR (Java Flight Recorder) parser for the JVM with a small, focused API.
**Status**: Early public release (v0.11.0) - API may evolve based on feedback. See [CHANGELOG.md](CHANGELOG.md) for details.
JAFAR provides both typed (interface-based) and untyped (Map-based) APIs for parsing JFR recordings with minimal ceremony. It emphasizes performance, low allocation, and ease of use.
## Requirements
- Java 21+
## Build
1) Fetch binary resources: `./get_resources.sh`
2) Build all modules: `./gradlew build`
3) Build shadow JARs (optional): `./gradlew shadowJar`
## Quick start (typed API)
Define a Java interface per JFR type and annotate with `@JfrType`. Methods correspond to event fields; use `@JfrField` to map differing names and `@JfrIgnore` to skip fields.
```java
import io.jafar.parser.api.*;
import java.nio.file.Paths;
@JfrType("custom.MyEvent")
public interface MyEvent { // no base interface required
String myfield();
}
try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to/recording.jfr"))) {
HandlerRegistration reg = p.handle(MyEvent.class, (e, ctl) -> {
System.out.println(e.myfield());
long pos = ctl.stream().position(); // current byte position while in handler
// ctl.abort(); // optionally stop parsing immediately without throwing
});
p.run();
reg.destroy(p); // deregister
}
```
Notes:
- Handlers run synchronously on the parser thread. Keep work small or offload.
- Exceptions thrown from a handler stop parsing and propagate from `run()`.
- Call `ctl.abort()` inside a handler to stop parsing early without an exception.
## Untyped API
Receive events as `Map` with nested maps/arrays when applicable.
```java
import io.jafar.parser.api.*;
import java.nio.file.Paths;
try (UntypedJafarParser p = JafarParser.newUntypedParser(Paths.get("/path/to/recording.jfr"))) {
HandlerRegistration> reg = p.handle((type, value) -> {
if ("jdk.ExecutionSample".equals(type.getName())) {
// You can retrieve the value by providing 'path' -> "eventThread", "javaThreadId"
Object threadId = Values.get(value, "eventThread", "javaThreadId");
// You can also get the value conveniently typed - for primitive values you need to use the boxed type in the call
long threadIdLong = Values.as(value, Long.class, "eventThread", "javaThreadId");
// use threadId ...
}
});
p.run();
reg.destroy(p);
}
```
### Complex and array values in untyped events
- **ComplexType**: Complex fields may appear either inline as `Map` or as a wrapper implementing `io.jafar.parser.api.ComplexType` (e.g., constant-pool backed references). Use `getValue()` on a `ComplexType` to obtain the resolved `Map`.
- **ArrayType**: When a field is an array, the value implements `io.jafar.parser.api.ArrayType`. Use `getType()` to inspect the array class (e.g., `int[].class`, `Object[].class`) and `getArray()` to access the underlying Java array.
Examples:
```java
import io.jafar.parser.api.*;
import java.util.Map;
try (UntypedJafarParser p = JafarParser.newUntypedParser(Paths.get("/path/to/recording.jfr"))) {
p.handle((type, value) -> {
// ComplexType: constant-pool backed references (e.g., eventThread)
Map thread = Values.as(value, Map.class, "eventThread").orElse(null);
if (thread != null) {
System.out.println("thread id=" + thread.get("javaThreadId") + ", name=" + thread.get("name"));
}
// ArrayType: arrays of primitives, Strings, maps, or ComplexType elements
Object framesVal = Values.get(value, "stackTrace", "frames");
// You can also reference the array elements directly
Object firstFrame = Values.get(value, "stackTrace", "frames", 0);
if (framesVal instanceof ArrayType at) {
Object arr = at.getArray();
if (arr instanceof Object[] objs) {
for (Object el : objs) {
if (el instanceof ComplexType cpx) {
Map m = cpx.getValue();
// use fields from the resolved element
} else if (el instanceof Map) {
Map m = (Map) el; // inline complex value
} else {
// primitive wrapper or String
}
}
} else if (arr instanceof int[] ints) {
for (int i : ints) { /* ... */ }
} else if (arr instanceof long[] longs) {
for (long l : longs) { /* ... */ }
}
}
});
p.run();
}
```
## Build-Time Handler Generation
JAFAR now supports **build-time handler generation** via annotation processor, providing massive performance benefits for production applications.
### Why Build-Time Generation?
**Benchmark Results:**
- **85% less memory allocation** (35.5 MB/sec vs 237.2 MB/sec)
- **Eliminates GC collections** (0 vs 3 GC pauses per benchmark)
- **Equivalent throughput** (no performance penalty)
- **Predictable latency** (no GC jitter)
[→ See Full Performance Report](doc/performance/PerformanceReport.md)
### How It Works
1. **Compile-time**: Annotation processor scans `@JfrType` interfaces and generates:
- Handler implementation classes
- Factory classes with thread-local caching
- ServiceLoader registration (META-INF/services)
2. **Runtime**: Parser auto-discovers factories via ServiceLoader, handlers are reused via thread-local cache
### Usage
#### 1. Add Annotation Processor Dependency
**Gradle:**
```gradle
dependencies {
implementation 'io.btrace:jafar-parser:0.11.0'
annotationProcessor 'io.btrace:jafar-processor:0.11.0'
}
```
**Maven:**
```xml
io.btrace
jafar-parser
0.11.0
org.apache.maven.plugins
maven-compiler-plugin
io.btrace
jafar-processor
0.11.0
```
#### 2. Define Event Interfaces (Top-Level Only)
```java
// Must be top-level interfaces (not nested/inner classes)
@JfrType("jdk.ExecutionSample")
public interface JFRExecutionSample {
@JfrField("startTime")
long startTime();
@JfrField("sampledThread")
JFRThread sampledThread();
}
@JfrType("java.lang.Thread")
public interface JFRThread {
@JfrField("javaThreadId")
long javaThreadId();
@JfrField("javaName")
String javaName();
}
```
**Note:** Annotation processor only processes **top-level interfaces**. Nested/inner classes with `@JfrType` are skipped and use runtime generation instead.
#### 3. Parse Events (Factories Auto-Discovered)
```java
try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to/recording.jfr"))) {
// Factories automatically discovered via ServiceLoader - no registration needed!
// Handle events (uses thread-local cached handlers)
p.handle(JFRExecutionSample.class, (event, ctl) -> {
JFRThread thread = event.sampledThread();
if (thread != null) {
System.out.println("Thread: " + thread.javaName());
}
});
p.run();
}
```
**That's it!** The annotation processor generates factories and registers them via ServiceLoader. No manual registration required.
### Runtime Generation (Default)
If you don't register factories, JAFAR falls back to **runtime bytecode generation** (existing behavior):
```java
try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to/recording.jfr"))) {
// No factory registration - handlers generated at runtime via ASM
p.handle(JFRExecutionSample.class, (event, ctl) -> {
// Handler generated on first use, cached globally
System.out.println("Event: " + event.startTime());
});
p.run();
}
```
### When to Use Build-Time Generation
✅ **Use build-time generation when:**
- Processing large JFR files or streams (millions of events)
- Memory allocation is a bottleneck
- Running in memory-constrained environments (containers)
- GC pauses affect latency SLAs
- Deploying to GraalVM native images
- Event types are known at compile time
✅ **Use runtime generation when:**
- Building JFR analysis tools (unknown event types)
- Rapid prototyping and exploration
- Processing arbitrary JFR recordings
- No build-time configuration desired
### Performance Impact
For processing **1 million ExecutionSample events:**
| Metric | Runtime Generation | Build-Time Generation | Benefit |
|--------|-------------------|----------------------|---------|
| Total Allocations | ~223 GB | ~37 GB | **-186 GB** |
| GC Collections | ~600-800 | ~50-100 | **-750 GC pauses** |
| GC Pause Time | ~2-3 seconds | ~200-300ms | **-2.7 seconds** |
| Throughput | ~189k events/sec | ~187k events/sec | Equivalent |
[→ Full Benchmark Results](doc/performance/BuildTimeBenchmarks.md)
## Core API overview
For the architecture of the `parser` module, see the [Parser Architecture](parser/ARCHITECTURE.md).
- `JafarParser`
- `newTypedParser(Path)` / `newUntypedParser(Path)`: start a session.
- `withParserListener(ChunkParserListener)`: observe low-level parse events (advanced, see below).
- `run()`: parse and invoke registered handlers.
- `TypedJafarParser`
- `handle(Class, JFRHandler) -> HandlerRegistration`
- Static `open(String|Path[, ParsingContext])` are also available, but prefer `JafarParser.newTypedParser(Path)`.
- `UntypedJafarParser`
- `handle(UntypedJafarParser.EventHandler) -> HandlerRegistration>`
- Static `open(String|Path[, ParsingContext])` also available.
- Data wrappers
- `ArrayType`: wrapper around arrays. `getType()` returns the array class; `getArray()` returns the backing Java array.
- `ComplexType`: wrapper around complex values. `getValue()` resolves to a `Map`. Note that some complex fields may be provided inline as a `Map` without a wrapper.
- `ParsingContext`
- `create()`: build a reusable context.
- `newTypedParser(Path)` / `newUntypedParser(Path)`: create parsers bound to the shared context.
- `uptime()`: cumulative processing time across sessions using the context.
- `Control`
- `stream().position()`: current byte position while a handler executes.
- `abort()`: stop parsing immediately (no exception thrown).
- `chunkInfo()`: chunk metadata with `startTime()`, `duration()`, `size()`, and `convertTicks(long, TimeUnit)`.
- Why `convertTicks(...)`? JFR records many time values in chunk-relative ticks. Converting on demand avoids creating `Instant`/`Duration` objects for every event, minimizing allocation and GC pressure when a scalar value suffices. Convert only when needed and to the unit you need.
### Typed runtime (JDK support)
- The typed parser defines small, generated classes at runtime. It automatically picks the best available strategy for the running JDK:
- JDK 15+: hidden classes via `MethodHandles.Lookup#defineHiddenClass` (fastest, unloadable)
- JDK 9–14: `MethodHandles.Lookup#defineClass(byte[])` (good)
- JDK 8: `sun.misc.Unsafe#defineAnonymousClass` (compatible; slightly heavier)
- Selection is automatic based on capability probes; no flags required. Enable debug logs to see the chosen strategy.
### Multi‑Release JAR (parser)
- The `parser` artifact is a Multi‑Release JAR:
- Base classes target Java 8 for broad compatibility.
- Java 21 overrides live under `META-INF/versions/21` and restore faster implementations (e.g., zero‑copy `ByteBuffer` slicing, `Arrays.equals` range checks, `Files.writeString`, etc.).
- On Java 21+, the JVM loads these optimized classes automatically. On older JVMs, the Java 8 fallbacks are used.
- Annotations
- `@JfrType("")`: declare the JFR type an interface represents.
- `@JfrField("", raw = false)`: map differing names or request raw representation.
- `@JfrIgnore`: exclude a method from mapping.
## Advanced usage
- Reusing context across many recordings
```java
ParsingContext ctx = ParsingContext.create();
try (TypedJafarParser p = ctx.newTypedParser(Paths.get("/path/to.a.jfr"))) {
p.handle(MyEvent.class, (e, ctl) -> {/*...*/});
p.run();
}
try (TypedJafarParser p = ctx.newTypedParser(Paths.get("/path/to.b.jfr"))) {
p.handle(MyEvent.class, (e, ctl) -> {/*...*/});
p.run();
}
System.out.println("uptime(ns)=" + ctx.uptime());
```
- Early termination with Control
```java
AtomicInteger seen = new AtomicInteger();
try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to.jfr"))) {
HandlerRegistration reg =
p.handle(JFRJdkExecutionSample.class, (e, ctl) -> {
if (seen.incrementAndGet() >= 1000) {
ctl.abort(); // stop without throwing
}
});
p.run();
reg.destroy(p);
}
```
- Converting JFR ticks to time units
```java
// Some fields are expressed in JFR ticks. Convert them only when needed.
try (UntypedJafarParser p = JafarParser.newUntypedParser(Paths.get("/file.jfr"))) {
p.handle((type, value, ctl) -> {
long ticksObj = value.get("startTime"); // example field holding ticks
long nanos = ctl.chunkInfo().convertTicks(n.longValue(), TimeUnit.NANOSECONDS);
// Use nanos directly, or wrap as Instant only when necessary
Instant startTs = ctl.chunkInfo().startTime().plusNanos(nanos);
// use the startTs instant ...
});
p.run();
}
```
- Observing parse lifecycle (low-level)
```java
import io.jafar.parser.internal_api.ChunkParserListener;
import io.jafar.parser.internal_api.metadata.MetadataEvent;
p.withParserListener(new ChunkParserListener() {
@Override public boolean onMetadata(ParserContext c, MetadataEvent md) {
// inspect metadata per chunk
return true; // continue
}
}).run();
```
## Gradle plugin: generate Jafar type interfaces
Plugin id: `io.btrace.jafar-gradle-plugin`
Adds task `generateJafarTypes` and wires it to `compileJava`. It can generate interfaces for selected JFR types from either the current JVM metadata (default) or a `.jfr` file.
**Generated class naming**: The generator includes the full namespace in generated interface names to avoid collisions. For example:
- `jdk.ExecutionSample` → `JFRJdkExecutionSample`
- `datadog.ExecutionSample` → `JFRDatadogExecutionSample`
- `jdk.gc.HeapSummary` → `JFRJdkGcHeapSummary`
This ensures that events with the same simple name but different namespaces generate distinct interfaces.
```gradle
plugins {
id 'io.btrace.jafar-gradle-plugin' version '0.11.0'
}
repositories {
mavenCentral()
mavenLocal()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
generateJafarTypes {
// Optional: use a JFR file to derive metadata; otherwise JVM runtime metadata is used
inputFile = file('/path/to/recording.jfr')
// Optional: where to generate sources (default: build/generated/sources/jafar/src/main)
outputDir = project.file('src/main/java')
// Optional: do not overwrite existing files (default: false)
overwrite = false
// Optional: filter event types by name (closure gets fully-qualified JFR type name)
eventTypeFilter {
it.startsWith('jdk.') && it != 'jdk.SomeExcludedType'
}
// Package for generated interfaces (default: io.jafar.parser.api.types)
targetPackage = 'com.acme.jfr.types'
}
```
You can also provide the input file via a project property: `-Pjafar.input=/path/to/recording.jfr`.
## Tools: Scrubbing sensitive fields in a `.jfr`
`io.jafar.tools.Scrubber` can scrub selected string fields in-place while copying to a new file.
Example: scrub the value of `jdk.InitialSystemProperty.value` when `key == 'java.home'`.
```java
import static io.jafar.tools.Scrubber.scrubFile;
import io.jafar.tools.Scrubber.ScrubField;
import java.nio.file.Paths;
scrubFile(Paths.get("/in.jfr"), Paths.get("/out-scrubbed.jfr"),
clz -> {
if (clz.equals("jdk.InitialSystemProperty")) {
return new ScrubField("key", "value", (k, v) -> "java.home".equals(k));
}
return null; // no scrubbing for other classes
}
);
```
## Demo
Build and run the demo application:
```shell
# First you need to publish parser, tools and plugin to local maven
cd demo
./build.sh
java -jar build/libs/jafar-demo-all.jar [jafar|jmc|jfr|jfr-stream] /path/to/recording.jfr
```
On an M1 and a ~600MiB JFR, the Jafar parser completes in ~1s vs ~7s with JMC (anecdotal). The stock `jfr` tool may OOM when printing all events.
## JFR Shell
JAFAR includes `jfr-shell`, an interactive CLI for exploring and analyzing JFR files with a powerful query language. See **[jfr-shell/README.md](jfr-shell/README.md)** for features and installation.
### Key Features
- **Interactive REPL** with intelligent tab completion
- **JfrPath query language** for filtering, projection, and aggregation
- **Flame graphs**: interactive HTML flame graphs from stack trace events
- **Scripting support**: record, save, and replay analysis workflows with variable substitution
- **Event decoration** for correlating and joining events (time-based and key-based)
- **Multiple output formats**: table and JSON
- **Multi-session support**: work with multiple recordings simultaneously
- **Non-interactive mode**: execute queries from command line for scripting/CI
- **Pluggable backends**: Jafar parser (full-featured) and JDK JFR API (limited, broadly compatible)
### Quick Example
```bash
# Install via JBang (easiest)
jbang app install jfr-shell@btraceio
# Open and analyze a recording
jfr-shell recording.jfr
jfr> events/jdk.ExecutionSample | groupBy(thread/name)
jfr> events/jdk.FileRead | top(10, by=bytes)
jfr> events/jdk.ExecutionSample | flamegraph()
# Event decoration: correlate samples with lock waits
jfr> events/jdk.ExecutionSample | decorateByTime(jdk.JavaMonitorWait, fields=monitorClass)
```
See **[Event Decoration and Joining](doc/cli/Tutorial.md#event-decoration-and-joining)** for advanced correlation and joining capabilities.
## MCP Server
JAFAR includes an MCP (Model Context Protocol) server that enables AI agents like Claude to analyze JFR recordings. See **[jfr-mcp/README.md](jfr-mcp/README.md)** for details.
### Quick Install
```bash
curl -Ls https://raw.githubusercontent.com/btraceio/jafar/main/jfr-mcp/install.sh | bash
```
This installs [JBang](https://www.jbang.dev) (if needed) and the `jfr-mcp` command in one step.
### Claude Code
```bash
claude mcp add jafar -- jbang jfr-mcp@btraceio --stdio
```
### Claude Desktop
```json
{
"mcpServers": {
"jafar": {
"command": "jbang",
"args": ["jfr-mcp@btraceio", "--stdio"]
}
}
}
```
## Heap Dump Analysis (NEW)
JAFAR now supports **heap dump (HPROF) analysis** using the same interactive shell. Query objects, classes, and GC roots with the HdumpPath query language.
### Quick Example
```bash
# Open a heap dump
jfr-shell dump.hprof
jfr> objects | count
| count |
|---------|
| 1923456 |
jfr> objects | groupBy(class, agg=count) | top(10, count)
| class | count |
|---------------------------|---------|
| java.util.HashMap$Node | 249,734 |
| java.lang.String | 238,750 |
| java.lang.Object[] | 156,234 |
jfr> objects/java.lang.String[shallow > 1KB] | stats(shallow)
| count | sum | min | max | avg |
|-------|-----------|------|--------|--------|
| 1,234 | 2,456,789 | 1024 | 65,536 | 1,991 |
jfr> gcroots | groupBy(type) | sortBy(count desc)
| type | count |
|-------------|-------|
| JAVA_FRAME | 2,345 |
| THREAD_OBJ | 1,234 |
```
### Key Features
- **Object queries**: Filter by class, size, type hierarchy
- **Class analysis**: Instance counts, memory footprint
- **GC root inspection**: Thread roots, JNI references, stack frames
- **Aggregations**: count, sum, stats, groupBy, top
- **Memory analysis**: pathToRoot, retentionPaths, retainedBreakdown, dominators
- **Leak detection**: Built-in detectors for common leak patterns
- **Heap diff**: `join(session=id)` compares two heap dumps side-by-side
- **Size units**: Use `1KB`, `1MB`, `1GB` in predicates
- **Instanceof support**: Query all implementations of interfaces
### HdumpPath Query Language
```
# Objects by class
objects/java.lang.String | top(10, shallow)
# Include subclasses
objects/instanceof/java.util.Map | groupBy(class)
# Filter with predicates
objects[shallow > 1MB and class ~ "com.myapp.*"]
# Class metadata
classes[instanceCount > 1000] | sortBy(instanceCount desc)
# GC roots
gcroots/THREAD_OBJ | select(type, object, threadSerial)
```
See **[doc/hdump-shell-quickstart.md](doc/hdump-shell-quickstart.md)** for quick start and **[doc/hdumppath.md](doc/hdumppath.md)** for complete reference.
## Documentation
### JFR Shell
- **[jfr-shell/README.md](jfr-shell/README.md)** - Interactive JFR analysis tool
- **[doc/cli/Architecture.md](doc/cli/Architecture.md)** - Architecture overview with diagrams
- **[doc/cli/Tutorial.md](doc/cli/Tutorial.md)** - Complete JFR Shell tutorial with event decoration
- **[doc/cli/Scripting.md](doc/cli/Scripting.md)** - Scripting guide: automate analysis workflows
- **[doc/cli/ScriptExecution.md](doc/cli/ScriptExecution.md)** - Script execution tutorial
- **[doc/cli/CommandRecording.md](doc/cli/CommandRecording.md)** - Command recording tutorial
- **[doc/cli/JFRPath.md](doc/cli/JFRPath.md)** - JfrPath query language reference
- **[doc/cli/Backends.md](doc/cli/Backends.md)** - Backend plugin guide and TCK (Technology Compatibility Kit)
- **[doc/cli/BackendQuickstart.md](doc/cli/BackendQuickstart.md)** - Build a custom backend in 10 minutes
### MCP Server
- **[jfr-mcp/README.md](jfr-mcp/README.md)** - MCP server overview and quick install
- **[doc/mcp/Tutorial.md](doc/mcp/Tutorial.md)** - Full MCP server tutorial
- **[doc/mcp/JBANGUsage.md](doc/mcp/JBANGUsage.md)** - JBang installation and usage
### Heap Dump Analysis
- **[doc/hdump-shell-quickstart.md](doc/hdump-shell-quickstart.md)** - Quick start guide for heap dump analysis
- **[doc/cli/hdump-shell-tutorial.md](doc/cli/hdump-shell-tutorial.md)** - Complete heap dump analysis tutorial
- **[doc/hdumppath.md](doc/hdumppath.md)** - HdumpPath query language reference
### General
- **[CHANGELOG.md](CHANGELOG.md)** - Version history and release notes
- **[LIMITATIONS.md](LIMITATIONS.md)** - Known limitations and workarounds
- **[PERFORMANCE.md](PERFORMANCE.md)** - Performance benchmarks and tuning tips
- **[CONTRIBUTING.md](CONTRIBUTING.md)** - How to contribute to JAFAR
- **[SECURITY.md](SECURITY.md)** - Security policy and vulnerability reporting
## Contributing
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
To report security vulnerabilities, see [SECURITY.md](SECURITY.md) (do not create public issues).
## License
Apache 2.0 (see `LICENSE`).