Removes memory heap bloat by improving DOM and JQUI datacache clearing when swapping WI in the editor, and all other 'reloadEditor' calls via slashcommands etc. Results in a slight delay depending on entries visible via pagination (about 2s for 50 visible entries), but I added a pleasant fade transition to make it feel organic.
| @@ -1,7 +1,7 @@ | |||
| 1 | import { Fuse } from '../lib.js'; | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | ||
| 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; | 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; |
| 4 | import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique, equalsIgnoreCaseAndAccents } from './utils.js'; | 4 | import { download, debounce, delay, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique, equalsIgnoreCaseAndAccents } from './utils.js'; |
| 5 | import { extension_settings, getContext } from './extensions.js'; | 5 | import { extension_settings, getContext } from './extensions.js'; |
| 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; | 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; |
| 7 | import { isMobile } from './RossAscends-mods.js'; | 7 | import { isMobile } from './RossAscends-mods.js'; |
| @@ -1754,12 +1754,12 @@ function registerWorldInfoSlashCommands() { | |||
| 1754 | */ | 1754 | */ |
| 1755 | export async function showWorldEditor(name) { | 1755 | export async function showWorldEditor(name) { |
| 1756 | if (!name) { | 1756 | if (!name) { |
| 1757 | hideWorldEditor(); | 1757 | await hideWorldEditor(); |
| 1758 | return; | 1758 | return; |
| 1759 | } | 1759 | } |
| 1760 | 1760 | ||
| 1761 | const wiData = await loadWorldInfo(name); | 1761 | const wiData = await loadWorldInfo(name); |
| 1762 | displayWorldEntries(name, wiData); | 1762 | await displayWorldEntries(name, wiData); |
| 1763 | } | 1763 | } |
| 1764 | 1764 | ||
| 1765 | /** | 1765 | /** |
| @@ -1815,8 +1815,8 @@ export async function updateWorldInfoList() { | |||
| 1815 | } | 1815 | } |
| 1816 | } | 1816 | } |
| 1817 | 1817 | ||
| 1818 | function hideWorldEditor() { | 1818 | async function hideWorldEditor() { |
| 1819 | displayWorldEntries(null, null); | 1819 | await displayWorldEntries(null, null); |
| 1820 | } | 1820 | } |
| 1821 | 1821 | ||
| 1822 | function getWIElement(name) { | 1822 | function getWIElement(name) { |
| @@ -1937,15 +1937,102 @@ function updateWorldEntryKeyOptionsCache(keyOptions, { remove = false, reset = f | |||
| 1937 | worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text)); | 1937 | worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text)); |
| 1938 | } | 1938 | } |
| 1939 | 1939 | ||
| 1940 | function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) { | 1940 | function clearEntryList() { |
| 1941 | updateEditor = (navigation, flashOnNav = true) => displayWorldEntries(name, data, navigation, flashOnNav); | 1941 | console.time('clearEntryList'); |
| 1942 | const $list = $('#world_popup_entries_list'); | ||
| 1943 | |||
| 1944 | |||
| 1945 | if (!$list.children().length) { | ||
| 1946 | console.warn('List already empty, skipping cleanup.'); | ||
| 1947 | console.timeEnd('clearEntryList'); | ||
| 1948 | return; | ||
| 1949 | } | ||
| 1950 | |||
| 1951 | // Step 1: Clean all <option> elements within <select> | ||
| 1952 | console.warn(`Before cleaning: ${$list.find('option').length} options in list`); | ||
| 1953 | $list.find('option').each(function () { | ||
| 1954 | const $option = $(this); | ||
| 1955 | $option.off(); | ||
| 1956 | $.cleanData([$option[0]]); | ||
| 1957 | $option.remove(); | ||
| 1958 | }); | ||
| 1959 | console.warn(`Post-clean: ${$list.find('option').length} options in list`); | ||
| 1960 | |||
| 1961 | // Step 2: Clean all <select> elements | ||
| 1962 | console.warn(`Before cleaning: ${$list.find('select').length} selects in list`); | ||
| 1963 | $list.find('select').each(function () { | ||
| 1964 | const $select = $(this); | ||
| 1965 | // Remove Select2-related data and container if present | ||
| 1966 | if ($select.hasClass('select2-hidden-accessible')) { | ||
| 1967 | try { | ||
| 1968 | $select.select2('destroy'); | ||
| 1969 | } catch (e) { | ||
| 1970 | console.warn('Select2 destroy failed:', e); | ||
| 1971 | } | ||
| 1972 | } | ||
| 1973 | const $container = $select.parent(); | ||
| 1974 | if ($container.length) { | ||
| 1975 | $container.find('*').off(); | ||
| 1976 | $.cleanData($container.find('*').get()); | ||
| 1977 | $container.remove(); | ||
| 1978 | } else { | ||
| 1979 | console.warn('container not found'); | ||
| 1980 | console.warn($select.parent().html()); | ||
| 1981 | } | ||
| 1982 | // Destroy Select2 if applicable | ||
| 1983 | |||
| 1984 | $select.off(); | ||
| 1985 | $.cleanData([$select[0]]); | ||
| 1986 | |||
| 1987 | }); | ||
| 1988 | console.warn(`Post-clean: ${$list.find('select').length} selects in list`); | ||
| 1989 | |||
| 1990 | // Step 3: Clean <div>, <span>, <input> | ||
| 1991 | console.warn(`Before cleaning: ${$list.find('div, span, input').length} divs, spans, inputs in list`); | ||
| 1992 | $list.find('div, span, input').each(function () { | ||
| 1993 | const $elem = $(this); | ||
| 1994 | $elem.off(); | ||
| 1995 | $.cleanData([$elem[0]]); | ||
| 1996 | $elem.remove(); | ||
| 1997 | }); | ||
| 1998 | console.warn(`Post-clean: ${$list.find('div, span, input').length} divs, spans, inputs in list`); | ||
| 1999 | |||
| 2000 | // Destroy Sortable | ||
| 2001 | console.warn(`Before cleaning: ${$list.sortable('instance') ? 1 : 0} sortable instance in list`); | ||
| 2002 | if ($list.sortable('instance')) { | ||
| 2003 | $list.sortable('destroy'); | ||
| 2004 | } | ||
| 2005 | console.warn(`Post-cleaning: ${$list.sortable('instance') ? 1 : 0} sortable instance in list`); | ||
| 2006 | |||
| 2007 | let totalElementsOfanyKindLeftInList = $list.children().length; | ||
| 2008 | |||
| 2009 | // Final cleanup | ||
| 2010 | if (totalElementsOfanyKindLeftInList !== 0) { | ||
| 2011 | console.time('empty'); | ||
| 2012 | console.warn(`Before .empty(): ${totalElementsOfanyKindLeftInList} elements left in list`); | ||
| 2013 | $list.empty(); | ||
| 2014 | totalElementsOfanyKindLeftInList = $list.children().length; | ||
| 2015 | console.warn(`After .empty(): ${totalElementsOfanyKindLeftInList} elements left in list`); | ||
| 2016 | console.timeEnd('empty'); | ||
| 2017 | } else { | ||
| 2018 | console.warn('Entry List is already totally empty, no need to call .empty()'); | ||
| 2019 | } | ||
| 2020 | |||
| 2021 | console.timeEnd('clearEntryList'); | ||
| 2022 | } | ||
| 2023 | |||
| 2024 | async function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) { | ||
| 2025 | updateEditor = async (navigation, flashOnNav = true) => await displayWorldEntries(name, data, navigation, flashOnNav); | ||
| 1942 | 2026 | ||
| 1943 | const worldEntriesList = $('#world_popup_entries_list'); | 2027 | const worldEntriesList = $('#world_popup_entries_list'); |
| 1944 | 2028 | ||
| 1945 | // We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements | 2029 | // We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements |
| 1946 | // This prevents jQuery from actually going through all registered events on the controls for each entry when removing it | 2030 | // This prevents jQuery from actually going through all registered events on the controls for each entry when removing it |
| 1947 | worldEntriesList.find('*').off(); | 2031 | //worldEntriesList.find('*').off(); |
| 1948 | worldEntriesList.empty().show(); | 2032 | worldEntriesList.css({ 'opacity': 0, 'transition': 'opacity 250ms ease-in-out' }); |
| 2033 | await delay(250); | ||
| 2034 | clearEntryList(); // Use enhanced cleanup | ||
| 2035 | worldEntriesList.show(); | ||
| 1949 | 2036 | ||
| 1950 | if (!data || !('entries' in data)) { | 2037 | if (!data || !('entries' in data)) { |
| 1951 | $('#world_popup_new').off('click').on('click', nullWorldInfo); | 2038 | $('#world_popup_new').off('click').on('click', nullWorldInfo); |
| @@ -2040,8 +2127,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl | |||
| 2040 | callback: async function (/** @type {object[]} */ page) { | 2127 | callback: async function (/** @type {object[]} */ page) { |
| 2041 | // We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements | 2128 | // We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements |
| 2042 | // This prevents jQuery from actually going through all registered events on the controls for each entry when removing it | 2129 | // This prevents jQuery from actually going through all registered events on the controls for each entry when removing it |
| 2043 | worldEntriesList.find('*').off(); | 2130 | //worldEntriesList.find('*').off(); |
| 2044 | worldEntriesList.empty(); | 2131 | clearEntryList(); |
| 2132 | //worldEntriesList.empty(); | ||
| 2045 | 2133 | ||
| 2046 | const keywordHeaders = await renderTemplateAsync('worldInfoKeywordHeaders'); | 2134 | const keywordHeaders = await renderTemplateAsync('worldInfoKeywordHeaders'); |
| 2047 | const blocksPromises = page.map(async (entry) => await getWorldEntry(name, data, entry)).filter(x => x); | 2135 | const blocksPromises = page.map(async (entry) => await getWorldEntry(name, data, entry)).filter(x => x); |
| @@ -2173,7 +2261,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl | |||
| 2173 | if (selectedIndex !== -1) { | 2261 | if (selectedIndex !== -1) { |
| 2174 | $('#world_editor_select').val(selectedIndex).trigger('change'); | 2262 | $('#world_editor_select').val(selectedIndex).trigger('change'); |
| 2175 | } else { | 2263 | } else { |
| 2176 | hideWorldEditor(); | 2264 | await hideWorldEditor(); |
| 2177 | } | 2265 | } |
| 2178 | } | 2266 | } |
| 2179 | }); | 2267 | }); |
| @@ -2211,6 +2299,12 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl | |||
| 2211 | await saveWorldInfo(name, data); | 2299 | await saveWorldInfo(name, data); |
| 2212 | }, | 2300 | }, |
| 2213 | }); | 2301 | }); |
| 2302 | |||
| 2303 | worldEntriesList.css('opacity', '1'); | ||
| 2304 | //remove inline CSS on the list | ||
| 2305 | worldEntriesList.removeAttr('style'); | ||
| 2306 | |||
| 2307 | |||
| 2214 | //$("#world_popup_entries_list").disableSelection(); | 2308 | //$("#world_popup_entries_list").disableSelection(); |
| 2215 | } | 2309 | } |
| 2216 | 2310 | ||
| @@ -3819,7 +3913,7 @@ export async function createNewWorldInfo(worldName, { interactive = false } = {} | |||
| 3819 | if (selectedIndex !== -1) { | 3913 | if (selectedIndex !== -1) { |
| 3820 | $('#world_editor_select').val(selectedIndex).trigger('change'); | 3914 | $('#world_editor_select').val(selectedIndex).trigger('change'); |
| 3821 | } else { | 3915 | } else { |
| 3822 | hideWorldEditor(); | 3916 | await hideWorldEditor(); |
| 3823 | } | 3917 | } |
| 3824 | 3918 | ||
| 3825 | return true; | 3919 | return true; |
| @@ -5385,7 +5479,7 @@ export function initWorldInfo() { | |||
| 5385 | const selectedIndex = String($('#world_editor_select').find(':selected').val()); | 5479 | const selectedIndex = String($('#world_editor_select').find(':selected').val()); |
| 5386 | 5480 | ||
| 5387 | if (selectedIndex === '') { | 5481 | if (selectedIndex === '') { |
| 5388 | hideWorldEditor(); | 5482 | await hideWorldEditor(); |
| 5389 | } else { | 5483 | } else { |
| 5390 | const worldName = world_names[selectedIndex]; | 5484 | const worldName = world_names[selectedIndex]; |
| 5391 | showWorldEditor(worldName); | 5485 | showWorldEditor(worldName); |