Files
ObsidianDragon/src/ui/explorer/explorer_block_cache.h
T
DanSandClaude Opus 4.8 a558a2a271 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>
2026-10-19 19:15:19 -05:00

126 lines
5.2 KiB
C++

#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <map>
#include <string>
#include <utility>
#include <vector>
struct sqlite3;
namespace dragonx {
namespace ui {
struct ExplorerBlockSummary {
int height = 0;
std::string hash;
int tx_count = 0;
int size = 0;
std::int64_t time = 0;
double difficulty = 0.0;
};
class ExplorerBlockCache {
public:
struct SavedTipValidation {
bool needed = false;
int height = 0;
std::string expectedHash;
};
ExplorerBlockCache();
ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath);
~ExplorerBlockCache();
ExplorerBlockCache(const ExplorerBlockCache&) = delete;
ExplorerBlockCache& operator=(const ExplorerBlockCache&) = delete;
static std::string defaultDatabasePath();
static std::string defaultLegacyJsonPath();
bool ensureOpen();
bool isOpen() const { return db_ != nullptr; }
const std::string& databasePath() const { return database_path_; }
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
// loadRange memoized per (min,max) and invalidated by revision() so the per-frame Explorer list only hits SQLite
// after the cache actually changed (store / clear / prune / spill reset / open / close). The reference is valid
// until the next call; copy it if you call again before using it.
const std::map<int, ExplorerBlockSummary>& loadRangeCached(int minHeight, int maxHeight);
std::uint64_t revision() const { return revision_; }
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
// Always returns the DB result; a block the DB refuses (unopenable file, failed insert) is kept in a small
// in-memory spill so the Explorer still shows it instead of refetching it forever.
bool storeBlock(const ExplorerBlockSummary& block);
// storeBlock for a batch inside one SAVEPOINT (one WAL commit instead of one per block). True if every block hit the DB.
bool storeBlocks(const std::vector<ExplorerBlockSummary>& blocks);
int cachedBlockCount();
void clearBlocks();
// Keep only the `maxRows` highest blocks (data-safe: the table is a re-fetchable RPC cache).
void pruneTo(int maxRows);
static constexpr int kMaxCachedBlocks = 20000;
// A failed open is not retried until this interval has passed (default 30 s). openAttempts() counts real attempts.
void setOpenRetryInterval(std::chrono::milliseconds interval) { open_retry_interval_ = interval; }
int openAttempts() const { return open_attempts_; }
std::size_t spillCount() const { return spill_.size(); }
SavedTipValidation prepareValidation(int currentHeight, const std::string& currentBestHash);
void applySavedTipValidation(const SavedTipValidation& validation,
const std::string& actualHash,
int currentHeight,
const std::string& currentBestHash);
void updateTip(int height, const std::string& hash);
// Rows changed through this connection since it opened (sqlite3_total_changes); 0 when closed.
// Lets tests check that a steady tip writes nothing.
int totalChanges() const;
private:
bool exec(const char* sql);
std::string getMetaValue(const std::string& key);
int getMetaInt(const std::string& key, int fallback);
bool setMetaValue(const std::string& key, const std::string& value);
bool createSchema();
void migrateLegacyJsonIfNeeded();
void close();
bool storeBlockRow(const ExplorerBlockSummary& block); // DB write only
void spillBlock(const ExplorerBlockSummary& block);
bool failOpen(const char* why);
sqlite3* db_ = nullptr;
std::string database_path_;
std::string legacy_json_path_;
// The saved tip as last written or read, so the per-frame check can skip SQLite while the chain
// tip hasn't moved. The database stays the source of truth; this is only a copy of it.
bool tip_known_ = false;
int tip_height_ = 0;
std::string tip_hash_;
// Failure latch for ensureOpen(): after a failed open nothing retries until retry_not_before_.
std::chrono::steady_clock::time_point retry_not_before_{};
std::chrono::milliseconds open_retry_interval_{30000};
int open_attempts_ = 0;
int stores_since_prune_ = 0;
// Bumped by every mutation that can change what loadRange returns; keys the loadRangeCached memo.
std::uint64_t revision_ = 1;
std::uint64_t range_cache_revision_ = 0;
std::map<std::pair<int, int>, std::map<int, ExplorerBlockSummary>> range_cache_;
// Blocks the DB refused, bounded and UI-thread only. Evicts the entry farthest from the last spilled height
// (the Explorer fetches around the visible window). Cleared with clearBlocks() and when the chain tip regresses.
static constexpr std::size_t kMaxSpillBlocks = 512;
std::map<int, ExplorerBlockSummary> spill_;
int spill_tip_height_ = 0;
std::string spill_tip_hash_;
};
} // namespace ui
} // namespace dragonx