You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With #236's rowid delete landed, _retract is no longer the bottleneck: it fell from 4,137.8 s / 73.5% of the run to 32.7 s / 2.1%. The new dominant cost is graph read queries: _db_execute on (query ...) is 1,328,250 calls costing
523.0 s — 33.6% of a 1,558.6 s run.
91% of that sits in two call sites, and both issue the same shape of query — a
single-entity point lookup, one entity at a time, in a loop over every candidate ident
of every file of every commit:
[:find ?c :where [<ident> :introduced-by ?c]]
This is the facts_dedup story again in a different table: a per-item lookup issued in a
loop where a set-at-a-time form is available, and the codebase already contains the
preload pattern that fixes it (_preload_provisional_idents, whose own docstring says it
exists because "this set is consulted where a per-ident check is too expensive").
Measurements
Full instrumented at-scale ingestion of this repo, master, 568 commits, 1,558.61 s,
inclusive/exclusive per-function attribution. Reproduced across three runs of the same
build — plain 1,600.55 s, leaf-instrumented 1,622.50 s, inclusive/exclusive 1,558.61 s
(±2%).
D1 — where the 1,558.61 s goes (exclusive, no double-counting)
component
calls
seconds
% of run
_db_execute(query ...)
1,328,250
523.02
33.6%
_forward_apply own Python (incl. _build_code_triples)
568
369.70
23.7%
_reverse_apply own Python
284
162.22
10.4%
_db_execute(retract ...)
88,917
21.81
1.4%
_db_execute(transact ...)
10,191
9.93
0.6%
fact_index.delete_facts (234,928 triples)
88,902
8.79
0.6%
_lineage_is_provisional own code
603,486
5.08
0.3%
_correction_sweep_apply own Python
284
4.82
0.3%
fact_index.insert_facts (314,091 triples)
10,191
3.42
0.2%
_entity_introduced_by_query own code
464,377
3.88
0.2%
_resolved_facts_triples
99,108
1.70
0.1%
all other wrapped functions
—
1.97
0.1%
attributed total
1,117.14
71.7%
unattributed (_extract_commit on the process pool, _run_ingestion orchestration, thread hops)
441.47
28.3%
Stage A 623.25 s (40.0%), Stage B 933.88 s (59.9%).
_entity_introduced_by_query (mcp_server.py:5378) and _lineage_is_provisional
(mcp_server.py:5210) are both one-line single-entity _db_execute wrappers with no
caching.
Suggested direction
The set-at-a-time precedent already exists in this file: _preload_known_deps, _preload_provisional_idents, _preload_known_entities, and _rebuild_index_from_graph's ident_map all bind ?e as an output variable and
retrieve the whole relation in one query.
The design question this needs to answer is cache maintenance, not whether the batch
query works.:introduced-by is written during the run by the very walks that read
it, so a run-start snapshot goes stale — unlike state.provisional_idents, which is
already a preload snapshot precisely because its staleness is tolerable there and _forward_apply documents at length (mcp_server.py:8306-8356) that it is "not the
authority". The writers are bounded and enumerable:
so a write-through in-run cache is feasible. Whether it should be that, or a per-commit
batch query scoped to the commit's candidate idents, is a spec decision — the per-commit
form avoids the staleness problem entirely at the cost of one query per commit over
~1,200 idents, and should be measured against the write-through form before either is
built.
The three sites are not equally worth it: at 0.99 ms and 0.46 ms per call the two :introduced-by sites carry 30.2% of the run, while _lineage_is_provisional at 0.08 ms
carries 3.2% despite being the most-called of the three. Do the first two.
What NOT to do — checked against the post-#236 numbers
Do not batch _correction_sweep_apply's case-3 :modified-in retract.#236 said this
was worthless before the rowid fix and that the post-fix table should decide it. The table
has now decided: that site is 82,210 calls costing 21.52 s — 1.4% of the run, down
from 1,629 s / 29.0%. The entire (retract ...) cost across every call site in the run is
21.81 s. Collapsing 82,210 calls into ~284 therefore has a hard ceiling of about 1.4% of
wall clock and a realistic recovery well under that. Still not worth doing — but note
the reason has changed: it is no longer "the cost follows facts, not calls", it is simply
that the whole line item is now small.
Do not target _resolved_facts_triples' _query_ident calls.#236's spec named this
as the expected largest residual inside _retract. Measured: _resolved_facts_triples is 1.70 s — 0.1% of the run, and _query_ident is never called at all during
ingestion, because it only fires for #uuid-tagged entities and every ingestion entity is
already a keyword ident. The expectation was wrong; this is a dead end.
Harness overhead is bounded by the three-run spread: plain 1,600.55 s vs fully
instrumented 1,558.61 s — the instrumented run was faster, so overhead is within
run-to-run variance.
Summary
With #236's rowid delete landed,
_retractis no longer the bottleneck: it fell from4,137.8 s / 73.5% of the run to 32.7 s / 2.1%. The new dominant cost is
graph read queries:
_db_executeon(query ...)is 1,328,250 calls costing523.0 s — 33.6% of a 1,558.6 s run.
91% of that sits in two call sites, and both issue the same shape of query — a
single-entity point lookup, one entity at a time, in a loop over every candidate ident
of every file of every commit:
This is the
facts_dedupstory again in a different table: a per-item lookup issued in aloop where a set-at-a-time form is available, and the codebase already contains the
preload pattern that fixes it (
_preload_provisional_idents, whose own docstring says itexists because "this set is consulted where a per-ident check is too expensive").
Measurements
Full instrumented at-scale ingestion of this repo,
master, 568 commits, 1,558.61 s,inclusive/exclusive per-function attribution. Reproduced across three runs of the same
build — plain 1,600.55 s, leaf-instrumented 1,622.50 s, inclusive/exclusive 1,558.61 s
(±2%).
D1 — where the 1,558.61 s goes (exclusive, no double-counting)
_db_execute(query ...)_forward_applyown Python (incl._build_code_triples)_reverse_applyown Python_db_execute(retract ...)_db_execute(transact ...)fact_index.delete_facts(234,928 triples)_lineage_is_provisionalown code_correction_sweep_applyown Pythonfact_index.insert_facts(314,091 triples)_entity_introduced_by_queryown code_resolved_facts_triples_extract_commiton the process pool,_run_ingestionorchestration, thread hops)Stage A 623.25 s (40.0%), Stage B 933.88 s (59.9%).
D2 — the 1.33M queries, by calling function
_correction_sweep_apply(inline:introduced-by+:modified-in)_entity_introduced_by_query(from_reverse_apply)_lineage_is_provisionalThe first two are 30.2% of the entire run on their own.
D3 — what #236 actually bought
_retract_retractfact_index.delete_facts_entity_introduced_by_set_provisional_batchretract_correction_sweep_applyretractWall clock: 5,133.19 s → 1,600.55 s against the plain harness, i.e. 20.3x the 78.87 s
forward-only baseline, down from 65x.
The code
mcp_server.py:8967-9001, inside_correction_sweep_apply's per-file loop:mcp_server.py:7914-7942, inside_reverse_apply's per-file loop — the same ident isqueried up to twice per pass:
_entity_introduced_by_query(mcp_server.py:5378) and_lineage_is_provisional(
mcp_server.py:5210) are both one-line single-entity_db_executewrappers with nocaching.
Suggested direction
The set-at-a-time precedent already exists in this file:
_preload_known_deps,_preload_provisional_idents,_preload_known_entities, and_rebuild_index_from_graph'sident_mapall bind?eas an output variable andretrieve the whole relation in one query.
The design question this needs to answer is cache maintenance, not whether the batch
query works.
:introduced-byis written during the run by the very walks that readit, so a run-start snapshot goes stale — unlike
state.provisional_idents, which isalready a preload snapshot precisely because its staleness is tolerable there and
_forward_applydocuments at length (mcp_server.py:8306-8356) that it is "not theauthority". The writers are bounded and enumerable:
_entity_introduced_by_set_provisional_batch(mcp_server.py:5457-5458)_re_date_structural_facts' supersede path (mcp_server.py:5568)_build_code_triples(mcp_server.py:6677-6736), via_forward_apply/_reverse_apply_forward_apply's external-entity path (mcp_server.py:8586)so a write-through in-run cache is feasible. Whether it should be that, or a per-commit
batch query scoped to the commit's candidate idents, is a spec decision — the per-commit
form avoids the staleness problem entirely at the cost of one query per commit over
~1,200 idents, and should be measured against the write-through form before either is
built.
The three sites are not equally worth it: at 0.99 ms and 0.46 ms per call the two
:introduced-bysites carry 30.2% of the run, while_lineage_is_provisionalat 0.08 mscarries 3.2% despite being the most-called of the three. Do the first two.
What NOT to do — checked against the post-#236 numbers
Do not batch
_correction_sweep_apply's case-3:modified-inretract. #236 said thiswas worthless before the rowid fix and that the post-fix table should decide it. The table
has now decided: that site is 82,210 calls costing 21.52 s — 1.4% of the run, down
from 1,629 s / 29.0%. The entire
(retract ...)cost across every call site in the run is21.81 s. Collapsing 82,210 calls into ~284 therefore has a hard ceiling of about 1.4% of
wall clock and a realistic recovery well under that. Still not worth doing — but note
the reason has changed: it is no longer "the cost follows facts, not calls", it is simply
that the whole line item is now small.
Do not target
_resolved_facts_triples'_query_identcalls. #236's spec named thisas the expected largest residual inside
_retract. Measured:_resolved_facts_triplesis1.70 s — 0.1% of the run, and
_query_identis never called at all duringingestion, because it only fires for
#uuid-tagged entities and every ingestion entity isalready a keyword ident. The expectation was wrong; this is a dead end.
Notes on the measurement
_transact/_retract/_db_executeand themain apply functions with a per-thread call stack giving inclusive and exclusive wall
time, driving a real in-process
mcp_server._run_ingestionagainst an isolated tempdirgraph. Same convention as perf: reverse walk issues 3,152 transactions per commit vs forward-only's 12 (263x write amplification) #233's stage-split harness.
_extract_commit, which runs inProcessPoolExecutorworkers and isinvisible to parent-process patching. It is most of the 28.3% unattributed. perf: reverse walk issues 3,152 transactions per commit vs forward-only's 12 (263x write amplification) #233
measured it at 180 s Stage A (pipelined, hidden behind the write side) and 132 s Stage B
(serial, directly on the critical path); nothing in perf: fact_index.delete_facts is O(index size) per triple — 73.5% of ingestion wall clock #236 changes parsing cost, so those
carry over approximately.
instrumented 1,558.61 s — the instrumented run was faster, so overhead is within
run-to-run variance.