-
-
Notifications
You must be signed in to change notification settings - Fork 26
Description
Summary
The custom-sort plugin causes significant performance degradation in Obsidian when its sorting functionality is repeatedly re-enabled (e.g., toggling the plugin off/on or triggering patchFileExplorer multiple times). This happens because the plugin monkey-patches getSortedFolderItems every time without checking whether it's already patched, leading to deeply nested wrapper functions and worsening performance with each invocation.
Steps to Reproduce
- Install and enable the custom-sort plugin.
- Open the developer console and enable the custom sorting 200 times to simulate the bug causing behaviour:
for(i = 0; i < 200; i++) {
app.commands.executeCommandById("custom-sort:enable-custom-sorting");
}- Create a new note in the file explorer and notice the long calculation period and GUI freeze
- If no performance degradation can be noticed, repeat from step 2
Root Cause
The plugin modifies the Obsidian sorting behaviour in its getSortedFolderItems(...) function using monkey-patching via the around() helper as can be seen here:
obsidian-custom-sort/src/main.ts
Lines 249 to 279 in f6f1d9b
| getSortedFolderItems(old: any) { | |
| return function (...args: any[]) { | |
| // quick check for plugin status | |
| if (plugin.settings.suspended) { | |
| return old.call(this, ...args); | |
| } | |
| const folder = args[0] | |
| const sortingData = plugin.determineAndPrepareSortingDataForFolder(folder) | |
| if (sortingData.sortSpec) { | |
| if (!plugin.customSortAppliedAtLeastOnce) { | |
| plugin.customSortAppliedAtLeastOnce = true | |
| setTimeout(() => { | |
| plugin.setRibbonIconToEnabled.apply(plugin) | |
| plugin.showNotice('Custom sort APPLIED.'); | |
| plugin.updateStatusBar() | |
| }) | |
| } | |
| return getSortedFolderItems.call(this, folder, sortingData.sortSpec, plugin.createProcessingContextForSorting(sortingData.sortingAndGroupingStats)) | |
| } else { | |
| return old.call(this, ...args); | |
| } | |
| }; | |
| } | |
| }) | |
| this.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper)) | |
| return patchableFileExplorer | |
| } else { | |
| return undefined | |
| } |
The problem arises because this patching is not idempotent. Every time patchFileExplorer() is called, a new wrapper is created around the existing one. And because for every enabling/re-enabling of the sorting functionality the patchFileExplorer() function is called, it adds a new layer of patching to it. This causes:
- A growing stack of .call() wrappers
- Increased memory usage
- Decreased Obsidian performance leading to long processing times when new files are created
How I Discovered the Issue
In my workflow, I use a git integration that regularly syncs new files into my vault. To ensure these new files are sorted correctly in the file explorer, I run the custom-sort: enable command programmatically every 30 seconds. This frequent re-invocation of the sorting logic causes the plugin to repeatedly re-patch getSortedFolderItems() without unwrapping the previous layer. As a result, the call stack and memory usage grow over time. Although the plugin appears to function correctly at first, the performance of the file explorer gradually degrades the longer Obsidian remains open, eventually becoming unresponsive due to the accumulation of nested wrappers.
I diagnosed the problem using the Performance Tab in the Developer Console and recorded a trace, because every new file creation took multiple seconds of calculations. After debugging for a while I noticed the following call chain:
As you can see, the explorer was patched multiple times (~1000 times) and each time a new sort is triggered the complete patch chain needs to be traversed to arrive at the final function, which causes performance problems.
How I Patched It Locally
To work around the issue, I introduced a simple class-level flag to ensure the file explorer is only patched once. Specifically, I added a new property to the plugin class:
private isExplorerPatched: boolean = false;In the MonkeyAroundUninstaller method, I then added a guard before applying the patch using
if (patchableFileExplorer) {
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(patchableFileExplorer.view.constructor.prototype, {
getSortedFolderItems(old: any) {
return function (...args: any[]) {
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
}
const folder = args[0]
const sortingData = plugin.determineAndPrepareSortingDataForFolder(folder)
if (sortingData.sortSpec) {
if (!plugin.customSortAppliedAtLeastOnce) {
plugin.customSortAppliedAtLeastOnce = true
setTimeout(() => {
plugin.setRibbonIconToEnabled.apply(plugin)
plugin.showNotice('Custom sort APPLIED.');
plugin.updateStatusBar()
})
}
return getSortedFolderItems.call(this, folder, sortingData.sortSpec, plugin.createProcessingContextForSorting(sortingData.sortingAndGroupingStats))
} else {
return old.call(this, ...args);
}
};
}
})
if (!plugin.isExplorerPatched) {
this.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
plugin.isExplorerPatched = true;
}
return patchableFileExplorer
} else {
return undefined
}This ensures that repeated calls to patchFileExplorer() - whether from commands, plugin reinitialization, or automation - will not redundantly wrap getSortedFolderItems(). With this change in place, performance no longer degrades over time, even when the patch is re-attempted every 30 seconds as part of my automated Git sync routine.
I do not know if this solution might cause problems in other areas of the plugin, but it seems to work fine.
Request for Fix
Thank you for your work on the Custom Sort plugin - it's an incredibly helpful addition to the Obsidian ecosystem and plays a key role in my workflow. Given the impact of this issue on long-running sessions or automated environments, I’d be grateful if you could consider implementing an official fix to prevent repeated patching.
