PaletteModel::rebuild splits the bulk score-only scan into per-thread
chunks via QtConcurrent::blockingMapped when item count exceeds
kParallelThreshold (10000). The scan is a pure map over read-only data
(FuzzyRanker::score is a stateless static; m_items not mutated); chunks
are concatenated in order and finalizeVisible imposes a deterministic
sort, so the result is bit-identical to the serial path regardless of
thread timing. Incremental (type-forward) path stays serial — it only
touches <=1000 items.
First-scan at 397k symbols (mobydick, 16 cores): ~373ms -> ~61ms.
Link Qt6::Concurrent into the palette lib. New test
parallelScanIsCorrectAndDeterministic (40k items) guards correctness.
Universal Ctags: SymbolIndex::ctagsBinary() prefers ctags-universal over
the alternatives-managed ctags (Exuberant 5.9 has no Kotlin/TS parser).
Kotlin/TypeScript now indexed — symbol count on mobydick: 17k -> 397k.
At 397k, per-keystroke ranking was 0.3-0.7s. Three structural fixes:
1. FuzzyRanker: replace std::set with vector+sort+unique; add
withRanges=false fast path that skips highlight computation during
the bulk scan (ranges materialised only for displayed rows).
2. PaletteModel: cap visible results to top-1000 (partial_sort),
score-only bulk, ranges for the top-K only. Nobody scrolls 400k.
3. Min query length (2): 1-char queries match everything and produce a
useless, expensive full scan; below the threshold the palette shows
the capped full list unranked. First real ranking at 2 chars; from
there incremental narrowing makes every keystroke sub-2ms.
Measured (mobydick 397k symbols, RelWithDebInfo, type-forward):
s: 337ms -> ~0ms | se: 507ms -> 373ms (1x first scan) |
ser: 586ms -> 1.9ms | server: 198ms -> 0.8ms.
3 new tests (incrementalMatchesFullScan, visibleResultsAreCapped,
shortQueryDoesNotRank). docs/PERF.md updated.
Two fixes for the Alt+G typing lag:
1. Default CMAKE_BUILD_TYPE to RelWithDebInfo when unspecified — the
shipped plugin was -O0. On mobydick (17k symbols) the worst
type-forward keystroke drops ~340ms -> ~29ms (~12x).
2. Debounce the PaletteWidget filter with an 80ms single-shot timer so a
fast typist's burst triggers one re-rank, not one per character.
applyQueryNow() flushes the pending query on Enter so activation uses
the latest text; activate() resets the debounce state.
The broad early-query keystrokes (1-2 chars matching most of N) are the
only ones still ~O(N); debounce ensures they are paid at most once per
pause. docs/PERF.md updated with measured numbers.
Add ProjectIndexer (project_lib, QtConcurrent + QFutureWatcher, 6 tests):
runs listFiles/listSymbols on the thread pool, delivers results on the UI
thread via filesReady/symbolsReady, caches per project root, and drops
stale results via a per-root generation counter.
Switchers now open the palette immediately — cache hit is instant, miss
shows a transient 'Indexing…' placeholder — and repopulate when the
background job finishes. The ~0.5-1s ctags stall on Alt+G no longer
blocks the UI thread. Cache invalidated on KateProjectBridge::
projectChanged. Register QList<Symbol> metatype; link Qt6::Concurrent.
Item building factored into buildFileItems/buildSymbolItems (shared by
the cache and async paths).
Add a benchmark harness (src/project/bench_switcher.cpp, manual target)
that times the Go-to-File/Symbol pipeline against a real project and
writes a metrics file. Measured on mobydick (12.2k files, 17.2k symbols);
findings in docs/PERF.md.
Bottleneck: PaletteModel::setQuery re-scored all N every keystroke
(Debug: 270-600ms/keystroke; cost grew with query length). Fix: when the
query is a pure append, re-score only the currently-visible subset — the
match set is monotonic under appending, so this is exact. Later keystrokes
drop from O(N) to O(matches) (symbols 'server' 30ms -> 9ms in Release).
Guarded by test_palettemodel::incrementalMatchesFullScan.
Also documented: shipped plugin is a Debug build (~5-8x slower than
Release), and ctags/listFiles run ~0.1-1s synchronously on open (move
off-thread = open follow-up).