Skip to content

Reader Library

The condensed-data-reader artifact lets any Java application read .cjfr files without taking the full CLI, agent, or JMC classes as dependencies.

Coordinates: me.bechberger:condensed-data-reader:VERSION

Size: ~500 KB + lz4-java (~700 KB). No JMC, no femtocli, no agent code.

Requires: Java 17+


Maven dependency

<dependency>
    <groupId>me.bechberger</groupId>
    <artifactId>condensed-data-reader</artifactId>
    <version>0.1.1</version>
</dependency>

Quick start

Read a single file

import me.bechberger.cjfr.*;
import java.nio.file.Path;

try (CJFRFile f = CJFRFile.open(Path.of("recording.cjfr"))) {
    CJFREvent e;
    while ((e = f.readEvent()) != null) {
        System.out.println(e.getEventType().getName() + " @ " + e.getStartTime());
    }
}

Read multiple files in time order

import me.bechberger.cjfr.*;
import java.nio.file.*;
import java.util.List;

var paths = List.of(
    Path.of("app_0.cjfr"),
    Path.of("app_1.cjfr"),
    Path.of("app_2.cjfr"));

try (CJFRFiles files = CJFRFiles.open(paths)) {
    CJFREvent e;
    while ((e = files.readEvent()) != null) {
        if (e.getEventType().getName().equals("jdk.GarbageCollection")) {
            long pause = e.getLong("longestPause");
            System.out.printf("GC id=%d pause=%d ns%n", e.getInt("gcId"), pause);
        }
    }
}

API reference

CJFRFile

Single-file reader. Implements AutoCloseable.

Method Returns Notes
CJFRFile.open(Path) CJFRFile Opens a .cjfr file
CJFRFile.open(Path, Options) CJFRFile Opens with custom options
CJFRFile.open(InputStream) CJFRFile Opens from stream (no footer access)
readEvent() CJFREvent or null Next event; null at EOF
readAllEvents() List<CJFREvent> Reads all remaining events
hasMoreEvents() boolean Peeks ahead
getStartTime() Instant or null From footer or stream header
getEndTime() Instant or null From footer or stream header
getDuration() Duration Total recording duration
getFormatVersion() int Footer format version; -1 if absent
getGeneratorConfiguration() String "default", "lossless", etc.
getEventTypes() List<CJFREventType> Populated after reading
isTruncated() boolean true for in-progress recordings
getFooter() CJFRFooter or null Raw footer object

CJFRFiles

Multi-file reader. Merges files in time order. Implements AutoCloseable.

Method Returns Notes
CJFRFiles.open(List<Path>) CJFRFiles Opens and merges
CJFRFiles.open(List<Path>, Options) CJFRFiles Opens with custom options
readEvent() CJFREvent or null Next event in time order
readAllEvents() List<CJFREvent> All remaining events
getStartTime() Instant Earliest start
getEndTime() Instant Latest end
getDuration() Duration Total span

CJFREvent

A single event. Wraps the lazy-loading struct.

Method Notes
getEventType() Returns CJFREventType
getStartTime() Instant or null
getDuration() Duration or null
getString(fieldName) Field as string
getLong(fieldName) Field as long
getInt(fieldName) Field as int
getDouble(fieldName) Field as double
getBoolean(fieldName) Field as boolean
getInstant(fieldName) Timestamp field as Instant
getDuration(fieldName) Duration field as Duration
getStruct(fieldName) Nested struct as CJFREvent
getList(fieldName) Array field as List<?>
getValue(fieldName) Raw value; useful when type is unknown
hasField(fieldName) Check field existence
getFieldNames() All field names on this type
getRawStruct() Low-level ReadStruct for advanced access

CJFREventType

Event type metadata.

Method Notes
getName() Fully-qualified name, e.g. jdk.GarbageCollection
getLabel() Human-readable label from @Label, e.g. Garbage Collection
isExperimental() true if @jdk.jfr.Experimental is present
getFieldNames() Declared field names
getFields() List<CJFRFieldType> with name, label, type

Options

Immutable configuration. All methods return a new instance.

Builder method Notes
Options.defaults() Default options
.withReconstitution(boolean) Expand combined events (default: true)
.withEventFilter(Predicate<String>) Filter by event type name
.withEventTypes(Set<String>) Convenience for a fixed set
.skipIntegrityCheck() Skip CRC32 check (useful for in-progress files)

Field names

Field names match the JFR specification names. Use event.getFieldNames() to enumerate them at runtime, or cross-reference with jfr print --json output or the JDK Event Descriptions.

Common fields:

Field Type Appears on
startTime Instant Most events
duration Duration Duration events (GC, compilation, …)
gcId int GC events
cause String jdk.GarbageCollection
longestPause long (ns) jdk.GarbageCollection (G1/CMS)
sumOfPauses long (ns) jdk.GarbageCollection (G1/CMS)
heapUsed long (bytes) jdk.GCHeapSummary
heapSpace.reservedSize Nested jdk.GCHeapSummary
stackTrace Nested Sampling events
sampledThread Nested jdk.ExecutionSample

G1GC vs ZGC/Shenandoah

longestPause and sumOfPauses are G1GC-specific fields. For ZGC and Shenandoah, use duration on jdk.GarbageCollection instead.


Examples

Count GC events by cause

try (CJFRFile f = CJFRFile.open(path)) {
    var byCause = new java.util.HashMap<String, Integer>();
    CJFREvent e;
    while ((e = f.readEvent()) != null) {
        if (e.getEventType().getName().equals("jdk.GarbageCollection")) {
            String cause = e.getString("cause");
            byCause.merge(cause, 1, Integer::sum);
        }
    }
    byCause.forEach((cause, count) ->
        System.out.printf("%-30s %d%n", cause, count));
}

Filter to a specific event type

Options opts = Options.defaults().withEventTypes(Set.of("jdk.GarbageCollection"));
try (CJFRFile f = CJFRFile.open(path, opts)) {
    f.readAllEvents().forEach(e ->
        System.out.println("GC #" + e.getInt("gcId") + " cause=" + e.getString("cause")));
}

Access nested fields (stack trace)

CJFREvent exec = ...; // jdk.ExecutionSample
CJFREvent stackTrace = exec.getStruct("stackTrace");
if (stackTrace != null) {
    List<?> frames = stackTrace.getList("frames");
    if (frames != null) {
        // Each frame is a CJFREvent with method, lineNumber, etc.
        frames.forEach(frame -> {
            if (frame instanceof CJFREvent f) {
                System.out.println(f.getStruct("method"));
            }
        });
    }
}

Read recording metadata without iterating events

try (CJFRFile f = CJFRFile.open(path)) {
    // Footer provides instant metadata (no stream reads needed)
    var footer = f.getFooter();
    if (footer != null) {
        System.out.println("Duration: " + f.getDuration());
        System.out.println("Total events: " + footer.totalEvents());
        footer.eventCounts().forEach((type, count) ->
            System.out.printf("  %-40s %d%n", type, count));
    }
}

Format versions and compatibility

The reader handles all .cjfr format versions automatically. Use CJFRFile.getFormatVersion() to inspect the footer version (1 or 2; -1 for files without a footer). Older files without footers still stream correctly — metadata like start time is read from the stream header.


Thread safety

CJFRFile and CJFRFiles are not thread-safe. Create one instance per thread or use external synchronisation.