Skip to content

Read non-future-helper marker without SPI - #330

Merged
pinodeca merged 1 commit into
mainfrom
perf/non-future-helper-marker-without-spi
Aug 4, 2026
Merged

Read non-future-helper marker without SPI#330
pinodeca merged 1 commit into
mainfrom
perf/non-future-helper-marker-without-spi

Conversation

@pinodeca

@pinodeca pinodeca commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Implements Step 0 of docs/plan-graph-rework.md.

Problem

Durofut::ensure calls same_statement_non_future_helper_name before doing anything else, and that helper issued SPI queries to read the df.non_future_helper session GUC and statement_timestamp(). mark_non_future_helper_call issued a third for set_config.

Every plain-SQL operand flowing through a composer paid a full plan + execute round trip. For the small graphs users actually write, this very likely dominates the composition cost.

Change

Read and write the marker through the C API instead:

  • GetConfigOption replaces current_setting()
  • set_config_option with GUC_ACTION_LOCAL replaces set_config(..., true)
  • GetCurrentStatementStartTimestamp replaces statement_timestamp()

No planner, no executor.

The marker now carries the internal TimestampTz integer rather than a formatted timestamp string. The GUC is transaction-local and is only ever written and read within a single statement by the same binary, so the encoding is private and needs no compatibility handling. The one unit test that synthesizes the marker in SQL is updated to match.

Behaviour

Unchanged. Same detection semantics, same error messages, same transaction-local scope. The existing helper-misuse tests passing unchanged is the whole acceptance criterion.

Why this is first

Two reasons from the plan:

  1. It is almost certainly the largest win in the programme for realistically-sized graphs, and it carries no design risk.
  2. Measurement hygiene. While an SPI round trip sits in the composer's hot path it masks every parsing difference, and Step 5's decision gate depends on being able to see them.

It also shrinks what Step 5 has to preserve: this check is the only hard error the composers currently raise.

Upgrade & migration

None. No schema change, no upgrade script, no wire-format change, no runtime schema detection. No SQL query text changes, so B1 backward compatibility is unaffected — the new .so issues no new or altered queries against any schema version.

Testing

  • cargo check --features pg17 — clean
  • cargo clippy --features pg17 -- -D warnings — clean
  • cargo fmt -p pg_durable -- --check — clean
  • ./scripts/test-unit.sh cannot_be_used_in_seq_composition — 4 passed

The four covering tests exercise df.setvar, df.unsetvar, df.clearvars and df.await_instance misuse detection against a real PG17 backend, including the manually synthesized marker path.

Durofut::ensure consults the df.non_future_helper marker GUC on every
non-JSON operand, and mark_non_future_helper_call writes it. Both went
through SPI, so each composer call paid a full plan+execute round trip
for current_setting() and statement_timestamp() -- on the hot path for
every plain-SQL operand in a chain.

Read and write the marker through the C API instead: GetConfigOption,
set_config_option with GUC_ACTION_LOCAL, and
GetCurrentStatementStartTimestamp. No planner, no executor.

The marker now carries the internal TimestampTz integer rather than a
formatted timestamp string. The GUC is transaction-local and is only
ever written and read within a single statement by the same binary, so
the encoding is private and needs no compatibility handling. The unit
test that synthesizes the marker in SQL is updated to match.

Behaviour is unchanged: same detection semantics, same error messages.

No schema change, no upgrade script, no SQL query text changes.
@pinodeca

pinodeca commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Measured impact

A/B of the same cargo pgrx install build (debug, --features http-allow-test-domains), PG 17, measured in a real backend.

Binary verification. The build timestamp reported at startup is not reliable here — build.rs does not re-run on checkout, so it kept reporting a stale value across reinstalls. Each side was instead confirmed by probing the marker's private encoding, which differs between the two implementations:

BEGIN;
SELECT df.setvar('p','1');
SELECT replace(current_setting('df.non_future_helper', true), E'\n', ' | ');
COMMIT;
  • maindf.setvar | 2026-08-04 21:46:39.047235+00 (formatted timestamp)
  • this branch → df.setvar | 839195079338397 (raw TimestampTz)

Fixed cost per composer call

x := 'select 1' ~> 'select 2' in a plpgsql loop, 20 000 calls, minus a || control loop to subtract plpgsql overhead. Both operands are plain SQL, so each call performs 2 marker lookups = 4 SPI queries on main. Warm, repeated runs:

µs/call
main 35.4, 34.9, 35.4
this branch 6.5, 6.7

~5.4× faster on the composer's fixed cost; ~29 µs saved per call, i.e. ~7 µs per eliminated SPI round trip.

Note the first run after a server restart is cold (11.9 µs on this branch) and understates the gain; the numbers above are warm steady state.

Independent cross-check

Left-deep ~> chain, best of 5:

n main (ms) this branch (ms)
10 0.825 0.580
50 19.752 11.525
100 59.328 47.606
200 201.910 203.688

At n=10 that is ~25 µs/step. A chain step does only 1 lookup — the accumulator starts with { and hits the starts_with('{') fast path — so 2 SPI queries. The two benchmarks therefore agree at ~13 µs per lookup, derived from independent measurements.

Caveat

This is a fixed per-call win, so it does not help long chains: per-step cost still climbs 58 → 230 → 476 → 1018 µs as n goes 10 → 200, and by n=200 the saving is inside the noise of the O(N²) accumulator copying. What this change does is remove the SPI cost that was previously masking that quadratic term.

@pinodeca
pinodeca merged commit 5f37239 into main Aug 4, 2026
5 checks passed
@pinodeca
pinodeca deleted the perf/non-future-helper-marker-without-spi branch August 4, 2026 22:01
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.

2 participants