perf(settings): write settings.json and wallets.json off the UI thread
Settings::save() and WalletIndex::save() ran the atomic write (temp file, fsync, rename, directory fsync) on the UI thread: on every preference change, and for wallets.json after every address-scan pass whose wallet metadata changed, i.e. repeatedly during sync, when the disk is busiest. Each took milliseconds to tens of milliseconds, and seconds under heavy disk load. The JSON is still built on the caller; the write is queued on an App-owned CoalescingWriter, latest wins per file, one write at a time. - After a fund- or security-critical setter (seed-migration stage, encryption pending, PIN enabled, active wallet file, wizard completed) the next save still waits until the file is on disk, so e.g. the sweep-submitted marker is durable before the sweep is broadcast. A failed durable save keeps the next one durable. - Saves before the writer is attached (the upgrade re-save at startup), and in tests and tools, stay synchronous. - main() flushes the queue (at most 3 s) before and after App::shutdown(), before the single-instance lock is released, so a relaunch never reads a stale settings.json. A kill or power loss right after an ordinary save can now lose that last preference change; the previous file stays intact. WalletIndex saves report "queued" rather than "written" (no caller reads it). On a wedged disk, exit can wait up to 2 x 3 s longer, inside the 15 s watchdog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8138,6 +8138,69 @@ void testAtomicFileWrite()
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// C269: Settings + WalletIndex saves through the App's background writer, under a temp $HOME (as
|
||||
// testWalletIndex does; Windows' config dir can't be redirected). The HOME redirect is what keeps a
|
||||
// developer's real settings.json untouched (getConfigDir() uses only $HOME/.config); XDG_CONFIG_HOME is
|
||||
// redirected too purely as a precaution. A queued save lands after flush(); a critical setter
|
||||
// makes the next save wait for the disk (readable with NO flush), and an older queued snapshot can't land
|
||||
// after it.
|
||||
void testSettingsBackgroundSave()
|
||||
{
|
||||
using ms = std::chrono::milliseconds;
|
||||
const char* oldHome = std::getenv("HOME");
|
||||
const std::string savedHome = oldHome ? oldHome : "";
|
||||
const char* oldXdg = std::getenv("XDG_CONFIG_HOME");
|
||||
const bool hadXdg = oldXdg != nullptr;
|
||||
const std::string savedXdg = hadXdg ? oldXdg : "";
|
||||
const fs::path tmp = makeTempDir();
|
||||
setenv("HOME", tmp.string().c_str(), 1);
|
||||
setenv("XDG_CONFIG_HOME", (tmp / ".config").string().c_str(), 1);
|
||||
{
|
||||
dragonx::util::CoalescingWriter w; // declared first: outlives s and idx
|
||||
dragonx::config::Settings s;
|
||||
s.setFileWriter(&w);
|
||||
s.setUIOpacity(0.55f);
|
||||
EXPECT_TRUE(s.save()); // queued
|
||||
s.setUIOpacity(0.65f);
|
||||
EXPECT_TRUE(s.save()); // coalesces over / follows the first
|
||||
EXPECT_TRUE(w.flush(ms(5000)));
|
||||
{
|
||||
dragonx::config::Settings back;
|
||||
EXPECT_TRUE(back.load());
|
||||
EXPECT_NEAR(back.getUIOpacity(), 0.65, 1e-6);
|
||||
}
|
||||
// A critical setter makes the next save wait for the disk: readable with NO flush, and the
|
||||
// queued older snapshot can't land after it.
|
||||
s.setUIOpacity(0.70f);
|
||||
EXPECT_TRUE(s.save()); // queued
|
||||
s.setPinEnabled(true);
|
||||
EXPECT_TRUE(s.save()); // durable
|
||||
{
|
||||
dragonx::config::Settings back;
|
||||
EXPECT_TRUE(back.load());
|
||||
EXPECT_TRUE(back.getPinEnabled());
|
||||
EXPECT_NEAR(back.getUIOpacity(), 0.70, 1e-6);
|
||||
}
|
||||
EXPECT_EQ(w.pendingCount(), (size_t)0);
|
||||
|
||||
dragonx::data::WalletIndex idx;
|
||||
idx.setFileWriter(&w);
|
||||
idx.load();
|
||||
dragonx::data::WalletIndexEntry e; e.fileName = "wallet.dat"; e.displayName = "Main";
|
||||
idx.upsert(e);
|
||||
EXPECT_TRUE(idx.save()); // queued
|
||||
EXPECT_TRUE(w.flush(ms(5000)));
|
||||
dragonx::data::WalletIndex idx2;
|
||||
EXPECT_TRUE(idx2.load());
|
||||
EXPECT_TRUE(idx2.find("wallet.dat") != nullptr);
|
||||
}
|
||||
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
|
||||
if (hadXdg) setenv("XDG_CONFIG_HOME", savedXdg.c_str(), 1); else unsetenv("XDG_CONFIG_HOME");
|
||||
removeTree(tmp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
// C218: openUrl/openFolder must return without waiting for the opener. Fake xdg-open/open that sleep 3 s
|
||||
// are the ONLY entries on PATH (posix_spawnp keeps searching after EACCES, so on a noexec temp dir an
|
||||
@@ -12665,6 +12728,7 @@ int main()
|
||||
testCoalescingWriterOrdering();
|
||||
testAtomicFileWrite();
|
||||
#ifndef _WIN32
|
||||
testSettingsBackgroundSave();
|
||||
testOpenerDoesNotBlock();
|
||||
#endif
|
||||
testHushChatCrypto();
|
||||
|
||||
Reference in New Issue
Block a user