LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_profile - profile_scene.ts Coverage Total Hit
Test: DashQL 915828b Lines: 99.3 % 402 399
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as dashql from '@ankoh/dashql-core';
       2              : import { U32_MAX } from './numeric_limits.js';
       3              : import { buildEdgePathBetweenRectangles, PathBuilder, selectHorizontalEdgeType, selectVerticalEdgeType } from '../../ui/graph/graph_edges.js';
       4              : 
       5              : export interface ProfileSceneRect {
       6              :     x: number;
       7              :     y: number;
       8              :     width: number;
       9              :     height: number;
      10              : }
      11              : 
      12              : /// An operator of the plan structure. Execution state lives in `ProfileExecutionState`.
      13              : export interface ProfileSceneOperator {
      14              :     id: number;
      15              :     kind: dashql.buffers.profile.ProfileOperatorKind;
      16              :     typeName: string | null;
      17              :     label: string;
      18              :     displayLabel: string;
      19              :     rect: ProfileSceneRect;
      20              :     ports: number;
      21              :     /// The estimated output rows, NaN if the plan has no estimate
      22              :     estimatedRows: number;
      23              :     /// The innermost fragment (U32_MAX if none)
      24              :     fragment: number;
      25              :     properties: Record<string, unknown>;
      26              : }
      27              : 
      28              : export const PROFILE_OPERATOR_PORT_OUTPUT = 0b01;
      29              : export const PROFILE_OPERATOR_PORT_INPUT = 0b10;
      30              : 
      31              : export interface ProfileSceneEdge {
      32              :     id: number;
      33              :     childOperator: number;
      34              :     parentOperator: number;
      35              :     /// The slot of the child that carries the edge (U32_MAX if none), its output is the flow over the edge
      36              :     slot: number;
      37              :     /// The estimated rows over the edge, null if the plan has no estimate
      38              :     outputCardinalityEstimated: number | null;
      39              :     path: string;
      40              : }
      41              : 
      42              : export interface ProfileSceneCrossEdge {
      43              :     id: number;
      44              :     sourceOperator: number;
      45              :     targetOperator: number;
      46              :     kind: string;
      47              :     properties: Record<string, unknown>;
      48              :     path: string;
      49              : }
      50              : 
      51              : export interface ProfileScenePipeline {
      52              :     id: number;
      53              :     operatorIds: readonly number[];
      54              :     path: string;
      55              : }
      56              : 
      57              : export interface ProfileSceneFragment {
      58              :     id: number;
      59              :     operatorIds: readonly number[];
      60              :     path: string;
      61              : }
      62              : 
      63              : export interface ProfileScene {
      64              :     width: number;
      65              :     height: number;
      66              :     /// Does the plan grow left-to-right? The root is then at the top left and the inputs of an operator are right of it.
      67              :     leftToRight: boolean;
      68              :     layoutConfig: dashql.buffers.profile.DerivedProfileLayoutConfigT;
      69              :     operators: readonly ProfileSceneOperator[];
      70              :     edges: readonly ProfileSceneEdge[];
      71              :     crossEdges: readonly ProfileSceneCrossEdge[];
      72              :     fragments: readonly ProfileSceneFragment[];
      73              :     pipelines: readonly ProfileScenePipeline[];
      74              : }
      75              : 
      76            1 : function readString(vm: dashql.buffers.profile.QueryProfile, id: number): string | null {
      77            1 :     return id !== U32_MAX ? vm.stringDictionary(id) : null;
      78            1 : }
      79              : 
      80            1 : function readAttributeRange(
      81            1 :     vm: dashql.buffers.profile.QueryProfile,
      82            1 :     begin: number,
      83            1 :     count: number,
      84            1 : ): Record<string, unknown> {
      85            1 :     const properties: Record<string, unknown> = {};
      86            1 :     const attribute = new dashql.buffers.profile.ProfileAttribute();
      87            1 :     for (let i = 0; i < count; ++i) {
      88            1 :         const value = vm.attributes(begin + i, attribute);
      89            1 :         if (value == null) continue;
      90            1 :         const name = readString(vm, value.name());
      91            1 :         const json = readString(vm, value.valueJson());
      92            1 :         if (name == null || json == null) continue;
      93            1 :         try {
      94            1 :             properties[name] = JSON.parse(json);
      95            1 :         } catch {
      96            0 :             properties[name] = json;
      97            0 :         }
      98            1 :     }
      99            1 :     return properties;
     100            1 : }
     101              : 
     102            1 : export function estimateRelativeDifference(estimated: number, actual: number): number {
     103            1 :     if (estimated === 0) return actual === 0 ? 0 : Number.POSITIVE_INFINITY;
     104            1 :     return (actual - estimated) / Math.abs(estimated);
     105            1 : }
     106              : 
     107            1 : export function buildCrossEdgePath(
     108            1 :     source: ProfileSceneRect,
     109            1 :     target: ProfileSceneRect,
     110            1 :     minHandleLength = 24,
     111            1 :     minInputHandleLength = 48,
     112            1 :     leftToRight = false,
     113            1 : ): string {
     114            1 :     if (leftToRight) {
     115              :         // The output of the source is on its left side, the input of the target on its right side
     116            1 :         const sourceX = source.x - source.width / 2;
     117            1 :         const sourceY = source.y;
     118            1 :         const targetX = target.x + target.width / 2;
     119            1 :         const targetY = target.y;
     120            1 :         const sourceHandleLength = Math.max(minHandleLength, Math.abs(sourceX - targetX) / 2);
     121            1 :         const targetHandleLength = Math.max(minInputHandleLength, sourceHandleLength);
     122            1 :         return `M ${sourceX} ${sourceY} C ${sourceX - sourceHandleLength} ${sourceY}, ${targetX + targetHandleLength} ${targetY}, ${targetX} ${targetY}`;
     123            1 :     }
     124            1 :     const sourceX = source.x;
     125            1 :     const sourceY = source.y - source.height / 2;
     126            1 :     const targetX = target.x;
     127            1 :     const targetY = target.y + target.height / 2;
     128            1 :     const sourceHandleLength = Math.max(minHandleLength, Math.abs(sourceY - targetY) / 2);
     129            1 :     const targetHandleLength = Math.max(minInputHandleLength, sourceHandleLength);
     130            1 :     return `M ${sourceX} ${sourceY} C ${sourceX} ${sourceY - sourceHandleLength}, ${targetX} ${targetY + targetHandleLength}, ${targetX} ${targetY}`;
     131            1 : }
     132              : 
     133            1 : export function truncateProfileLabel(label: string, maxChars: number): string {
     134            1 :     const chars = Array.from(label);
     135            1 :     if (chars.length <= maxChars) return label;
     136            1 :     if (maxChars === 0) return '';
     137            1 :     return `${chars.slice(0, maxChars - 1).join('')}…`;
     138            1 : }
     139              : 
     140            1 : export function buildPipelinePath(operatorIds: readonly number[], operators: readonly Pick<ProfileSceneOperator, "rect">[]): string {
     141            1 :     if (operatorIds.length === 0) return '';
     142            1 :     const padding = 4;
     143            1 :     const radius = 8;
     144            1 :     const parts: string[] = [];
     145            1 :     for (const operatorId of operatorIds) {
     146            1 :         const op = operators[operatorId];
     147            1 :         if (op == null) continue;
     148            1 :         const x = op.rect.x - op.rect.width / 2 - padding;
     149            1 :         const y = op.rect.y - op.rect.height / 2 - padding;
     150            1 :         const width = op.rect.width + padding * 2;
     151            1 :         const height = op.rect.height + padding * 2;
     152            1 :         parts.push(`M ${x + radius} ${y} H ${x + width - radius} Q ${x + width} ${y} ${x + width} ${y + radius} V ${y + height - radius} Q ${x + width} ${y + height} ${x + width - radius} ${y + height} H ${x + radius} Q ${x} ${y + height} ${x} ${y + height - radius} V ${y + radius} Q ${x} ${y} ${x + radius} ${y} Z`);
     153            1 :     }
     154            1 :     return parts.join(' ');
     155            1 : }
     156              : 
     157              : interface FragmentPoint {
     158              :     x: number;
     159              :     y: number;
     160              : }
     161              : 
     162              : interface FragmentSegment {
     163              :     from: FragmentPoint;
     164              :     to: FragmentPoint;
     165              : }
     166              : 
     167            1 : function pointKey(point: FragmentPoint): string {
     168            1 :     return `${point.x},${point.y}`;
     169            1 : }
     170              : 
     171            1 : function segmentDirection(segment: FragmentSegment): number {
     172            1 :     if (segment.to.x > segment.from.x) return 0;
     173            1 :     if (segment.to.y > segment.from.y) return 1;
     174            1 :     if (segment.to.x < segment.from.x) return 2;
     175            1 :     return 3;
     176            1 : }
     177              : 
     178            1 : function selectNextFragmentSegment(
     179            1 :     candidates: readonly FragmentSegment[] | undefined,
     180            1 :     previous: FragmentSegment,
     181            1 :     unused: ReadonlySet<FragmentSegment>,
     182            1 : ): FragmentSegment | undefined {
     183            1 :     const previousDirection = segmentDirection(previous);
     184            1 :     const turnPriority = [1, 0, 3, 2];
     185            1 :     for (const turn of turnPriority) {
     186            1 :         const direction = (previousDirection + turn) % 4;
     187            1 :         const candidate = candidates?.find(segment => unused.has(segment) && segmentDirection(segment) === direction);
     188            1 :         if (candidate != null) return candidate;
     189            1 :     }
     190              :     return undefined;
     191            1 : }
     192              : 
     193            1 : function renderRoundedFragmentContour(points: readonly FragmentPoint[], radius: number): string {
     194            1 :     const contour = points.length > 1 && pointKey(points[0]) === pointKey(points[points.length - 1])
     195            1 :         ? points.slice(0, -1)
     196            0 :         : points;
     197            1 :     if (contour.length < 3) return '';
     198              : 
     199            1 :     const corners = contour.map((point, index) => {
     200            1 :         const previous = contour[(index + contour.length - 1) % contour.length];
     201            1 :         const next = contour[(index + 1) % contour.length];
     202            1 :         const previousDistance = Math.abs(previous.x - point.x) + Math.abs(previous.y - point.y);
     203            1 :         const nextDistance = Math.abs(next.x - point.x) + Math.abs(next.y - point.y);
     204            1 :         const offset = Math.min(radius, previousDistance / 2, nextDistance / 2);
     205              :         const entry = {
     206            1 :             x: point.x + Math.sign(previous.x - point.x) * offset,
     207            1 :             y: point.y + Math.sign(previous.y - point.y) * offset,
     208              :         };
     209              :         const exit = {
     210            1 :             x: point.x + Math.sign(next.x - point.x) * offset,
     211            1 :             y: point.y + Math.sign(next.y - point.y) * offset,
     212              :         };
     213            1 :         return { point, entry, exit };
     214            1 :     });
     215              : 
     216            1 :     const first = corners[0];
     217            1 :     const parts = [`M ${first.entry.x} ${first.entry.y}`, `Q ${first.point.x} ${first.point.y} ${first.exit.x} ${first.exit.y}`];
     218            1 :     for (let i = 1; i < corners.length; ++i) {
     219            1 :         const corner = corners[i];
     220            1 :         parts.push(`L ${corner.entry.x} ${corner.entry.y}`);
     221            1 :         parts.push(`Q ${corner.point.x} ${corner.point.y} ${corner.exit.x} ${corner.exit.y}`);
     222            1 :     }
     223            1 :     parts.push('Z');
     224            1 :     return parts.join(' ');
     225            1 : }
     226              : 
     227            1 : function appendFragmentRect(rectangles: ProfileSceneRect[], rect: ProfileSceneRect, padding: number): void {
     228            1 :     rectangles.push({
     229            1 :         x: rect.x - rect.width / 2 - padding,
     230            1 :         y: rect.y - rect.height / 2 - padding,
     231            1 :         width: rect.width + padding * 2,
     232            1 :         height: rect.height + padding * 2,
     233            1 :     });
     234            1 : }
     235              : 
     236            1 : function appendFragmentCorridor(
     237            1 :     rectangles: ProfileSceneRect[],
     238            1 :     from: ProfileSceneRect,
     239            1 :     to: ProfileSceneRect,
     240            1 :     width: number,
     241            1 :     padding: number,
     242            1 :     leftToRight: boolean,
     243            1 : ): void {
     244              :     // An operator directly below or next to its parent shares the envelope with it: the gap between the two is
     245              :     // filled as wide as the narrower of the padded operators instead of a corridor along the edge.
     246            1 :     if (leftToRight ? Math.abs(from.y - to.y) < 0.5 : Math.abs(from.x - to.x) < 0.5) {
     247            1 :         const left = Math.max(from.x - from.width / 2, to.x - to.width / 2) - padding;
     248            1 :         const right = Math.min(from.x + from.width / 2, to.x + to.width / 2) + padding;
     249            1 :         const top = Math.max(from.y - from.height / 2, to.y - to.height / 2) - padding;
     250            1 :         const bottom = Math.min(from.y + from.height / 2, to.y + to.height / 2) + padding;
     251            1 :         rectangles.push(leftToRight
     252            1 :             ? { x: Math.min(from.x, to.x), y: top, width: Math.abs(to.x - from.x), height: bottom - top }
     253            1 :             : { x: left, y: Math.min(from.y, to.y), width: right - left, height: Math.abs(to.y - from.y) });
     254            1 :         return;
     255            1 :     }
     256            1 :     const halfWidth = width / 2;
     257            1 :     if (leftToRight) {
     258              :         // Along the edge: from the left side of the child to the middle of the gap, then up, then to the parent
     259            1 :         const direction = Math.sign(to.x - from.x) || -1;
     260            1 :         const fromX = from.x + direction * from.width / 2;
     261            1 :         const toX = to.x - direction * to.width / 2;
     262            1 :         const midX = (fromX + toX) / 2;
     263            1 :         rectangles.push({
     264            1 :             x: Math.min(fromX, midX) - halfWidth,
     265            1 :             y: from.y - halfWidth,
     266            1 :             width: Math.abs(midX - fromX) + width,
     267            1 :             height: width,
     268            1 :         });
     269            1 :         rectangles.push({
     270            1 :             x: midX - halfWidth,
     271            1 :             y: Math.min(from.y, to.y) - halfWidth,
     272            1 :             width,
     273            1 :             height: Math.abs(to.y - from.y) + width,
     274            1 :         });
     275            1 :         rectangles.push({
     276            1 :             x: Math.min(midX, toX) - halfWidth,
     277            1 :             y: to.y - halfWidth,
     278            1 :             width: Math.abs(toX - midX) + width,
     279            1 :             height: width,
     280            1 :         });
     281            1 :         return;
     282            1 :     }
     283            1 :     const direction = Math.sign(to.y - from.y) || 1;
     284            1 :     const fromY = from.y + direction * from.height / 2;
     285            1 :     const toY = to.y - direction * to.height / 2;
     286            1 :     const midY = (fromY + toY) / 2;
     287            1 :     rectangles.push({
     288            1 :         x: from.x - halfWidth,
     289            1 :         y: Math.min(fromY, midY) - halfWidth,
     290            1 :         width,
     291            1 :         height: Math.abs(midY - fromY) + width,
     292            1 :     });
     293            1 :     rectangles.push({
     294            1 :         x: Math.min(from.x, to.x) - halfWidth,
     295            1 :         y: midY - halfWidth,
     296            1 :         width: Math.abs(to.x - from.x) + width,
     297            1 :         height: width,
     298            1 :     });
     299            1 :     rectangles.push({
     300            1 :         x: to.x - halfWidth,
     301            1 :         y: Math.min(midY, toY) - halfWidth,
     302            1 :         width,
     303            1 :         height: Math.abs(toY - midY) + width,
     304            1 :     });
     305            1 : }
     306              : 
     307            1 : function traceFragmentRectangles(rectangles: readonly ProfileSceneRect[]): string {
     308            1 :     if (rectangles.length === 0) return '';
     309            1 :     const xs = Array.from(new Set(rectangles.flatMap(rect => [rect.x, rect.x + rect.width]))).sort((a, b) => a - b);
     310            1 :     const ys = Array.from(new Set(rectangles.flatMap(rect => [rect.y, rect.y + rect.height]))).sort((a, b) => a - b);
     311            1 :     const filled = Array.from({ length: ys.length - 1 }, () => new Uint8Array(xs.length - 1));
     312            1 :     for (const rect of rectangles) {
     313            1 :         const left = xs.indexOf(rect.x);
     314            1 :         const right = xs.indexOf(rect.x + rect.width);
     315            1 :         const top = ys.indexOf(rect.y);
     316            1 :         const bottom = ys.indexOf(rect.y + rect.height);
     317            1 :         for (let y = top; y < bottom; ++y) {
     318            1 :             for (let x = left; x < right; ++x) filled[y][x] = 1;
     319              :         }
     320            1 :     }
     321              : 
     322            1 :     const segments: FragmentSegment[] = [];
     323            1 :     const isFilled = (x: number, y: number) => y >= 0 && y < filled.length && x >= 0 && x < filled[y].length && filled[y][x] !== 0;
     324            1 :     for (let y = 0; y < filled.length; ++y) {
     325            1 :         for (let x = 0; x < filled[y].length; ++x) {
     326            1 :             if (!isFilled(x, y)) continue;
     327            1 :             if (!isFilled(x, y - 1)) segments.push({ from: { x: xs[x], y: ys[y] }, to: { x: xs[x + 1], y: ys[y] } });
     328            1 :             if (!isFilled(x + 1, y)) segments.push({ from: { x: xs[x + 1], y: ys[y] }, to: { x: xs[x + 1], y: ys[y + 1] } });
     329            1 :             if (!isFilled(x, y + 1)) segments.push({ from: { x: xs[x + 1], y: ys[y + 1] }, to: { x: xs[x], y: ys[y + 1] } });
     330            1 :             if (!isFilled(x - 1, y)) segments.push({ from: { x: xs[x], y: ys[y + 1] }, to: { x: xs[x], y: ys[y] } });
     331            1 :         }
     332              :     }
     333              : 
     334            1 :     const segmentsByStart = new Map<string, FragmentSegment[]>();
     335            1 :     for (const segment of segments) {
     336            1 :         const key = pointKey(segment.from);
     337            1 :         const candidates = segmentsByStart.get(key);
     338            1 :         if (candidates == null) segmentsByStart.set(key, [segment]);
     339            1 :         else candidates.push(segment);
     340            1 :     }
     341              : 
     342            1 :     const parts: string[] = [];
     343            1 :     const unused = new Set(segments);
     344            1 :     while (unused.size > 0) {
     345            1 :         const first = unused.values().next().value!;
     346            1 :         unused.delete(first);
     347            1 :         const start = first.from;
     348            1 :         let previousSegment = first;
     349            1 :         let current = first.to;
     350            1 :         const points = [start, current];
     351            1 :         while (pointKey(current) !== pointKey(start)) {
     352            1 :             const candidates = segmentsByStart.get(pointKey(current));
     353            1 :             const next = selectNextFragmentSegment(candidates, previousSegment, unused);
     354            1 :             if (next == null) break;
     355            1 :             unused.delete(next);
     356            1 :             previousSegment = next;
     357            1 :             current = next.to;
     358            1 :             const previous = points[points.length - 1];
     359            1 :             const beforePrevious = points[points.length - 2];
     360            1 :             if (beforePrevious != null
     361            1 :                 && (beforePrevious.x === previous.x && previous.x === current.x
     362            1 :                     || beforePrevious.y === previous.y && previous.y === current.y)) {
     363            1 :                 points[points.length - 1] = current;
     364              :             } else {
     365            1 :                 points.push(current);
     366              :             }
     367            1 :         }
     368            1 :         if (pointKey(current) === pointKey(start)) {
     369            1 :             const path = renderRoundedFragmentContour(points, 8);
     370            1 :             if (path.length > 0) parts.push(path);
     371            1 :         }
     372            1 :     }
     373            1 :     return parts.join(' ');
     374            1 : }
     375              : 
     376              : /// The padding of a fragment contour around its operators
     377            1 : const FRAGMENT_PADDING = 8;
     378              : 
     379            1 : export function buildFragmentPath(
     380            1 :     operatorIds: readonly number[],
     381            1 :     operators: readonly Pick<ProfileSceneOperator, 'rect'>[],
     382            1 :     edges: readonly Pick<ProfileSceneEdge, 'childOperator' | 'parentOperator'>[] = [],
     383            1 :     padding = FRAGMENT_PADDING,
     384            1 :     corridorWidth = padding * 2,
     385            1 :     leftToRight = false,
     386            1 : ): string {
     387            1 :     const rectangles: ProfileSceneRect[] = [];
     388            1 :     const memberIds = new Set(operatorIds);
     389            1 :     for (const operatorId of operatorIds) {
     390            1 :         const op = operators[operatorId];
     391            1 :         if (op == null) continue;
     392            1 :         appendFragmentRect(rectangles, op.rect, padding);
     393            1 :     }
     394            1 :     for (const edge of edges) {
     395            1 :         if (!memberIds.has(edge.childOperator) || !memberIds.has(edge.parentOperator)) continue;
     396            1 :         const child = operators[edge.childOperator];
     397            1 :         const parent = operators[edge.parentOperator];
     398            1 :         if (child == null || parent == null) continue;
     399            1 :         appendFragmentCorridor(rectangles, child.rect, parent.rect, corridorWidth, padding, leftToRight);
     400            1 :     }
     401            1 :     return traceFragmentRectangles(rectangles);
     402            1 : }
     403              : 
     404              : /// The values of a range of an index vector
     405            1 : function readIndexRange(read: (index: number) => number | null, begin: number, count: number): number[] {
     406            1 :     const out: number[] = [];
     407            1 :     for (let i = 0; i < count; ++i) {
     408            1 :         const value = read(begin + i);
     409            1 :         if (value != null) out.push(value);
     410            1 :     }
     411            1 :     return out;
     412            1 : }
     413              : 
     414              : /// Materialize the structure of a plan for rendering. The scene carries no execution state.
     415            1 : export function materializeProfileScene(viewModel: dashql.FlatBufferPtr<dashql.buffers.profile.QueryProfile>): ProfileScene {
     416            1 :     const vm = viewModel.read();
     417            1 :     const layoutConfig = vm.layoutConfig()!.unpack();
     418            1 :     const input = layoutConfig.input!;
     419            1 :     const leftToRight = input.orientation === dashql.buffers.profile.ProfileLayoutOrientation.LEFT_TO_RIGHT;
     420            1 :     const selectEdgeType = leftToRight ? selectHorizontalEdgeType : selectVerticalEdgeType;
     421            1 :     const operators: ProfileSceneOperator[] = new Array(vm.operatorsLength());
     422            1 :     const tmpOperator = new dashql.buffers.profile.ProfileOperator();
     423            1 :     for (let i = 0; i < vm.operatorsLength(); ++i) {
     424            1 :         const op = vm.operators(i, tmpOperator)!;
     425            1 :         const layout = op.layoutRect()!;
     426            1 :         const typeName = readString(vm, op.typeName());
     427            1 :         const label = readString(vm, op.label()) ?? typeName ?? 'operator';
     428            1 :         operators[i] = {
     429            1 :             id: i,
     430            1 :             kind: op.kind(),
     431            1 :             typeName,
     432            1 :             label,
     433            1 :             displayLabel: truncateProfileLabel(label, input.maxLabelChars),
     434            1 :             rect: {
     435            1 :                 x: layout.x(),
     436            1 :                 y: layout.y(),
     437            1 :                 width: layout.width(),
     438            1 :                 height: layout.height(),
     439            1 :             },
     440            1 :             ports: 0,
     441            1 :             estimatedRows: op.estimatedRows(),
     442            1 :             fragment: op.fragment(),
     443            1 :             properties: readAttributeRange(vm, op.attributesBegin(), op.attributeCount()),
     444            1 :         };
     445            1 :     }
     446              : 
     447            1 :     const edges: ProfileSceneEdge[] = [];
     448            1 :     const tmpEdge = new dashql.buffers.profile.ProfileOperatorEdge();
     449            1 :     for (let i = 0; i < vm.operatorEdgesLength(); ++i) {
     450            1 :         const edge = vm.operatorEdges(i, tmpEdge)!;
     451            1 :         const child = operators[edge.child()];
     452            1 :         const parent = operators[edge.parent()];
     453            1 :         child.ports |= PROFILE_OPERATOR_PORT_OUTPUT;
     454            1 :         parent.ports |= PROFILE_OPERATOR_PORT_INPUT;
     455            1 :         const edgeType = selectEdgeType(child.rect.x, child.rect.y, parent.rect.x, parent.rect.y);
     456            1 :         const path = buildEdgePathBetweenRectangles(
     457            1 :             new PathBuilder(), edgeType,
     458            1 :             child.rect.x, child.rect.y, parent.rect.x, parent.rect.y,
     459            1 :             child.rect.width, child.rect.height, parent.rect.width, parent.rect.height, 4,
     460            1 :         ).render();
     461            1 :         edges.push({
     462            1 :             id: i,
     463            1 :             childOperator: child.id,
     464            1 :             parentOperator: parent.id,
     465            1 :             slot: edge.slot(),
     466            1 :             outputCardinalityEstimated: Number.isNaN(child.estimatedRows) ? null : child.estimatedRows,
     467            1 :             path,
     468            1 :         });
     469            1 :     }
     470              : 
     471            1 :     const crossEdges: ProfileSceneCrossEdge[] = [];
     472            1 :     const tmpCrossEdge = new dashql.buffers.profile.ProfileOperatorCrossEdge();
     473            1 :     for (let i = 0; i < vm.operatorCrossEdgesLength(); ++i) {
     474            1 :         const edge = vm.operatorCrossEdges(i, tmpCrossEdge)!;
     475            1 :         const source = operators[edge.source()];
     476            1 :         const target = operators[edge.target()];
     477            1 :         if (source == null || target == null) continue;
     478            1 :         source.ports |= PROFILE_OPERATOR_PORT_OUTPUT;
     479            1 :         target.ports |= PROFILE_OPERATOR_PORT_INPUT;
     480            1 :         const properties = readAttributeRange(vm, edge.attributesBegin(), edge.attributeCount());
     481            1 :         crossEdges.push({
     482            1 :             id: i,
     483            1 :             sourceOperator: source.id,
     484            1 :             targetOperator: target.id,
     485            1 :             kind: typeof properties.kind === 'string' ? properties.kind : 'reference',
     486            1 :             properties,
     487            1 :             path: buildCrossEdgePath(source.rect, target.rect, undefined, undefined, leftToRight),
     488            1 :         });
     489            1 :     }
     490              : 
     491              :     // A fragment owns the operators that are not in a nested fragment, its contour encloses only those.
     492              :     // A nested fragment draws a contour of its own next to the one of its parent.
     493            1 :     const tmpFragment = new dashql.buffers.profile.ProfileFragment();
     494            1 :     const fragments: ProfileSceneFragment[] = [];
     495            1 :     for (let i = 0; i < vm.fragmentsLength(); ++i) {
     496            1 :         const fragment = vm.fragments(i, tmpFragment)!;
     497            1 :         if (fragment.hidden() !== 0) continue;
     498            1 :         const operatorIds = readIndexRange(j => vm.fragmentOperators(j), fragment.operatorsBegin(), fragment.operatorCount());
     499            1 :         fragments.push({
     500            1 :             id: i,
     501            1 :             operatorIds,
     502            1 :             path: buildFragmentPath(operatorIds, operators, edges, FRAGMENT_PADDING, undefined, leftToRight),
     503            1 :         });
     504            1 :     }
     505              : 
     506            1 :     const pipelines: ProfileScenePipeline[] = [];
     507            1 :     const tmpPipeline = new dashql.buffers.profile.ProfilePipeline();
     508            1 :     const tmpSlot = new dashql.buffers.profile.ProfileSlot();
     509            1 :     for (let i = 0; i < vm.pipelinesLength(); ++i) {
     510            1 :         const pipeline = vm.pipelines(i, tmpPipeline)!;
     511            1 :         const operatorIds = readIndexRange(j => vm.slots(j, tmpSlot)?.operator() ?? null, pipeline.slotsBegin(), pipeline.slotCount());
     512            1 :         pipelines.push({
     513            1 :             id: i,
     514            1 :             operatorIds,
     515            1 :             path: buildPipelinePath(operatorIds, operators),
     516            1 :         });
     517            1 :     }
     518              : 
     519            1 :     const bounds = vm.layoutRect();
     520            1 :     return {
     521            1 :         width: bounds?.width() ?? 0,
     522            1 :         height: bounds?.height() ?? 0,
     523            1 :         leftToRight,
     524            1 :         layoutConfig,
     525            1 :         operators,
     526            1 :         edges,
     527            1 :         crossEdges,
     528            1 :         fragments,
     529            1 :         pipelines,
     530            1 :     };
     531            1 : }
        

Generated by: LCOV version 2.0-1