Add support for displaying detailed Citus query plans instead of 'Custom Scan' placeholder. #7885
							parent
							
								
									c1da777fba
								
							
						
					
					
						commit
						8d55e9a445
					
				| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   viewBox="0 0 64 64"
 | 
			
		||||
   id="svg2"
 | 
			
		||||
   sodipodi:docname="ex_citus.svg"
 | 
			
		||||
   inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs2" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="namedview2"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#000000"
 | 
			
		||||
     borderopacity="0.25"
 | 
			
		||||
     inkscape:showpageshadow="2"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pagecheckerboard="0"
 | 
			
		||||
     inkscape:deskcolor="#d1d1d1"
 | 
			
		||||
     inkscape:zoom="2.368263"
 | 
			
		||||
     inkscape:cx="-1.055626"
 | 
			
		||||
     inkscape:cy="63.970936"
 | 
			
		||||
     inkscape:window-width="1440"
 | 
			
		||||
     inkscape:window-height="786"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="25"
 | 
			
		||||
     inkscape:window-maximized="1"
 | 
			
		||||
     inkscape:current-layer="svg2" />
 | 
			
		||||
  <path
 | 
			
		||||
     fill="#1b324f"
 | 
			
		||||
     d="M 39.722625,27.973765 C 40.337554,24.89912 31.523573,23.600937 26.467491,29.818551 20.79648,36.787746 32.206827,44.098567 45.18866,34.32803 53.456038,28.04209 56.12073,19.433085 51.269624,15.060257 41.840714,6.4512526 19.020018,14.445328 11.36757,31.526687 19.020018,18.61318 38.219466,12.532216 45.871914,20.389641 c 3.211296,3.279621 1.434835,9.907188 -4.441153,12.708531 -9.497235,4.441154 -8.609005,-5.261058 -1.708136,-5.124407"
 | 
			
		||||
     id="path1"
 | 
			
		||||
     style="stroke-width:0.683254" />
 | 
			
		||||
  <path
 | 
			
		||||
     fill="#239646"
 | 
			
		||||
     d="M 39.244347,21.209547 C 23.529497,16.495092 2.2119613,35.762864 13.144031,48.471395 20.386527,56.943749 45.735264,52.844223 53.86599,27.29051 44.232104,44.918473 26.057538,47.514839 19.156669,42.458757 11.025942,36.514444 21.138107,19.843038 39.244347,21.209547"
 | 
			
		||||
     id="path2"
 | 
			
		||||
     style="stroke-width:0.683254" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <style>
 | 
			
		||||
      .cls-5{fill:#34495e;}
 | 
			
		||||
      .cls-6{fill:none;stroke-linecap:round;}
 | 
			
		||||
      .cls-5,.cls-6{stroke-linejoin:round;stroke:#34495e;}
 | 
			
		||||
    </style>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="43.3 54.7 42.3 51.6 40 54.8"/>
 | 
			
		||||
  <line class="cls-6" x1="49.6" x2="41.5" y1="53.3" y2="47.6"/>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="43.3 9.32 42.3 12.4 40 9.21"/>
 | 
			
		||||
  <line class="cls-6" x1="49.6" x2="41.5" y1="10.6" y2="16.4"/>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="45.4 31.6 42.8 29.6 42.8 33.6"/>
 | 
			
		||||
  <line class="cls-6" x1="51.2" x2="41.4" y1="31.5" y2="31.5"/>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="45 19 41.9 18 43.2 21.7"/>
 | 
			
		||||
  <line class="cls-6" x1="51" x2="41.6" y1="19.6" y2="23"/>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="45 43.5 41.9 44.4 43.2 40.7"/>
 | 
			
		||||
  <line class="cls-6" x1="51" x2="41.6" y1="42.8" y2="39.5"/>
 | 
			
		||||
  <g transform="translate(51.6 26.3)">
 | 
			
		||||
    <path d="m-23.3 3.39c0.389-1.95-5.19-2.77-8.39 1.17-3.59 4.41 3.63 9.04 11.9 2.86 5.24-3.98 6.92-9.43 3.85-12.2-5.97-5.45-20.4-0.389-25.3 10.4 4.85-8.18 17-12 21.8-7.05 2.03 2.08 0.909 6.27-2.81 8.05-6.01 2.81-5.45-3.33-1.08-3.25" fill="#1b324f" style="stroke-width:.433"/>
 | 
			
		||||
    <path d="m-23.6-0.895c-9.95-2.99-23.5 9.22-16.5 17.3 4.59 5.37 20.6 2.77 25.8-13.4-6.1 11.2-17.6 12.8-22 9.61-5.15-3.76 1.25-14.3 12.7-13.5" fill="#239646" style="stroke-width:.433"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <style>
 | 
			
		||||
      .cls-5{fill:#34495e;}
 | 
			
		||||
      .cls-6{fill:none;stroke-linecap:round;}
 | 
			
		||||
      .cls-5,.cls-6{stroke-linejoin:round;stroke:#34495e;}
 | 
			
		||||
    </style>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <polygon class="cls-5" transform="translate(8)" points="45.4 31.6 42.8 29.6 42.8 33.6"/>
 | 
			
		||||
  <line class="cls-6" x1="51.2" x2="41.4" y1="31.5" y2="31.5"/>
 | 
			
		||||
  <g transform="translate(51.6 26.3)">
 | 
			
		||||
    <path d="m-23.3 3.39c0.389-1.95-5.19-2.77-8.39 1.17-3.59 4.41 3.63 9.04 11.9 2.86 5.24-3.98 6.92-9.43 3.85-12.2-5.97-5.45-20.4-0.389-25.3 10.4 4.85-8.18 17-12 21.8-7.05 2.03 2.08 0.909 6.27-2.81 8.05-6.01 2.81-5.45-3.33-1.08-3.25" fill="#1b324f" style="stroke-width:.433"/>
 | 
			
		||||
    <path d="m-23.6-0.895c-9.95-2.99-23.5 9.22-16.5 17.3 4.59 5.37 20.6 2.77 25.8-13.4-6.1 11.2-17.6 12.8-22 9.61-5.15-3.76 1.25-14.3 12.7-13.5" fill="#239646" style="stroke-width:.433"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 965 B  | 
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs>
 | 
			
		||||
 <style>
 | 
			
		||||
.lights { fill:#fff }
 | 
			
		||||
.other-worker > rect { stroke-width:0.5; fill:#addff3; stroke: #2980b9; }
 | 
			
		||||
.other-worker .squares, .other-worker .side-handles { fill: #2980b9; }
 | 
			
		||||
.selected-worker > rect { stroke-width:0.5; fill:#f0ecb6; stroke: #c18f35; }
 | 
			
		||||
.selected-worker .squares, .selected-worker .side-handles { fill: #c18f35; }
 | 
			
		||||
</style></defs>
 | 
			
		||||
<g class="other-worker" transform="matrix(1.2 0 0 1.25 -7.14 -1.2)"><rect x="12.3" y="11.9" width="39.8" height="8.41" ry=".942"/><path class="squares" d="m19.2 13.4h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm-9.16 3.07h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27z"/><path class="side-handles" d="m14.1 13h1.24v6.1h-1.24zm34.5 0h1.24v6.1h-1.24z"/><g class="lights"><rect x="35.3" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="38.1" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="41" y="13.9" width="1.83" height="1.83" ry=".6"/></g></g>
 | 
			
		||||
<g class="other-worker" transform="matrix(1.2 0 0 1.25 -7.14 24.7)"><rect x="12.3" y="11.9" width="39.8" height="8.41" ry=".942"/><path class="squares" d="m19.2 13.4h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm-9.16 3.07h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27z"/><path class="side-handles" d="m14.1 13h1.24v6.1h-1.24zm34.5 0h1.24v6.1h-1.24z"/><g class="lights"><rect x="35.3" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="38.1" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="41" y="13.9" width="1.83" height="1.83" ry=".6"/></g></g>
 | 
			
		||||
<g class="selected-worker" transform="matrix(1.2 0 0 1.25 -6.84 11.7)"><rect x="12.3" y="11.9" width="39.8" height="8.41" ry=".942"/><path class="squares" d="m19.2 13.4h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm-9.16 3.07h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27zm3.05 0h2.27v2.27h-2.27z"/><path class="side-handles" d="m14.1 13h1.24v6.1h-1.24zm34.5 0h1.24v6.1h-1.24z"/><g class="lights"><rect x="35.3" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="38.1" y="13.9" width="1.83" height="1.83" ry=".6"/><rect x="41" y="13.9" width="1.83" height="1.83" ry=".6"/></g></g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue