perf(skins): skip redundant skin parsing, base reloads and theme writes
Skin handling did more work than it needed: - the bundled-skin scan fully parsed each of the 9 bundled skin files to read name, author, dark and images from their first 327-345 bytes. The build copies the rest of ui.toml into every skin, so each file is 51-54 KB; - setActiveSkin() reloaded ui.toml right after main() had loaded it, a second full parse with nothing merged in between; - in the Windows single-file exe, every getBundledSkinsDirectory() call (skin refresh, the dragonx entry, each image-dir lookup) re-extracted all embedded themes with a blind overwrite: more than once per launch, and again on skin changes. Now: - bundled skins are parsed only up to the first table header after [theme] other than [theme.images]. Anything unusual (no [theme], images defined later, a parse error in the slice) falls back to the full parse, and user skins are still parsed in full; - UISchema tracks whether its state is exactly the base (the last change was a base load and no hot-reload is pending), and setActiveSkin() skips reloadBase() in that case; - the bundled-skins directory is resolved once per process (re-checked with exists()), and theme extraction rewrites a file only when its bytes differ, through the new resources::writeFileIfDifferent(). It compares bytes, not sizes, so a same-length palette edit still updates. Measured on the Lite build (median of 5 runs): - bundled-skin scan: 15 -> 1 ms; - from "Initializing ObsidianDragon" to the activated skin: 29 -> 12 ms; - ui.toml is loaded once per launch instead of twice, and the light skin merges its overlay exactly once. A Python mirror of the header-slice parse agrees with a full parse on name, author, dark and images for all 18 source and build-expanded skin files, and full screenshot sweeps over all 10 skins match the previous build within noise. New unit tests cover writeFileIfDifferent(): missing file, identical bytes (mtime untouched), same-length change, and a longer file truncated. Behaviour changes: - a bundled skin with a TOML error after its header is now listed instead of skipped; the error surfaces only when it is selected; - deleting a single file inside the extracted bundled-themes dir mid-session is repaired only at the next launch; - the "Extracted N embedded bundled themes" count includes files that were already current. Not verifiable here: Linux has no embedded themes, so the new writeFileIfDifferent() ran only in the unit tests and the extraction path was only compiled, in the Windows cross-build. That a second Windows launch leaves the theme files' mtimes untouched is still to be checked on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15332,6 +15332,44 @@ void testLoadLanguageFromLooseFiles()
|
||||
EXPECT_TRUE(i18n.loadLanguage("en")); // restore the built-in English table
|
||||
EXPECT_EQ(std::string(i18n.translate("overview")), std::string("Overview"));
|
||||
}
|
||||
|
||||
// resources::writeFileIfDifferent (bundled-theme extraction): rewrite a file only when its bytes differ.
|
||||
void testWriteFileIfDifferent()
|
||||
{
|
||||
namespace res = dragonx::resources;
|
||||
const fs::path dir = testTmpPath("obsidian-write-if-different");
|
||||
removeTree(dir);
|
||||
fs::create_directories(dir);
|
||||
const fs::path p = dir / "theme.toml";
|
||||
const std::string v1 = "[theme]\nname = \"A\"\n";
|
||||
auto bytes = [](const std::string& str) { return reinterpret_cast<const uint8_t*>(str.data()); };
|
||||
|
||||
// (a) missing file -> written with the right content
|
||||
EXPECT_EQ(res::writeFileIfDifferent(p.string(), bytes(v1), v1.size()), 1);
|
||||
EXPECT_EQ(readProvisionFile(p), v1);
|
||||
|
||||
// (b) identical bytes -> untouched (mtime pinned to a known older value first, so any rewrite would show)
|
||||
const auto pinned = fs::last_write_time(p) - std::chrono::hours(1);
|
||||
fs::last_write_time(p, pinned);
|
||||
EXPECT_EQ(res::writeFileIfDifferent(p.string(), bytes(v1), v1.size()), 0);
|
||||
EXPECT_TRUE(fs::last_write_time(p) == pinned);
|
||||
EXPECT_EQ(readProvisionFile(p), v1);
|
||||
|
||||
// (c) same length, one byte flipped -> rewritten (not a size-only check)
|
||||
std::string v2 = v1;
|
||||
v2[v2.size() - 3] = 'B';
|
||||
EXPECT_EQ(v2.size(), v1.size());
|
||||
EXPECT_EQ(res::writeFileIfDifferent(p.string(), bytes(v2), v2.size()), 1);
|
||||
EXPECT_EQ(readProvisionFile(p), v2);
|
||||
|
||||
// (d) longer file on disk -> rewritten and truncated to the new size
|
||||
writeTestFile(p, v2 + "# trailing bytes that must disappear\n");
|
||||
EXPECT_EQ(res::writeFileIfDifferent(p.string(), bytes(v1), v1.size()), 1);
|
||||
EXPECT_EQ(readProvisionFile(p), v1);
|
||||
EXPECT_EQ(static_cast<size_t>(fs::file_size(p)), v1.size());
|
||||
|
||||
removeTree(dir);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -15582,6 +15620,7 @@ int main()
|
||||
testTextFit();
|
||||
testTextFitBanner();
|
||||
testLoadLanguageFromLooseFiles();
|
||||
testWriteFileIfDifferent();
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||
|
||||
Reference in New Issue
Block a user