M3-16: Search load test¶
Status: ready | Depends on: M1-72, M3-14 | Milestone: M3
Goal¶
After this ticket make perf measures the two performance targets of the search on a recorded host profile, prints value and threshold of each and fails when one is missed: search.p95_target, the p95 of the server time of GET /api/v1/search (search, type messages) on a corpus of exactly search.benchmark_messages messages, and search.prefix_p95_target, the p95 of GET /api/v1/search/thread-titles (searchThreadTitles) on the same world. The corpus is synthetic and deterministic, as 14 "Performance targets and load test" demands: German and English texts with Zipf-distributed word frequencies, several spaces and channels, people who each see only a part of the channels, and queries with one to three words, phrases and negation. The measurement runs against a server process with the default configuration on a durable database, and it counts only when the path behaved correctly while it was measured: every answer 200, every hit from a channel the searcher may read, every query built from a visible message finds something. The same run measures what the specification leaves to the load test: the duration of a rewrite of messages.search_vector with its GIN index per million messages, which the upgrade guide turns into a maintenance window (14 "Index maintenance", RK-38), the share of the search index in the database size (16 "Capacity and resources"), and the time difference between a term that occurs only in channels the searcher cannot read and a term that occurs nowhere (the residual risk of 14 "Permission filter", RK-10). A make target bench-search runs the same test by hand. This is the search part of M3 exit criterion 3, the "Load test" row of 14 "Tests" and the confirmation "Load test" of ADR 0032; the target stops being unmeasured.
Read first¶
docs/PLAN.md: section 3 "Way of working per ticket" (item 6:make perfin every ticket that touches a performance target, no lowered threshold); section 4 "Closing a milestone" item 3; section 5 "Test strategy" row "Load, endurance, chaos"; section 9 "M3 Platform services" (Goal, Exit criteria 3 and 4, Red team "timing side channel of the search", Risks "a change of the search vectors rewrites the table") with the rows M3-13 and M3-14 (what is measured), M3-15 (file text, not measured here) and M3-16 (this ticket); section 7 rows M1-01, M1-16, M1-71 and M1-72; section 8 row M2-36 (the sibling load test of the agent path); section 19 P-07, P-09, P-10, P-11 and P-26. No ticket of the plan depends on M3-16; its result is the search part of M3 exit criterion 3.- Dependencies: the header names M1-72 (the perf frame, D-W-108) and M3-14. Implied through it and therefore not in the header: M3-13 (search vectors,
threads.search_language,search_tsquery,kernel/searchlang), M3-07 and the M1 tickets below them, among them M1-01 (theperfframe target andPERF_TIMEOUT), M1-07 (configuration defaults), M1-08 (tools/configdocwith the rules D4 and D6 andtools/configdoc/planned.yaml), M1-12 (pgtest), M1-16 (perf/event_commit, the firstperf/internal/report,pgtest.Durable()), M1-31 (updateMyProfile,users.locale), M1-37 (createUserwithlocal_account,redeemEnrollmentCode), M1-48 (createGrant), M1-53, M1-55, M1-56 and M1-57 (channels, threads, messages, history), M1-71 (test/harness). docs/spec/14-search.md: 1 "Goals and limits"; 2 "What is searchable"; 3.1 "Language per object"; 3.2 "Expressions"; 3.3 "Limits of language processing"; 4.1 "Input"; 4.2 "Query construction"; 4.3 "Permission filter" (items 1 to 4 and the paragraph "Residual risk"); 4.5 "Ranking and sorting" (the candidate limit); 4.7 "Paging"; 4.8 "Example: messages"; 4.9 "Prefix search for the quick switcher"; 7 "Performance targets and load test"; 8 "Index maintenance" ("Changing the expression"); 9 "Limit and outlook"; 10 "Module and port"; 11 "Endpoints"; 12 "Tests" (row "Load test"); 13 R-SU-04; 15 "Configuration values of this chapter".docs/spec/02-architecture.md: 6 "Code layout" (perf/,tools/, componenttestsupport); 7.1 "Module register as the source" (exclude: [tools],testsupport); 17 "Performance targets"; 22 "Configuration values of this chapter" (the paragraph onperf.*withPlaceLoad test, the rowperf.reference_profile).docs/spec/03-data-model.md(core contract, read only): 1 "Principles" items 1 (UUIDv7 from the application), 2 (one server clock) and 12 (only the owning module writes a table; design note 6 says why the corpus fixture of a throwaway database is no module); tablesthreadsandmessages(columns, checks, unique keys, indexes, the import rule forunread_floor_seq); 5 "Search vectors".docs/spec/05-actions.md(core contract, read only): 3 "Rate-limit classes" (rl.searchper principal); 4.8 rowsearch.query.docs/spec/07-permissions.md: 7.4 "Query pattern" rules 1, 3 and 5.docs/spec/16-operations.md: 13 "Capacity and resources" ("indexes (the load test measures their share)").docs/spec/23-quality.md: 2 (the paragraph onmake perf), 3 (coverage leaves outperf/**), R-QA-11.docs/api/openapi.yaml: operationssearch,searchThreadTitles,createSpace,createChannel,createUser,redeemEnrollmentCode,updateMyProfile,createGrant,getThread,listThreadMessages,createMessage,createThread; schemasSearchHitPage,SearchHit,ThreadTitleHitList,Message,Thread,ThreadCreate,Problem(x-problem-codes).- ADRs 0032 "Full-text search in PostgreSQL" (Consequences, Confirmation "Load test in the release workflow"), 0063 "Performance gate for the event log" (points 2 to 5, Confirmation, More Information), 0050 (module register).
docs/operations/configuration.md: 4 subsections of chapters 02 and 14 and the keys of design note 17;docs/operations/upgrade.md: 1 item 1 (release notes name the duration of migrations on large tables) and 4 (rules M3, M4, M6).docs/RISKS.md: RK-10 (time side channel of the search), RK-38 (rewrite ofmessages).docs/security/threat-model.md: T-AZ-02 ("response time"), T-DS-01 (its test column names the load tests).docs/DECISIONS.md: D-L4, D-W-100, D-W-106 (M2-36 got M1-72 for the same frame); section 10 for the rows of this ticket. No item ofdocs/OPEN-QUESTIONS.mdconcerns this ticket.- Briefs:
docs/briefs/M1-72-performance-gate.md(the perf frame this ticket extends:perf/internal/report,.cache/perf/results.json, profile modes, durability check, the seeding rule,harness.StartProcess,Options.DurableDB, the open-loop scheduler ofload_messages),M1-71-contract-tests.md(test/harness:Start,Options,NewUser,AdminDB),M1-16-event-log.md(origin of the report,pgtest.Durable()),M1-01-server-gates.md(perftarget,PERF_TIMEOUT, the Makefile conventions for server targets),M1-08-config-reference.md(Placecolumn, D4, D6,planned.yaml),M3-13-search-vectors.md(the generated columns, the GIN indexes and their migration,search_tsquery,kernel/searchlangwithNewandMap.For, the rewrite note inmigrations/README.md),M3-14-search-coordinator.md(the coordinator, the read operations and their SQL,search.statement_timeout, the cursor).M2-36-agent-path-perf.mdis the sibling load test (named constants,-p 1, the move of the scheduler toperf/internal/load); it is not a dependency. - Code on the base branch (merged names win, D-W-100): the root
Makefile(targetperf, variablePERF_TIMEOUT, helpersskip,GO_ON,GO_PREREQS);services/server/perf/(event_commit,load_messages,internal/report,README.md);services/server/test/harness;internal/platform/pg/pgtest;internal/platform/config;internal/kernel/searchlang;internal/kernel/ids;migrations/(the search columns and GIN indexes of M3-13);internal/search;internal/messaging/adapters/postgres/queries/search.sql;internal/chat/adapters/postgres/queries/search.sql;tools/configdoc/planned.yaml.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. - Limits, windows and thresholds only through named configuration keys listed in
docs/operations/configuration.md; code comments and docs name the key, never the number. - Specification first: a change to behaviour, API, event, right or configuration updates
docs/spec/ordocs/api/in the same change. The core contract (docs/spec/00-glossary.md,03-data-model.md,04-sync-protocol.md,05-actions.md) is not edited by this ticket.
Note on rule 9: the parameters of the load (counts, shares, durations of design note 3) are named constants of a test, not limits of the product; their values stand in perf/internal/searchbench/constants.go and perf/README.md, and every product limit they depend on is read by its key name.
Product requirements implemented here:
make perfmeasures the p95 of the server time ofGET /api/v1/searchwith typemessagesagainstsearch.p95_targeton a corpus ofsearch.benchmark_messagesmessages, and the p95 ofGET /api/v1/search/thread-titlesagainstsearch.prefix_p95_target, and fails when one is missed (14 7, PLAN row M3-16, M3 exit criterion 3, ADR 0032 Confirmation).- The corpus follows 14 7: synthetic German and English texts, word frequency after a Zipf distribution, several spaces and channels, a principal sees a part of the channels; the queries have one to three words, phrases and negation.
search.p95_target,search.prefix_p95_targetandsearch.benchmark_messagesare values of the load test, not runtime configuration: likeperf.*in 02 22 they have no environment name, are not in the configuration schema of the server, andmake perfreads them from the table of 14 15 (D-L4, ADR 0063); no value appears as a literal in code or docs.- Measured only on databases with
fsyncandsynchronous_commiton; every result names the profile mode ofperf.reference_profile; onlymatchandemulatedcan end green,belownever does (ADR 0063 points 2 and 3). make perfruns at the milestone close, in the release and in every ticket that touches a performance target, never inmake checkor the pre-push gate (P-07; PLAN 3 item 6; 14 7 "not on every PR").- The measured server runs with the default configuration; only the seeding server raises
ratelimit.*, and every searcher of the measurement stays withinrl.search(R-QA-11, 05 3). - Rights before everything also under load: every hit and every title hit comes from a channel the searcher may read, and a term that occurs only in channels the searcher cannot read gives the same answer as a term that occurs nowhere (14 4.3, T-AZ-02).
- Every search statement runs under
search.statement_timeout(14 7); a503 unavailablewithreasonsearch_timeoutduring the measurement is a failure, not a slow sample. - No threshold is lowered and no default of a key named in this brief is changed to pass a target (PLAN 3 item 6, ADR 0063 point 5). A miss on the reference profile is a release blocker (14 7) and leads to the path of 14 9: first optimise query and indexes, a different engine only with a new ADR.
- The load test measures the duration of a rewrite of a search vector expression on its corpus, which the upgrade guide turns into the maintenance window of a release (14 8, RK-38), and the share of the search index in the size of the database (16 13); no fixed duration or size goes into documentation.
Scope¶
All paths under services/server/ unless they start with docs/, tools/configdoc/ or tools/ci/ or name the root Makefile or a root file. Names of M1-72's perf frame below are the expected ones; where the merged code names them differently, follow the code (Boundaries).
Create:
perf/search/(build tagperf, the one tag of M1-16 and M1-72):search_test.gowithTestPerf_Searchand its phasesmessages,prefix,probeandrewrite(design notes 7 to 13), and a package comment naming both targets. The package clause follows the convention M1-72 chose forload_messages.perf/internal/searchbench/(untagged, unit tested inmake testwithout a container; the database parts ofloader.goandrewrite.goare tested by the small-corpus tests inperf/internal/searchworld):constants.go: the named test parameters of design note 3.vocab.go: function words, generated stems with inflections, the Zipf samplers, the probe terms (design note 5).corpus.go: the deterministic generator of threads and messages by index (design note 5).plan.go: thePlanderived from configuration defaults, the values of 14 15 and the constants (design notes 3, 4 and 7).queries.go: search queries, prefix queries and probe queries per searcher (design notes 8, 9 and 11).loader.go: the bulk loader of threads and messages and the consistency queries (design note 6).rewrite.go: the rewrite and size measurement (design note 12).samples.go: samples per phase, window filter, per-class statistics (design notes 7 and 13).checks.go: the response checks of design note 10.doc.go: package documentation.
perf/internal/searchworld/(untagged):Seedbuilds spaces, channels, people, locales and grants through the public API (design note 4) and returns the handles;seed_test.goholds the small-corpus integration tests of "Tests", which need the container runtime like everypgtesttest and run inmake test.perf/internal/load/only if M1-72 kept its open-loop scheduler insideperf/load_messagesand no other ticket moved it yet: move it here without a change of behaviour so thatload_messagesandperf/searchuse one scheduler; M1-72's tests keep their names and stay green. Never write a second scheduler.
Change:
perf/internal/report(M1-16, M1-72): the threshold loader reads every row withPlaceLoad testof the tables "Configuration values of this chapter" of 02 and 14 (design note 2); extend its tests; add the exit-logic test of "Tests". No other new mechanism.perf/README.md: a sectionsearch(what is measured, the measuring point, world and corpus, load shape, phases, parameters with their values, failure conditions, how to readextra, how to runmake bench-search).- Root
Makefile: the new targetbench-search(design note 1) with a##help text and an entry in.PHONY; add-p 1to thego testcall ofperfif it does not serialize the packages yet (design note 14). tools/ci/tests/test_make_server_gates.py(M1-01):bench-searchin the skip test and absent fromgatesandcheck(Tests).docs/spec/14-search.md: 7, 8, 12 and 15 as design note 16 says (new text in English; existing text stays as it is).docs/spec/02-architecture.md: 6 the line ofperf/(thresholds also from 14 15); 17 one sentence after the table (design note 16).docs/spec/23-quality.md2: the sentence onmake perfnames the targets of 02 and 14.docs/operations/configuration.md: regenerate withmake config-reference(the three rows lose their environment name and getPlaceLoad test).tools/configdoc/planned.yaml: remove or narrow every rule that matched only the three keys of requirement 13, so rule D6 finds no stale rule; keep rules that still match a key withPlaceServer.docs/operations/upgrade.md: one new paragraph in English in section 4 on a release that changes a search vector expression (design note 12).docs/adr/0063-performance-gate-for-the-event-log.md: "Confirmation" gains a bullet for M3-16 (both search targets, durable database, profile modes, failure on a miss); "More Information" names the targets of 14 7; the decision stays unchanged.docs/adr/0032-full-text-search-in-postgresql.md: the Confirmation bullet "Load test" namesperf/searchandsearch.prefix_p95_target. Thenmake adr-index.docs/quality/gates.md:bench-searchin the section onmake perf(by hand, not inmake check).docs/RISKS.md: the column "Mitigation" of RK-38 names the measurement per million messages and the upgrade guide, that of RK-10 names the probe phase ofperf/searchas the measurement of the residual risk, and the column "Reference" of RK-10 adds M3-16 (RK-38 names it already); no rating changes.docs/DECISIONS.mdsection 10: rowsD-M3-16-<n>(design note 18).services/server/CHANGELOG.mdand the rootCHANGELOG.md(the make target) under "Unreleased".
If the base branch already carries a sentence of design note 16 (a planning change made after this brief), keep it and change nothing there.
This ticket creates no migration, no operation, no event, no metric and no runtime configuration key. It creates one new top-level make target, bench-search.
Out of scope¶
- The search itself: vectors,
search_tsquery, the language assignment and the migrations (M3-13); coordinator, read operations, ranking, highlighting and cursor (M3-14); file text extraction (M3-15). Only a fix of design note 15 may touch them. - Types
threadsandfilesand the filters of 14 4.4: 14 7 names a target only for typemessagesand for the prefix search. - The thresholds,
search.benchmark_messagesandperf.reference_profilethemselves; the other targets ofmake perf(M1-16, M1-72, M2-36, M3-24). - Running
make perfinside the release workflow (release chain, M7-10) and the milestone-close run on the reference host (owner operation after the merge, PLAN 4 item 3). - The capacity table of 16 13 (M7-18); this ticket records the index share only in
results.json. - The job "Reassigning the language" of 14 8 and any change of a vector expression.
- The offline search of the clients (M4-12, M4-23).
- Ratings in
docs/RISKS.mdand in the threat model. - Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M3-16-search-load-test; one writer per worktree. - Conventional Commits with scope (for example
test(perf): ...,docs(spec): ...,build(make): ...,perf(<module>): ...for a fix of design note 15) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief and the fixes of design note 15, each with a test and listed in the report. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1; in-process listeners use port 0, and the server subprocess ofharness.StartProcesslistens on loopback ports picked free just before its start (M1-72). PostgreSQL runs only as a throwaway container throughpgtest, withpgtest.Durable()for every measured database (P-09, P-10). Without a Docker-compatible runtime the tests fail; they never skip. - Stop only processes and containers you started, by PID or container ID. Temporary directories only via
mktemp -dort.TempDir(), removed at the end. - Never read or print secrets. API tokens and DPoP proofs never appear in logs, test output,
results.jsonor failure messages. No network calls except package and image downloads for pinned tools and Go modules. - Never lower a threshold, disable a gate or delete a test. A missed target on a host in mode
belowis reported as not proven, never "fixed" by changing numbers. - If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100). Where the merged code of an M1 or M2 ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>. - The same holds for the merged code of M1-72, M3-13 and M3-14: follow their names and list each difference in the report.
- This ticket extends the perf frame of M1-72, a dependency since D-W-108 (D-W-106 added the same edge for the load test of the agent path). If the base branch lacks
perf/internal/reportwith the profile modes and the results file, orharness.StartProcesswithOptions.DurableDB, stop withVerdict: BLOCKED perf frame missing; never rebuild that frame here, it is the scope of its own ticket. - Raised rate limits only on the seeding server; the measured server runs with the default configuration of every key.
- Direct SQL writes only in
perf/internal/searchbench(the bulk loader and the rewrite measurement of design notes 6 and 12), only through the admin pool of the throwaway container, and never while a request runs against a server on that database (TestPerf_Searchloads between the seeding and the measured server and rewrites after the measured server stopped); no production package gains a bulk path, a test hook or a second write path. - No real third-party service and no data from outside the repository: vocabulary and texts are generated (design note 5).
Design notes¶
-
Start check. Run
make checkon the fresh worktree; if it is red, stop withVerdict: BLOCKED base branch redand the failing gate. Confirm that the base has M3-14 (searchandsearchThreadTitlesenabled inapi/operations.yaml, the read operations ofmessagingandchat) and M3-13 (messages.search_vector,threads.title_search,search_tsquery,kernel/searchlang), elseVerdict: BLOCKED dependency M3-14 missing; the perf frame is checked as Boundaries say. Runmake perfonce only at the end (Definition of done); if a target of another ticket fails in that run, run that package on the base commit and report both values, so that a host limit is not mistaken for a regression of this ticket. -
Where the test lives (Ruling for docs/DECISIONS.md). The plan row names
tools/bench-searchand a Makefile target. 02 6 puts load tests underservices/server/perf/with the build tagperfand the shared report, whiletools/holds generators and gates that go-arch-lint skips (02 7.1); Go'sinternalrule keepsperf/internal/reportout of reach of a package outsideperf/, and a second results file or profile detection would contradict M1-72 (one results file, one table). So the test isperf/search, the libraryperf/internal/searchbench, the seedingperf/internal/searchworld, and the plan's tool is the make targetbench-search, the entry point for runs by hand (14 7 "by hand"). The plan row names these paths since D-W-108.Recipe of
bench-search, built likeperf(M1-01, M1-72): withoutGO_ONprint the SKIP line with theskipmacro; else$(GO_PREREQS) perf, then inservices/servergo test -tags perf -count=1 -timeout $(PERF_TIMEOUT) -run '^TestPerf_Search$$' ./perf/search/without-race, then print the result table the wayperfprints it; exit non-zero when the test fails. Not ingates,checkor the pre-push hook (P-07). Uses: by-hand runs, tuning after a missed target, and the window of a release that changes a vector expression (design note 12). -
Values of the load test (Ruling for docs/DECISIONS.md:
search.p95_target,search.prefix_p95_targetandsearch.benchmark_messagesgetPlaceLoad test). 14 15 has no columnPlace, so the generator of the reference gives the three the placeServerand environment names, although no server process reads them and 02 22 says that targets of load tests have neither. Add the columnPlaceto 14 15 (Serverfor every other row,Load testfor the three) with the sentence of design note 16, runmake config-reference, and cleantools/configdoc/planned.yaml(Scope). If the merged code registered one of the three in the configuration schema, remove it there (rule D4 of M1-08 would fail) and name it in the report.Loader in
perf/internal/report: read every row withPlaceLoad testfrom the tables "Configuration values of this chapter" of 02 and 14, each section found by its title, never by number; a name in both tables fails, a missing row fails, and so does a target or count whose value does not parse (perf.reference_profilekeeps M1-72's own parser). Classification by the column "Meaning": a meaning that contains the wordtargetis a target, a lower bound when it contains(minimum), else an upper bound; every otherLoad testrow is a parameter (perf.reference_profile,perf.sustained_messages_per_s,search.benchmark_messages). If M1-72's classifier knew only the exact meaningstarget,target (minimum)andtarget (maximum), this rule keeps their results;TestThresholdsFromChapterTableagainst the real 02 proves it.search.benchmark_messagesis a count. -
Parameters (Ruling for docs/DECISIONS.md: 14 7 fixes languages, Zipf, spaces, channels and a partial view, nothing else). Named constants in
perf/internal/searchbench/constants.go, documented with their values inperf/README.mdand copied intoresults.json:- World:
Seed = 1;Spaces = 8withTextChannelsPerSpace = 4andForumChannelsPerSpace = 4;ForumPostsPerChannel = 600;Authors = 32;Searchers = 24, each withreadon two of the spaces, so a searcher sees a quarter of the channels. - Corpus:
ContentWords = 50000stems per language;ZipfS = 1.07(natural text is close to one, andrand.NewZipfneeds a value above one);FunctionWordShare = 0.4(running text);MessageWordsMin = 3,MessageWordsMax = 40;TitleWordsMin = 2,TitleWordsMax = 6;CorpusSpan = 365 * 24h(a year, so the recency ofsearch.recency_scalematters);DeletedShare = 0.01;ProbeTermShare = 0.2;LoadBatch = 100000;ConsistencySamples = 50. - Load:
BudgetShare = 0.5(every searcher plans at most half ofrl.search);Warmup = 30s,Window = 3m,Drain = 30s,MinSamples = 1000;ProbeWarmup = 20,ProbeQueries = 200. - Query mix:
ShareOneWord = 0.40,ShareTwoWords = 0.25,ShareThreeWords = 0.15,SharePhrase = 0.10,ShareNegation = 0.10; bandsBandFrequentMax = 100,BandMediumMax = 5000withShareFrequent = 0.2,ShareMedium = 0.5,ShareRare = 0.3;SharePrefixOneWord = 0.7.
The constants form
DefaultParams();Plantakes aParamsvalue and the corpus size, soTestPerf_SearchusesDefaultParams()withsearch.benchmark_messages, and the small-corpus tests use a small world with the same code. - World:
-
World through the API (the seeding rule of M1-72: what involves rights is built through the public API by a seeding server with raised
ratelimit.*, R-QA-11).searchworld.Seedcreates, with the owner and step-up as the harness does:Spacesspaces (createSpace), each withTextChannelsPerSpacetext channels andForumChannelsPerSpaceforum channels (createChannel); a text channel gets its main thread with the channel (M1-53).Authorspeople (createUserwithlocal_account,redeemEnrollmentCodewith one device, asharness.NewUser); the first half setslocalede, the secondenthroughupdateMyProfile; each gets a grant of levelwriteon every space (createGrant).Searcherspeople,deandenalternating; searcherk(from 0) gets a grant of levelreadon the spacesk mod Spacesand(k + Spaces / 2) mod Spaces, so every space is visible to the same number of searchers. The probe space of design note 11 is the last space.- Generated handles and display names (
author-01,searcher-01,space-01,text-01-1,forum-01-1) and addresses atexample.com.
Seedtakes the owner client and a function that creates a person client, so it runs againstharness.Start(the small-corpus tests) and against the seeding process of M1-72 alike. It returns the ids and, per searcher, the set of visible channels computed from the grants it made: the test's truth, asBuildMatrixWorldkeeps it. -
Corpus (pure functions of
Seedand an index, so every run on every machine builds the same corpus and the query generator can rebuild any message without storing it):- Vocabulary per language: a list of about forty function words written in this ticket (common German and English words such as
der,und,nicht,the,and,not; facts of the language, no third-party list), andContentWordsstems generated from syllables, lower case, four to ten letters, a share of the German stems with U+00E4, U+00F6, U+00FC or U+00DF (a, o, u with diaeresis, sharp s; umlaut folding of 14 3.3). Each occurrence of a stem takes an inflection from a fixed list per language (German: none,e,en,er,es,ung,ungen,lich; English: none,s,ed,ing,er,ly), so the stemmed configurations do real work. The generator rejects a stem whose forms collide with a function word or with a form of another stem. - Words of a text: with probability
FunctionWordSharea function word (Zipf over the function words), else a content stem drawn byrand.NewZipfwithZipfSover theContentWordsranks, then inflected. The first word starts with a capital letter, the text ends with.or?. Messages haveMessageWordsMintoMessageWordsMaxwords, titlesTitleWordsMintoTitleWordsMaxcontent words; titles stay withinthread.title_max_chars, messages withinmessage.max_content_bytes. - Threads:
ForumPostsPerChannelposts per forum channel, each with a random author as creator and a title in the creator's language; the main thread of every text channel. - Messages: indexes
0toN - 1withN=search.benchmark_messages. Messageigetscreated_at = start + (i + 1) * CorpusSpan / N, withstart= load time minusCorpusSpanand the load time read once from the database clock (03 1 item 2), so time grows with the index. The first messages, one per post, open the posts; every later message picks a channel uniformly among all channels, then the main thread (text channel) or a post (forum channel) uniformly. The author is uniform among the authors, the language that of the author throughsearchlang.Map.For(the functionidentityuses since M3-13, built withsearchlang.Newfrom the defaults ofsearch.languagesandsearch.locale_map); with probabilityDeletedSharea message is deleted, never the first message of a post. - Probe: a message in a channel of the probe space carries the probe term at a random position with probability
ProbeTermShare; the absent term occurs nowhere (design note 11). Both are generated stems outside the vocabulary, checked against every form. - Randomness:
math/rand/v2with PCG seeded from (Seed,i) for messageiand from (Seed, an offset plus the post number) for titles; no global generator.
- Vocabulary per language: a list of about forty function words written in this ticket (common German and English words such as
-
Bulk loader (Ruling for docs/DECISIONS.md). Building
search.benchmark_messagesmessages throughcreateMessagewould use most of the time limit of theperftarget and measure the write path thatperf.message_create_p99of M1-72 already holds; the targets of this ticket concern the read path. So rights, people and channels come through the API (design note 4), and the content rows (thethreadsof the posts,messages, the counters of all threads) come from a bulk loader into the throwaway database, a fixture like the DM fixture of M3-14's tests. 03 1 item 12 binds the modules of the product; the loader is test support of 02 6 and never writes a running instance. P-26 concerns the demo instance, not a throwaway perf database. That the rows equal what the product writes is checked against the product in everymake test(small-corpus tests) and in every perf run (below).- Rows of
threads(posts):id(UUIDv7 fromids.NewGeneratorwith a clock standing at the row'screated_atand a seeded reader, one generator per id column because the generator never goes back in time, so ids follow time, 03 1 item 1),channel_id,kind = 'thread',title,title_source = 'manual',state = 'open',created_by,created_at(that of its first message),search_languageof the creator, and the finallast_message_seq,message_count(without deleted messages),unread_floor_seq = last_message_seq(the import rule of 03threads) andlast_activity_at(newest message). - Rows of
messages:id(UUIDv7 as above),thread_id,channel_id,thread_seq(gapless per thread in index order, from 1 in a post and after the currentlast_message_seqin a main thread),author_principal_id,client_msg_id(UUIDv7),client_payload_sha256(SHA-256 of the content),origin = 'human',kind = 'text',content_md,created_at,search_language; a deleted message has an emptycontent_md,deleted_atequal tocreated_at,deleted_bythe author andversion = 2. Every other column keeps its database default (has_attachmentsfalse); the generated columns compute the vectors. - Order:
COPYof the post threads, then of the messages in batches ofLoadBatchrows (one transaction each), then oneUPDATEof the counters of the main threads, thenVACUUM (ANALYZE)ofmessagesandthreads, so the GIN pending list is merged and the planner has statistics, as on an instance where autovacuum has run. Record each duration. - Checks after the load (each failure fails the run, naming counts and ids, never content): the count of
messagesequalssearch.benchmark_messages; one aggregate query shows per threadlast_message_seq = max(thread_seq),message_countequal to the non-deleted count andlast_activity_at = max(created_at); checks, foreign keys and unique keys are enforced by the database duringCOPY. - Checks through the product: before phase
messages, forConsistencySamplesrandom threadsgetThreadreturns the loadedtitle,last_message_seq,message_countandlast_activity_at, andlistThreadMessagesreturns the loaded messages inthread_seqorder with the loadedcontent_md. After phaseprobe, onecreateMessageper language into a loaded thread getsthread_seqequal to the previouslast_message_seqplus one and thesearch_language(read through the admin pool) that the loader wrote for that author, and onecreateThread(forum post) by an author gets thethreads.search_languagethe loader wrote for that author. - Columns follow the merged migrations. If a column the loader must fill is named differently, follow the migration and name it in the report; if the product fills a column with a value the loader cannot derive, stop with
Verdict: BLOCKED loader cannot build <column>.
- Rows of
-
Measured server, load and measuring point (Ruling for docs/DECISIONS.md: 14 7 names the target "server duration ... without network" but no load and no measuring point).
- Sequence of
TestPerf_Search: start the seeding process (harness.StartProcesson a durable database, everyratelimit.*raised);Seed; stop it; load (design note 6); start the measured process on the same database with the default configuration and the profile limits of M1-72's mode; the product checks of design note 6; phasesmessages,prefixandprobe; the writing product checks; stop the measured process; phaserewrite(design note 12); write the results. - Load: every searcher sends open loop, evenly spaced at
BudgetShareofratelimit.rl.search.per_minute, the searchers phase-shifted evenly over one interval; the plan never approachesratelimit.rl.search.burst. A request goes out at its scheduled time even while earlier ones are open (the scheduler ofperf/internal/loador the merged equivalent). Both endpoints sharerl.search, so the phases run one after the other. No background writes: 14 7 defines the target on a corpus, and the write path has its own targets. - Measuring point: from the scheduled send time to the moment the whole response body has been read, on loopback, in the test process. The sample bounds the server time from above and avoids coordinated omission (M1-72's rule for
load_messages); the client's DPoP proof lies inside the interval and only makes the bound looser. - Each phase:
Warmupwithout samples,Windowwith samples, then up toDrainfor open requests; a sample counts when its scheduled time lies in the window. The value is the nearest-rank p95 of the report. - The
Planchecks before any work that the searchers' planned rate timesWindowreachesMinSampleswith the default ofratelimit.rl.search.per_minute, else it fails naming that key and the constants.
- Sequence of
-
Query mix of phase
messages(Ruling for docs/DECISIONS.md). Every request isGET /api/v1/search?q=...with the defaults ofopenapi.yaml(typemessages, sortrelevance, no filter, no cursor, nolimit), the search a person types. Per query the language is German or English with equal chance, and the class follows the shares of design note 3: one word, two words, three words, a phrase, or a word with a negated second word (a -b). Every word of the word classes and of the negation class is a content stem from a frequency band chosen by share (frequent: content ranks 1 toBandFrequentMax; medium: up toBandMediumMax; rare: the rest), uniform within the band, with a random inflection. Reason for the bands: frequent words hit the candidate limitsearch.max_candidates, which R-SU-04 says makes the cost independent of the number of hits, and their share lies well above the five percent a p95 may ignore, so the target cannot pass on rare words alone. A phrase is two adjacent words, at least one of them a content word, of a random message that is not deleted and lies in a channel the searcher may read, written"a b"; it must find a hit (design note 10). No query consists of function words only, none contains a probe term, and every query stays withinsearch.query_max_chars. -
Prefix queries of phase
prefix:GET /api/v1/search/thread-titles?q=...withoutlimit. A query takes a random post in a channel the searcher may read and one word of its title (shareSharePrefixOneWord) or two, each cut to a random length betweensearch.prefix_min_charsand its full length, joined by a space; it must find a title (design note 10). Main threads have an empty title and never match (M3-14). -
Failure conditions (Ruling for docs/DECISIONS.md: a measurement counts only when the path behaved correctly while it was taken).
TestPerf_Searchfails, andmake perfexits non-zero, when a p95 misses its threshold or on any of:- a status other than
200, among them429 rate_limitedand503 unavailablewithreasonsearch_timeout; - a body that does not decode strictly (unknown members rejected) into
SearchHitPageorThreadTitleHitList, or a hit ofkindother thanmessagein phasemessages; - a hit whose
message.channel_id, or a title hit whosechannel_id, the searcher may not read by the truth of design note 4; - a phrase or prefix query without a hit;
- a probe answer with an item, with
candidates_cappedtrue or withnext_cursor; - a check of design note 6 or 12, a corpus whose count differs from
search.benchmark_messages, fewer thanMinSamplessamples in a phase.
Failure messages name counts, handles, ids, the phase and the query class and the query (synthetic words), never message content.
- a status other than
-
Phase
probe(RK-10, T-AZ-02; Ruling for docs/DECISIONS.md: measured without a threshold, because 14 4.3 accepts the residual risk and names no bound). The searchers without a grant on the probe space send, at the rate of design note 7, queries for the probe term (frequent, but only in channels they cannot read) and for the absent term (nowhere), alternating:ProbeWarmupof each unmeasured, then untilProbeQueriesof each are measured. Both answers must be equal: no item,candidates_cappedfalse, nonext_cursor(design note 10).extrarecords p50 and p95 of both terms and their differences, the measurement of the residual risk that RK-10 rates and the red team of M3 checks. -
Phase
rewriteand sizes (14 8, RK-38, 16 13; Ruling for docs/DECISIONS.md: measured per million messages, no fixed number in documentation, because the duration depends on host and corpus). After the measured process stopped, through the admin pool:- Sizes first:
pg_table_sizeofmessages,pg_relation_sizeof every index onmessages.search_vectorand onthreads.title_search, the size of the database, the share of these indexes in it, each also per million messages. - Read the generation expression of
messages.search_vector(pg_get_exproverpg_attrdefof the column), its nullability and the definition of every index on it (pg_indexes.indexdef); never copy the expression from a migration into code. - Time, in one transaction,
ALTER TABLE messages DROP COLUMN search_vectorfollowed byALTER TABLE messages ADD COLUMN search_vector tsvector ... GENERATED ALWAYS AS (<expression>) STOREDwith the saved nullability: the drop changes only the catalog and removes the index, the add rewrites the table once, as a changed expression in a release does. Then time each saved index definition, built withoutCONCURRENTLYbecause a maintenance window has no writers. Recordmaintenance_work_memwith the times. - Check afterwards: expression and index definitions in the catalog equal the saved ones, and one fixed phrase of the corpus, queried as SQL with
search_tsqueryand@@, returns the same message ids before and after. extrarecords the row count andrewrite_seconds,rewrite_seconds_per_million,index_build_secondsandindex_build_seconds_per_million.
docs/operations/upgrade.md(a new paragraph in English, section 4 after the table): a release that changes an expression of a search vector rewritesmessages(14 8); that is the known exception to rule M3 and needs a maintenance window; the release notes name its expected duration under "Migrations" (section 1 item 1), computed from the values per million messages of the search load test (make bench-search, the valuesrewrite_seconds_per_millionandindex_build_seconds_per_millionunderextra.searchin.cache/perf/results.json) times the number of messages of the instance in millions. No number in the guide. - Sizes first:
-
Results through
perf/internal/report(M1-72): rowssearch.p95_targetandsearch.prefix_p95_targetwithvaluethe p95 in the unit of the 14 row,thresholdandcomparisonfrom 14 15,pass,samples,test(perf/search); profile and durability fields unchanged from M1-72.extraunder one keysearch: the corpus counts (messages, deleted messages, posts, main threads, channels, spaces) next tosearch.benchmark_messages; searchers, visible share, planned and achieved rates; per phase p50, p99 and maximum; per query class and per frequency band count and p95; the share of answers withcandidates_capped; the durations of seeding,COPYandVACUUM (ANALYZE); the probe values (design note 11); sizes and rewrite values (design note 12); the PostgreSQL settingsshared_buffers,work_memandmaintenance_work_memas read; the defaults ofsearch.max_candidates,search.recency_scaleandsearch.statement_timeoutin effect; all constants. PostgreSQL runs with the defaults of its image and the durability ofpgtest.Durable(), without tuning (Ruling for docs/DECISIONS.md: 16 prescribes no tuning forbothy-postgres, so the shipped database runs with the image defaults too). The table line of each target shows value, threshold, pass and the corpus size. -
Serial packages and duration. Two load tests on one host at the same time measure each other: if the
perfrecipe does not pass-p 1togo testyet, add it (Ruling for docs/DECISIONS.md, as in M2-36). Report the duration of each step ofTestPerf_Searchand of the wholemake perf. If the time limit of theperftarget (PERF_TIMEOUT, M1-01) no longer suffices for the package, raise it together with its named value in chapter 23 in the same change (the guard test of M1-01 keeps both equal) and record a row; never shrink the corpus orWindow. -
A missed target in mode
matchoremulatedwith a durable database: find the cause withEXPLAIN (ANALYZE, BUFFERS)of the read operations of M3-14 for the slowest classes and bands inextra, and fix it where it is owned (the SQL ininternal/messaging/adapters/postgres/queries/search.sqlorinternal/chat/adapters/postgres/queries/search.sql, or the coordinator ininternal/search/app), each fix with a test that holds the behaviour, the M3-14 tests green, each fix in the report. Never lower a threshold, never change a default of a key named here, never weaken the rights filter, the page check or the candidate limit. A new index or a changed vector expression changes 03 (core contract): stop withVerdict: BLOCKED target missedwith the table, the plans and a proposed change (14 9 point 1, ADR 0063 point 5). In modebelowreport the values as not proven. -
Spec texts. Every text this ticket adds to
docs/spec/,docs/operations/,docs/adr/,docs/RISKS.md,docs/quality/,docs/DECISIONS.mdand the READMEs is English (owner rule: everything added to the repository is English); existing text around it stays as it is, and the section titles keep their names. The valuesServerandLoad testof the columnPlaceare identifiers that the reference generator and M1-08's tooling read (02 22 andtools/docs/config_reference.pyuse them already), so they stay as they are.- 14 7, below the table, a new paragraph: "Measuring point: from the scheduled send time to the fully read response over loopback, an upper bound of the server time; the server runs with its default configuration; open load below
ratelimit.rl.search.per_minuteper person; requests with the defaults ofopenapi.yaml. The testperf/searchruns inmake perf;make bench-searchruns it by hand. The same run measures the rewrite ofmessages.search_vectorper million messages (section 8), the share of the search indexes in the database size and the time difference between a term that occurs only in channels the searcher cannot read and a term that occurs nowhere (residual risk of 4.3)." - 14 8 "Changing the expression": replace the clause "the upgrade guide ... gives the measured duration from the load test" with the English sentence "The load test measures the duration per million messages (section 7); the upgrade guide (
docs/operations/upgrade.md) says how the release notes turn it into the maintenance window." - 14 12, row "Load test", column of the expectation: "
search.p95_targetatsearch.benchmark_messagesandsearch.prefix_p95_target(perf/search)". - 14 15: the column
Place(Serverfor every other row,Load testfor the three keys), and above the table the sentence: "ColumnPlace:Server= configuration reference of the server;Load test= threshold or parameter of the load test likeperf.*in 02 section 22, without an environment name and not in the configuration schema;make perfreads them from this table." - 02 6, the line of
perf/: append "(thresholds from section 22 and from 14 section 15)". - 02 17, after the table, a new sentence: "The performance targets of the search (
search.p95_target,search.prefix_p95_target) stand in 14 section 7;perf/searchholds them in the samemake perf." - 23 2, the sentence on
make perf: append "(with the performance targets of 02 and 14)".
- 14 7, below the table, a new paragraph: "Measuring point: from the scheduled send time to the fully read response over loopback, an upper bound of the server time; the server runs with its default configuration; open load below
-
Configuration keys used, read by name, no new key, no changed default:
perf.reference_profile,search.p95_target,search.prefix_p95_target,search.benchmark_messages(through the report from 02 and 14), and through the defaults ofinternal/platform/config:ratelimit.rl.search.per_minute,ratelimit.rl.search.burst,search.languages,search.locale_map,search.query_max_chars,search.prefix_min_chars,search.max_candidates,search.statement_timeout,search.recency_scale,thread.title_max_chars,message.max_content_bytes. The seeding server raises theratelimit.*keys through the harnessConfigmap, never through code. -
Rulings for
docs/DECISIONS.md(section 10, one rowD-M3-16-<n>each, with date, decision, reason and place): the location and the make target (design note 1);PlaceLoad testfor the three keys and the loader rule (design note 2); the parameters (design note 3); the bulk loader with its checks (design note 6);VACUUM (ANALYZE)before measuring (design note 6); the load and the measuring point (design note 7); the query mix and the bands (design note 8); the failure conditions (design note 10); the probe phase without threshold (design note 11); the rewrite measurement per million and no number in documentation (design note 12); PostgreSQL without tuning (design note 13);-p 1and a raised time limit, each only if made (design note 14).
Tests (write them first)¶
Unit tests (untagged, in make test, no container needed):
perf/internal/report:TestThresholdsFromChapterTable(or its merged equivalent) also reads the real 14:search.p95_targetandsearch.prefix_p95_targetwith unitmsand comparison<=,search.benchmark_messagesas a parameter of kind count; the results for 02 are unchanged.TestLoadTestRowsAcrossChapters: with fixture copies, a name in the tables of both 02 and 14 fails, a row withPlaceServeris never loaded, a missingsearch.p95_targetfails, a meaningtarget (minimum)gives>=,p95 target in the load testgives<=,corpus size in the load testgives a parameter.TestReport_SearchTargetsFailAboveThreshold: a fixture result forsearch.p95_targetat the threshold plus one millisecond makes the exit logic fail and marks that row failed, a result exactly at the threshold passes; the same forsearch.prefix_p95_target; a passing value withfsyncoff in the profile still ends not green.
perf/internal/searchbench:TestVocabulary_Deterministic: the sameSeedgives the same function words, stems, inflections and probe terms; another seed gives other stems.TestVocabulary_NoCollisions: no form of a stem equals a function word or a form of another stem; the probe term and the absent term equal no form; every stem has four to ten letters; some German stems contain U+00E4, U+00F6, U+00FC or U+00DF (a, o, u with diaeresis, sharp s).TestZipf_Shape: over a large sample the mean count per rank falls from rank 1 to ranks 2 to 10, 11 to 100 and 101 to 1000, and the share of rank 1 lies within a stated tolerance of the valueZipfSgives.TestCorpus_PureByIndex: messageibuilt twice is equal in every field;created_atgrows strictly withi; the first messages open the posts in order; the share of deleted messages lies within a stated tolerance ofDeletedShareand no first message is deleted; the probe term occurs only in channels of the probe space and the absent term nowhere; every text stays withinmessage.max_content_bytes, every title withinthread.title_max_chars; the language of every message issearchlang.Map.Forof its author's locale.TestPlan_FromDefaults: channel, post and searcher counts follow the constants;Nequalssearch.benchmark_messagesof the real 14; every searcher sees two spaces; every space is visible to the same number of searchers; exactly the searchers without the probe space are probe searchers; the planned rate per searcher isBudgetShareofratelimit.rl.search.per_minute; the planned samples perWindowreachMinSamples.TestPlan_FollowsKeys: halvingratelimit.rl.search.per_minutehalves the planned rate; a value too low forMinSamplesgives an error namingratelimit.rl.search.per_minute.TestQueries_Mix: over ten thousand queries the class and band shares lie within a stated tolerance; every phrase is two adjacent words, at least one a content word, of a message that is not deleted and lies in a channel its searcher may read; negation has the forma -b; no query consists of function words only or contains a probe term; every query stays withinsearch.query_max_chars; the same seed gives the same sequence.TestPrefixQueries: every word has at leastsearch.prefix_min_charscharacters and is a prefix of a word of the title of a post in a channel the searcher may read; the one-word share lies within tolerance.TestLoaderRows(the row builder, without database):thread_seqis gapless per thread, from 1 in a post and after the givenlast_message_seqin a main thread; the counters of every thread equal the aggregates of its messages andunread_floor_seqequalslast_message_seq; a deleted row has an emptycontent_md,deleted_atequal tocreated_at,deleted_bythe author andversion2; every id is a UUIDv7 whose time equalscreated_atto the millisecond, ascending withcreated_at;client_payload_sha256is the SHA-256 of the content.TestSamples_Window: samples scheduled inWarmupor afterWindoware left out; the p95 is the nearest rank of the report; fewer thanMinSamplessamples fail; statistics per class and band add up to the totals.TestChecks: each condition of design note 10 fails with a message that names phase, class and query and contains no message content; a correct answer passes.
Makefile tests (tools/ci/tests/test_make_server_gates.py of M1-01, pytest, in make test):
test_server_gates_skip_without_selected_go_module(or its merged equivalent) also runsbench-searchand expects itsSKIPline and exit 0 without a selected Go module.test_gates_contains_server_gates_but_not_perf(or its merged equivalent) also asserts thatbench-searchis a prerequisite neither ofgatesnor ofcheck.
Integration tests (untagged, in make test; PostgreSQL 18 through pgtest, the real server through harness.Start with raised ratelimit.*; a small plan of four spaces with one text and one forum channel each, four searchers, a few posts and two thousand messages, so every searcher sees half of the spaces; perf/internal/searchworld/seed_test.go):
TestSmallCorpus_SeedLoadAndProductAgree:Seed, then the loader; the aggregate check passes;getThreadandlistThreadMessagesagree with the loader for every thread;createMessageinto a loaded post and into a main thread continuesthread_seqand stores thesearch_languagethe loader wrote for that author;createThreadby adeand by anenauthor stores thethreads.search_languagethe loader wrote for them.TestSmallCorpus_SearchFindsLoadedRows: a German and an English phrase of loaded messages are found bysearchfor a searcher who sees their channel and give zero items for a searcher who does not;searchThreadTitleswith a prefix of a loaded title returns the post; for a searcher without the probe space the probe term and the absent term give equal answers.TestSmallCorpus_RewriteKeepsDefinitions: the rewrite of design note 12 on the small corpus leaves expression and index definitions in the catalog unchanged, the fixed phrase returns the same ids before and after, and all sizes are above zero.
Tagged test (build tag perf, run by make perf and make bench-search): TestPerf_Search as in design notes 7 to 13. It has no red run before its package exists; the red runs to keep for the report are those of the tests above.
Commands that must pass, from the repository root:
cd services/server && go test -race -count=1 ./perf/internal/...
cd services/server && go vet -tags perf ./perf/...
make config-reference
make config-doc-check
make adr-index
make docs
make bench-search
make perf
make check
Acceptance criteria¶
-
make perfrunsTestPerf_Searchand writessearch.p95_targetandsearch.prefix_p95_targetwith value, threshold, comparison, pass and samples, together with the corpus size and the profile mode, into.cache/perf/results.jsonand the table: the p95 againstsearch.p95_targetatsearch.benchmark_messages. -
make perfexits non-zero when one of the two is missed (TestReport_SearchTargetsFailAboveThreshold). - The corpus holds exactly
search.benchmark_messagesmessages in German and English with Zipf-distributed words over several spaces and channels; every searcher sees a part of the channels; the queries have one to three words, phrases and negation (14 7). - Thresholds and corpus size come only from 14 15, where they now carry
PlaceLoad test; the configuration reference shows them without environment name, and no value is a literal in code or docs. - The measured server runs with the default configuration on a durable database; no request is rate limited or times out; every hit comes from a channel the searcher may read; every phrase and prefix query finds something.
- The probe term and the absent term give equal answers, and their time difference is in
results.json. - Rewrite and index build per million messages and the share of the search indexes are in
results.json; 14 8 and the upgrade guide say how the release notes use them, without a number. - The small-corpus tests prove in
make testthat the loader writes what the product writes. -
make bench-searchruns only the search load test and is not part ofmake check. - On the build host both targets pass in mode
matchoremulated; in modebelowthe report lists them as not proven. - 14, 02, 23, the configuration reference, ADR 0063, ADR 0032, the upgrade guide,
docs/RISKS.md, the plan row anddocs/DECISIONS.mdcarry this ticket. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why.
services/server/perf/README.mdupdated (the search test, measuring point, world and corpus, load, phases, parameters with their values, failure conditions, extras,make bench-search).- The spec texts of design note 16, the regenerated
docs/operations/configuration.md,tools/configdoc/planned.yaml, the upgrade guide, ADR 0063 and ADR 0032 anddocs/RISKS.mdcarry this ticket; Rulings of design note 18 recorded indocs/DECISIONS.mdsection 10 (one row each with reason). services/server/CHANGELOG.mdand rootCHANGELOG.mdentries under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen andmake perfrun on the build host with its full table in the report (PLAN 3 item 6).- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. The red team checks at least: the measuring point cannot under-report (scheduled send time, whole body read); a leak, a wrong count, a timeout or a rate limit cannot end green; the corpus loader cannot diverge from the product unnoticed; the probe phase measures the time side channel of the search (PLAN 9 "Red team"); no threshold or key default was changed. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check, the fullmake perftable with profile mode, host data (logical CPUs, memory,GOMAXPROCS) and durability fields, theextravalues of the search test (corpus, rates, per-class and per-band p95,candidates_cappedshare, probe values, sizes, rewrite and index build per million) and the duration of each step and ofmake perf. - Review and red team verdicts and the findings fixed; every fix of design note 15 with its test.
- What is not proven (at least: the values on
perf.reference_profilewhen the mode was neithermatchnoremulated), and each difference between this brief and the merged names of M1-72, M3-13 or M3-14. - Follow-ups that could not be done in this scope, each with its reason, and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.