From 833cddedb090025e17f5ed58936748e171fa709e Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Tue, 30 Jun 2026 08:17:40 +0200 Subject: [PATCH] feat(parser): parse hash buckets --- src/enums.ts | 5 ++ src/filters.ts | 12 ++++ .../__tests__/12-line-wrapped-plans/01-expect | 2 +- .../__tests__/12-line-wrapped-plans/02-expect | 2 +- .../__tests__/12-line-wrapped-plans/05-expect | 2 +- .../__tests__/12-line-wrapped-plans/06-expect | 4 +- .../__tests__/12-line-wrapped-plans/07-expect | 4 +- .../__tests__/from-text/45-buckets-expect | 23 ++++++++ .../__tests__/from-text/45-buckets-plan | 23 ++++++++ .../__tests__/from-text/46-buckets-expect | 24 ++++++++ .../__tests__/from-text/46-buckets-plan | 56 +++++++++++++++++++ src/services/plan-service.ts | 28 ++++++++++ 12 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/services/__tests__/from-text/45-buckets-expect create mode 100644 src/services/__tests__/from-text/45-buckets-plan create mode 100644 src/services/__tests__/from-text/46-buckets-expect create mode 100644 src/services/__tests__/from-text/46-buckets-plan diff --git a/src/enums.ts b/src/enums.ts index a949ee8e..5d776150 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -106,6 +106,8 @@ export enum Property { GROUP_COUNT = "Group Count", GROUP_KEY = "Group Key", HASH_CONDITION = "Hash Cond", + HASH_BATCHES = "Hash Batches", + HASH_BUCKETS = "Hash Buckets", HEAP_FETCHES = "Heap Fetches", INDEX_NAME = "Index Name", IO_READ_TIME = "I/O Read Time", @@ -123,10 +125,13 @@ export enum Property { NODE_ID = "nodeId", NODE_TYPE = "Node Type", OPERATION = "Operation", + ORIGINAL_HASH_BATCHES = "Original Hash Batches", + ORIGINAL_HASH_BUCKETS = "Original Hash Buckets", OUTPUT = "Output", PARALLEL_AWARE = "Parallel Aware", PARENT_RELATIONSHIP = "Parent Relationship", PARTIAL_MODE = "Partial Mode", + PEAK_MEMORY_USAGE = "Peak Memory Usage", PEAK_SORT_SPACE_USED = "Peak Sort Space Used", PEV_PLAN_TAG = "plan_", PLANNER_ESTIMATE_DIRECTION = "*Planner Row Estimate Direction", diff --git a/src/filters.ts b/src/filters.ts index 32a57365..06b93188 100644 --- a/src/filters.ts +++ b/src/filters.ts @@ -67,6 +67,13 @@ export function formatCost(value: unknown): string { return (value as number).toLocaleString(undefined, { minimumFractionDigits: 2 }) } +export function formatNumber(value: unknown): string { + if (value === undefined) { + return "N/A" + } + return (value as number).toLocaleString() +} + export function formatRows(value: unknown): string { if (value === undefined) { return "N/A" @@ -245,6 +252,8 @@ const nodePropFormatters: Partial> = { [Property.EXCLUSIVE_TEMP_WRITTEN_BLOCKS]: formatBlocksHtml, [Property.FULL_SORT_GROUPS]: formatSortGroups, [Property.HEAP_FETCHES]: formatRows, + [Property.HASH_BATCHES]: formatNumber, + [Property.HASH_BUCKETS]: formatNumber, [Property.IO_READ_TIME]: formatDuration, [Property.IO_WRITE_TIME]: formatDuration, [Property.JIT]: formatJit, @@ -254,8 +263,11 @@ const nodePropFormatters: Partial> = { [Property.LOCAL_IO_WRITE_TIME]: formatDuration, [Property.LOCAL_READ_BLOCKS]: formatBlocksHtml, [Property.LOCAL_WRITTEN_BLOCKS]: formatBlocksHtml, + [Property.ORIGINAL_HASH_BATCHES]: formatNumber, + [Property.ORIGINAL_HASH_BUCKETS]: formatNumber, [Property.OUTPUT]: formatList, [Property.PARALLEL_AWARE]: formatBoolean, + [Property.PEAK_MEMORY_USAGE]: formatKilobytes, [Property.PLANNER_ESTIMATE_DIRECTION]: formatEstimateDirection, [Property.PLANNER_ESTIMATE_FACTOR]: formatFactor, [Property.PLAN_ROWS]: formatRows, diff --git a/src/services/__tests__/12-line-wrapped-plans/01-expect b/src/services/__tests__/12-line-wrapped-plans/01-expect index eb835753..0c098f5b 100644 --- a/src/services/__tests__/12-line-wrapped-plans/01-expect +++ b/src/services/__tests__/12-line-wrapped-plans/01-expect @@ -54,7 +54,7 @@ "Actual Total Time": 0.062, "Actual Rows": 2, "Actual Loops": 1, - "Buckets": 1024, + "Hash Buckets": 1024, "Plans": [ { "Node Type": "Seq Scan", diff --git a/src/services/__tests__/12-line-wrapped-plans/02-expect b/src/services/__tests__/12-line-wrapped-plans/02-expect index eb835753..0c098f5b 100644 --- a/src/services/__tests__/12-line-wrapped-plans/02-expect +++ b/src/services/__tests__/12-line-wrapped-plans/02-expect @@ -54,7 +54,7 @@ "Actual Total Time": 0.062, "Actual Rows": 2, "Actual Loops": 1, - "Buckets": 1024, + "Hash Buckets": 1024, "Plans": [ { "Node Type": "Seq Scan", diff --git a/src/services/__tests__/12-line-wrapped-plans/05-expect b/src/services/__tests__/12-line-wrapped-plans/05-expect index 1fa85d19..d401938a 100644 --- a/src/services/__tests__/12-line-wrapped-plans/05-expect +++ b/src/services/__tests__/12-line-wrapped-plans/05-expect @@ -111,7 +111,7 @@ "Actual Total Time": 4.997, "Actual Rows": 10, "Actual Loops": 3, - "Buckets": 1024, + "Hash Buckets": 1024, "Shared Hit Blocks": 26, "Shared Read Blocks": 9, "Shared Written Blocks": 0, diff --git a/src/services/__tests__/12-line-wrapped-plans/06-expect b/src/services/__tests__/12-line-wrapped-plans/06-expect index 8550e2a3..0ba111de 100644 --- a/src/services/__tests__/12-line-wrapped-plans/06-expect +++ b/src/services/__tests__/12-line-wrapped-plans/06-expect @@ -127,7 +127,7 @@ "Actual Total Time": 0.555, "Actual Rows": 10, "Actual Loops": 3, - "Buckets": 1024, + "Hash Buckets": 1024, "Shared Hit Blocks": 33, "Shared Read Blocks": 0, "Shared Written Blocks": 0, @@ -164,7 +164,7 @@ "Actual Total Time": 1.289, "Actual Rows": 10, "Actual Loops": 3, - "Buckets": 1024, + "Hash Buckets": 1024, "Shared Hit Blocks": 14, "Shared Read Blocks": 0, "Shared Written Blocks": 0, diff --git a/src/services/__tests__/12-line-wrapped-plans/07-expect b/src/services/__tests__/12-line-wrapped-plans/07-expect index 6079f87e..ec6d252b 100644 --- a/src/services/__tests__/12-line-wrapped-plans/07-expect +++ b/src/services/__tests__/12-line-wrapped-plans/07-expect @@ -127,7 +127,7 @@ "Actual Total Time": 0.4, "Actual Rows": 10, "Actual Loops": 3, - "Buckets": 1024, + "Hash Buckets": 1024, "Shared Hit Blocks": 33, "Shared Read Blocks": 0, "Shared Written Blocks": 0, @@ -164,7 +164,7 @@ "Actual Total Time": 0.933, "Actual Rows": 10, "Actual Loops": 3, - "Buckets": 1024, + "Hash Buckets": 1024, "Shared Hit Blocks": 14, "Shared Read Blocks": 0, "Shared Written Blocks": 0, diff --git a/src/services/__tests__/from-text/45-buckets-expect b/src/services/__tests__/from-text/45-buckets-expect new file mode 100644 index 00000000..d32c3abc --- /dev/null +++ b/src/services/__tests__/from-text/45-buckets-expect @@ -0,0 +1,23 @@ +{ + "Plan": { + "Node Type": "Hash Join", + "Plans": [ + { + "Node Type": "Seq Scan" + }, { + "Node Type": "Hash", + "Shared Hit Blocks": 14909, + "Shared Read Blocks": 98728, + "Temp Written Blocks": 88109, + "Hash Buckets": 1048576, + "Hash Batches": 16, + "Peak Memory Usage": 62533, + "Plans": [ + { + "Node Type": "Seq Scan" + } + ] + } + ] + } +} diff --git a/src/services/__tests__/from-text/45-buckets-plan b/src/services/__tests__/from-text/45-buckets-plan new file mode 100644 index 00000000..10bc7214 --- /dev/null +++ b/src/services/__tests__/from-text/45-buckets-plan @@ -0,0 +1,23 @@ + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------- + Hash Join (cost=471037.90..1304640.12 rows=14999862 width=114) (actual time=1480.644..9895.695 rows=15001138 loops=1) + Hash Cond: (details.eid = entetes.eid) + Buffers: shared hit=15940 read=282899, temp read=246065 written=246065 + I/O Timings: shared read=242.957, temp read=317.342 write=830.025 + -> Seq Scan on details (cost=0.00..335215.62 rows=15001362 width=65) (actual time=0.014..1114.539 rows=15001289 loops=1) + Filter: (b IS NOT NULL) + Buffers: shared hit=1031 read=184171 + I/O Timings: shared read=157.615 + -> Hash (cost=238637.70..238637.70 rows=9999056 width=57) (actual time=1474.384..1474.385 rows=9999900 loops=1) + Buckets: 1048576 Batches: 16 Memory Usage: 62533kB + Buffers: shared hit=14909 read=98728, temp written=88109 + I/O Timings: shared read=85.342, temp write=294.483 + -> Seq Scan on entetes (cost=0.00..238637.70 rows=9999056 width=57) (actual time=0.016..567.370 rows=9999900 loops=1) + Filter: (a > 100) + Rows Removed by Filter: 101 + Buffers: shared hit=14909 read=98728 + I/O Timings: shared read=85.342 + Planning: + Buffers: shared hit=8 + Planning Time: 0.119 ms + Execution Time: 10244.597 ms diff --git a/src/services/__tests__/from-text/46-buckets-expect b/src/services/__tests__/from-text/46-buckets-expect new file mode 100644 index 00000000..c1128094 --- /dev/null +++ b/src/services/__tests__/from-text/46-buckets-expect @@ -0,0 +1,24 @@ +{ + "Plan": { + "Node Type": "Aggregate", + "Plans": [ + { + "Node Type": "Hash Join", + "Plans": [ + { + "Node Type": "Seq Scan" + }, + { + "Node Type": "Hash", + "Actual Rows": 1001000, + "Hash Buckets": 262144, + "Original Hash Buckets": 262144, + "Hash Batches": 2048, + "Original Hash Batches": 8, + "Peak Memory Usage": 35157 + } + ] + } + ] + } +} diff --git a/src/services/__tests__/from-text/46-buckets-plan b/src/services/__tests__/from-text/46-buckets-plan new file mode 100644 index 00000000..9a6b0ed4 --- /dev/null +++ b/src/services/__tests__/from-text/46-buckets-plan @@ -0,0 +1,56 @@ +-- source: https://www.postgresql.org/message-id/flat/CA%2BhUKGKWWmf%3DWELLG%3DaUGbcugRaSQbtm0tKYiBut-B2rVKX63g%40mail.gmail.com + +SET max_parallel_workers_per_gather = 0; +SET synchronize_seqscans = off; +SET work_mem = '4MB'; + + + +CREATE TABLE r AS SELECT generate_series(1, 10000000)::int i; +ANALYZE r; + + + +-- 1k uniform keys + 1m duplicates +CREATE TABLE s1k (i int); +INSERT INTO s1k SELECT generate_series(1, 1000)::int i; +ALTER TABLE s1k SET (autovacuum_enabled = off); +ANALYZE s1k; +INSERT INTO s1k SELECT 42 FROM generate_series(1, 1000000); + + + +EXPLAIN ANALYZE SELECT COUNT(*) FROM r JOIN s1k USING (i); +SET +SET +SET +SELECT 10000000 +ANALYZE +CREATE TABLE +INSERT 0 1000 +ALTER TABLE +ANALYZE +INSERT 0 1000000 + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------- + Aggregate (cost=302291.30..302291.31 rows=1 width=8) (actual time=3656.686..3656.688 rows=1.00 loops=1) + Buffers: shared hit=6734 read=41984 dirtied=13621, temp read=49855 written=49855 + -> Hash Join (cost=27826.00..300076.30 rows=886000 width=0) (actual time=146.635..3619.685 rows=1001000.00 loops=1) + Hash Cond: (r.i = s1k.i) + Buffers: shared hit=6734 read=41984 dirtied=13621, temp read=49855 written=49855 + -> Seq Scan on r (cost=0.00..144294.76 rows=10000676 width=4) (actual time=0.201..1200.806 rows=10000000.00 loops=1) + Buffers: shared hit=2304 read=41984 dirtied=13621 + -> Hash (cost=13290.00..13290.00 rows=886000 width=4) (actual time=145.449..145.450 rows=1001000.00 loops=1) + Buckets: 262144 (originally 262144) Batches: 2048 (originally 8) Memory Usage: 35157kB + Buffers: shared hit=4430, temp written=2930 + -> Seq Scan on s1k (cost=0.00..13290.00 rows=886000 width=4) (actual time=0.011..61.783 rows=1001000.00 loops=1) + Buffers: shared hit=4430 + Planning: + Buffers: shared hit=83 read=9 + Planning Time: 0.495 ms + JIT: + Functions: 10 + Options: Inlining false, Optimization false, Expressions true, Deforming true + Timing: Generation 0.492 ms (Deform 0.134 ms), Inlining 0.000 ms, Optimization 0.266 ms, Emission 3.684 ms, Total 4.442 ms + Execution Time: 3674.128 ms +(20 rows) diff --git a/src/services/plan-service.ts b/src/services/plan-service.ts index df9560e1..c6b1d78c 100644 --- a/src/services/plan-service.ts +++ b/src/services/plan-service.ts @@ -994,6 +994,10 @@ export class PlanService { return } + if (this.parseBuckets(extraMatches[2], element as Node)) { + return + } + if (this.parseWAL(extraMatches[2], element as Node)) { return } @@ -1120,6 +1124,30 @@ export class PlanService { el[_.map([type, method, "blocks"], _.capitalize).join(" ")] = value } + private parseBuckets(text: string, el: Node): boolean { + + enum BucketsMatch { + Buckets = 1, + OriginalBuckets, + Batches, + OriginalBatches, + MemoryUsage, + } + + const bucketsRegex = /Buckets:\s+([0-9]*)(?:\s*\(originally\s+([0-9]*)\))*\s+Batches:\s+([0-9]*)(?:\s*\(originally\s+([0-9]*)\))*\s+Memory Usage:\s+([0-9]*)kB.*\s*$/g + const bucketsMatches = bucketsRegex.exec(text) + + if (bucketsMatches) { + el[Property.HASH_BUCKETS] = parseInt(bucketsMatches[BucketsMatch.Buckets], 0) + el[Property.HASH_BATCHES] = parseInt(bucketsMatches[BucketsMatch.Batches], 0) + el[Property.ORIGINAL_HASH_BUCKETS] = parseInt(bucketsMatches[BucketsMatch.OriginalBuckets], 0) + el[Property.ORIGINAL_HASH_BATCHES] = parseInt(bucketsMatches[BucketsMatch.OriginalBatches], 0) + el[Property.PEAK_MEMORY_USAGE] = parseInt(bucketsMatches[BucketsMatch.MemoryUsage], 0) + return true + } + return false + } + private getWorker(node: Node, workerNumber: number): Worker | undefined { return _.find(node[Property.WORKERS], (worker) => { return worker[Property.WORKER_NUMBER] === workerNumber