perf(history): write the history snapshot on change, off the UI thread

Every history refresh, address refresh, connect and lock-state check
rewrote the encrypted tx-history snapshot on the UI thread: a JSON
document of every row, its encryption, then INSERT OR REPLACE and a
prune, an estimated 40-120 ms on a 5k-transaction wallet, even when
nothing had changed.

Now a signature of what would be stored (about 1 ms on 5k rows)
decides whether to write. Confirmations count only by the thresholds
the UI reads (conflicted, pending, 1-9, 10-99, 100+); per-address scan
heights, exact confirmations and the tip drift every block and form a
separate part.

- Routine refreshes write a content change at most every 20 s (a
  deferred one is released from the update loop) and the per-block
  drift at most every 10 min. Nothing changed means no write.
- A mining-address relabel, a passphrase change, a rescan, a
  disconnect, a wallet lock and exit write whatever is not on disk yet,
  at once.
- The UI thread only copies the rows and the key into a job; a single
  background writer (newest job wins) builds, encrypts and stores it. A
  failed write is retried.
- Exit queues the final write so it overlaps the daemon stop, then
  waits up to 3 s for the writer; a disk that hangs past that keeps the
  previous snapshot.
- A loaded snapshot counts as stored, so the first identical refresh
  doesn't rewrite it. An address refresh re-keys the snapshot only when
  the address set changed.

After a hard kill the snapshot can now be up to ~20 s behind for rows
and 10 min for scan heights, exact confirmations and the tip (it was
one refresh); the first refresh replaces it either way. Lite builds,
which never read this cache, no longer write it on Refresh or a mining
relabel, and the screenshot capture mode's demo rows never reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent c0568eb930
commit 93b6871511
8 changed files with 436 additions and 27 deletions
+14
View File
@@ -849,6 +849,10 @@ void App::update()
}
}
// Release a deferred history-snapshot store / retry a failed write (O(1) when idle). After the drains, so
// a store a callback deferred this tick is seen; capture_mode_ returned earlier, so never during a sweep.
pumpTransactionHistoryStore();
// Apply any lite-wallet refresh the controller's background worker produced (main thread).
if (lite_wallet_) {
// Finalize any completed async open on the main thread (flips walletOpen / surfaces failure).
@@ -6205,6 +6209,8 @@ void App::beginShutdown()
stop_user_choice_ = 0;
stop_kill_offered_ = false;
shutdown_start_time_ = std::chrono::steady_clock::now();
// Queue the final tx-history snapshot now so its write overlaps the daemon stop; shutdown() drains it.
storeTransactionHistoryCacheIfAvailable(data::HistoryStoreMode::Now);
async_tasks_.cancelAll();
// The seed-migration adopt task drives daemon stop/start and swaps wallet.dat; let it finish
// before shutdown touches the daemon, so the two never race. shutting_down_ is already set, so
@@ -7317,6 +7323,14 @@ void App::renderLoadingOverlay(float contentH)
void App::shutdown()
{
// Full node: persist whatever a refresh changed since beginShutdown() (everything, on the direct-exit path),
// then drain + join the history writer. Bounded like the settings flush: on a wedged disk the in-flight write
// is left running (one autocommit INSERT in WAL mode: a cut-off write keeps the previous snapshot) and _Exit
// ends it. Lite: the store returns at once and the writer never started.
storeTransactionHistoryCacheIfAvailable(data::HistoryStoreMode::Now);
if (history_writer_.flush(std::chrono::seconds(3))) history_writer_.stop();
else DEBUG_LOGF("[History] snapshot write still running at exit - left to finish (WAL keeps the old one)\n");
// Ensure the mining-control thread is stopped + joined (idempotent; beginShutdown already did it on the
// normal quit path, but shutdown() can also run without it). Must precede xmrig_manager_ teardown.
stopMiningControlThread();