mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
perf(google_chat): defer heavy google-cloud imports to first adapter use (#22681)
Plugin discovery imports every bundled platform plugin at model_tools
import time. The google_chat adapter unconditionally pulled in
google.cloud.pubsub_v1, googleapiclient, grpc, httplib2, and friends at
module top — about 33 MB RSS and 110 ms wall on every CLI invocation,
even ones that never construct a gateway adapter.
Wrap the heavy imports in _load_google_modules(): an idempotent loader
that rebinds the module-level globals (pubsub_v1, service_account,
HttpError, MediaFileUpload, …) on first call and is invoked from
GoogleChatAdapter.__init__, connect(), and check_google_chat_requirements().
The HttpError = Exception placeholder is preserved for the brief window
before the loader runs, so 'except HttpError as exc:' clauses stay
correct (Python looks up the name at try/except evaluation time, not
at function definition time).
Measured impact on a 9950X3D, 7-run medians:
import cli: 895 → 787 ms (-108 ms / -12%)
133 → 110 MB ( -23 MB / -17%)
import model_tools: 491 → 400 ms ( -91 ms / -19%)
95 → 66 MB ( -29 MB / -31%)
google_chat alone: 244 → 132 ms (-112 ms / -46%)
83 → 50 MB ( -33 MB / -40%)
hermes chat -q (cold): 177 → 145 MB ( -32 MB / -18%)
Real-world win lands on every path that imports cli.py: hermes chat,
hermes gateway, cron jobs, batch runs, subagents. Long-lived gateway
processes save ~30 MB resident.
All 157 google_chat tests pass; full gateway suite (5050 tests) green.
This commit is contained in:
parent
0d9800743c
commit
8f83046f6c
1 changed files with 92 additions and 22 deletions
|
|
@ -46,27 +46,75 @@ import re
|
|||
from pathlib import Path as _Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
try:
|
||||
import httplib2
|
||||
from google.cloud import pubsub_v1
|
||||
from google.api_core import exceptions as gax_exceptions
|
||||
from google.oauth2 import service_account
|
||||
from google_auth_httplib2 import AuthorizedHttp
|
||||
from googleapiclient.discovery import build as build_service
|
||||
from googleapiclient.errors import HttpError
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
# Heavy google-cloud + googleapiclient imports are deferred to first
|
||||
# adapter use. Importing them eagerly here added ~110ms wall and ~33MB
|
||||
# RSS to *every* CLI invocation (the plugin loader imports this module at
|
||||
# ``model_tools`` import time, so ``hermes status``, ``hermes chat``, etc.
|
||||
# all paid the cost even though they never instantiate the adapter).
|
||||
#
|
||||
# All names below are module globals that ``_load_google_modules()``
|
||||
# rebinds on first call. The ``HttpError = Exception`` placeholder is
|
||||
# important: ``except HttpError as exc:`` clauses elsewhere in this
|
||||
# module bind the *current* module-global at try/except evaluation time,
|
||||
# so as long as ``_load_google_modules()`` runs before any such
|
||||
# ``try`` block executes (which it does — ``__init__`` calls it), the
|
||||
# rebound real ``googleapiclient.errors.HttpError`` is what actually
|
||||
# matches at runtime.
|
||||
GOOGLE_CHAT_AVAILABLE: bool = False
|
||||
httplib2: Any = None # type: ignore
|
||||
pubsub_v1: Any = None # type: ignore
|
||||
gax_exceptions: Any = None # type: ignore
|
||||
service_account: Any = None # type: ignore
|
||||
AuthorizedHttp: Any = None # type: ignore
|
||||
build_service: Any = None # type: ignore
|
||||
HttpError: Any = Exception # type: ignore
|
||||
MediaFileUpload: Any = None # type: ignore
|
||||
|
||||
_google_modules_loaded: bool = False
|
||||
|
||||
|
||||
def _load_google_modules() -> bool:
|
||||
"""Lazily import the heavy google-cloud + googleapiclient stack.
|
||||
|
||||
Idempotent. Returns True if the optional deps are installed and
|
||||
were successfully imported, False otherwise. On success, mutates
|
||||
the module globals so existing code using ``pubsub_v1``,
|
||||
``service_account``, ``HttpError``, etc. transparently uses the
|
||||
real classes.
|
||||
|
||||
Why deferred: the import chain pulls in google.cloud.pubsub_v1,
|
||||
googleapiclient, grpc, and friends — about 33MB RSS and 110ms wall
|
||||
on a fresh interpreter. Plugin discovery imports this module on
|
||||
every CLI invocation, even ones that never touch a gateway.
|
||||
"""
|
||||
global GOOGLE_CHAT_AVAILABLE, _google_modules_loaded
|
||||
global httplib2, pubsub_v1, gax_exceptions, service_account
|
||||
global AuthorizedHttp, build_service, HttpError, MediaFileUpload
|
||||
if _google_modules_loaded:
|
||||
return GOOGLE_CHAT_AVAILABLE
|
||||
_google_modules_loaded = True
|
||||
try:
|
||||
import httplib2 as _httplib2
|
||||
from google.cloud import pubsub_v1 as _pubsub_v1
|
||||
from google.api_core import exceptions as _gax_exceptions
|
||||
from google.oauth2 import service_account as _service_account
|
||||
from google_auth_httplib2 import AuthorizedHttp as _AuthorizedHttp
|
||||
from googleapiclient.discovery import build as _build_service
|
||||
from googleapiclient.errors import HttpError as _HttpError
|
||||
from googleapiclient.http import MediaFileUpload as _MediaFileUpload
|
||||
except ImportError:
|
||||
GOOGLE_CHAT_AVAILABLE = False
|
||||
return False
|
||||
httplib2 = _httplib2
|
||||
pubsub_v1 = _pubsub_v1
|
||||
gax_exceptions = _gax_exceptions
|
||||
service_account = _service_account
|
||||
AuthorizedHttp = _AuthorizedHttp
|
||||
build_service = _build_service
|
||||
HttpError = _HttpError
|
||||
MediaFileUpload = _MediaFileUpload
|
||||
GOOGLE_CHAT_AVAILABLE = True
|
||||
except ImportError:
|
||||
GOOGLE_CHAT_AVAILABLE = False
|
||||
httplib2 = None # type: ignore
|
||||
pubsub_v1 = None # type: ignore
|
||||
gax_exceptions = None # type: ignore
|
||||
service_account = None # type: ignore
|
||||
AuthorizedHttp = None # type: ignore
|
||||
build_service = None # type: ignore
|
||||
HttpError = Exception # type: ignore
|
||||
MediaFileUpload = None # type: ignore
|
||||
return True
|
||||
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
|
||||
|
|
@ -181,8 +229,14 @@ _TYPING_CONSUMED_SENTINEL = "<consumed>"
|
|||
|
||||
|
||||
def check_google_chat_requirements() -> bool:
|
||||
"""Check if Google Chat optional dependencies are installed."""
|
||||
return GOOGLE_CHAT_AVAILABLE
|
||||
"""Check if Google Chat optional dependencies are installed.
|
||||
|
||||
Triggers the lazy import of the google-cloud + googleapiclient stack
|
||||
on first call. Subsequent calls hit the cached result. This is the
|
||||
canonical "are the deps available" probe used by the plugin registry
|
||||
and the adapter's own startup gate.
|
||||
"""
|
||||
return _load_google_modules()
|
||||
|
||||
|
||||
# Hostnames we trust to host Google Chat attachment download URIs. Anything
|
||||
|
|
@ -400,6 +454,16 @@ class GoogleChatAdapter(BasePlatformAdapter):
|
|||
# attribute to ``gateway.config.Platform`` — bundled platform plugins
|
||||
# are looked up by value, not attribute (matches Teams, IRC).
|
||||
super().__init__(config, Platform("google_chat"))
|
||||
# Trigger the deferred google-cloud + googleapiclient import here so
|
||||
# that any code path which constructs the adapter and then calls
|
||||
# methods directly (notably the test suite, which builds an adapter
|
||||
# and invokes ``_send_file`` / ``_create_message`` / etc. without
|
||||
# going through ``connect()``) sees real classes for ``MediaFileUpload``,
|
||||
# ``service_account``, ``HttpError``, and friends. The module-level
|
||||
# globals were previously eager-imported; making this lazy saved
|
||||
# ~110ms / ~33MB on every CLI invocation. Idempotent — pays the cost
|
||||
# exactly once per process.
|
||||
_load_google_modules()
|
||||
self._subscriber: Optional[Any] = None
|
||||
self._chat_api: Optional[Any] = None
|
||||
# User-authed Chat API client built lazily from the OAuth refresh
|
||||
|
|
@ -685,7 +749,13 @@ class GoogleChatAdapter(BasePlatformAdapter):
|
|||
# ------------------------------------------------------------------
|
||||
async def connect(self) -> bool:
|
||||
"""Validate config, authenticate, start Pub/Sub pull, resolve bot id."""
|
||||
if not GOOGLE_CHAT_AVAILABLE:
|
||||
# First call into the heavy google-cloud stack — trigger the lazy
|
||||
# import. ``_load_google_modules()`` is idempotent and rebinds the
|
||||
# module globals (``pubsub_v1``, ``service_account``, ``HttpError``,
|
||||
# …) used throughout this file. Anything that runs *before* this
|
||||
# call would see the placeholders, so connect() is the natural
|
||||
# gate.
|
||||
if not _load_google_modules():
|
||||
self._set_fatal_error(
|
||||
code="missing_deps",
|
||||
message="google-cloud-pubsub / google-api-python-client not installed",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue