Merge lp:~cmm2-deactivatedaccount/pantheon-files/memory-leak-fixes into lp:~elementary-apps/pantheon-files/trunk

Proposed by cmm2
Status: Merged
Approved by: Jeremy Wootten
Approved revision: 1790
Merged at revision: 1789
Proposed branch: lp:~cmm2-deactivatedaccount/pantheon-files/memory-leak-fixes
Merge into: lp:~elementary-apps/pantheon-files/trunk
Diff against target: 396 lines (+122/-30)
12 files modified
libcore/gof-directory-async.vala (+45/-11)
libcore/gof-file.c (+10/-0)
libcore/marlin-file-operations.c (+13/-9)
libcore/pantheon-files-core-C.vapi (+1/-1)
src/MimeActions.vala (+2/-2)
src/TextRenderer.vala (+8/-1)
src/View/AbstractDirectoryView.vala (+21/-1)
src/View/AbstractTreeView.vala (+1/-1)
src/View/IconView.vala (+1/-1)
src/View/ListView.vala (+1/-1)
src/View/PropertiesWindow.vala (+13/-2)
src/marlin-icon-renderer.c (+6/-0)
To merge this branch: bzr merge lp:~cmm2-deactivatedaccount/pantheon-files/memory-leak-fixes
Reviewer Review Type Date Requested Status
Jeremy Wootten Approve
Review via email: mp+254188@code.launchpad.net

Commit message

Improve memory utilization and reduce memory leaks.

Description of the change

This branch attempts to fix most of the file related memory leaks in Pantheon Files.

The user interface patch is still somewhat experimental and may have side effects on unexpected areas of the program. It breaks some previous inadvertent assumptions that file objects are forever resident in memory. However, most of those issues should be fixed.

Memory utilization comparisons are listed below.

Preparation:
    mkdir -p test/old test/new
    pushd test/old
    for i in $(seq 250000); do echo '' > $i; done
    popd
    ./pantheon-files

Procedure:
    1) Measure memory usage.
    2) Copy test/old to test/new/old.
    3) Measure memory usage.
    4) Create 10 files in test/new/ (this purges the undo manager history).
    5) Delete test/new/*.
    6) Repeat steps #1 through #5.

Results:
    http://pastebin.com/5RNvn4Jn was used for memory usage reporting.

    Comparison:
    http://pastebin.com/raw.php?i=cyBfMLtc

To post a comment you must log in.
Revision history for this message
cmm2 (cmm2-deactivatedaccount) wrote :

My apologies for the delay in submitting these changes. I forgot about this for a while, until Jeremy removed me from the bug listing. I should've mentioned something earlier.

Shouldn't be anything too unusual with the patches. Of course, let me know if something goes wrong.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

Thanks for continuing to work on this cmm2! The branch substantially reduces memory usage on copy and deleting 250000 files as described. On my system, one cycle of the test described increased memory usage by over 500MB whereas in the branch it increased by about 75MB (Using System Monitor). For comparison, Thunar's memory increased by about 100MB. PCManFM however, used no extra memory (!) provided the files were not displayed. Presumably some information is still cached in Files and Thunar so this is not necessarily a leak?

No problems with the code.

You inserted a TODO - were you going to do this in this branch or later?

I'd like to test the branch for regressions but subject to that I will approve. Thanks again.

Revision history for this message
cmm2 (cmm2-deactivatedaccount) wrote :

I need to look into it, but I suspect the initial 70-100M allocation is not actually used, but is simply freed memory that the allocator has not released to the OS. This explains why subsequent operations do not expand the total memory amount.

The blame for this additional memory requirement seems to fall on marlin_file_changes_consume and marlin_undo_manager (or whatever they are named -- sorry, I'm writing this from a tablet). Both can be optimized: file changes should go out immediately, and not queue up infinitely; and the undo tracker should remember user input, not a recursive list of all source files.

Some file managers don't track destination files for undoing if they belong to a created subdirectory, thus making their aftermath memory requirement effectively nil.

> You inserted a TODO - were you going to do this in this branch or later?

Probably sometime later. If the selected_files problem was causing known crashes I'd fix it, but right now it seems like a pedantic hypothetical.

Revision history for this message
Jeremy Wootten (jeremywootten) wrote :

I have not found any obvious regressions so I will approve this for merging now. Don't forget to claim your well-deserved bounty!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'libcore/gof-directory-async.vala'
--- libcore/gof-directory-async.vala 2015-03-06 03:36:39 +0000
+++ libcore/gof-directory-async.vala 2015-03-26 02:53:22 +0000
@@ -48,7 +48,7 @@
48 private Cancellable cancellable;48 private Cancellable cancellable;
49 private FileMonitor? monitor = null;49 private FileMonitor? monitor = null;
5050
51 private List<GOF.File>? sorted_dirs = null;51 private List<unowned GOF.File>? sorted_dirs = null;
5252
53 public signal void file_loaded (GOF.File file);53 public signal void file_loaded (GOF.File file);
54 public signal void file_added (GOF.File file);54 public signal void file_added (GOF.File file);
@@ -466,6 +466,25 @@
466 file_hash.insert (gof.location, gof);466 file_hash.insert (gof.location, gof);
467 }467 }
468468
469 public GOF.File file_cache_find_or_insert (GLib.File file,
470 bool update_hash = false)
471 {
472 GOF.File? result = file_hash.lookup (file);
473
474 if (result == null) {
475 result = GOF.File.cache_lookup (file);
476
477 if (result == null) {
478 result = new GOF.File (file, location);
479 file_hash.insert (file, result);
480 }
481 else if (update_hash)
482 file_hash.insert (file, result);
483 }
484
485 return (!) result;
486 }
487
469 /* TODO move this to GOF.File */488 /* TODO move this to GOF.File */
470 private delegate void func_query_info (GOF.File gof);489 private delegate void func_query_info (GOF.File gof);
471490
@@ -509,7 +528,9 @@
509528
510 if (!gof.is_hidden && gof.is_folder ()) {529 if (!gof.is_hidden && gof.is_folder ()) {
511 /* add to sorted_dirs */530 /* add to sorted_dirs */
512 sorted_dirs.insert_sorted (gof, GOF.File.compare_by_display_name);531 if (sorted_dirs.find (gof) == null)
532 sorted_dirs.insert_sorted (gof,
533 GOF.File.compare_by_display_name);
513 }534 }
514535
515 if (track_longest_name && gof.basename.length > longest_file_name.length) {536 if (track_longest_name && gof.basename.length > longest_file_name.length) {
@@ -523,7 +544,6 @@
523 }544 }
524545
525 private void notify_file_added (GOF.File gof) {546 private void notify_file_added (GOF.File gof) {
526 file_hash.insert (gof.location, gof);
527 query_info_async.begin (gof, add_and_refresh);547 query_info_async.begin (gof, add_and_refresh);
528 }548 }
529549
@@ -533,6 +553,13 @@
533553
534 if (!gof.is_hidden && gof.is_folder ()) {554 if (!gof.is_hidden && gof.is_folder ()) {
535 /* remove from sorted_dirs */555 /* remove from sorted_dirs */
556
557 /* Addendum note: GLib.List.remove() does not unreference objects.
558 See: https://bugzilla.gnome.org/show_bug.cgi?id=624249
559 https://bugzilla.gnome.org/show_bug.cgi?id=532268
560
561 The declaration of sorted_dirs has been changed to contain
562 weak pointers as a temporary solution. */
536 sorted_dirs.remove (gof);563 sorted_dirs.remove (gof);
537 }564 }
538565
@@ -619,21 +646,23 @@
619646
620 public static void notify_files_changed (List<GLib.File> files) {647 public static void notify_files_changed (List<GLib.File> files) {
621 foreach (var loc in files) {648 foreach (var loc in files) {
622 GOF.File gof = GOF.File.get (loc);649 Async? dir = cache_lookup_parent (loc);
623 Async? dir = cache_lookup (gof.directory);
624650
625 if (dir != null)651 if (dir != null) {
652 GOF.File gof = dir.file_cache_find_or_insert (loc);
626 dir.notify_file_changed (gof);653 dir.notify_file_changed (gof);
654 }
627 }655 }
628 }656 }
629657
630 public static void notify_files_added (List<GLib.File> files) {658 public static void notify_files_added (List<GLib.File> files) {
631 foreach (var loc in files) {659 foreach (var loc in files) {
632 GOF.File gof = GOF.File.get (loc);660 Async? dir = cache_lookup_parent (loc);
633 Async? dir = cache_lookup (gof.directory);
634661
635 if (dir != null)662 if (dir != null) {
663 GOF.File gof = dir.file_cache_find_or_insert (loc, true);
636 dir.notify_file_added (gof);664 dir.notify_file_added (gof);
665 }
637 }666 }
638 }667 }
639668
@@ -642,10 +671,10 @@
642 bool found;671 bool found;
643672
644 foreach (var loc in files) {673 foreach (var loc in files) {
645 GOF.File gof = GOF.File.get (loc);674 Async? dir = cache_lookup_parent (loc);
646 Async? dir = cache_lookup (gof.directory);
647675
648 if (dir != null) {676 if (dir != null) {
677 GOF.File gof = dir.file_cache_find_or_insert (loc);
649 dir.notify_file_removed (gof);678 dir.notify_file_removed (gof);
650 found = false;679 found = false;
651680
@@ -725,6 +754,11 @@
725 return cached_dir;754 return cached_dir;
726 }755 }
727756
757 public static Async? cache_lookup_parent (GLib.File file) {
758 GLib.File? parent = file.get_parent ();
759 return parent != null ? cache_lookup (parent) : cache_lookup (file);
760 }
761
728 public bool remove_dir_from_cache () {762 public bool remove_dir_from_cache () {
729 /* we got to increment the dir ref to remove the toggle_ref */763 /* we got to increment the dir ref to remove the toggle_ref */
730 this.ref ();764 this.ref ();
731765
=== modified file 'libcore/gof-file.c'
--- libcore/gof-file.c 2015-02-27 16:40:41 +0000
+++ libcore/gof-file.c 2015-03-26 02:53:22 +0000
@@ -114,6 +114,7 @@
114 file->basename = g_file_get_basename (file->location);114 file->basename = g_file_get_basename (file->location);
115 //file->parent_dir = g_file_enumerator_get_container (enumerator);115 //file->parent_dir = g_file_enumerator_get_container (enumerator);
116116
117 //g_debug ("%s: create %p", __func__, file);
117 return (file);118 return (file);
118}119}
119120
@@ -946,6 +947,8 @@
946}947}
947948
948static void gof_file_finalize (GObject* obj) {949static void gof_file_finalize (GObject* obj) {
950 //g_debug ("%s: delete %p", __func__, obj);
951
949 GOFFile *file;952 GOFFile *file;
950953
951 file = GOF_FILE (obj);954 file = GOF_FILE (obj);
@@ -977,6 +980,13 @@
977 /* TODO remove the target_gof */980 /* TODO remove the target_gof */
978 _g_free0 (file->thumbnail_path);981 _g_free0 (file->thumbnail_path);
979982
983#ifndef NDEBUG
984 g_warn_if_fail (file->target_gof == NULL);
985#endif
986
987 _g_free0 (file->owner);
988 _g_free0 (file->group);
989
980 G_OBJECT_CLASS (gof_file_parent_class)->finalize (obj);990 G_OBJECT_CLASS (gof_file_parent_class)->finalize (obj);
981}991}
982992
983993
=== modified file 'libcore/marlin-file-operations.c'
--- libcore/marlin-file-operations.c 2015-03-01 10:56:59 +0000
+++ libcore/marlin-file-operations.c 2015-03-26 02:53:22 +0000
@@ -4002,19 +4002,23 @@
4002static gboolean4002static gboolean
4003test_dir_is_parent (GFile *child, GFile *root)4003test_dir_is_parent (GFile *child, GFile *root)
4004{4004{
4005 GFile *f;4005 GFile *f = child;
40064006 GFile *prev = NULL;
4007 f = g_file_dup (child);4007
4008 while (f) {4008 if (g_file_equal (child, root))
4009 return TRUE;
4010
4011 while ((f = g_file_get_parent (f))) {
4012 if (prev) g_object_unref (prev);
4013
4009 if (g_file_equal (f, root)) {4014 if (g_file_equal (f, root)) {
4010 g_object_unref (f);4015 g_object_unref (f);
4011 return TRUE;4016 return TRUE;
4012 }4017 }
4013 f = g_file_get_parent (f);4018 prev = f;
4014 }4019 }
4015 if (f) {4020 if (prev) g_object_unref (prev);
4016 g_object_unref (f);4021
4017 }
4018 return FALSE;4022 return FALSE;
4019}4023}
40204024
40214025
=== modified file 'libcore/pantheon-files-core-C.vapi'
--- libcore/pantheon-files-core-C.vapi 2015-03-01 10:56:59 +0000
+++ libcore/pantheon-files-core-C.vapi 2015-03-26 02:53:22 +0000
@@ -31,7 +31,7 @@
31 public void add_file(GOF.File file, GOF.Directory.Async dir);31 public void add_file(GOF.File file, GOF.Directory.Async dir);
32 public void remove_file (GOF.File file, GOF.Directory.Async dir);32 public void remove_file (GOF.File file, GOF.Directory.Async dir);
33 public void file_changed (GOF.File file, GOF.Directory.Async dir);33 public void file_changed (GOF.File file, GOF.Directory.Async dir);
34 public unowned GOF.File file_for_path(Gtk.TreePath path);34 public GOF.File? file_for_path (Gtk.TreePath path);
35 public static GLib.Type get_type ();35 public static GLib.Type get_type ();
36 public bool get_first_iter_for_file (GOF.File file, out Gtk.TreeIter iter);36 public bool get_first_iter_for_file (GOF.File file, out Gtk.TreeIter iter);
37 public bool get_tree_iter_from_file (GOF.File file, GOF.Directory.Async directory, out Gtk.TreeIter iter);37 public bool get_tree_iter_from_file (GOF.File file, GOF.Directory.Async directory, out Gtk.TreeIter iter);
3838
=== modified file 'src/MimeActions.vala'
--- src/MimeActions.vala 2015-02-06 22:57:55 +0000
+++ src/MimeActions.vala 2015-03-26 02:53:22 +0000
@@ -39,7 +39,7 @@
39 public static AppInfo? get_default_application_for_files (GLib.List<unowned GOF.File> files) {39 public static AppInfo? get_default_application_for_files (GLib.List<unowned GOF.File> files) {
40 assert (files != null);40 assert (files != null);
41 /* Need to make a new list to avoid corrupting the selection */41 /* Need to make a new list to avoid corrupting the selection */
42 unowned GLib.List<GOF.File> sorted_files = null;42 GLib.List<unowned GOF.File> sorted_files = null;
43 files.@foreach ((file) => {43 files.@foreach ((file) => {
44 sorted_files.prepend (file);44 sorted_files.prepend (file);
45 });45 });
@@ -120,7 +120,7 @@
120 public static List<AppInfo>? get_applications_for_files (GLib.List<unowned GOF.File> files) {120 public static List<AppInfo>? get_applications_for_files (GLib.List<unowned GOF.File> files) {
121 assert (files != null);121 assert (files != null);
122 /* Need to make a new list to avoid corrupting the selection */122 /* Need to make a new list to avoid corrupting the selection */
123 unowned GLib.List<GOF.File> sorted_files = null;123 GLib.List<unowned GOF.File> sorted_files = null;
124 files.@foreach ((file) => {124 files.@foreach ((file) => {
125 sorted_files.prepend (file);125 sorted_files.prepend (file);
126 });126 });
127127
=== modified file 'src/TextRenderer.vala'
--- src/TextRenderer.vala 2014-11-04 14:51:14 +0000
+++ src/TextRenderer.vala 2015-03-26 02:53:22 +0000
@@ -25,7 +25,7 @@
25 public Marlin.ZoomLevel zoom_level {get; set;}25 public Marlin.ZoomLevel zoom_level {get; set;}
26 public bool follow_state {get; set;}26 public bool follow_state {get; set;}
27 public new string background { set; private get;}27 public new string background { set; private get;}
28 public GOF.File file {set; private get;}28 public GOF.File? file {set; private get;}
29 public int text_width;29 public int text_width;
30 public int text_height;30 public int text_height;
3131
@@ -91,6 +91,11 @@
91 layout);91 layout);
9292
93 style_context.restore ();93 style_context.restore ();
94
95 /* The render call should always be preceded by a set_property call
96 from GTK. It should be safe to unreference or free the allocated
97 memory here. */
98 file = null;
94 }99 }
95100
96 public void set_up_layout (string? text, Gdk.Rectangle cell_area) {101 public void set_up_layout (string? text, Gdk.Rectangle cell_area) {
@@ -211,6 +216,7 @@
211216
212 private void invalidate () {217 private void invalidate () {
213 set_widget (null);218 set_widget (null);
219 file = null;
214 }220 }
215221
216 private void on_entry_editing_done () {222 private void on_entry_editing_done () {
@@ -224,6 +230,7 @@
224 string path = entry.get_data ("marlin-text-renderer-path");230 string path = entry.get_data ("marlin-text-renderer-path");
225 edited (path, text);231 edited (path, text);
226 }232 }
233 file = null;
227 }234 }
228235
229 private bool on_entry_focus_out_event (Gdk.Event event) {236 private bool on_entry_focus_out_event (Gdk.Event event) {
230237
=== modified file 'src/View/AbstractDirectoryView.vala'
--- src/View/AbstractDirectoryView.vala 2015-03-25 08:59:04 +0000
+++ src/View/AbstractDirectoryView.vala 2015-03-26 02:53:22 +0000
@@ -192,7 +192,27 @@
192192
193 private GLib.List<GLib.AppInfo> open_with_apps;193 private GLib.List<GLib.AppInfo> open_with_apps;
194 protected GLib.List<GOF.Directory.Async>? loaded_subdirectories = null;194 protected GLib.List<GOF.Directory.Async>? loaded_subdirectories = null;
195 protected GLib.List<unowned GOF.File> selected_files = null ;195
196 /* TODO: Remove the "unowned" portion of the declaration for
197 selected_files and on code that repeats its type.
198
199 Selected files are originally obtained with
200 gtk_tree_model_get(): this function increases the reference
201 count of the file object.
202
203 In order to prevent the obvious memory leak when inserting
204 these owned objects into the unowned element container, the
205 objects are unreferenced upon being inserted.
206
207 This results in a container filled with weak references to
208 objects with reference counts of 1.
209
210 A scenario may occur that the cell holding the original file
211 object is destroyed, and the container is left with pointers
212 pointing to freed memory. Reference counting the container
213 elements would prevent this possibility. */
214 protected GLib.List<unowned GOF.File> selected_files = null;
215
196 private GLib.List<unowned GOF.File>? templates = null;216 private GLib.List<unowned GOF.File>? templates = null;
197217
198 private GLib.AppInfo default_app;218 private GLib.AppInfo default_app;
199219
=== modified file 'src/View/AbstractTreeView.vala'
--- src/View/AbstractTreeView.vala 2015-02-12 18:28:34 +0000
+++ src/View/AbstractTreeView.vala 2015-03-26 02:53:22 +0000
@@ -163,7 +163,7 @@
163 selected_files = null;163 selected_files = null;
164164
165 tree.get_selection ().selected_foreach ((model, path, iter) => {165 tree.get_selection ().selected_foreach ((model, path, iter) => {
166 unowned GOF.File? file; /* can be null if click on blank row in list view */166 GOF.File? file; /* can be null if click on blank row in list view */
167 model.@get (iter, FM.ListModel.ColumnID.FILE_COLUMN, out file, -1);167 model.@get (iter, FM.ListModel.ColumnID.FILE_COLUMN, out file, -1);
168 if (file != null)168 if (file != null)
169 selected_files.prepend (file);169 selected_files.prepend (file);
170170
=== modified file 'src/View/IconView.vala'
--- src/View/IconView.vala 2015-02-12 18:28:34 +0000
+++ src/View/IconView.vala 2015-03-26 02:53:22 +0000
@@ -172,7 +172,7 @@
172 selected_files = null;172 selected_files = null;
173173
174 tree.selected_foreach ((tree, path) => {174 tree.selected_foreach ((tree, path) => {
175 unowned GOF.File file;175 GOF.File? file;
176 file = model.file_for_path (path);176 file = model.file_for_path (path);
177177
178 if (file != null)178 if (file != null)
179179
=== modified file 'src/View/ListView.vala'
--- src/View/ListView.vala 2015-01-24 12:31:20 +0000
+++ src/View/ListView.vala 2015-03-26 02:53:22 +0000
@@ -89,7 +89,7 @@
89 }89 }
9090
91 private void set_path_expanded (Gtk.TreePath path, bool expanded) {91 private void set_path_expanded (Gtk.TreePath path, bool expanded) {
92 unowned GOF.File? file = model.file_for_path (path);92 GOF.File? file = model.file_for_path (path);
9393
94 if (file != null)94 if (file != null)
95 file.set_expanded (expanded);95 file.set_expanded (expanded);
9696
=== modified file 'src/View/PropertiesWindow.vala'
--- src/View/PropertiesWindow.vala 2015-03-06 03:36:39 +0000
+++ src/View/PropertiesWindow.vala 2015-03-26 02:53:22 +0000
@@ -40,7 +40,7 @@
4040
41 private uint count;41 private uint count;
42 private GLib.List<GOF.File> files;42 private GLib.List<GOF.File> files;
43 private GOF.File goffile;43 private unowned GOF.File goffile;
44 private FM.AbstractDirectoryView view;44 private FM.AbstractDirectoryView view;
4545
46 private Gee.Set<string>? mimes;46 private Gee.Set<string>? mimes;
@@ -105,7 +105,18 @@
105 }105 }
106106
107 view = _view;107 view = _view;
108 files = _files.copy ();108
109 /* The properties window may outlive the passed-in file object
110 lifetimes. The objects must be referenced as a precaution.
111
112 GLib.List.copy() would not guarantee valid references: because it
113 does a shallow copy (copying the pointer values only) the objects'
114 memory may be freed even while this code is using it. */
115 foreach (unowned GOF.File file in _files)
116 /* prepend(G) is declared "owned G", so ref() will be called once
117 on the unowned foreach value. */
118 files.prepend (file);
119
109 count = files.length();120 count = files.length();
110121
111 if (count < 1 ) {122 if (count < 1 ) {
112123
=== modified file 'src/marlin-icon-renderer.c'
--- src/marlin-icon-renderer.c 2015-01-18 20:46:03 +0000
+++ src/marlin-icon-renderer.c 2015-03-26 02:53:22 +0000
@@ -661,6 +661,12 @@
661 g_object_unref (pix);661 g_object_unref (pix);
662 }662 }
663 }663 }
664
665 /* The render call should always be preceded by a set_property call from
666 GTK. It should be safe to unreference or free the allocate memory
667 here. */
668 _g_object_unref0 (priv->file);
669 _g_object_unref0 (priv->drop_file);
664}670}
665671
666/*672/*

Subscribers

People subscribed via source and target branches

to all changes: