#!/usr/bin/env python3 # Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Measure what one text reply costs the bot's own code, against the live bot. tool/latency.py testbot # 20 x !pomoc, judged at 100 ms p95 tool/latency.py testbot --text '!ja' --times 50 tool/latency.py testbot --budget 50 Posts for real into the named group, which must be in SELFTEST_GROUPS -- the same fail-closed rule as scripts/selftest.sh. The command's cooldown is switched off for the run and put back afterwards. Exit 1 when the 95th percentile of the bot's own time is over budget; see signal_bot/latency.py for what "own" excludes and why. """ from __future__ import annotations import argparse import asyncio import os import sys from typing import Final import httpx from dotenv import load_dotenv from signal_bot.app import HTTP_TIMEOUT_SECONDS, USER_AGENT from signal_bot.latency import LatencyError, measure from signal_bot.served import OWN_BUDGET_MS EXIT_OVER_BUDGET: Final = 1 EXIT_REFUSED: Final = 2 DEFAULT_TIMES: Final = 20 async def run(group: str, text: str, times: int, budget_ms: int, base: str) -> int: """One measured run; the exit code is the verdict.""" async with httpx.AsyncClient( timeout=HTTP_TIMEOUT_SECONDS, headers={"User-Agent": USER_AGENT} ) as client: report = await measure(client, base, group, text, times, budget_ms) sys.stdout.write(report.render() + "\n") return 0 if report.within_budget else EXIT_OVER_BUDGET def main() -> int: """Parse arguments and run one measurement.""" parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument("group", help="test group name; must be in SELFTEST_GROUPS") parser.add_argument("--text", default="!pomoc", help="the command to type (default: !pomoc)") parser.add_argument("--times", type=int, default=DEFAULT_TIMES, help="replies to sample") parser.add_argument("--budget", type=int, default=OWN_BUDGET_MS, help="p95 own_ms allowed") args = parser.parse_args() load_dotenv() base = f"http://127.0.0.1:{os.environ.get('BIND_PORT', '8137')}" try: return asyncio.run(run(args.group, args.text, args.times, args.budget, base)) except (LatencyError, httpx.HTTPError) as error: sys.stderr.write(f"latency: {error}\n") return EXIT_REFUSED if __name__ == "__main__": raise SystemExit(main())