perf(mining): memoize the earnings card on tx_revision
The Mining tab's earnings card walked all N wallet transactions on every frame to total Today / Yesterday / All time and pick the 4 most recent rows: four string compares per transaction plus a memo search on receives. It also ran localtime and mktime every frame to find local midnight. The pass is now a pure function behind a one-slot cache keyed on WalletState::tx_revision, the transaction count, the filter pill, the Solo/Pool mode, solo support and local midnight, with a 2 s maximum age as a safety net. It reruns only when one of those changes or the 2 s age runs out, instead of every frame. The totals are bit-identical (same loop, same summation order), and crossing local midnight still moves Today into Yesterday at once. Local midnight is recomputed only when the clock second changes, through the shared util::localTime; a failed localtime now yields a zeroed struct instead of an uninitialised one. The "x ago" labels and Est. Daily are still computed per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6318,6 +6318,106 @@ void testMiningThreadCommit()
|
||||
EXPECT_EQ(effectivePoolThreads(0, 0, 0), 0);
|
||||
}
|
||||
|
||||
void testMiningEarningsCore()
|
||||
{
|
||||
using dragonx::TransactionInfo;
|
||||
using namespace dragonx::ui;
|
||||
auto mk = [](const char* type, double amt, std::int64_t ts, const char* memo, int conf, const char* txid) {
|
||||
TransactionInfo t; t.type = type; t.amount = amt; t.timestamp = ts; t.memo = memo;
|
||||
t.confirmations = conf; t.txid = txid; return t;
|
||||
};
|
||||
const std::int64_t today = 1000000, yest = today - 86400;
|
||||
std::vector<TransactionInfo> txs = {
|
||||
mk("mined", 3.0, today + 10, "", 150, "a"),
|
||||
mk("receive", 2.0, today + 5, "Mining Pool payout #7", 3, "b"),
|
||||
mk("receive", 9.0, today + 5, "thanks", 3, "c"), // not mining
|
||||
mk("generate", -1.5, yest + 1, "", 20, "d"), // abs(amount)
|
||||
mk("immature", 1.0, yest - 50, "", 0, "e"), // older than yesterday
|
||||
mk("send", 4.0, today, "", 9, "f"), // not mining
|
||||
};
|
||||
auto all = ComputeMiningEarnings(txs, 0, true, false, today, yest);
|
||||
EXPECT_EQ(all.allTimeCount, 4); EXPECT_NEAR(all.allTime, 7.5, 1e-9);
|
||||
EXPECT_EQ(all.todayCount, 2); EXPECT_NEAR(all.today, 5.0, 1e-9);
|
||||
EXPECT_EQ(all.yesterdayCount, 1); EXPECT_NEAR(all.yesterday, 1.5, 1e-9);
|
||||
EXPECT_EQ(all.recent.size(), static_cast<size_t>(3)); // solo mode: a, d, e
|
||||
EXPECT_TRUE(all.recent[0].mature); EXPECT_FALSE(all.recent[1].mature);
|
||||
EXPECT_EQ(all.recent[0].txid, std::string("a"));
|
||||
EXPECT_NEAR(all.recent[1].amount, 1.5, 1e-12);
|
||||
auto pool = ComputeMiningEarnings(txs, 0, true, true, today, yest);
|
||||
EXPECT_EQ(pool.recent.size(), static_cast<size_t>(1)); // pool mode: b only
|
||||
EXPECT_TRUE(pool.recent[0].isPoolPayout);
|
||||
EXPECT_EQ(ComputeMiningEarnings(txs, 1, true, false, today, yest).allTimeCount, 3); // Solo
|
||||
EXPECT_EQ(ComputeMiningEarnings(txs, 2, true, false, today, yest).allTimeCount, 1); // Pool
|
||||
EXPECT_EQ(ComputeMiningEarnings(txs, 1, false, false, today, yest).allTimeCount, 1); // no solo: 1 = Pool
|
||||
EXPECT_EQ(ComputeMiningEarnings(txs, 2, false, false, today, yest).allTimeCount, 4); // no solo: 2 = no skip
|
||||
std::vector<TransactionInfo> many;
|
||||
for (int i = 0; i < 10; ++i) many.push_back(mk("mined", 1.0, today, "", 1, "m"));
|
||||
EXPECT_EQ(ComputeMiningEarnings(many, 0, true, false, today, yest).recent.size(), static_cast<size_t>(4));
|
||||
EXPECT_EQ(ComputeMiningEarnings({}, 0, true, false, today, yest).allTimeCount, 0);
|
||||
}
|
||||
|
||||
void testMiningEarningsCache()
|
||||
{
|
||||
using dragonx::TransactionInfo;
|
||||
using namespace dragonx::ui;
|
||||
TransactionInfo t; t.type = "mined"; t.amount = 1.0; t.timestamp = 2000000; t.txid = "x";
|
||||
std::vector<TransactionInfo> txs = {t};
|
||||
MiningEarningsCache cache;
|
||||
MiningEarningsKey k; k.txRevision = 7; k.txCount = txs.size(); k.todayStart = 1000000; k.soloSupported = true;
|
||||
EXPECT_NEAR(cache.get(k, txs, 0).allTime, 1.0, 1e-9);
|
||||
EXPECT_EQ(cache.computeCount(), 1);
|
||||
cache.get(k, txs, 500); cache.get(k, txs, 1999); // hits
|
||||
EXPECT_EQ(cache.computeCount(), 1);
|
||||
txs[0].amount = 5.0; // write WITHOUT a revision bump:
|
||||
EXPECT_NEAR(cache.get(k, txs, 1999).allTime, 1.0, 1e-9); // stale until the 2 s max age...
|
||||
EXPECT_NEAR(cache.get(k, txs, 2000).allTime, 5.0, 1e-9); // ...then recomputed
|
||||
int c = cache.computeCount(); EXPECT_EQ(c, 2);
|
||||
k.txRevision = 8; cache.get(k, txs, 2001); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
k.filter = 1; cache.get(k, txs, 2002); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
k.poolMode = true; cache.get(k, txs, 2003); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
k.soloSupported = false; cache.get(k, txs, 2004); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
k.todayStart += 86400; cache.get(k, txs, 2005); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
k.txCount += 1; cache.get(k, txs, 2006); EXPECT_EQ(cache.computeCount(), ++c);
|
||||
cache.get(k, txs, 2007); EXPECT_EQ(cache.computeCount(), c); // hit
|
||||
EXPECT_TRUE(IntervalElapsed(100, 200, 1000)); // clock went backwards -> resample
|
||||
EXPECT_FALSE(IntervalElapsed(1199, 200, 1000)); EXPECT_TRUE(IntervalElapsed(1200, 200, 1000));
|
||||
}
|
||||
|
||||
void testMiningEarningsMidnightRollover()
|
||||
{
|
||||
#ifndef _WIN32
|
||||
using dragonx::TransactionInfo;
|
||||
using namespace dragonx::ui;
|
||||
const char* prev = std::getenv("TZ");
|
||||
const bool hadTz = prev != nullptr;
|
||||
const std::string savedTz = hadTz ? prev : "";
|
||||
setenv("TZ", "XXX-3", 1); dragonx::util::refreshTimezone(); // UTC+3: local midnight is 21:00 UTC
|
||||
const std::int64_t midnight = 86400LL * 20000 - 3 * 3600; // 2024-10-04 00:00:00 local
|
||||
LocalDayStartMemo day;
|
||||
EXPECT_EQ(day.get(midnight - 2), midnight - 86400); // 23:59:58 local: still the previous day
|
||||
EXPECT_EQ(day.get(midnight - 2), midnight - 86400); // same second: memo hit
|
||||
EXPECT_EQ(day.get(midnight - 1), midnight - 86400);
|
||||
EXPECT_EQ(day.get(midnight), midnight); // 00:00:00 local: rolled over
|
||||
EXPECT_EQ(day.get(midnight + 3599), midnight);
|
||||
|
||||
TransactionInfo t; t.type = "mined"; t.amount = 2.0; t.timestamp = midnight - 600; t.txid = "m"; // 23:50
|
||||
const std::vector<TransactionInfo> txs = {t};
|
||||
LocalDayStartMemo memo;
|
||||
MiningEarningsCache cache;
|
||||
MiningEarningsKey k; k.txRevision = 5; k.txCount = txs.size(); k.soloSupported = true;
|
||||
k.todayStart = memo.get(midnight - 1);
|
||||
const MiningEarningsTotals before = cache.get(k, txs, 10000);
|
||||
EXPECT_EQ(before.todayCount, 1); EXPECT_EQ(before.yesterdayCount, 0);
|
||||
k.todayStart = memo.get(midnight + 1); // the clock passes midnight, no tx change
|
||||
const MiningEarningsTotals after = cache.get(k, txs, 10500); // 0.5 s later: well inside the max age
|
||||
EXPECT_EQ(cache.computeCount(), 2);
|
||||
EXPECT_EQ(after.todayCount, 0); EXPECT_EQ(after.yesterdayCount, 1);
|
||||
EXPECT_NEAR(after.today, 0.0, 1e-9); EXPECT_NEAR(after.yesterday, 2.0, 1e-9);
|
||||
if (hadTz) setenv("TZ", savedTz.c_str(), 1); else unsetenv("TZ");
|
||||
dragonx::util::refreshTimezone();
|
||||
#endif
|
||||
}
|
||||
|
||||
void testMiningBenchmarkModel()
|
||||
{
|
||||
dragonx::ui::ThreadBenchmark benchmark;
|
||||
@@ -14645,6 +14745,9 @@ int main()
|
||||
testConsoleInputModel();
|
||||
testMiningBenchmarkModel();
|
||||
testMiningThreadCommit();
|
||||
testMiningEarningsCore();
|
||||
testMiningEarningsCache();
|
||||
testMiningEarningsMidnightRollover();
|
||||
testBalanceAddressListModel();
|
||||
testExplorerBlockCache();
|
||||
testTransactionHistoryCache();
|
||||
|
||||
Reference in New Issue
Block a user