playbook/antigravity-awesome-skills/skills/vercel-optimize/lib/gates/slow-route.mjs

89 lines
4.3 KiB
JavaScript

// Primary threshold (p95>500 AND inv>=1400) WHY: 1.4k/14d is the floor where p95 stabilizes statistically
// and a 3-5x performance win still pays for engineering time. Secondary (p95>1500 AND inv>=250) catches "catastrophically
// slow at any volume" — usually a broken sync call or cold-start chain the customer wants to know about.
//
// 5xx disqualifier: when error rate >50% the route is failing, not slow — latency reflects crash time,
// not work time. route_errors covers it independently; we disqualify here so budget isn't spent on a
// sub-agent that will correctly abstain.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
const ERROR_RATE_DISQUALIFY_THRESHOLD = 0.5;
export const metadata = {
id: 'slow_route',
threshold: '(p95 > 500 AND inv >= 1400) OR (p95 > 1500 AND inv >= 250); disqualified when 5xx rate > 50%; Vercel Workflow runtime endpoints are hard-gated',
billingDimension: 'function-duration',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Routes with p95 function duration above 500ms at meaningful traffic (>=1,400 invocations in window), OR catastrophically slow routes (>1500ms p95 at any volume >=250). High duration drives both function-duration cost and user-perceived latency. Investigate sequential awaits, slow external APIs, missing caching, N+1 patterns. Routes with >50% 5xx rate are disqualified — those are reliability problems, not performance tuning targets, and surface via route_errors instead. Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`) are hard-gated before launch because long-running step/flow requests are expected orchestration, not app-route bottlenecks.',
};
export function gate(signals) {
const routes = extractFunctionRoutes(signals);
const errorRates = extractErrorRatesByRoute(signals);
return routes
.filter((r) => (r.p95Ms > 500 && r.invocations >= 1400) || (r.p95Ms > 1500 && r.invocations >= 250))
.map((r) => {
const errorRate = errorRates.get(r.route);
const candidate = {
kind: metadata.id,
scope: 'route',
route: r.route,
files: [],
priority: Math.round(r.p95Ms * Math.max(r.invocations, 1) / 1000),
confidence: 0.94,
o11ySignal: `inv=${r.invocations},p95=${r.p95Ms}ms${errorRate != null ? `,5xx=${(errorRate * 100).toFixed(0)}%` : ''}`,
reason: 'slow high-traffic route',
question: `What is the concrete bottleneck in ${r.route} (p95=${r.p95Ms}ms over ${r.invocations} invocations), and which file-level change would reduce it?`,
evidence: { metric: 'fnDurationP95ByRoute', route: r.route, p95Ms: r.p95Ms, invocations: r.invocations, errorRate },
};
if (errorRate != null && errorRate > ERROR_RATE_DISQUALIFY_THRESHOLD) {
candidate.disqualified = true;
candidate.disqualifyReason = `high error rate (${(errorRate * 100).toFixed(0)}% 5xx — reliability issue, not performance; covered by route_errors gate)`;
}
return withRouteShapeWarnings(candidate, signals);
});
}
// Routes without status data are absent from the map → gate falls back to "no disqualification".
function extractErrorRatesByRoute(signals) {
const m = signals.metrics?.fnStatusByRoute;
const out = new Map();
if (!Array.isArray(m?.rows)) return out;
const perRoute = new Map();
for (const row of m.rows) {
if (!row?.route) continue;
const v = row.value ?? 0;
const prior = perRoute.get(row.route) ?? { errors5xx: 0, total: 0 };
if (/^5/.test(String(row.http_status ?? ''))) prior.errors5xx += v;
prior.total += v;
perRoute.set(row.route, prior);
}
for (const [route, r] of perRoute) {
if (r.total > 0) out.set(route, r.errors5xx / r.total);
}
return out;
}
function extractFunctionRoutes(signals) {
const dur = signals.metrics?.fnDurationP95ByRoute;
if (!dur?.ok && !Array.isArray(dur?.rows)) return [];
const req = signals.metrics?.requestsByRouteCache;
const invByRoute = new Map();
for (const row of (req?.rows ?? [])) {
if (!row.route) continue;
invByRoute.set(row.route, (invByRoute.get(row.route) ?? 0) + (row.value ?? 0));
}
return (dur?.rows ?? [])
.filter((r) => r.route)
.map((r) => ({
route: r.route,
p95Ms: Math.round(r.value ?? 0),
invocations: invByRoute.get(r.route) ?? 0,
}));
}