Regex cache (#4858) feat: optimize regex extension performance - Implemented caching mechanism for regex scripts to improve message rendering speed. - Added automatic cache invalidation on script updates. - Optimized regex compilation to reduce CPU usage. --------- Co-authored-by: GhostXia <33112711+GhostXia@users.noreply.github.com>

0225562f4ef27d52bbfc7d4e1a6e6c961074875b

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +59 -2Showing whitespace changes
public/scripts/extensions/regex/engine.js+56 -1
@@ -35,6 +35,61 @@ export const SCRIPT_TYPE_UNKNOWN = -1;
3535const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false });
3636
3737/**
38+ * Manages the compiled regex cache with LRU eviction.
39+ */
40+export class RegexProvider {
41+ /** @type {Map<string, RegExp>} */
42+ #cache = new Map();
43+ /** @type {number} */
44+ #maxSize = 1000;
45+
46+ static instance = new RegexProvider();
47+
48+ /**
49+ * Gets a regex instance by its string representation.
50+ * @param {string} regexString The regex string to retrieve
51+ * @returns {RegExp?} Compiled regex or null if invalid
52+ */
53+ get(regexString) {
54+ const isCached = this.#cache.has(regexString);
55+ const regex = isCached
56+ ? this.#cache.get(regexString)
57+ : regexFromString(regexString);
58+
59+ if (!regex) {
60+ return null;
61+ }
62+
63+ if (isCached) {
64+ // LRU: Move to end by re-inserting
65+ this.#cache.delete(regexString);
66+ this.#cache.set(regexString, regex);
67+ } else {
68+ // Evict oldest if at capacity
69+ if (this.#cache.size >= this.#maxSize) {
70+ const firstKey = this.#cache.keys().next().value;
71+ this.#cache.delete(firstKey);
72+ }
73+ this.#cache.set(regexString, regex);
74+ }
75+
76+ // Reset lastIndex for global/sticky regexes
77+ if (regex.global || regex.sticky) {
78+ regex.lastIndex = 0;
79+ }
80+
81+ return regex;
82+ }
83+
84+ /**
85+ * Clears the entire cache.
86+ */
87+ clear() {
88+ this.#cache.clear();
89+ }
90+}
91+
92+/**
3893 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
3994 *
4095 * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts
@@ -353,7 +408,7 @@ export function runRegexScript(regexScript, rawString, { characterOverride } = {
353408 }
354409 };
355410 const regexString = getRegexString();
356411 const findRegex = regexFromStringRegexProvider.instance.get(regexString);
357412
358413 // The user skill issued. Return with nothing.
359414 if (!findRegex) {
public/scripts/extensions/regex/index.js+3 -1
@@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashComman
88import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
1111import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, RegexProvider, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js';
1212import { t } from '../../i18n.js';
1313import { accountStorage } from '../../util/AccountStorage.js';
1414import { getPresetManager } from '../../preset-manager.js';
@@ -1628,6 +1628,8 @@ async function checkCharEmbeddedRegexScripts() {
16281628 }
16291629 }
16301630
1631+ // Clear cache and reload scripts
1632+ RegexProvider.instance.clear();
16311633 await loadRegexScripts();
16321634}
16331635