Measurement Campaign Analysis¶
Archive status — 2026-10-07¶
This is a historical report. Measurements and decisions describe the original
run and revision; recommendations and commands are not current operating
instructions. Path corrections below change navigation, not measurements.
The original ignored report remains unchanged at .artifacts/analysis/2026-05-19-measurement-campaign-analysis.md
(SHA-256 cf24246b1b69ca815a2bd9fd8fa2ef2d9d017826a67a9909747a03d05e619e71).
Some original evidence is unavailable in this checkout. The missing paths are listed below; those observations have not been independently revalidated. Do not treat this archive as a fully reproducible current benchmark.
See the report archive index for coverage and remaining gaps.
| Artifact or input reference | Current availability |
|---|---|
.artifacts/20260502T172501Z |
unavailable historical path |
.artifacts/analysis/2026-05-03-measurement-campaign-analysis.md |
available |
.artifacts/analysis/2026-05-16-measurement-campaign-analysis.md |
available |
.artifacts/benchmarks/backend/20260511T151057Z |
available |
.artifacts/benchmarks/short-duckdb |
unavailable historical path |
.artifacts/benchmarks/short-sqlite |
unavailable historical path |
benchmarks/performance/short_benchmark.local.json |
available |
Resolved directory moves:
benchmarks/short_benchmark.local.json→benchmarks/performance/short_benchmark.local.json..artifacts/20260511T151057Z→.artifacts/benchmarks/backend/20260511T151057Z.
Date: 2026-05-19
Hardware Snapshot¶
- Host kernel:
Linux verona 6.18.31-gentoo-dist #1 SMP PREEMPT_DYNAMIC Fri May 15 16:31:41 -00 2026 x86_64 - OS:
Gentoo Linux 2.18 - CPU:
Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz - CPU topology:
1 socket,6 physical cores,12 logical CPUs, max reported frequency4.70 GHz - Memory at analysis time:
46 GiBRAM total,83 GiBswap total - GPU: NVIDIA GeForce GTX 1050, driver
580.159.03, CUDA13.0,727 MiB / 2048 MiBmemory in use,8%utilization in the operator-providednvidia-smioutput at2026-05-19 19:04:41 - Embedding device environment:
CODIRA_EMBED_DEVICEwas unset - Embedding device selection in code:
src/codira/semantic/embeddings.py::_configured_embedding_device()returnscpuwhenCODIRA_EMBED_DEVICEis unset - Embeddings in this analysis should therefore be treated as CPU-backed
Compared Campaigns¶
- Current campaign:
.artifacts/benchmarks/short-duckdb - Preceding campaign:
.artifacts/benchmarks/short-sqlite - Previous reports examined:
.artifacts/analysis/2026-05-03-measurement-campaign-analysis.md.artifacts/analysis/2026-05-16-measurement-campaign-analysis.md
Metadata:
| Campaign | Run time | Version | Commit | Manifest | Active backend evidence |
|---|---|---|---|---|---|
.artifacts/benchmarks/short-sqlite |
2026-05-18T06:52:19Z |
1.24.2 |
d877d4475824faf003abf40c12c29e11bcb83aae |
benchmarks/performance/short_benchmark.local.json |
indexes contain .codira/index.db |
.artifacts/benchmarks/short-duckdb |
2026-05-18T13:28:17Z |
1.24.2 |
d877d4475824faf003abf40c12c29e11bcb83aae |
benchmarks/performance/short_benchmark.local.json |
indexes contain .codira/index.duckdb |
Git-history comparison:
- Both campaigns record the same commit:
d877d4475824faf003abf40c12c29e11bcb83aae. git log d877d4475824faf003abf40c12c29e11bcb83aae..d877d4475824faf003abf40c12c29e11bcb83aaereturned no commits.- The difference between the two newest campaigns is therefore not a software-version delta. It is a same-version backend comparison between SQLite and DuckDB.
git show --no-patch --format=fuller d877d4475824faf003abf40c12c29e11bcb83aaeidentifies the commit aschore(release): 1.24.2 [skip ci], committed on2026-05-18T06:18:54Z.
Measurement notes:
- The short campaign covers three repositories:
codira,cpython, andredis. - Hyperfine means are the primary top-level comparison surface.
scripts/benchmark_index.pyrecordstimings.totalas wall-clock for one index pass.timings.embeddingsis cumulative diagnostic hook time and is not additive tototal.- The GPU is present and visible on the host, but the benchmarked runs should still be treated as CPU-backed because
CODIRA_EMBED_DEVICEwas unset.
Objective 1: Performance Evolution¶
Overall result:
- Comparable Hyperfine measurements:
18 - Improved measurements:
2 - Regressed measurements:
16 - The current DuckDB run is substantially slower than the preceding SQLite run for full indexing, warm indexing, and
ctxonlarge-cpython.
Aggregate by command family:
| Family | SQLite sum | DuckDB sum | Absolute delta | Delta % |
|---|---|---|---|---|
index --full |
1245.346s | 7285.324s | +6039.977s | +485.00% |
warm index |
9.347s | 1451.576s | +1442.229s | +15429.41% |
ctx |
39.054s | 1336.439s | +1297.385s | +3322.03% |
sym |
0.228s | 0.917s | +0.689s | +302.60% |
symlist |
0.227s | 0.383s | +0.156s | +68.54% |
emb |
5.640s | 5.736s | +0.096s | +1.70% |
audit |
0.209s | 0.302s | +0.093s | +44.50% |
calls |
0.213s | 0.297s | +0.084s | +39.55% |
plugins |
0.190s | 0.195s | +0.006s | +3.00% |
caps |
0.160s | 0.164s | +0.003s | +2.16% |
cov |
0.173s | 0.175s | +0.001s | +0.72% |
help |
0.146s | 0.142s | -0.005s | -3.08% |
Largest regressions:
| Benchmark | SQLite | DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
large-cpython index --full |
993.287s | 6264.873s | +5271.586s | +530.72% |
large-cpython ctx --json getText |
26.282s | 1324.590s | +1298.308s | +4939.97% |
large-cpython warm index |
7.472s | 1264.227s | +1256.755s | +16819.34% |
medium-redis index --full |
205.823s | 824.763s | +618.940s | +300.71% |
small-codira index --full |
46.236s | 195.687s | +149.451s | +323.23% |
medium-redis warm index |
1.473s | 147.909s | +146.436s | +9942.75% |
small-codira warm index |
0.402s | 39.440s | +39.038s | +9701.31% |
small-codira sym main --json |
0.228s | 0.917s | +0.689s | +302.60% |
Measured improvements:
| Benchmark | SQLite | DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
medium-redis ctx --json 'hdr histogram' |
6.917s | 5.985s | -0.932s | -13.47% |
small-codira help |
0.146s | 0.142s | -0.005s | -3.08% |
Index phase deltas:
| Phase | SQLite | DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
large-cpython indexing |
919.503s | 6868.534s | +5949.031s | +646.98% |
large-cpython total |
977.551s | 6924.774s | +5947.222s | +608.38% |
medium-redis indexing |
195.060s | 846.532s | +651.473s | +333.99% |
medium-redis total |
199.807s | 850.894s | +651.087s | +325.86% |
large-cpython embeddings |
1793.998s | 2062.321s | +268.323s | +14.96% |
small-codira indexing |
42.084s | 204.841s | +162.757s | +386.74% |
small-codira total |
44.538s | 207.076s | +162.538s | +364.94% |
small-codira embeddings |
82.781s | 92.947s | +10.167s | +12.28% |
medium-redis embeddings |
384.061s | 342.058s | -42.003s | -10.94% |
Storage-size comparison:
| Repository | SQLite index | DuckDB index | Absolute delta | DuckDB / SQLite |
|---|---|---|---|---|
small-codira |
16,003,072 bytes | 120,598,528 bytes | +104,595,456 bytes | 7.54x |
medium-redis |
75,685,888 bytes | 383,791,104 bytes | +308,105,216 bytes | 5.07x |
large-cpython |
646,705,152 bytes | 1,905,274,880 bytes | +1,258,569,728 bytes | 2.95x |
Conclusion for Objective 1:
- The current campaign confirms the previous report's DuckDB regression with a cleaner same-version backend comparison.
- Because both campaigns use commit
d877d447..., the regression cannot be attributed to code changes between the two campaigns. - The dominant regression is in DuckDB indexing and warm-index refresh behavior.
- CPU embeddings remain expensive, but they do not explain the largest delta. For
large-cpython,indexingregressed by+5949.031s, whileembeddingsregressed by+268.323s. - The current hardware does not explain the regression; the SQLite campaign on the same hardware and same commit is much faster.
Backend-Agnostic Core Impact On SQLite¶
Comparison:
- Earlier SQLite campaign subset:
.artifacts/20260502T172501Z, version1.21.7, commit2a5cbaec12ab3d9874257e8f8c34622120828c8f - Current SQLite campaign subset:
.artifacts/benchmarks/short-sqlite, version1.24.2, commitd877d4475824faf003abf40c12c29e11bcb83aae - Matched Hyperfine measurements:
18 - Matched repositories:
small-codira,medium-redis,large-cpython
Aggregate by command family:
| Family | Earlier SQLite | Current SQLite | Absolute delta | Delta % |
|---|---|---|---|---|
index --full |
1232.721s | 1245.346s | +12.625s | +1.02% |
ctx |
36.493s | 39.054s | +2.561s | +7.02% |
warm index |
8.737s | 9.347s | +0.610s | +6.98% |
emb |
5.733s | 5.640s | -0.093s | -1.63% |
symlist |
0.198s | 0.227s | +0.029s | +14.74% |
audit |
0.184s | 0.209s | +0.025s | +13.78% |
sym |
0.204s | 0.228s | +0.024s | +11.74% |
calls |
0.190s | 0.213s | +0.024s | +12.44% |
caps |
0.176s | 0.160s | -0.016s | -8.93% |
plugins |
0.178s | 0.190s | +0.012s | +6.54% |
cov |
0.180s | 0.173s | -0.007s | -3.72% |
help |
0.148s | 0.146s | -0.002s | -1.45% |
Largest SQLite deltas:
| Benchmark | Earlier SQLite | Current SQLite | Absolute delta | Delta % |
|---|---|---|---|---|
small-codira index --full |
38.587s | 46.236s | +7.650s | +19.82% |
medium-redis index --full |
201.274s | 205.823s | +4.549s | +2.26% |
large-cpython ctx --json getText |
23.947s | 26.282s | +2.334s | +9.75% |
large-cpython index --full |
992.861s | 993.287s | +0.426s | +0.04% |
large-cpython warm index |
7.158s | 7.472s | +0.314s | +4.39% |
medium-redis warm index |
1.173s | 1.473s | +0.300s | +25.56% |
Interpretation:
- The backend-agnostic core evolution did not produce a large SQLite performance win on the short matched subset.
- It also did not produce a catastrophic SQLite regression.
- The largest absolute SQLite regression is
+7.650sonsmall-codira index --full; on the largest matched repository,large-cpython index --fullis effectively flat at+0.426s,+0.04%. - Therefore the current severe slowdown is not a backend-agnostic core regression visible on SQLite code paths. It is specific to the DuckDB backend path.
Detailed SQLite regression assessment:
- The operator's concern is valid: the expected direction was progress, and several user-facing SQLite commands regressed.
- The most relevant regressions on the short subset are:
large-cpython ctx --json getText:+2.334s,+9.75%medium-redis warm index:+0.300s,+25.56%small-codira symlist:+0.029s,+14.74%small-codira audit:+0.025s,+13.78%small-codira sym:+0.024s,+11.74%small-codira calls:+0.024s,+12.44%- The absolute deltas for
sym,symlist,audit, andcallsare small, but they are still noticeable because these commands are interactive. - This should not be accepted as an unavoidable cost of backend agnosticism.
Why the regression is probably optimizable:
- The backend-agnostic core uses an explicit plugin/backend dispatch point through
active_index_backend(). - Query functions such as
find_logical_symbols()now delegate through the active backend interface instead of binding directly to SQLite helpers. - That abstraction is architecturally necessary, but the current cost model does not require repeated registry/plugin resolution, connection setup, or redundant query-side conversions on every small command.
- The protocol boundary in
BackendQueryConnectionis minimal and does not by itself require a large runtime tax.
Likely optimization directions:
-
Cache active backend factories or backend instances per process for command execution.
Rationale:
- Small commands are sensitive to fixed overhead.
- Plugin snapshot and backend selection should be paid once per command process, not repeatedly across helper calls.
Expected gain:
- Low-to-medium absolute gain, but directly relevant to
sym,symlist,audit, andcalls.
-
Reuse backend connections within a single command.
Rationale:
ctxand graph expansion can perform several related backend lookups.- Passing and reusing a connection through the whole command path should avoid repeated open/close or adapter setup costs.
Expected gain:
- Most relevant for
ctx,calls, and graph/reference expansion.
-
Fuse small query backend calls where the backend boundary added round trips.
Rationale:
- Backend agnosticism should not mean every logical step becomes a separate backend call.
- Backend APIs can expose coarse-grained operations while preserving a backend-neutral contract.
Expected gain:
- Potentially meaningful for
ctx, where a+2.334sSQLite regression onlarge-cpythonis large enough to investigate.
-
Add a SQLite regression budget to the benchmark gate.
Rationale:
- Backend-agnostic refactors should keep SQLite as the control backend.
- A practical gate should fail or warn when core refactors regress key SQLite commands beyond a small threshold.
Suggested first thresholds:
ctx: investigate at>5%or>1s- warm
index: investigate at>10%or>0.25s sym,symlist,audit,calls: investigate at>10%even if absolute time is small
Conclusion:
- The SQLite regressions are not dramatic, but they are real enough to track.
- They are not a necessary consequence of backend agnosticism. They are more likely an implementation tax from backend dispatch, plugin lookup, connection lifecycle, or extra query-layer work introduced during the backend-agnostic migration.
- The right response is a targeted SQLite control-backend optimization pass, not backing out the backend-agnostic architecture.
DuckDB Evolution And Current Badness¶
DuckDB evolution comparison:
- Earlier DuckDB campaign subset:
.artifacts/benchmarks/backend/20260511T151057Z, version1.23.6.post1.dev1, commitd7c0182b83d0783b8a0a9f01f284c82ef0cacc5c - Current DuckDB campaign subset:
.artifacts/benchmarks/short-duckdb, version1.24.2, commitd877d4475824faf003abf40c12c29e11bcb83aae - Matched Hyperfine measurements:
18
Aggregate DuckDB movement:
| Family | Earlier DuckDB | Current DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
index --full |
8232.385s | 7285.324s | -947.061s | -11.50% |
warm index |
1593.516s | 1451.576s | -141.940s | -8.91% |
ctx |
1430.318s | 1336.439s | -93.878s | -6.56% |
emb |
5.863s | 5.736s | -0.127s | -2.17% |
sym |
1.020s | 0.917s | -0.103s | -10.12% |
Largest DuckDB changes:
| Benchmark | Earlier DuckDB | Current DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
large-cpython index --full |
7166.569s | 6264.873s | -901.696s | -12.58% |
large-cpython warm index |
1406.628s | 1264.227s | -142.401s | -10.12% |
large-cpython ctx --json getText |
1417.332s | 1324.590s | -92.742s | -6.54% |
medium-redis index --full |
887.202s | 824.763s | -62.439s | -7.04% |
small-codira index --full |
178.613s | 195.687s | +17.074s | +9.56% |
Current DuckDB vs current SQLite:
| Family | Current SQLite | Current DuckDB | Absolute delta | Delta % |
|---|---|---|---|---|
index --full |
1245.346s | 7285.324s | +6039.977s | +485.00% |
warm index |
9.347s | 1451.576s | +1442.229s | +15429.41% |
ctx |
39.054s | 1336.439s | +1297.385s | +3322.03% |
sym |
0.228s | 0.917s | +0.689s | +302.60% |
symlist |
0.227s | 0.383s | +0.156s | +68.54% |
Interpretation:
- DuckDB improved since the previous DuckDB campaign, especially on
large-cpython index --full(-901.696s,-12.58%), but it remains far outside an acceptable backend parity envelope. - The current badness is not marginal. It is multi-hundred-percent slower for full indexing and orders of magnitude slower for warm indexing.
- The fact that current SQLite remains close to the earlier SQLite subset, while DuckDB remains dramatically slower, isolates the problem to DuckDB backend architecture and implementation.
Objective 2: Hotspots And Performance-Sensitive Areas¶
Hotspots in importance order:
-
DuckDB full-index persistence and compatibility path
Evidence:
index --fullregressed by+6039.977sacross the short campaign.large-cpython indexingregressed by+5949.031s.- Current DuckDB
large-cpython-index.profis dominated by import-resolution frames: <frozen importlib._bootstrap>:_find_spec:6025.842s,28,838,992calls<frozen importlib._bootstrap_external>:find_spec:5375.489s,28,838,982calls<frozen importlib._bootstrap_external>:_get_spec:5354.058s,28,838,982calls<frozen importlib._bootstrap_external>:find_spec:4922.607s,346,030,708calls- Current DuckDB
small-codira-index.profhas the same shape: _find_spec:182.435s,769,627calls- external
find_spec:163.075s,769,617calls - Preceding SQLite index profiles are instead dominated by
_flush_embedding_rowsandembed_texts. - DuckDB index files are substantially larger than SQLite index files on the same corpus.
Relevant code areas:
packages/codira-backend-duckdb/src/codira_backend_duckdb/__init__.py::DuckDBIndexBackend.persist_analysispackages/codira-backend-duckdb/src/codira_backend_duckdb/__init__.py::DuckDBConnection.executepackages/codira-backend-duckdb/src/codira_backend_duckdb/__init__.py::DuckDBConnection.executemanypackages/codira-backend-duckdb/src/codira_backend_duckdb/__init__.py::_duckdb_lastrowidpackages/codira-backend-duckdb/src/codira_backend_duckdb/duckdb_support.py::_store_analysis
-
DuckDB warm-index and
ctxfreshness pathEvidence:
- Warm
indexregressed by+1442.229sacross only three repositories. large-cpython warm indexregressed by+1256.755s.large-cpython ctx --json getTextregressed by+1298.308s.- Current DuckDB
large-cpython-ctx.profis also dominated by import-resolution frames: _find_and_load:375.928s_find_and_load_unlocked:333.310s_find_spec:328.531s- external
find_spec:299.612s
Relevant code areas:
src/codira/cli.py::_ensure_indexsrc/codira/cli.py::_run_locked_index_refreshsrc/codira/indexer.py::index_repo- DuckDB backend load/reuse paths for existing file hashes, runtime inventory, and connection setup
- Warm
-
CPU embedding inference
Evidence:
- Current DuckDB full-index embedding diagnostic time remains high:
large-cpython:2062.321smedium-redis:342.058ssmall-codira:92.947sCODIRA_EMBED_DEVICEis unset, and_configured_embedding_device()returnscpu.- The GPU is available on the host, but GPU acceleration was not used by the benchmarked runs because
CODIRA_EMBED_DEVICEwas unset. - This remains a real hotspot, but it is not the primary DuckDB-vs-SQLite regression in this campaign.
Relevant code areas:
src/codira/semantic/embeddings.py::embed_textssrc/codira/semantic/embeddings.py::_configured_embedding_device- backend embedding flush/persistence helpers
-
Small query surfaces under DuckDB
Evidence:
small-codira sym main --json:+0.689s,+302.60%small-codira symlist --json --limit 20:+0.156s,+68.54%small-codira audit --json:+0.093s,+44.50%small-codira calls main --json:+0.084s,+39.55%
Relevant code areas:
src/codira/query/exact.pysrc/codira/query/graph_enrichment.py- DuckDB query adapter methods and backend query implementations
-
Semantic retrieval and vector scoring
Evidence:
- Previous reports identified vector retrieval and Python-side similarity scoring as performance-sensitive.
- In the current short campaign, semantic retrieval is not the largest first-order regression because
large-cpython ctxis dominated by the DuckDB warm-refresh/import-resolution path. - Vector-native retrieval remains important, but it is already covered by open issue
#20.
Relevant code areas:
src/codira/query/context.py::_retrieve_embedding_candidatessrc/codira/query/producers.py::retrieve_candidates- backend
embedding_candidates()implementations
-
JSON analyzer accepted-file double-load path
Evidence:
- The 2026-05-03 report evaluated and rejected the accepted-file double-load optimization after measuring the
cldr-jsoncorpus. - The 2026-05-16 report explicitly kept that no-go decision.
- The current short campaign does not include
cldr-jsonand provides no new fact that invalidates the earlier decision.
- The 2026-05-03 report evaluated and rejected the accepted-file double-load optimization after measuring the
Conclusion:
- Do not reopen this as an immediate performance action.
Objective 3: Performance Enhancement Plan¶
Assessment on current hardware:
- There is significant performance headroom.
- The data does not support a hardware-limited conclusion.
- The dominant issue is backend implementation behavior under DuckDB, not CPU/GPU capacity.
- Heavy refactoring can produce large gains if the DuckDB backend moves away from row-by-row SQLite-shaped compatibility behavior toward backend-native bulk persistence and bounded warm-index checks.
Internet and documentation check:
- DuckDB's Python DB-API documentation warns not to use Python
executemanyfor large data ingestion and points users to bulk-oriented alternatives:https://duckdb.org/docs/stable/clients/python/dbapi. - DuckDB's appender documentation describes appenders as the efficient path for loading data, with batched/cached appends rather than statement-by-statement insertion:
https://duckdb.org/docs/current/clients/c/appender.htmlandhttps://duckdb.org/docs/current/data/appender.html. - DuckDB's data import overview documents several efficient ingestion methods beyond ordinary
INSERTstatements:https://duckdb.org/docs/current/data/overview. - SQLite documentation explains the classic transaction-cost issue for many small writes; this is relevant background, but the current SQLite backend already behaves much better than DuckDB on the measured workload:
https://www.sqlite.org/faq.html. - SQLite does not expose a DuckDB-style appender API. Its efficient ingestion path is explicit transactions, prepared statement reuse, and, where applicable, multi-row
INSERTorINSERT INTO ... SELECT .... - The practical SQLite conclusion for Codira is that further write-path gains may exist through staging tables or table-oriented inserts, but the current SQLite backend is already close enough to its earlier performance that this is secondary to DuckDB.
- These sources do not support treating the measured
+485%full-index and+15429%warm-index DuckDB regressions as an inherent DuckDB-vs-SQLite law. They support the opposite conclusion: DuckDB is being driven through an unsuitable SQLite-shaped row-write interface.
Salvageability assessment:
- The DuckDB backend is salvageable, but not by small query tweaks.
- A substantial refactor is required.
- A complete rewrite is not yet justified because:
- query parity and backend activation already exist;
- the failure is concentrated in persistence, warm-index, and adapter behavior;
- the current DuckDB backend already improved from the previous DuckDB campaign by
-11.50%on aggregate full indexing and-8.91%on aggregate warm indexing. - It is not a lost cause because the current SQLite backend proves that the core workload can be indexed much faster on the same codebase, hardware, and benchmark subset.
Required DuckDB plugin architecture changes:
-
Split the DuckDB backend into query-compatible and write-native layers.
Current problem:
- The backend exposes SQLite-like
execute,executemany, cursor, andlastrowidcompatibility around DuckDB. - This lets shared persistence helpers treat DuckDB like a DB-API SQLite replacement.
Required change:
- Keep a compatibility adapter only for query/read paths where it is cheap and correct.
- Add a DuckDB-native write pipeline owned by the plugin.
- Route full-index persistence through that write pipeline instead of through SQLite-shaped row operations.
- The backend exposes SQLite-like
-
Replace row-by-row persistence with table-oriented batch persistence.
Current problem:
DuckDBConnection.executemany()delegates to the Python DB-API surface.DuckDBConnection.execute()calls_duckdb_lastrowid()after sequence-backed inserts, which can add one sequence lookup per inserted row.
Required change:
- Accumulate per-file analysis artifacts into table-specific buffers:
filesmodulesclassesfunctionsimportsoverloadsenum_membersdocstring_issuessymbol_indexembeddings- graph edge tables
- Flush those buffers with DuckDB-native bulk ingestion rather than per-row inserts.
- Use explicit ID allocation in Python for a file's persistence batch, or use a backend-owned deterministic ID mapping, so code does not need per-row
lastrowidemulation.
-
Make transaction boundaries batch-scoped, not file-row scoped.
Current problem:
- The plugin emulates SQLite savepoint semantics by committing and starting a transaction around file writes on a shared connection.
Required change:
- Use one controlled transaction per index batch or per bounded chunk of files.
- Preserve failure isolation by staging rows in memory until a file analysis is complete, then append that file's rows atomically within the current batch.
- If per-file rollback is required by contract, implement it at the staging layer before rows reach DuckDB, not by making DuckDB imitate SQLite savepoints for every file.
Current rollback contract and backend-specific recommendation:
- The formal backend contract for
persist_analysis()says only that a backend persists one normalized file snapshot and returns recomputed/reused semantic-artifact counts. - The contract does not explicitly require per-file database rollback.
- The current indexer behavior does require deterministic per-file failure reporting:
_persist_indexed_file_analyses()catches persistence failures, records anIndexFailure, and continues with the remaining parsed files. - Therefore the semantic requirement is not "use a database rollback per file"; it is "a failed file must not leave partial rows that corrupt the final committed index."
SQLite situation:
index_repo()opens one shared backend connection for the run and commits after persistence, derived-index rebuild, and runtime inventory updates.- SQLite
persist_analysis()wraps each file inSAVEPOINT persist_analysiswhen called with that shared connection. - On one file persistence failure, SQLite rolls back to the savepoint, releases it, reports the failure through the indexer, and continues.
- This is a reasonable implementation for SQLite incremental indexing because SQLite supports savepoints natively and the existing persistence path is row-oriented.
- It is still overhead, but the current data does not identify SQLite savepoints as the dominant SQLite regression:
large-cpython index --fullis effectively flat between earlier SQLite and current SQLite (+0.426s,+0.04%).
DuckDB situation:
- DuckDB does not support the SQLite savepoint pattern used by the SQLite backend.
- The current DuckDB plugin substitutes per-file
COMMIT,BEGIN TRANSACTION, andCOMMIT/ROLLBACKaround each file when using a shared connection. - That preserves a form of per-file isolation, but it is the wrong hot-path shape for DuckDB full indexing and fights the backend-native bulk-ingestion model.
Recommendation:
- Keep the backend-neutral semantic invariant: failed files must not leave partial rows, and valid files must be reported deterministically.
- Do not encode per-file database rollback as a required backend contract.
- Let SQLite continue to use savepoints for incremental indexing unless a dedicated SQLite benchmark proves the overhead matters.
- Move DuckDB full indexing to in-memory per-file staging plus batch-oriented writes. If a file fails before staging completes, skip it. If a batch write fails, fail the batch/run or retry with smaller diagnostic chunks.
- For incremental DuckDB indexing, use staging-and-swap or bounded chunk transactions rather than per-file transaction cycling.
- If a later persistence redesign touches both backends, express the contract as "file write atomicity or staged discard on failure," not "savepoint-compatible per-file rollback."
-
Separate warm-index metadata checks from expensive persistence setup.
Current problem:
- Warm
indexandctxare orders of magnitude slower under DuckDB, which means unchanged repositories are not staying on a cheap readiness path.
Required change:
- Add a minimal metadata/readiness path that opens DuckDB only enough to read file hashes, schema metadata, analyzer inventory, and runtime inventory.
- Avoid schema repair, bulk writer setup, derived index rebuilds, and embedding machinery unless the readiness check proves they are needed.
- Add a regression benchmark for unchanged DuckDB indexes.
- Warm
-
Keep query optimization secondary.
Current problem:
sym,symlist,audit, andcallsare slower under DuckDB, but their absolute deltas are small compared with indexing and warm refresh.
Required change:
- Do not spend first-pass effort on query micro-optimization.
- Re-measure after the write path and warm path are fixed, then optimize only remaining measured overhead.
Local GPU assessment:
- The host GPU is visible and usable according to the operator-provided
nvidia-smioutput. - Current Codira embeddings use
sentence-transformers/all-MiniLM-L6-v2, with fixed dimension384. - Device selection is controlled by
CODIRA_EMBED_DEVICE; unset meanscpu. - The current default embedding batch size is
32. - For the local NVIDIA GeForce GTX 1050 with
2 GiBVRAM, GPU use may help long full-index embedding phases, but it is not a safe default without calibration because: - model and CUDA/Torch startup overhead can dominate short
ctxruns; 2 GiBVRAM leaves limited headroom for batch size and desktop GPU usage;- the current DuckDB bottleneck is much larger than embedding-device effects.
- Recommended decision: benchmark before changing defaults. Run the short SQLite campaign with
CODIRA_EMBED_DEVICE=cpuandCODIRA_EMBED_DEVICE=cuda, comparetimings.embeddings, full-index Hyperfine means, andctxlatency, then decide whether to encode a host-local setting.
Decision:
- Salvage path: substantial refactor of the DuckDB plugin persistence and warm-index architecture.
- Rewrite threshold: if a focused prototype using DuckDB-native bulk writes cannot get
small-codiraandmedium-redisfull-index times within roughly2xof SQLite while preserving query parity, then a plugin rewrite becomes justified. - Lost-cause threshold: only if the native-write prototype still shows unavoidable multi-x slowdown after removing DB-API row insertion,
lastrowidemulation, and warm-path overwork. Current evidence has not reached that threshold.
Plan in importance order:
-
Make DuckDB persistence backend-native.
Scope:
- Instrument
DuckDBIndexBackend.persist_analysis,DuckDBConnection.execute,DuckDBConnection.executemany,_duckdb_lastrowid, and_store_analysis. - Confirm the source of import-resolution churn seen in
large-cpython-index.profandsmall-codira-index.prof. - Replace the confirmed slow path with DuckDB-native batch persistence.
- Avoid per-row sequence round trips where possible.
- Preserve SQLite behavior and DuckDB query parity.
Expected gain:
- Current short-campaign
index --fulloverhead is+6039.977s. - A reasonable first target is reclaiming
50%to80%of that DuckDB-specific overhead after the exact frame-level cause is confirmed. - Approximate per-repository savings at that target:
large-cpython:2600sto4200smedium-redis:300sto500ssmall-codira:75sto120s
Risk:
- Medium-to-heavy backend refactor.
- Must preserve deterministic IDs, relation tables, graph query parity, and embedding reuse.
- Instrument
-
Fix DuckDB warm-index and
ctxrefresh behavior.Scope:
- Profile warm
codira indexseparately on the three short-campaign corpora. - Add phase-level timing for existing file hash loading, runtime inventory loading, analyzer inventory loading, derived-index rebuild, connection open/close, and unchanged-file decisions.
- Confirm that unchanged files do not re-enter expensive persistence or embedding paths.
- Fix the first confirmed warm-path bottleneck.
- Add a bounded-regression benchmark for unchanged DuckDB indexes.
Expected gain:
- Current short-campaign warm-index overhead is
+1442.229s. - Current
large-cpython ctxoverhead is+1298.308s. - A realistic immediate target is restoring warm
indexto within2xof the SQLite campaign for unchanged repositories. That would save more than1200sonlarge-cpython warm indexalone.
Risk:
- Medium.
- It may share the same root cause as action 1, but the
ctxpath must be verified independently because it has user-facing latency impact.
- Profile warm
-
Re-measure and then clean up DuckDB small-query overhead.
Scope:
- Run
sym,symlist,audit,calls, andcovagainst fixed DuckDB and SQLite indexes after actions 1 and 2. - Attribute remaining overhead to SQL shape, adapter cost, connection setup, or result conversion.
- Optimize only measured remaining regressions.
Expected gain:
- Low absolute campaign gain, below
1sper measured command in this run. - Useful for CLI responsiveness after larger backend regressions are fixed.
Risk:
- Low-to-medium.
- Not first-order until full-index and warm-index regressions are fixed.
- Run
-
Keep vector-native semantic retrieval on the existing roadmap.
Scope:
- Do not open a new immediate action for this in this report.
- Continue to treat it as part of issue
#20.
Expected gain:
- Prior estimate remains plausible:
15%to35%on semantic-heavy large-repoctx. - It is not the immediate worst offender in the current short campaign.
Risk:
- Heavy architecture work.
-
Keep embedding hardware/runtime tuning on the existing roadmap.
Scope:
- Do not open a new immediate action for calibration.
- Continue to treat calibration as issue
#28, configuration as issues#17and#27, and daemon/runtime lifetime as issue#22. - Do not assume GPU gains until the embedding stack is verified on GPU and
CODIRA_EMBED_DEVICEis explicitly configured.
Expected gain:
- Previous estimates remain plausible for large full indexes, but the current same-version backend comparison shows a larger DuckDB-specific regression.
Risk:
- Medium-to-heavy because this crosses hardware detection, configuration, and runtime lifecycle.
-
Do not reopen the JSON accepted-file double-load optimization.
Reason:
- It was evaluated and discarded in the 2026-05-03 report.
- The 2026-05-16 report preserved that no-go decision.
- The current short campaign provides no new JSON-heavy evidence.
Objective 4: Roadmap Snapshot Cross-Reference¶
Snapshot validation:
issues.jsonexists and parses.milestones.jsonexists and parses.issues.json:data.repository.issues.totalCount = 16,hasNextPage = false.milestones.json:data.repository.milestones.totalCount = 5,hasNextPage = false.- Nested milestone issue lists have
hasNextPage = false. - Conclusions in this section are based only on local snapshot files.
Existing open issues related to Objective 3:
| Objective 3 area | Open issue relation | Match quality | Reason |
|---|---|---|---|
| Vector-native semantic retrieval | #20 Feature: Introduce Optional Vector Database Backend for Semantic Retrieval |
Strong | Issue body targets optional vector-database-backed semantic retrieval. |
| Embedding runtime lifetime | #22 Feature: Daemon Mode for Incremental File Watching and Automatic Reindexing |
Partial | Daemon mode can keep process/runtime state warm, but the issue is broader than embedding inference. |
| Embedding hardware tuning | #28 feat(embeddings): Add embeddings calibration script (hardware-aware auto-tuning) |
Strong for calibration | Issue body covers device selection, thread count, batch size, and GPU memory limits. |
| Configuration needed for tuning | #17 Introduce install-time configuration system for Codira (hardware + plugins) and |
||
#27 Introduce configuration injection in core <-> plugin interface |
Supporting | These issues provide configuration surfaces, not direct performance fixes. |
Closed or not-open issue check:
- The local open issue snapshot does not list issue
#10. milestones.jsonlists issue#10 Enable immediate support for multiple production-grade backendsasCLOSEDunder Phase 1.git log --grep='#10\|issue-10\|DuckDB\|duckdb\|backend' --allshows the DuckDB implementation and follow-up fixes already landed, including:d18b0bc feat(backend): scaffold duckdb backend package835d2d6 feat(backend): implement duckdb backend lifecycle0efb1f8 feat(backend): integrate duckdb activation path2193b60 fix(backend): replace DuckDB savepoints with shared transactionsa46a9db fix(backend): restore DuckDB parity with SQLite index behavior- later backend-agnostic migration commits through
8b9e8b9 refactor(backend): complete backend-agnostic core migration - Based on the local snapshots and git history, the current DuckDB performance regression is not covered by an open issue.
Completely new points relative to the open roadmap:
- DuckDB full-index persistence performance parity.
- DuckDB warm-index and automatic refresh performance parity.
- DuckDB small-query overhead cleanup after the larger backend regressions are fixed.
Actions that should immediately increase performance, excluding actions already covered by open issues:
-
Create and implement a DuckDB persistence performance task.
Detailed plan:
- Add a focused benchmark that runs the same representative workload with
CODIRA_INDEX_BACKEND=sqliteandCODIRA_INDEX_BACKEND=duckdb. - Include at least:
small-codiramedium-redis- one large Python-heavy corpus, because
large-cpythonis the strongest measured regression - Instrument:
DuckDBIndexBackend.persist_analysisDuckDBConnection.executeDuckDBConnection.executemany_duckdb_lastrowid_store_analysis- Confirm whether the import-resolution hotspot is caused by repeated backend driver loading, driver-level parameter binding, per-row sequence lookup, or another backend-local path.
- Replace the confirmed slow path with DuckDB-native bulk persistence.
- Validate:
- identical indexed/reused/deleted/failed counts
- equivalent
sym,ctx,calls,refs,audit, andcovresults on the benchmarked corpus - improved Hyperfine and index-phase timings versus
.artifacts/benchmarks/short-duckdb
Expected immediate gain:
- Highest expected gain among new actions.
- Target: reclaim at least half of the current DuckDB full-index overhead before broader backend work continues.
- Add a focused benchmark that runs the same representative workload with
-
Create and implement a DuckDB warm-index/readiness performance task.
Detailed plan:
- Benchmark warm
codira indexandctxon already-indexed DuckDB output directories. - Add phase-level timing around:
- file hash loading
- analyzer inventory loading
- runtime inventory loading
- derived-index rebuild checks
- connection open and schema repair
- unchanged-file skip decisions
- Confirm that unchanged files stay on the reuse path.
- Fix the first confirmed warm-path bottleneck.
- Add a focused regression benchmark or test proving bounded unchanged-index behavior.
Expected immediate gain:
- Very high if it restores warm indexing close to the SQLite campaign.
- Current measured upper bound from the short campaign is
+1442.229saggregate warm-index overhead.
- Benchmark warm
-
Re-measure and clean up DuckDB small-query overhead only after actions 1 and 2.
Detailed plan:
- Re-run the same short benchmark command set after the persistence and warm-index fixes.
- If
sym,symlist,audit, orcallsstill regress materially, attribute the remaining overhead to adapter, SQL, connection, or result-conversion cost. - Patch only the confirmed bottleneck.
Expected immediate gain:
- Small in total runtime, but useful for CLI responsiveness.
- Current measured upper bound is
+0.689sforsmall-codira sym.
Actions explicitly not proposed as new immediate work:
- Do not propose JSON accepted-file double-load reuse. It was evaluated and discarded in the previous reports, and the current campaign does not invalidate that decision.
- Do not propose vector-native retrieval as a new action. It is already covered by open issue
#20. - Do not propose embedding calibration as a new action. It is already covered by open issue
#28. - Do not propose configuration plumbing for tuning as a new action. It is already covered by open issues
#17and#27. - Do not propose daemon/runtime lifetime as a new action. It is already partially covered by open issue
#22.
Final Assessment¶
The current campaign is a same-version backend comparison, and it confirms that DuckDB remains the dominant performance problem. The worst offender is DuckDB indexing, followed by DuckDB warm-index and ctx refresh behavior. The profile evidence is materially different from SQLite: DuckDB index and large ctx profiles are dominated by repeated import-resolution frames, while SQLite profiles are dominated by embedding work.
There is no basis for saying that no significant gain can be obtained on the current hardware. The preceding SQLite campaign on the same hardware and same commit is much faster. The first performance work should therefore target DuckDB persistence and warm-index behavior before reopening broader semantic retrieval, embedding runtime, or JSON analyzer ideas.