Repository: TagStudioDev/TagStudio
Issue category: Performance Issue

Issue:
[Bug]: Adding (or searching for) tags gets increasingly slower in the same session (running process)
### Checklist

- [x] I am using an up-to-date version.
- [x] I have read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/docs/index.md).
- [x] I have searched existing [issues](https://github.com/TagStudioDev/TagStudio/issues).

### TagStudio Version

main

### Operating System & Version

Windows 10

### Description

Updating a library, adding tags to new entries gets increasingly slower over time.

Some details in hopes they help:
Library has around 300~ tags, 1200~ existing entries
Slowness becomes pretty much unbearable (tag search takes >5 seconds) after like 5-6 new entries, which each receive maybe 8~ tags.
Closing the library (keeping the application open) and reopening the library does not help.
Even clearing the search_field input, causing it to search for *all tags* takes seconds after use, whereas this is near instant on a fresh start of the process.
Closing the process and reopening, then performing the exact same search is 10x faster

### Attempted fixes / debugging information

I've noticed search gets run for every keypress in the search_field, which I've made a local change for using QTimer as a debounce timer but that didn't make noticeable improvements (I can possibly PR this if that seems interesting to you).
Essentially it waits 225ms after a keypress to perform the search, getting canceled by every subsequent keypress, so hopefully when the name is already known by the user, we can perform a single tag search, at the end.

I suspect there's a memory leak somewhere, or an infinitely growing list that is making every subsequent iteration take increasingly longer, but I'm not  familiar enough with the project to find this.

Also the logs didn't seem to provide much useful information to debug this.

### Reproduction

I made a reproduction, all tags are obfuscated (replaced with numbers) because the data is personal.
All entries are replaced with empty `.txt` files

https://drive.google.com/file/d/1QZ86CO6JzLVMQlm8kZRk9675_-l2q98o/view?usp=sharing

Note: I think the size of the tags or the size of the search matters in reproducing the issue, the real tags are about 8-12 characters wide.

1. Open the library (I noticed the `folders` contains an absolute path, so I'm not sure the repro is sharable without some manual editing ?)
2. Add these tags to each entry separately:
```
12
23
34
45
56
78
89
90
```
*using the keyboard, pressing enter inbetween, no cursor is involved other than pressing "Add Tags"
3. By the 8th~ entry you will notice it takes increasingly longer to complete all these steps for each subsequent entry
[Bug]: Adding (or searching for) tags gets increasingly slower in the same session (running process)
### Checklist

- [x] I am using an up-to-date version.
- [x] I have read the [documentation](https://github.com/TagStudioDev/TagStudio/blob/main/docs/index.md).
- [x] I have searched existing [issues](https://github.com/TagStudioDev/TagStudio/issues).

### TagStudio Version

main

### Operating System & Version

Windows 10

### Description

Updating a library, adding tags to new entries gets increasingly slower over time.

Some details in hopes they help:
Library has around 300~ tags, 1200~ existing entries
Slowness becomes pretty much unbearable (tag search takes >5 seconds) after like 5-6 new entries, which each receive maybe 8~ tags.
Closing the library (keeping the application open) and reopening the library does not help.
Even clearing the search_field input, causing it to search for *all tags* takes seconds after use, whereas this is near instant on a fresh start of the process.
Closing the process and reopening, then performing the exact same search is 10x faster

### Attempted fixes / debugging information

I've noticed search gets run for every keypress in the search_field, which I've made a local change for using QTimer as a debounce timer but that didn't make noticeable improvements (I can possibly PR this if that seems interesting to you).
Essentially it waits 225ms after a keypress to perform the search, getting canceled by every subsequent keypress, so hopefully when the name is already known by the user, we can perform a single tag search, at the end.

I suspect there's a memory leak somewhere, or an infinitely growing list that is making every subsequent iteration take increasingly longer, but I'm not  familiar enough with the project to find this.

Also the logs didn't seem to provide much useful information to debug this.

### Reproduction

I made a reproduction, all tags are obfuscated (replaced with numbers) because the data is personal.
All entries are replaced with empty `.txt` files

https://drive.google.com/file/d/1QZ86CO6JzLVMQlm8kZRk9675_-l2q98o/view?usp=sharing

Note: I think the size of the tags or the size of the search matters in reproducing the issue, the real tags are about 8-12 characters wide.

1. Open the library (I noticed the `folders` contains an absolute path, so I'm not sure the repro is sharable without some manual editing ?)
2. Add these tags to each entry separately:
```
12
23
34
45
56
78
89
90
```
*using the keyboard, pressing enter inbetween, no cursor is involved other than pressing "Add Tags"
3. By the 8th~ entry you will notice it takes increasingly longer to complete all these steps for each subsequent entry


Hints:
I've been able to reproduce this, and it does indeed look like memory usage is increasing the more searches are preformed
After *much* debugging, I believe I've narrowed this down to the `translate_qobject()` calls inside the `TagWidget` objects that are use for the context menus. When removing these translation calls or replacing them with `translate_formatted()` calls, the issue is resolved for me. I'll open up a quick PR and hope that this fixes the issue in your case as well
I've been able to reproduce this, and it does indeed look like memory usage is increasing the more searches are preformed
After *much* debugging, I believe I've narrowed this down to the `translate_qobject()` calls inside the `TagWidget` objects that are use for the context menus. When removing these translation calls or replacing them with `translate_formatted()` calls, the issue is resolved for me. I'll open up a quick PR and hope that this fixes the issue in your case as well

Patch:
diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py
index d35065675..fdd986ab5 100644
--- a/tagstudio/src/qt/modals/tag_search.py
+++ b/tagstudio/src/qt/modals/tag_search.py
@@ -52,7 +52,7 @@ def __init__(self, library: Library, exclude: list[int] = None, is_tag_chooser:
         self.search_field.setObjectName("searchField")
         self.search_field.setMinimumSize(QSize(0, 32))
         Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags")
-        self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text()))
+        self.search_field.textEdited.connect(lambda text: self.update_tags(text))
         self.search_field.returnPressed.connect(lambda: self.on_return(self.search_field.text()))
 
         self.scroll_contents = QWidget()
@@ -119,7 +119,7 @@ def __build_row_item_widget(self, tag: Tag):
             row.addWidget(add_button)
         return container
 
-    def construct_tag_button(self, query: str | None):
+    def build_create_tag_button(self, query: str | None):
         """Constructs a Create Tag Button."""
         container = QWidget()
         row = QHBoxLayout(container)
@@ -208,7 +208,7 @@ def update_tags(self, query: str | None = None):
         else:
             # If query doesnt exist add create button
             self.first_tag_id = None
-            c = self.construct_tag_button(query)
+            c = self.build_create_tag_button(query)
             self.scroll_layout.addWidget(c)
         self.search_field.setFocus()
 
diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py
index efc288bbe..1af377e3c 100644
--- a/tagstudio/src/qt/widgets/tag.py
+++ b/tagstudio/src/qt/widgets/tag.py
@@ -4,10 +4,8 @@
 
 
 import math
-from pathlib import Path
 from types import FunctionType
 
-from PIL import Image
 from PySide6.QtCore import QEvent, Qt, Signal
 from PySide6.QtGui import QAction, QEnterEvent, QFontMetrics
 from PySide6.QtWidgets import (
@@ -93,10 +91,6 @@ def leaveEvent(self, event: QEvent) -> None:  # noqa: N802
 
 
 class TagWidget(QWidget):
-    edit_icon_128: Image.Image = Image.open(
-        str(Path(__file__).parents[3] / "resources/qt/images/edit_icon_128.png")
-    ).resize((math.floor(14 * 1.25), math.floor(14 * 1.25)))
-    edit_icon_128.load()
     on_remove = Signal()
     on_click = Signal()
     on_edit = Signal()
@@ -126,20 +120,22 @@ def __init__(
         self.bg_button.setText(tag.name)
         if has_edit:
             edit_action = QAction(self)
-            Translations.translate_qobject(edit_action, "generic.edit")
+            edit_action.setText(Translations.translate_formatted("generic.edit"))
             edit_action.triggered.connect(on_edit_callback)
             edit_action.triggered.connect(self.on_edit.emit)
             self.bg_button.addAction(edit_action)
         # if on_click_callback:
         self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
 
+        # TODO: This currently doesn't work in "Add Tag" menus. Either fix this or
+        # disable it in that context.
         search_for_tag_action = QAction(self)
-        Translations.translate_qobject(search_for_tag_action, "tag.search_for_tag")
+        search_for_tag_action.setText(Translations.translate_formatted("tag.search_for_tag"))
         search_for_tag_action.triggered.connect(self.on_click.emit)
         self.bg_button.addAction(search_for_tag_action)
-        add_to_search_action = QAction(self)
-        Translations.translate_qobject(add_to_search_action, "tag.add_to_search")
-        self.bg_button.addAction(add_to_search_action)
+        # add_to_search_action = QAction(self)
+        # add_to_search_action.setText(Translations.translate_formatted("tag.add_to_search"))
+        # self.bg_button.addAction(add_to_search_action)
 
         self.inner_layout = QHBoxLayout()
         self.inner_layout.setObjectName("innerLayout")


Test patch: