Skip to content

test: disable Postgres JIT, release retained test servers, and reorder slow packages - #29375

Draft
kylecarbs wants to merge 6 commits into
mainfrom
kyle/pg-test-jit-off
Draft

kylecarbs wants to merge 6 commits into
mainfrom
kyle/pg-test-jit-off

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Profiling of test-go-pg found that the suite is CPU-bound on the 8-core runner, split roughly 50/50 between the Go test binaries and PostgreSQL, that a large share of the PostgreSQL half is spent LLVM JIT-compiling queries on tiny, statistics-less test databases, and that the coderd test binary retains every test server it ever started until the process exits. This PR disables JIT for the test PostgreSQL, uses FILE_COPY when cloning template databases on PostgreSQL 15+, releases the cleanup closures that retained finished test servers, runs the slowest packages first, and parallelizes the two Terraform tests that formed the enterprise/coderd tail.

Draft, to measure the effect in CI. No tests are removed or skipped.

CI measurements

Runner variance is large, so the numbers below are the DONE ... in Xs line from gotestsum on uncached (-count=1) Linux runs.

PG13 PG17
main, five consecutive runs 370, 380, 418, 435, 534 385, 414, 424, 564, 608
this PR, two uncached runs with all changes 348, 329 350, 341

Both PR samples are below every main sample. Per-package effects that are visible regardless of noise: coderd/database 172-328s -> 47-77s, and the post-enterprise/coderd tail is gone (last packages to finish are now small ones).

Problem

PostgreSQL JIT

pg_stat_statements on the coderd/database package attributed 354s of 469s total statement time to GetUserStatusCounts (756 calls, ~470ms each) on nearly empty databases. EXPLAIN ANALYZE shows why: the planner estimates 5.7M rows where 61 exist, the cost crosses jit_above_cost/jit_optimize_above_cost, and PostgreSQL runs LLVM inlining and optimization for every execution: 317.7ms with jit=on vs 0.31ms with jit=off. Any complex query on a fresh test database is exposed to this.

Locally, with go test and PostgreSQL 13 pinned to 8 CPUs, jit=off moved the full suite from 317s to 286s wall and cut PostgreSQL CPU from 1155s to 902s (-22%); coderd/database went from 152s to 40s. In CI the coderd/database package went from 172-328s on recent main runs to 77s (PG13) and 43s (PG17).

On PostgreSQL 15+, the default CREATE DATABASE strategy is WAL_LOG, which roughly doubles per-clone CPU compared to FILE_COPY for the ~9100 short-lived databases the suite creates (52.7ms vs 27.8ms per create+connect+drop with shared_buffers=2GB). This is part of why test-go-pg-17 was slower than the PG13 job.

Retained test servers

testing.T removes finished cleanups by reslicing c.cleanups, which leaves each closure in the backing array, and every top-level test stays reachable from the test binary's root until it exits. Helper cleanups in coderdtest, coderdenttest, agenttest, and aibridgedtest captured the coderd.API (directly or via a dial closure), its chi router, NATS server, provisioner daemon, or agent. Weak-pointer tracking in coderdtest.NewWithAPI showed 469 of 1607 APIs still reachable at the end of the coderd package run, with live heap climbing from 17MB to ~4GB and GC at ~20% of Go CPU.

enterprise/coderd tail

enterprise/coderd ran alone for the final ~75s because TestWorkspaceTagsTerraform (67s) and TestWorkspaceTemplateParamsChange (14s) used t.Setenv(TF_CLI_CONFIG_FILE), which forces them to run serially before the package's parallel tests start. go test ./... also runs packages alphabetically, so enterprise/coderd only started ~330s into the run.

Fix

  • -c jit=off in make test-postgres-docker, ALTER SYSTEM SET jit = 'off' in scripts/embedded-pg/main.go, and -c jit=off in dbtestutil.openContainer.
  • dbtestutil.createDatabaseSQL appends STRATEGY = FILE_COPY when server_version_num >= 150000.
  • New testutil.Cleanup(t, f) registers a holder that drops its reference to f once it has run, with a test that fails when swapped for plain t.Cleanup. Used for the heavyweight helper cleanups. coderd package: APIs reachable at end of run 469/1607 -> 9/1607, peak RSS ~7.0GB -> ~1.2GB.
  • coderd.New stores the tailnet resume signing keycache on API and closes it in API.Close so its refresh timer does not outlive the API.
  • coderdenttest.WithTerraformCLIConfigPath passes terraform.ServeOptions.CliConfigPath explicitly; both Terraform tests drop t.Setenv and run in parallel, including their subtests.
  • make test lists the slowest packages first when TEST_PACKAGES is ./... (go test preserves argument order and deduplicates). Custom TEST_PACKAGES values are unchanged.
  • Linux test-go-pg jobs run with GOGC=200. With retention fixed this is safe: on the coderd package it cuts Go user CPU 14% (90s -> 77s) for a peak RSS of 2.1GB instead of 1.2GB. GOGC=400 saves 18% but needs 5.7GB, so it was not chosen.
Profiling notes and follow-ups

Method

  • Reconstructed the per-package CI timeline from gotestsum output on recent main runs. Wall time on the shared runners varies a lot between runs (PG13 DONE ranged 370-534s across five consecutive main runs), so per-package times and CPU accounting are the reliable signals.
  • Reproduced locally with go test and the PostgreSQL container pinned to 8 CPUs (taskset, --cpuset-cpus, GOMAXPROCS=8).
  • Measured Go CPU (/usr/bin/time), PostgreSQL CPU (container cgroup cpu.stat), per-test timing (go test -json), Go CPU and heap profiles, GC trace, and PostgreSQL statement time (pg_stat_statements with a 500k entry table, since every test database gets its own entries).
  • Microbenchmarked CREATE DATABASE ... WITH TEMPLATE / DROP DATABASE on PG13 and PG17 with different shared_buffers and clone strategies.
  • Found retention roots with viewcore on a core dump (runtime.main -> testing.tRunner -> []*testing.T -> cleanup backing array -> closure -> API), then tracked every API created by coderdtest.NewWithAPI with weak.Pointer to attribute the remaining retainers to specific helpers.

Where the time goes

  • CI (main, PG13, -count=1): DONE 31255 tests in 435s, 283 packages, per-package sum 1419s. Longest: coderd 227s, coderd/database 176s, cli 122s, coderd/database/migrations 119s, enterprise/coderd 105s.
  • Compilation is not the bottleneck in CI: Depot persists GOCACHE (a PR run showed 165 packages (cached)). A fully cold compile of all test binaries on 8 cores costs 3m26s / 26 CPU-minutes.
  • Local baseline pinned to 8 CPUs: wall 317s, Go user+sys 1181s, PostgreSQL 1155s, i.e. 7.4 of 8 cores busy throughout. Leaf-test slot time is 7431s over up to 64 slots, so -parallel is not the limit.
  • Go init() is not a factor: the coderd/database binary used 2.25s CPU over a 33s run.
  • Per-database lifecycle: ~9100 CREATE/DROP DATABASE pairs per run; on PG13 both force an immediate checkpoint that scans all of shared_buffers (17k requested checkpoints). About 11% of PG CPU. Smaller shared_buffers saves ~25% of that.
  • Connections: ~14.5k for the coderd package alone (~9 per test database, ~2-3ms PG CPU each). NewDB opens a dedicated connection just for ALTER DATABASE ... SET TIMEZONE.
  • Go side of coderd: OPA/rego evaluation ~22% of CPU, GC ~20%, coderdtest.NewWithAPI 13.5% (including aibridge/prices.Seed, which parses a 189KB JSON and upserts ~1000 rows per test server), agentrsa.GenerateDeterministicKey 4%.
  • The 9 APIs still reachable after this PR come from test-local t.Cleanup closures that capture api (for example setupDynamicParamsTest), which is negligible.

Follow-ups

  • Parse the embedded aibridge/prices JSON once per process instead of per coderd.New.
  • Reuse the broker connection for the timezone ALTER DATABASE in dbtestutil.NewDB.
  • The job is CPU-bound, so sharding test-go-pg across runners or a larger runner scales wall time almost linearly at similar total cost.
  • TestGetUserStatusCounts creates ~900 databases; one per timezone would remove most of them.

Generated by Coder Agents on behalf of @kylecarbs.

Test databases are cloned from a template, are tiny, and have no
planner statistics, so cost estimates for complex queries are wildly
inflated and cross jit_above_cost. PostgreSQL then LLVM-compiles the
query: GetUserStatusCounts takes 318ms with JIT and 0.3ms without on a
test database. Disable JIT in every place the test PostgreSQL is
configured (Makefile container, embedded PostgreSQL on macOS/Windows,
dbtestutil's fallback container).

Request STRATEGY = FILE_COPY when cloning the template on PostgreSQL 15
or newer; the WAL_LOG default roughly doubles per-database CPU for the
thousands of short-lived, fsync=off databases the suite creates.

Run TestWorkspaceTagsTerraform subtests in parallel. Each subtest uses
its own coderd, provisioner daemon, and plugin cache, and only the
read-only provider mirror is shared. The test dominated the
enterprise/coderd package, which is the last package to finish.
…allelize tests

TestWorkspaceTagsTerraform and TestWorkspaceTemplateParamsChange used
t.Setenv(TF_CLI_CONFIG_FILE), which forced both to run serially and made
them the ~80s tail of the enterprise/coderd package. Thread the path
through terraform.ServeOptions.CliConfigPath via a new coderdenttest
option so both tests and their subtests can run in parallel.
… first

testing.T removes finished cleanups by reslicing, which leaves each
closure in the backing array, and top-level tests stay reachable until
the binary exits. Helper cleanups that captured a coderd.API, its
router, NATS server, provisioner daemon, agent, or aibridged daemon
therefore kept every test server alive for the whole package run.

Add testutil.Cleanup, which drops its reference to the closure once it
has run, and use it for those helpers. In the coderd package this cuts
APIs still reachable at the end of the run from 469/1607 to 9/1607,
peak RSS from ~7.0GB to ~1.2GB, and end-of-run live heap from ~4GB to
well under 1GB, which shrinks every GC cycle.

Also close the tailnet resume signing keycache created by coderd.New in
API.Close so its refresh timer does not outlive the API, and list the
slowest packages first in make test so enterprise/coderd no longer
starts near the end of the run and finishes last.
@kylecarbs kylecarbs changed the title test: disable Postgres JIT for test databases and parallelize tag tests test: disable Postgres JIT, release retained test servers, and reorder slow packages Sep 15, 2026
Locally on the coderd package GOGC=200 cuts Go user CPU 14% (90s to
77s) for a peak RSS of 2.1GB instead of 1.2GB; GOGC=400 saves 18% but
needs 5.7GB. Limited to Linux since the macOS runners are memory
constrained.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant