Fix memory leak when viewing many files#243
Open
AlexB7 wants to merge 2 commits into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix for a memory leak when viewing many files in a folder. Eventually all RAM will be used resulting in swapping or a freeze. Fixes issues #234, #48 and #36.
Claude Code was used to find and fix this bug. Claude output:
The bug (present since the 2016 eog→xviewer fork, matching issue #36 from 2017 and upstream eog bug #674284 from 2012):
In src/xviewer-jobs.c, xviewer_job_load_run(), xviewer_job_save_run(), and xviewer_job_cancel() each take an extra g_object_ref(job) that's meant to be released either by an early-return path or by the g_idle_add_full(..., g_object_unref) completion callback. The early-return paths (triggered when a job gets cancelled) were missing the matching g_object_unref — unlike the sibling xviewer_job_copy_run/xviewer_job_transform_run, which handle this correctly. Since navigating to a new image cancels any still-in-flight load job for the previous one, this leaks the XviewerJobLoad object — and therefore the XviewerImage and its decoded GdkPixbuf pixel buffer — every time a load is interrupted by browsing forward, which is exactly what happens during normal/fast browsing or slideshow mode. This is almost certainly the actual long-standing leak.
A second, unrelated, newer bug in src/xviewer-image.c (introduced by commit c43d002, Nov 2025) that leaked the GdkPixbufAnimation on every static-image load was also fixed. That one was real but wasn't the historical leak — it only exists in builds after that commit.
Additional findings:
Root cause: xviewer_window_reload_image() (src/xviewer-window.c:6587) nulls out window->priv->image before the reload's background job finishes. When the job completes, xviewer_job_load_cb() (line 1360) only disconnects the previous "thumbnail_changed"/"file-changed" signal handlers if priv->image != NULL (line 1378) — but since reload_image() already nulled it, that guard is skipped. xviewer_window_display_image() is then called and unconditionally adds another pair of signal connections (lines 934–940) onto the same XviewerImage object (the list store reuses the same object when the file-monitor sees the same path change, rather than creating a new one). So every time the watched file changes on disk (exactly what your test does by overwriting /tmp/leaktest.png in place), one more redundant handler pair accumulates permanently on that image object — they're never cleaned up. Each additional handler is small, but they never get removed and each one re-fires (and does its own work, e.g. thumbnail regeneration) on every subsequent change, which matches a slow, continuing — but not exploding — growth in RSS.
While auditing this path I also noticed a smaller, unrelated genuine bug in src/xviewer-list-store.c's file_monitor_changed_cb: in the G_FILE_MONITOR_EVENT_DELETED case it fetches image via gtk_tree_model_get (transfer-full) but never g_object_unrefs it, unlike the CHANGES_DONE_HINT case just above. It wouldn't fire in your test (no delete event, just in-place overwrite), but it's worth fixing too.
Summary:
Why VmRSS grew from 104060 kB → 126272 kB across the extra 270 cycles: there's a third, separate leak — this one specific to your test's pattern of repeatedly overwriting the same open file (rather than navigating between files), which is exactly the scenario in the original 2012 upstream bug report (a motion-style webcam tool rewriting one file in place).
Root cause: xviewer_window_reload_image() (xviewer-window.c:6587, triggered by the GFileMonitor "file changed" → auto-reload path) sets priv->image = NULL before the reload's background job finishes. The job-completion callback xviewer_job_load_cb() only disconnects the old "thumbnail_changed"/"file-changed" signal handlers when priv->image != NULL — but it's already NULL by then, so that cleanup is skipped. xviewer_window_display_image() then unconditionally adds another pair of signal handlers onto the same XviewerImage object (the list store reuses one object per path rather than creating a new one on a plain modify). So every overwrite of your watched file leaks one more permanent signal-handler pair, and each surviving stale handler keeps re-firing (redoing its own work) on every subsequent change — a slow, genuinely-still-growing leak rather than a one-time cost, matching your 300-cycle data.
Fixed by making the signal connection idempotent (disconnect-then-connect, mirroring the pattern already used correctly elsewhere in the same file). I also fixed a smaller, unrelated ref leak I spotted in the same code path (xviewer-list-store.c, G_FILE_MONITOR_EVENT_DELETED case never unreffed the fetched image — doesn't fire for in-place overwrites, so not your test's cause, but a real bug).
Found and fixed two more real leaks, and rigorously bisected a much smaller residual one. Summary:
Two more genuine bugs fixed (same pattern as before — a missing g_object_unref on an early-return path):
Where the ~85 KB/cycle residual leak stands: I built the project directly from source, ran the repro under Valgrind and then with direct in-process mallinfo2() checkpoints (immune to allocator-fragmentation noise, unlike RSS) at ~10 points through the reload pipeline. That bisection:
This residual leak only shows up in the narrow "same file repeatedly overwritten in place while open" scenario (your test's exact pattern, and the original 2012 webcam-monitoring bug report) — it's ~100-200x smaller per event than what's already fixed, which covered ordinary image navigation/browsing.