From 16e0ee9947ed52a2563fddf1d9256baf160302cf Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 3 Oct 2026 19:52:09 -0500 Subject: [PATCH] perf: stop per-frame SQLite writes and console spam, make perf.log opt-in - Explorer: the tip check runs every frame the Explorer tab is open, and it read both tip rows and rewrote them each time (two autocommit WAL writes per frame). A steady tip is now kept in memory and skips SQLite. A new tip writes both rows in one savepoint, so a crash between them can't pair a new height with the old hash. A test checks that 100 steady checks write nothing. - Console: while the daemon was in the Error state the Console tab added its error line on every poll, i.e. every frame, re-laying out the whole buffer each time. It prints once per error now: on entering Error, when the message changes, or after another crash (EmbeddedDaemon::getUnexpectedExitCount(), never reset). setState writes the message before the state, so a reader that sees Error sees that error's message. The stray " ===" suffix is gone. - perf.log: the profiler appended a block every 300 frames on every run and never rotated the file (tens of MB a day for a wallet left open). It is now off unless the app starts with DRAGONX_PERFLOG=1; otherwise its zone hooks are near no-ops. Co-Authored-By: Claude Opus 5.5 --- src/daemon/embedded_daemon.cpp | 6 ++- src/daemon/embedded_daemon.h | 4 ++ src/main.cpp | 7 ++- src/ui/explorer/explorer_block_cache.cpp | 56 +++++++++++++++++---- src/ui/explorer/explorer_block_cache.h | 12 ++++- src/ui/windows/console_command_executor.cpp | 12 ++++- src/ui/windows/console_command_executor.h | 2 + src/ui/windows/console_tab_helpers.cpp | 7 +++ src/ui/windows/console_tab_helpers.h | 8 +++ src/util/perf_log.h | 4 +- tests/test_phase4.cpp | 15 ++++++ 11 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index 96ea26e..b5dbf30 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -256,11 +256,13 @@ std::vector EmbeddedDaemon::getChainParams() void EmbeddedDaemon::setState(State s, const std::string& message) { - state_ = s; + // Message first, state second: a reader that sees Error must also see that error's message, not the + // previous one (the console prints the message when it sees the state change). if (!message.empty() && s == State::Error) { std::lock_guard lk(error_mutex_); // dedicated mutex — never taken with output_mutex_ held last_error_ = message; } + state_ = s; if (state_callback_) { state_callback_(s, message); @@ -1098,6 +1100,7 @@ void EmbeddedDaemon::monitorProcess() } crash_count_++; + unexpected_exits_++; // before setState(Error): a reader that sees Error sees the new count // Build informative error message with full diagnostic details std::string error_msg = "dragonxd exited unexpectedly (" + translateWindowsExitCode(exit_code) + ")"; @@ -1671,6 +1674,7 @@ void EmbeddedDaemon::monitorProcess() return; } crash_count_++; + unexpected_exits_++; // before setState(Error): a reader that sees Error sees the new count setState(State::Error, "dragonxd exited unexpectedly (" + exitInfo + ")"); return; } diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index 069833c..d7a88ef 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -446,6 +446,9 @@ public: int getCrashCount() const { return crash_count_.load(); } /** Reset crash counter (call on successful connection or manual restart) */ void resetCrashCount() { crash_count_ = 0; } + /** Unexpected exits since this object was created; never reset (unlike the crash count), so a + * reader can tell a new crash from an error it has already reported. */ + std::uint64_t getUnexpectedExitCount() const { return unexpected_exits_.load(); } private: void setState(State s, const std::string& message = ""); @@ -494,6 +497,7 @@ private: std::string stratum_allowip_; // -stratumallowip subnet (empty = daemon default: loopback only) std::string wallet_file_; // -wallet= for the active wallet; empty/"wallet.dat" = default std::atomic crash_count_{0}; // consecutive crash counter + std::atomic unexpected_exits_{0}; // all unexpected exits (see getUnexpectedExitCount) std::atomic rescan_on_next_start_{false}; // -rescan flag for next start std::atomic zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start std::atomic salvage_on_next_start_{false}; // -salvagewallet flag for next start diff --git a/src/main.cpp b/src/main.cpp index d8f313a..4020e82 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,6 +77,7 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, #include #include #include +#include #include #include #include @@ -1447,8 +1448,10 @@ int main(int argc, char* argv[]) dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui(); } - // Initialize performance profiler — writes to /perf.log - { + // Performance profiler — writes to /perf.log. Opt-in (DRAGONX_PERFLOG=1): always + // on, it appended a block every 300 frames and was never rotated (tens of MB a day for any wallet + // left open). + if (const char* perfEnv = std::getenv("DRAGONX_PERFLOG"); perfEnv && *perfEnv && std::strcmp(perfEnv, "0") != 0) { std::string perfPath = (std::filesystem::path( dragonx::util::Platform::getObsidianDragonDir()) / "perf.log").string(); dragonx::util::PerfLog::instance().init(perfPath); diff --git a/src/ui/explorer/explorer_block_cache.cpp b/src/ui/explorer/explorer_block_cache.cpp index 20ca5da..41d87c7 100644 --- a/src/ui/explorer/explorer_block_cache.cpp +++ b/src/ui/explorer/explorer_block_cache.cpp @@ -229,7 +229,16 @@ ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation( int currentHeight, const std::string& currentBestHash) { SavedTipValidation validation; - if (currentHeight <= 0 || !ensureOpen()) return validation; + if (currentHeight <= 0) return validation; + + // Steady state: the tip hasn't moved since it was saved. This runs every frame the Explorer is + // open, and used to read both meta rows and rewrite them each time (two autocommit WAL writes per + // frame). Only an exact match takes the shortcut; anything else goes through the checks below. + if (tip_known_ && !currentBestHash.empty() && + currentHeight == tip_height_ && currentBestHash == tip_hash_) { + return validation; + } + if (!ensureOpen()) return validation; int savedHeight = getMetaInt("tip_height", 0); std::string savedHash = getMetaValue("tip_hash"); @@ -248,8 +257,15 @@ ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation( if (currentHeight == savedHeight) { if (currentBestHash.empty()) return validation; - if (currentBestHash != savedHash) clearBlocks(); - updateTip(currentHeight, currentBestHash); + if (currentBestHash != savedHash) { + clearBlocks(); // same height, different block: a reorg + updateTip(currentHeight, currentBestHash); + } else { + // Already saved: remember it so later frames skip SQLite (nothing to write). + tip_known_ = true; + tip_height_ = savedHeight; + tip_hash_ = savedHash; + } return validation; } @@ -280,9 +296,26 @@ void ExplorerBlockCache::applySavedTipValidation(const SavedTipValidation& valid void ExplorerBlockCache::updateTip(int height, const std::string& hash) { + tip_known_ = false; if (!ensureOpen()) return; - setMetaValue("tip_height", std::to_string(std::max(0, height))); - setMetaValue("tip_hash", hash); + // Both rows in one transaction, so a crash between them can't save a new height with the old hash + // (which would wipe the cache for nothing on the next run). A savepoint, not BEGIN, so it nests + // inside a caller's transaction instead of failing. + if (!exec("SAVEPOINT explorer_tip")) return; + const bool written = setMetaValue("tip_height", std::to_string(std::max(0, height))) && + setMetaValue("tip_hash", hash); + if (!written) exec("ROLLBACK TO explorer_tip"); + const bool released = exec("RELEASE explorer_tip"); + if (written && released && height > 0 && !hash.empty()) { + tip_known_ = true; + tip_height_ = height; + tip_hash_ = hash; + } +} + +int ExplorerBlockCache::totalChanges() const +{ + return db_ ? sqlite3_total_changes(db_) : 0; } bool ExplorerBlockCache::exec(const char* sql) @@ -322,16 +355,16 @@ int ExplorerBlockCache::getMetaInt(const std::string& key, int fallback) } } -void ExplorerBlockCache::setMetaValue(const std::string& key, const std::string& value) +bool ExplorerBlockCache::setMetaValue(const std::string& key, const std::string& value) { - if (!ensureOpen()) return; + if (!ensureOpen()) return false; Statement statement(db_, "INSERT OR REPLACE INTO explorer_cache_meta (key, value) VALUES (?, ?)"); - if (!statement.handle) return; - if (!bindText(statement.handle, 1, key)) return; - if (!bindText(statement.handle, 2, value)) return; - sqlite3_step(statement.handle); + if (!statement.handle) return false; + if (!bindText(statement.handle, 1, key)) return false; + if (!bindText(statement.handle, 2, value)) return false; + return sqlite3_step(statement.handle) == SQLITE_DONE; } bool ExplorerBlockCache::createSchema() @@ -392,6 +425,7 @@ void ExplorerBlockCache::migrateLegacyJsonIfNeeded() void ExplorerBlockCache::close() { + tip_known_ = false; if (!db_) return; sqlite3_close(db_); db_ = nullptr; diff --git a/src/ui/explorer/explorer_block_cache.h b/src/ui/explorer/explorer_block_cache.h index e98e77c..9e3334b 100644 --- a/src/ui/explorer/explorer_block_cache.h +++ b/src/ui/explorer/explorer_block_cache.h @@ -56,11 +56,15 @@ public: 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); - void setMetaValue(const std::string& key, const std::string& value); + bool setMetaValue(const std::string& key, const std::string& value); bool createSchema(); void migrateLegacyJsonIfNeeded(); void close(); @@ -68,6 +72,12 @@ private: 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_; }; } // namespace ui diff --git a/src/ui/windows/console_command_executor.cpp b/src/ui/windows/console_command_executor.cpp index 36ef6b1..8f357ae 100644 --- a/src/ui/windows/console_command_executor.cpp +++ b/src/ui/windows/console_command_executor.cpp @@ -6,6 +6,7 @@ #include "console_channel.h" #include "console_command_reference.h" // consoleCommandCategories / liteConsoleCommandCategories #include "console_input_model.h" // BuildConsoleRpcCall +#include "console_tab_helpers.h" // ShouldPrintDaemonErrorLine #include "../../app.h" #include "../../data/wallet_state.h" @@ -126,7 +127,16 @@ void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) add("", ConsoleChannel::None); add(TR("console_daemon_stopped"), ConsoleChannel::Info); } else if (cur == DState::Error) { - add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleChannel::Error); + // Once per error (see ShouldPrintDaemonErrorLine): printing on every poll added a line per + // frame and made the console re-lay-out its whole buffer each time. + std::string err = d->getLastError(); + const std::uint64_t exits = d->getUnexpectedExitCount(); + if (ShouldPrintDaemonErrorLine(prev == static_cast(DState::Error), true, err, last_daemon_error_, + exits != last_printed_exit_count_)) { + add(std::string(TR("console_daemon_error")) + " " + err, ConsoleChannel::Error); + last_daemon_error_ = std::move(err); + last_printed_exit_count_ = exits; + } } last_daemon_state_ = static_cast(cur); } diff --git a/src/ui/windows/console_command_executor.h b/src/ui/windows/console_command_executor.h index f203913..394b0af 100644 --- a/src/ui/windows/console_command_executor.h +++ b/src/ui/windows/console_command_executor.h @@ -110,6 +110,8 @@ private: size_t last_daemon_output_size_ = 0; size_t last_xmrig_output_size_ = 0; int last_daemon_state_ = -1; // daemon::EmbeddedDaemon::State as int + std::string last_daemon_error_; // error text last printed (main thread only) + std::uint64_t last_printed_exit_count_ = 0; // daemon unexpected-exit count at that print bool last_rpc_connected_ = false; std::deque> results_; // {text, isError} std::mutex results_mutex_; diff --git a/src/ui/windows/console_tab_helpers.cpp b/src/ui/windows/console_tab_helpers.cpp index a79b346..0b53b91 100644 --- a/src/ui/windows/console_tab_helpers.cpp +++ b/src/ui/windows/console_tab_helpers.cpp @@ -51,6 +51,13 @@ float ClampConsoleWrapWidth(float contentWidth, float paddingX) return std::max(50.0f, contentWidth - paddingX * 2.0f); } +bool ShouldPrintDaemonErrorLine(bool wasError, bool isError, + const std::string& error, const std::string& lastPrinted, + bool newUnexpectedExit) +{ + return isError && (!wasError || error != lastPrinted || newUnexpectedExit); +} + bool ConsoleCommandCarriesSecret(const std::string& cmd) { size_t end = 0; diff --git a/src/ui/windows/console_tab_helpers.h b/src/ui/windows/console_tab_helpers.h index 2cb691c..473a95f 100644 --- a/src/ui/windows/console_tab_helpers.h +++ b/src/ui/windows/console_tab_helpers.h @@ -16,6 +16,14 @@ float ComputeConsoleOutputHeight(float availableHeight, float minHeightRatio); float ClampConsoleWrapWidth(float contentWidth, float paddingX); +// The console's daemon-error line prints once per error: on entering the Error state, when the +// reason changes while still in it, or when the daemon crashed again meanwhile (the console only polls +// while its tab is shown, so it can miss the Running state between two identical crashes). It used to +// print on every poll, i.e. every frame, while in Error. +bool ShouldPrintDaemonErrorLine(bool wasError, bool isError, + const std::string& error, const std::string& lastPrinted, + bool newUnexpectedExit = false); + // True if `cmd`'s first token names a console/RPC command that carries a SECRET on its command line // (passphrase, private/spending/viewing key, mnemonic). Output-secret commands (dumpprivkey, // z_exportkey, z_exportmnemonic) are NOT covered — their secret is in the result, a separate concern. diff --git a/src/util/perf_log.h b/src/util/perf_log.h index e1560f7..ef729b9 100644 --- a/src/util/perf_log.h +++ b/src/util/perf_log.h @@ -31,6 +31,8 @@ namespace util { * * Every N frames (default 300 ≈ 5s at 60fps) it appends a summary block * to /perf.log with min/avg/max/p95 for each zone. + * Off unless the app is started with DRAGONX_PERFLOG=1 (main.cpp calls init() only then); every + * call below is a near no-op until init() succeeds. */ class PerfLog { public: @@ -78,7 +80,7 @@ public: /// Begin a named zone. Returns an opaque time point. std::chrono::steady_clock::time_point beginZone() { - return Clock::now(); + return ready_ ? Clock::now() : Clock::time_point{}; } /// End a named zone. Pass the time point from beginZone(). diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 7e6ba52..c5fa764 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -5468,6 +5468,15 @@ void testRendererHelpers() EXPECT_NEAR(dragonx::ui::ClampConsoleWrapWidth(40.0f, 8.0f), 50.0, 0.0001); EXPECT_NEAR(dragonx::ui::ClampConsoleWrapWidth(200.0f, 12.0f), 176.0, 0.0001); + // The daemon-error line prints once per error, not on every poll (it flooded one line per frame). + using dragonx::ui::ShouldPrintDaemonErrorLine; + EXPECT_TRUE(ShouldPrintDaemonErrorLine(false, true, "bind failed", "")); // entering Error + EXPECT_FALSE(ShouldPrintDaemonErrorLine(true, true, "bind failed", "bind failed")); // still the same error + EXPECT_TRUE(ShouldPrintDaemonErrorLine(true, true, "binary missing", "bind failed")); // reason changed + EXPECT_TRUE(ShouldPrintDaemonErrorLine(false, true, "bind failed", "bind failed")); // a new episode + EXPECT_FALSE(ShouldPrintDaemonErrorLine(true, false, "bind failed", "")); // not in Error + EXPECT_TRUE(ShouldPrintDaemonErrorLine(true, true, "exit code 1", "exit code 1", true)); // crashed again + EXPECT_EQ(dragonx::ui::ClampMiningThreads(0, 8), 1); EXPECT_EQ(dragonx::ui::ClampMiningThreads(12, 8), 8); EXPECT_EQ(dragonx::ui::ClampMiningThreads(4, 8), 4); @@ -5783,6 +5792,12 @@ void testExplorerBlockCache() auto sameTipValidation = cache.prepareValidation(10, "hash-10"); EXPECT_FALSE(sameTipValidation.needed); + // A steady tip writes nothing: this check runs every frame the Explorer is open (it used to + // rewrite both tip rows each time). + const int changesBeforeSteadyTip = cache.totalChanges(); + for (int i = 0; i < 100; ++i) EXPECT_FALSE(cache.prepareValidation(10, "hash-10").needed); + EXPECT_EQ(cache.totalChanges(), changesBeforeSteadyTip); + auto advancedValidation = cache.prepareValidation(12, "hash-12"); EXPECT_TRUE(advancedValidation.needed); EXPECT_EQ(advancedValidation.height, 10);