fix(explorer): commit the loadRangeCached/revision memoization the perf fix depends on

Repairs dev: 3ee9099 switched the Explorer call sites (and a test) to ExplorerBlockCache::loadRangeCached()
/ revision(), but those definitions were sitting uncommitted in the working tree (left over from the
explorer block-search feature, ebbeb84) and were not staged with that commit — so dev as pushed referenced
undefined members and would not compile on a clean checkout.

The infra: loadRangeCached() memoizes loadRange() per (min,max), invalidated by revision_, which is bumped
on every mutation that can change what loadRange returns (open/close/store/clear/prune/spill-reset); the
memo is capped at 16 ranges. My local builds passed only because the working tree already had these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 19:15:19 -05:00
co-authored by Claude Opus 4.8
parent 3ee90993ed
commit a558a2a271
2 changed files with 33 additions and 1 deletions
+22 -1
View File
@@ -123,6 +123,7 @@ bool ExplorerBlockCache::ensureOpen()
}
db_ = openedDb;
++revision_;
// UI-thread-only handle: a short wait is enough; a miss falls back to the in-memory spill instead of
// freezing the frame for 2 s.
sqlite3_busy_timeout(db_, 250);
@@ -176,6 +177,20 @@ std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight,
return blocks;
}
const std::map<int, ExplorerBlockSummary>& ExplorerBlockCache::loadRangeCached(int minHeight, int maxHeight)
{
if (range_cache_revision_ != revision_) { range_cache_.clear(); range_cache_revision_ = revision_; }
if (range_cache_.size() >= 16) range_cache_.clear();
const std::pair<int, int> key(minHeight, maxHeight);
auto it = range_cache_.find(key);
if (it == range_cache_.end()) {
auto loaded = loadRange(minHeight, maxHeight); // may open the DB (bumps revision_)
if (range_cache_revision_ != revision_) { range_cache_.clear(); range_cache_revision_ = revision_; }
it = range_cache_.emplace(key, std::move(loaded)).first;
}
return it->second;
}
std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
{
std::vector<ExplorerBlockSummary> results;
@@ -256,6 +271,7 @@ bool ExplorerBlockCache::storeBlockRow(const ExplorerBlockSummary& block)
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
{
if (block.height < 1 || block.hash.empty()) return false;
++revision_;
const bool stored = storeBlockRow(block);
if (stored) {
spill_.erase(block.height);
@@ -297,6 +313,7 @@ int ExplorerBlockCache::cachedBlockCount()
void ExplorerBlockCache::clearBlocks()
{
++revision_;
spill_.clear();
if (!ensureOpen()) return;
exec("DELETE FROM explorer_blocks");
@@ -305,6 +322,7 @@ void ExplorerBlockCache::clearBlocks()
void ExplorerBlockCache::pruneTo(int maxRows)
{
if (maxRows < 1 || !db_) return;
++revision_;
// Keep the maxRows highest blocks. The OFFSET row is the (maxRows+1)-th highest; when the table is smaller the
// subquery is NULL and nothing is deleted. Freed pages are reused, so the file stops growing at its high-water
// mark (no VACUUM / auto_vacuum: changing auto_vacuum needs a rebuild of existing databases).
@@ -333,8 +351,10 @@ ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation(
// No DB to validate against: keep the spill honest ourselves (tip regressed, or a different block at the same height).
if (!spill_.empty() && !currentBestHash.empty() &&
(currentHeight < spill_tip_height_ ||
(currentHeight == spill_tip_height_ && currentBestHash != spill_tip_hash_)))
(currentHeight == spill_tip_height_ && currentBestHash != spill_tip_hash_))) {
spill_.clear();
++revision_;
}
if (!currentBestHash.empty()) { spill_tip_height_ = currentHeight; spill_tip_hash_ = currentBestHash; }
return validation;
}
@@ -526,6 +546,7 @@ void ExplorerBlockCache::close()
{
tip_known_ = false;
if (!db_) return;
++revision_;
sqlite3_close(db_);
db_ = nullptr;
}