mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
`hermes --cpu-profile PATH ...` profiles the process from argument parsing to exit and writes PATH (pstats) plus PATH.txt (top 80 functions by cumulative time). A relative PATH goes under the active profile's logs/profiles/. - Enabled in _prepare_agent_startup (every parsed launch: fast chat path, full parser, -z) plus the Termux TUI fast path, which skips it. - -z/--oneshot exits through os._exit (atexit skipped on purpose), so _exit_after_oneshot writes the profile explicitly. - No env var: the flag is the only switch. - main.py stays at its size on main: _dashboard_validate_serve_args moved to main_dashboard.py to offset the added lines.
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""``hermes --cpu-profile PATH``: process-wide cProfile, written when the process exits.
|
|
|
|
Enabled once after argument parsing and written from ``atexit``, so short commands and
|
|
long-running ones (``gateway run``) are covered without wrapping every handler. The one-shot
|
|
(``-z``) exit skips ``atexit`` on purpose (``main._exit_after_oneshot``) and calls
|
|
:func:`write_cpu_profile` itself. A relative PATH lands in the active profile's
|
|
``logs/profiles/``. Profiles cover THIS process only: under ``--tui`` that is the launcher,
|
|
while the agent runs in the TUI's own backend process.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import atexit
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_profiler = None
|
|
_profile_path: Path | None = None
|
|
|
|
|
|
def resolve_profile_path(path: str | os.PathLike[str]) -> Path:
|
|
"""Absolute (or ``~``) paths as given; relative ones under ``<hermes home>/logs/profiles``."""
|
|
target = Path(path).expanduser()
|
|
if not target.is_absolute():
|
|
from hermes_constants import get_hermes_home
|
|
|
|
target = get_hermes_home() / "logs" / "profiles" / target
|
|
return target.resolve()
|
|
|
|
|
|
def enable_cpu_profile(path: str | os.PathLike[str] | None) -> Path | None:
|
|
"""Start profiling to ``path``; idempotent so the fast-path → full-parser handoff can't
|
|
register a second writer. Returns the resolved path, or None when ``path`` is empty."""
|
|
global _profiler, _profile_path
|
|
if not path:
|
|
return None
|
|
if _profiler is not None:
|
|
return _profile_path
|
|
import cProfile
|
|
|
|
target = resolve_profile_path(path)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
_profiler, _profile_path = cProfile.Profile(), target
|
|
_profiler.enable()
|
|
atexit.register(write_cpu_profile)
|
|
return target
|
|
|
|
|
|
def enable_cpu_profile_from_args(args) -> None:
|
|
"""Honour ``--cpu-profile`` on a parsed CLI namespace and say where the profile goes."""
|
|
enabled = enable_cpu_profile(getattr(args, "cpu_profile", None))
|
|
if enabled is not None:
|
|
print(f"CPU profiling enabled: {enabled}", file=sys.stderr)
|
|
|
|
|
|
def write_cpu_profile() -> None:
|
|
"""Stop profiling and write ``PATH`` (pstats) plus ``PATH.txt`` (top 80 by cumulative time)."""
|
|
global _profiler
|
|
profiler, target = _profiler, _profile_path
|
|
if profiler is None or target is None:
|
|
return
|
|
_profiler = None
|
|
profiler.disable()
|
|
import pstats
|
|
|
|
try:
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
profiler.dump_stats(str(target))
|
|
with target.with_suffix(target.suffix + ".txt").open("w", encoding="utf-8") as fh:
|
|
pstats.Stats(profiler, stream=fh).sort_stats("cumulative").print_stats(80)
|
|
except OSError:
|
|
logger.exception("Failed to write CPU profile: %s", target)
|