DOCS/PERFORMANCE.MD
Performance
GlassFrame Protocol is built to stay fast under exactly the conditions that make speed hardest: a raid, a nuke attempt, or a wave of borderline messages all create a burst of work at the same moment. Two small pieces exist specifically for that.
EventQueue - bounded concurrency
src/core/EventQueue.js is a plain async queue with a concurrency cap.
Every actual Discord mutation PunishmentEngine performs (ban/kick/timeout/
role removal) and every outbound Groq request GroqClient makes goes
through one of two queues instead of firing directly:
frame.actionQueue- Discord mutations, default concurrency 4 (config.performance.actionConcurrency).frame.aiQueue- Groq requests, default concurrency 3 (config.performance.aiConcurrency).
Nothing is ever dropped - tasks queue instead of running unbounded. This
matters most during a burst: twenty raid removals firing at once is exactly
what trips Discord's per-route rate limit and makes every request slower,
including the ones that matter. Capping concurrency keeps steady throughput
instead of a stall-then-flood pattern. GroqClient's per-request timeout
clock only starts once a request actually leaves the queue, so a busy
moment never produces a false "the AI didn't respond in time" the way it
would if the timer started at the moment of the API call being requested.
Both queues expose getStats() (active, pending, completed, failed,
concurrency), surfaced through frame.getMetrics() and !gf metrics.
PerformanceMonitor - lightweight internal metrics
src/core/PerformanceMonitor.js tracks three things, all in memory,
neither requiring a dependency:
- Event counts -
recordEvent(name, guildId)increments a counter. Every layer's main handler calls this once per event it processes (basicSecurity.messageCreate,antiRaid.guildMemberAdd,antiNuke.auditEntry.<type>,groqClient.classify, ...).guildIdis optional. - Per-guild activity - when a handler passes
guildIdtorecordEvent, that guild's tally increments too, separately from the by-name counters above.topGuildsByActivity(limit)returns guild IDs ranked busiest-first. Not part ofgetMetrics()'s own snapshot below - it feedsframe.getEngineReport()instead, powering the owner-only!gf enginedashboard's Servers page. Seedocs/ENGINE.md. - Latency samples -
time(name)returns a function; call it when the unit of work finishes. The last 50 samples per name are kept and averaged. Used aroundpunishmentEngine.executeandgroqClient.classify, the two places most likely to be slow (Discord API round trips, network calls).
frame.getMetrics() returns all of this plus every cache's stats in one
snapshot:
{
performance: { uptimeMs, events: { "basicSecurity.messageCreate": 412, ... }, avgLatencyMs: { "groqClient.classify": 340.2, ... } },
queues: { actions: { active, pending, completed, failed, concurrency }, ai: { ... } },
caches: { threatScores: { size, hits, misses, evictions }, openCases: { ... }, groqVerdicts: { ... } }
}
!gf metrics renders this same bot-wide snapshot as a Components V2
message when switched to the "Global" view. Neither EventQueue nor
PerformanceMonitor phones out anywhere - everything here stays in the
process.
frame.getGuildMetrics(guildId) returns a smaller, per-server shape
instead - { status, whitelistSize, flaggedMembers, openCases } - using
Cache.countByPrefix() against the same shared threat-score and case
caches (keyed guildId:userId) rather than tracking a separate per-guild
counter. !gf metrics shows this per-server view by default, with a
button on the message to switch to the bot-wide snapshot above and back -
see Commands - The metrics button.