From 9fde5634f15a3f7aee6098e76f7bd228633c6bfe Mon Sep 17 00:00:00 2001
From: brvphoenix <30111323+brvphoenix@users.noreply.github.com>
Date: Sat, 25 Nov 2023 13:50:45 +0800
Subject: [PATCH] Fix JS memory leak
The memory leak can be reproduced easily by opening two web pages of qbittorrent so that the WebUI pages are updated with full_update = true. If you have a large number of torrents, such as 100 torrents, you can observe a rapid increase in memory usage.
This is caused by the incorrect usage of dispose and empty methods in the js codes and none of them garbage collect the elements. If event listeners are added to the DOM elements, those DOM elements will not be garbage collected at all event if they are not referenced or out of the scope, which will cause memory leaks. If some elements are expected to be removed, the correct way is to use destroy method instead.
https://github.com/mootools/mootools-core/blob/master/Docs/Element/Element.md#element-method-dispose-elementdispose
https://github.com/mootools/mootools-core/blob/master/Docs/Element/Element.md#element-method-empty-elementempty
https://github.com/mootools/mootools-core/blob/master/Docs/Element/Element.md#element-method-destroy-elementdestroy
Closes #19034.
PR #19969.
---
src/webui/www/private/scripts/client.js | 8 +++-----
src/webui/www/private/scripts/contextmenu.js | 2 +-
src/webui/www/private/scripts/dynamicTable.js | 10 ++++------
src/webui/www/private/scripts/prop-webseeds.js | 3 +--
src/webui/www/private/views/log.html | 2 ++
src/webui/www/private/views/preferences.html | 4 ++--
src/webui/www/private/views/rss.html | 4 ++--
7 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js
index 1a36c26b1..e2e8f279c 100644
--- a/src/webui/www/private/scripts/client.js
+++ b/src/webui/www/private/scripts/client.js
@@ -455,7 +455,7 @@ window.addEvent('load', function() {
const categoryList = $('categoryFilterList');
if (!categoryList)
return;
- categoryList.empty();
+ categoryList.getChildren().each(c => c.destroy());
const create_link = function(hash, text, count) {
let display_name = text;
@@ -540,8 +540,7 @@ window.addEvent('load', function() {
if (tagFilterList === null)
return;
- while (tagFilterList.firstChild !== null)
- tagFilterList.removeChild(tagFilterList.firstChild);
+ tagFilterList.getChildren().each(c => c.destroy());
const createLink = function(hash, text, count) {
const html = ''
@@ -594,8 +593,7 @@ window.addEvent('load', function() {
if (trackerFilterList === null)
return;
- while (trackerFilterList.firstChild !== null)
- trackerFilterList.removeChild(trackerFilterList.firstChild);
+ trackerFilterList.getChildren().each(c => c.destroy());
const createLink = function(hash, text, count) {
const html = ''
diff --git a/src/webui/www/private/scripts/contextmenu.js b/src/webui/www/private/scripts/contextmenu.js
index 167570bc3..b9487abb8 100644
--- a/src/webui/www/private/scripts/contextmenu.js
+++ b/src/webui/www/private/scripts/contextmenu.js
@@ -444,7 +444,7 @@ window.qBittorrent.ContextMenu = (function() {
updateCategoriesSubMenu: function(category_list) {
const categoryList = $('contextCategoryList');
- categoryList.empty();
+ categoryList.getChildren().each(c => c.destroy());
categoryList.appendChild(new Element('li', {
html: '
QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]'
}));
diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js
index be66507bc..e30a40175 100644
--- a/src/webui/www/private/scripts/dynamicTable.js
+++ b/src/webui/www/private/scripts/dynamicTable.js
@@ -817,8 +817,7 @@ window.qBittorrent.DynamicTable = (function() {
let rowPos = rows.length;
while ((rowPos < trs.length) && (trs.length > 0)) {
- trs[trs.length - 1].dispose();
- trs.pop();
+ trs.pop().destroy();
}
},
@@ -840,7 +839,7 @@ window.qBittorrent.DynamicTable = (function() {
this.selectedRows.erase(rowId);
const tr = this.getTrByRowId(rowId);
if (tr !== null) {
- tr.dispose();
+ tr.destroy();
this.rows.erase(rowId);
return true;
}
@@ -852,8 +851,7 @@ window.qBittorrent.DynamicTable = (function() {
this.rows.empty();
const trs = this.tableBody.getElements('tr');
while (trs.length > 0) {
- trs[trs.length - 1].dispose();
- trs.pop();
+ trs.pop().destroy();
}
},
@@ -1562,7 +1560,7 @@ window.qBittorrent.DynamicTable = (function() {
if (!country_code) {
if (td.getChildren('img').length > 0)
- td.getChildren('img')[0].dispose();
+ td.getChildren('img')[0].destroy();
return;
}
diff --git a/src/webui/www/private/scripts/prop-webseeds.js b/src/webui/www/private/scripts/prop-webseeds.js
index a56685933..ce42d0343 100644
--- a/src/webui/www/private/scripts/prop-webseeds.js
+++ b/src/webui/www/private/scripts/prop-webseeds.js
@@ -50,8 +50,7 @@ window.qBittorrent.PropWebseeds = (function() {
removeRow: function(url) {
if (this.rows.has(url)) {
- const tr = this.rows.get(url);
- tr.dispose();
+ this.rows.get(url).destroy();
this.rows.erase(url);
return true;
}
diff --git a/src/webui/www/private/views/log.html b/src/webui/www/private/views/log.html
index cdd78d99c..9e1866349 100644
--- a/src/webui/www/private/views/log.html
+++ b/src/webui/www/private/views/log.html
@@ -424,4 +424,6 @@
return exports();
})();
+
+ Object.freeze(window.qBittorrent.Log);
diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html
index e33cdecd4..48a959d31 100644
--- a/src/webui/www/private/views/preferences.html
+++ b/src/webui/www/private/views/preferences.html
@@ -1884,7 +1884,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
// Advanced Tab
const updateNetworkInterfaces = function(default_iface, default_iface_name) {
const url = 'api/v2/app/networkInterfaceList';
- $('networkInterface').empty();
+ $('networkInterface').getChildren().each(c => c.destroy());
new Request.JSON({
url: url,
method: 'get',
@@ -1911,7 +1911,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
const updateInterfaceAddresses = function(iface, default_addr) {
const url = 'api/v2/app/networkInterfaceAddressList';
- $('optionalIPAddressToBind').empty();
+ $('optionalIPAddressToBind').getChildren().each(c => c.destroy());
new Request.JSON({
url: url,
method: 'get',
diff --git a/src/webui/www/private/views/rss.html b/src/webui/www/private/views/rss.html
index 4599440b0..520ccf582 100644
--- a/src/webui/www/private/views/rss.html
+++ b/src/webui/www/private/views/rss.html
@@ -402,13 +402,13 @@
});
});
- $('rssDetailsView').empty();
+ $('rssDetailsView').getChildren().each(c => c.destroy());
rssArticleTable.updateTable(false);
};
const showDetails = (feedUid, articleID) => {
markArticleAsRead(pathByFeedId.get(feedUid), articleID);
- $('rssDetailsView').empty();
+ $('rssDetailsView').getChildren().each(c => c.destroy());
let article = feedData[feedUid].filter((article) => article.id === articleID)[0];
if (article) {
$('rssDetailsView').append((() => {