public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH] override deselection in CheckboxModel to improve performance
Date: Mon, 22 Mar 2021 10:00:45 +0100	[thread overview]
Message-ID: <20210322090046.26278-2-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20210322090046.26278-1-t.lamprecht@proxmox.com>

After the UI hang for tens of seconds for a few thousand elements got
deselected I investigated with Firefox developer tools performance
analysis, where the waterfall view showed that most of the time was
spent in array splicing.

Previously, all removed elements got removed on by one from the
`selected` Ext.util.Collection - which is basically an helper class
around arrays and objects, most of it may have become obsolete with
modern browsers. The single remove resulted into further splicing of
the array, and all in all it resulted in a dramatically increased
complexity, ~ O(n^3).

The "remove one by one" logic comes highly probably from the fact
that users can register a `beforedeselection` listener which can
block a removal of a specific record. But, that's not used by us and
not really something one would often need in practice, but still its
a documented feature of ExtJS grids we want to keep; so go for an
alternative.
So, override `doDeselect` and change the old removal logic to one
that first record those entries which got blocked from removal and
remove them in one go from the "to-be-removed" collection.

Before/After this patch on my FF 86.0.1 with my i9-9900K and
deselecting ~10k records went from ~40s to about 33 ms total, so for
that case we went 1000x faster.

The remaining time is now mostly spend in the event fire/handling
logic, but even with 50k records we spent <<500ms in total, so not
too bad and thus kept as is for now.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---

Note, I had some more optimizations for the deselectAll case specifically, but
I omitted that as that one provided small extra benefits, so less
changes/overrides where favored.

 src/Toolkit.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 65 insertions(+), 1 deletion(-)

diff --git a/src/Toolkit.js b/src/Toolkit.js
index 6dcccd3..79bc5ae 100644
--- a/src/Toolkit.js
+++ b/src/Toolkit.js
@@ -492,11 +492,75 @@ Ext.define('Proxmox.validIdReOverride', {
     validIdRe: /^[a-z_][a-z0-9\-_@]*$/i,
 });
 
-// use whole checkbox cell to multiselect, not only the checkbox
 Ext.define('Proxmox.selection.CheckboxModel', {
     override: 'Ext.selection.CheckboxModel',
 
+    // [P] use whole checkbox cell to multiselect, not only the checkbox
     checkSelector: '.x-grid-cell-row-checker',
+
+    // [ P: optimized to remove all records at once as single remove is O(n^3) slow ]
+    // records can be an index, a record or an array of records
+    doDeselect: function(records, suppressEvent) {
+        var me = this,
+            selected = me.selected,
+            i = 0,
+            len, record,
+            commit;
+        if (me.locked || !me.store) {
+            return false;
+        }
+        if (typeof records === "number") {
+            // No matching record, jump out
+            record = me.store.getAt(records);
+            if (!record) {
+                return false;
+            }
+            records = [
+                record,
+            ];
+        } else if (!Ext.isArray(records)) {
+            records = [
+                records,
+            ];
+        }
+	// [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
+	// returning false, thus the original implementation removed only here in the commit fn,
+	// which has an abysmal performance O(n^3). As blocking removal is not the norm, go do the
+	// reverse, record blocked records and remove them from the to-be-removed array before
+	// applying it. A FF86 i9-9900K on 10k records goes from >40s to ~33ms for >90% deselection
+	let committed = false;
+	commit = function() {
+	    committed = true;
+	    if (record === me.selectionStart) {
+		me.selectionStart = null;
+	    }
+	};
+	let removalBlocked = [];
+        len = records.length;
+        me.suspendChanges();
+        for (; i < len; i++) {
+            record = records[i];
+            if (me.isSelected(record)) {
+		committed = false;
+                me.onSelectChange(record, false, suppressEvent, commit);
+		if (!committed) {
+		    removalBlocked.push(record);
+		}
+                if (me.destroyed) {
+                    return false;
+                }
+            }
+        }
+	if (removalBlocked.length > 0) {
+	    records.remove(removalBlocked);
+	}
+	selected.remove(records); // [P] FAST(er)
+	me.lastSelected = selected.last();
+        me.resumeChanges();
+        // fire selchange if there was a change and there is no suppressEvent flag
+	me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
+	return records.length > 0;
+    },
 });
 
 // force alert boxes to be rendered with an Error Icon
-- 
2.20.1





  reply	other threads:[~2021-03-22  9:00 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-03-22  9:00 [pmg-devel] [PATCH] fix #3164: api: quarantine: allow to return spam from all users Thomas Lamprecht
2021-03-22  9:00 ` Thomas Lamprecht [this message]
2021-03-22  9:00 ` [pmg-devel] [PATCH] fix #3164: allow one to display all quarantined spam mails Thomas Lamprecht
2021-03-22 15:46   ` Dominik Csapak
2021-03-22 14:06 ` [pmg-devel] [PATCH] fix #3164: api: quarantine: allow to return spam from all users Stoiko Ivanov
2021-03-22 14:12   ` Fuhrmann, Markus
2021-03-22 16:38   ` Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210322090046.26278-2-t.lamprecht@proxmox.com \
    --to=t.lamprecht@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal