|
| 1 | +# Large Build Performance |
| 2 | + |
| 3 | +This guide records the measured changes that make a full CCG build practical on |
| 4 | +large source trees. It is scoped to parsing, graph persistence, edge resolution, |
| 5 | +and search-document rebuilding; it is not a general database tuning guide. |
| 6 | + |
| 7 | +## Benchmark scope |
| 8 | + |
| 9 | +The representative workload was a Kotlin corpus with 1,592 files (about 4.86 |
| 10 | +MB). Every comparison used a fresh SQLite database, the same binary and build |
| 11 | +options, and the same source snapshot. The figures are single fresh-build |
| 12 | +measurements: they identify the bottleneck, not a statistically rigorous |
| 13 | +throughput claim. |
| 14 | + |
| 15 | +The final graph contained 9,419 nodes, 6,942 persisted edges, and 9,402 search |
| 16 | +documents. |
| 17 | + |
| 18 | +| Stage | Before import-file index | After import-file index | |
| 19 | +| --- | ---: | ---: | |
| 20 | +| Parse and spool | 8,682 ms | 8,728 ms | |
| 21 | +| Node persistence | 594 ms | 560 ms | |
| 22 | +| Edge resolution | 16,712 ms | 888 ms | |
| 23 | +| Search rebuild | 315 ms | 307 ms | |
| 24 | +| Total | 26,442 ms | 10,603 ms | |
| 25 | + |
| 26 | +The import-file index reduced total build time by about 59.9% and edge |
| 27 | +resolution by about 94.7% for this workload. The parser worker pool had already |
| 28 | +reduced an earlier parse stage from 11,123 ms to 8,716 ms (about 21.6%). |
| 29 | + |
| 30 | +## What changed |
| 31 | + |
| 32 | +### Stage timing |
| 33 | + |
| 34 | +`BuildStats.Timing` and the build-complete log report parse, node persistence, |
| 35 | +edge resolution, search rebuild, and total duration. Profile a representative |
| 36 | +fresh build before changing concurrency, batches, or database configuration. |
| 37 | + |
| 38 | +### Bounded parsing concurrency |
| 39 | + |
| 40 | +The parse-and-spool stage uses four workers. A coordinator preserves input and |
| 41 | +spool-record order; database writes, edge resolution, and search rebuilding |
| 42 | +remain sequential inside their existing transaction. This keeps failure and |
| 43 | +cancellation behavior unchanged while using available CPU for parsing. |
| 44 | + |
| 45 | +### Edge batches |
| 46 | + |
| 47 | +Edges are resolved and upserted in batches capped at 4,000 edges. A one-off |
| 48 | +8,000-edge measurement was slightly slower (29,147 ms versus 29,107 ms total), |
| 49 | +so 4,000 remains the production limit. Treat a larger batch as a hypothesis to |
| 50 | +measure, not an automatic improvement. |
| 51 | + |
| 52 | +### Build-scoped import-file index |
| 53 | + |
| 54 | +The decisive bottleneck was import suffix resolution. Before the index, |
| 55 | +`GetFileNodesByPathSuffix` ran 1,983 times and spent 14,742 ms reading all file |
| 56 | +nodes in the namespace and comparing paths in Go. |
| 57 | + |
| 58 | +During full-build edge resolution, CCG now reads the namespace's real file |
| 59 | +nodes once, then builds an in-memory index for the transaction lifetime: |
| 60 | + |
| 61 | +- an exact directory map; |
| 62 | +- a directory-suffix map; and |
| 63 | +- the existing priority rule: exact directories win, otherwise every match at |
| 64 | + the longest suffix is returned. |
| 65 | + |
| 66 | +The index is created after file nodes are persisted and discarded when the full |
| 67 | +build's edge-resolution phase ends. Incremental builds and ordinary suffix |
| 68 | +queries retain their existing paths. This avoids a long-lived store cache and |
| 69 | +its invalidation requirements. |
| 70 | + |
| 71 | +## Correctness checks |
| 72 | + |
| 73 | +Performance changes were accepted only after comparing independently built, |
| 74 | +fresh databases. In both directions, natural-key comparisons found no |
| 75 | +differences in the final 9,419 nodes, 6,942 persisted edges, or 9,402 search |
| 76 | +documents. |
| 77 | + |
| 78 | +Focused tests also cover: |
| 79 | + |
| 80 | +- exact-directory priority; |
| 81 | +- ambiguous longest-suffix matches; |
| 82 | +- namespace and `kind=file` filtering; and |
| 83 | +- a single import-file read per build resolver. |
| 84 | + |
| 85 | +## Reproducing a measurement |
| 86 | + |
| 87 | +Use a disposable database and a stable source snapshot. For example: |
| 88 | + |
| 89 | +```bash |
| 90 | +ccg --db-driver sqlite --db-dsn /tmp/ccg-benchmark.db --log-json build /path/to/repository |
| 91 | +``` |
| 92 | + |
| 93 | +Read the build-complete JSON log fields for the stage durations. Repeat enough |
| 94 | +times to account for local CPU, filesystem cache, and database differences. |
| 95 | +Compare graph contents as well as elapsed time; a faster build that changes |
| 96 | +nodes, edges, or search documents is a regression. |
| 97 | + |
| 98 | +## PostgreSQL note |
| 99 | + |
| 100 | +PostgreSQL was not benchmarked for this change because no local PostgreSQL |
| 101 | +instance was available during the measurement. It was not the immediate next |
| 102 | +optimization: the former path transferred and scanned all file nodes 1,983 |
| 103 | +times, so changing database drivers alone would retain that work and may make |
| 104 | +it more expensive over a network. |
| 105 | + |
| 106 | +If PostgreSQL is the deployment target, repeat the same fresh-build and |
| 107 | +content-equivalence procedure against its real topology before tuning indexes, |
| 108 | +connection settings, or write concurrency. |
0 commit comments