refactor(audit): batch 3 — decompose App::update() + collapse acrylic fork

Three P1 structural refactors from the audit; no behavior change.

1. Extract the ~180-line daemon-stdout rescan/witness parser out of the
   ~790-line App::update() into a pure, static, unit-testable
   NetworkRefreshService::parseDaemonRescanOutput() returning a
   DaemonRescanScan result struct (sits next to the existing static parse*
   siblings). The scanning half moved verbatim; App::update() keeps the
   state-application half, now reading scan.* fields. Adds
   testParseDaemonRescanOutput() covering all six daemon signals + edge
   cases against fixture log snippets — previously untestable without a
   live daemon.

2. Extract the ~95-line global keyboard-shortcut block (Ctrl+, / theme
   cycle / F5 / low-spec / effects / gradient / wizard) out of
   App::update() into App::handleGlobalShortcuts().

3. Collapse the ImGuiAcrylic frontend fork: the GLAD and DX11 namespaces
   were ~375 lines of semantically-identical code (the frontend makes zero
   direct GL/DX calls — all backend work delegates to AcrylicMaterial, which
   has a backend per API). Extended the single frontend's guard to
   #if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) and deleted the
   DX11 duplicate, leaving the no-backend stub. Kills the Windows/DX11 drift
   hazard (acrylic frontend changes now made once).

Verified: full-node + Lite build clean; ctest 1/1 (incl. the new parser
tests); source-hygiene clean; and the collapsed acrylic path was
compile-verified under DX11 via the mingw-w64 Windows cross-build
(ObsidianDragon.exe links).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:40:40 -05:00
parent 7891c689fb
commit 280a71e973
6 changed files with 331 additions and 602 deletions

View File

@@ -2,6 +2,7 @@
#include "../util/logger.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <map>
@@ -1336,6 +1337,190 @@ void NetworkRefreshService::resetJobs()
}
}
NetworkRefreshService::DaemonRescanScan NetworkRefreshService::parseDaemonRescanOutput(const std::string& output)
{
DaemonRescanScan scan;
bool& foundRescan = scan.foundRescan;
bool& finished = scan.finished;
float& rescanPct = scan.rescanPct;
std::string& lastStatus = scan.lastStatus;
// Sapling witness rebuild progress. Two daemon signals:
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
// one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
bool& foundWitness = scan.foundWitness;
float& witnessPct = scan.witnessPct; // -1 = no fraction parsed this batch
int& witnessRemaining = scan.witnessRemaining;
int& witnessReadTotal = scan.witnessReadTotal; // new daemon: the <total> in "<done> / <total>"
bool& witnessRebuilt = scan.witnessRebuilt; // new daemon's completion line seen this batch
std::vector<std::string>& witnessTxids = scan.witnessTxids; // distinct-tx keys seen this batch
int& witnessTotalTxs = scan.witnessTotalTxs; // max N seen this batch
size_t pos = 0;
while (pos < output.size()) {
size_t eol = output.find('\n', pos);
if (eol == std::string::npos) eol = output.size();
std::string line = output.substr(pos, eol - pos);
pos = eol + 1;
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
lastStatus = line;
}
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9;
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
lastStatus = line;
}
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
finished = true;
}
// This daemon prints no "Done rescanning" line; instead it logs the rescan
// benchmark timing exactly when the scan finishes, e.g.:
// "... rescan 16760577ms"
// Match the lowercase " rescan " bench category ending in "<digits>ms" (the
// progress lines are "Still rescanning"/"Rescanning...", which never end in ms).
if (line.find(" rescan ") != std::string::npos) {
std::string t = line;
while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' '))
t.pop_back();
if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 &&
std::isdigit(static_cast<unsigned char>(t[t.size() - 3]))) {
finished = true;
}
}
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
auto witIdx = line.find("Building Witnesses for block");
if (witIdx != std::string::npos) {
foundWitness = true;
auto compIdx = line.find(" complete", witIdx);
if (compIdx != std::string::npos) {
size_t numEnd = compIdx, numStart = numEnd;
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
line[numStart - 1] == '.')) numStart--;
if (numStart < numEnd) {
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
auto remIdx = line.find(" remaining", witIdx);
if (remIdx != std::string::npos) {
size_t numEnd = remIdx, numStart = numEnd;
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
if (numStart < numEnd) {
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
// Newer multi-threaded daemon: "Reading blocks for witness rebuild: <done> / <total>".
// This replaced the per-block "Building Witnesses" line and is an exact done/total
// count, so capture BOTH (total drives the denominator directly — no peak anchoring).
auto rdIdx = line.find("Reading blocks for witness rebuild:");
if (rdIdx != std::string::npos) {
foundWitness = true;
auto slash = line.find('/', rdIdx);
if (slash != std::string::npos) {
size_t dEnd = slash;
while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--;
size_t dStart = dEnd;
while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--;
size_t tStart = slash + 1;
while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++;
size_t tEnd = tStart;
while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++;
if (dStart < dEnd && tStart < tEnd) {
try {
long done = std::stol(line.substr(dStart, dEnd - dStart));
long total = std::stol(line.substr(tStart, tEnd - tStart));
if (total > 0 && done >= 0 && done <= total) {
witnessRemaining = static_cast<int>(total - done);
witnessReadTotal = static_cast<int>(total);
}
} catch (...) {}
}
}
lastStatus = line;
}
// New daemon's witness-rebuild completion line:
// "... rebuilt <n> note witness cache(s) to height <h> in <ms>ms using <t> thread(s)".
// The parallel rebuild logs no progress, so on completion snap the bar to 100%.
if (line.find("note witness cache") != std::string::npos &&
line.find("thread(s)") != std::string::npos) {
foundWitness = true;
witnessRebuilt = true;
lastStatus = line;
}
// Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
// <i> is the tx's slot in an unordered map (bounces — useless as progress), so
// we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
// hash between "for tx " and the following comma, and N after " of ".
{
auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
if (setIdx != std::string::npos) {
foundWitness = true;
size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size();
size_t comma = line.find(',', hStart);
if (comma != std::string::npos && comma > hStart) {
witnessTxids.push_back(line.substr(hStart, comma - hStart));
auto ofIdx = line.find(" of ", comma);
if (ofIdx != std::string::npos) {
size_t nStart = ofIdx + 4, nEnd = nStart;
while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++;
if (nEnd > nStart) {
try {
int n = std::stoi(line.substr(nStart, nEnd - nStart));
if (n > witnessTotalTxs) witnessTotalTxs = n;
} catch (...) {}
}
}
}
lastStatus = line;
}
}
}
return scan;
}
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
{

View File

@@ -293,6 +293,23 @@ public:
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
const std::vector<std::string>& requestedOpids);
struct DaemonRescanScan {
bool foundRescan = false;
bool finished = false;
float rescanPct = 0.0f;
std::string lastStatus;
bool foundWitness = false;
float witnessPct = -1.0f;
int witnessRemaining = -1;
int witnessReadTotal = -1;
bool witnessRebuilt = false;
std::vector<std::string> witnessTxids;
int witnessTotalTxs = -1;
};
// Pure line-by-line scan of newly-appended dragonxd stdout for rescan + Sapling
// witness-rebuild progress. No I/O, no state — unit-tested against fixture log snippets.
static DaemonRescanScan parseDaemonRescanOutput(const std::string& output);
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);