Skip to content

Commit 798917c

Browse files
authored
VLE cache + performance improvements (#2376)
VLE cache + perf: version counter, thin entries, variadic elimination, edge cleanup list removal Replace snapshot-based VLE cache invalidation with per-graph version counters and add performance optimizations for VLE traversal. Cache invalidation: - Add DSM (PG 17+) and shmem (PG <17) per-graph monotonic version counters for cross-backend cache invalidation - Replace snapshot xmin/xmax/curcid comparison with version counter check in is_ggctx_invalid(), with snapshot fallback for safety - Add executor hooks in CREATE/DELETE/SET/MERGE to increment the graph version counter on mutations - Add SQL trigger function (age_invalidate_graph_cache) for catching SQL-level mutations (INSERT/UPDATE/DELETE/TRUNCATE) - Auto-install trigger on new label tables via label_commands.c with LookupFuncName check for backward compatibility - Add TRUNCATE interception in ProcessUtility hook (ag_catalog.c) - Add shmem_request/startup hooks for PG <17 (age.c) - Use search_label_relation_cache() in get_graph_oid_for_table() for fast label-to-graph lookup instead of catalog table scan Thin entries (lazy property fetch): - Replace Datum edge_properties/vertex_properties with 6-byte ItemPointerData TID in vertex_entry and edge_entry - Add get_vertex_entry_properties() and get_edge_entry_properties() that do heap_fetch via stored TID on demand - Add is_an_edge_match() fast path: skip property access entirely for label-only VLE patterns (the common case) Edge cleanup list removal: - Remove ggctx->edges linked list from GRAPH_global_context. With thin entries, edge_entry has no palloc'd sub-structures to free (TIDs are inline). The cleanup list existed to pfree datumCopy'd edge_properties Datums, which no longer exist. hash_destroy() handles all edge hash table memory. Saves ~5.6 GB at SF10. Performance optimizations: - Reduce VERTEX/EDGE_HTAB_INITIAL_SIZE from 1,000,000 to 10,000. PG dynahash grows automatically; large initial size wastes memory. - Eliminate extract_variadic_args in age_match_vle_terminal_edge: direct PG_GETARG_DATUM + cached arg types via fn_extra - Eliminate extract_variadic_args in age_match_vle_edge_to_id_qual: same pattern - Add agtype_access_operator 2-arg fast path: bypasses extract_variadic_args_min for the common property access case - Add age_tointeger 1-arg fast path: bypasses extract_variadic_args with cached arg type - Add GraphIdStack (flat array-based) DFS stacks replacing ListGraphId linked-list stacks for push/pop without palloc/pfree Upgrade script: - Add trigger installation on pre-existing label tables during extension upgrade via DO block in age--1.7.0--y.y.y.sql. Tables created after upgrade get triggers automatically via label_commands.c. Regression tests: - Add VLE cache invalidation tests (CREATE, DELETE, SET mutations) - Add thin entry edge property fetch tests (RETURN p, UNWIND) - Add direct SQL trigger tests (INSERT, UPDATE, DELETE, TRUNCATE on label tables with VLE cache invalidation verification) All 32 regression tests pass. SF3 Benchmarks (9.3M vertices, 52.7M edges, warm cache, median): Total IC1-IC12: 1,530s -> 1,159s (-24.3%, 371s saved) VLE-heavy queries: IC3 (KNOWS*1..2 + 2 countries): 34.9s -> 20.7s (-40.6%) IC5 (forum members, KNOWS*1..2): 46.2s -> 29.9s (-35.4%) IC6 (tag co-occurrence, KNOWS*1..2): 28.2s -> 19.2s (-31.8%) IC9 (recent messages, KNOWS*1..2): 86.0s -> 60.4s (-29.7%) IC11 (KNOWS*1..2 + WORK_AT): 18.7s -> 11.0s (-41.5%) IC1 (KNOWS*1..3 + profile): 1269.4s -> 974.9s (-23.2%) Short Reads (IS1-IS7): No meaningful change -- non-VLE queries, sub-millisecond to sub-second. Within run-to-run variance. Updates (IU1-IU8, SF3, median, 50 ops each): IU2 (Like Post): 99.5ms -> 6.3ms (-93.6%) IU3 (Like Comment): 318.3ms -> 5.7ms (-98.2%) IU7 (Add Comment): 344.4ms -> 11.3ms (-96.7%) IU5 (Forum Member): 12.3ms -> 5.9ms (-52.0%) Version counter eliminates redundant VLE cache rebuilds on mutations. Previously, every INSERT/UPDATE/DELETE invalidated the cache via snapshot comparison, forcing a full rebuild on the next VLE query. Now, mutations just bump a counter; rebuild only occurs when VLE actually runs and finds the counter changed. Graph cache memory (SF3, calculated): Total: ~15.7 GB -> ~8.7 GB (-45%, 7.0 GB saved) Thin entries (TID replaces datumCopy'd properties): -5.3 GB Edge cleanup list removal (no longer needed): -1.7 GB Co-authored-by: Claude Opus <noreply@anthropic.com> modified: age--1.7.0--y.y.y.sql modified: regress/expected/age_global_graph.out modified: regress/sql/age_global_graph.sql modified: sql/age_main.sql modified: src/backend/age.c modified: src/backend/catalog/ag_catalog.c modified: src/backend/commands/label_commands.c modified: src/backend/executor/cypher_create.c modified: src/backend/executor/cypher_delete.c modified: src/backend/executor/cypher_merge.c modified: src/backend/executor/cypher_set.c modified: src/backend/utils/adt/age_global_graph.c modified: src/backend/utils/adt/age_graphid_ds.c modified: src/backend/utils/adt/age_vle.c modified: src/backend/utils/adt/agtype.c modified: src/include/utils/age_global_graph.h modified: src/include/utils/age_graphid_ds.h
1 parent ee25e2a commit 798917c

17 files changed

Lines changed: 1629 additions & 266 deletions

age--1.7.0--y.y.y.sql

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,54 @@ $function$;
408408

409409
COMMENT ON FUNCTION ag_catalog.age_pg_upgrade_status() IS
410410
'Returns the current pg_upgrade readiness status of the AGE installation.';
411+
412+
--
413+
-- VLE cache invalidation trigger function
414+
-- Installed on graph label tables to catch SQL-level mutations
415+
-- and increment the per-graph version counter for VLE cache invalidation.
416+
--
417+
CREATE FUNCTION ag_catalog.age_invalidate_graph_cache()
418+
RETURNS trigger
419+
LANGUAGE c
420+
AS 'MODULE_PATHNAME';
421+
422+
--
423+
-- Install the cache invalidation trigger on all pre-existing label tables.
424+
-- New label tables created after this upgrade will get the trigger
425+
-- automatically via label_commands.c. This DO block handles tables
426+
-- that were created before the upgrade.
427+
--
428+
DO $$
429+
DECLARE
430+
r RECORD;
431+
BEGIN
432+
FOR r IN
433+
SELECT n.nspname AS schema_name, c.relname AS table_name
434+
FROM ag_catalog.ag_label l
435+
JOIN pg_catalog.pg_class c ON c.oid = l.relation
436+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
437+
WHERE l.name != '_ag_label_vertex'
438+
AND l.name != '_ag_label_edge'
439+
LOOP
440+
-- Skip if trigger already exists on this table
441+
IF NOT EXISTS (
442+
SELECT 1 FROM pg_catalog.pg_trigger t
443+
JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
444+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
445+
WHERE n.nspname = r.schema_name
446+
AND c.relname = r.table_name
447+
AND t.tgname = '_age_cache_invalidate'
448+
)
449+
THEN
450+
EXECUTE format(
451+
'CREATE TRIGGER _age_cache_invalidate '
452+
'AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE '
453+
'ON %I.%I '
454+
'FOR EACH STATEMENT '
455+
'EXECUTE FUNCTION ag_catalog.age_invalidate_graph_cache()',
456+
r.schema_name, r.table_name
457+
);
458+
END IF;
459+
END LOOP;
460+
END;
461+
$$;

regress/expected/age_global_graph.out

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,294 @@ NOTICE: graph "ag_graph_3" has been dropped
413413

414414
(1 row)
415415

416+
-----------------------------------------------------------------------------------------------------------------------------
417+
--
418+
-- VLE cache invalidation tests
419+
--
420+
-- These tests verify that the graph version counter properly invalidates
421+
-- the VLE hash table cache when the graph is mutated, and that thin
422+
-- entry lazy property fetch returns correct data.
423+
--
424+
-- Setup: create a graph with a chain a->b->c->d
425+
SELECT * FROM create_graph('vle_cache_test');
426+
NOTICE: graph "vle_cache_test" has been created
427+
create_graph
428+
--------------
429+
430+
(1 row)
431+
432+
SELECT * FROM cypher('vle_cache_test', $$
433+
CREATE (a:Node {name: 'a'})-[:Edge]->(b:Node {name: 'b'})-[:Edge]->(c:Node {name: 'c'})-[:Edge]->(d:Node {name: 'd'})
434+
$$) AS (v agtype);
435+
v
436+
---
437+
(0 rows)
438+
439+
-- VLE query: find all paths from a's neighbors (should find b, b->c, b->c->d)
440+
SELECT * FROM cypher('vle_cache_test', $$
441+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
442+
RETURN n.name
443+
ORDER BY n.name
444+
$$) AS (name agtype);
445+
name
446+
------
447+
"b"
448+
"c"
449+
"d"
450+
(3 rows)
451+
452+
-- Now add a new node e connected to d. This should invalidate the cache.
453+
SELECT * FROM cypher('vle_cache_test', $$
454+
MATCH (d:Node {name: 'd'})
455+
CREATE (d)-[:Edge]->(:Node {name: 'e'})
456+
$$) AS (v agtype);
457+
v
458+
---
459+
(0 rows)
460+
461+
-- VLE query again: should now also find e via a->b->c->d->e (4 hops won't reach,
462+
-- but d->e is 1 hop from d, and a->b->c->d->e would be 4 hops from a).
463+
-- Increase range to *1..4 to include e
464+
SELECT * FROM cypher('vle_cache_test', $$
465+
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
466+
RETURN n.name
467+
ORDER BY n.name
468+
$$) AS (name agtype);
469+
name
470+
------
471+
"b"
472+
"c"
473+
"d"
474+
"e"
475+
(4 rows)
476+
477+
-- Test cache invalidation on DELETE: remove node c and its edges
478+
SELECT * FROM cypher('vle_cache_test', $$
479+
MATCH (c:Node {name: 'c'})
480+
DETACH DELETE c
481+
$$) AS (v agtype);
482+
v
483+
---
484+
(0 rows)
485+
486+
-- VLE query: should only find b now (c is gone, so b->c path is broken)
487+
SELECT * FROM cypher('vle_cache_test', $$
488+
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
489+
RETURN n.name
490+
ORDER BY n.name
491+
$$) AS (name agtype);
492+
name
493+
------
494+
"b"
495+
(1 row)
496+
497+
-- Test cache invalidation on SET: change b's name property
498+
SELECT * FROM cypher('vle_cache_test', $$
499+
MATCH (b:Node {name: 'b'})
500+
SET b.name = 'b_modified'
501+
RETURN b.name
502+
$$) AS (name agtype);
503+
name
504+
--------------
505+
"b_modified"
506+
(1 row)
507+
508+
-- VLE query: verify the updated property is returned via lazy fetch
509+
SELECT * FROM cypher('vle_cache_test', $$
510+
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
511+
RETURN n.name
512+
ORDER BY n.name
513+
$$) AS (name agtype);
514+
name
515+
--------------
516+
"b_modified"
517+
(1 row)
518+
519+
-- Test VLE with edge properties (exercises thin entry edge property fetch)
520+
SELECT * FROM drop_graph('vle_cache_test', true);
521+
NOTICE: drop cascades to 4 other objects
522+
DETAIL: drop cascades to table vle_cache_test._ag_label_vertex
523+
drop cascades to table vle_cache_test._ag_label_edge
524+
drop cascades to table vle_cache_test."Node"
525+
drop cascades to table vle_cache_test."Edge"
526+
NOTICE: graph "vle_cache_test" has been dropped
527+
drop_graph
528+
------------
529+
530+
(1 row)
531+
532+
SELECT * FROM create_graph('vle_cache_test2');
533+
NOTICE: graph "vle_cache_test2" has been created
534+
create_graph
535+
--------------
536+
537+
(1 row)
538+
539+
SELECT * FROM cypher('vle_cache_test2', $$
540+
CREATE (a:N {name: 'a'})-[:E {weight: 1}]->(b:N {name: 'b'})-[:E {weight: 2}]->(c:N {name: 'c'})
541+
$$) AS (v agtype);
542+
v
543+
---
544+
(0 rows)
545+
546+
-- VLE path output to verify edge properties are fetched correctly via
547+
-- thin entry lazy fetch. Returning the full path forces build_path()
548+
-- to call get_edge_entry_properties() for each edge in the result.
549+
-- The output must contain the correct weight values (1 and 2).
550+
SELECT * FROM cypher('vle_cache_test2', $$
551+
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
552+
RETURN p
553+
ORDER BY n.name
554+
$$) AS (p agtype);
555+
p
556+
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
557+
[{"id": 844424930131969, "label": "N", "properties": {"name": "a"}}::vertex, {"id": 1125899906842626, "label": "E", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"weight": 1}}::edge, {"id": 844424930131970, "label": "N", "properties": {"name": "b"}}::vertex]::path
558+
[{"id": 844424930131969, "label": "N", "properties": {"name": "a"}}::vertex, {"id": 1125899906842626, "label": "E", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"weight": 1}}::edge, {"id": 844424930131970, "label": "N", "properties": {"name": "b"}}::vertex, {"id": 1125899906842625, "label": "E", "end_id": 844424930131971, "start_id": 844424930131970, "properties": {"weight": 2}}::edge, {"id": 844424930131971, "label": "N", "properties": {"name": "c"}}::vertex]::path
559+
(2 rows)
560+
561+
-- VLE edge properties via UNWIND + relationships() to individually verify
562+
-- each edge's properties are correctly fetched from the heap via TID.
563+
SELECT * FROM cypher('vle_cache_test2', $$
564+
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
565+
WITH p, n
566+
UNWIND relationships(p) AS e
567+
RETURN n.name, e.weight
568+
ORDER BY n.name, e.weight
569+
$$) AS (name agtype, weight agtype);
570+
name | weight
571+
------+--------
572+
"b" | 1
573+
"c" | 1
574+
"c" | 2
575+
(3 rows)
576+
577+
-- Cleanup
578+
SELECT * FROM drop_graph('vle_cache_test2', true);
579+
NOTICE: drop cascades to 4 other objects
580+
DETAIL: drop cascades to table vle_cache_test2._ag_label_vertex
581+
drop cascades to table vle_cache_test2._ag_label_edge
582+
drop cascades to table vle_cache_test2."N"
583+
drop cascades to table vle_cache_test2."E"
584+
NOTICE: graph "vle_cache_test2" has been dropped
585+
drop_graph
586+
------------
587+
588+
(1 row)
589+
590+
-----------------------------------------------------------------------------------------------------------------------------
591+
--
592+
-- VLE cache invalidation via direct SQL (trigger tests)
593+
--
594+
-- These tests verify that the SQL trigger (age_invalidate_graph_cache)
595+
-- properly invalidates the VLE cache when label tables are mutated
596+
-- via direct SQL INSERT, UPDATE, DELETE, and TRUNCATE — not via Cypher.
597+
--
598+
-- Setup: create graph with a chain a->b->c using Cypher
599+
SELECT * FROM create_graph('vle_trigger_test');
600+
NOTICE: graph "vle_trigger_test" has been created
601+
create_graph
602+
--------------
603+
604+
(1 row)
605+
606+
SELECT * FROM cypher('vle_trigger_test', $$
607+
CREATE (a:Node {name: 'a'})-[:Edge]->(b:Node {name: 'b'})-[:Edge]->(c:Node {name: 'c'})
608+
$$) AS (v agtype);
609+
v
610+
---
611+
(0 rows)
612+
613+
-- Prime the VLE cache: find all nodes reachable from a
614+
SELECT * FROM cypher('vle_trigger_test', $$
615+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
616+
RETURN n.name
617+
ORDER BY n.name
618+
$$) AS (name agtype);
619+
name
620+
------
621+
"b"
622+
"c"
623+
(2 rows)
624+
625+
-- Direct SQL INSERT on vertex: add a new vertex via SQL.
626+
-- This should fire the trigger and invalidate the VLE cache.
627+
-- Use _graphid() with the label's id and nextval for the entry id.
628+
INSERT INTO vle_trigger_test."Node" (id, properties)
629+
SELECT ag_catalog._graphid(l.id,
630+
nextval('vle_trigger_test."Node_id_seq"'::regclass)),
631+
'{"name": "d"}'::agtype
632+
FROM ag_catalog.ag_label l
633+
JOIN ag_catalog.ag_graph g ON g.graphid = l.graph
634+
WHERE g.name = 'vle_trigger_test' AND l.name = 'Node';
635+
-- VLE query: results should be unchanged (d has no edges) but cache was rebuilt
636+
SELECT * FROM cypher('vle_trigger_test', $$
637+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
638+
RETURN n.name
639+
ORDER BY n.name
640+
$$) AS (name agtype);
641+
name
642+
------
643+
"b"
644+
"c"
645+
(2 rows)
646+
647+
-- Direct SQL UPDATE on vertex: change b's name to 'b_updated'
648+
-- This should fire the trigger and invalidate the VLE cache.
649+
UPDATE vle_trigger_test."Node"
650+
SET properties = '{"name": "b_updated"}'::agtype
651+
WHERE properties @> '{"name": "b"}'::agtype;
652+
-- VLE query: verify updated property is visible (cache invalidated by trigger)
653+
SELECT * FROM cypher('vle_trigger_test', $$
654+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
655+
RETURN n.name
656+
ORDER BY n.name
657+
$$) AS (name agtype);
658+
name
659+
-------------
660+
"b_updated"
661+
"c"
662+
(2 rows)
663+
664+
-- Direct SQL DELETE on edge: remove the edge from b_updated to c
665+
DELETE FROM vle_trigger_test."Edge"
666+
WHERE end_id = (SELECT id FROM vle_trigger_test."Node"
667+
WHERE properties @> '{"name": "c"}'::agtype);
668+
-- VLE query: only b_updated reachable now (edge to c is gone)
669+
SELECT * FROM cypher('vle_trigger_test', $$
670+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
671+
RETURN n.name
672+
ORDER BY n.name
673+
$$) AS (name agtype);
674+
name
675+
-------------
676+
"b_updated"
677+
(1 row)
678+
679+
-- Direct SQL TRUNCATE on edge table: remove all edges
680+
TRUNCATE vle_trigger_test."Edge";
681+
-- VLE query: no edges exist, should return no rows
682+
SELECT * FROM cypher('vle_trigger_test', $$
683+
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
684+
RETURN n.name
685+
ORDER BY n.name
686+
$$) AS (name agtype);
687+
name
688+
------
689+
(0 rows)
690+
691+
-- Cleanup
692+
SELECT * FROM drop_graph('vle_trigger_test', true);
693+
NOTICE: drop cascades to 4 other objects
694+
DETAIL: drop cascades to table vle_trigger_test._ag_label_vertex
695+
drop cascades to table vle_trigger_test._ag_label_edge
696+
drop cascades to table vle_trigger_test."Node"
697+
drop cascades to table vle_trigger_test."Edge"
698+
NOTICE: graph "vle_trigger_test" has been dropped
699+
drop_graph
700+
------------
701+
702+
(1 row)
703+
416704
-----------------------------------------------------------------------------------------------------------------------------
417705
--
418706
-- End of tests

0 commit comments

Comments
 (0)