Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/.env.*
!.env.example
*.log
!tests/fixtures/*.log
41 changes: 13 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,34 @@
# JVMDelta

JVMDelta is a Rust project for comparing JVM performance measurements before and after a software change. The goal is to make regressions visible in local development and CI without requiring a JVM to analyze recorded data.

Performance evidence is often spread across GC logs and JFR recordings. Comparing runs by hand is slow and makes it easy to overlook a change in pause time or allocation. JVMDelta aims to ingest those recordings, compare a baseline with a candidate, and produce clear reports.
JVMDelta is a Rust project for comparing JVM performance measurements before and after a software change. GC logs and JFR recordings can contain evidence that is tedious to inspect by hand. JVMDelta aims to make those measurements accessible locally and in CI without needing a JVM for analysis.

## Current status

Day 1 is a foundation release. The `jvmdelta` executable provides `--help` and `--version`. It does **not** parse recordings, compare runs, detect regressions, or produce reports yet. Unsupported commands fail with a nonzero exit status.

## Architecture

- `crates/jvmdelta-core`: reusable library with small domain modules for GC logs, metrics, comparisons, regression rules, and reports. Its current types define vocabulary only.
- `crates/jvmdelta-cli`: Clap based executable. CLI presentation stays outside the core library.
- `tests/fixtures`: reserved for representative, licensed input files as parsers are implemented.
- `docs`: architecture, development guidance, and the implementation roadmap.
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).

See [architecture](docs/ARCHITECTURE.md) for the intended data flow and boundaries.
Comparison, regression detection, JFR parsing, JSON/HTML reports, and a GitHub Action are **planned**, not implemented.

## Planned features

- Parse JVM Unified GC logs and JFR recordings.
- Compute supported pause, memory, and allocation metrics.
- Compare baseline and candidate measurements and detect configured regressions.
- Render terminal, JSON, and HTML reports.
- Supply a GitHub Action for CI use.
## Architecture

These features are planned; no analysis command is available today.
- `crates/jvmdelta-core`: bounded streaming GC parser and small domain modules for future metrics, comparison, regression, and reports.
- `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.
- `docs`: [architecture](docs/ARCHITECTURE.md), [format support](docs/GC_LOG_SUPPORT.md), [development](docs/DEVELOPMENT.md), and [roadmap](docs/ROADMAP.md).

## Build and use

Requires stable Rust 1.85 or newer. No JVM is needed for the current CLI.
Requires stable Rust 1.85 or newer. Java is not required to build, test, or inspect a log.

```sh
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
```

The installed binary is named `jvmdelta`; for example, `jvmdelta --help`.
The installed executable is named `jvmdelta`. `gc inspect` uses strict parsing: unreadable files and malformed supported summaries exit nonzero with an error on stderr.

## Development
## Development and contributing

```sh
cargo fmt --all -- --check
Expand All @@ -48,8 +37,4 @@ cargo test --workspace
cargo build --workspace --release
```

See [development guidance](docs/DEVELOPMENT.md) for test and contribution practices.

## Contributing

Issues and focused pull requests are welcome. Discuss input format choices and regression semantics before implementing a parser or detector. Add representative fixtures and tests for behavior, keep parser errors explicit, and run the checks above. Contributions are licensed under [Apache-2.0](LICENSE).
Focused issues and pull requests are welcome. Discuss new GC variants or comparison semantics before extending support. Add representative, redistributable fixtures and tests, keep errors explicit, and document limitations. Contributions are licensed under [Apache-2.0](LICENSE).
86 changes: 76 additions & 10 deletions crates/jvmdelta-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,81 @@
use clap::Parser;
use clap::{Parser, Subcommand};
use jvmdelta_core::gc_log::{EventKind, GcLogParser, Record, SkipReason};
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;

/// JVM performance regression analysis (analysis commands are planned).
/// Inspect JVM GC logs. Comparison and regression analysis are planned.
#[derive(Debug, Parser)]
#[command(
name = "jvmdelta",
version,
about = "JVM performance regression analysis; analysis commands are planned",
long_about = None
)]
struct Cli {}
#[command(name = "jvmdelta", version, about = "Inspect JVM GC logs; comparison and regression analysis are planned", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}

#[derive(Debug, Subcommand)]
enum Command {
/// Inspect supported G1 Unified GC pause summaries.
Gc {
#[command(subcommand)]
command: GcCommand,
},
}

#[derive(Debug, Subcommand)]
enum GcCommand {
/// Count and classify pause summaries in a GC log.
Inspect {
/// Path to a UTF-8 JVM Unified GC log.
path: PathBuf,
},
}

fn main() {
Cli::parse();
if let Err(error) = run() {
eprintln!("jvmdelta: {error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match cli.command {
Some(Command::Gc {
command: GcCommand::Inspect { path },
}) => inspect(path),
None => Ok(()),
}
}

fn inspect(path: PathBuf) -> Result<(), Box<dyn Error>> {
let file =
File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?;
let mut counts = [0_u64; 4];
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::Skipped(SkipReason::Unrelated) => skipped += 1,
Record::Skipped(SkipReason::UnsupportedEvent) => unsupported += 1,
}
}
println!("File: {}", path.display());
println!("Supported events: {}", counts.iter().sum::<u64>());
println!("Young: {}", counts[0]);
println!("Full: {}", counts[1]);
println!("Remark: {}", counts[2]);
println!("Cleanup: {}", counts[3]);
println!("Unrelated entries skipped: {skipped}");
println!("Unsupported pause types skipped: {unsupported}");
Ok(())
}
56 changes: 53 additions & 3 deletions crates/jvmdelta-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ fn help_describes_current_scope() {
let output = run("--help");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("JVM performance regression analysis"));
assert!(stdout.contains("analysis commands are planned"));
assert!(stdout.contains("Inspect JVM GC logs"));
assert!(stdout.contains("comparison and regression analysis are planned"));
assert!(stdout.contains("gc"));
assert!(stdout.contains("--version"));
}

Expand All @@ -32,5 +33,54 @@ fn version_reports_package_version() {
fn unsupported_command_fails() {
let output = run("compare");
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("unexpected argument"));
assert!(String::from_utf8_lossy(&output.stderr).contains("unrecognized subcommand"));
}

fn inspect(path: &str) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_jvmdelta"))
.args(["gc", "inspect", path])
.output()
.unwrap_or_else(|error| panic!("failed to run CLI: {error}"))
}

#[test]
fn inspect_valid_and_empty_files() {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/fixtures/");
let output = inspect(&format!("{root}jdk25-g1-real.log"));
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Supported events: 2"));
assert!(stdout.contains("Young: 1"));
assert!(stdout.contains("Full: 1"));
assert!(stdout.contains("Unrelated entries skipped: 4"));

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"));
std::fs::remove_file(empty).expect("remove temp file");
}

#[test]
fn inspect_rejects_missing_and_malformed_files() {
let missing = tempfile_path("missing");
let output = inspect(missing.to_str().expect("UTF-8 path"));
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("cannot open"));
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/malformed-g1-synthetic.log"
);
let output = inspect(path);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("line 2"));
}

fn tempfile_path(suffix: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("jvmdelta-cli-{}-{suffix}.log", std::process::id()))
}
Loading
Loading