Merge lp:~jeremywootten/pantheon-files/fix-hang-on-large-copy into lp:~elementary-apps/pantheon-files/trunk

Proposed by Jeremy Wootten
Status: Superseded
Proposed branch: lp:~jeremywootten/pantheon-files/fix-hang-on-large-copy
Merge into: lp:~elementary-apps/pantheon-files/trunk
Diff against target: 435 lines (+116/-87)
4 files modified
src/View/AbstractDirectoryView.vala (+68/-55)
src/View/AbstractTreeView.vala (+26/-18)
src/View/IconView.vala (+21/-13)
src/View/Slot.vala (+1/-1)
To merge this branch: bzr merge lp:~jeremywootten/pantheon-files/fix-hang-on-large-copy
Reviewer Review Type Date Requested Status
Zisu Andrei (community) Needs Information
Review via email: mp+310070@code.launchpad.net

This proposal has been superseded by a proposal from 2017-01-11.

Commit message

Fix hang on selecting many files after paste or view change.

Description of the change

This fixes the exponentially increasing time take to select the debuting files in the view after pasting. With a large number of files (e.g. 1000+) the interface appears to hang when using List View.

TO TEST:

A.
IN ICON VIEW
1) Create two test folders and open each in a tab
2) In one folder create a large number (~1000) of small files (~1K)
3) Select all the files (Ctrl-A)
4) Copy the files to the clipboard
5) Switch to empty folder and paste the files.

In trunk the interface appears to hang for a long time after the pasted files appear (A core is maxed out).

B. IN LIST VIEW
1) Create many files as described under A.
2) Select them all.
3) Switch to icon view.

In trunk the interface appears to hang for a long time while the selected files are re-selected (one at a time) in the new view (A core is maxed out).

KILL THE PANTHEON-FILES PROCESS BEFORE RUNNING THIS BRANCH.
In this branch the pasted files or reselected files are all selected soon after they appear. The interface does not hang.

The branch additionally fixes pasting into a List or Column View - all the pasted files are selected (trunk only selects the one file, unlike Icon View)

The same code is now used whether pasting is done via keyboard or drag and drop (simplification).

To post a comment you must log in.
Revision history for this message
Zisu Andrei (matzipan) wrote :

I agree that this branch seems to alleviate the issue, but it does not make it disappear entirely.

review: Approve (functionality)
Revision history for this message
Zisu Andrei (matzipan) wrote :

 Code also LGTM.

Not merging this yet. Waiting on merging okay from Cody.

review: Approve
Revision history for this message
Zisu Andrei (matzipan) wrote :

Scratch that. I'm seeing a very nasty bug. I thought it was in trunk too but it isn't.

My folder structure is like this:

folder 1
2000 text files

If I am viewing this folder and I press ctrl+a, I then deselect "folder 1", I then click "folder 1" to enter it. This results in opening all 2000 files in Scratch.

review: Needs Fixing
2369. By Jeremy Wootten

Merge trunk to r2377

2370. By Jeremy Wootten

Update menu actions immediately on first selection change; select pasted files and dropped files the same way; remove unnecessary flag

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

Thanks for spotting that bad regression. The latest version fixes that and also simplifies the code a little. Both pasted and dropped files are selected but it was being done in different ways for some reason. Now pasted files are selected in the same way as dropped files.

Revision history for this message
Zisu Andrei (matzipan) wrote :

I can't seem to be noticing any significant improvement (maybe I was mistaken earlier). I made sure to kill all the processes, double checked the versions I have installed, etc.

I tried both over an SSD and an HDD.

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

The problem does not occur if you use List or Column view (at least not with the operations mentioned in TESTING). Only if both views are Icon View. This is because only one of the pasted files is selected in List View, but they are all selected in Icon View (which is a bug in itself - the behaviour should be consistent).

You should be able to reproduce the bug using only one tab using the second set of instructions now in the description.

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

Testing instructions corrected and extended.

2371. By Jeremy Wootten

Merge trunk to r2441

2372. By Jeremy Wootten

Merge trunk to r2470

2373. By Jeremy Wootten

Merge trunk to r2572 and resolve conflict

2374. By Jeremy Wootten

Merge trunk to r2573 and resolve conflict in AbstractDirectoryView

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/View/AbstractDirectoryView.vala'
2--- src/View/AbstractDirectoryView.vala 2016-12-31 19:42:09 +0000
3+++ src/View/AbstractDirectoryView.vala 2017-01-11 17:17:31 +0000
4@@ -215,9 +215,7 @@
5 private bool can_trash_or_delete = true;
6
7 /* Rapid keyboard paste support */
8- protected bool pasting_files = false;
9 protected bool select_added_files = false;
10- private HashTable? pasted_files = null;
11
12 public bool renaming {get; protected set; default = false;}
13
14@@ -230,12 +228,20 @@
15 action_set_enabled (common_actions, "paste_into", false);
16 action_set_enabled (window.win_actions, "select_all", false);
17
18+ /* Fix problems when navigating away from directory with large number
19+ * of selected files (e.g. OverlayBar critical errors)
20+ */
21+ disconnect_tree_signals ();
22+
23 size_allocate.disconnect (on_size_allocate);
24 clipboard.changed.disconnect (on_clipboard_changed);
25 view.enter_notify_event.disconnect (on_enter_notify_event);
26 view.key_press_event.disconnect (on_view_key_press_event);
27 } else if (!value && _is_frozen) {
28- update_menu_actions ();
29+ /* Ensure selected files and menu actions are up to date */
30+ connect_tree_signals ();
31+ on_view_selection_changed ();
32+
33 size_allocate.connect (on_size_allocate);
34 clipboard.changed.connect (on_clipboard_changed);
35 view.enter_notify_event.connect (on_enter_notify_event);
36@@ -417,37 +423,45 @@
37 });
38 }
39 }
40- public void select_glib_files (GLib.List<GLib.File> location_list, GLib.File? focus_location) {
41- unselect_all ();
42+
43+ public void select_glib_files_when_thawed (GLib.List<GLib.File> location_list, GLib.File? focus_location) {
44 GLib.List<GOF.File>? file_list = null;
45
46 location_list.@foreach ((loc) => {
47 file_list.prepend (GOF.File.@get (loc));
48 });
49
50+ GLib.File? focus = focus_location != null ? focus_location.dup () : null;
51+
52 /* Because the Icon View disconnects the model while loading, we need to wait until
53 * the tree is thawed and the model reconnected before selecting the files */
54 Idle.add_full (GLib.Priority.LOW, () => {
55 if (!tree_frozen) {
56- file_list.@foreach ((file) => {
57- Gtk.TreeIter iter;
58- if (model.get_first_iter_for_file (file, out iter)) {
59- Gtk.TreePath path = model.get_path (iter);
60- if (path != null) {
61- select_path (path);
62- if (focus_location != null && focus_location.equal (file.location)) {
63- /* set cursor and scroll to focus location*/
64- set_cursor (path, false, false, true);
65- }
66- }
67- }
68- });
69+ select_file_paths (file_list, focus);
70 return false;
71- } else
72+ } else {
73 return true;
74+ }
75 });
76 }
77
78+ private void select_file_paths (GLib.List<GOF.File> files, GLib.File? focus) {
79+
80+ Gtk.TreeIter iter;
81+ disconnect_tree_signals (); /* Avoid unnecessary signal processing */
82+ unselect_all ();
83+
84+ foreach (GOF.File f in files) {
85+ if (model.get_first_iter_for_file (f, out iter)) {
86+ var path = model.get_path (iter);
87+ select_path (path, focus != null && focus.equal (f.location)); /* Cursor follows if matches focus location*/
88+ }
89+ }
90+
91+ connect_tree_signals ();
92+ on_view_selection_changed (); /* Update selected files and menu actions */
93+ }
94+
95 public unowned GLib.List<GLib.AppInfo> get_open_with_apps () {
96 return open_with_apps;
97 }
98@@ -577,7 +591,7 @@
99 return; /* file not in model */
100
101 var path = model.get_path (iter);
102- select_path (path);
103+ select_path (path); /* Cursor does not follow */
104 }
105
106 /** Directory signal handlers. */
107@@ -1190,6 +1204,7 @@
108 clipboard.copy_files (get_selected_files_for_transfer (get_files_for_action ()));
109 }
110
111+
112 public static void after_pasting_files (GLib.HashTable? uris, void* pointer) {
113 if (pointer == null)
114 return;
115@@ -1200,29 +1215,12 @@
116 return;
117 }
118
119- view.pasting_files = false;
120- if (uris == null || uris.size () == 0)
121+ if (uris == null || uris.size () == 0) {
122 return;
123-
124- view.pasted_files = uris;
125-
126- Idle.add (() => {
127- /* Select the most recently pasted files */
128- GLib.List<GLib.File> pasted_files_list = null;
129- view.pasted_files.foreach ((k, v) => {
130- if (k is GLib.File)
131- pasted_files_list.prepend (k as File);
132- });
133-
134- view.select_glib_files (pasted_files_list, pasted_files_list.first ().data);
135- return false;
136- });
137+ }
138 }
139
140 private void on_common_action_paste_into (GLib.SimpleAction action, GLib.Variant? param) {
141- if (pasting_files)
142- return;
143-
144 var file = get_files_for_action ().nth_data (0);
145
146 if (file != null && clipboard.can_paste) {
147@@ -1236,10 +1234,10 @@
148
149 if (target.has_uri_scheme ("trash")) {
150 /* Pasting files into trash is equivalent to trash or delete action */
151- pasting_files = false;
152+ select_added_files = false;
153 call_back = (GLib.Callback)after_trash_or_delete;
154 } else {
155- pasting_files = true;
156+ select_added_files = true;
157 /* callback takes care of selecting pasted files */
158 call_back = (GLib.Callback)after_pasting_files;
159 }
160@@ -2570,15 +2568,25 @@
161 activate_selected_items (Marlin.OpenFlag.DEFAULT);
162 }
163
164+ uint update_selected_timeout_id = 0;
165 protected virtual void on_view_selection_changed () {
166+ /* updating selecting file list is expensive for large selections so throttle */
167+ if (update_selected_timeout_id == 0) {
168+ after_selected_files_changed (); /* Make sure first update happens immediately */
169+ update_selected_timeout_id = Timeout.add_full (GLib.Priority.LOW, 100, () => {
170+ update_selected_timeout_id = 0;
171+ after_selected_files_changed ();
172+ return false;
173+ });
174+ }
175+ }
176+
177+ private void after_selected_files_changed () {
178 update_selected_files ();
179 update_menu_actions ();
180- if (is_frozen)
181- return;
182-
183 selection_changed (get_selected_files ());
184 }
185-
186+
187 /** Keyboard event handling **/
188
189 /** Returns true if the code parameter matches the keycode of the keyval parameter for
190@@ -2801,7 +2809,7 @@
191 linear_select_path (path);
192 } else if (no_mods) {
193 unselect_path (old_path);
194- select_path (path);
195+ select_path (path, true); /* Cursor follows */
196 }
197 }
198 return true;
199@@ -3161,12 +3169,13 @@
200 }
201
202 if (!path_selected && click_zone != ClickZone.HELPER) {
203- if (no_mods)
204+ if (no_mods) {
205 unselect_all ();
206-
207+ }
208 /* If modifier pressed then default handler determines selection */
209- if (no_mods && !on_blank)
210- select_path (path);
211+ if (no_mods && !on_blank) {
212+ select_path (path, true); /* Cursor follows */
213+ }
214 }
215
216 bool result = true;
217@@ -3222,7 +3231,7 @@
218 if (path_selected) {
219 unselect_path (path);
220 } else {
221- select_path (path);
222+ select_path (path, true); /* Cursor follows */
223 }
224 }
225
226@@ -3255,9 +3264,10 @@
227
228 case Gdk.BUTTON_SECONDARY:
229 if (click_zone == ClickZone.NAME ||
230- (!single_click_rename && click_zone == ClickZone.BLANK_PATH))
231+ (!single_click_rename && click_zone == ClickZone.BLANK_PATH)) {
232
233- select_path (path);
234+ select_path (path); /* Curso does not follow */
235+ }
236
237 unblock_drag_and_drop ();
238 result = handle_secondary_button_click (event);
239@@ -3462,6 +3472,7 @@
240 cancel_drag_timer ();
241 cancel_timeout (ref drag_scroll_timer_id);
242 cancel_timeout (ref add_remove_file_timeout_id);
243+ cancel_timeout (ref update_selected_timeout_id);
244 /* List View will take care of unloading subdirectories */
245 }
246
247@@ -3518,7 +3529,7 @@
248 public abstract Gtk.TreePath? get_path_at_cursor ();
249 public abstract void select_all ();
250 public abstract void unselect_all ();
251- public abstract void select_path (Gtk.TreePath? path);
252+ public abstract void select_path (Gtk.TreePath? path, bool cursor_follows = false);
253 public abstract void unselect_path (Gtk.TreePath? path);
254 public abstract bool path_is_selected (Gtk.TreePath? path);
255 public abstract bool get_visible_range (out Gtk.TreePath? start_path, out Gtk.TreePath? end_path);
256@@ -3544,7 +3555,9 @@
257 protected abstract void thaw_tree ();
258 protected new abstract void freeze_child_notify ();
259 protected new abstract void thaw_child_notify ();
260-
261+ protected abstract void connect_tree_signals ();
262+ protected abstract void disconnect_tree_signals ();
263+
264 /** Unimplemented methods
265 * fm_directory_view_parent_set () - purpose unclear
266 */
267
268=== modified file 'src/View/AbstractTreeView.vala'
269--- src/View/AbstractTreeView.vala 2016-12-31 19:42:09 +0000
270+++ src/View/AbstractTreeView.vala 2017-01-11 17:17:31 +0000
271@@ -64,6 +64,10 @@
272
273 protected void set_up_view () {
274 connect_tree_signals ();
275+ tree.realize.connect ((w) => {
276+ tree.grab_focus ();
277+ tree.columns_autosize ();
278+ });
279 }
280
281 protected override void set_up_name_renderer () {
282@@ -76,13 +80,11 @@
283 name_renderer.yalign = 0.5f;
284 }
285
286- protected void connect_tree_signals () {
287+ protected override void connect_tree_signals () {
288 tree.get_selection ().changed.connect (on_view_selection_changed);
289-
290- tree.realize.connect ((w) => {
291- tree.grab_focus ();
292- tree.columns_autosize ();
293- });
294+ }
295+ protected override void disconnect_tree_signals () {
296+ tree.get_selection ().changed.disconnect (on_view_selection_changed);
297 }
298
299 protected override Gtk.Widget? create_view () {
300@@ -131,21 +133,27 @@
301 tree.get_selection ().unselect_all ();
302 }
303
304- public override void select_path (Gtk.TreePath? path) {
305+ /* Avoid using this function with "cursor_follows = true" to select large numbers of files one by one
306+ * It would take an exponentially long time. Use "select_files" function in parent class.
307+ */
308+ public override void select_path (Gtk.TreePath? path, bool cursor_follows = false) {
309 if (path != null) {
310 var selection = tree.get_selection ();
311- /* Unlike for IconView, set_cursor unselects previously selected paths (Gtk bug?),
312- * so we have to remember them and reselect afterwards */
313- GLib.List<Gtk.TreePath> selected_paths = null;
314- selection.selected_foreach ((m, p, i) => {
315- selected_paths.prepend (p);
316- });
317- /* Ensure cursor follows last selection */
318- tree.set_cursor (path, null, false); /* This selects path but unselects rest! */
319 selection.select_path (path);
320- selected_paths.@foreach ((p) => {
321- selection.select_path (p);
322- });
323+ if (cursor_follows) {
324+ /* Unlike for IconView, set_cursor unselects previously selected paths (Gtk bug?),
325+ * so we have to remember them and reselect afterwards */
326+ GLib.List<Gtk.TreePath> selected_paths = null;
327+ selection.selected_foreach ((m, p, i) => {
328+ selected_paths.prepend (p);
329+ });
330+ /* Ensure cursor follows last selection */
331+ tree.set_cursor (path, null, false); /* This selects path but unselects rest! */
332+
333+ selected_paths.@foreach ((p) => {
334+ selection.select_path (p);
335+ });
336+ }
337 }
338 }
339 public override void unselect_path (Gtk.TreePath? path) {
340
341=== modified file 'src/View/IconView.vala'
342--- src/View/IconView.vala 2016-12-31 19:42:09 +0000
343+++ src/View/IconView.vala 2017-01-11 17:17:31 +0000
344@@ -47,6 +47,9 @@
345 (tree as Gtk.CellLayout).add_attribute (icon_renderer, "file", FM.ListModel.ColumnID.FILE_COLUMN);
346
347 connect_tree_signals ();
348+ tree.realize.connect ((w) => {
349+ tree.grab_focus ();
350+ });
351 }
352
353 protected override void set_up_name_renderer () {
354@@ -61,12 +64,11 @@
355 }
356
357
358- private void connect_tree_signals () {
359+ protected override void connect_tree_signals () {
360 tree.selection_changed.connect (on_view_selection_changed);
361-
362- tree.realize.connect ((w) => {
363- tree.grab_focus ();
364- });
365+ }
366+ protected override void disconnect_tree_signals () {
367+ tree.selection_changed.disconnect (on_view_selection_changed);
368 }
369
370 protected override Gtk.Widget? create_view () {
371@@ -132,14 +134,19 @@
372 }
373
374 public override void unselect_all () {
375- tree.unselect_all ();
376+ tree.unselect_all ();
377 }
378
379- public override void select_path (Gtk.TreePath? path) {
380+ /* Avoid using this function with "cursor_follows = true" to select large numbers of files one by one
381+ * It would take an exponentially long time. Use "select_files" function in parent class.
382+ */
383+ public override void select_path (Gtk.TreePath? path, bool cursor_follows = false) {
384 if (path != null) {
385- /* Ensure cursor follows last selection */
386- tree.set_cursor (path, null, false); /* This selects path but does not unselect the rest (unlike TreeView) */
387- tree.select_path (path);
388+ tree.select_path (path); /* This selects path but does not unselect the rest (unlike TreeView) */
389+
390+ if (cursor_follows) {
391+ tree.set_cursor (path, null, false);
392+ }
393 }
394 }
395
396@@ -360,22 +367,23 @@
397 end_path = direction_change ? last_selected : first_selected;
398 }
399
400+ /* Cursor follows when selecting path */
401 if (before_first) {
402 do {
403 p2 = p.copy ();
404- select_path (p);
405+ select_path (p, true);
406 p.next ();
407 } while (p.compare (p2) != 0 && p.compare (end_path) <= 0);
408 } else if (after_last) {
409 do {
410- select_path (p);
411+ select_path (p, true);
412 p2 = p.copy ();
413 p.prev ();
414 } while (p.compare (p2) != 0 && p.compare (end_path) >= 0);
415 } else {/* between first and last */
416 do {
417 p2 = p.copy ();
418- select_path (p);
419+ select_path (p, true);
420 p.prev ();
421 } while (p.compare (p2) != 0 && p.compare (first_selected) >= 0);
422
423
424=== modified file 'src/View/Slot.vala'
425--- src/View/Slot.vala 2016-09-21 18:27:17 +0000
426+++ src/View/Slot.vala 2017-01-11 17:17:31 +0000
427@@ -351,7 +351,7 @@
428
429 public override void select_glib_files (GLib.List<GLib.File> files, GLib.File? focus_location) {
430 if (dir_view != null)
431- dir_view.select_glib_files (files, focus_location);
432+ dir_view.select_glib_files_when_thawed (files, focus_location);
433 }
434
435 public void select_gof_file (GOF.File gof) {

Subscribers

People subscribed via source and target branches

to all changes: