docs(quick-261001-qoa): add a docs page on performance and scaling compared to PHP/WinterCMS

This commit is contained in:
Jakub Zych
2026-10-01 19:21:34 +02:00
parent 4103d95a1a
commit ecdef31c38
3 changed files with 275 additions and 1 deletions

View File

@@ -0,0 +1,200 @@
---
phase: quick-261001-qoa
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
# summercms.go (framework) docs - one commit at the end of Task 2
- docs/architecture/performance-and-scaling.md
- docs/index.md
- docs/setup/coming-from-wintercms.md
autonomous: true
requirements: [QUICK-261001-qoa]
estimate:
tokens: 60000
raw_tokens: 60000
tasks: 2
confidence: low
must_haves:
truths:
- "A guide page docs/architecture/performance-and-scaling.md exists with front matter title 'Performance and scaling', section architecture, order 50, and summer docs:build publishes it as architecture/performance-and-scaling.html, last in the Architecture sidebar after Request lifecycle"
- "The page explains, for a developer coming from WinterCMS/OctoberCMS, the per-request PHP-FPM boot versus SummerCMS booting once (party lifecycle, cabana compiling YAML at boot, surf assembling the ServeMux), memory per FPM worker versus one Go process, slow-I/O concurrency (pm.max_children exhaustion versus goroutines), the shared GORM+River connection pool, and Eloquent hydration versus GORM"
- "The expectations table (trivial/cached 10-30x per core and sub-millisecond, typical CRUD 3-10x, DB-heavy 1.2-2x with Postgres dominating) sits under a NOTE callout stating these are typical ranges for PHP-FPM with Laravel versus Go services and not measurements of SummerCMS, which has no benchmarks yet"
- "The caveats are accurate to the code: slow queries and N+1 port over unchanged; wire keeps response bodies byte-compatible; the throttle middleware that serve builds always uses surf.MemoryStore (surf.Store is the interface, but serve offers no way to supply another store), so N replicas multiply the effective limit; bouncer.NewPostgresBlacklist shares revocations across replicas; serve runs the conga worker in-process unless queue.work_in_serve is false, and queue:work runs workers separately; thumbnails use a pure-Go imaging library; GC pauses are sub-millisecond"
- "The scaling section covers the single stateless binary, vertical scaling without worker tuning, horizontal replicas with Postgres (and an uploads bucket every replica reaches) as shared state, and Centrifugo scaling separately from the lighthouse publisher; the measuring section uses tide (summer parity:record / parity:replay) plus a load tool such as vegeta or k6 on identical hardware and Postgres, comparing per-route p50/p99, requests per second and RSS"
- "docs/index.md and docs/setup/coming-from-wintercms.md link to the new page"
- "go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' and go run ./cmd/summer docs:build --check pass (identifiers, links, anchors, command names, consuming-application names, headings, callouts), go vet ./... is green, and the change lands as one docs commit with no co-author or session trailer"
artifacts:
- path: "docs/architecture/performance-and-scaling.md"
provides: "Performance and scaling guide page (architecture section, order 50)"
contains: "# Performance and scaling"
- path: "docs/index.md"
provides: "Architecture bullet in Where to start links the new page"
contains: "architecture/performance-and-scaling.md"
- path: "docs/setup/coming-from-wintercms.md"
provides: "Intro pointer from the WinterCMS concept map to the new page"
contains: "../architecture/performance-and-scaling.md"
key_links:
- from: "docs/architecture/performance-and-scaling.md front matter"
to: "docs/site.yaml sections (architecture)"
via: "section: architecture must match the directory and a listed section; order 50 must be unique in the section"
pattern: "^section: architecture$"
- from: "docs/architecture/performance-and-scaling.md code spans"
to: "modules/surf, modules/bouncer, modules/lagoon, modules/cabana, modules/party"
via: "internal/docsite identifier checker (pkg.Ident spans resolved against the module packages, go doc -c fallback)"
pattern: "surf\\.MemoryStore"
- from: "docs/architecture/performance-and-scaling.md links"
to: "docs/services/rate-limiting.md, jobs.md, authentication.md, parity-testing.md, realtime.md, storage.md, docs/architecture/application-lifecycle.md"
via: "internal/docsite link and anchor checker"
pattern: "\\]\\(\\.\\./services/"
---
<objective>
Add one explanation page to the SummerCMS docs, "Performance and scaling", for developers coming from WinterCMS/OctoberCMS who are evaluating SummerCMS. It explains why a compiled SummerCMS binary is faster and lighter than WinterCMS on PHP-FPM, sets honest expectations by endpoint shape, lists where the port will not win (with caveats that match the code as it is today), describes how to scale and operate the binary, and shows how to measure the difference with the framework's own parity tooling.
Purpose: the user approved the content analysis in conversation (items 1-9 in the planning context); this plan turns it into a page that passes the docs checker and matches the existing guide pages.
Output: docs/architecture/performance-and-scaling.md, a link from docs/index.md and from docs/setup/coming-from-wintercms.md, one docs commit.
Placement decision (planner's discretion): the page goes in the Architecture section, order 50, after Request lifecycle (order 40). It is an explanation of the process model, which is what that section covers (introduction, Go modules, application lifecycle, request lifecycle); Setup holds procedures. Pages are discovered by walking docs/ (internal/docsite/load.go walkPages), the sidebar is ordered by site.yaml section order then front matter `order`, so no site.yaml change is needed.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@CLAUDE.md
@docs/site.yaml
@docs/index.md
@docs/architecture/application-lifecycle.md
@docs/architecture/request-lifecycle.md
@docs/setup/coming-from-wintercms.md
@docs/services/rate-limiting.md
@docs/services/jobs.md
@docs/services/parity-testing.md
Verified facts the page relies on (planner checked these against the code on 2026-10-01; the executor re-checks every identifier before writing):
- Front matter rules (internal/docsite/load.go): fields title, description, section, order; section must equal the directory and be listed in docs/site.yaml; the first body line must be exactly "# " + title; description at most 160 characters; order unique within the section. Architecture currently uses orders 10, 20, 30, 40.
- Policy (internal/docsite/check_policy.go): headings in docs/ pages are plain ASCII with no links, code spans or raw HTML; callouts are only NOTE, TIP or WARNING (written as a blockquote whose first line is [!NOTE] etc.); every Go-lexer fence needs a src= reference, so the page carries no Go code fences. YAML and sh fences are allowed without src=.
- Commands (internal/docsite/check_commands.go): every `summer <cmd>` and `./bin/<app> <cmd>` in sh/shell/bash/console fences and in code spans must be a real command. Real ones used here: `serve`, `queue:work`, `migrate`, `route:list` (application binary) and `parity:record`, `parity:replay` (summer tool). Other programs (vegeta, k6) are not checked.
- Consuming-application names are rejected by internal/docsite/check_forbidden.go (case-insensitive pattern in that file). Say "the application" or "host application"; use `acme` / `blog` as example names, as the other pages do.
- `surf.MemoryStore` (in-process, mutex-guarded store), `surf.Store` (interface with Attempt), `surf.NewFixedWindowLimiter(store surf.Store, trusted)` exist. BUT surf.BuildRouter (modules/surf/router.go around line 444) always constructs the throttle limiter with NewMemoryStore; neither `serve` nor any option lets an application supply a different store today. docs/services/rate-limiting.md already states that counters live per instance and the effective limit is the configured limit times the number of instances.
- `bouncer.PostgresBlacklist`, `bouncer.NewPostgresBlacklist`, `bouncer.NewMemoryBlacklist`, `bouncer.BlacklistStore` exist; docs/services/authentication.md section "Refreshing and revoking" documents them.
- `queue.work_in_serve` is read in modules/conga/client.go (default true, overridden when the key is set); docs/services/jobs.md section "Running workers" documents it and `queue:work`. The worker listens on one dedicated Postgres connection opened from `database.dsn` (PgBouncer needs session pooling for it).
- `lagoon.Publish(app, sqlDB, gdb)` stores the one shared `*sql.DB` pool and the GORM handle on the app; conga builds River on that same `*sql.DB` (riverdatabasesql). lagoon exposes NO pool-size or connection-limit config key; the only database key is `database.dsn`. Do not name or invent one.
- `party.Activate`, `cabana.CompiledController` (YAML compiled at boot), `surf.Assemble` (registers routes and compiles the ServeMux) exist; docs/architecture/application-lifecycle.md has the anchor #start-up-sequence.
- wire keeps API bodies byte-compatible with a PHP backend (`wire.WriteJSON`, `wire.Time`, `wire.TriBool`, `wire.Slice` exist).
- Thumbnails (`attach.File.Thumb`, modules/lagoon/attach/thumb.go) are produced with the pure-Go github.com/disintegration/imaging library, not GD/Imagick.
- Uploads: the framework registers only the file:// and mem:// gocloud.dev bucket drivers (modules/lagoon/attach/bucket.go); docs/services/storage.md documents only those two. With several replicas a file:// bucket must sit on storage every replica mounts. Do not promise S3/GCS support.
- tide records and replays parity fixtures; `summer parity:record --spec ... --target ... --output ... --vars ...` and `summer parity:replay --fixtures ... --target ... --vars ...` are the documented forms (docs/services/parity-testing.md, section "The parity commands").
- Realtime: lighthouse publishes to Centrifugo through its Centrifugo driver (docs/services/realtime.md, section "The Centrifugo driver"); the Centrifugo server runs separately.
- Style of existing pages: plain declarative prose, short paragraphs, second person, WinterCMS/Laravel comparison first, British spelling (organised, behaviour), module names linked to their README on first mention (for example [surf](../../modules/surf/README.md)), example binary ./bin/acme. No emojis, no marketing superlatives.
- Baseline: go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1 passes in about 3 s; go run ./cmd/summer docs:build --check prints "docs:build: no problems found".
</context>
<tasks>
<task type="tracer">
<name>Task 1: Tracer - publish the page shell with its first section and both inbound links, proven by the docs checker and a scratch build</name>
<files>docs/architecture/performance-and-scaling.md, docs/index.md, docs/setup/coming-from-wintercms.md</files>
<read_first>docs/architecture/request-lifecycle.md (tone and front matter of the neighbouring page), docs/architecture/application-lifecycle.md (Start-up sequence wording to stay consistent with), docs/index.md, docs/setup/coming-from-wintercms.md lines 7-14, internal/docsite/check_policy.go, internal/docsite/check_forbidden.go</read_first>
<action>
Create docs/architecture/performance-and-scaling.md with this front matter, in the same layout as the other architecture pages: title "Performance and scaling"; a quoted description of at most 160 characters along the lines of "Why a SummerCMS binary serves requests faster and with less memory than WinterCMS on PHP-FPM, where it does not, and how to scale and measure it." (count it; the checker enforces 160); section architecture; order 50. The first body line is exactly "# Performance and scaling".
Write the introduction (two short paragraphs): WinterCMS runs under PHP-FPM, which boots the application for every request; a SummerCMS application is one Go process that boots once and serves every request from memory. Say the page explains where the speed and memory savings come from, where a port will not get faster, and how to scale and measure an application. State plainly that the figures are typical for PHP-FPM with Laravel compared with Go services and that SummerCMS has no published benchmarks yet; point to the measuring section at the end of the page.
Write the first content section (content item 1), heading "## Boot once, not per request": PHP-FPM is shared-nothing, so every request re-runs the Laravel/Winter bootstrap: service providers, plugin registration, configuration, routes and YAML cache lookups; even with OPcache this typically costs about 15-60 ms before the controller runs. SummerCMS does that work once at start-up: [party](../../modules/party/README.md) runs every plugin's Register and Boot (`party.Activate`), [cabana](../../modules/cabana/README.md) compiles each plugin's fields.yaml and columns.yaml into a `cabana.CompiledController` at boot, and [surf](../../modules/surf/README.md) assembles every route into one net/http ServeMux (`surf.Assemble`). A request then only matches a route and runs its middleware chain, which costs microseconds. Link [Application lifecycle](application-lifecycle.md#start-up-sequence) and [Request lifecycle](request-lifecycle.md). End the tracer content there; the remaining sections are added in Task 2 (headings for them are not written yet, so the page never shows an empty section).
Add the inbound links:
- docs/index.md: in the "Where to start" Architecture bullet, extend the list so it also names performance and scaling compared with PHP-FPM, linking [performance and scaling](architecture/performance-and-scaling.md); keep the sentence's existing shape.
- docs/setup/coming-from-wintercms.md: append one sentence to the intro paragraph that begins "This page maps the concepts." pointing readers who want to know how the single-binary process model changes speed, memory and scaling compared with PHP-FPM to [Performance and scaling](../architecture/performance-and-scaling.md).
Rules: headings plain ASCII (write "will not", not contractions with apostrophes, to keep anchors simple), no Go code fences, no consuming-application names (use "the application"), British spelling. Do not commit yet: the user wants the page to land as one commit, made at the end of Task 2.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1 && go run ./cmd/summer docs:build --check && bash -c 'd=$(mktemp -d) && go run ./cmd/summer docs:build --out "$d/site" && test -f "$d/site/architecture/performance-and-scaling.html" && grep -q "performance-and-scaling" "$d/site/index.html"'</automated>
</verify>
<done>The page exists with valid front matter (section architecture, order 50, first line "# Performance and scaling"), an introduction and the "Boot once, not per request" section; docs/index.md and docs/setup/coming-from-wintercms.md link to it; TestDocsTree, TestDocsBuildRealTree and docs:build --check pass; a scratch build writes architecture/performance-and-scaling.html and the landing page links it. Nothing committed yet.</done>
</task>
<task type="auto">
<name>Task 2: Write the remaining sections (memory, slow I/O, connections, hydration, expectations, caveats, scaling, measuring), verify every identifier, run the gates and commit once</name>
<files>docs/architecture/performance-and-scaling.md</files>
<read_first>docs/services/rate-limiting.md (Named buckets: the per-instance counter sentence), docs/services/jobs.md (Running workers), docs/services/authentication.md lines 50-60 (Refreshing and revoking), docs/services/parity-testing.md lines 80-95 (The parity commands), docs/services/storage.md lines 11-30 (Bucket URLs), docs/services/realtime.md (The Centrifugo driver), modules/surf/router.go lines 434-450, modules/conga/client.go lines 30-60</read_first>
<action>
First verify every identifier and command the page will name, from the repository root: go doc ./modules/surf MemoryStore; go doc ./modules/surf Store; go doc ./modules/surf NewFixedWindowLimiter; go doc ./modules/surf Assemble; go doc ./modules/bouncer PostgresBlacklist; go doc ./modules/bouncer NewPostgresBlacklist; go doc ./modules/lagoon Publish; go doc ./modules/party Activate; go doc ./modules/cabana CompiledController; go doc ./modules/wire WriteJSON; go doc ./modules/lagoon/attach File.Thumb. Hand-check config keys by grep: queue.work_in_serve in modules/conga/client.go, database.dsn in modules/lagoon/connection.go, storage.uploads.bucket_url in modules/lagoon/attach/bucket.go. If any check fails, reword the page around what exists; never name an identifier, command or config key you did not confirm.
Then append these sections to docs/architecture/performance-and-scaling.md, in this order (headings are plain ASCII):
"## Memory" (item 2): an FPM worker typically holds 30-80 MB, and FPM needs one worker per concurrent request, so 20 workers take roughly 1-1.5 GB. One SummerCMS process typically sits at 50-150 MB while serving thousands of concurrent requests.
"## Slow I/O and concurrency" (item 3): a request waiting on an external API, SMTP, the search engine or the realtime server holds its FPM worker the whole time; when every worker is busy (pm.max_children reached), new requests queue and p99 latency climbs sharply. In Go each request runs on a goroutine that costs a few KB, so waiting requests do not block others.
"## Database connections" (item 4): PHP-FPM usually opens about one Postgres connection per worker. SummerCMS opens one database/sql pool per process: [lagoon](../../modules/lagoon/README.md) publishes it with `lagoon.Publish`, GORM queries use it and [conga](../../modules/conga/README.md) runs River jobs on the same pool. When the job worker runs, it adds one dedicated listening connection; link [Running workers](../services/jobs.md#running-workers) for the PgBouncer note. Do not name any pool-size or connection-limit config key: lagoon has none.
"## Model hydration" (item 5): Eloquent hydration builds attribute arrays and runs magic accessors and casts per model; GORM scans rows into plain structs through reflection, which costs less per row. Keep this qualitative; do not quote a number.
"## What to expect" (item 6): open with a NOTE callout (a blockquote whose first line is [!NOTE]) that contains the exact phrase "not measurements of SummerCMS": the ranges are typical for PHP-FPM with Laravel compared with Go services, SummerCMS has no benchmarks yet, and the measuring section shows how to get real numbers for an application. Then a table with columns Endpoint shape, Examples, Typical gain over PHP-FPM; rows: trivial or cached responses (health checks, settings or lookup endpoints) - 10-30x throughput per core, sub-millisecond latency in Go; typical CRUD (paginated lists, a record with a relation or two, validated writes) - 3-10x; database-heavy (large joins, aggregates, full-text queries) - 1.2-2x, Postgres time dominates.
"## Where you will not win" (item 7), as a bullet list:
- A line-by-line port keeps the PHP queries, so slow queries and N+1 patterns come over unchanged; fix them (for example with GORM's Preload) once the port passes parity.
- [wire](../../modules/wire/README.md) keeps response bodies byte-compatible with the PHP backend, so payload size and client-side parsing do not change.
- Rate limiting, accurate to the code: the throttle middleware that `serve` builds keeps its counters in the process (`surf.MemoryStore`, behind the `surf.Store` interface), and serve offers no way to supply a different store yet, so with N replicas the effective limit is N times the configured one. Put this in a WARNING callout and give the options that exist today: lower the per-route limits by the replica count, or enforce the limit at the load balancer. Link [Rate limiting](../services/rate-limiting.md#named-buckets). Do NOT write that a shared store can be configured.
- Revoked tokens: use `bouncer.NewPostgresBlacklist` (a `bouncer.PostgresBlacklist`) rather than the in-memory blacklist, so a revocation applies on every replica; link [Refreshing and revoking](../services/authentication.md#refreshing-and-revoking).
- Jobs: `serve` runs the conga job worker in the same process by default; under load set `queue.work_in_serve` to false and run `./bin/acme queue:work` as separate processes (link [Running workers](../services/jobs.md#running-workers)).
- Thumbnails are generated in pure Go (`attach.File.Thumb`), which can be slower than GD or Imagick for large images.
- Garbage collection pauses are sub-millisecond and do not matter at this scale.
"## Scaling and operations" (item 8): the application is one stateless binary that starts in milliseconds, ships in a small container image and needs no OPcache warm-up. Vertical scaling is more cores, with no worker-count tuning. Horizontal scaling is N replicas behind a load balancer sharing Postgres; with several replicas the uploads bucket must also be shared: a file:// bucket on storage every replica mounts (link [Storage](../services/storage.md#bucket-urls)); apply the rate-limit and blacklist notes above first. Centrifugo scales on its own: [lighthouse](../../modules/lighthouse/README.md) only publishes to it (link [The Centrifugo driver](../services/realtime.md#the-centrifugo-driver)). A short sh fence may show `./bin/acme migrate` once before a rollout and `./bin/acme serve --addr 127.0.0.1:8080` per replica behind a reverse proxy.
"## Measuring it yourself" (item 9): record fixtures from the PHP backend with [tide](../../modules/tide/README.md), using the summer parity commands exactly as docs/services/parity-testing.md writes them (an sh fence with summer parity:record against the PHP backend and summer parity:replay against the port, loopback addresses, and link [The parity commands](../services/parity-testing.md#the-parity-commands)); once replay passes, drive the same routes under load against both backends with a load tool such as vegeta or k6, on identical hardware and the same Postgres instance, with OPcache enabled and warm on the PHP side, and compare per-route p50 and p99 latency, requests per second and resident memory (RSS). Keep vegeta and k6 in prose; do not invent their flags.
Finally run the gates (see verify), fix every reported problem, then make ONE commit containing only docs/architecture/performance-and-scaling.md, docs/index.md and docs/setup/coming-from-wintercms.md, with the message "docs(architecture): add performance and scaling page compared to PHP-FPM". Per the user's global and project CLAUDE.md, the commit message carries no co-author line and no session attribution trailer of any kind. Do not stage the untracked landing-page zip in the repository root or any .planning file (planning docs are committed separately by the orchestrator).
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go vet ./... && go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1 && go run ./cmd/summer docs:build --check && grep -q 'not measurements of SummerCMS' docs/architecture/performance-and-scaling.md && grep -q '\[!NOTE\]' docs/architecture/performance-and-scaling.md && grep -q '\[!WARNING\]' docs/architecture/performance-and-scaling.md && grep -q 'surf.MemoryStore' docs/architecture/performance-and-scaling.md && grep -q 'bouncer.NewPostgresBlacklist' docs/architecture/performance-and-scaling.md && grep -q 'queue.work_in_serve' docs/architecture/performance-and-scaling.md && grep -q 'queue:work' docs/architecture/performance-and-scaling.md && grep -q 'parity:record' docs/architecture/performance-and-scaling.md && test "$(grep -ciE 'max_open|max_idle|pool_size' docs/architecture/performance-and-scaling.md)" = 0 && bash -c 'set -e; msg=$(git log -1 --format=%B); files=$(git show --name-only --format= HEAD); ! grep -qi -e co-authored-by -e claude-session <<< "$msg"; test "$(echo $files)" = "docs/architecture/performance-and-scaling.md docs/index.md docs/setup/coming-from-wintercms.md"'</automated>
</verify>
<done>The page covers content items 1-9 in the listed sections; the expectations table sits under a NOTE that says the ranges are not measurements of SummerCMS; the rate-limit caveat sits in a WARNING and states that serve always uses surf.MemoryStore with no way to supply another store; no pool-size key is named; every identifier, command and config key was confirmed; go vet, TestDocsTree, TestDocsBuildRealTree and docs:build --check pass; HEAD is a single docs commit touching exactly the three docs files, with no co-author or session trailer.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| framework docs -> public docs site | docs/ pages are published as HTML, Markdown, llms.txt and a search index; anything written here becomes public |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-qoa-01 | Information disclosure | docs/architecture/performance-and-scaling.md | low | mitigate | No consuming-application names; TestDocsTree and docs:build --check run the forbidden-name rule over sources and rendered outputs (internal/docsite/check_forbidden.go) |
| T-qoa-02 | Repudiation (misleading guidance) | rate-limit and scaling advice | medium | mitigate | Caveats are written against the code (surf.BuildRouter always uses surf.MemoryStore; bouncer.NewPostgresBlacklist; queue.work_in_serve), and the page must not claim a configurable shared rate-limit store or pool-size key; acceptance greps enforce the latter |
| T-qoa-SC | Tampering | npm/pip/cargo/go installs | low | accept | No dependency is installed or added; docs-only change |
</threat_model>
<verification>
- go vet ./... green.
- go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1 passes (identifiers, links and anchors, commands, headings, callouts, forbidden names, front matter).
- go run ./cmd/summer docs:build --check prints "docs:build: no problems found".
- A scratch docs:build writes architecture/performance-and-scaling.html and the landing page links it.
- git show --stat HEAD lists only the three docs files; the message has no co-author or session trailer.
</verification>
<success_criteria>
- A WinterCMS developer finds "Performance and scaling" last in the Architecture sidebar and from the landing page and the Coming from WinterCMS page.
- The page covers all nine approved content items, frames every number as typical PHP-FPM/Laravel versus Go ranges rather than SummerCMS measurements, and states the multi-replica caveats as the code behaves today.
- All docs gates and go vet are green; one docs commit.
</success_criteria>
<output>
Create `.planning/quick/261001-qoa-add-a-docs-page-on-performance-and-scali/261001-qoa-SUMMARY.md` when done
</output>

View File

@@ -0,0 +1,73 @@
---
phase: quick-261001-qoa
plan: 01
subsystem: docs
tags: [docs, architecture, performance, scaling]
status: complete
requires: []
provides:
- "docs/architecture/performance-and-scaling.md (Architecture section, order 50)"
affects:
- docs/index.md
- docs/setup/coming-from-wintercms.md
tech-stack:
added: []
patterns: []
key-files:
created:
- docs/architecture/performance-and-scaling.md
modified:
- docs/index.md
- docs/setup/coming-from-wintercms.md
decisions:
- "Rate-limit caveat states that serve always builds the throttle limiter on surf.MemoryStore with no way to supply another store; the page advises dividing limits by replica count or limiting at the load balancer"
- "No pool-size or connection-limit config key is named, because lagoon has none (only database.dsn)"
metrics:
duration: 10 min
completed: 2026-10-01
actuals:
tokens: 2900
tasks: 2
commits: 1
plan_head_before: 2fc5eac6d5073f1ec0c6a9215193991d96352594
plan_head_after: 4103d95a1ac46796655eb87c392b937113d7f665
---
# Quick 261001-qoa Plan 01: Performance and scaling docs page Summary
A new Architecture guide page compares a SummerCMS binary with WinterCMS on PHP-FPM: boot once versus per-request boot, memory, slow-I/O concurrency, the shared GORM+River pool and model hydration. It includes an expectations table under a NOTE saying the ranges are not measurements of SummerCMS, multi-replica caveats that match the code (including a WARNING on per-process rate limits), scaling and operations, and measuring with tide parity:record/parity:replay plus vegeta or k6.
## Tasks
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Tracer: page shell, intro, "Boot once, not per request", inbound links | 4103d95 (single commit, per plan) | docs/architecture/performance-and-scaling.md, docs/index.md, docs/setup/coming-from-wintercms.md |
| 2 | Remaining sections, identifier checks, gates, commit | 4103d95 | docs/architecture/performance-and-scaling.md |
The tracer gate ran before expansion: TestDocsTree and TestDocsBuildRealTree passed, `docs:build --check` reported no problems, and a scratch build wrote architecture/performance-and-scaling.html with the landing page linking it.
## Verification
- Identifiers confirmed with go doc: surf.MemoryStore, surf.Store, surf.NewFixedWindowLimiter, surf.Assemble, bouncer.PostgresBlacklist, bouncer.NewPostgresBlacklist, lagoon.Publish, party.Activate, cabana.CompiledController, wire.WriteJSON, attach.File.Thumb.
- Config keys checked by hand: queue.work_in_serve (modules/conga/client.go:55), database.dsn (modules/lagoon/connection.go:97), storage.uploads.bucket_url (modules/lagoon/attach/bucket.go). surf.BuildRouter always uses NewMemoryStore (modules/surf/router.go). Thumbnails use github.com/disintegration/imaging. `serve --addr` exists (modules/surf/serve.go).
- `go vet ./...`: green.
- `go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1`: ok.
- `go run ./cmd/summer docs:build --check`: "docs:build: no problems found".
- Scratch build: the sidebar order is introduction, go-modules-and-workspaces, application-lifecycle, request-lifecycle, performance-and-scaling.
- Acceptance greps all pass. No max_open, max_idle or pool_size appears. The commit touches exactly the three docs files and has no co-author or session trailer.
## Deviations from Plan
- Task 1 intro: the tracer pointed to "the measuring section at the end of this page" in plain text, because the `#measuring-it-yourself` anchor did not exist yet and the link checker would have failed. Task 2 turned it into a link once the section existed. The final content matches the plan.
- The commit has no Co-Authored-By or session trailer, following the user's global CLAUDE.md and the orchestrator's constraints.
Otherwise the plan was executed as written.
## Known Stubs
None.
## Self-Check: PASSED
- FOUND: docs/architecture/performance-and-scaling.md
- FOUND: commit 4103d95