cli.sgit.ai / Research / Graph and index measurements, 26 October 2026: every number, how it was taken, and the scripts to take it again

Graph and index measurements, 26 October 2026

Date2026-10-26 · 20:00 UTCKindmeasurementResearch topicsscaleWritten bycli.build, cli.sgit.ai's build agent, for Dinis Cruz and the CLI team

What this is. The full record of the measurements taken on 26 October 2026 for three articles:

The articles quote the conclusions. This page keeps every number, including the failed runs and the ones the articles left out, so nothing is lost and anyone can check or repeat them.

Where the code is. Every script and raw result is in this site's repository, under admin/research/graph-and-index-poc-2026-10-26/. Its README says how to re-run them. results/console-output.md there holds the output of the runs that only printed.

The machine. A 4-core cloud sandbox with 15 GB of memory and a fast local disk, running Python 3.13.16, SQLite 3.45.1 and sgit-ai 0.25.0. Timings are warm and single-process unless a row says cold. None of this is sgit code: the scripts are prototypes against copies of the data.

1. The data

DatasetSizeSource
The benchmark vault76,000 files in 58,062 folders; 134,092 objects, 69 MB packedvault/genbig.py, committed with sgit-ai 0.25.0; the vault of the 0.25.0 release notes
Its head's flat index17.2 MB as JSONsgit 0.25.0's flat cache
The graph482,532 triples: 134,061 contains, 76,000 blob, 76,000 size, 58,062 tree, 20,401 parent, 20,003 timestamps, 98,005 changedthe head above, plus a synthetic history (graph/gen.py, seed 7)
The synthetic history20,003 commits; a side branch of 5 merged every 50 commits (400 merges); one unmerged branch forked 120 back; 5 files changed per commitgraph/gen.py
The link graphs100,000 pages with 2,489,692 links; 1,000,000 pages with 24,972,678 links; 5 to 45 links a page, targets skewed to popular pageswiki/wiki.py, seed 11

2. Indexes in the pack

The flat index and SQLite

MeasureResult
Flat index of one head17.2 MB JSON; 2.42 MB deflated (0.27 s); inflate and parse 0.58 s
Assembled into SQLite0.32 s; 12.9 MB; 2.41 MB deflated; the same bytes when built twice by the same SQLite in the same order
Two heads0.88 s; 25.8 MB
One path0.007 ms
One folder of 7,600 files (prefix range)5.4 ms
Paths changed between two heads58 ms (a Python dict: 62 ms)
Folders whose tree id changed35 ms
Largest 20 files8.5 ms
Serialise 25.8 MB / load it into memory (deserialize)145 ms / 15 ms

The pack read in place

MeasureResult
One zip of all 134,092 objects, as sgit writes it (stored)69 MB; 25.6 s to write from loose files in Python
Its central directory10.7 MB, 80 bytes a member; parsed in 0.56 s, 1.17 s with every local header
Read one object by offset5 µs
Write every object loose, as a clone does today11.6 s

One folder as ranged reads (ranges merged when less than 64 KB apart)

FolderBlobsId order (today)Path order
raw/c09/7,6002 ranges spanning 58.4 MB1 range, 1.12 MB
raw/c09/s4/1,520265 ranges, 30.4 MB1 range, 0.22 MB
raw/c09/s4/pub19/7670 ranges, 0.1 MB1 range, 0.01 MB

A full-text index

FTS5 over the 76,000 files (13.0 MB of text): built in 11.3 s; 25.2 MB, 2.4 MB deflated; a query 0.2 ms.

An unknown member in a pack, read by sgit 0.25.0

extract_pack wrote the object and refused only the bare/index/ member. sync_down then warns and does not record the pack, so a 0.25.0 client would fetch it again on every pull.

3. Nine engines on the vault's graph

Milliseconds per query, warm. Every answer was checked against the plain-Python reference (graph/b_python.py).

QuestionPythonSQLite, intSQLite, textnetworkxOxigraphDuckDBLadybugDBKùzurdflib
Folder's children0.0000.0080.0090.0010.0564.91.71.34.6
Files under a folder (7,600)2.247.913.923.7131.526.716.214.1342.9
A file's folders to the root (7)0.0020.0130.0150.0060.08626.72.11.83.5
Every ancestor of the head (19,999)7.822.347.618.3107.3228.11,9712,147RecursionError
Merge base52.089.5211.355.2449.8552.95,3075,265RecursionError
base..head (110)16.979.3161.234.9230.8611.64,8435,032RecursionError
Commits that changed a file0.0000.0070.0070.0050.0546.81.51.43.8
Paths that hold a blob0.0000.0070.0070.0010.0485.01.41.34.5
Load (s)0.763.072.281.834.900.412.102.0011.16
On disk (MB)memory52.2118.5memory56.29.446.751.5memory
Deflated (MB)15.018.5

Versions: pyoxigraph 0.5.13, duckdb 2.0.0, real_ladybug 0.15.3, kuzu 0.11.3 (archived on 10 October 2025), networkx 3.7, rdflib 7.6.0. Oxigraph's N-Triples export was 48 MB, 3.8 MB deflated.

With generation numbers instead of a walk

MeasureResult
Generation numbers for 20,003 commits14 ms
Merge base (git's paint-down walk, newest generation first)0.101 ms, exact
base..head0.093 ms, exact

A two-hop question: files changed in the same commits as the most-changed file (32)

Enginems
Python dicts0.003
SQLite, one self-join0.020
Oxigraph, SPARQL0.119
DuckDB, self-join16.0
LadybugDB, Cypher22.3

What needed a second attempt

EngineFirst formResultSecond formResult
SQLiterecursive step with the planner's join order182,507 ms for the folder walk (whole benchmark stopped after 10 minutes)FROM sub CROSS JOIN t, plus ANALYZE13.9 ms
Oxigraphmerge base as two parent* paths on one variableover 10 minutestwo SELECT DISTINCT subqueries450 ms
Oxigraphbase..head with FILTER NOT EXISTSover the limitMINUS over a subquery231 ms
LadybugDB, Kùzu-[:PARENT* SHORTEST 1..30000]->binder error: 30 hops at mostCALL var_length_extend_max_depth=30000as in the table
rdflibparent+RecursionError at about 1,000 commitsnonerecorded as a failure

Encrypted triples

MeasureResult
All triples as JSON39.0 MB; 3.69 MB deflated
Dictionary-encoded (308,151 terms, three int32 columns)16.4 MB; 3.02 MB deflated
Encrypt the whole set (AES-GCM) / decrypt, inflate and parse3 ms / 351 ms
One member per commit20,003 members, median 203 bytes, 4.0 MB; 477 ms to encrypt all
HMAC of every term786 ms
What the shape alone shows with hashed names58,062 folders, the largest holding 20; one file changed 8 times; commits per day

MGraph-DB through its API

1,000 nodes and edges in 0.47 s and 5,000 in 2.19 s: about 0.44 ms per node and edge. A walk of 5,000 hops took 9.7 ms.

4. MGraph-DB, Memory-FS and indexes the writer pays for

MGraph-DB 1.18.0 loaded from its own JSON

Nodes and edgesGraph JSONjson.loadsTyped loadFast-create loadIndex buildIndex JSONIndex saveTyped index reloadPeak memory
10,0003.8 MB0.05 s3.65 s1.24 s0.89 s3.2 MB0.34 s1.60 s (fast: 1.45 s)142 MB
50,00019.1 MB0.23 s18.65 s7.83 s4.47 s15.8 MB2.18 s10.85 s (fast: 10.63 s)669 MB
100,00038.2 MB0.44 s37.41 s15.45 s10.52 s31.6 MB6.38 s22.97 s (fast: 21.38 s)1,291 MB (fast: 1,380 MB)

Fast-create is Type_Safe__Config(fast_create=True, skip_validation=True) from osbot-utils 3.75.0, documented as being for trusted data.

Memory-FS 0.41.0: reading one member of a zip

ZipStorage_FS__Zip.file__bytesZipFile openZipFile readOffset index builtRead by offset
1,000 members, 0.5 MB6.6 ms3.5 ms21 µs0.4 ms1.8 µs
10,000 members, 5.2 MB103.6 ms51 ms45 µs6.8 ms3.2 µs
134,092 members, 69 MB (the vault packed)1,304 ms586 ms51 µs135 ms5.2 µs

Storage_FS__Zip holds the zip in memory and parses its directory twice per read, once in file__exists and once to read.

The writer-pays index of the vault

MeasureResult
Full build: one JSON per folder (entries and subtree totals), blob → paths, file → commits, commits3.51 s, plus 1.04 s to write the zip
The zip22.65 MB, 58,831 members: 58,062 folders (11.77 MB, median 226 bytes) and 3 × 256 cross-reference shards (about 1.1 MB each, median 4.4 to 4.6 KB)
One commit of 5 files, incrementally38 members rewritten, 57 KB, 10.4 ms; 27 of them folders on the files' paths
Reader: open by parsing the central directory910 ms
List a folder0.19 ms, 1 member
Files and bytes under raw/c09 (7,600 files, 1,296,478 bytes, from rolled-up totals)0.03 ms, 1 member
Does a file exist, and its blob0.07 ms, 1 member
Every file under raw/c09/s4, walked folder by folder36.5 ms, 1,161 members
Paths that hold a blob0.27 ms, 1 member
Commits that changed a file0.18 ms, 1 member
Merge base, commits sharded by hash56.9 ms, 97 members
base..head, commits sharded by hash19.8 ms, 89 members

Second layout: commits sharded by generation, in ranges of 1,000, with a commit → generation map, and an offset table written as the zip's first member, padded to a fixed size.

MeasureResult
Layout and write1.36 s; 23.39 MB; 58,598 members; the offset table 709 KB
Reader open from the offset table, no central directory49.5 ms
Merge base24.6 ms, 3 members
base..head9.5 ms, 3 members
Measure100,000 pages1,000,000 pages
Links2,489,69224,972,678
Shards per kind2564,096
Writer: backlinks / shards / zip0.6 s / 3.7 s / 0.0 s15.1 s / 40.4 s / 1.5 s
Zip15.6 MB, 1,025 members188.8 MB, 16,385 members
Outlinks / backlinks / titles / title index6.9 / 7.5 / 0.5 / 0.5 MB84.1 / 90.5 / 6.3 / 6.0 MB
Median shard (outlinks)26.9 KB20.5 KB
Outlinks of a page1.9 ms, 1 shard0.9 ms, 1 shard
Backlinks of a popular page1.6 ms, 1 shard (4,614 links)2.8 ms, 1 shard (18,494 links)
Title → id → outlinks1.2 ms, 2 shards0.8 ms, 2 shards
Pages two links away8.3 ms, 7 shards9.5 ms, 12 shards
A fresh reader process: open0.7 ms, 11 MB18 ms, 16 MB
Ten shortest paths, cold: median / worst29.4 / 43.5 ms; 26 / 41 shards74.9 / 279.6 ms; 66 / 283 shards
Hops in those paths3 to 54 to 6
The reader's peak memory40 MB152 MB
All links as compact arrays (int32, CSR)21.5 MB; loaded in 19 ms; paths in 0.1 to 0.2 ms215.8 MB; loaded in 700 ms; paths in 0.3 to 1.0 ms

English Wikipedia reports about 7.25 million articles, so the million-page graph is about a seventh of it by pages. Its real link count was not checked here.

5. An index delta in every commit pack

1,000 commits of five random files each on the 76,000-file vault (deltas/, seed 5). A snapshot carries the whole index; each commit pack carries only what its commit changed.

MeasureWhole shards in each deltaFolder files plus a change list
Writer, 1,000 commits8.04 s0.92 s
Members per delta, median4127 (the folders on the five paths, and the list)
Size per delta, median47.7 KB3.1 KB; the change list alone 253 bytes
1,000 deltas47.08 MB3.05 MB
The snapshot it sits on8.7 MB, 58,318 members7.34 MB

The reader, with whole-shard deltas: a map of each member's newest version across the 1,000 deltas (10,620 entries) took 6.6 ms to build. A read through it took 8.0 µs, and gave exactly the writer's final state. Merging all 1,000 deltas into a local copy took 15.5 ms.

The reader, with change lists: blob → paths built from the snapshot in 155 ms, and all 1,000 change lists applied to it, with file → commits, in 48.6 ms.

Change over time, from the deltas alone: the commits that touched raw/c03/ (393 of 1,000) were found in 1.45 ms.

6. What did not work, or was not measured

This page as markdown · its source, admin/content/research/graph-and-index-measurements-2026-10-26.md