Hi Surya,
On Fri, Oct 23, 2026 at 2:50 PM surya poondla <[email protected]> wrote:
> It's bounded though, a capped process
> gets at most roughly 1.6-1.8x its share there, and only once nworkers *
> cost_delay gets near 1s.
I measured it on master + v2 with vacuum_cost_delay = 100ms, 20 btree
indexes and 10% of the rows deleted, N = 3. Cost spent per second, as
a multiple of the budget:
limit 200, PARALLEL 4/8/16: 1 s cap 1.08x to 1.18x, v2 1.10x to 1.17x
limit 20, PARALLEL 4: 1 s cap 1.05x, v2 1.05x
limit 20, PARALLEL 16: 1 s cap 1.54x to 1.55x, v2 1.07x
So the cap shows up only with many workers and a small limit, and it
stays within your bound.
> Regarding the WaitLatch() for HEAD, worth remembering it was tried before
> (4753ef37e0e) and reverted in 720de00af49 because the millisecond timeout
> broke fractional cost_delay.
Chao's loop as posted sleeps the whole milliseconds in WaitLatch() and
the rest in pg_usleep(). Fractional delays matched v2: 0.96x to 0.97x
for both at 2.5ms, 0.92x to 0.95x for both at 0.5ms. It also held the
budget in the runs above: 1.02x to 1.04x at limit 200, 1.03x at
limit 20.
One thing is independent of the cap: at 100ms, v2 itself runs at
1.10x to 1.19x. On Unix pg_usleep() is nanosleep()
(src/port/pgsleep.c:50), which returns early when a signal handler
runs, and v2 does not go back to sleep. The leader got 25 SIGUSR1 at
PARALLEL 4 and 47 at PARALLEL 16 during one VACUUM (VERBOSE), one per
worker message (src/backend/libpq/pqmq.c:187), e.g. the index
progress update (vacuumparallel.c:1217). A diagnostic build of v2
that only resumes the interrupted sleep gives 1.02x to 1.03x. At 10ms
with a larger table v2 measured 1.00x, in line with your runs.
On interrupts, pg_cancel_backend() took 5.0 to 16.1 ms with v2 and
8.5 to 14.9 ms with the loop (N = 10). If the cancel lands right
before the sleep (forced with gdb), v2 sleeps the full 6.8 s at
PARALLEL 16, the 1 s cap about 1 s, and the loop 0.4 to 3.2 ms.
Setup and all values are in the attached measurements.txt. One Linux
machine with other jobs running; the ratios repeat within 0.08 across
rounds. Windows not measured.
regards,
Manu
Parallel vacuum v2 at vacuum_cost_delay = 100ms: 1 s cap and WaitLatch loop
Linux x86_64, master 5ffb8c74093, no assertions, -O2. Builds:
v2 master + v2-0001
1 s cap v2 + "msec = Min(msec, 1000)" after the v2 cap in
compute_parallel_delay() (Bharath's option 2)
loop v2 with pg_usleep(msec * 1000) replaced by Chao's loop as
posted: CHECK_FOR_INTERRUPTS(), WaitLatch() for the whole
milliseconds, pg_usleep() for the rest, remaining time
recomputed from a start timestamp. make check passes.
resume diagnostic only: v2 with pg_usleep() called again for the
remaining time when it returns early.
Table: 10000 rows, 21 int columns, 20 btree indexes, 10% of rows
deleted. Default page costs. Cost spent = hits + 2 * misses + 20 *
dirtied from the VACUUM VERBOSE buffer usage (includes the workers),
divided by elapsed time, as a multiple of the budget
(vacuum_cost_limit / vacuum_cost_delay). N = 3, rounds alternate the
builds, all values listed.
1. vacuum_cost_limit = 200 (budget 2000 units/s)
PARALLEL master v2 1 s cap
4 2.72 2.83 2.72 1.12 1.11 1.10 1.10 1.10 1.10
8 3.44 3.63 3.04 1.11 1.15 1.13 1.08 1.11 1.14
16 5.07 3.81 3.64 1.13 1.17 1.10 1.09 1.18 1.10
PARALLEL loop
4 1.03 1.03 1.03
8 1.02 1.02 1.02
16 1.04 1.04 1.03
The 1 s cap does not change anything measurable here.
2. vacuum_cost_limit = 20 (budget 200 units/s)
PARALLEL master v2 1 s cap loop
4 2.75 2.75 2.76 1.05 1.05 1.06 1.05 1.05 1.05 1.03 1.03 1.03
16 3.52 3.54 3.54 1.07 1.07 1.07 1.54 1.54 1.55 1.03 1.03 1.03
Elapsed at PARALLEL 16: v2 74.7-74.9 s, 1 s cap 51.8-52.0 s, loop
77.8-78.0 s.
3. Why v2 is above the budget at 100ms
Same setup as 1, PARALLEL 4 and 16, rounds alternating v2 / resume /
loop:
v2 1.10 1.18 | 1.11 1.19 | 1.10 1.13
resume 1.03 1.03 | 1.02 1.03 | 1.03 1.03
loop 1.03 1.04 | 1.03 1.04 | 1.03 1.04
SIGUSR1 received by the leader during one VACUUM (PARALLEL n, VERBOSE)
(gdb catch signal on the leader): v2 25 at PARALLEL 4, 47 at 16; loop
25 and 50. No SIGURG.
pg_usleep() on a Unix backend is nanosleep() (src/port/pgsleep.c:50,
under #if defined(FRONTEND) || !defined(WIN32)), which returns early
when a signal handler runs. Workers send PROCSIG_PARALLEL_MESSAGE to
the leader for each message (src/backend/libpq/pqmq.c:187), for example
the index progress update (src/backend/commands/vacuumparallel.c:1217).
Resuming the sleep is enough to bring v2 to the loop's numbers.
For reference, on 2026-10-22 with vacuum_cost_delay = 10ms and 200000
rows, v2 was at 1.00x at PARALLEL 4, 8 and 16.
4. Interrupt latency
VACUUM (PARALLEL 16), vacuum_cost_delay = 100ms, vacuum_cost_limit = 1,
page_hit = page_miss = 0, page_dirty = 20: every sleep is at the cap
(6.8 s with v2 and the loop, 1 s with the 1 s cap). Time from sending
the signal to the client getting the error, psql startup included.
pg_cancel_backend, v2 and loop alternated, N = 10 each:
v2 5.0 7.7 8.4 9.0 10.4 11.5 12.7 13.0 14.9 16.1 ms
loop 8.5 9.3 9.3 9.4 10.1 10.2 10.3 11.1 14.2 14.9 ms
Forced race: gdb stops the leader right before the sleep and sets
QueryCancelPending and InterruptPending as the handler would; for the
loop it stops at WaitLatch() entry (VacuumDelay only) and also calls
SetLatch(MyLatch), which the handler does too.
v2 6767 6780 6786 ms (N = 3)
1 s cap 960 969 980 986 986 ms (N = 5)
loop 0.4 0.8 0.9 0.9 0.9 1.0 1.1 1.1 2.0 3.2 ms (N = 10)
5. Fractional vacuum_cost_delay (the reason 4753ef37 was reverted)
Table of 200000 rows (otherwise the same), budget 40000 units/s in both
cases, N = 3, rounds alternate the builds:
2.5ms, limit 100, PARALLEL 4: v2 0.96 0.96 0.96, loop 0.96 0.96 0.96
2.5ms, limit 100, PARALLEL 16: v2 0.97 0.97 0.97, loop 0.96 0.97 0.97
0.5ms, limit 20, PARALLEL 4: v2 0.92 0.92 0.92, loop 0.92 0.92 0.93
0.5ms, limit 20, PARALLEL 16: v2 0.95 0.94 0.94, loop 0.95 0.94 0.94
Caveats: one machine (Ryzen AI 9 HX 370, Linux 7.2), data on tmpfs,
fsync = off. The lane monitor flagged CPU contention from other jobs
during most runs. The ratios of v2, 1 s cap, loop and resume repeat
within 0.08 across rounds (master varies more: 3.64-5.07 at PARALLEL 16,
limit 200); the millisecond latencies are more sensitive to it. The race
is emulated with gdb, which models the outcome, not how often it
happens. Windows not measured.