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
4 changes: 4 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ its Cargo build stays a diagnostic path next to authoritative Bazel
compilation. A sole `lex[simple_match]` Performance Analysis failure on a
non-cypher PR is a known short-bench noise pattern — see triage in
[`docs/development/benchmarking.md`](../../docs/development/benchmarking.md).
M6 pure kernels use simulation; durable open/recovery/commit/GC/compaction use
the declared Blacksmith 4-vCPU Ubuntu 24.04 walltime runner. Weekly/manual runs
also retain exact-SHA replay and compaction peak-RSS artifacts while CodSpeed
memory mode is unavailable for this project.

### `binding-release-candidate.yml`

Expand Down
53 changes: 52 additions & 1 deletion .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:
branches: ["main"]
# Lets CodSpeed trigger a backtest run to seed the baseline.
workflow_dispatch:
schedule:
- cron: "17 7 * * 1"

permissions:
contents: read
Expand Down Expand Up @@ -42,10 +44,59 @@ jobs:
tool: cargo-codspeed@5.0.1

- name: Build benchmark targets
run: cargo codspeed build -m simulation -p graphforge-core -p graphforge-cypher
run: |
cargo codspeed build -m simulation -p graphforge-core -p graphforge-cypher
cargo codspeed build -m simulation -p graphforge-storage --bench m6_storage

- name: Verify fail-closed M6 benchmark inventory
run: python3 scripts/ci/check-m6-benchmarks.py

- name: Run benchmarks
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
with:
mode: simulation
run: cargo codspeed run

m6-walltime:
name: M6 Durable Walltime (Blacksmith 4 vCPU Ubuntu 24.04)
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master as of 2026-08-05
with:
toolchain: "1.96.0"
- uses: taiki-e/install-action@288e746965032cfcc232e09af2daf5f23c14d780 # v2.86.1
with:
tool: cargo-codspeed@5.0.1
- name: Build durable filesystem benchmarks only
run: cargo codspeed build -m walltime -p graphforge-storage --bench m6_storage_io
- uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
with:
mode: walltime
run: cargo codspeed run

m6-memory-fallback:
name: M6 Scheduled Peak Memory Artifact
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master as of 2026-08-05
with:
toolchain: "1.96.0"
- name: Compile M6 storage benchmarks
run: cargo bench -p graphforge-storage --bench m6_storage --bench m6_storage_io --no-run
- name: Record replay peak resident memory
run: /usr/bin/time -v cargo bench -p graphforge-storage --bench m6_storage replay_merge_fingerprint -- --sample-count 10 2> replay-memory.txt
- name: Record compaction peak resident memory
run: /usr/bin/time -v cargo bench -p graphforge-storage --bench m6_storage_io spill_compaction -- --sample-count 10 2> compaction-memory.txt
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: m6-memory-${{ github.sha }}-blacksmith-4vcpu-ubuntu-2404
path: |
replay-memory.txt
compaction-memory.txt
if-no-files-found: error
retention-days: 1
1 change: 1 addition & 0 deletions Cargo.lock

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

6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help lint format type-check security workflow-lint license-check third-party-notices third-party-notices-check cargo-deny-licenses test pre-push pre-push-clean pre-push-preflight pre-push-fast bazel-test clean test-tck docstring-coverage test-network benchmark test-perf test-perf-xs test-perf-slow test-perf-large coverage coverage-rust coverage-python coverage-node coverage-quick coverage-report coverage-diff coverage-strict check-coverage check-coverage-rust check-coverage-python check-coverage-node check-patch-coverage test-durations test-analytics docs-serve docs-build docs-clean cargo-build codspeed-build codspeed-run bench-traversal bench-fixed-hop-limit bench-fixed-hop-livejournal bench-m4-entry bench-g500-scale20 bench-g500-ladder bench-adjacency-200m bench-file-backed-128m m4-entry-matrix-check durability-isolation-check native-consumers release-load-matrix-check release-load-matrix bulk-construction-conformance-check bulk-construction-conformance cargo-test cargo-check cargo-clippy cargo-fmt cargo-fmt-check clean-builds clean-builds-all pnpm-install pnpm-build pnpm-test-bdd install build release-version-check package-license-verify publish-dry-run publish-dry-run-npm publish-dry-run-docs publish-dry-run-python publish-dry-run-cargo record-release-artifacts clean-env-verify-check clean-env-verify-preflight clean-env-verify
.PHONY: help lint format type-check security workflow-lint license-check third-party-notices third-party-notices-check cargo-deny-licenses test pre-push pre-push-clean pre-push-preflight pre-push-fast bazel-test clean test-tck docstring-coverage test-network benchmark test-perf test-perf-xs test-perf-slow test-perf-large coverage coverage-rust coverage-python coverage-node coverage-quick coverage-report coverage-diff coverage-strict check-coverage check-coverage-rust check-coverage-python check-coverage-node check-patch-coverage test-durations test-analytics docs-serve docs-build docs-clean cargo-build codspeed-build codspeed-build-walltime codspeed-run bench-traversal bench-fixed-hop-limit bench-fixed-hop-livejournal bench-m4-entry bench-g500-scale20 bench-g500-ladder bench-adjacency-200m bench-file-backed-128m m4-entry-matrix-check durability-isolation-check native-consumers release-load-matrix-check release-load-matrix bulk-construction-conformance-check bulk-construction-conformance cargo-test cargo-check cargo-clippy cargo-fmt cargo-fmt-check clean-builds clean-builds-all pnpm-install pnpm-build pnpm-test-bdd install build release-version-check package-license-verify publish-dry-run publish-dry-run-npm publish-dry-run-docs publish-dry-run-python publish-dry-run-cargo record-release-artifacts clean-env-verify-check clean-env-verify-preflight clean-env-verify

help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -276,6 +276,10 @@ coverage-rust: ## Core + same-SHA Python/Node adapter Rust coverage ledger

codspeed-build: ## Build the CodSpeed benchmark targets (simulation mode; see docs/development/benchmarking.md)
cargo codspeed build -m simulation -p graphforge-core -p graphforge-cypher
cargo codspeed build -m simulation -p graphforge-storage --bench m6_storage

codspeed-build-walltime: ## Build only M6 durable I/O benchmarks in walltime mode
cargo codspeed build -m walltime -p graphforge-storage --bench m6_storage_io

codspeed-run: ## Run the CodSpeed benchmarks locally (requires the codspeed CLI)
codspeed run --mode simulation -- cargo codspeed run
Expand Down
7 changes: 6 additions & 1 deletion cargo-bazel-lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"checksum": "c79eadabcf22e7d0d308d31294a7d03c7538b15a3276913e4a72497bfc36f979",
"checksum": "e2a8b06a6d2ec90b3c7752d4f68cf0ac1d0df9be549b22ee81da847751192bf9",
"crates": {
"adler2 2.0.1": {
"name": "adler2",
Expand Down Expand Up @@ -13846,6 +13846,11 @@
},
"deps_dev": {
"common": [
{
"id": "codspeed-divan-compat 5.0.1",
"target": "codspeed_divan_compat",
"alias": "divan"
},
{
"id": "wait-timeout 0.2.1",
"target": "wait_timeout"
Expand Down
9 changes: 9 additions & 0 deletions crates/graphforge-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ rustix = { version = "1.1", features = ["fs"] }
named-lock = "0.4.1"

[dev-dependencies]
divan = { workspace = true }
wait-timeout = "0.2"

[[bench]]
name = "m6_storage"
harness = false

[[bench]]
name = "m6_storage_io"
harness = false

[lints]
workspace = true
149 changes: 149 additions & 0 deletions crates/graphforge-storage/benches/m6_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! Deterministic M6 storage kernels for CodSpeed CPU simulation (#782).

use divan::Bencher;
use graphforge_storage::{
GraphDeltaJournalLimits, GraphDeltaOp, GraphDeltaOpKind, GraphDeltaPayload,
ReconstructedGraphState, apply_delta_runs, decode_delta_run, encode_delta_run,
Comment thread
DecisionNerd marked this conversation as resolved.
};
use std::collections::{BTreeMap, BTreeSet};
use uuid::Uuid;

fn main() {
divan::main();
}

fn fixture(count: usize) -> Vec<GraphDeltaOp> {
(0..count)
.map(|index| GraphDeltaOp {
operation_uuid: Uuid::from_u128(0x1000 + index as u128),
kind: GraphDeltaOpKind::UpsertNode,
payload: GraphDeltaPayload::UpsertNodeV2 {
node_uuid: Uuid::from_u128(0x2000 + index as u128).to_string(),
node_id: index as u64 + 1,
type_ids: vec![1],
created_at_micros: index as i64,
updated_at_micros: index as i64,
},
})
.collect()
}

#[divan::bench(args = [1, 100, 10_000])]
fn gfdr_encode(bencher: Bencher, count: usize) {
let operations = fixture(count);
bencher.bench(|| {
encode_delta_run(
1,
Uuid::from_u128(1),
Uuid::from_u128(2),
divan::black_box(&operations),
GraphDeltaJournalLimits::default(),
)
.unwrap()
});
}

#[divan::bench(args = [1, 100, 10_000])]
fn gfdr_decode_verify(bencher: Bencher, count: usize) {
let bytes = encode_delta_run(
1,
Uuid::from_u128(1),
Uuid::from_u128(2),
&fixture(count),
GraphDeltaJournalLimits::default(),
)
.unwrap();
bencher.bench(|| {
decode_delta_run(
divan::black_box(&bytes),
Some(1),
GraphDeltaJournalLimits::default(),
)
.unwrap()
});
}

#[divan::bench(args = [1, 100, 10_000])]
fn replay_merge_fingerprint(bencher: Bencher, operations: usize) {
let first = fixture(operations);
let mut second = fixture(operations);
for (index, operation) in second.iter_mut().enumerate() {
operation.operation_uuid = Uuid::from_u128(0x1_0000 + index as u128);
if let GraphDeltaPayload::UpsertNodeV2 {
updated_at_micros, ..
} = &mut operation.payload
{
*updated_at_micros += 1;
}
}
let encoded_first = encode_delta_run(
1,
Uuid::from_u128(10),
Uuid::from_u128(20),
&first,
GraphDeltaJournalLimits::default(),
)
.unwrap();
let encoded_second = encode_delta_run(
2,
Uuid::from_u128(11),
Uuid::from_u128(21),
&second,
GraphDeltaJournalLimits::default(),
)
.unwrap();
bencher.bench(|| {
let limits = GraphDeltaJournalLimits::default();
let runs = [
decode_delta_run(&encoded_first, Some(1), limits).unwrap(),
decode_delta_run(&encoded_second, Some(2), limits).unwrap(),
];
let mut state = ReconstructedGraphState::default();
let evidence = apply_delta_runs(&mut state, &runs, limits).unwrap();
divan::black_box(evidence.state_fingerprint)
});
}
Comment thread
DecisionNerd marked this conversation as resolved.

#[divan::bench(args = [1, 100, 10_000])]
fn transaction_classification(bencher: Bencher, count: usize) {
let operations = fixture(count);
bencher.bench(|| {
divan::black_box(
operations
.iter()
.filter(|op| matches!(op.kind, GraphDeltaOpKind::UpsertNode))
.count(),
)
});
}

#[divan::bench(args = [1, 100, 10_000])]
fn manifest_reachability(bencher: Bencher, count: usize) {
// Version-1 synthetic generation manifest: each generation retains its
// immediate predecessor. Building it is fixture setup, while the measured
// closure is the deterministic bounded ancestor walk used by cleanup.
let parents: BTreeMap<u64, Option<u64>> = (0..count as u64)
.map(|generation| (generation, generation.checked_sub(1)))
.collect();
bencher.bench(|| {
let mut reachable = BTreeSet::new();
let mut cursor = (count as u64).checked_sub(1);
while let Some(generation) = cursor {
reachable.insert(generation);
cursor = parents[&generation];
}
divan::black_box(reachable)
});
}

#[divan::bench(args = [1, 100, 10_000])]
fn transaction_stage_and_classify(bencher: Bencher, count: usize) {
let operations = fixture(count);
bencher.bench(|| {
let staged: Vec<_> = operations
.iter()
.map(|operation| (operation.operation_uuid, operation.kind))
.collect();
divan::black_box(staged)
});
}
Loading