From 58b71804d8de5ecf7187eca40d42480f28786ea2 Mon Sep 17 00:00:00 2001 From: Royston Shufflebotham Date: Sat, 2 Nov 2024 22:47:59 +0000 Subject: [PATCH] Show details Citus query plan instead of just 'Custom Scan' --- .../misc/static/explain/img/ex_citus.svg | 42 ++++++++++ .../img/ex_citus_distributed_one_of_many.svg | 24 ++++++ .../img/ex_citus_distributed_one_of_one.svg | 16 ++++ .../explain/img/ex_citus_worker_task.svg | 14 ++++ web/pgadmin/static/js/Explain/ImageMapper.js | 76 +++++++++++++++++++ web/pgadmin/static/js/Explain/index.jsx | 40 ++++++++++ 6 files changed, 212 insertions(+) create mode 100644 web/pgadmin/misc/static/explain/img/ex_citus.svg create mode 100644 web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_many.svg create mode 100644 web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_one.svg create mode 100644 web/pgadmin/misc/static/explain/img/ex_citus_worker_task.svg diff --git a/web/pgadmin/misc/static/explain/img/ex_citus.svg b/web/pgadmin/misc/static/explain/img/ex_citus.svg new file mode 100644 index 000000000..7337cac4e --- /dev/null +++ b/web/pgadmin/misc/static/explain/img/ex_citus.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_many.svg b/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_many.svg new file mode 100644 index 000000000..bf3dc0b01 --- /dev/null +++ b/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_many.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_one.svg b/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_one.svg new file mode 100644 index 000000000..0cde91a01 --- /dev/null +++ b/web/pgadmin/misc/static/explain/img/ex_citus_distributed_one_of_one.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/web/pgadmin/misc/static/explain/img/ex_citus_worker_task.svg b/web/pgadmin/misc/static/explain/img/ex_citus_worker_task.svg new file mode 100644 index 000000000..4b516fd5c --- /dev/null +++ b/web/pgadmin/misc/static/explain/img/ex_citus_worker_task.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/web/pgadmin/static/js/Explain/ImageMapper.js b/web/pgadmin/static/js/Explain/ImageMapper.js index 6a03bd8d1..db616ece3 100644 --- a/web/pgadmin/static/js/Explain/ImageMapper.js +++ b/web/pgadmin/static/js/Explain/ImageMapper.js @@ -41,10 +41,86 @@ const ImageMapper = { 'image': 'ex_bmp_or.svg', 'image_text': 'Bitmap OR', }, + 'Citus Job': function(data) { + // A 'Citus Job' represents a distributed query operation. + // The details of the distributed operation are in the sub-plans, + // but this node contains task count information, showing how many shards + // the query is being distributed to. + + const taskCount = data['Task Count']; + const tasksShown = data['Tasks Shown']; + + // "Task Count" is the number of shard operations being run. + // "Tasks Shown" is either "All" or "One of N" depending on whether the returned query plan + // contains one sample task or all of them. + + // We show single-shard or multi-shard with different images, and we show the + // literal value of 'Tasks Shown' as the image text. + + const image = (taskCount === 1) + ? 'ex_citus_distributed_one_of_one.svg' + : 'ex_citus_distributed_one_of_many.svg'; + + return { + 'image': image, + 'image_text': tasksShown + }; + }, + 'Citus Task': function(data) { + // A 'Citus Task' represents a Task executed on a particular worker node. + // The details of the Task are in the sub-plans, so for this node we just show + // some details of the worker node. + + const node = data['Node']; + // "Node" has a value like "host=citus-worker-7 port=8394 dbname=postgres" + // That's a bit long to display, so we shrink it to 'citus-worker-7:8394 postgres' + const hostMatch = node.match(/host=(\S+)/); + const portMatch = node.match(/port=(\S+)/); + const dbnameMatch = node.match(/dbname=(\S+)/); + + const host = hostMatch ? hostMatch[1] : ''; + let port = portMatch ? portMatch[1] : ''; + if (port === '5432') { + // Default port. Don't bother showing. + port = ''; + } + const dbname = dbnameMatch ? dbnameMatch[1] : ''; + + let imageText = `Task ${host}`; + if (port) { + imageText += `:${port}`; + } + if (dbname) { + imageText += ` ${dbname}`; + } + return { + 'image': 'ex_citus_worker_task.svg', + 'image_text': imageText + }; + }, 'CTE Scan': { 'image': 'ex_cte_scan.svg', 'image_text': 'CTE Scan', }, + 'Custom Scan': function(data) { + const customPlanProvider = data['Custom Plan Provider']; + + let image; + + switch (customPlanProvider) { + case 'Citus Adaptive': + image = 'ex_citus.svg'; + break; + default: + image = 'ex_unknown.svg'; + break; + } + + return { + 'image': image, + 'image_text': data['Custom Plan Provider'] + }; + }, 'Function Scan': { 'image': 'ex_result.svg', 'image_text': 'Function Scan', diff --git a/web/pgadmin/static/js/Explain/index.jsx b/web/pgadmin/static/js/Explain/index.jsx index e5702b713..b070721d5 100644 --- a/web/pgadmin/static/js/Explain/index.jsx +++ b/web/pgadmin/static/js/Explain/index.jsx @@ -350,6 +350,46 @@ function parsePlan(data, ctx) { } } + const citusDistributedQuery = data['Distributed Query']; + if (citusDistributedQuery) { + // This is a Citus Distributed Query plan. + // It contains a 'Job' with one or more 'Tasks' in it. + // We'll convert those Tasks into sub-Plans of this main plan and process it + // with the regular Plan layout code. + delete data['Distributed Query']; + + // Convert the Job into a 'Citus Job' sub-plan. + // That allows us to show details of the Task count etc. + const citusJob = citusDistributedQuery['Job']; + const jobPlan = { + 'Node Type': 'Citus Job', + ...citusJob + }; + data['Plans'] = [jobPlan]; + + // Convert each of the Tasks into 'Citus Task' sub-plans of the Job plan. + const citusTasks = jobPlan['Tasks']; + if (citusTasks) { + delete jobPlan['Tasks']; + + const citusTaskPlans = citusTasks.map(citusJobTask => { + const taskPlan = { + 'Node Type': 'Citus Task', + ...citusJobTask + }; + + // A Citus Task contains a 'Remote Plan' which is the actual plan + // executed on the worker nodes. It's actually an array of arrays. + const remotePlan = taskPlan['Remote Plan']; + delete taskPlan['Remote Plan']; + // A Remote Plan is an array of arrays of Plans. + taskPlan['Plans'] = remotePlan.flatMap(arr => arr.map(planLevel1Entry => planLevel1Entry['Plan'])); + return taskPlan; + }); + jobPlan['Plans'] = citusTaskPlans; + } + } + // Start calculating xpos, ypos, width and height for child plans if any if ('Plans' in data) { data['width'] += offsetX;