Skip to content

Commit 99ce7c1

Browse files
committed
docs(ui): publish a capacity & throughput operator doc
No user-facing doc surfaced capacity/throughput/concurrency numbers -- they existed only in scattered code comments and the iterate-loop load-test harness output. Adds /docs/capacity as the throughput/concurrency counterpart to the existing /docs/ams-sizing (CPU/RAM/disk), scoped strictly to the numbers and where each comes from: - AMS iterate-loop orchestration throughput, from the committed load-test harness (packages/loopover-engine/docs/iterate-loop-load-test.md, #4913's tooling). - Review-gate PR-processing concurrency caps (SWEEP_MAX_PRS/ISSUE_WAKE_MAX_PRS/MERGE_WAKE_MAX_PRS from src/settings/agent-sweep.ts) and Cloudflare Queue consumer bounds (max_batch_size/ max_concurrency from wrangler.jsonc), both verified against their literal source values. Closes #4914
1 parent 2372739 commit 99ce7c1

6 files changed

Lines changed: 170 additions & 1 deletion

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: Capacity and throughput
3+
description: Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing queue, so an operator can plan for load instead of guessing.
4+
---
5+
6+
Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing
7+
queue — the counterpart to [Resource sizing](/docs/ams-sizing) (CPU/RAM/disk), scoped strictly to
8+
throughput/concurrency and where each number comes from.
9+
10+
## AMS: iterate-loop orchestration throughput
11+
12+
From the committed load-test harness (`packages/loopover-engine/docs/iterate-loop-load-test.md`,
13+
built for #4913/#5224), measuring `runIterateLoop`'s own orchestration overhead under concurrent,
14+
multi-tenant-like load with a 15ms simulated per-iteration driver latency:
15+
16+
<FeatureRow
17+
items={[
18+
{ title: "Concurrency 1", description: "63 attempts/sec" },
19+
{ title: "Concurrency 8", description: "422 attempts/sec" },
20+
{ title: "Concurrency 32", description: "982 attempts/sec" },
21+
{ title: "Concurrency 128", description: "1,577 attempts/sec" },
22+
]}
23+
/>
24+
25+
<Callout variant="note">
26+
Absolute numbers are host-dependent — treat these as a rough sense of scale and of how throughput
27+
scales with concurrency, not a hard target. Reproduce them on your own hardware with:
28+
</Callout>
29+
30+
<CodeBlock lang="bash" code={`npm run loadtest:iterate-loop`} />
31+
32+
## Review-gate: PR-processing concurrency caps
33+
34+
Fixed, hardcoded caps from `src/settings/agent-sweep.ts`, sized against a single GitHub App
35+
installation's shared ~5,000-request/hour REST rate-limit bucket (each PR touched costs roughly 9
36+
REST GETs):
37+
38+
<FeatureRow
39+
items={[
40+
{
41+
title: "SWEEP_MAX_PRS = 3",
42+
description: "The recurring sweep's per-tick cap — sized for a sweep that re-runs every ~2 minutes, so a low steady-state budget compounds safely across ticks.",
43+
},
44+
{
45+
title: "ISSUE_WAKE_MAX_PRS = 25",
46+
description: "One-shot budget for an issue-linked wake event — a rarer trigger than a merge, so a larger one-time budget doesn't risk compounding across repeated events in one rate-limit window.",
47+
},
48+
{
49+
title: "MERGE_WAKE_MAX_PRS = 15",
50+
description: "One-shot budget for a merge-triggered wake — sized lower than the issue-wake budget because merges are a far more common trigger; a repeated-merge burst inside one rate-limit window must not compound the way the rarer issue-wake trigger safely can.",
51+
},
52+
]}
53+
/>
54+
55+
## Review-gate: Cloudflare Queue consumer bounds
56+
57+
From `wrangler.jsonc`'s `loopover-jobs` queue consumer — Cloudflare Queues consumer-binding
58+
attributes, read at `wrangler deploy` time (not runtime-configurable via `env.SOMETHING`):
59+
60+
<FeatureRow
61+
items={[
62+
{
63+
title: "max_batch_size = 5",
64+
description: "The most jobs one batch can bundle at once — bounds how many heavy sweep/backfill jobs land in a single delivery.",
65+
},
66+
{
67+
title: "max_concurrency = 3",
68+
description: "Bounded fan-out, sized to drain one heavy installation's worst sweep within the 2-minute cron interval without flooding that installation's own GitHub rate-limit bucket.",
69+
},
70+
]}
71+
/>
72+
73+
<Callout variant="note" title="Re-tuning these for real multi-tenant volume">
74+
`max_batch_size × max_concurrency` bounds the most jobs that can be in flight — each making
75+
GitHub calls — at any one instant, kept comfortably under an installation's rate-limit headroom.
76+
A genuinely multi-tenant volume model needs real production job-volume data (job rate, distinct-
77+
installation count, GitHub rate-limit headroom actually observed) to re-derive against — re-tune
78+
from a live dashboard/`audit_events` query when that data exists, not by guessing a bigger
79+
number.
80+
</Callout>
81+
82+
**Takeaways:**
83+
84+
- Iterate-loop's own orchestration overhead scales roughly linearly with concurrency — the
85+
bottleneck at real scale is each attempt's actual coding-agent driver latency, not the loop's
86+
scheduling/bookkeeping around it.
87+
- The review-gate's PR-processing caps are deliberately asymmetric across trigger type
88+
(`SWEEP_MAX_PRS` \< `MERGE_WAKE_MAX_PRS` \< `ISSUE_WAKE_MAX_PRS`) because each trigger recurs at a
89+
different rate — a budget sized for a rare trigger would compound dangerously if applied to a
90+
frequent one.
91+
- Every number above was measured or verified directly against the source it's read from (the
92+
committed load-test harness, or the literal constant/config value) — none are estimates.
93+
94+
See [Resource sizing](/docs/ams-sizing) for CPU/RAM/disk numbers, and the [AMS Cloud Readiness
95+
milestone](https://github.com/JSONbored/loopover/milestone/28) for the per-tenant scheduling and
96+
queue-fairness design work these numbers feed into.

apps/loopover-ui/src/components/site/docs-nav.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const docsNav: DocsGroup[] = [
5353
items: [
5454
{ to: "/docs/self-hosting-operations", label: "Operations" },
5555
{ to: "/docs/self-hosting-backup-scaling", label: "Backup & scaling" },
56+
{ to: "/docs/capacity", label: "Capacity & throughput" },
5657
{ to: "/docs/self-hosting-troubleshooting", label: "Troubleshooting" },
5758
],
5859
},

apps/loopover-ui/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { Route as DocsHowReviewsWorkRouteImport } from './routes/docs.how-review
5757
import { Route as DocsGithubAppRouteImport } from './routes/docs.github-app'
5858
import { Route as DocsFumadocsSpikeApiReferenceRouteImport } from './routes/docs.fumadocs-spike-api-reference'
5959
import { Route as DocsFederatedFleetIntelligenceRouteImport } from './routes/docs.federated-fleet-intelligence'
60+
import { Route as DocsCapacityRouteImport } from './routes/docs.capacity'
6061
import { Route as DocsBranchAnalysisRouteImport } from './routes/docs.branch-analysis'
6162
import { Route as DocsBetaOnboardingRouteImport } from './routes/docs.beta-onboarding'
6263
import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.ams-unattended-scheduling'
@@ -341,6 +342,11 @@ const DocsFederatedFleetIntelligenceRoute =
341342
path: '/federated-fleet-intelligence',
342343
getParentRoute: () => DocsRoute,
343344
} as any)
345+
const DocsCapacityRoute = DocsCapacityRouteImport.update({
346+
id: '/capacity',
347+
path: '/capacity',
348+
getParentRoute: () => DocsRoute,
349+
} as any)
344350
const DocsBranchAnalysisRoute = DocsBranchAnalysisRouteImport.update({
345351
id: '/branch-analysis',
346352
path: '/branch-analysis',
@@ -523,6 +529,7 @@ export interface FileRoutesByFullPath {
523529
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
524530
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
525531
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
532+
'/docs/capacity': typeof DocsCapacityRoute
526533
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
527534
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
528535
'/docs/github-app': typeof DocsGithubAppRoute
@@ -598,6 +605,7 @@ export interface FileRoutesByTo {
598605
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
599606
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
600607
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
608+
'/docs/capacity': typeof DocsCapacityRoute
601609
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
602610
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
603611
'/docs/github-app': typeof DocsGithubAppRoute
@@ -677,6 +685,7 @@ export interface FileRoutesById {
677685
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
678686
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
679687
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
688+
'/docs/capacity': typeof DocsCapacityRoute
680689
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
681690
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
682691
'/docs/github-app': typeof DocsGithubAppRoute
@@ -757,6 +766,7 @@ export interface FileRouteTypes {
757766
| '/docs/ams-unattended-scheduling'
758767
| '/docs/beta-onboarding'
759768
| '/docs/branch-analysis'
769+
| '/docs/capacity'
760770
| '/docs/federated-fleet-intelligence'
761771
| '/docs/fumadocs-spike-api-reference'
762772
| '/docs/github-app'
@@ -832,6 +842,7 @@ export interface FileRouteTypes {
832842
| '/docs/ams-unattended-scheduling'
833843
| '/docs/beta-onboarding'
834844
| '/docs/branch-analysis'
845+
| '/docs/capacity'
835846
| '/docs/federated-fleet-intelligence'
836847
| '/docs/fumadocs-spike-api-reference'
837848
| '/docs/github-app'
@@ -910,6 +921,7 @@ export interface FileRouteTypes {
910921
| '/docs/ams-unattended-scheduling'
911922
| '/docs/beta-onboarding'
912923
| '/docs/branch-analysis'
924+
| '/docs/capacity'
913925
| '/docs/federated-fleet-intelligence'
914926
| '/docs/fumadocs-spike-api-reference'
915927
| '/docs/github-app'
@@ -1303,6 +1315,13 @@ declare module '@tanstack/react-router' {
13031315
preLoaderRoute: typeof DocsFederatedFleetIntelligenceRouteImport
13041316
parentRoute: typeof DocsRoute
13051317
}
1318+
'/docs/capacity': {
1319+
id: '/docs/capacity'
1320+
path: '/capacity'
1321+
fullPath: '/docs/capacity'
1322+
preLoaderRoute: typeof DocsCapacityRouteImport
1323+
parentRoute: typeof DocsRoute
1324+
}
13061325
'/docs/branch-analysis': {
13071326
id: '/docs/branch-analysis'
13081327
path: '/branch-analysis'
@@ -1564,6 +1583,7 @@ interface DocsRouteChildren {
15641583
DocsAmsUnattendedSchedulingRoute: typeof DocsAmsUnattendedSchedulingRoute
15651584
DocsBetaOnboardingRoute: typeof DocsBetaOnboardingRoute
15661585
DocsBranchAnalysisRoute: typeof DocsBranchAnalysisRoute
1586+
DocsCapacityRoute: typeof DocsCapacityRoute
15671587
DocsFederatedFleetIntelligenceRoute: typeof DocsFederatedFleetIntelligenceRoute
15681588
DocsFumadocsSpikeApiReferenceRoute: typeof DocsFumadocsSpikeApiReferenceRoute
15691589
DocsGithubAppRoute: typeof DocsGithubAppRoute
@@ -1615,6 +1635,7 @@ const DocsRouteChildren: DocsRouteChildren = {
16151635
DocsAmsUnattendedSchedulingRoute: DocsAmsUnattendedSchedulingRoute,
16161636
DocsBetaOnboardingRoute: DocsBetaOnboardingRoute,
16171637
DocsBranchAnalysisRoute: DocsBranchAnalysisRoute,
1638+
DocsCapacityRoute: DocsCapacityRoute,
16181639
DocsFederatedFleetIntelligenceRoute: DocsFederatedFleetIntelligenceRoute,
16191640
DocsFumadocsSpikeApiReferenceRoute: DocsFumadocsSpikeApiReferenceRoute,
16201641
DocsGithubAppRoute: DocsGithubAppRoute,

apps/loopover-ui/src/routes/docs-routes-loading-state.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("docs route Suspense fallback (#6982)", () => {
4545
"docs.tsx",
4646
]);
4747
const inScope = docsRouteFiles.filter((name) => !outOfScope.has(name));
48-
expect(inScope.length).toBe(46);
48+
expect(inScope.length).toBe(47);
4949

5050
for (const file of inScope) {
5151
const source = readFileSync(join(routesDir, file), "utf8");
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createFileRoute, notFound } from "@tanstack/react-router";
2+
import { Suspense } from "react";
3+
4+
import { DocsPage } from "@/components/site/docs-page";
5+
import { LoadingState } from "@/components/site/state-views";
6+
import { docsClientLoader } from "@/lib/docs-client-loader";
7+
8+
// Rendered from content/docs/capacity.mdx via fumadocs-mdx's browser entry
9+
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
10+
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
11+
// for why the loader below resolves only a plain, serializable path string.
12+
export const Route = createFileRoute("/docs/capacity")({
13+
loader: async () => {
14+
const { docsSource } = await import("@/lib/docs-source");
15+
const page = docsSource.getPage(["capacity"]);
16+
if (!page) throw notFound();
17+
return { path: page.path, title: page.data.title, description: page.data.description };
18+
},
19+
head: () => ({
20+
meta: [
21+
{ title: "Capacity and throughput — LoopOver docs" },
22+
{
23+
name: "description",
24+
content:
25+
"Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing queue, so an operator can plan for load instead of guessing.",
26+
},
27+
{ property: "og:title", content: "Capacity and throughput — LoopOver docs" },
28+
{
29+
property: "og:description",
30+
content:
31+
"Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing queue, so an operator can plan for load instead of guessing.",
32+
},
33+
{ property: "og:url", content: "/docs/capacity" },
34+
],
35+
links: [{ rel: "canonical", href: "/docs/capacity" }],
36+
}),
37+
component: Capacity,
38+
});
39+
40+
function Capacity() {
41+
const { path, title, description } = Route.useLoaderData();
42+
const Content = docsClientLoader.getComponent(path);
43+
return (
44+
<DocsPage eyebrow="Maintainers" title={title} description={description}>
45+
<Suspense fallback={<LoadingState />}>
46+
<Content />
47+
</Suspense>
48+
</DocsPage>
49+
);
50+
}

apps/loopover-ui/src/routes/docs.index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const AUDIENCES: Audience[] = [
7979
{ to: "/docs/ams-observability", label: "Observing your miner" },
8080
{ to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
8181
{ to: "/docs/ams-sizing", label: "Resource sizing" },
82+
{ to: "/docs/capacity", label: "Capacity & throughput" },
8283
{ to: "/docs/ams-config-precedence", label: "Config precedence" },
8384
{ to: "/docs/ams-env-reference", label: "Env var reference" },
8485
{ to: "/docs/ams-discovery-plane", label: "Discovery plane (opt-in)" },

0 commit comments

Comments
 (0)