blob: 60c8bf51b09688b132a611c63541f0ee9e730eda [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import type { ProfileDagNode, ProfileGraphIR } from './profileGraphTypes';
import {
buildElkGraph,
formatBytes,
formatDurationNs,
layoutProfileDag,
selectSlowestOperators,
} from './profileGraphLayout';
function node(id: string, fragmentId: string, pipelineId: string, maxNs: number): ProfileDagNode {
return {
id,
fragmentId,
pipelineId,
ordinal: 0,
operatorType: 'OLAP_SCAN_OPERATOR',
operatorFamily: 'SCAN',
role: 'SOURCE',
label: 'OLAP SCAN',
planNodeId: 1,
nereidsId: null,
destId: null,
destIds: [],
known: true,
lineNumber: 1,
planInfo: {},
timing: { execTime: { maxNs } },
metrics: {},
analysis: { heat: 1, waitHeat: 0, isBottleneck: false },
};
}
function fixture(): ProfileGraphIR {
const first = node('fragment:0/pipeline:0/operator:0', 'fragment:0', 'fragment:0/pipeline:0', 10);
const second = node('fragment:1/pipeline:2/operator:0', 'fragment:1', 'fragment:1/pipeline:2', 20);
return {
schemaVersion: '1.0',
parserVersion: '0.2.0-ts.1',
profile: {},
graph: {
direction: 'BOTTOM_TO_TOP',
nodes: [first, second],
edges: [{
id: 'edge:0',
kind: 'EXCHANGE',
source: first.id,
target: second.id,
relationId: '1',
resolved: true,
metadata: { crossFragment: true },
}],
},
fragments: [
{ id: 'fragment:0', number: 0, pipelineIds: [first.pipelineId], nodeIds: [first.id] },
{ id: 'fragment:1', number: 1, pipelineIds: [second.pipelineId], nodeIds: [second.id] },
],
pipelines: [
{ id: first.pipelineId, fragmentId: first.fragmentId, number: 0, instanceNum: 1, nodeIds: [first.id] },
{ id: second.pipelineId, fragmentId: second.fragmentId, number: 2, instanceNum: 8, nodeIds: [second.id] },
],
unresolvedReferences: [],
warnings: [],
summary: {
fragmentCount: 2,
pipelineCount: 2,
nodeCount: 2,
edgeCount: 1,
unresolvedEdgeCount: 0,
criticalNodeId: second.id,
maxExecTimeNs: 20,
maxWaitTimeNs: null,
},
};
}
describe('Profile graph layout adapter', () => {
it('builds bottom-to-top compound Fragment input for ELK', () => {
const graph = buildElkGraph(fixture());
expect(graph.layoutOptions?.['elk.direction']).toBe('UP');
expect(graph.layoutOptions?.['elk.hierarchyHandling']).toBe('INCLUDE_CHILDREN');
expect(graph.children.map((fragment) => fragment.id)).toEqual(['fragment:0', 'fragment:1']);
expect(graph.edges).toEqual([{ id: 'edge:0', sources: ['fragment:0/pipeline:0/operator:0'], targets: ['fragment:1/pipeline:2/operator:0'] }]);
});
it('maps ELK output to fixed read-only React Flow nodes', async () => {
const result = await layoutProfileDag(fixture(), {
layout: (graph) => Promise.resolve({
...graph,
children: graph.children.map((fragment, index) => ({
...fragment,
x: index * 300,
y: index * 200,
width: 260,
height: 180,
children: fragment.children?.map((operator) => ({ ...operator, x: 20, y: 54 })),
})),
}),
});
const operator = result.nodes.find((item) => item.id === 'fragment:1/pipeline:2/operator:0');
expect(operator).toMatchObject({ parentId: 'fragment:1', draggable: false, connectable: false, position: { x: 20, y: 54 } });
expect(operator?.data).toMatchObject({ pipelineLabel: 'Pipeline 2', instanceNum: 8 });
expect(result.edges[0]).toMatchObject({ animated: false, reconnectable: false, data: { crossFragment: true } });
});
it('ranks slow operators and formats metrics without confusing zero and unknown', () => {
expect(selectSlowestOperators(fixture()).map((item) => item.execMaxNs)).toEqual([20, 10]);
expect(formatDurationNs(null)).toBe('Unknown');
expect(formatDurationNs(0)).toBe('0 ns');
expect(formatBytes(1536)).toBe('1.5 KiB');
});
});