Game-setting locale indexer re-runs itself once a second on Linux (inotify IN_OPEN feedback loop)
Summary
On Linux the "Game setting locales" indexer runs continuously for as long as the app is open, about once per second, with no file changes in any instance. Each pass reads every instance's options.txt; that read is itself an inotify event; the instance file watcher sees options.txt and queues another index pass. Each pass also emits game-option-locales-updated to the frontend, so the WebKit process stays busy too.
Measured on 0.21.3 and 0.21.4 (Flatpak, Fedora 44, kernel 7.2): 495 passes in the first 5 minutes of one session, 3333 in a longer one, 137 in the first 90 s after updating to 0.21.4. Main process at ~96 % CPU and WebKitWebProcess at ~95 % (8 JSC Heap Helper threads at 85 % each, 2 GB heap) while it runs. UI is sluggish the whole time.
Log pattern
INFO index_installed_sources: ... Game setting locales: indexing installed instances instances=2
INFO index_installed_sources: ... snapshot loaded instance_id="local:..." game_version="1.20.1" mods=446
INFO index_installed_sources: ... snapshot loaded instance_id="local:..." game_version="26.2" mods=68
INFO index_installed_sources: ... origin resolution finished pinned=0 unresolved=133
INFO ... Game setting locales: indexing pass finished elapsed_ms=1116
INFO ... Game setting locales: emitted game-option-locales-updated
INFO init_watcher: ... Game setting locales: indexing queued started=true
INFO init_watcher: ... Game setting locales: indexing queued started=true
INFO index_installed_sources: ... indexing installed instances instances=2
... repeats indefinitely, ~1 Hz
The two indexing queued lines per cycle are one per instance (two options.txt files). Note every queue comes from the init_watcher span; no label request received lines, so the frontend is not the trigger.
Evidence that no file is being written
strace -f -e openat,rename,renameat2,utimensat,unlink,unlinkat on the backend for 6 s: no write-mode opens, renames or unlinks anywhere under profiles/. Only O_RDONLY opens of options.txt and pack directories (16–19 per instance in 6 s).
inotifywait -m -e open,close_nowrite,access profiles/<instance>/options.txt:
11:12:22 OPEN options.txt
11:12:22 ACCESS options.txt
11:12:22 CLOSE_NOWRITE options.txt
11:12:22 OPEN options.txt
...
inotifywait -r -m -e create,delete,modify,moved_to,moved_from,attrib,close_write profiles/ over the same period: no events on options.txt at all.
Cause
packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs: index_installed_sources calls read_document(&options_path(...)) for every instance.
notify 8.2.0's inotify backend includes WatchMask::OPEN in its watch mask (notify/src/inotify.rs, add_single_watch), so that read produces an EventKind::Access(Open) event.
notify-debouncer-mini 0.7.0 discards the event kind, so the callback in packages/app-lib/src/state/instances/watcher.rs cannot distinguish a read from a write. Its filter (watcher.rs ~L164-180) matches the options.txt file name and calls crate::api::instance::queue_game_locale_index().
- The indexer task (
start_game_locale_indexer) wakes, sleeps 300 ms, runs the pass, emits the event, and step 1 repeats.
Linux only: FSEvents and ReadDirectoryChangesW do not report opens, so macOS and Windows never enter the loop.
The same mechanism presumably fires reconcile_synced_option_file for command_history.txt, hotbar.nbt and servers.dat whenever anything reads them, but the locale indexer is the one that reads its own trigger file.
Suggested fix
Either of:
- Switch the instance watcher to
notify-debouncer-full (which keeps EventKind) and ignore EventKind::Access(_) before the path matching, or filter access events out via notify::Config / the debouncer's underlying watcher config where available.
- Or, keep the mini debouncer but make the locale indexer not read
options.txt through a watched path (e.g. snapshot the file contents once into the DB and have the watcher pass the changed document in), or add a cheap guard: skip queuing when the indexer itself is mid-pass and the file's mtime has not changed since the last pass.
Also worth debouncing the game-option-locales-updated emit so a burst of passes does not become a burst of frontend refreshes.
Environment
- Modrinth App 0.21.3 and 0.21.4, Flatpak
com.modrinth.ModrinthApp (Flathub)
- Fedora 44, Linux 7.2.4, GNOME Wayland, 16 GB RAM
- 2 instances: Fabulously Optimized (26.2, 68 mods) and Prominence II Hasturian Era (1.20.1, 446 mods)
- Full session logs available on request (
launcher_logs/session_*.log, 1–8 MB, almost entirely this loop)
Game-setting locale indexer re-runs itself once a second on Linux (inotify IN_OPEN feedback loop)
Summary
On Linux the "Game setting locales" indexer runs continuously for as long as the app is open, about once per second, with no file changes in any instance. Each pass reads every instance's
options.txt; that read is itself an inotify event; the instance file watcher seesoptions.txtand queues another index pass. Each pass also emitsgame-option-locales-updatedto the frontend, so the WebKit process stays busy too.Measured on 0.21.3 and 0.21.4 (Flatpak, Fedora 44, kernel 7.2): 495 passes in the first 5 minutes of one session, 3333 in a longer one, 137 in the first 90 s after updating to 0.21.4. Main process at ~96 % CPU and
WebKitWebProcessat ~95 % (8 JSC Heap Helper threads at 85 % each, 2 GB heap) while it runs. UI is sluggish the whole time.Log pattern
The two
indexing queuedlines per cycle are one per instance (twooptions.txtfiles). Note every queue comes from theinit_watcherspan; nolabel request receivedlines, so the frontend is not the trigger.Evidence that no file is being written
strace -f -e openat,rename,renameat2,utimensat,unlink,unlinkaton the backend for 6 s: no write-mode opens, renames or unlinks anywhere underprofiles/. OnlyO_RDONLYopens ofoptions.txtand pack directories (16–19 per instance in 6 s).inotifywait -m -e open,close_nowrite,access profiles/<instance>/options.txt:inotifywait -r -m -e create,delete,modify,moved_to,moved_from,attrib,close_write profiles/over the same period: no events onoptions.txtat all.Cause
packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs:index_installed_sourcescallsread_document(&options_path(...))for every instance.notify8.2.0's inotify backend includesWatchMask::OPENin its watch mask (notify/src/inotify.rs,add_single_watch), so that read produces anEventKind::Access(Open)event.notify-debouncer-mini0.7.0 discards the event kind, so the callback inpackages/app-lib/src/state/instances/watcher.rscannot distinguish a read from a write. Its filter (watcher.rs~L164-180) matches theoptions.txtfile name and callscrate::api::instance::queue_game_locale_index().start_game_locale_indexer) wakes, sleeps 300 ms, runs the pass, emits the event, and step 1 repeats.Linux only: FSEvents and ReadDirectoryChangesW do not report opens, so macOS and Windows never enter the loop.
The same mechanism presumably fires
reconcile_synced_option_fileforcommand_history.txt,hotbar.nbtandservers.datwhenever anything reads them, but the locale indexer is the one that reads its own trigger file.Suggested fix
Either of:
notify-debouncer-full(which keepsEventKind) and ignoreEventKind::Access(_)before the path matching, or filter access events out vianotify::Config/ the debouncer's underlying watcher config where available.options.txtthrough a watched path (e.g. snapshot the file contents once into the DB and have the watcher pass the changed document in), or add a cheap guard: skip queuing when the indexer itself is mid-pass and the file's mtime has not changed since the last pass.Also worth debouncing the
game-option-locales-updatedemit so a burst of passes does not become a burst of frontend refreshes.Environment
com.modrinth.ModrinthApp(Flathub)launcher_logs/session_*.log, 1–8 MB, almost entirely this loop)