perf(wallets): walk added wallet folders off the UI thread
Every open of the Wallets dialog, Refresh, folder add or remove, and Open on a stale row walked each user-added folder recursively on the UI thread before the dialog drew: up to 40,000 entries per folder, roughly 50-150 ms warm on Linux and seconds on a cold disk, a network share or Windows. The datadir listing (a few top-level files) stays synchronous, so a wallet just created shows at once. The recursive walk runs on a detached worker with the same depth, hit and entry limits; its rows are merged at the start of a later frame and only the new rows are probed. - The dialog opens at once with the datadir rows. The external rows arrive when the walk ends, with "Scanning folders..." beside the scanned-folders caption, and the card grows once. - Reopening within 60 s with the same folders reuses the last walk without touching the disk, so its rows and sizes can be up to a minute stale: a wallet copied into a folder in that minute needs Refresh. Refresh and Open on a stale row always walk again. - While a walk or a wallet probe runs (up to 30 s) the dialog keeps frames coming, so rows and badges appear without moving the mouse. - The 200-row cap now holds across all folders; each extra folder could add one row past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,7 @@
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "util/wallet_file_probe.h"
|
||||
#include "util/wallet_folder_scan.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
#include "wallet/lite_diagnostics.h"
|
||||
#include "wallet/lite_owned_string.h"
|
||||
@@ -3340,6 +3341,123 @@ void testVerifySaplingParams()
|
||||
fsn::remove_all(dir2, rmec);
|
||||
}
|
||||
|
||||
// Wallets dialog: the pure folder walk that moved out of WalletsDialog::scan() (now run on a worker).
|
||||
void testWalletFolderScan()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
namespace fsn = std::filesystem;
|
||||
const fsn::path root = makeTempDir() / "wallet_folder_scan";
|
||||
const fsn::path dd = root / "datadir", ext = root / "ext", ext2 = root / "ext2";
|
||||
auto touch = [](const fsn::path& p, const std::string& body = "x") {
|
||||
std::error_code ec;
|
||||
fsn::create_directories(p.parent_path(), ec);
|
||||
std::ofstream(p.string(), std::ios::binary) << body;
|
||||
};
|
||||
auto names = [](const std::vector<ScannedWallet>& v) {
|
||||
std::set<std::string> s;
|
||||
for (const auto& w : v) s.insert(w.fileName);
|
||||
return s;
|
||||
};
|
||||
|
||||
// Datadir: top level only, wallet-prefixed, in-place links and node files hidden.
|
||||
for (const char* n : { "wallet.dat", "wallet-x.dat", "wallet-ip-1a.dat", "peers.dat", "other.dat" }) touch(dd / n);
|
||||
touch(dd / "blocks" / "blk00000.dat");
|
||||
{
|
||||
std::unordered_set<std::string> seen;
|
||||
const auto rows = scanDatadirWallets(dd.string(), seen);
|
||||
EXPECT_EQ(rows.size(), static_cast<std::size_t>(2));
|
||||
EXPECT_TRUE((names(rows) == std::set<std::string>{ "wallet.dat", "wallet-x.dat" }));
|
||||
for (const auto& w : rows) EXPECT_TRUE(w.inDatadir);
|
||||
EXPECT_EQ(seen.size(), static_cast<std::size_t>(2));
|
||||
std::unordered_set<std::string> seen2;
|
||||
EXPECT_TRUE(scanDatadirWallets((root / "missing").string(), seen2).empty());
|
||||
}
|
||||
|
||||
// External: any .dat except node junk, recursive within the depth cap, never into node subtrees.
|
||||
touch(ext / "a.dat");
|
||||
touch(ext / "peers.dat");
|
||||
touch(ext / "blk1.dat");
|
||||
touch(ext / "blocks" / "b.dat");
|
||||
touch(ext / "sub" / "w.dat");
|
||||
touch(ext / "d1" / "d2" / "d3" / "deep.dat");
|
||||
touch(ext / "notes.txt");
|
||||
{
|
||||
WalletScanLimits lim;
|
||||
lim.maxDepth = 2;
|
||||
std::unordered_set<std::string> seen;
|
||||
const auto rows = scanExternalWalletFolders({ ext.string() }, lim, seen, 0);
|
||||
EXPECT_TRUE((names(rows) == std::set<std::string>{ "a.dat", "w.dat" }));
|
||||
for (const auto& w : rows) {
|
||||
EXPECT_TRUE(!w.inDatadir);
|
||||
if (w.fileName == "w.dat") EXPECT_TRUE(fsn::path(w.dir) == ext / "sub"); // the real parent
|
||||
if (w.fileName == "a.dat") EXPECT_TRUE(fsn::path(w.dir) == ext);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::unordered_set<std::string> seen;
|
||||
const auto rows = scanExternalWalletFolders({ ext.string() }, WalletScanLimits{}, seen, 0);
|
||||
EXPECT_TRUE((names(rows) == std::set<std::string>{ "a.dat", "w.dat", "deep.dat" }));
|
||||
}
|
||||
// The hit cap holds across folders.
|
||||
touch(ext2 / "e.dat");
|
||||
{
|
||||
WalletScanLimits lim;
|
||||
lim.maxHits = 1;
|
||||
std::unordered_set<std::string> seen;
|
||||
EXPECT_EQ(scanExternalWalletFolders({ ext.string(), ext2.string() }, lim, seen, 0).size(),
|
||||
static_cast<std::size_t>(1));
|
||||
}
|
||||
// The visit cap stops the walk early.
|
||||
{
|
||||
WalletScanLimits lim;
|
||||
lim.maxVisited = 1;
|
||||
std::unordered_set<std::string> seen;
|
||||
EXPECT_TRUE(scanExternalWalletFolders({ ext.string() }, lim, seen, 0).size() <= 1);
|
||||
}
|
||||
// Rows already listed (the datadir pass) count toward the hit cap.
|
||||
{
|
||||
std::unordered_set<std::string> seen;
|
||||
EXPECT_TRUE(scanExternalWalletFolders({ ext.string() }, WalletScanLimits{}, seen, 200).size() <= 1);
|
||||
}
|
||||
// A cancelled walk returns nothing.
|
||||
{
|
||||
std::atomic<bool> cancel{true};
|
||||
std::unordered_set<std::string> seen;
|
||||
EXPECT_TRUE(scanExternalWalletFolders({ ext.string() }, WalletScanLimits{}, seen, 0, &cancel).empty());
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// A symlink to a datadir wallet inside an added folder is the same physical wallet: listed once.
|
||||
{
|
||||
const fsn::path lnDir = root / "ext_link";
|
||||
std::error_code ec;
|
||||
fsn::create_directories(lnDir, ec);
|
||||
fsn::create_symlink(dd / "wallet.dat", lnDir / "linked.dat", ec);
|
||||
EXPECT_TRUE(!ec);
|
||||
touch(lnDir / "own.dat");
|
||||
std::unordered_set<std::string> seen;
|
||||
const auto dataRows = scanDatadirWallets(dd.string(), seen);
|
||||
const auto rows = scanExternalWalletFolders({ lnDir.string() }, WalletScanLimits{}, seen, dataRows.size());
|
||||
EXPECT_TRUE((names(rows) == std::set<std::string>{ "own.dat" }));
|
||||
}
|
||||
#endif
|
||||
removeTree(root.parent_path());
|
||||
}
|
||||
|
||||
void testExternalScanCache()
|
||||
{
|
||||
using dragonx::util::externalScanKey;
|
||||
using dragonx::util::externalScanCacheUsable;
|
||||
EXPECT_TRUE(externalScanKey({ "a", "b" }) != externalScanKey({ "ab" }));
|
||||
EXPECT_TRUE(externalScanKey({}).empty());
|
||||
const std::string k = externalScanKey({ "/home/u/wallets" });
|
||||
EXPECT_TRUE(externalScanCacheUsable(true, k, k, 10.0, 60.0, false));
|
||||
EXPECT_TRUE(!externalScanCacheUsable(true, k, k, 10.0, 60.0, /*force=*/true));
|
||||
EXPECT_TRUE(!externalScanCacheUsable(true, k, externalScanKey({ "/other" }), 10.0, 60.0, false));
|
||||
EXPECT_TRUE(!externalScanCacheUsable(true, k, k, 61.0, 60.0, false));
|
||||
EXPECT_TRUE(!externalScanCacheUsable(false, k, k, 10.0, 60.0, false));
|
||||
EXPECT_TRUE(!externalScanCacheUsable(true, k, k, -1.0, 60.0, false));
|
||||
}
|
||||
|
||||
void testPlatformEnsureDirectory()
|
||||
{
|
||||
using dragonx::util::Platform;
|
||||
@@ -13570,6 +13688,8 @@ int main()
|
||||
#endif
|
||||
testPlatformEnsureDirectory();
|
||||
testVerifySaplingParams();
|
||||
testWalletFolderScan();
|
||||
testExternalScanCache();
|
||||
testConnectHasStalled();
|
||||
testIsLocalHost();
|
||||
testAllowsPlaintextRemote();
|
||||
|
||||
Reference in New Issue
Block a user