mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:17:02 +01:00
Bot.listen awaited every frame to completion, so one Commons fetch or one attachment upload held up every text reply behind it (DOCS-latency.md). Each frame now runs in its own task, never more than LIMIT at once; the reader waits for a slot before spawning, so a burst applies back-pressure at the websocket instead of fanning out upstream. One person's commands stay serialised (user_lock.py): two quick !kot would otherwise both pass the cooldown gate before either recorded a use. Every served command reports its timings split into the bot's own time and everybody else's (served.py); /_selftest carries that block and tool/latency.py holds own_ms to 100 ms at p95, with the command's cooldown switched off for the run and put back afterwards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""What one command cost, split into the bot's own time and everybody else's.
|
|
|
|
``DOCS-latency.md`` found 72% of a ``!kot`` reply inside the signal-cli
|
|
container and most of the rest on Commons -- none of it this repository's
|
|
code. So "the bot is slow" needs a number that excludes both, or every
|
|
measurement is a measurement of somebody else. ``own_ms`` is that number:
|
|
the wall clock from the frame being read to the handler returning, minus
|
|
every upstream request and every send. It is what ``/_selftest`` reports
|
|
and what ``tool/latency.py`` holds to a budget.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
# The bot's own share of one reply, at the 95th percentile. The whole
|
|
# reply is bounded by the container, and is reported, not budgeted.
|
|
OWN_BUDGET_MS: Final = 100
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Served:
|
|
"""The timings of one command attempt, in milliseconds."""
|
|
|
|
command: str
|
|
outcome: str
|
|
# Frame read to a slot free: the queue behind other people's frames.
|
|
queue_ms: int
|
|
# Slot to handler: parse, scope, tally, resolve, and this person's lock.
|
|
dispatch_ms: int
|
|
# The handler, wall clock, everything included.
|
|
handler_ms: int
|
|
# Of which: outbound sends to signal-cli...
|
|
send_ms: int
|
|
# ...and requests to Commons, the cat API, and the like.
|
|
upstream_ms: int
|
|
|
|
@property
|
|
def own_ms(self) -> int:
|
|
"""Time spent in this repository's code, and nowhere else."""
|
|
return self.queue_ms + self.dispatch_ms + self.handler_ms - self.send_ms - self.upstream_ms
|
|
|
|
def as_dict(self) -> dict[str, int | str]:
|
|
"""The shape ``/_selftest`` answers with."""
|
|
return {
|
|
"command": self.command,
|
|
"outcome": self.outcome,
|
|
"queue_ms": self.queue_ms,
|
|
"dispatch_ms": self.dispatch_ms,
|
|
"handler_ms": self.handler_ms,
|
|
"send_ms": self.send_ms,
|
|
"upstream_ms": self.upstream_ms,
|
|
"own_ms": self.own_ms,
|
|
}
|