Skip to content

M3-16 Search load test

No Date Ruling Reason
D-M3-16-1 2026-10-24 The search load test is services/server/perf/search (TestPerf_Search, build tag perf) with its untagged library perf/internal/searchbench and the seeding and product checks perf/internal/searchworld; the tool of the plan row is the make target bench-search, which runs only that test and prints its two targets (perfreport -required search.p95_target,search.prefix_p95_target). bench-search is in neither gates, check nor the pre-push hook. Place: root Makefile, perf/internal/report/perfreport/main.go. 02 section 6 puts load tests under perf/ with the shared report; Go's internal rule keeps perf/internal/report out of reach of tools/, and a second results file would contradict M1-72.
D-M3-16-2 2026-10-24 search.p95_target, search.prefix_p95_target and search.benchmark_messages get the place Load test in a new column Place of 14 section 15 (every other row Server): no environment name, not in the configuration schema. perf/internal/report reads the Load test rows of the tables "Configuration values of this chapter" of 02 and 14 (report.LoadAll, each section by its title); a name in both chapters fails, a missing row or an unparseable value fails at lookup. A meaning with the word target is a target, a lower bound with (minimum), else an upper bound; every other Load test row is a parameter. Both search targets join report.Required; a result can carry a note (the corpus size) that the table prints. Place: perf/internal/report/report.go, threshold.go, results.go. 02 section 22 already treats perf.* this way; the three values are read by no server process.
D-M3-16-3 2026-10-24 The parameters of the run are the named constants of perf/internal/searchbench/constants.go (world, corpus, load, query mix; values in perf/README.md, copied into extra.search.params); DefaultParams feeds TestPerf_Search, SmallParams the small-corpus tests with the same code. 14 section 7 fixes languages, Zipf, spaces, channels and a partial view, nothing else.
D-M3-16-4 2026-10-24 Rights, people and channels come through the public API (searchworld.Seed on a seeding process with every ratelimit.* raised); the content rows (threads of the posts, messages, the counters of every thread) come from a bulk loader through the admin pool of the throwaway database while no server runs on it: COPY of the posts, COPY of the messages in batches of LoadBatch, one UPDATE of the main threads, then VACUUM (ANALYZE) of messages and threads, then the count and the per-thread aggregate check. Ids are UUIDv7 from ids.NewGenerator at each row's created_at with a seeded ChaCha8 reader, one generator per id column; threads.version grows by one per message and per deletion as the product's AppendToThread and deletion do; client_payload_sha256 is the SHA-256 of the text as the brief fixes it (a replay of a loaded client_msg_id is not part of the test). searchworld.CheckThreads compares ConsistencySamples threads with getThread and listThreadMessages before the measurement and searchworld.CheckWrites writes one message per language and one post per language after it; the small-corpus tests in make test compare every thread. Place: perf/internal/searchbench/loader.go, perf/internal/searchworld/check.go. Building search.benchmark_messages messages through createMessage would use most of the time limit and measure the write path that perf.message_create_p99 holds; the checks show that the rows equal what the product writes.
D-M3-16-5 2026-10-24 VACUUM (ANALYZE) of messages and threads after the load and before the measurement. The GIN pending list is merged and the planner has statistics, as on an instance where autovacuum has run.
D-M3-16-6 2026-10-24 Measured server: the seeding process restarted on the same durable database with the default configuration and the limits of the profile mode (perfkit.Env.Measure). Load: every searcher open loop at BudgetShare of ratelimit.rl.search.per_minute, the searchers phase-shifted evenly (perfkit.Schedule, the one scheduler of M1-72 and M3-24), no background writes; phases one after the other, each Warmup, Window, up to Drain. Measuring point: from the scheduled send time to the fully read body on loopback in the test process, nearest-rank p95. Before each phase every person renews its token pair (refreshToken), so no sample pays a renewal. Place: perf/search/search_test.go, perf/internal/searchworld/searchworld.go. 14 section 7 names the target "server duration without network" but no load and no measuring point; the scheduled send time avoids coordinated omission.
D-M3-16-7 2026-10-24 Query mix of phase messages: GET /api/v1/search?q= with the defaults of openapi.yaml, German or English with equal chance, classes one, two and three words, phrase and negation a -b by their shares; the words of a query come from one frequency band (frequent, medium, rare) chosen by share; a phrase is two adjacent words of a visible live message, at least one a content word. Phase prefix: one or two words of the title of a visible post, each cut to between search.prefix_min_chars and its full length. Place: perf/internal/searchbench/queries.go. Frequent words reach search.max_candidates (R-SU-04), and their share lies above the five percent a p95 may ignore.
D-M3-16-8 2026-10-24 A measurement counts only when the path behaved correctly: a status other than 200 (among them 429 rate_limited and 503 unavailable with reason search_timeout), a body that does not decode strictly into SearchHitPage or ThreadTitleHitList, a hit of another kind or from a channel the searcher may not read, a phrase or prefix query without a hit, a probe answer with an item, candidates_capped or next_cursor, a failed load, product or rewrite check, or fewer than MinSamples samples fail the test and mark both targets failed. Failure messages name phase, class, query, counts and ids, never content. Place: perf/internal/searchbench/checks.go, perf/search/search_test.go. ADR 0063 point 5 and T-AZ-02: a fast but wrong or leaking path must not end green.
D-M3-16-9 2026-10-24 Phase probe has no threshold: the searchers without the probe space send the probe term (only in channels of the probe space) and the absent term (nowhere) alternating, ProbeWarmup of each unmeasured and ProbeQueries of each measured; both must give the empty page, and extra.search.probe records p50, p95 and their differences. 14 4.3 accepts the residual risk and names no bound; RK-10 is rated on the measured values.
D-M3-16-10 2026-10-24 Phase rewrite, after the measured process stopped: sizes first (table, indexes on messages.search_vector and threads.title_search, database, the share of the search indexes), then in one transaction DROP COLUMN and ADD COLUMN ... GENERATED ALWAYS AS (<expression read from the catalog>) STORED and the saved index definitions without CONCURRENTLY; afterwards expression and index definitions equal the saved ones and a fixed phrase finds the same messages. extra.search.rewrite holds the values and the values per million messages; the upgrade guide turns them into the maintenance window, no fixed duration or size stands in documentation. Place: perf/internal/searchbench/rewrite.go, docs/operations/upgrade.md. The duration depends on host and corpus (14 section 8, RK-38).
D-M3-16-11 2026-10-24 PostgreSQL of the load test runs with the defaults of its image and the durability of pgtest.Durable(), without tuning; extra.search.postgres records shared_buffers, work_mem and maintenance_work_mem. 16 prescribes no tuning for bothy-postgres, so the shipped database runs with the image defaults too.
D-M3-16-12 2026-10-24 make perf already passes -p 1 (M1-72), so no change there; PERF_TIMEOUT stays as it is. The packages run one after another; see the report of M3-16 for the measured duration.