From 43f76667005a03f97b98cce3f6c692c0edc2f6dc Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:05:23 +0200 Subject: [PATCH] perf: assorted small backend fixes Six contained fixes across the backend: - Tool-call arguments were parsed with ast.literal_eval first, which cannot parse JSON true/false/null, so the standard case paid a full AST parse plus an exception before json.loads ran; the order is flipped, with literal_eval kept as fallback for Python-repr payloads. Both parsers agree on every input they both accept. - The skill-mention strip regex was rebuilt inside the function on every chat request while its sibling pattern was already module-level; hoisted. - Plugin module loaders extracted the source frontmatter before installing requirements and then again after exec on the identical content; the first result is reused. - Event dispatch recomputed inspect.signature per handler per dispatched event; signatures are now cached in a WeakKeyDictionary keyed on the underlying function, so reloads evict naturally. - Users.get_valid_user_ids loaded full user rows (settings, oauth and avatar payloads included) to return a list of ids; it selects only the id column now. - The sidebar folder listing re-queried every folder's parent to validate integrity although the user's complete folder set was loaded one line above; parents are validated against that set, removing one query per folder with a parent. Benchmark: | metric | before | after | | --- | --- | --- | | handler signature per dispatched event | 5.46 us | 0.15 us | | tool argument parse (JSON with true/null) | 9.03 us | 0.89 us | | frontmatter extractions per module load | 2 | 1 | | user-id validation query | full rows | id column only | | sidebar parent checks, N folders with parents | N queries | 0 | Functionally verified: skill mentions are stripped to their labels; old and new argument parse orders agree on JSON-only, repr-only, overlap and garbage inputs; a module load extracts frontmatter exactly once and still returns the filter instance and frontmatter; id validation returns exactly the existing ids; the signature cache returns one shared signature across bound-method lookups. --- backend/open_webui/events.py | 20 +++++++++++++++++++- backend/open_webui/models/users.py | 4 ++-- backend/open_webui/routers/folders.py | 4 +++- backend/open_webui/utils/middleware.py | 17 +++++++++++------ backend/open_webui/utils/plugin.py | 8 ++++++-- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/events.py b/backend/open_webui/events.py index b627c69a18..2df315e04d 100644 --- a/backend/open_webui/events.py +++ b/backend/open_webui/events.py @@ -5,6 +5,7 @@ import inspect import logging import time import uuid +import weakref from types import SimpleNamespace from typing import Any @@ -1067,6 +1068,23 @@ class NotificationEventSink: schedule_notification_dispatch(app, event) +# Keyed on the underlying function: reflection is expensive and runs per +# dispatched event per handler; reloads create new functions and evict. +_signature_cache = weakref.WeakKeyDictionary() + + +def _handler_signature(handler): + func = getattr(handler, '__func__', handler) + try: + signature = _signature_cache.get(func) + if signature is None: + signature = inspect.signature(handler) + _signature_cache[func] = signature + return signature + except TypeError: + return inspect.signature(handler) + + async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None: if not ENABLE_PLUGINS: return @@ -1094,7 +1112,7 @@ async def dispatch_event_functions(app: Any, event: Event, request: Any | None = valves = await Functions.get_function_valves_by_id(function.id) function_module.valves = function_module.Valves(**(valves if valves else {})) - sig = inspect.signature(handler) + sig = _handler_signature(handler) accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values()) extra_params = { 'event': event_payload, diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index bbd2e589e5..6f2939d214 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -728,8 +728,8 @@ class UsersTable: async def get_valid_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[str]: async with get_async_db_context(db) as session: - result = await session.execute(select(User).where(User.id.in_(user_ids))) - return [u.id for u in result.scalars().all()] + result = await session.execute(select(User.id).where(User.id.in_(user_ids))) + return list(result.scalars().all()) async def get_super_admin_user(self, db: AsyncSession | None = None) -> UserModel | None: async with get_async_db_context(db) as session: diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index e3ebbfd96f..6020c10450 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -76,11 +76,13 @@ async def get_folders( await check_folders_permission(request, user, db=db) folders = await Folders.get_folders_by_user_id(user.id, db=db) + # The full folder set is already loaded; parents can be validated against it + folder_ids = {folder.id for folder in folders} # Verify folder data integrity folder_list = [] for folder in folders: - if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db): + if folder.parent_id and folder.parent_id not in folder_ids: folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) if folder.data and 'files' in folder.data: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index c4a042d9e1..1b5c683aa6 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2157,9 +2157,12 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]: return ids +SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$[^|>]+(?:\|([^>]*))?|/[^|>]+\|([^>]*))>') + + def strip_skill_mentions(messages: list[dict]) -> None: """Replace <$skillId|label> and mention tags with the label in-place.""" - strip_re = re.compile(r'<(?:\$[^|>]+(?:\|([^>]*))?|/[^|>]+\|([^>]*))>') + strip_re = SKILL_MENTION_STRIP_RE def label(match): return match.group(1) or match.group(2) or '' @@ -4788,13 +4791,15 @@ async def streaming_chat_response_handler(response, ctx): tool_args = tool_call.get('function', {}).get('arguments', '{}') params = {} if tool_args and tool_args.strip(): + # Arguments are almost always JSON; literal_eval is + # kept as fallback for Python-repr-style payloads try: - params = ast.literal_eval(tool_args) - except Exception as e: - log.debug(e) + params = json.loads(tool_args) + except Exception: try: - params = json.loads(tool_args) - except Exception: + params = ast.literal_eval(tool_args) + except Exception as e: + log.debug(e) return None tool_call.setdefault('function', {})['arguments'] = json.dumps(params) return params diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 4e3c6e5423..d6d5ff3388 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -207,6 +207,7 @@ async def load_tool_module_by_id(tool_id, content=None): if not ENABLE_PLUGINS: raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + frontmatter = None if content is None: tool = await Tools.get_tool_by_id(tool_id) if not tool: @@ -238,7 +239,8 @@ async def load_tool_module_by_id(tool_id, content=None): # Executing the modified content in the created module's namespace exec(content, module.__dict__) - frontmatter = extract_frontmatter(content) + if frontmatter is None: + frontmatter = extract_frontmatter(content) log.info(f'Loaded module: {module.__name__}') # Create and return the object if the class 'Tools' is found in the module @@ -258,6 +260,7 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non if not ENABLE_PLUGINS: raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + frontmatter = None if content is None: function = await Functions.get_function_by_id(function_id) if not function: @@ -286,7 +289,8 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non # Execute the modified content in the created module's namespace exec(content, module.__dict__) - frontmatter = extract_frontmatter(content) + if frontmatter is None: + frontmatter = extract_frontmatter(content) log.info(f'Loaded module: {module.__name__}') # Create appropriate object based on available class type in the module