perf(explorer): fetch each block with one RPC and back off on failure

Each recent block was fetched with two round trips (getblockhash then
getblock), and a height that failed, such as one briefly above the tip
during a reorg, was refetched on every frame.

- getblock is now called once with the height directly, falling back to
  the getblockhash plus getblock pair only when an older daemon answers
  "not found" for the short string; any other error is a real answer.
- A failed fetch backs off with a doubling wait (2 s up to 32 s) before
  any new fetch is scheduled, and a successful fetch resets the backoff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 09:24:51 -05:00
co-authored by Claude Opus 4.8
parent ebbeb846e8
commit 944fafa1c7
+23 -3
View File
@@ -30,6 +30,7 @@
#include <atomic>
#include <map>
#include <set>
#include <chrono>
namespace dragonx {
namespace ui {
@@ -73,6 +74,10 @@ static std::string s_fuzzy_query_cached;
static std::vector<BlockSummary> s_fuzzy_results;
static constexpr int kMaxFuzzyResults = 100;
static std::atomic<int> s_pending_block_fetches{0};
// Backoff for failing recent-block fetches (main thread only): consecutive failed fetches double the
// wait (2s..60s) before any new fetch is scheduled, so a failing height is not refetched every frame.
static int s_block_fetch_failures = 0;
static std::chrono::steady_clock::time_point s_block_fetch_retry_after{};
static bool s_recent_disk_cache_validation_pending = false;
// Block detail modal
@@ -277,6 +282,7 @@ static bool fetchRecentBlock(App* app, int height) {
auto* rpc = app->rpc();
if (height < 1 || !worker || !rpc) return false;
if (s_pending_block_heights.find(height) != s_pending_block_heights.end()) return false;
if (std::chrono::steady_clock::now() < s_block_fetch_retry_after) return false;
s_pending_block_heights.insert(height);
s_pending_block_fetches.fetch_add(1);
@@ -285,9 +291,17 @@ static bool fetchRecentBlock(App* app, int height) {
BlockSummary block;
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Recent blocks");
auto hashResult = rpc->call("getblockhash", {height});
auto hash = hashResult.get<std::string>();
auto result = rpc->call("getblock", {hash});
json result;
try {
// One round trip: getblock accepts a height string directly.
result = rpc->call("getblock", {std::to_string(height)});
} catch (const rpc::RpcError& e) {
// Older daemons treat a short string as a hash and answer -5 (not found); fall back to
// getblockhash + getblock. Any other error (e.g. -8 out of range) is a real answer.
if (e.code != -5) throw;
auto hash = rpc->call("getblockhash", {height}).get<std::string>();
result = rpc->call("getblock", {hash});
}
block.height = result.value("height", 0);
block.hash = result.value("hash", "");
block.time = result.value("time", (int64_t)0);
@@ -300,6 +314,12 @@ static bool fetchRecentBlock(App* app, int height) {
return [height, block = std::move(block)]() mutable {
if (block.height > 0) {
s_recent_block_cache.storeBlock(block);
s_block_fetch_failures = 0;
} else {
// A height briefly above the daemon tip (reorg) lands here too; it self-heals after the wait.
s_block_fetch_failures = std::min(s_block_fetch_failures + 1, 5);
s_block_fetch_retry_after = std::chrono::steady_clock::now() +
std::chrono::seconds(std::min(60, 1 << s_block_fetch_failures));
}
s_pending_block_heights.erase(height);
if (s_pending_block_fetches.load() > 0) {