go-doc/backend/scripts/seed_perf_data.go

301 lines
10 KiB
Go
Raw Permalink Normal View History

2026-10-07 15:03:50 +02:00
// Command seed_perf_data floods the go-doc flat-file store with generated
// documentation and feedback so the platform can be exercised under realistic
// load.
//
// go run ./scripts/seed_perf_data.go -data ../data -docs 120 -comments 25 -workers 8
//
// Every document is a syntactically valid Markdown page carrying all three
// audience sections, and every comment is a real JSON record with a real badge.
// The generator is deterministic for a given -seed so runs are reproducible.
package main
import (
"flag"
"fmt"
"log/slog"
"math/rand"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"go-doc/storage"
)
func main() {
var (
dataDir = flag.String("data", storage.ResolveRoot(), "flat-file data directory")
docCount = flag.Int("docs", 120, "number of Markdown documents to generate (spec floor: 100)")
perDoc = flag.Int("comments", 25, "comments to inject per document")
workers = flag.Int("workers", 8, "parallel seeding workers")
seed = flag.Int64("seed", 20260101, "PRNG seed for reproducible output")
force = flag.Bool("force", false, "overwrite documents that already exist")
minBodyKB = flag.Int("min-body-kb", 2, "minimum document body size in KiB")
skipTemplate = flag.Bool("skip-go-doc", true, "leave data/docs/go-doc.md untouched")
)
flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
if *docCount < 100 && !*force {
logger.Warn("doc count is below the 100 document floor from the specification", "docs", *docCount)
}
engine, err := storage.New(*dataDir)
if err != nil {
logger.Error("cannot open the data directory", "error", err, "data", *dataDir)
os.Exit(1)
}
logger.Info("seeding flat-file store",
"data_root", engine.Root(),
"docs", *docCount,
"comments_per_doc", *perDoc,
"total_comments", *docCount**perDoc,
"workers", *workers,
)
jobs := make(chan int, *docCount)
for i := 0; i < *docCount; i++ {
jobs <- i
}
close(jobs)
var (
docsWritten atomic.Int64
docsSkipped atomic.Int64
commentsWritten atomic.Int64
bytesWritten atomic.Int64
wg sync.WaitGroup
failures atomic.Int64
)
start := time.Now()
for w := 0; w < *workers; w++ {
wg.Add(1)
go func(worker int) {
defer wg.Done()
// Each worker owns its own PRNG stream, which keeps output
// reproducible regardless of scheduling order.
rng := rand.New(rand.NewSource(*seed + int64(worker)*7919))
for index := range jobs {
title := documentTitle(index)
slug := storage.Slugify(title)
if *skipTemplate && slug == "go-doc" {
docsSkipped.Add(1)
continue
}
if !*force {
exists, err := engine.DocExists(slug)
if err != nil {
failures.Add(1)
logger.Error("existence check failed", "slug", slug, "error", err)
continue
}
if exists {
docsSkipped.Add(1)
continue
}
}
body := documentBody(title, slug, *minBodyKB, rng)
if _, err := engine.SaveDoc(slug, body); err != nil {
failures.Add(1)
logger.Error("document write failed", "slug", slug, "error", err)
continue
}
docsWritten.Add(1)
bytesWritten.Add(int64(len(body)))
for n := 0; n < *perDoc; n++ {
role := storage.Roles()[rng.Intn(len(storage.Roles()))]
comment := storage.CommentInput{
Author: authorName(rng, role),
Role: string(role),
Content: commentBody(role, slug, n, rng),
}
if _, err := engine.AppendComment(slug, comment); err != nil {
failures.Add(1)
logger.Error("comment write failed", "slug", slug, "error", err)
break
}
commentsWritten.Add(1)
}
}
}(w)
}
wg.Wait()
elapsed := time.Since(start)
metas, err := engine.ListDocs()
if err != nil {
logger.Error("index read failed", "error", err)
os.Exit(1)
}
totalComments, byRole := 0, map[string]int{}
for _, meta := range metas {
stats, err := engine.Stats(meta.Slug)
if err != nil {
continue
}
totalComments += stats.Total
for role, n := range stats.ByRole {
byRole[role] += n
}
}
rate := 0.0
if elapsed > 0 {
rate = float64(commentsWritten.Load()) / elapsed.Seconds()
}
logger.Info("seed complete",
"elapsed", elapsed.Round(time.Millisecond),
"documents_written", docsWritten.Load(),
"documents_skipped", docsSkipped.Load(),
"documents_indexed", len(metas),
"comments_written", commentsWritten.Load(),
"comments_indexed", totalComments,
"markdown_bytes", bytesWritten.Load(),
"comments_per_second", int(rate),
"by_role", byRole,
)
if failures.Load() > 0 {
logger.Error("seed completed with failures", "failures", failures.Load())
os.Exit(1)
}
fmt.Printf("\nSeeded %d documents and %d comments into %s\n",
docsWritten.Load(), commentsWritten.Load(), filepath.Clean(engine.Root()))
}
var serviceNouns = []string{
"Gateway", "Pipeline", "Registry", "Collector", "Scheduler", "Router",
"Indexer", "Notifier", "Bridge", "Vault", "Ledger", "Beacon",
"Annex", "Relay", "Forge", "Harbor", "Signal", "Archive",
"Compass", "Foundry", "Sentinel", "Orchid", "Quartz", "Meridian",
}
var serviceScopes = []string{
"Billing", "Identity", "Storage", "Messaging", "Search", "Payments",
"Analytics", "Provisioning", "Fulfilment", "Telemetry", "Consent", "Routing",
}
// documentTitle builds a stable, human title for the nth generated document.
func documentTitle(index int) string {
scope := serviceScopes[index%len(serviceScopes)]
noun := serviceNouns[(index/len(serviceScopes))%len(serviceNouns)]
return fmt.Sprintf("%s %s Service %03d", scope, noun, index)
}
func documentBody(title, slug string, minBodyKB int, rng *rand.Rand) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s\n\n", title)
fmt.Fprintf(&b, "> Generated performance fixture for `%s`. Regenerate with `make seed`.\n\n", slug)
b.WriteString("This page isolates the same system across three audiences. Only the section that\n")
b.WriteString("matches the selected tab is rendered in the reader.\n\n")
b.WriteString("## For Users\n\n")
writeParagraphs(&b, rng, 6, minBodyKB/6+1, []string{
"The guided flow walks a new user from an empty workspace to a first successful run.",
"Every destructive action asks for confirmation and explains what is about to change.",
"Keyboard shortcuts mirror the visual toolbar so the workflow stays fast.",
})
b.WriteString("\n### Common tasks\n\n")
for _, task := range []string{"Create your first workspace", "Invite a teammate", "Export a report", "Reset your password"} {
fmt.Fprintf(&b, "- %s\n", task)
}
b.WriteString("\n")
b.WriteString("## For Operators\n\n")
writeParagraphs(&b, rng, 6, minBodyKB/6+1, []string{
"The deployment is idempotent: re-running the pipeline converges on the declared state.",
"Health checks probe the read path before the write path is allowed to serve traffic.",
"Rollback restores the previous artefact and replays the migration in reverse.",
})
b.WriteString("\n### Runbook\n\n")
b.WriteString("```bash\n# verify the release is healthy\ncurl -fsS https://localhost:8080/api/health\n\n# roll back to the previous artefact\nmake rollback RELEASE=previous\n```\n\n")
b.WriteString("| Signal | Threshold | Action |\n| --- | --- | --- |\n| p99 latency | 250 ms | Page the on-call |\n| error rate | 1 % | Halt the rollout |\n| queue depth | 1000 | Scale workers |\n\n")
b.WriteString("## For Developers\n\n")
writeParagraphs(&b, rng, 6, minBodyKB/6+1, []string{
"The API is versioned in the path; a breaking change ships under a new major prefix.",
"Every write is atomic: readers observe either the previous file or the new one, never a partial write.",
"The engine slugifies titles into lower-case, hyphen-separated identifiers.",
})
b.WriteString("\n### Endpoints\n\n")
b.WriteString("```http\nGET /api/docs\nGET /api/docs/{slug}\nPOST /api/docs/{slug}\nDELETE /api/docs/{slug}\nGET /api/docs/{slug}/comments\nPOST /api/docs/{slug}/comments\n```\n\n")
b.WriteString("### Integration notes\n\n")
fmt.Fprintf(&b, "Inline references such as `data/docs/%s.md` and **bold emphasis** must survive a round trip through the editor.\n", slug)
// Top up the body so every fixture is at least minBodyKB.
for b.Len() < minBodyKB*1024 {
b.WriteString("\nAdditional reference material for load testing. ")
fmt.Fprintf(&b, "Section %d of the generated body for `%s`. ", rng.Intn(1<<20), slug)
}
b.WriteString("\n")
return b.String()
}
func writeParagraphs(b *strings.Builder, rng *rand.Rand, count, targetLen int, seeds []string) {
for i := 0; i < count; i++ {
seed := seeds[i%len(seeds)]
fmt.Fprintf(b, "%s ", seed)
for len(b.String()) < targetLen*1024 && i == count-1 {
fmt.Fprintf(b, "Reference sentence %d covering the behaviour of `%s`. ", rng.Intn(1<<20), "service")
break
}
b.WriteString("\n\n")
}
}
var givenNames = []string{"ana", "bo", "chi", "dee", "eli", "fen", "gita", "hugo", "iris", "jo", "kit", "lena", "mo", "nia", "omar", "pia"}
func authorName(rng *rand.Rand, role storage.Role) string {
name := givenNames[rng.Intn(len(givenNames))]
suffix := map[storage.Role]string{
storage.RoleUser: " (user)",
storage.RoleOperator: " (ops)",
storage.RoleDeveloper: " (dev)",
}[role]
return name + suffix
}
var commentSeeds = map[storage.Role][]string{
storage.RoleUser: {
"The guided setup was easier to follow than the older docs.",
"I could not find the export button on the first screen.",
"Step 3 assumes a workspace that most new accounts do not have.",
"The keyboard shortcut list is genuinely useful.",
"Copy in the empty state does not match what actually happens.",
},
storage.RoleOperator: {
"Rollback took 20 minutes; the runbook should call that out.",
"Health checks pass while the write path is still degraded.",
"We need the p99 latency threshold surfaced on the dashboard.",
"The idempotent redeploy saved us during last week's incident.",
"Queue depth alerting fires too late to be actionable.",
},
storage.RoleDeveloper: {
"The retry budget is undocumented; please add the numbers.",
"A `--dry-run` flag would make this safe to run in CI.",
"The response schema changed without a version bump.",
"Slug collisions are possible when titles differ only by punctuation.",
"Atomic writes look correct, but the rename is not fsynced in CI images.",
},
}
func commentBody(role storage.Role, slug string, index int, rng *rand.Rand) string {
seeds := commentSeeds[role]
base := seeds[rng.Intn(len(seeds))]
return fmt.Sprintf("%s (finding %d on %s)", base, index+1, slug)
}