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:
@@ -18,6 +18,7 @@
|
||||
#include "util/fetch_backoff.h"
|
||||
#include "util/logger.h"
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/transaction_history_store_policy.h"
|
||||
#include "util/coalescing_writer.h"
|
||||
#include "data/address_book.h"
|
||||
#include "data/wallet_index.h"
|
||||
@@ -6102,6 +6103,103 @@ void testTransactionHistoryCachePrunesOldWallets()
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testHistorySnapshotSignature()
|
||||
{
|
||||
using namespace dragonx::data;
|
||||
using dragonx::TransactionInfo;
|
||||
auto mk = [](const char* id, double amt, int conf, const char* memo = "") {
|
||||
return TransactionInfo{id, "receive", amt, 1700000000, conf, "zs-a", "", memo};
|
||||
};
|
||||
const std::unordered_set<std::string> none;
|
||||
const std::unordered_map<std::string, int> h{{"zs-a", 100}};
|
||||
const std::vector<TransactionInfo> a{mk("t1", 1.0, 3), mk("t2", 2.0, 40)};
|
||||
const auto base = historySnapshotSignature(a, none, "w", 7, h, 500, "tip");
|
||||
EXPECT_EQ(base.rows, static_cast<size_t>(2));
|
||||
|
||||
// row order is irrelevant
|
||||
const std::vector<TransactionInfo> reordered{a[1], a[0]};
|
||||
const auto r = historySnapshotSignature(reordered, none, "w", 7, h, 500, "tip");
|
||||
EXPECT_EQ(r.content, base.content);
|
||||
EXPECT_EQ(r.slow, base.slow);
|
||||
|
||||
// confirmations inside a bucket move only the slow axis
|
||||
auto c7 = a; c7[0].confirmations = 7;
|
||||
const auto s7 = historySnapshotSignature(c7, none, "w", 7, h, 500, "tip");
|
||||
EXPECT_EQ(s7.content, base.content);
|
||||
EXPECT_TRUE(s7.slow != base.slow);
|
||||
// crossing a UI threshold is a content change: <0|0, 0|1, 9|10, 99|100
|
||||
auto contentAt = [&](int conf) {
|
||||
auto v = a; v[0].confirmations = conf;
|
||||
return historySnapshotSignature(v, none, "w", 7, h, 500, "tip").content;
|
||||
};
|
||||
EXPECT_TRUE(contentAt(-1) != contentAt(0));
|
||||
EXPECT_TRUE(contentAt(0) != contentAt(1));
|
||||
EXPECT_TRUE(contentAt(9) != contentAt(10));
|
||||
EXPECT_EQ(contentAt(10), contentAt(99));
|
||||
EXPECT_TRUE(contentAt(99) != contentAt(100));
|
||||
EXPECT_EQ(contentAt(100), contentAt(5000));
|
||||
|
||||
// every persisted field, the wallet hash and the key id are content
|
||||
auto m = a; m[0].memo = "x";
|
||||
EXPECT_TRUE(historySnapshotSignature(m, none, "w", 7, h, 500, "tip").content != base.content);
|
||||
auto ty = a; ty[0].type = "mined";
|
||||
EXPECT_TRUE(historySnapshotSignature(ty, none, "w", 7, h, 500, "tip").content != base.content);
|
||||
auto am = a; am[0].amount = 1.0000001;
|
||||
EXPECT_TRUE(historySnapshotSignature(am, none, "w", 7, h, 500, "tip").content != base.content);
|
||||
auto fa = a; fa[0].from_address = "zs-other";
|
||||
EXPECT_TRUE(historySnapshotSignature(fa, none, "w", 7, h, 500, "tip").content != base.content);
|
||||
EXPECT_TRUE(historySnapshotSignature(a, none, "w2", 7, h, 500, "tip").content != base.content);
|
||||
EXPECT_TRUE(historySnapshotSignature(a, none, "w", 8, h, 500, "tip").content != base.content);
|
||||
|
||||
// the tip only moves the slow axis
|
||||
const auto t = historySnapshotSignature(a, none, "w", 7, h, 501, "tip2");
|
||||
EXPECT_EQ(t.content, base.content);
|
||||
EXPECT_TRUE(t.slow != base.slow);
|
||||
|
||||
// pending sends are excluded
|
||||
auto withPending = a; withPending.push_back(mk("opid-1", -1.0, 0));
|
||||
const auto p = historySnapshotSignature(withPending, {"opid-1"}, "w", 7, h, 500, "tip");
|
||||
EXPECT_EQ(p.content, base.content);
|
||||
EXPECT_EQ(p.slow, base.slow);
|
||||
EXPECT_EQ(p.rows, static_cast<size_t>(2));
|
||||
|
||||
// heights are slow; entries replace() would not persist are ignored
|
||||
auto h2 = h; h2["zs-a"] = 101;
|
||||
const auto hs = historySnapshotSignature(a, none, "w", 7, h2, 500, "tip");
|
||||
EXPECT_EQ(hs.content, base.content);
|
||||
EXPECT_TRUE(hs.slow != base.slow);
|
||||
auto h3 = h; h3[""] = 5; h3["zs-neg"] = -1;
|
||||
EXPECT_EQ(historySnapshotSignature(a, none, "w", 7, h3, 500, "tip").slow, base.slow);
|
||||
}
|
||||
|
||||
void testHistoryStorePolicy()
|
||||
{
|
||||
using namespace dragonx::data;
|
||||
using M = HistoryStoreMode;
|
||||
using A = HistoryStoreAction;
|
||||
const HistorySnapshotSig s1{1, 10, 5}, s2{2, 10, 5}, s1slow{1, 11, 5};
|
||||
auto act = [](M mode, bool have, HistorySnapshotSig stored, HistorySnapshotSig current, double since) {
|
||||
HistoryStoreInput in;
|
||||
in.mode = mode;
|
||||
in.haveStored = have;
|
||||
in.stored = stored;
|
||||
in.current = current;
|
||||
in.sinceLastSubmitSec = since;
|
||||
return decideHistoryStore(in);
|
||||
};
|
||||
EXPECT_TRUE(act(M::Coalesce, true, s1, s1, 1e9) == A::Skip); // nothing changed
|
||||
EXPECT_TRUE(act(M::Coalesce, true, s1, s2, 5.0) == A::Defer); // changed, too soon
|
||||
EXPECT_TRUE(act(M::Coalesce, true, s1, s2, kHistoryMinStoreIntervalSec) == A::Store);
|
||||
EXPECT_TRUE(act(M::Coalesce, false, s1, s1, 1e9) == A::Store); // nothing on disk yet
|
||||
EXPECT_TRUE(act(M::Coalesce, false, s1, s1, 3.0) == A::Defer); // retry is paced
|
||||
EXPECT_TRUE(act(M::Coalesce, true, s1, s1slow, 100.0) == A::Skip); // slow drift: not yet
|
||||
EXPECT_TRUE(act(M::Coalesce, true, s1, s1slow, kHistorySlowStoreIntervalSec) == A::Store);
|
||||
EXPECT_TRUE(act(M::Now, true, s1, s1, 0.0) == A::Skip); // clean: no write
|
||||
EXPECT_TRUE(act(M::Now, true, s1, s1slow, 0.0) == A::Store); // slow drift flushed
|
||||
EXPECT_TRUE(act(M::Now, true, s1, s2, 0.0) == A::Store); // ignores pacing
|
||||
EXPECT_TRUE(act(M::Now, false, s1, s1, 0.0) == A::Store);
|
||||
}
|
||||
|
||||
// The background writer's path: a job carries its own key copy (a lockKey() after queueing cannot drop it),
|
||||
// the copy is wiped, a keyless job is refused, keyId() tracks the key across lock/unlock, and a late job for the
|
||||
// previous wallet keeps the salt of the wallet unlocked since.
|
||||
@@ -12800,6 +12898,8 @@ int main()
|
||||
testExplorerBlockCache();
|
||||
testTransactionHistoryCache();
|
||||
testTransactionHistoryCachePrunesOldWallets();
|
||||
testHistorySnapshotSignature();
|
||||
testHistoryStorePolicy();
|
||||
testTransactionHistoryCacheBackgroundWrite();
|
||||
testAddressBookScope();
|
||||
testWalletIndex();
|
||||
|
||||
Reference in New Issue
Block a user