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, ...). guildId is optional.
  • Per-guild activity - when a handler passes guildId to recordEvent, that guild's tally increments too, separately from the by-name counters above. topGuildsByActivity(limit) returns guild IDs ranked busiest-first. Not part of getMetrics()'s own snapshot below - it feeds frame.getEngineReport() instead, powering the owner-only !gf engine dashboard's Servers page. See docs/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 around punishmentEngine.execute and groqClient.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.