From e5aa61d964d072e13f4c994ad5bf34b5dc3de5c5 Mon Sep 17 00:00:00 2001 From: Christian-Manuel Butzke Date: Tue, 7 Jul 2026 00:43:18 +0900 Subject: [PATCH] perf: lazy-import celpy and jsonschema to cut CLI startup ~2x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit celpy (with its lark + pendulum deps, ~0.1s) and jsonschema (~0.04s) were imported at module load, so every invocation — even determa-state --version — paid for them. Neither is needed to load, inspect, snapshot, or step a machine: celpy is only used to *evaluate* a CEL guard/action, jsonschema only to validate a machine's structure. Defer both to first use (celpy via a memoized loader in cel.py; jsonschema inside validator._structural_errors). Guard-free paths (--version, --help, state, enabled, send-from-store) no longer import them. determa-state --version: ~370ms -> ~170ms (warm). No behaviour change: 150 unit + 67 conformance green, ruff + mypy clean. --- src/determa/state/cel.py | 35 +++++++++++++++++++++++++++------- src/determa/state/validator.py | 4 ++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/determa/state/cel.py b/src/determa/state/cel.py index a2d485d..ccf7c86 100644 --- a/src/determa/state/cel.py +++ b/src/determa/state/cel.py @@ -12,28 +12,47 @@ from __future__ import annotations from functools import lru_cache -from typing import Any +from typing import TYPE_CHECKING, Any, cast -import celpy -import celpy.celtypes as celtypes +if TYPE_CHECKING: + import celpy class CelError(Exception): """A CEL expression failed to compile or evaluate (e.g. division by zero).""" -_env = celpy.Environment() +# celpy (with its lark + pendulum transitive deps) costs ~0.1s to import, but is only +# needed to *evaluate* an expression — not to load, inspect, snapshot, or step a machine. +# Import it lazily so the CLI and library stay fast on guard-free paths (SPEC §6). +_celpy: Any = None +_celtypes: Any = None +_env: Any = None + + +def _load() -> tuple[Any, Any, Any]: + global _celpy, _celtypes, _env + if _celpy is None: + import celpy as celpy_mod + import celpy.celtypes as celtypes_mod + + _celpy = celpy_mod + _celtypes = celtypes_mod + _env = celpy_mod.Environment() + return _celpy, _celtypes, _env @lru_cache(maxsize=2048) def _program(expr: str) -> celpy.Runner: + celpy_mod, _, env = _load() try: - return _env.program(_env.compile(expr)) - except celpy.CELEvalError as exc: # type: ignore[attr-defined] + return cast("celpy.Runner", env.program(env.compile(expr))) + except celpy_mod.CELEvalError as exc: raise CelError(f"compile error: {expr!r}: {exc}") from exc def _to_cel(value: Any) -> Any: + _, celtypes, _ = _load() if isinstance(value, dict): return celtypes.MapType({k: _to_cel(v) for k, v in value.items()}) if isinstance(value, list): @@ -48,6 +67,7 @@ def _from_cel(value: Any) -> Any: so every CEL result is coerced to its native equivalent here — the single choke point for esv assignments, published payloads, and spawn args. """ + _, celtypes, _ = _load() if isinstance(value, celtypes.BoolType): # subclasses int — check before IntType return bool(value) if isinstance(value, (celtypes.IntType, celtypes.UintType)): @@ -67,7 +87,8 @@ def _from_cel(value: Any) -> Any: def evaluate(expr: str, bindings: dict[str, Any]) -> Any: """Evaluate a CEL expression, returning a canonical native/JSON value (§5.1).""" + celpy_mod, _, _ = _load() try: return _from_cel(_program(expr).evaluate(_to_cel(bindings))) - except celpy.CELEvalError as exc: # type: ignore[attr-defined] + except celpy_mod.CELEvalError as exc: raise CelError(str(exc)) from exc diff --git a/src/determa/state/validator.py b/src/determa/state/validator.py index 3668e5d..08ce92f 100644 --- a/src/determa/state/validator.py +++ b/src/determa/state/validator.py @@ -26,8 +26,6 @@ from pathlib import Path from typing import Any, cast -import jsonschema - from .errors import ErrorRecord, ValidationError RESERVED_NAMES = frozenset({"top", "id", "parent", "event"}) @@ -67,6 +65,8 @@ def collect_errors(doc: dict[str, Any]) -> list[ErrorRecord]: def _structural_errors(doc: dict[str, Any]) -> list[ErrorRecord]: + import jsonschema # deferred: ~40ms to import, only needed when validating a machine + validator = jsonschema.Draft202012Validator(schema()) out: list[ErrorRecord] = [] for err in sorted(validator.iter_errors(doc), key=lambda e: list(e.absolute_path)):