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:
Teknium 2026-05-09 11:07:06 -07:00 committed by GitHub
parent 0d9800743c
commit 8f83046f6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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",