Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | 31x 31x 31x 31x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 202000x 202000x 202000x 650x 650x 201350x 201350x 201350x 2x 2x 201348x 1x 201348x 1x 202000x 202000x 202000x 3x 3x 3x | import { Logger } from "@shared/logger";
/** Sustained runtime text lines per second that reach the client. */
export const RUNTIME_TEXT_LINES_PER_SECOND = 50;
/** Lines a run may emit at once before the sustained rate applies. */
export const RUNTIME_TEXT_LINE_BURST = 200;
/** A run that keeps dropping lines reports the count at most this often. */
export const RUNTIME_TEXT_DROP_LOG_INTERVAL_MS = 10_000;
/** Sent to the client once per run, when the first line is dropped. */
export const RUNTIME_TEXT_LIMIT_NOTICE =
`Runtime output rate limit exceeded (${RUNTIME_TEXT_LINES_PER_SECOND} lines/s); further lines are dropped`;
export type RuntimeTextDecision = "forward" | "notice" | "drop";
interface RuntimeTextLimiterOptions {
linesPerSecond?: number;
burst?: number;
logIntervalMs?: number;
now?: () => number;
logger?: Pick<Logger, "warn">;
}
/**
* Budget of one run for runtime text lines: stdout/stderr output that is neither
* a simulator protocol marker nor serial output. Sketch code controls these
* lines and can write them far faster than any client reads them, so a token
* bucket admits a burst and a sustained rate; every further line is dropped and
* counted. The client is told once, the log only aggregated.
*/
export class RuntimeTextLimiter {
private readonly linesPerSecond: number;
private readonly burst: number;
private readonly logIntervalMs: number;
private readonly now: () => number;
private readonly logger: Pick<Logger, "warn">;
private tokens: number;
private refilledAt: number;
private dropped = 0;
private droppedSinceLog = 0;
private loggedAt = 0;
constructor(options: RuntimeTextLimiterOptions = {}) {
this.linesPerSecond = options.linesPerSecond ?? RUNTIME_TEXT_LINES_PER_SECOND;
this.burst = options.burst ?? RUNTIME_TEXT_LINE_BURST;
this.logIntervalMs = options.logIntervalMs ?? RUNTIME_TEXT_DROP_LOG_INTERVAL_MS;
this.now = options.now ?? Date.now;
this.logger = options.logger ?? new Logger("RuntimeTextLimiter");
this.tokens = this.burst;
this.refilledAt = this.now();
}
/** Decides one line: forward it, replace it by the one-time notice, or drop it. */
admit(): RuntimeTextDecision {
const now = this.now();
this.refill(now);
if (this.tokens >= 1) {
this.tokens -= 1;
return "forward";
}
this.dropped += 1;
this.droppedSinceLog += 1;
if (this.dropped === 1) {
this.logDrops(now, "Runtime output rate limit exceeded; dropping excess lines of this run");
return "notice";
}
if (now - this.loggedAt >= this.logIntervalMs) {
this.logDrops(now, "Runtime output still exceeds its rate limit");
}
return "drop";
}
get droppedLines(): number {
return this.dropped;
}
private refill(now: number): void {
const elapsedMs = Math.max(0, now - this.refilledAt);
this.refilledAt = now;
this.tokens = Math.min(this.burst, this.tokens + (elapsedMs * this.linesPerSecond) / 1_000);
}
private logDrops(now: number, message: string): void {
this.logger.warn(`${message} (${this.droppedSinceLog} dropped since last report, ${this.dropped} total)`);
this.loggedAt = now;
this.droppedSinceLog = 0;
}
}
|