From eafa12e611faad579313f1a094d109cd0445b3d6 Mon Sep 17 00:00:00 2001 From: neonjava Date: Wed, 14 Oct 2026 00:47:30 +0530 Subject: [PATCH 1/2] feat: aggregate GC pause counts and exact durations --- crates/jvmdelta-cli/src/main.rs | 55 ++++-- crates/jvmdelta-cli/tests/cli.rs | 25 +++ crates/jvmdelta-core/src/metrics.rs | 157 +++++++++++++++++- crates/jvmdelta-core/tests/metrics_summary.rs | 78 +++++++++ tests/fixtures/JvmDeltaGcFixture.java | 9 + tests/fixtures/jdk17-g1-real.log | 5 + tests/fixtures/jdk21-g1-real.log | 5 + 7 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 crates/jvmdelta-core/tests/metrics_summary.rs create mode 100644 tests/fixtures/JvmDeltaGcFixture.java create mode 100644 tests/fixtures/jdk17-g1-real.log create mode 100644 tests/fixtures/jdk21-g1-real.log diff --git a/crates/jvmdelta-cli/src/main.rs b/crates/jvmdelta-cli/src/main.rs index c28968c..e6af686 100644 --- a/crates/jvmdelta-cli/src/main.rs +++ b/crates/jvmdelta-cli/src/main.rs @@ -1,5 +1,6 @@ use clap::{Parser, Subcommand}; use jvmdelta_core::gc_log::{EventKind, GcLogParser, Record, SkipReason}; +use jvmdelta_core::metrics::{GcSummary, PauseStats}; use std::error::Error; use std::fs::File; use std::io::BufReader; @@ -51,31 +52,55 @@ fn run() -> Result<(), Box> { fn inspect(path: PathBuf) -> Result<(), Box> { let file = File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?; - let mut counts = [0_u64; 4]; + let mut summary = GcSummary::default(); let mut skipped = 0_u64; let mut unsupported = 0_u64; for record in GcLogParser::new(BufReader::new(file)) { match record? { - Record::Event(event) => { - let index = match event.kind { - EventKind::Young => 0, - EventKind::Full => 1, - EventKind::Remark => 2, - EventKind::Cleanup => 3, - }; - counts[index] += 1; - } + Record::Event(event) => summary.record(&event)?, Record::Skipped(SkipReason::Unrelated) => skipped += 1, Record::Skipped(SkipReason::UnsupportedEvent) => unsupported += 1, } } println!("File: {}", path.display()); - println!("Supported events: {}", counts.iter().sum::()); - println!("Young: {}", counts[0]); - println!("Full: {}", counts[1]); - println!("Remark: {}", counts[2]); - println!("Cleanup: {}", counts[3]); + println!("Supported events: {}", summary.all().count()); + for (name, kind) in categories() { + println!("{name}: {}", summary.category(kind).count()); + } println!("Unrelated entries skipped: {skipped}"); println!("Unsupported pause types skipped: {unsupported}"); + println!("Pause durations (ms; mean rounded down to 1 ns):"); + print_duration_stats("All", summary.all()); + for (name, kind) in categories() { + print_duration_stats(name, summary.category(kind)); + } Ok(()) } + +fn categories() -> [(&'static str, EventKind); 4] { + [ + ("Young", EventKind::Young), + ("Full", EventKind::Full), + ("Remark", EventKind::Remark), + ("Cleanup", EventKind::Cleanup), + ] +} + +fn print_duration_stats(name: &str, stats: &PauseStats) { + let min = stats.min_ns().map(|value| format_ms(u128::from(value))); + let mean = stats + .mean_ns_floor() + .map(|value| format_ms(u128::from(value))); + let max = stats.max_ns().map(|value| format_ms(u128::from(value))); + println!( + " {name}: total={} min={} mean={} max={}", + format_ms(stats.total_ns()), + min.as_deref().unwrap_or("n/a"), + mean.as_deref().unwrap_or("n/a"), + max.as_deref().unwrap_or("n/a") + ); +} + +fn format_ms(ns: u128) -> String { + format!("{}.{:06}", ns / 1_000_000, ns % 1_000_000) +} diff --git a/crates/jvmdelta-cli/tests/cli.rs b/crates/jvmdelta-cli/tests/cli.rs index ddc2636..d3dddd5 100644 --- a/crates/jvmdelta-cli/tests/cli.rs +++ b/crates/jvmdelta-cli/tests/cli.rs @@ -57,12 +57,18 @@ fn inspect_valid_and_empty_files() { assert!(stdout.contains("Young: 1")); assert!(stdout.contains("Full: 1")); assert!(stdout.contains("Unrelated entries skipped: 4")); + assert!(stdout.contains("All: total=22.970000 min=10.235000 mean=11.485000 max=12.735000")); + assert!(stdout.contains("Remark: total=0.000000 min=n/a mean=n/a max=n/a")); let empty = tempfile_path("empty"); std::fs::write(&empty, "").expect("write empty file"); let output = inspect(empty.to_str().expect("UTF-8 path")); assert!(output.status.success()); assert!(String::from_utf8_lossy(&output.stdout).contains("Supported events: 0")); + assert!( + String::from_utf8_lossy(&output.stdout) + .contains("All: total=0.000000 min=n/a mean=n/a max=n/a") + ); std::fs::remove_file(empty).expect("remove temp file"); } @@ -81,6 +87,25 @@ fn inspect_rejects_missing_and_malformed_files() { assert!(String::from_utf8_lossy(&output.stderr).contains("line 2")); } +#[test] +fn inspect_real_jdk17_and_21_excerpts() { + for version in [17, 21] { + let path = format!( + "{}/../../tests/fixtures/jdk{version}-g1-real.log", + env!("CARGO_MANIFEST_DIR") + ); + let output = inspect(&path); + assert!( + output.status.success(), + "JDK {version}: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Supported events: 2")); + assert!(stdout.contains("Unrelated entries skipped: 3")); + } +} + fn tempfile_path(suffix: &str) -> std::path::PathBuf { std::env::temp_dir().join(format!("jvmdelta-cli-{}-{suffix}.log", std::process::id())) } diff --git a/crates/jvmdelta-core/src/metrics.rs b/crates/jvmdelta-core/src/metrics.rs index 277d653..37c37de 100644 --- a/crates/jvmdelta-core/src/metrics.rs +++ b/crates/jvmdelta-core/src/metrics.rs @@ -1,9 +1,162 @@ -//! Performance measurement definitions. +//! Exact, incremental measurements over validated GC pause events. -/// Units supported by future measurements. +use crate::gc_log::{EventKind, GcEvent}; +use std::fmt; + +/// Units used by core measurements. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Unit { Nanoseconds, Bytes, Count, } + +/// Counts and duration statistics for pause summary records. +/// +/// `count` counts records, not distinct GC IDs or collection cycles. `total_ns` +/// uses `u128` so it can represent sums larger than a single `u64` duration. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct PauseStats { + count: u64, + total_ns: u128, + min_ns: Option, + max_ns: Option, +} + +impl PauseStats { + /// Number of pause summary records observed. + pub fn count(&self) -> u64 { + self.count + } + + /// Exact sum of pause durations in nanoseconds; zero when empty. + pub fn total_ns(&self) -> u128 { + self.total_ns + } + + /// Shortest pause in nanoseconds, or `None` when empty. + pub fn min_ns(&self) -> Option { + self.min_ns + } + + /// Longest pause in nanoseconds, or `None` when empty. + pub fn max_ns(&self) -> Option { + self.max_ns + } + + /// Arithmetic mean, rounded down to a whole nanosecond; `None` when empty. + pub fn mean_ns_floor(&self) -> Option { + if self.count == 0 { + return None; + } + // A mean of u64 inputs cannot exceed u64::MAX. + u64::try_from(self.total_ns / u128::from(self.count)).ok() + } + + fn with_duration(self, duration_ns: u64) -> Result { + let count = self + .count + .checked_add(1) + .ok_or(SummaryError::CountOverflow)?; + let total_ns = self + .total_ns + .checked_add(u128::from(duration_ns)) + .ok_or(SummaryError::DurationOverflow)?; + Ok(Self { + count, + total_ns, + min_ns: Some( + self.min_ns + .map_or(duration_ns, |value| value.min(duration_ns)), + ), + max_ns: Some( + self.max_ns + .map_or(duration_ns, |value| value.max(duration_ns)), + ), + }) + } +} + +/// Exact pause summaries for all events and each supported category. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct GcSummary { + all: PauseStats, + young: PauseStats, + full: PauseStats, + remark: PauseStats, + cleanup: PauseStats, +} + +impl GcSummary { + /// Add one validated GC pause event. If an overflow occurs, the summary is unchanged. + pub fn record(&mut self, event: &GcEvent) -> Result<(), SummaryError> { + let next_all = self.all.with_duration(event.duration_ns)?; + let category = match event.kind { + EventKind::Young => &mut self.young, + EventKind::Full => &mut self.full, + EventKind::Remark => &mut self.remark, + EventKind::Cleanup => &mut self.cleanup, + }; + let next_category = category.with_duration(event.duration_ns)?; + self.all = next_all; + *category = next_category; + Ok(()) + } + + /// Statistics over every supported pause summary record. + pub fn all(&self) -> &PauseStats { + &self.all + } + + /// Statistics for one supported pause category. + pub fn category(&self, kind: EventKind) -> &PauseStats { + match kind { + EventKind::Young => &self.young, + EventKind::Full => &self.full, + EventKind::Remark => &self.remark, + EventKind::Cleanup => &self.cleanup, + } + } +} + +/// An arithmetic limit was reached while accumulating events. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SummaryError { + CountOverflow, + DurationOverflow, +} + +impl fmt::Display for SummaryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CountOverflow => write!(f, "GC pause count exceeds u64"), + Self::DurationOverflow => write!(f, "total GC pause duration exceeds u128 nanoseconds"), + } + } +} + +impl std::error::Error for SummaryError {} + +#[cfg(test)] +mod tests { + use super::{PauseStats, SummaryError}; + + #[test] + fn count_overflow_is_explicit() { + let stats = PauseStats { + count: u64::MAX, + ..PauseStats::default() + }; + assert_eq!(stats.with_duration(1), Err(SummaryError::CountOverflow)); + } + + #[test] + fn total_overflow_is_explicit() { + let stats = PauseStats { + count: 1, + total_ns: u128::MAX, + ..PauseStats::default() + }; + assert_eq!(stats.with_duration(1), Err(SummaryError::DurationOverflow)); + } +} diff --git a/crates/jvmdelta-core/tests/metrics_summary.rs b/crates/jvmdelta-core/tests/metrics_summary.rs new file mode 100644 index 0000000..e887263 --- /dev/null +++ b/crates/jvmdelta-core/tests/metrics_summary.rs @@ -0,0 +1,78 @@ +use jvmdelta_core::gc_log::{EventKind, GcEvent, GcLogParser, Record}; +use jvmdelta_core::metrics::GcSummary; +use std::io::Cursor; + +fn summarize(input: &str) -> GcSummary { + let mut summary = GcSummary::default(); + for record in GcLogParser::new(Cursor::new(input)) { + if let Record::Event(event) = record.expect("fixture parses") { + summary.record(&event).expect("sample fits in summary"); + } + } + summary +} + +#[test] +fn empty_summary_has_no_extrema_or_mean() { + let summary = summarize(""); + assert_eq!(summary.all().count(), 0); + assert_eq!(summary.all().total_ns(), 0); + assert_eq!(summary.all().min_ns(), None); + assert_eq!(summary.all().max_ns(), None); + assert_eq!(summary.all().mean_ns_floor(), None); + assert_eq!(summary.category(EventKind::Full).mean_ns_floor(), None); +} + +#[test] +fn category_and_overall_durations_are_exact() { + let input = include_str!("../../../tests/fixtures/jdk17-g1-synthetic.log"); + let summary = summarize(input); + assert_eq!(summary.all().count(), 4); + assert_eq!(summary.all().total_ns(), 10_877_000); + assert_eq!(summary.all().min_ns(), Some(50_000)); + assert_eq!(summary.all().max_ns(), Some(6_250_000)); + assert_eq!(summary.all().mean_ns_floor(), Some(2_719_250)); + assert_eq!(summary.category(EventKind::Young).count(), 1); + assert_eq!(summary.category(EventKind::Young).total_ns(), 3_452_000); + assert_eq!(summary.category(EventKind::Full).total_ns(), 6_250_000); + assert_eq!(summary.category(EventKind::Remark).total_ns(), 1_125_000); + assert_eq!(summary.category(EventKind::Cleanup).total_ns(), 50_000); +} + +#[test] +fn mean_rounds_down_to_nanosecond_and_ids_are_not_deduplicated() { + let mut summary = GcSummary::default(); + for duration in [1, 2] { + summary + .record(&GcEvent { + source_line: 1, + gc_id: 7, + kind: EventKind::Remark, + description: String::new(), + heap: None, + duration_ns: duration, + uptime_ns: None, + source: String::new(), + }) + .expect("fits"); + } + assert_eq!(summary.category(EventKind::Remark).count(), 2); + assert_eq!(summary.category(EventKind::Remark).total_ns(), 3); + assert_eq!(summary.category(EventKind::Remark).mean_ns_floor(), Some(1)); + assert_eq!(summary.category(EventKind::Full).min_ns(), None); +} + +#[test] +fn real_jdk17_and_21_samples_parse_and_aggregate() { + let jdk17 = summarize(include_str!("../../../tests/fixtures/jdk17-g1-real.log")); + assert_eq!(jdk17.all().count(), 2); + assert_eq!(jdk17.category(EventKind::Young).total_ns(), 1_420_000); + assert_eq!(jdk17.category(EventKind::Full).total_ns(), 2_327_000); + assert_eq!(jdk17.all().total_ns(), 3_747_000); + + let jdk21 = summarize(include_str!("../../../tests/fixtures/jdk21-g1-real.log")); + assert_eq!(jdk21.all().count(), 2); + assert_eq!(jdk21.category(EventKind::Young).total_ns(), 1_456_000); + assert_eq!(jdk21.category(EventKind::Full).total_ns(), 3_257_000); + assert_eq!(jdk21.all().total_ns(), 4_713_000); +} diff --git a/tests/fixtures/JvmDeltaGcFixture.java b/tests/fixtures/JvmDeltaGcFixture.java new file mode 100644 index 0000000..0c15d03 --- /dev/null +++ b/tests/fixtures/JvmDeltaGcFixture.java @@ -0,0 +1,9 @@ +public class JvmDeltaGcFixture { + public static void main(String[] args) { + for (int i = 0; i < 100; i++) { + byte[] bytes = new byte[1024 * 1024]; + bytes[0] = 1; + } + System.gc(); + } +} diff --git a/tests/fixtures/jdk17-g1-real.log b/tests/fixtures/jdk17-g1-real.log new file mode 100644 index 0000000..6470f1d --- /dev/null +++ b/tests/fixtures/jdk17-g1-real.log @@ -0,0 +1,5 @@ +[0.028s][info][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) +[0.029s][info][gc,heap ] GC(0) Eden regions: 1->0(10) +[0.029s][info][gc ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 13M->1M(32M) 1.420ms +[0.044s][info][gc,start ] GC(32) Pause Full (System.gc()) +[0.047s][info][gc ] GC(32) Pause Full (System.gc()) 7M->0M(32M) 2.327ms diff --git a/tests/fixtures/jdk21-g1-real.log b/tests/fixtures/jdk21-g1-real.log new file mode 100644 index 0000000..6e0aeb8 --- /dev/null +++ b/tests/fixtures/jdk21-g1-real.log @@ -0,0 +1,5 @@ +[0.038s][info][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) +[0.039s][info][gc,heap ] GC(0) Eden regions: 1->0(13) +[0.039s][info][gc ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 15M->1M(32M) 1.456ms +[0.058s][info][gc,start ] GC(28) Pause Full (System.gc()) +[0.061s][info][gc ] GC(28) Pause Full (System.gc()) 5M->1M(32M) 3.257ms From a1eb4e3ba004b24d8dd8f7d3d8d250b54f89b22a Mon Sep 17 00:00:00 2001 From: neonjava Date: Wed, 14 Oct 2026 00:47:36 +0530 Subject: [PATCH 2/2] docs: record JDK 17 and 21 validation and metric semantics --- README.md | 7 ++++--- docs/ARCHITECTURE.md | 6 +++--- docs/DEVELOPMENT.md | 6 ++++-- docs/GC_LOG_SUPPORT.md | 4 ++-- docs/ROADMAP.md | 6 +++--- tests/fixtures/README.md | 36 +++++++++++++++++------------------- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 88715d0..3d7e6c5 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ JVMDelta is a Rust project for comparing JVM performance measurements before and ## Current status -The current CLI inspects a narrow subset of G1 Unified GC pause summaries. It counts supported young, full, remark, and cleanup pauses, unrelated entries, and unknown pause categories. The core library exposes parsed events with exact units and source-line provenance. Invalid supported summaries fail with a line-numbered error. See [supported syntax and limitations](docs/GC_LOG_SUPPORT.md). +The current CLI inspects a narrow subset of G1 Unified GC pause summaries. It counts supported young, full, remark, and cleanup pauses, unrelated entries, and unknown pause categories. It also reports exact total, minimum, maximum, and nanosecond-rounded-down mean pause durations per category and overall. The core library exposes parsed events with exact units and source-line provenance. Invalid supported summaries fail with a line-numbered error. See [supported syntax and limitations](docs/GC_LOG_SUPPORT.md). Comparison, regression detection, JFR parsing, JSON/HTML reports, and a GitHub Action are **planned**, not implemented. ## Architecture -- `crates/jvmdelta-core`: bounded streaming GC parser and small domain modules for future metrics, comparison, regression, and reports. +- `crates/jvmdelta-core`: bounded streaming GC parser and constant-size pause statistics; small domain modules reserve comparison, regression, and report concepts. - `crates/jvmdelta-cli`: Clap executable, file access, counts, and exit status. -- `tests/fixtures`: synthetic JDK 17/21-style examples and selected real JDK 25 G1 output. +- `tests/fixtures`: synthetic edge cases and selected real Temurin 17/21 and OpenJDK 25 G1 output. - `docs`: [architecture](docs/ARCHITECTURE.md), [format support](docs/GC_LOG_SUPPORT.md), [development](docs/DEVELOPMENT.md), and [roadmap](docs/ROADMAP.md). ## Build and use @@ -24,6 +24,7 @@ cargo build --workspace cargo run -p jvmdelta-cli -- --help cargo run -p jvmdelta-cli -- --version cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk25-g1-real.log +cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk17-g1-real.log ``` The installed executable is named `jvmdelta`. `gc inspect` uses strict parsing: unreadable files and malformed supported summaries exit nonzero with an error on stderr. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 89a7545..e918109 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -9,11 +9,11 @@ UTF-8 G1 Unified GC log ↓ bounded line reader (64 KiB per line) validated pause event / explicit skip reason / structured error ↓ -CLI category counts or SDK caller processing +constant-size pause statistics or SDK caller processing ``` -`GcLogParser` yields records one at a time, so SDK users can aggregate or store only what they need. An event retains its source line and original line, GC ID, pause category, description, optional heap values in bytes, duration in nanoseconds, and optional uptime in nanoseconds. The CLI stops at the first error. SDK users may continue after malformed candidate lines, but terminal errors end iteration. +`GcLogParser` yields records one at a time, so SDK users can aggregate or store only what they need. An event retains its source line and original line, GC ID, pause category, description, optional heap values in bytes, duration in nanoseconds, and optional uptime in nanoseconds. `GcSummary` consumes borrowed events, retaining only category and overall counts, exact `u128` nanosecond totals, and optional `u64` extrema. Its mean is integer division of total by count, rounded down to one nanosecond. Recording is atomic on overflow. The CLI stops at the first parser error. SDK users may continue after malformed candidate lines, but terminal errors end iteration. -The comparison, metrics, regression, and report modules remain small domain definitions for later work. They do not calculate results. The intended later flow is measurements → baseline/candidate comparison → regression decisions → report model → output renderer. Metric definitions must state units and aggregation rules; comparison must avoid implying statistical significance from one sample. +The comparison, regression, and report modules remain small domain definitions for later work. They do not calculate results. The intended later flow is measurements → baseline/candidate comparison → regression decisions → report model → output renderer. Comparison must avoid implying statistical significance from one sample. See [GC log support](GC_LOG_SUPPORT.md) for the exact accepted syntax and limitations. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 1dd5ca3..aa7bfcc 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -11,10 +11,12 @@ cargo test --workspace cargo build --workspace --release ``` -Run the CLI against the real captured fixture: +Run the CLI against real captured fixtures: ```sh cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk25-g1-real.log +cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk17-g1-real.log +cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk21-g1-real.log ``` -Keep core logic independent of CLI formatting. Add unit tests for parser behavior and CLI integration tests for exit codes and output. Record each fixture's origin, JDK version, collector, and reproduction procedure in `tests/fixtures/README.md`. Do not commit sensitive recordings, credentials, or build artifacts. See [format support](GC_LOG_SUPPORT.md) before extending the parser. +Keep core logic independent of CLI formatting. Add unit tests for parser and aggregate behavior and CLI integration tests for exit codes and output. Record each fixture's origin, JDK version, collector, and reproduction procedure in `tests/fixtures/README.md`. Rust tests must not require Java. Do not commit sensitive recordings, credentials, or build artifacts. See [format support](GC_LOG_SUPPORT.md) before extending the parser. diff --git a/docs/GC_LOG_SUPPORT.md b/docs/GC_LOG_SUPPORT.md index a97c302..fbab459 100644 --- a/docs/GC_LOG_SUPPORT.md +++ b/docs/GC_LOG_SUPPORT.md @@ -1,6 +1,6 @@ # G1 Unified GC log support -JVMDelta parses a deliberately narrow subset of HotSpot Unified Logging as emitted by G1 on JDK 17 and 21. The JDK 17 and 21 fixtures are **synthetic examples** modeled on documented syntax; they have not been validated against those local JVM versions. A separate small JDK 25 fixture was captured locally and checks the same syntax. The parser does not identify a JVM version or independently verify the collector from a log; callers should supply G1 logs. +JVMDelta parses a deliberately narrow subset of HotSpot Unified Logging as emitted by G1 on JDK 17 and 21. Real Temurin 17.0.20.1 and 21.0.12.1 excerpts now validate young and full summaries; synthetic fixtures cover additional syntax and failures. A separate JDK 25 fixture checks the same syntax. The parser does not identify a JVM version or independently verify the collector from a log; callers should supply G1 logs. Authoritative references: @@ -33,7 +33,7 @@ The event has ID 0, uptime 123,000,000 ns, heap 20/8/128 MiB expressed as bytes, cargo run -p jvmdelta-cli -- gc inspect tests/fixtures/jdk25-g1-real.log ``` -The command prints supported event totals, counts for each category, unrelated entries skipped, and unsupported pause types skipped. It does not compute regression scores. +The command prints supported pause totals and counts for each category, unrelated entries skipped, unsupported pause types skipped, and duration statistics in milliseconds. For every category and overall, total/minimum/maximum are exact. Mean is rounded down to one nanosecond before display. An empty category has total `0.000000` ms and `n/a` for minimum, mean, and maximum. Counts refer to pause summary records, not distinct GC IDs or collection cycles. The CLI does not compute regression scores. ## Fixtures diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6a4fe80..77d11df 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -8,13 +8,13 @@ Cargo workspace, CLI foundation, documentation, Apache-2.0 license, and CI. A bounded streaming parser for selected G1 Unified GC pause summaries, typed events with exact units and source provenance, explicit errors and skip reasons, real and synthetic fixtures, and `gc inspect`. See [format support](GC_LOG_SUPPORT.md). -## Day 3 — measurements and broader fixture validation +## Completed — Day 3 -Validate the supported syntax against local JDK 17 and 21 output when available. Add carefully defined pause duration summaries and GC counts to the core, with tests for aggregation and missing data. Consider explicit support for additional decorators and duration units only after fixture evidence. +Validated young and full pause summaries against local Temurin 17 and 21 G1 output. Added constant-size category and overall pause counts and exact duration statistics, with explicit missing-data behavior and CLI output. Real fixture excerpts and reproduction instructions are checked in. Remark/cleanup examples remain synthetic. ## Day 4 — comparison semantics -Define baseline/candidate data sets, zero-baseline behavior, and missing-metric behavior. Implement deterministic comparisons without implying significance from a single run. +Define baseline/candidate data sets, zero-baseline behavior, and missing-metric behavior. Implement deterministic comparisons without implying significance from a single run. Decide how to handle unequal run duration and event counts before showing deltas. ## Day 5 — regression policy and CLI diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 159d62f..6c2c44c 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -1,28 +1,26 @@ # GC log fixtures -- `jdk17-g1-synthetic.log`: synthetic JDK 17-style G1 examples: young, full, remark, cleanup, and a `gc,start` line. Created for parser tests; not captured from JDK 17. -- `jdk21-g1-synthetic.log`: synthetic JDK 21-style G1 examples with wall-clock and uptime decorations, K/G units, and a detail record. Created for parser tests; not captured from JDK 21. -- `malformed-g1-synthetic.log`: synthetic malformed size-unit case. -- `jdk25-g1-real.log`: selected unchanged lines from a local OpenJDK 25.0.3 G1 run on Fedora. It contains one young summary, one full summary, a start record, and three heap detail records. Pause timings and heap quantities are specific to that run; do not compare them as performance evidence. +The Rust test suite reads these text files and does not need Java. -To reproduce a similar JDK 25 log, save this as `/tmp/JvmDeltaGcFixture.java`: +- `jdk17-g1-real.log`: five selected, unchanged lines from Eclipse Temurin 17.0.20.1+1 G1 output: young start, heap detail, young summary, full start, and full summary. +- `jdk21-g1-real.log`: the same categories from Eclipse Temurin 21.0.12.1+1 G1 output. +- `jdk25-g1-real.log`: selected, unchanged lines from a local Fedora OpenJDK 25.0.3 G1 run. +- `jdk17-g1-synthetic.log`: synthetic edge-case examples for young, full, remark, cleanup, and `gc,start`. It is not a recording from JDK 17. +- `jdk21-g1-synthetic.log`: synthetic wall-clock decoration, K/G unit, and GC detail examples. It is not a recording from JDK 21. +- `malformed-g1-synthetic.log`: synthetic invalid size-unit case. -```java -public class JvmDeltaGcFixture { - public static void main(String[] args) { - for (int i = 0; i < 100; i++) { - byte[] bytes = new byte[1024 * 1024]; - bytes[0] = 1; - } - System.gc(); - } -} -``` +The real 17/21 excerpts were generated on 2026-10-14 using the checked-in [`JvmDeltaGcFixture.java`](JvmDeltaGcFixture.java) allocation workload. The Temurin JRE archives were downloaded to `/tmp` from the [Adoptium binary API](https://adoptium.net/installation/ci-scripts/) and verified against the API's SHA-256 metadata. The archive SHA-256 values were: + +- Temurin 17 JRE: `0b2b640e3046b64c8ec504de0ab9d91bb5610182bda21fad454681ce54d45a62` +- Temurin 21 JRE: `2413149700df0f7d440500a84a8f764c535f21e5a5e87d38328b64eec2c5b500` -Then run: +A local JDK 17 or 21 can reproduce comparable output without changing the system configuration. From the repository root, replace `JAVA17_HOME` and `JAVA21_HOME` with local paths: ```sh -java -Xms32m -Xmx32m -XX:+UseG1GC '-Xlog:gc*:file=/tmp/jvmdelta-gc.log:uptime,level,tags' /tmp/JvmDeltaGcFixture.java +mkdir -p /tmp/jvmdelta-fixture-classes +javac --release 17 -d /tmp/jvmdelta-fixture-classes tests/fixtures/JvmDeltaGcFixture.java +"$JAVA17_HOME/bin/java" -Xms32m -Xmx32m -XX:+UseG1GC '-Xlog:gc*:file=/tmp/jvmdelta-jdk17-gc.log:uptime,level,tags' -cp /tmp/jvmdelta-fixture-classes JvmDeltaGcFixture +"$JAVA21_HOME/bin/java" -Xms32m -Xmx32m -XX:+UseG1GC '-Xlog:gc*:file=/tmp/jvmdelta-jdk21-gc.log:uptime,level,tags' -cp /tmp/jvmdelta-fixture-classes JvmDeltaGcFixture ``` -The exact event IDs, heap values, and timing vary between runs. The fixture has no application data or personal paths. +A JDK 17 or newer compiler can compile the workload. The archive used for validation provided JREs; the installed JDK 25 compiler built the source with `--release 17`. Exact event IDs, heap values, and timings vary between runs. The fixtures contain no application data or personal paths and must not be interpreted as performance benchmarks.