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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# D-CODEGRAPH-0-BIN reader: unchanged refresh 2.58 µs; changed-crate intake 2.5 ms fails the 1 ms gate (2026-11-02)

**Status:** MEASURED (release, `crates/lance-graph-codegraph/examples/bin_timing.rs`, mask-risc store of 17,155 rows written by `ruff_charon import`, N = 41 fixed in advance, 4 CPUs, load ≈ 0.3). Plan [`d-codegraph-0-binary-lanes-v1.md`](../plans/d-codegraph-0-binary-lanes-v1.md) §5.

| measurement | median | gate |
|---|---|---|
| unchanged `refresh_crate` (`rows.bin`, 64-byte header read) | 2.58 µs (range 2.53–14.92) | ≤ 20 µs: met |
| changed-crate intake from `rows.bin` | 2.54–2.84 ms | ≤ 1 ms: **NOT met** |
| — `fs::read` / `CrateFile::parse` / `read_bin_as` | 0.16 / 0.35 / 2.04 ms | |
| same crate from `rows.tsv` (baseline) | 5.08–7.41 ms | |

- The file is 556 KB as `rows.bin` against 3.05 MB as `rows.tsv`.
- **Where the gate fails:** `read_bin_as` spends about 2 ms in `Symbols::intern` (a hash plus an allocation per new name). This is the interner replacement the plan already assigns to chunk B3: `SYM_HASH` → global id, label locators into retained buffers, no intern at open or refresh, falsifiers F4 and F5.
- Verification (`parse`) is 0.35 ms. After B3 it is the next item to look at.
- Chunks B1 and B2 are in lance-graph PR #1929, with the gate reported as open. Merging them before B3 is the operator's decision.

OPEN: B3 (interner) and the re-run of this gate; Quack counting (F7) behind the codec9-column gap.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

551 entries, 2026-08-06 .. 2026-11-02.
552 entries, 2026-08-06 .. 2026-11-02.

| date | entry id | finding | file |
|---|---|---|---|
Expand All @@ -36,6 +36,7 @@ exactly one of them, never both.
| 2026-11-02 | `D-CODEGRAPH-0` | | [2026-11-02-d-codegraph-0-gap-map.md](2026-11-02-d-codegraph-0-gap-map.md) |
| 2026-11-02 | `codegraph-review-operator-decisions` | | [2026-11-02-codegraph-review-operator-decisions.md](2026-11-02-codegraph-review-operator-decisions.md) |
| 2026-11-02 | `codegraph-dr3-dt64-ruff-architecture-review` | | [2026-11-02-codegraph-dr3-dt64-ruff-architecture-review.md](2026-11-02-codegraph-dr3-dt64-ruff-architecture-review.md) |
| 2026-11-02 | `D-CODEGRAPH-0-BIN` | | [2026-11-02-codegraph-bin-reader-timing-gate.md](2026-11-02-codegraph-bin-reader-timing-gate.md) |
| 2026-11-01 | `D-XLOG-NATIVE-7` | | [2026-11-01-xlog-resident-hypercube-context.md](2026-11-01-xlog-resident-hypercube-context.md) |
| 2026-11-01 | `D-XLOG-NATIVE-8` | | [2026-11-01-xlog-behaviour-lens-f100.md](2026-11-01-xlog-behaviour-lens-f100.md) |
| 2026-11-01 | `D-DT64-7-5` | | [2026-11-01-maptable-versioned-local-map.md](2026-11-01-maptable-versioned-local-map.md) |
Expand Down
100 changes: 100 additions & 0 deletions crates/lance-graph-codegraph/examples/bin_timing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Timing gate for the binary rows format (plan `d-codegraph-0-binary-lanes-v1`
//! §5): an unchanged `refresh_crate` against a `rows.bin` store, and the intake
//! of one whole crate from `rows.bin` (read, verify, fill lanes) beside the
//! same crate read from `rows.tsv`. N runs each, pre-registered at 41; median
//! and range in microseconds. Run in release.
//!
//! Usage: `bin_timing <dir> <crate>` where `<dir>` holds `<crate>.rows.bin`
//! and `<crate>.rows.tsv` (as `ruff_charon import` writes them).

use std::hint::black_box;
use std::path::PathBuf;
use std::time::Instant;

use lance_graph_codegraph::artifact::{self, Lanes, Symbols};
use lance_graph_codegraph::artifact_bin::CrateFile;
use lance_graph_codegraph::Codegraph;

const N: usize = 41;

fn stats(name: &str, mut ns: Vec<u128>) {
ns.sort_unstable();
let us = |v: u128| v as f64 / 1000.0;
println!(
"{name}: median {:.2} us, min {:.2} us, max {:.2} us (N={})",
us(ns[ns.len() / 2]),
us(ns[0]),
us(ns[ns.len() - 1]),
ns.len()
);
}

fn main() {
let mut args = std::env::args().skip(1);
let dir = PathBuf::from(args.next().expect("usage: bin_timing <dir> <crate>"));
let krate = args.next().expect("usage: bin_timing <dir> <crate>");
let bin = dir.join(format!("{krate}.rows.bin"));
let tsv = dir.join(format!("{krate}.rows.tsv"));

let mut g = Codegraph::open(&dir).expect("store opens");
let rows = {
let file = CrateFile::parse(std::fs::read(&bin).expect("bin")).expect("parse");
file.n_rows()
};
println!("crate {krate}: {rows} rows");

let mut unchanged = Vec::with_capacity(N);
for _ in 0..N {
let t = Instant::now();
let s = g.refresh_crate(&dir, &krate).expect("refresh");
unchanged.push(t.elapsed().as_nanos());
black_box(s);
}
stats("refresh_crate (unchanged, rows.bin)", unchanged);

let mut bin_intake = Vec::with_capacity(N);
for _ in 0..N {
let t = Instant::now();
let file = CrateFile::parse(std::fs::read(&bin).expect("bin")).expect("parse");
let mut syms = Symbols::default();
let mut lanes = Lanes::default();
artifact::read_bin_as(&file, &krate, &mut syms, &mut lanes).expect("lanes");
bin_intake.push(t.elapsed().as_nanos());
black_box(lanes.len());
}
stats(
"changed-crate intake (rows.bin: read+verify+lanes)",
bin_intake,
);

let (mut r, mut v, mut l) = (Vec::new(), Vec::new(), Vec::new());
for _ in 0..N {
let t = Instant::now();
let buf = std::fs::read(&bin).expect("bin");
r.push(t.elapsed().as_nanos());
let t = Instant::now();
let file = CrateFile::parse(buf).expect("parse");
v.push(t.elapsed().as_nanos());
let t = Instant::now();
let mut syms = Symbols::default();
let mut lanes = Lanes::default();
artifact::read_bin_as(&file, &krate, &mut syms, &mut lanes).expect("lanes");
l.push(t.elapsed().as_nanos());
black_box(lanes.len());
}
stats(" of which fs::read", r);
stats(" of which CrateFile::parse (verify)", v);
stats(" of which read_bin_as (intern + lanes)", l);

let mut tsv_intake = Vec::with_capacity(N);
for _ in 0..N {
let t = Instant::now();
let text = std::fs::read_to_string(&tsv).expect("tsv");
let mut syms = Symbols::default();
let mut lanes = Lanes::default();
artifact::read_text_as(&text, &krate, &mut syms, &mut lanes).expect("lanes");
tsv_intake.push(t.elapsed().as_nanos());
black_box(lanes.len());
}
stats("baseline intake (rows.tsv: read+parse+lanes)", tsv_intake);
}
175 changes: 153 additions & 22 deletions crates/lance-graph-codegraph/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
//! Columns are located by name from the `#schema` line, so a file with
//! permuted columns reads identically.

use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::hash::{BuildHasherDefault, Hasher};
use std::path::Path;
use std::path::{Path, PathBuf};

use crate::artifact_bin::{self, BinRefusal, CrateFile};
use crate::graph::IntakeRefusal;

/// The only artifact version this reader accepts.
Expand Down Expand Up @@ -97,7 +98,9 @@ pub fn pred_code(s: &str) -> u8 {
pub fn digest(bytes: &[u8]) -> u64 {
let mut h = FastHasher(0xcbf2_9ce4_8422_2325);
h.write(bytes);
h.write_u64(bytes.len() as u64);
// Little-endian, never `write_u64`: its default folds native-endian
// bytes, and the ruff writer (`ruff_charon::store_bin::digest`) folds LE.
h.write(&(bytes.len() as u64).to_le_bytes());
h.finish()
}

Expand Down Expand Up @@ -427,6 +430,30 @@ impl Lanes {

const SUFFIX: &str = ".rows.tsv";

/// File-name suffix of the binary artifact, preferred over [`SUFFIX`] when both
/// exist for a crate.
pub const BIN_SUFFIX: &str = ".rows.bin";

/// Which file a crate is read from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CrateSource {
/// `<krate>.rows.bin`.
Bin(PathBuf),
/// `<krate>.rows.tsv`.
Tsv(PathBuf),
}

/// The file `<krate>` is read from in `dir`: `<krate>.rows.bin` when it
/// exists, otherwise `<krate>.rows.tsv` when that exists, otherwise `None`.
pub fn crate_source(dir: &Path, krate: &str) -> Option<CrateSource> {
let bin = dir.join(format!("{krate}{BIN_SUFFIX}"));
if bin.is_file() {
return Some(CrateSource::Bin(bin));
}
let tsv = dir.join(format!("{krate}{SUFFIX}"));
tsv.is_file().then_some(CrateSource::Tsv(tsv))
}

/// The text of the single file `<krate>.rows.tsv` of `dir`.
pub fn read_crate_text(dir: &Path, krate: &str) -> Result<String, IntakeRefusal> {
fs::read_to_string(dir.join(format!("{krate}{SUFFIX}"))).map_err(IntakeRefusal::Io)
Expand All @@ -440,34 +467,138 @@ pub fn read_crate(dir: &Path, krate: &str, symbols: &mut Symbols) -> Result<Lane
Ok(lanes)
}

/// Read every `*.rows.tsv` in `dir`, sorted by file name so the row order is
/// deterministic.
/// Read every crate of `dir`, sorted by crate stem so the row order is
/// deterministic. A crate with a `*.rows.bin` is read from it; otherwise from
/// its `*.rows.tsv`.
pub fn read_dir(dir: &Path, symbols: &mut Symbols) -> Result<Lanes, IntakeRefusal> {
let mut paths = Vec::new();
// stem -> whether a binary file exists for it
let mut stems: BTreeMap<String, bool> = BTreeMap::new();
for entry in fs::read_dir(dir).map_err(IntakeRefusal::Io)? {
let path = entry.map_err(IntakeRefusal::Io)?.path();
let is_rows = path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(SUFFIX));
if is_rows {
paths.push(path);
let name = entry.map_err(IntakeRefusal::Io)?.file_name();
let Some(name) = name.to_str() else {
continue;
};
if let Some(stem) = name.strip_suffix(BIN_SUFFIX) {
stems.insert(stem.to_owned(), true);
} else if let Some(stem) = name.strip_suffix(SUFFIX) {
stems.entry(stem.to_owned()).or_insert(false);
}
}
paths.sort();
let mut lanes = Lanes::default();
for p in &paths {
let text = fs::read_to_string(p).map_err(IntakeRefusal::Io)?;
let stem = p
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(SUFFIX))
.unwrap_or("");
read_text_as(&text, stem, symbols, &mut lanes)?;
for (stem, has_bin) in &stems {
if *has_bin {
let buf =
fs::read(dir.join(format!("{stem}{BIN_SUFFIX}"))).map_err(IntakeRefusal::Io)?;
let file = CrateFile::parse(buf).map_err(IntakeRefusal::Bin)?;
read_bin_as(&file, stem, symbols, &mut lanes)?;
} else {
let text = fs::read_to_string(dir.join(format!("{stem}{SUFFIX}")))
.map_err(IntakeRefusal::Io)?;
read_text_as(&text, stem, symbols, &mut lanes)?;
}
}
Ok(lanes)
}

/// A symbol id of a verified file, translated into the interner's id space.
fn translate(table: &[u32], id: u32, tag: u32, row: usize) -> Result<u32, IntakeRefusal> {
usize::try_from(id)
.ok()
.and_then(|i| table.get(i))
.copied()
.ok_or(IntakeRefusal::Bin(BinRefusal::IdRange { tag, row }))
}

/// Append one verified binary file's rows to `lanes`, attributing each to
/// `krate`, and record the file's header digest under `krate`.
///
/// The file's symbol table is interned once, in table order; rows then carry
/// translated ids, so no text is parsed and each distinct symbol, grade, file
/// and predicate label is interned once per file. Every lane [`read_text_as`]
/// fills is filled here in the same row order.
pub fn read_bin_as(
file: &CrateFile,
krate: &str,
sym: &mut impl Interner,
lanes: &mut Lanes,
) -> Result<(), IntakeRefusal> {
let kid = sym.intern(krate);
let mut table: Vec<u32> = Vec::with_capacity(file.n_symbols());
for i in 0..file.header().n_symbols {
table.push(sym.intern(file.symbol(i)));
}
// predicate codeword -> (P_* code, interned label id for P_OTHER)
let mut preds: HashMap<u16, (u8, u32)> = HashMap::new();
let mut grades: HashMap<&str, u32> = HashMap::new();
let mut files: HashMap<&str, u32> = HashMap::new();
let mut dash: Option<u32> = None;
for row in 0..file.n_rows() {
let (code, label_id) = match preds.get(&file.predicate_id(row)) {
Some(&hit) => hit,
None => {
let label = file.predicate(row);
let code = pred_code(label);
let id = if code == P_OTHER {
sym.intern(label)
} else {
NO_ID
};
preds.insert(file.predicate_id(row), (code, id));
(code, id)
}
};
let subject = translate(&table, file.subject(row), artifact_bin::tag::SUBJ, row)?;
let object = translate(&table, file.object(row), artifact_bin::tag::OBJ, row)?;
let may = match file.may_target(row) {
Some(t) => translate(&table, t, artifact_bin::tag::MAYT, row)?,
None => NO_ID,
};
let grade = file.grade(row);
let grade_id = match grades.get(grade) {
Some(&id) => id,
None => {
let id = sym.intern(grade);
grades.insert(grade, id);
id
}
};
let file_id = match file.file(row) {
None => match dash {
Some(id) => id,
None => {
let id = sym.intern("-");
dash = Some(id);
id
}
},
Some(label) => match files.get(label) {
Some(&id) => id,
None => {
let id = sym.intern(label);
files.insert(label, id);
id
}
},
};
let (start, end) = file.span(row).unwrap_or((NO_SPAN, NO_SPAN));
// Every fallible step is done: from here the row is pushed whole.
if code == P_OTHER {
lanes.other_pred.push((lanes.pred.len() as u32, label_id));
}
lanes.pred.push(code);
lanes.subject.push(subject);
lanes.object.push(object);
lanes.grade.push(grade_id);
lanes.file.push(file_id);
lanes.start.push(start);
lanes.end.push(end);
lanes.krate.push(kid);
lanes.may_target.push(may);
}
lanes.set_digest(kid, file.header().digest);
Ok(())
}

fn number(field: &str) -> Option<u32> {
if field == "-" {
Some(NO_SPAN)
Expand Down
Loading
Loading