Refactoring proposal, written on branch tmp_20260709102359. Part of the
Band 7 (architecture) "globals → owned state" effort, following the same
playbook as userfields: replace globals with a Parameters member
(a8391422) and, most closely, dynlibs: replace globals with a RAII DynamicLibraries class (4c6a8231).
Status: implemented on branch tmp_20260709102359 (commits 3a6838bf …
35fa6d75), awaiting human review and merge into dev. The RAII-class approach
(§5 Option B) and full removal of the transitional singleton (§8 Q1) were chosen.
Verified: full run_all_tests.sh and scripts/orient.sh (0 FAIL), api_examples
make test (30 PASS, release build), all three cross-compiles, and per-command
A/B byte-identical output. See §8 for how the open questions were resolved.
dbindex.h exports ten mutable globals that together form a single
process-wide k-mer index singleton:
extern unsigned int * kmercount; /* matches per kmer */
extern uint64_t * kmerhash; /* per-kmer offset into kmerindex */
extern unsigned int * kmerindex; /* the seqno lists */
extern struct bitmap_s * * kmerbitmap; /* per-kmer bitmaps (dense kmers) */
extern unsigned int * dbindex_map; /* index-slot -> seqno */
extern unsigned int dbindex_count; /* number of indexed sequences */
extern unsigned int kmerhashsize; /* 4^wordlength */
extern uint64_t kmerindexsize; /* total entries in kmerindex */
extern uhandle_s * dbindex_uh; /* unique-kmer finder (build only) */
extern unsigned int dbindex_wordlength; /* effective index width */
(bitmap_mincount is an eleventh piece of shared state, a file-static in
dbindex.cc.)
The goal is to fold these into a single owned object so that no command relies
on process-global index state — matching the reentrancy/thread-safety aim of
E4 and the "no non-const global variables" rule in CLAUDE.md.
- They are one object pretending to be ten. Every allocation in
dbindex_prepareand every free indbindex_freemoves them as a group; their invariants (kmerhash[i] + j < kmerindexsize,dbindex_count <= seqcount,kmerhashsize == 4^dbindex_wordlength) are cross-field and entirely implicit. dbindex_wordlengthis derived index state, not config. It is set bydbindex_prepare(fromparameters.opt_wordlength) and overwritten byudb_readwhen a UDB file declares a different width (udb.cc:407). It must not be migrated toParameters— that is exactly the trap recorded in thee1_f3_runtime_mutated_globalsnote (mutatingopt_wordlengthcaused a live orient SIGSEGV). It belongs inside the index object.- The read API is a thread-safety contract in disguise. The search engines
read the index concurrently from worker threads (
searchcore.ccsearch_topscores). That is safe only because the reads are const. Nothing in the current types says so.
Grouped by how they touch the index. This is the surface the refactor must thread an object reference through.
| File | Role | Touches |
|---|---|---|
dbindex.cc |
owner/impl | all globals + bitmap_mincount |
udb.cc |
owner + direct builder | fills the raw arrays from a UDB file (udb_read), reads them back when writing/reporting (udb_make, udb_stats, udb_info, udb_fasta) |
searchcore.cc |
hot-path reader | getters in search_topscores; dbindex_wordlength at :849 |
sintax.cc |
owner + hot-path reader | lifecycle + getters (:308-356) + dbindex_wordlength (:430) |
orient.cc |
owner + reader | lifecycle + dbindex_getmatchcount (:229-230) + dbindex_wordlength (:101,215) |
cluster.cc |
owner (incremental) | dbindex_prepare (no addall), repeated dbindex_addsequence, dbindex_wordlength (:597) |
chimera.cc |
owner | dbindex_prepare/addsequence/addallsequences/free |
search.cc |
owner | dbindex_prepare+addallsequences+free, or udb_read |
Key structural facts:
- Each command owns its own index for the duration of one run
(
prepare … use … free). There is no cross-command sharing, so each command can own a local object and thread a reference down its own call tree — the refactor is per-command and independently committable. searchinfo_salready carriesParameters const *(searchcore.h:162, added in the E1 shared-infra phase). Adding aDbindex const *beside it is the same, already-established pattern.udb_readis the odd one out. It builds the index by filling the raw arrays directly (bypassingdbindex_prepare) and is called by four commands (orient,sintax,search,chimera) plusudb.ccinternals. It must gain aDbindex &out-parameter to populate the caller's object.- The cluster session holds the index across calls (
cluster.h:81-118):cluster_session_initrequiresdbindex_prepareto have been called, and centroids are added incrementally via the session. The session must store aDbindex &(like it already stores aParameters const &).
dbindex.h is included by the public vsearch_api.h (:156), and the
lifecycle functions are called directly by the shipped examples:
api_examples/example_search.cc, example_cluster.cc, example_chimera.cc,
example_lifecycle.cc, example_reinit.cc, example_dbinfo.cc
So changing these signatures is an ABI/API break. Per the
e1_abi_decision note, Band 7 E1 already chose to break the
libvsearch_core ABI (Option B), so this is acceptable — but the change
must be done in lockstep:
- update
api_examples/*.cc, - update
LIBRARY_API.md(bump the documented API version), - re-run the library test net against a release build (the
library_tests_need_release_buildnote: a debuglibvsearch.asegfaults the examples via the_GLIBCXX_DEBUGABI mismatch).
Introduce one type in dbindex.h:
struct Dbindex
{
/* owned buffers (public: this is a data-carrying struct, and udb_read fills
them in place — see below). East-const, RAII-freed in the destructor. */
unsigned int * kmercount = nullptr;
uint64_t * kmerhash = nullptr;
unsigned int * kmerindex = nullptr;
bitmap_s * * kmerbitmap = nullptr;
unsigned int * dbindex_map = nullptr;
uhandle_s * uh = nullptr; /* build-time only */
unsigned int count = 0;
unsigned int hashsize = 0;
uint64_t indexsize = 0;
unsigned int wordlength = 0; /* effective index width (derived state) */
Dbindex() = default;
~Dbindex(); /* RAII: frees everything (was dbindex_free) */
Dbindex(Dbindex const &) = delete; /* owns raw buffers: non-copyable */
auto operator=(Dbindex const &) -> Dbindex & = delete;
/* lifecycle (was the free functions) */
auto prepare(bool use_bitmap, int seqmask, Parameters const & parameters) -> void;
auto add_sequence(unsigned int seqno, int seqmask) -> void;
auto add_all_sequences(int seqmask, Parameters const & parameters) -> void;
auto clear() -> void; /* explicit free for reuse (was dbindex_free) */
/* read API (const == thread-safe for concurrent search workers) */
auto getbitmap(unsigned int kmer) const -> unsigned char *;
auto getmatchcount(unsigned int kmer) const -> unsigned int;
auto getmatchlist(unsigned int kmer) const -> unsigned int *;
auto getmapping(unsigned int index) const -> unsigned int;
auto getcount() const -> unsigned int;
};Notes on the design choices:
- RAII class, mirroring
dynlibs'sDynamicLibraries. The destructor subsumesdbindex_free;clear()stays for the in-place reuse case (dbindex_preparecurrently callsdbindex_freefirst for idempotency, andexample_reinitrelies on prepare→free→prepare). - Public data members.
udb_readbuilds the index by writing the arrays directly; keeping them public lets that code becomedbindex.kmercount[i] = …with no behavioural change and nofriend. It also honours the project's "structso all members are public" leaning. bitmap_mincountbecomes a local inprepare(it is only read inside the same function's loop) — a small bonus cleanup; the file-staticdisappears.bitmap_thresholdstays aconstexprindbindex.cc.- The getters stay out-of-line in
dbindex.cc(defined as before, now as members) to keep codegen — and therefore the hot path — identical.
API-shape decision to confirm (see §8, Q1). The alternative to member
functions is to keep free functions that take an explicit Dbindex &
(dbindex_prepare(Dbindex &, …), dbindex_addsequence(Dbindex &, …)). That is
a smaller diff for the library examples and closer to the current C-ish style,
but the RAII-class form is what CLAUDE.md ("prefer RAII") and the dynlibs
precedent point to. Recommendation: the RAII class (Option B above).
Each step compiles and passes tests on its own. Commit messages follow the
dbindex: … convention; add the Co-Authored-By: Florian FILLOUX trailer.
-
dbindex: introduce the Dbindex struct alongside the globalsDefinestruct Dbindexand its methods indbindex.{h,cc}, implemented by delegating to the existing globals (thin wrappers). Nothing else changes yet. This lets every later step migrate one consumer at a time while the tree stays green. (Optional: skip if the big-bang per-file migration below is preferred — but the wrapper keeps commits small.) -
dbindex: make bitmap_mincount a local in prepareIndependent micro-cleanup; removes one file-static. -
dbindex: thread Dbindex through udb_readAddDbindex & dbindexout-param toudb_readand rewrite its raw-array fills asdbindex.<member>. Update all four external callers + the two internal ones to pass a localDbindex. This is the biggest single step becauseudb_readis the direct builder — do it early so the UDB path is settled. -
dbindex: migrate the search command(search.cc+searchcore.cc) AddDbindex const * dbindextosearchinfo_s; owner insearch.cccreates a localDbindex, sets it on each per-threadsearchinfo_s; the getters insearch_topscoresbecomesi->dbindex->getX(…);:849readssi->dbindex->wordlength. -
dbindex: migrate the sintax command(sintax.cc) Same pattern (owner + hot-path reader in one file). -
dbindex: migrate the orient command(orient.cc) -
dbindex: migrate the chimera command(chimera.cc) -
dbindex: migrate the cluster command + session(cluster.cc,cluster.h) StoreDbindex &incluster_session_s; thread it to the incrementaladd_sequencesites and the:597wordlengthread. Update thecluster.hdoc comments (ask before touching comments — seeCLAUDE.md). -
dbindex: migrate the udb command family(udb_make,udb_stats,udb_info,udb_fasta) to read/write through a localDbindex. -
dbindex: delete the globalsRemove theexterndeclarations and definitions; drop the thin wrappers from step 1 so the methods do the real work. Grep confirms zero remaining references.dbindex_freeand the free-function getters are gone. -
dbindex: update the library examples and LIBRARY_API.mdMigrateapi_examples/*.ccto the new API, bump the documented API version, refresh the numbered lifecycle comments invsearch_api.h(:74-84,:186).
(If Option A / free-functions is chosen in §8, steps 4-10 pass Dbindex &
explicitly instead of via searchinfo_s/cluster_session_s, and step 11 is a
much smaller diff.)
- Build matrix (per
CLAUDE.md): debug--enable-debug, then the three cross-compiles (mingw/Windows, POWER, RISC-V).kmerhashsize/kmerindexsizewidth and thebitmap_spointer array are the portability-sensitive bits. cppcheckeach modified.cc;clang-tidyfor the new class (rule-of-five, const-correctness).- Test net:
vsearch-tests/run_all_tests.sh; runscripts/orient.shmanually (orient tests are disabled by default —orient_tests_disablednote) since orient is a migrated consumer. - Library net:
api_examplesmake testagainst a release build (library_tests_need_release_build). - Performance:
search/cluster/sintaxsharesearch_topscores— the hot loop. The getters must stay out-of-line and const so codegen is unchanged; confirm withhyperfine(usearch_global.sh, aclustertest) if the disassembly is not obviously identical.
- Q1 — API shape. Resolved: Option B (RAII class with member functions),
and full removal of the transitional singleton. The library session/batch
entries (
search_session_init,search_batch,cluster_session_init,chimera_detect_init/_thread_init/_batch) take the caller'sDbindex(const&, except cluster's which is mutable — the session adds centroids incrementally);the_indexand thedbindex_*free functions are gone. - Q2 — scope of this branch. Resolved: landed as one sequence here — the
library-example migration +
LIBRARY_API.mdride in the same branch (commit61bec09f), immediately before the singleton removal (a7704301). - Q3 — comment edits. Resolved: symbol references updated, explanations
preserved. Comments in
dbindex.{h,cc},cluster.h,search.h,chimera.h,orient.cc,sintax.cc,searchcore.cc,db.h,vsearch_api.handLIBRARY_API.mdthat named the renamed/removed symbols were updated to the currentDbindex::prepare/dbindex.wordlengthetc. (commits61bec09f,a7704301,35fa6d75); purely conceptual prose was left verbatim. Still wants a human eye on the exact wording.