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 : }
|