diff --git a/src/main.c b/src/main.c index 2745d2c..6a04864 100644 --- a/src/main.c +++ b/src/main.c @@ -41,6 +41,7 @@ #include #include +#include #include static XviewerStartupFlags flags; @@ -96,6 +97,17 @@ main (int argc, char **argv) GError *error = NULL; GOptionContext *ctx; + /* Pin the mmap threshold so that large image buffers (which are + * typically several MB and can vary a lot in size while browsing) + * always go through mmap/munmap, guaranteeing their memory is + * released back to the OS immediately on free. Without this, + * glibc's malloc can grow its heap arena to fit a peak allocation + * and never shrink it back, causing RSS to climb and plateau at + * high-water marks that never come back down while browsing many + * large images in succession. */ + mallopt (M_MMAP_THRESHOLD, 128 * 1024); + mallopt (M_MMAP_MAX, 65536); + bindtextdomain (GETTEXT_PACKAGE, XVIEWER_LOCALE_DIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); diff --git a/src/xviewer-image.c b/src/xviewer-image.c index 5af0ef7..2cbfeed 100644 --- a/src/xviewer-image.c +++ b/src/xviewer-image.c @@ -1302,6 +1302,7 @@ xviewer_image_real_load (XviewerImage *img, if (gdk_pixbuf_animation_is_static_image (priv->anim)) { priv->image = gdk_pixbuf_animation_get_static_image (priv->anim); + g_object_unref (priv->anim); priv->anim = NULL; } else { priv->anim_iter = gdk_pixbuf_animation_get_iter (priv->anim,NULL); diff --git a/src/xviewer-jobs.c b/src/xviewer-jobs.c index 1e80d30..f87d555 100644 --- a/src/xviewer-jobs.c +++ b/src/xviewer-jobs.c @@ -279,12 +279,16 @@ xviewer_job_cancel (XviewerJob *job) g_object_ref (job); /* check if job was cancelled previously */ - if (job->cancelled) + if (job->cancelled) { + g_object_unref (job); return; + } /* check if job finished previously */ - if (job->finished) + if (job->finished) { + g_object_unref (job); return; + } /* show info for debugging */ xviewer_debug_message (DEBUG_JOBS, @@ -566,8 +570,21 @@ xviewer_job_load_run (XviewerJob *job) &job->error); /* check if the current job was previously cancelled */ - if (xviewer_job_is_cancelled (job)) + if (xviewer_job_is_cancelled (job)) { + /* xviewer_image_load() above may have already finished + * decoding the full image into memory before this + * cancellation was noticed (e.g. small/cached files loading + * faster than the user can browse past them). Since this + * job's "finished" signal is never emitted, the image will + * never be displayed or data-reffed, so its decoded pixel + * data would otherwise leak forever. Force-release it. */ + if (job_load->data & XVIEWER_IMAGE_DATA_IMAGE) { + xviewer_image_data_ref (job_load->image); + xviewer_image_data_unref (job_load->image); + } + g_object_unref (job); return; + } /* --- enter critical section --- */ g_mutex_lock (job->mutex); @@ -857,8 +874,10 @@ xviewer_job_save_run (XviewerJob *job) } /* check if the current job was previously cancelled */ - if (xviewer_job_is_cancelled (job)) + if (xviewer_job_is_cancelled (job)) { + g_object_unref (job); return; + } save_job = XVIEWER_JOB_SAVE (job); @@ -1233,6 +1252,7 @@ xviewer_job_thumbnail_run (XviewerJob *job) if (!job_thumbnail->thumbnail) { job->finished = TRUE; + g_object_unref (job); return; } diff --git a/src/xviewer-list-store.c b/src/xviewer-list-store.c index 04603ec..8bf1768 100644 --- a/src/xviewer-list-store.c +++ b/src/xviewer-list-store.c @@ -417,6 +417,7 @@ file_monitor_changed_cb (GFileMonitor *monitor, -1); xviewer_list_store_remove (store, &iter); + g_object_unref (image); } break; case G_FILE_MONITOR_EVENT_CREATED: diff --git a/src/xviewer-thumbnail.c b/src/xviewer-thumbnail.c index 9c59aa6..4378840 100644 --- a/src/xviewer-thumbnail.c +++ b/src/xviewer-thumbnail.c @@ -494,6 +494,7 @@ xviewer_thumbnail_load (XviewerImage *image, GError **error) (data->failed_thumb_exists && gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail (factory, data->uri_str, data->mtime))) { xviewer_debug_message (DEBUG_THUMBNAIL, "%s: bad permissions or valid failed thumbnail present",data->uri_str); set_thumb_error (error, XVIEWER_THUMB_ERROR_GENERIC, "Thumbnail creation failed"); + xviewer_thumb_data_free (data); return NULL; } diff --git a/src/xviewer-window.c b/src/xviewer-window.c index cd8207c..463a83a 100644 --- a/src/xviewer-window.c +++ b/src/xviewer-window.c @@ -931,6 +931,13 @@ xviewer_window_display_image (XviewerWindow *window, XviewerImage *image) priv = window->priv; if (image != NULL) { + g_signal_handlers_disconnect_by_func (image, + image_thumb_changed_cb, + window); + g_signal_handlers_disconnect_by_func (image, + image_file_changed_cb, + window); + g_signal_connect (image, "thumbnail_changed", G_CALLBACK (image_thumb_changed_cb), @@ -1021,6 +1028,15 @@ xviewer_window_update_openwith_menu (XviewerWindow *window, XviewerImage *image) if (priv->actions_open_with != NULL) { gtk_ui_manager_remove_action_group (priv->ui_mgr, priv->actions_open_with); + /* gtk_ui_manager_insert_action_group() below takes its own + * reference; removing it only drops that one. The reference + * from gtk_action_group_new() (which this struct field has + * been implicitly holding) must be released here too, + * otherwise every action group -- and every GAppInfo/ + * GKeyFile-backed action it holds for each installed + * "Open With" candidate app -- leaks on every single image + * display. */ + g_object_unref (priv->actions_open_with); priv->actions_open_with = NULL; } @@ -1150,8 +1166,25 @@ xviewer_window_clear_load_job (XviewerWindow *window) XviewerWindowPrivate *priv = window->priv; if (priv->load_job != NULL) { - if (!priv->load_job->finished) + if (!priv->load_job->finished) { xviewer_job_cancel (priv->load_job); + } else { + /* The job already finished decoding the full image + * before we got a chance to act on it: the "finished" + * signal disconnected below never gets to run, so + * xviewer_job_load_cb()/xviewer_window_display_image() + * never see this image and never data-ref it. Without + * this, the fully decoded pixel data would stay + * resident forever. Force-release it. This is safe + * even if this image happens to be on-screen already, + * since data-ref/unref only frees when the count + * would drop to zero. */ + XviewerImage *load_image = XVIEWER_JOB_LOAD (priv->load_job)->image; + if (xviewer_image_has_data (load_image, XVIEWER_IMAGE_DATA_IMAGE)) { + xviewer_image_data_ref (load_image); + xviewer_image_data_unref (load_image); + } + } g_signal_handlers_disconnect_by_func (priv->load_job, xviewer_job_progress_cb,