301 lines
10 KiB
Go
301 lines
10 KiB
Go
|
|
// 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)
|
||
|
|
}
|