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
1=== modified file 'libcore/gof-directory-async.vala'
2--- libcore/gof-directory-async.vala 2015-03-06 03:36:39 +0000
3+++ libcore/gof-directory-async.vala 2015-03-26 02:53:22 +0000
4@@ -48,7 +48,7 @@
5 private Cancellable cancellable;
6 private FileMonitor? monitor = null;
7
8- private List<GOF.File>? sorted_dirs = null;
9+ private List<unowned GOF.File>? sorted_dirs = null;
10
11 public signal void file_loaded (GOF.File file);
12 public signal void file_added (GOF.File file);
13@@ -466,6 +466,25 @@
14 file_hash.insert (gof.location, gof);
15 }
16
17+ public GOF.File file_cache_find_or_insert (GLib.File file,
18+ bool update_hash = false)
19+ {
20+ GOF.File? result = file_hash.lookup (file);
21+
22+ if (result == null) {
23+ result = GOF.File.cache_lookup (file);
24+
25+ if (result == null) {
26+ result = new GOF.File (file, location);
27+ file_hash.insert (file, result);
28+ }
29+ else if (update_hash)
30+ file_hash.insert (file, result);
31+ }
32+
33+ return (!) result;
34+ }
35+
36 /* TODO move this to GOF.File */
37 private delegate void func_query_info (GOF.File gof);
38
39@@ -509,7 +528,9 @@
40
41 if (!gof.is_hidden && gof.is_folder ()) {
42 /* add to sorted_dirs */
43- sorted_dirs.insert_sorted (gof, GOF.File.compare_by_display_name);
44+ if (sorted_dirs.find (gof) == null)
45+ sorted_dirs.insert_sorted (gof,
46+ GOF.File.compare_by_display_name);
47 }
48
49 if (track_longest_name && gof.basename.length > longest_file_name.length) {
50@@ -523,7 +544,6 @@
51 }
52
53 private void notify_file_added (GOF.File gof) {
54- file_hash.insert (gof.location, gof);
55 query_info_async.begin (gof, add_and_refresh);
56 }
57
58@@ -533,6 +553,13 @@
59
60 if (!gof.is_hidden && gof.is_folder ()) {
61 /* remove from sorted_dirs */
62+
63+ /* Addendum note: GLib.List.remove() does not unreference objects.
64+ See: https://bugzilla.gnome.org/show_bug.cgi?id=624249
65+ https://bugzilla.gnome.org/show_bug.cgi?id=532268
66+
67+ The declaration of sorted_dirs has been changed to contain
68+ weak pointers as a temporary solution. */
69 sorted_dirs.remove (gof);
70 }
71
72@@ -619,21 +646,23 @@
73
74 public static void notify_files_changed (List<GLib.File> files) {
75 foreach (var loc in files) {
76- GOF.File gof = GOF.File.get (loc);
77- Async? dir = cache_lookup (gof.directory);
78+ Async? dir = cache_lookup_parent (loc);
79
80- if (dir != null)
81+ if (dir != null) {
82+ GOF.File gof = dir.file_cache_find_or_insert (loc);
83 dir.notify_file_changed (gof);
84+ }
85 }
86 }
87
88 public static void notify_files_added (List<GLib.File> files) {
89 foreach (var loc in files) {
90- GOF.File gof = GOF.File.get (loc);
91- Async? dir = cache_lookup (gof.directory);
92+ Async? dir = cache_lookup_parent (loc);
93
94- if (dir != null)
95+ if (dir != null) {
96+ GOF.File gof = dir.file_cache_find_or_insert (loc, true);
97 dir.notify_file_added (gof);
98+ }
99 }
100 }
101
102@@ -642,10 +671,10 @@
103 bool found;
104
105 foreach (var loc in files) {
106- GOF.File gof = GOF.File.get (loc);
107- Async? dir = cache_lookup (gof.directory);
108+ Async? dir = cache_lookup_parent (loc);
109
110 if (dir != null) {
111+ GOF.File gof = dir.file_cache_find_or_insert (loc);
112 dir.notify_file_removed (gof);
113 found = false;
114
115@@ -725,6 +754,11 @@
116 return cached_dir;
117 }
118
119+ public static Async? cache_lookup_parent (GLib.File file) {
120+ GLib.File? parent = file.get_parent ();
121+ return parent != null ? cache_lookup (parent) : cache_lookup (file);
122+ }
123+
124 public bool remove_dir_from_cache () {
125 /* we got to increment the dir ref to remove the toggle_ref */
126 this.ref ();
127
128=== modified file 'libcore/gof-file.c'
129--- libcore/gof-file.c 2015-02-27 16:40:41 +0000
130+++ libcore/gof-file.c 2015-03-26 02:53:22 +0000
131@@ -114,6 +114,7 @@
132 file->basename = g_file_get_basename (file->location);
133 //file->parent_dir = g_file_enumerator_get_container (enumerator);
134
135+ //g_debug ("%s: create %p", __func__, file);
136 return (file);
137 }
138
139@@ -946,6 +947,8 @@
140 }
141
142 static void gof_file_finalize (GObject* obj) {
143+ //g_debug ("%s: delete %p", __func__, obj);
144+
145 GOFFile *file;
146
147 file = GOF_FILE (obj);
148@@ -977,6 +980,13 @@
149 /* TODO remove the target_gof */
150 _g_free0 (file->thumbnail_path);
151
152+#ifndef NDEBUG
153+ g_warn_if_fail (file->target_gof == NULL);
154+#endif
155+
156+ _g_free0 (file->owner);
157+ _g_free0 (file->group);
158+
159 G_OBJECT_CLASS (gof_file_parent_class)->finalize (obj);
160 }
161
162
163=== modified file 'libcore/marlin-file-operations.c'
164--- libcore/marlin-file-operations.c 2015-03-01 10:56:59 +0000
165+++ libcore/marlin-file-operations.c 2015-03-26 02:53:22 +0000
166@@ -4002,19 +4002,23 @@
167 static gboolean
168 test_dir_is_parent (GFile *child, GFile *root)
169 {
170- GFile *f;
171-
172- f = g_file_dup (child);
173- while (f) {
174+ GFile *f = child;
175+ GFile *prev = NULL;
176+
177+ if (g_file_equal (child, root))
178+ return TRUE;
179+
180+ while ((f = g_file_get_parent (f))) {
181+ if (prev) g_object_unref (prev);
182+
183 if (g_file_equal (f, root)) {
184 g_object_unref (f);
185 return TRUE;
186 }
187- f = g_file_get_parent (f);
188- }
189- if (f) {
190- g_object_unref (f);
191- }
192+ prev = f;
193+ }
194+ if (prev) g_object_unref (prev);
195+
196 return FALSE;
197 }
198
199
200=== modified file 'libcore/pantheon-files-core-C.vapi'
201--- libcore/pantheon-files-core-C.vapi 2015-03-01 10:56:59 +0000
202+++ libcore/pantheon-files-core-C.vapi 2015-03-26 02:53:22 +0000
203@@ -31,7 +31,7 @@
204 public void add_file(GOF.File file, GOF.Directory.Async dir);
205 public void remove_file (GOF.File file, GOF.Directory.Async dir);
206 public void file_changed (GOF.File file, GOF.Directory.Async dir);
207- public unowned GOF.File file_for_path(Gtk.TreePath path);
208+ public GOF.File? file_for_path (Gtk.TreePath path);
209 public static GLib.Type get_type ();
210 public bool get_first_iter_for_file (GOF.File file, out Gtk.TreeIter iter);
211 public bool get_tree_iter_from_file (GOF.File file, GOF.Directory.Async directory, out Gtk.TreeIter iter);
212
213=== modified file 'src/MimeActions.vala'
214--- src/MimeActions.vala 2015-02-06 22:57:55 +0000
215+++ src/MimeActions.vala 2015-03-26 02:53:22 +0000
216@@ -39,7 +39,7 @@
217 public static AppInfo? get_default_application_for_files (GLib.List<unowned GOF.File> files) {
218 assert (files != null);
219 /* Need to make a new list to avoid corrupting the selection */
220- unowned GLib.List<GOF.File> sorted_files = null;
221+ GLib.List<unowned GOF.File> sorted_files = null;
222 files.@foreach ((file) => {
223 sorted_files.prepend (file);
224 });
225@@ -120,7 +120,7 @@
226 public static List<AppInfo>? get_applications_for_files (GLib.List<unowned GOF.File> files) {
227 assert (files != null);
228 /* Need to make a new list to avoid corrupting the selection */
229- unowned GLib.List<GOF.File> sorted_files = null;
230+ GLib.List<unowned GOF.File> sorted_files = null;
231 files.@foreach ((file) => {
232 sorted_files.prepend (file);
233 });
234
235=== modified file 'src/TextRenderer.vala'
236--- src/TextRenderer.vala 2014-11-04 14:51:14 +0000
237+++ src/TextRenderer.vala 2015-03-26 02:53:22 +0000
238@@ -25,7 +25,7 @@
239 public Marlin.ZoomLevel zoom_level {get; set;}
240 public bool follow_state {get; set;}
241 public new string background { set; private get;}
242- public GOF.File file {set; private get;}
243+ public GOF.File? file {set; private get;}
244 public int text_width;
245 public int text_height;
246
247@@ -91,6 +91,11 @@
248 layout);
249
250 style_context.restore ();
251+
252+ /* The render call should always be preceded by a set_property call
253+ from GTK. It should be safe to unreference or free the allocated
254+ memory here. */
255+ file = null;
256 }
257
258 public void set_up_layout (string? text, Gdk.Rectangle cell_area) {
259@@ -211,6 +216,7 @@
260
261 private void invalidate () {
262 set_widget (null);
263+ file = null;
264 }
265
266 private void on_entry_editing_done () {
267@@ -224,6 +230,7 @@
268 string path = entry.get_data ("marlin-text-renderer-path");
269 edited (path, text);
270 }
271+ file = null;
272 }
273
274 private bool on_entry_focus_out_event (Gdk.Event event) {
275
276=== modified file 'src/View/AbstractDirectoryView.vala'
277--- src/View/AbstractDirectoryView.vala 2015-03-25 08:59:04 +0000
278+++ src/View/AbstractDirectoryView.vala 2015-03-26 02:53:22 +0000
279@@ -192,7 +192,27 @@
280
281 private GLib.List<GLib.AppInfo> open_with_apps;
282 protected GLib.List<GOF.Directory.Async>? loaded_subdirectories = null;
283- protected GLib.List<unowned GOF.File> selected_files = null ;
284+
285+ /* TODO: Remove the "unowned" portion of the declaration for
286+ selected_files and on code that repeats its type.
287+
288+ Selected files are originally obtained with
289+ gtk_tree_model_get(): this function increases the reference
290+ count of the file object.
291+
292+ In order to prevent the obvious memory leak when inserting
293+ these owned objects into the unowned element container, the
294+ objects are unreferenced upon being inserted.
295+
296+ This results in a container filled with weak references to
297+ objects with reference counts of 1.
298+
299+ A scenario may occur that the cell holding the original file
300+ object is destroyed, and the container is left with pointers
301+ pointing to freed memory. Reference counting the container
302+ elements would prevent this possibility. */
303+ protected GLib.List<unowned GOF.File> selected_files = null;
304+
305 private GLib.List<unowned GOF.File>? templates = null;
306
307 private GLib.AppInfo default_app;
308
309=== modified file 'src/View/AbstractTreeView.vala'
310--- src/View/AbstractTreeView.vala 2015-02-12 18:28:34 +0000
311+++ src/View/AbstractTreeView.vala 2015-03-26 02:53:22 +0000
312@@ -163,7 +163,7 @@
313 selected_files = null;
314
315 tree.get_selection ().selected_foreach ((model, path, iter) => {
316- unowned GOF.File? file; /* can be null if click on blank row in list view */
317+ GOF.File? file; /* can be null if click on blank row in list view */
318 model.@get (iter, FM.ListModel.ColumnID.FILE_COLUMN, out file, -1);
319 if (file != null)
320 selected_files.prepend (file);
321
322=== modified file 'src/View/IconView.vala'
323--- src/View/IconView.vala 2015-02-12 18:28:34 +0000
324+++ src/View/IconView.vala 2015-03-26 02:53:22 +0000
325@@ -172,7 +172,7 @@
326 selected_files = null;
327
328 tree.selected_foreach ((tree, path) => {
329- unowned GOF.File file;
330+ GOF.File? file;
331 file = model.file_for_path (path);
332
333 if (file != null)
334
335=== modified file 'src/View/ListView.vala'
336--- src/View/ListView.vala 2015-01-24 12:31:20 +0000
337+++ src/View/ListView.vala 2015-03-26 02:53:22 +0000
338@@ -89,7 +89,7 @@
339 }
340
341 private void set_path_expanded (Gtk.TreePath path, bool expanded) {
342- unowned GOF.File? file = model.file_for_path (path);
343+ GOF.File? file = model.file_for_path (path);
344
345 if (file != null)
346 file.set_expanded (expanded);
347
348=== modified file 'src/View/PropertiesWindow.vala'
349--- src/View/PropertiesWindow.vala 2015-03-06 03:36:39 +0000
350+++ src/View/PropertiesWindow.vala 2015-03-26 02:53:22 +0000
351@@ -40,7 +40,7 @@
352
353 private uint count;
354 private GLib.List<GOF.File> files;
355- private GOF.File goffile;
356+ private unowned GOF.File goffile;
357 private FM.AbstractDirectoryView view;
358
359 private Gee.Set<string>? mimes;
360@@ -105,7 +105,18 @@
361 }
362
363 view = _view;
364- files = _files.copy ();
365+
366+ /* The properties window may outlive the passed-in file object
367+ lifetimes. The objects must be referenced as a precaution.
368+
369+ GLib.List.copy() would not guarantee valid references: because it
370+ does a shallow copy (copying the pointer values only) the objects'
371+ memory may be freed even while this code is using it. */
372+ foreach (unowned GOF.File file in _files)
373+ /* prepend(G) is declared "owned G", so ref() will be called once
374+ on the unowned foreach value. */
375+ files.prepend (file);
376+
377 count = files.length();
378
379 if (count < 1 ) {
380
381=== modified file 'src/marlin-icon-renderer.c'
382--- src/marlin-icon-renderer.c 2015-01-18 20:46:03 +0000
383+++ src/marlin-icon-renderer.c 2015-03-26 02:53:22 +0000
384@@ -661,6 +661,12 @@
385 g_object_unref (pix);
386 }
387 }
388+
389+ /* The render call should always be preceded by a set_property call from
390+ GTK. It should be safe to unreference or free the allocate memory
391+ here. */
392+ _g_object_unref0 (priv->file);
393+ _g_object_unref0 (priv->drop_file);
394 }
395
396 /*

Subscribers

People subscribed via source and target branches

to all changes: