Bound per-row memory in writable clauses - #2517
Open
crdv7 wants to merge 1 commit into
Open
Conversation
SET and REMOVE create EState-owned tuple slots for every updated entity and run transient builders in an executor-lifetime memory context. DELETE RLS slots have the same ownership problem, while MERGE SET shares the update path. Large writable statements therefore retain slots and scratch until executor shutdown. Use standalone slots for per-row writes and RLS checks, release them explicitly, and allocate update/delete scratch in the writable CustomScan's per-tuple context. Reset that context before each input row while retaining node-lifetime slots in the EState. Copy properties for paths retained by MERGE's cross-row de-duplication state into the query-lifetime context. Add eager multi-row MERGE coverage for ON CREATE SET and ON MATCH SET, plus terminal and non-terminal repeated-path coverage across row-context resets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SETandREMOVEcurrently create EState-owned tuple slots for every updatedentity and allocate transient update state in an executor-lifetime memory
context. These resources remain until executor shutdown, so a large writable
statement becomes progressively more expensive and retains memory in
proportion to the number of processed rows. The UPDATE and DELETE RLS paths
have the same per-row slot ownership problem, and
MERGE ... SETshares theproperty-update path.
This PR gives row-local slots and scratch a row-local lifetime while preserving
the executor- and statement-lifetime state that must survive across rows.
It does not change Cypher semantics, agtype serialization, entity lookup, or
the on-disk representation.
Root cause
apply_update_list()creates a label-table write slot withExecInitExtraTupleSlot()for every updated entity. That API appends the slotto
EState.es_tupleTableand pins itsTupleDescresource until executorshutdown. Temporary RLS slots in the UPDATE and DELETE paths use the same API.
Instrumentation shows that the tuple table grows once per updated row:
apply_update_list()andprocess_delete_list()also run their directallocations and downstream agtype builders while the current allocation
context is executor-lifetime. Row-local arrays, hash tables, parse state,
temporary strings, entity wrappers, and serialized values therefore
accumulate until the statement ends.
CPU profile
The following profile is Apache AGE master at
80141740, runningSETover100,000 vertices with two properties. It was sampled with:
ResourceOwnerAddToHash()accounts for 66.74% of sampled CPU andResourceOwnerForget()for 27.98%. Both stacks originate from the per-rowEState-owned slots described above.
The same workload on the final patched commit has no
ResourceOwnerAddToHash()orResourceOwnerForget()samples. CPU time isdistributed across the actual property rebuild and PostgreSQL tuple/index
update paths; reclaiming the preceding row's memory context accounts for
1.08% of the patched profile.
Implementation
MakeSingleTupleTableSlot()for the per-entity SET/REMOVE write slotand the per-row UPDATE/DELETE RLS slots, then release each with
ExecDropSingleTupleTableSlot()in the same row.ecxt_per_tuple_memory, and restore the caller's memory context beforereturning.
SET,REMOVE, DELETE,or eager MERGE input row.
reused for the node lifetime.
created_paths_listas statement-lifetime de-duplication state.Properties for a newly retained path are copied to
es_query_cxtbefore therow context is reset. Duplicate paths do not incur that copy.
The per-row bound applies to expression and writable-clause scratch. MERGE's
created_paths_liststill retains one entry per unique path created by thestatement, and its eager result buffer retains one tuple per result row. Both
are required across input rows and are released when the MERGE node ends.
An audit of all
ExecInitExtraTupleSlot()call sites found three per-rowuses that require this change. MERGE insert slots, initialization helpers, and
executor-shutdown scans are created once for their intended lifetime and stay
unchanged.
Performance
Environment and method
801417404978823bd8732452c3f7959017584785(Apache master)
1ebaf28617d669b25dcfb9b3a1c5545f879210b9--disable-debug --disable-cassert,CFLAGS=-O2 -gshared_buffers=1GB,fsync=off,synchronous_commit=off,full_page_writes=off,autovacuum=off,jit=offThe two variants use separate databases bound to their exact
age.sofiles.Each writable workload shown below has five measured runs in AB/BA-interleaved
variant order. Fixture reset,
VACUUM ANALYZE, and library selection areoutside the timed interval. Elapsed values are medians; CV is the sample
coefficient of variation. Memory is the maximum private mapping total sampled
from the backend's
/proc/<pid>/smaps_rollupduring each statement.The workloads exercise different parts of the writable executor:
SETnew key rebuilds each entity's property map while inserting a property.SETexisting key replaces a value already present in the property map.REMOVErebuilds the property map while omitting one existing key.SET += mapexercises whole-map merge rather than a single-property update.DELETEdoes not rebuild or serialize properties, so it isolates theper-row slot and scratch-lifetime cost more directly.
100,000 vertices, two properties
SETnew keySETexisting keyREMOVESET += mapDELETEAcross the four property-update workloads, the median maximum private memory is
1.00-1.12 GiB on master and 23-26 MiB with the patch.
For
DELETE, it falls from approximately 271 MiB to 22 MiB.100,000 vertices, 100 properties
SETnew keySETexisting keyREMOVESET += mapAcross the wide-property workloads, the median maximum private memory is
4.90-5.62 GiB on master and 0.33-0.34 GiB with the patch.
The narrow workload demonstrates the slot-lifetime cost directly. The
100-property workload also magnifies scratch retained in the wrong memory
context. Remaining width-dependent agtype serialization work is intentionally
outside this PR.
Reproduction
For a quick timing comparison, run the attached SQL once against Apache master
and once against this branch:
The script builds the fixture outside the measured statements and uses a
transaction rollback after every workload so that each starts from the same
data.
For elapsed time plus backend RSS/private-memory sampling on Linux, use the
attached one-command runner for each installed build:
Set
KEYS=100for the wide-property case. The runner emits local results underwrite-memory-results/and also coversMERGE ... ON MATCH SET; generatedresult files do not need to be attached to the PR.
repro_write_memory_lifetime.sql
run_write_memory_benchmark.sh
Testing
COPT=-Werror: 43/43 regression testspassed.
MEMORY_CONTEXT_CHECKING: 43/43 regressiontests passed, including the new multi-row and repeated-path MERGE coverage.
Suggested review order
src/backend/executor/cypher_set.c: standalone per-row slots and theper-tuple allocation boundary.
src/backend/executor/cypher_delete.c: DELETE scratch and RLS slotownership.
src/backend/executor/cypher_merge.c: per-row reset points and preservationof cross-row de-duplication properties.
regress/sql/cypher_merge.sqland expected output.