Skip to content

Commit 6f24cbe

Browse files
authored
Save optimization (#2152)
* don't resave saved values if they didn't change * dont resave settings unless changed * save mod settings on apply * set dirty if some settings values are not set in save data; also make settings load fail not skip saved value load * add taken vs dirty system for perma and temp borrows of saved and settings containers * add spaces to ifs for hater mat * remove unused func declaration * fix taken value not being read in mod setting manager * getsavecontainertemp can probs be private * document dirty vs taken for saved values * also document the thought process in getsavecontainer vs temp * move to enum, use clearer var names * fix missing auto& * rename res2 variable to res * add queueSave to the one config loading failure mode that didnt have it
1 parent 34a11b6 commit 6f24cbe

8 files changed

Lines changed: 157 additions & 22 deletions

File tree

loader/include/Geode/loader/Mod.hpp

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ namespace geode {
8282
friend void GEODE_CALL ::geode_implicit_load(Mod*);
8383

8484
void settingReact(geode::Function<void()> fn);
85+
86+
matjson::Value& getSaveContainerTemp();
8587
public:
8688
// no copying
8789
Mod(Mod const&) = delete;
@@ -242,6 +244,7 @@ namespace geode {
242244
}
243245

244246
matjson::Value& getSaveContainer();
247+
matjson::Value const& getSaveContainerConst() const;
245248
matjson::Value& getSavedSettingsData();
246249

247250
/**
@@ -286,7 +289,7 @@ namespace geode {
286289

287290
template <class T>
288291
T getSavedValue(std::string_view key) {
289-
auto& saved = this->getSaveContainer();
292+
auto& saved = this->getSaveContainerConst();
290293
if (auto res = saved.get(key).andThen([](auto&& v) {
291294
return v.template as<T>();
292295
}); res.isOk()) {
@@ -297,13 +300,15 @@ namespace geode {
297300

298301
template <class T>
299302
T getSavedValue(std::string_view key, T const& defaultValue) {
300-
auto& saved = this->getSaveContainer();
303+
auto& saved = this->getSaveContainerConst();
301304
if (auto res = saved.get(key).andThen([](auto&& v) {
302305
return v.template as<T>();
303306
}); res.isOk()) {
304307
return res.unwrap();
305308
}
306-
saved[key] = matjson::Value(defaultValue);
309+
310+
auto& savedMutable = this->getSaveContainerTemp();
311+
savedMutable[key] = matjson::Value(defaultValue);
307312
return defaultValue;
308313
}
309314

@@ -316,8 +321,22 @@ namespace geode {
316321
*/
317322
template <class T>
318323
T setSavedValue(std::string_view key, T const& value) {
319-
auto& saved = this->getSaveContainer();
320324
auto old = this->getSavedValue<T>(key);
325+
326+
// optimization: if the value is the same, don't write to the save container
327+
// constexpr checks needed to avoid compile errors for types that don't support operator==
328+
if constexpr (std::ranges::range<T>) {
329+
using Elem = std::ranges::range_value_t<T>;
330+
if constexpr (requires(Elem const& a, Elem const& b) { { a == b } -> std::convertible_to<bool>; }) {
331+
if (old == value) return old;
332+
}
333+
}
334+
else if constexpr (requires(T const& a, T const& b) { { a == b } -> std::convertible_to<bool>; }) {
335+
if (old == value) return old;
336+
}
337+
338+
// value changed, write to save container
339+
auto& saved = this->getSaveContainer();
321340
saved[key] = value;
322341
return old;
323342
}

loader/include/Geode/loader/ModSettingsManager.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace geode {
1616
friend class ::geode::Mod;
1717

1818
void markRestartRequired();
19+
void queueSave();
20+
void saveFinished();
1921

2022
public:
2123
static ModSettingsManager* from(Mod* mod);
@@ -67,5 +69,7 @@ namespace geode {
6769
* for this mod, they are also reloaded for the dependant mods
6870
*/
6971
void addDependant(Mod* mod);
72+
73+
bool shouldSave() const;
7074
};
7175
}

loader/src/loader/Mod.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ matjson::Value& Mod::getSaveContainer() {
4545
return m_impl->getSaveContainer();
4646
}
4747

48+
matjson::Value& Mod::getSaveContainerTemp() {
49+
return m_impl->getSaveContainerTemp();
50+
}
51+
52+
matjson::Value const& Mod::getSaveContainerConst() const {
53+
return m_impl->getSaveContainerConst();
54+
}
55+
4856
matjson::Value& Mod::getSavedSettingsData() {
4957
return m_impl->m_settings->getSaveData();
5058
}

loader/src/loader/ModImpl.cpp

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ VersionInfo Mod::Impl::getVersion() const {
155155
}
156156

157157
matjson::Value& Mod::Impl::getSaveContainer() {
158+
// saved value taken, so we need to save it every time
159+
// since we dont know what the caller will do with the container
160+
m_saveRequestState = SaveRequestState::SaveUntilExit;
161+
162+
return m_saved;
163+
}
164+
165+
matjson::Value& Mod::Impl::getSaveContainerTemp() {
166+
// saved value dirty - caller promises to get rid
167+
// of its ref before next save
168+
if(m_saveRequestState == SaveRequestState::Clean) {
169+
m_saveRequestState = SaveRequestState::SaveOnce;
170+
}
171+
172+
return m_saved;
173+
}
174+
175+
matjson::Value const& Mod::Impl::getSaveContainerConst() const {
158176
return m_saved;
159177
}
160178

@@ -170,7 +188,7 @@ bool Mod::Impl::needsEarlyLoad(std::vector<Mod*>& checked) const {
170188
checked.push_back(m_self);
171189
if (this->getMetadata().needsEarlyLoad()) return true;
172190
for (auto& dep : m_dependants) {
173-
if(std::find(checked.begin(), checked.end(), dep) != checked.end()) continue;
191+
if (std::find(checked.begin(), checked.end(), dep) != checked.end()) continue;
174192
if (dep->m_impl->needsEarlyLoad(checked)) return true;
175193
}
176194
return false;
@@ -199,11 +217,19 @@ Result<> Mod::Impl::loadData() {
199217
// Check if settings exist
200218
auto settingPath = m_saveDirPath / "settings.json";
201219
if (std::filesystem::exists(settingPath)) {
202-
GEODE_UNWRAP_INTO(auto json, utils::file::readJson(settingPath));
203-
auto load = m_settings->load(json);
204-
if (!load) {
205-
log::warn("Unable to load settings: {}", load.unwrapErr());
220+
if (auto json = utils::file::readJson(settingPath)) {
221+
auto load = m_settings->load(json.unwrap());
222+
if (!load) {
223+
m_settings->queueSave();
224+
log::warn("Unable to load settings: {}", load.unwrapErr());
225+
}
226+
} else {
227+
// this used to early return but skipping saved values is not great behavior here imo
228+
m_settings->queueSave();
229+
log::warn("Unable to load settings: {}", json.unwrapErr());
206230
}
231+
} else {
232+
m_settings->queueSave();
207233
}
208234

209235
// Saved values
@@ -233,18 +259,36 @@ Result<> Mod::Impl::saveData() {
233259
}
234260

235261
// ModSettingsManager keeps track of the whole savedata
236-
matjson::Value json = m_settings->save();
262+
if (m_settings->shouldSave()) {
263+
log::debug("Saving settings for mod {}", m_metadata.getID());
237264

238-
// saveData is expected to be synchronous, and always called from GD thread
239-
ModStateEvent(ModEventType::DataSaved, std::move(m_self)).send();
265+
matjson::Value json = m_settings->save();
240266

241-
auto res = utils::file::writeStringSafe(m_saveDirPath / "settings.json", json.dump());
242-
if (!res) {
243-
log::error("Unable to save settings: {}", res.unwrapErr());
267+
// saveData is expected to be synchronous, and always called from GD thread
268+
ModStateEvent(ModEventType::DataSaved, std::move(m_self)).send();
269+
270+
auto res = utils::file::writeStringSafe(m_saveDirPath / "settings.json", json.dump());
271+
if (!res) {
272+
log::error("Unable to save settings: {}", res.unwrapErr());
273+
} else {
274+
m_settings->saveFinished();
275+
}
276+
} else {
277+
// duplicated line to retain old expectations of saveData being called after json dump but before file write
278+
ModStateEvent(ModEventType::DataSaved, std::move(m_self)).send();
244279
}
245-
auto res2 = utils::file::writeStringSafe(m_saveDirPath / "saved.json", m_saved.dump());
246-
if (!res2) {
247-
log::error("Unable to save values: {}", res2.unwrapErr());
280+
281+
if (m_saveRequestState != SaveRequestState::Clean) {
282+
log::debug("Saving values for mod {}", m_metadata.getID());
283+
284+
auto res = utils::file::writeStringSafe(m_saveDirPath / "saved.json", m_saved.dump());
285+
if (!res) {
286+
log::error("Unable to save values: {}", res.unwrapErr());
287+
}
288+
289+
if(m_saveRequestState == SaveRequestState::SaveOnce) {
290+
m_saveRequestState = SaveRequestState::Clean;
291+
}
248292
}
249293

250294
return Ok();

loader/src/loader/ModImpl.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
#include <Geode/loader/Signal.hpp>
99

1010
namespace geode {
11+
enum class SaveRequestState {
12+
Clean,
13+
SaveOnce,
14+
SaveUntilExit,
15+
};
16+
1117
class Mod::Impl {
1218
public:
1319
Mod* m_self;
@@ -49,6 +55,11 @@ namespace geode {
4955
* Saved values
5056
*/
5157
matjson::Value m_saved = matjson::Value();
58+
/**
59+
* Whether the saved values need to be saved to disk one time
60+
* (container dirty)
61+
*/
62+
SaveRequestState m_saveRequestState = SaveRequestState::Clean;
5263
/**
5364
* Setting values. This is behind unique_ptr for interior mutability
5465
*/
@@ -105,6 +116,8 @@ namespace geode {
105116
bool isEphemeral() const;
106117

107118
matjson::Value& getSaveContainer();
119+
matjson::Value& getSaveContainerTemp();
120+
matjson::Value const& getSaveContainerConst() const;
108121

109122
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
110123
void setMetadata(ModMetadata const& metadata);

loader/src/loader/ModSettingsManager.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class ModSettingsManager::Impl final {
188188
// update this by calling saveSettingValueToSave
189189
matjson::Value savedata;
190190
bool restartRequired = false;
191+
SaveRequestState saveRequestState = SaveRequestState::Clean;
191192

192193
bool loadSettingValueFromSave(std::string const& key) {
193194
if (this->savedata.contains(key) && this->settings.contains(key)) {
@@ -205,6 +206,12 @@ class ModSettingsManager::Impl final {
205206
return true;
206207
}
207208
else {
209+
if (!this->savedata.contains(key)) {
210+
log::error("Unable to load setting '{}' for mod {} (not found in savedata)", key, this->modID);
211+
if(saveRequestState == SaveRequestState::Clean) {
212+
saveRequestState = SaveRequestState::SaveOnce;
213+
}
214+
}
208215
return false;
209216
}
210217
}
@@ -237,7 +244,9 @@ class ModSettingsManager::Impl final {
237244
}
238245
if (auto v3 = (*gen)(key, modID, setting.json)) {
239246
setting.v3 = v3.unwrap();
240-
this->loadSettingValueFromSave(key);
247+
248+
// the loading is unnecessary, m_saveData is not initialized yet
249+
// this->loadSettingValueFromSave(key);
241250
}
242251
else {
243252
log::error(
@@ -280,6 +289,22 @@ void ModSettingsManager::markRestartRequired() {
280289
m_impl->restartRequired = true;
281290
}
282291

292+
void ModSettingsManager::queueSave() {
293+
if(m_impl->saveRequestState == SaveRequestState::Clean) {
294+
m_impl->saveRequestState = SaveRequestState::SaveOnce;
295+
}
296+
}
297+
298+
void ModSettingsManager::saveFinished() {
299+
if(m_impl->saveRequestState == SaveRequestState::SaveOnce) {
300+
m_impl->saveRequestState = SaveRequestState::Clean;
301+
}
302+
}
303+
304+
bool ModSettingsManager::shouldSave() const {
305+
return m_impl->saveRequestState != SaveRequestState::Clean;
306+
}
307+
283308
Result<> ModSettingsManager::registerCustomSettingType(std::string_view type, SettingGenerator generator) {
284309
GEODE_UNWRAP(SharedSettingTypesPool::get().add(m_impl->modID, type, std::move(generator)));
285310
m_impl->createSettings();
@@ -310,6 +335,14 @@ Result<> ModSettingsManager::load(matjson::Value const& json) {
310335
}
311336
}
312337
}
338+
339+
for (auto const& [key, _] : m_impl->settings) {
340+
if (!json.contains(key)) {
341+
log::error("Unable to load setting '{}' for mod {} (not found in savedata)", key, m_impl->modID);
342+
this->queueSave();
343+
break;
344+
}
345+
}
313346
}
314347
return Ok();
315348
}
@@ -323,6 +356,8 @@ matjson::Value ModSettingsManager::save() {
323356
}
324357

325358
matjson::Value& ModSettingsManager::getSaveData() {
359+
m_impl->saveRequestState = SaveRequestState::SaveUntilExit;
360+
326361
return m_impl->savedata;
327362
}
328363

loader/src/loader/SettingV3.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,11 @@ Mod* SettingV3::getMod() const {
575575

576576
void SettingV3::markChanged() {
577577
auto manager = ModSettingsManager::from(this->getMod());
578-
if (m_impl->requiresRestart) {
579-
manager->markRestartRequired();
578+
if (manager) {
579+
if (m_impl->requiresRestart) {
580+
manager->markRestartRequired();
581+
}
582+
manager->queueSave();
580583
}
581584
SettingChangedEventV3(this->getModID(), this->getKey()).send(shared_from_this());
582585
}

loader/src/ui/mods/settings/ModSettingsPopup.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,16 @@ bool ModSettingsPopup::init(Mod* mod, bool forceDisableTheme) {
6565

6666
void ModSettingsPopup::updateState(SettingNode* invoker) {
6767
BaseSettingsPopup::updateState(invoker);
68-
m_restartBtn->setVisible(ModSettingsManager::from(m_mod)->restartRequired());
68+
69+
auto manager = ModSettingsManager::from(m_mod);
70+
m_restartBtn->setVisible(manager->restartRequired());
71+
72+
// frame delay for debounce (avoids repeating save for every changed setting)
73+
Loader::get()->queueInMainThread([mod = m_mod, manager] {
74+
if (manager->shouldSave()) {
75+
(void) mod->saveData();
76+
}
77+
});
6978
}
7079

7180
void ModSettingsPopup::onOpenSaveDirectory(CCObject*) {

0 commit comments

Comments
 (0)