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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chaintools"
version = "0.0.11"
version = "0.0.12"
edition = "2024"
authors = ["alejandrogzi <alejandrxgzi@gmail.com>"]
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</span>

<p align="center">
<a href="https://img.shields.io/badge/version-0.0.11-green" target="_blank">
<img alt="Version Badge" src="https://img.shields.io/badge/version-0.0.11-green">
<a href="https://img.shields.io/badge/version-0.0.12-green" target="_blank">
<img alt="Version Badge" src="https://img.shields.io/badge/version-0.0.12-green">
</a>
<a href="https://crates.io/crates/chaintools" target="_blank">
<img alt="Crates.io Version" src="https://img.shields.io/crates/v/chaintools">
Expand All @@ -29,7 +29,7 @@
<span>work with .chain files in Rust</span>
<br>
<br>
<a href="https://docs.rs/chaintools/0.0.11/chaintools/">docs</a> .
<a href="https://docs.rs/chaintools/0.0.12/chaintools/">docs</a> .
<a href="https://github.com/alejandrogzi/chaintools/tree/master/assets/usage/usage.md">usage</a> .
<a href="https://github.com/alejandrogzi/chaintools/tree/master/assets/tools">tools</a> .
<a href="https://genome.ucsc.edu/goldenpath/help/chain.html">chains</a>
Expand Down Expand Up @@ -60,7 +60,7 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
chaintools = { version = "0.0.11", features = ["mmap", "gzip", "parallel"] }
chaintools = { version = "0.0.12", features = ["mmap", "gzip", "parallel"] }
```

## Benchmarks
Expand Down
19 changes: 19 additions & 0 deletions assets/changelog/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to **chaintools** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.12] - 2026-08-10

### Added

- **`compare --memory-ceiling <GIB>` / `-M`** — the in-memory ceiling for
canonical mappings is now configurable per run instead of a fixed
constant; the ceiling value is reported in the error when exceeded.

### Changed

- **`compare` default memory ceiling raised from 8 to 16 GiB** — the
canonical-mapping estimate is checked against the configurable ceiling
(default 16 GiB) before canonicalization, so larger inputs pass by
default while oversized ones still fail cleanly instead of OOMing.
Per-key external sorting remains the planned upgrade path for inputs
beyond the ceiling.

## [0.0.11] - 2026-08-09

### Added
Expand Down Expand Up @@ -320,6 +337,8 @@ First stable release.
- Automatic gzip (`.chain.gz`) detection and decompression (`gzip` feature).
- Test suite, benchmark binary, CI workflows, Docker image, and rustdoc documentation.

[0.0.12]: https://github.com/alejandrogzi/chaintools/releases/tag/v0.0.12
[0.0.11]: https://github.com/alejandrogzi/chaintools/releases/tag/v0.0.11
[0.0.10]: https://github.com/alejandrogzi/chaintools/releases/tag/v0.0.10
[0.0.9]: https://github.com/alejandrogzi/chaintools/releases/tag/v0.0.9
[0.0.8]: https://github.com/alejandrogzi/chaintools/releases/tag/v0.0.8
Expand Down
9 changes: 5 additions & 4 deletions assets/tools/compare.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Both inputs must be paths; there is no stdin mode.
- `--by-sequence`: add the per-target agreement table and top-chain lists.
- `--top <N>`: with `--by-sequence`, show the top `N` scored chains per target.
Default `3`.
- `--memory-ceiling <GIB>` / `-M`: fail when the canonical-mapping estimate exceeds this many GiB. Default `16`.

## What is compared

Expand Down Expand Up @@ -149,7 +150,7 @@ file is not an error — that may be a real coverage difference.
## Memory

Canonical mappings are held in memory (one ~32-byte segment per block, both
files). The estimate is checked against a fixed 8 GiB ceiling before
canonicalization; past it the command fails with a clear error instead of
OOMing. Per-key external sorting is the planned upgrade path for larger inputs
and is not implemented.
files). The estimate is checked against a configurable ceiling (default 16 GiB,
set via `--memory-ceiling <GIB>`) before canonicalization; past it the command
fails with a clear error instead of OOMing. Per-key external sorting is the
planned upgrade path for larger inputs and is not implemented.
57 changes: 51 additions & 6 deletions src/cli/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use clap::Args;
use super::CliError;
use super::stats::{self, ChainStats, TargetStats};

// ponytail: fixed in-memory ceiling; add per-key external sort if real inputs exceed 8 GiB.
const CANONICAL_MEMORY_BUDGET: u128 = 8 * 1024 * 1024 * 1024;
// ponytail: in-memory ceiling, configurable via --memory-ceiling; add per-key external sort if real inputs exceed it.
const DEFAULT_MEMORY_CEILING_GIB: u64 = 16;
const ESTIMATED_SEGMENT_BYTES: u128 = 32;

#[derive(Debug, Args)]
Expand All @@ -34,6 +34,15 @@ pub struct CompareArgs {
help = "Top scored chains to show per target with --by-sequence"
)]
top: usize,

#[arg(
short = 'M',
long = "memory-ceiling",
value_name = "GIB",
default_value_t = DEFAULT_MEMORY_CEILING_GIB,
help = "Memory ceiling in GiB for canonical mappings before compare fails"
)]
memory_ceiling: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -123,7 +132,7 @@ where
let stats_a = analyze_path(&args.chain_a)?;
let stats_b = analyze_path(&args.chain_b)?;
ensure_compatible(&stats_a, &stats_b)?;
check_memory_budget(stats_a.blocks, stats_b.blocks)?;
check_memory_budget(stats_a.blocks, stats_b.blocks, args.memory_ceiling)?;

let mut names = NameInterner::default();
let mappings_a = canonicalize_path(&args.chain_a, stats_a.blocks, &mut names)?;
Expand Down Expand Up @@ -184,11 +193,12 @@ fn check_sizes(
Ok(())
}

fn check_memory_budget(a_blocks: u64, b_blocks: u64) -> Result<(), CliError> {
fn check_memory_budget(a_blocks: u64, b_blocks: u64, ceiling_gib: u64) -> Result<(), CliError> {
let estimated = (u128::from(a_blocks) + u128::from(b_blocks)) * ESTIMATED_SEGMENT_BYTES * 2;
if estimated > CANONICAL_MEMORY_BUDGET {
let budget = u128::from(ceiling_gib) * 1024 * 1024 * 1024;
if estimated > budget {
return Err(CliError::Message(format!(
"canonical mapping estimate ({:.2} GiB) exceeds compare's fixed 8 GiB ceiling; per-key external sort is the upgrade path and is not implemented",
"canonical mapping estimate ({:.2} GiB) exceeds compare's {ceiling_gib} GiB memory ceiling; per-key external sort is the upgrade path and is not implemented",
estimated as f64 / (1024.0 * 1024.0 * 1024.0)
)));
}
Expand Down Expand Up @@ -802,8 +812,19 @@ fn percent_ratio_u128(numerator: u128, denominator: u128) -> String {
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use std::io::Cursor;

#[derive(Debug, Parser)]
struct CompareHarness {
#[command(flatten)]
args: CompareArgs,
}

fn parse_compare_args(argv: &[&str]) -> CompareArgs {
CompareHarness::try_parse_from(argv).expect("args parse").args
}

fn stats_text(text: &str) -> ChainStats {
stats::analyze(&mut StreamingReader::new(Cursor::new(text.as_bytes())))
.expect("valid stats fixture")
Expand Down Expand Up @@ -913,4 +934,28 @@ mod tests {
assert_eq!(coverage.query_covered_bp, 20);
assert_eq!(coverage.query_multi_bp, 10);
}

#[test]
fn memory_budget_respects_ceiling() {
// estimate = (a_blocks + b_blocks) * 32 bytes * 2 files = 64 bytes/block.
// The 16 GiB default admits exactly 2^28 blocks and rejects one more;
// a raised ceiling admits the overflow case.
assert!(check_memory_budget(268_435_456, 0, 16).is_ok());
assert!(check_memory_budget(268_435_457, 0, 16).is_err());
assert!(check_memory_budget(268_435_457, 0, 17).is_ok());
}

#[test]
fn parses_memory_ceiling_flag() {
let args = parse_compare_args(&[
"compare", "-a", "a.chain", "-b", "b.chain", "--memory-ceiling", "32",
]);
assert_eq!(args.memory_ceiling, 32);

let args = parse_compare_args(&["compare", "-a", "a.chain", "-b", "b.chain", "-M", "4"]);
assert_eq!(args.memory_ceiling, 4);

let args = parse_compare_args(&["compare", "-a", "a.chain", "-b", "b.chain"]);
assert_eq!(args.memory_ceiling, DEFAULT_MEMORY_CEILING_GIB);
}
}
Loading