From d601f786e23ea60f1afaa187b595d6c895ac2fe7 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 13 Oct 2026 17:42:30 -0500 Subject: [PATCH] perf(lite): cache walletExists() to stop per-frame FFI While no lite wallet is open the startup auto-open check called walletExists() every frame. That call crosses into the backend and touches the filesystem (and prints), so it ran a disk stat on every frame the app idled at the first-run prompt. - walletExists() now caches its result for about two seconds on the main thread, and the cache is invalidated whenever an open or lifecycle result is consumed, since the wallet file may have changed. - The auto-open condition is reordered so the cheap timer checks gate the call, and the click-time overwrite decision uses a new walletExistsFresh() that bypasses the cache. Tests: the cache returns the first queried value within the TTL and the fresh read re-queries the backend and refreshes the cache. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 6 +++--- src/wallet/lite_wallet_controller.cpp | 18 +++++++++++++++++- src/wallet/lite_wallet_controller.h | 8 ++++++++ tests/test_phase4.cpp | 17 +++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 32ebca3..1ea22a8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -952,8 +952,8 @@ void App::update() const double nowSecs = ImGui::GetTime(); const double retryInterval = lite_wallet_->lastOpenWasWarmup() ? 4.0 : 20.0; if (!lite_wallet_->walletOpen() && !lite_wallet_->openInProgress() && - lite_wallet_->walletExists() && - (!lite_autoopen_done_ || nowSecs - lite_open_last_attempt_ > retryInterval)) { + (!lite_autoopen_done_ || nowSecs - lite_open_last_attempt_ > retryInterval) && + lite_wallet_->walletExists()) { lite_autoopen_done_ = true; lite_open_last_attempt_ = nowSecs; lite_wallet_->beginOpenExisting(); @@ -3718,7 +3718,7 @@ void App::renderLiteFirstRunPrompt() wallet::LiteWalletRestoreRequest req; req.seedPhrase = seedTrim; // normalized: NBSP-glued pastes restore correctly req.birthday = static_cast(std::max(0, restoreBirthday)); - req.overwrite = lite_wallet_->walletExists(); // replace any existing wallet file + req.overwrite = lite_wallet_->walletExistsFresh(); // replace any existing wallet file if (lite_wallet_->beginRestoreWalletAsync(std::move(req))) { restoreErr.clear(); restoring = true; diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index d1441e3..445fe71 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -671,6 +671,7 @@ void LiteWalletController::pumpAsyncOpen() outcome = std::move(openResult_->value()); openResult_->reset(); } + invalidateWalletExists(); // a real open result was consumed: the wallet file may have changed if (openThread_.joinable()) openThread_.join(); // producer set its result, then exits if (outcome.ok) { @@ -842,6 +843,7 @@ void LiteWalletController::pumpLifecycleResult() out = std::move(lifecycleResult_->value()); lifecycleResult_->reset(); } + invalidateWalletExists(); // a real lifecycle result was consumed: the wallet file may have changed if (lifecycleThread_.joinable()) lifecycleThread_.join(); // producer set its result, then exits // Finalize on the main thread: flip walletOpen()/status, persist, start sync/worker on success; @@ -1400,7 +1402,21 @@ bool LiteWalletController::walletExists() const { // chainName_ is always a backend-accepted value ("main"/"test"/"regtest"); litelib_wallet_ // exists panics on unknown chains, but settings migration guarantees a valid one. - return bridge_ && bridge_->walletExists(chainName_); + // Cached ~2s: called every frame while no wallet is open (main-thread only). + const auto now = std::chrono::steady_clock::now(); + if (wexCached_ && now - wexAt_ < std::chrono::seconds(2)) return wexValue_; + wexValue_ = bridge_ && bridge_->walletExists(chainName_); + wexCached_ = true; + wexAt_ = now; + return wexValue_; +} + +bool LiteWalletController::walletExistsFresh() const +{ + wexValue_ = bridge_ && bridge_->walletExists(chainName_); + wexCached_ = true; + wexAt_ = std::chrono::steady_clock::now(); + return wexValue_; } namespace { diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 508329c..48914d9 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -193,6 +193,7 @@ public: // True if a wallet file already exists on disk for this chain. Use to auto-open on startup // (open via openWallet({}) needs no passphrase — initialize_existing just loads the file). bool walletExists() const; + bool walletExistsFresh() const; // uncached; also refreshes the cache const WalletBackendStatus& status() const { return status_; } LiteWalletLifecycleAvailability availability() const { return lifecycle_.availability(); } @@ -399,6 +400,13 @@ private: // mutex would instead serialize sync against syncstatus polling and defeat the design. std::shared_ptr bridge_; std::string chainName_; // backend chain id (for walletExists); from connection settings + // walletExists() is called every frame while no wallet is open; the backend call touches the + // filesystem (and prints). Cache it (main-thread only - no lock). walletExistsFresh() bypasses + // the cache for the click-time overwrite decision. + mutable bool wexCached_ = false; + mutable bool wexValue_ = false; + mutable std::chrono::steady_clock::time_point wexAt_{}; + void invalidateWalletExists() { wexCached_ = false; } LiteConnectionSettings connectionSettings_; // kept for the failover candidate-server list WalletCapabilities capabilities_; // for thread-local lifecycle services (async path) LiteWalletLifecycleOptions lifecycleOptions_; // ditto (allowBridgeCalls / rollout gate) diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 12b8d9c..d350e4e 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -18146,6 +18146,22 @@ void testTransactionCsv() { } // namespace +void testLiteWalletExistsCache() +{ + using namespace dragonx::wallet; + const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, /*embeddedDaemon*/ false, /*liteBackendLinked*/ true); + const LiteConnectionSettings conn = defaultLiteConnectionSettings(); + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = true; + LiteWalletController controller(liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + EXPECT_TRUE(controller.walletExists()); // queries backend, caches true + dragonx::test::g_liteFakeWalletExists = false; + EXPECT_TRUE(controller.walletExists()); // still cached (TTL not elapsed) + EXPECT_FALSE(controller.walletExistsFresh()); // uncached read, refreshes the cache + EXPECT_FALSE(controller.walletExists()); // cache now false + dragonx::test::g_liteFakeWalletExists = true; // restore default for later tests +} + int main() { testConnectionConfig(); @@ -18339,6 +18355,7 @@ int main() testLiteClientBridgeUsesRuntimeOwnedStringCleanup(); testLiteBackendInjectableFakeBridge(); testLiteWalletControllerLifecycle(); + testLiteWalletExistsCache(); testLiteWalletControllerOpenFailover(); testLiteWalletControllerAsyncLifecycleFailover(); testLiteWalletControllerConsoleCommand();