diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/json_handler.ts b/ui/src/plugins/dev.perfetto.DataExplorer/json_handler.ts index 1935773340c..adb8516b36a 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/json_handler.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/json_handler.ts @@ -18,6 +18,7 @@ import {getAllNodes as getAllNodesUtil} from './query_builder/graph_utils'; import {Trace} from '../../public/trace'; import {SqlModules} from '../../plugins/dev.perfetto.SqlModules/sql_modules'; import {nodeRegistry} from './query_builder/node_registry'; +import {restoreLegacySecondaryInputs} from './query_builder/legacy_connections'; // Interfaces for the serialized JSON structure export interface SerializedNode { @@ -25,7 +26,14 @@ export interface SerializedNode { type: NodeType; state: object; nextNodes: string[]; - // Input node IDs (for multi-source nodes like Union, Merge, IntervalIntersect) + + // Graph-level connection fields (automatically captured during serialization). + // These replace per-node connection serialization (e.g. primaryInputId inside + // node state, or node-specific fields like leftNodeId, intervalNodes, etc.). + primaryInputId?: string; + secondaryInputIds?: {[port: string]: string}; + + // Deprecated: kept for backward compatibility with old saved graphs. inputNodeIds?: string[]; } @@ -50,12 +58,25 @@ function serializeNode(node: QueryNode): SerializedNode { throw new Error(`Node type ${node.type} is not serializable.`); } - return { + const serialized: SerializedNode = { nodeId: node.nodeId, type: node.type, state: node.serializeState(), nextNodes: node.nextNodes.map((n: QueryNode) => n.nodeId), }; + + // Automatically capture connections at the graph level. + if (node.primaryInput) { + serialized.primaryInputId = node.primaryInput.nodeId; + } + if (node.secondaryInputs && node.secondaryInputs.connections.size > 0) { + serialized.secondaryInputIds = {}; + for (const [port, inputNode] of node.secondaryInputs.connections) { + serialized.secondaryInputIds[port.toString()] = inputNode.nodeId; + } + } + + return serialized; } interface LabelData { @@ -265,7 +286,8 @@ export function deserializeState( }); } - // Third pass: set backward connections using the node registry + // Third pass: restore backward connections from graph-level fields. + // Falls back to per-node hooks for backward compatibility with old formats. for (const serializedNode of serializedGraph.nodes) { const node = nodes.get(serializedNode.nodeId); if (!node) { @@ -273,28 +295,39 @@ export function deserializeState( `Graph is corrupted. Node "${serializedNode.nodeId}" not found.`, ); } - const descriptor = nodeRegistry.getByNodeType(serializedNode.type); - if (!descriptor) { - throw new Error(`Unknown node type: ${serializedNode.type}`); + + // Restore primary input from graph-level field, or from node state + // for backward compatibility with old saved graphs. + const primaryInputId = + serializedNode.primaryInputId ?? + (serializedNode.state as {primaryInputId?: string}).primaryInputId; + if (primaryInputId) { + const inputNode = nodes.get(primaryInputId); + if (inputNode) { + node.primaryInput = inputNode; + } } - // Restore primary input for nodes that have one - const hasPrimary = - descriptor.hasPrimaryInput ?? descriptor.type === 'modification'; - if (hasPrimary) { - const serializedState = serializedNode.state as { - primaryInputId?: string; - }; - if (serializedState.primaryInputId) { - const inputNode = nodes.get(serializedState.primaryInputId); + // Restore secondary inputs from graph-level field. + if (serializedNode.secondaryInputIds && node.secondaryInputs) { + node.secondaryInputs.connections.clear(); + for (const [portStr, inputNodeId] of Object.entries( + serializedNode.secondaryInputIds, + )) { + const inputNode = nodes.get(inputNodeId); if (inputNode) { - node.primaryInput = inputNode; + node.secondaryInputs.connections.set( + parseInt(portStr, 10), + inputNode, + ); } } + } else if (node.secondaryInputs) { + // Backward compatibility: old saved graphs stored connection IDs + // inside node state. A single lookup table in legacy_connections.ts + // maps each node type to the old field name pattern. + restoreLegacySecondaryInputs(node, serializedNode.state, nodes); } - - // Node-specific connection deserialization - descriptor.deserializeConnections?.(node, serializedNode.state, nodes); } // Fourth pass: post-deserialization (resolve internal references, then diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/json_handler_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/json_handler_unittest.ts index 0100e6f81fb..7a088f5ffbf 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/json_handler_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/json_handler_unittest.ts @@ -2804,4 +2804,144 @@ describe('JSON serialization/deserialization', () => { text: 'Test label', }); // (400-400, 250-250) }); + + // ========================================================================= + // Tests for graph-level connection serialization + // Connections (primaryInputId, secondaryInputIds) should be stored at the + // SerializedNode level, NOT inside each node's serialized state. + // ========================================================================= + + test('serializes primaryInput at graph level, not in node state', () => { + const tableNode = new TableSourceNode({ + sqlTable: sqlModules.getTable('slice'), + trace, + sqlModules, + }); + const filterNode = new FilterNode({ + filters: [{column: 'name', op: '=', value: 'test'}], + }); + addConnection(tableNode, filterNode); + + const initialState: DataExplorerState = { + rootNodes: [tableNode], + selectedNodes: new Set(), + nodeLayouts: new Map(), + labels: [], + }; + + const json = serializeState(initialState); + const parsed = JSON.parse(json); + + // Find the filter node in the serialized output + const serializedFilter = parsed.nodes.find( + (n: SerializedNode) => n.nodeId === filterNode.nodeId, + ); + + // Connection should be at the SerializedNode level + expect(serializedFilter.primaryInputId).toBe(tableNode.nodeId); + + // Connection should NOT be inside node state + expect(serializedFilter.state.primaryInputId).toBeUndefined(); + }); + + test('serializes secondaryInputs at graph level, not in node state', () => { + const tableNode1 = new TableSourceNode({ + sqlTable: sqlModules.getTable('slice'), + trace, + sqlModules, + }); + const tableNode2 = new TableSourceNode({ + sqlTable: sqlModules.getTable('slice'), + trace, + sqlModules, + }); + const intervalNode = new IntervalIntersectNode({ + inputNodes: [tableNode1, tableNode2], + }); + tableNode1.nextNodes.push(intervalNode); + tableNode2.nextNodes.push(intervalNode); + + const initialState: DataExplorerState = { + rootNodes: [tableNode1, tableNode2], + selectedNodes: new Set(), + nodeLayouts: new Map(), + labels: [], + }; + + const json = serializeState(initialState); + const parsed = JSON.parse(json); + + // Find the interval intersect node + const serializedInterval = parsed.nodes.find( + (n: SerializedNode) => n.nodeId === intervalNode.nodeId, + ); + + // Secondary inputs should be at the SerializedNode level + expect(serializedInterval.secondaryInputIds).toEqual({ + '0': tableNode1.nodeId, + '1': tableNode2.nodeId, + }); + + // Connection IDs should NOT be inside node state + expect(serializedInterval.state.intervalNodes).toBeUndefined(); + }); + + test('deserializes connections from graph-level fields', () => { + // Build a graph: table -> filter (primaryInput) and table1, table2 -> union (secondaryInputs) + const tableNode = new TableSourceNode({ + sqlTable: sqlModules.getTable('slice'), + trace, + sqlModules, + }); + const filterNode = new FilterNode({ + filters: [{column: 'name', op: '=', value: 'x'}], + }); + addConnection(tableNode, filterNode); + + const tableNode2 = new TableSourceNode({ + sqlTable: sqlModules.getTable('slice'), + trace, + sqlModules, + }); + const unionNode = new UnionNode({ + inputNodes: [], + selectedColumns: [], + }); + tableNode.nextNodes.push(unionNode); + tableNode2.nextNodes.push(unionNode); + unionNode.secondaryInputs.connections.set(0, tableNode); + unionNode.secondaryInputs.connections.set(1, tableNode2); + + const initialState: DataExplorerState = { + rootNodes: [tableNode, tableNode2], + selectedNodes: new Set(), + nodeLayouts: new Map(), + labels: [], + }; + + const json = serializeState(initialState); + const deserialized = deserializeState(json, trace, sqlModules); + + // Find the union node in deserialized graph + const allNodes = new Map(); + const queue = [...deserialized.rootNodes]; + while (queue.length > 0) { + const node = queue.shift()!; + if (allNodes.has(node.nodeId)) continue; + allNodes.set(node.nodeId, node); + queue.push(...node.nextNodes); + } + + const deserializedUnion = [...allNodes.values()].find( + (n) => n.type === NodeType.kUnion, + ) as UnionNode; + expect(deserializedUnion).toBeDefined(); + expect(deserializedUnion.secondaryInputs.connections.size).toBe(2); + + const deserializedFilter = [...allNodes.values()].find( + (n) => n.type === NodeType.kFilter, + ) as FilterNode; + expect(deserializedFilter).toBeDefined(); + expect(deserializedFilter.primaryInput).toBeDefined(); + }); }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/core_nodes.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/core_nodes.ts index 8352248597b..82bd1fe6c71 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/core_nodes.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/core_nodes.ts @@ -193,17 +193,6 @@ export function registerCoreNodes() { ...(state as SqlSourceSerializedState), trace, }), - deserializeConnections: (node, state, allNodes) => { - const sqlSourceNode = node as SqlSourceNode; - const conns = SqlSourceNode.deserializeConnections( - allNodes, - state as SqlSourceSerializedState, - ); - sqlSourceNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.inputNodes.length; i++) { - sqlSourceNode.secondaryInputs.connections.set(i, conns.inputNodes[i]); - } - }, }); nodeRegistry.register('timerange', { @@ -287,16 +276,6 @@ export function registerCoreNodes() { state as AddColumnsNodeState, ), ), - deserializeConnections: (node, state, allNodes) => { - const addColumnsNode = node as AddColumnsNode; - const s = state as {secondaryInputNodeId?: string}; - if (s.secondaryInputNodeId) { - const secondaryInputNode = allNodes.get(s.secondaryInputNodeId); - if (secondaryInputNode) { - addColumnsNode.secondaryInputs.connections.set(0, secondaryInputNode); - } - } - }, }); nodeRegistry.register('modify_columns', { @@ -357,9 +336,6 @@ export function registerCoreNodes() { type: 'modification', category: 'Filter', nodeType: NodeType.kFilterDuring, - // Override: multisource nodes default to no primary input, but - // FilterDuring has both a primary input and secondary inputs. - hasPrimaryInput: true, factory: (state) => { return new FilterDuringNode(state as FilterDuringNodeState); }, @@ -368,20 +344,6 @@ export function registerCoreNodes() { ...FilterDuringNode.deserializeState(state as FilterDuringNodeState), sqlModules, }), - deserializeConnections: (node, state, allNodes) => { - const filterDuringNode = node as FilterDuringNode; - const conns = FilterDuringNode.deserializeConnections( - allNodes, - state as {secondaryInputNodeIds?: string[]}, - ); - filterDuringNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.secondaryInputNodes.length; i++) { - filterDuringNode.secondaryInputs.connections.set( - i, - conns.secondaryInputNodes[i], - ); - } - }, }); nodeRegistry.register('filter_in', { @@ -392,9 +354,6 @@ export function registerCoreNodes() { type: 'modification', category: 'Filter', nodeType: NodeType.kFilterIn, - // Override: multisource nodes default to no primary input, but - // FilterIn has both a primary input and secondary inputs. - hasPrimaryInput: true, factory: (state) => { return new FilterInNode(state as FilterInNodeState); }, @@ -402,20 +361,6 @@ export function registerCoreNodes() { new FilterInNode( FilterInNode.deserializeState(state as FilterInNodeState), ), - deserializeConnections: (node, state, allNodes) => { - const filterInNode = node as FilterInNode; - const conns = FilterInNode.deserializeConnections( - allNodes, - state as {secondaryInputNodeIds?: string[]}, - ); - filterInNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.secondaryInputNodes.length; i++) { - filterInNode.secondaryInputs.connections.set( - i, - conns.secondaryInputNodes[i], - ); - } - }, }); nodeRegistry.register('interval_intersect', { @@ -444,17 +389,6 @@ export function registerCoreNodes() { ), sqlModules, }), - deserializeConnections: (node, state, allNodes) => { - const intervalNode = node as IntervalIntersectNode; - const conns = IntervalIntersectNode.deserializeConnections( - allNodes, - state as IntervalIntersectSerializedState, - ); - intervalNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.inputNodes.length; i++) { - intervalNode.secondaryInputs.connections.set(i, conns.inputNodes[i]); - } - }, }); nodeRegistry.register('join', { @@ -484,19 +418,6 @@ export function registerCoreNodes() { ...JoinNode.deserializeState(state as JoinSerializedState), sqlModules, }), - deserializeConnections: (node, state, allNodes) => { - const joinNode = node as JoinNode; - const conns = JoinNode.deserializeConnections( - allNodes, - state as JoinSerializedState, - ); - if (conns.leftNode) { - joinNode.secondaryInputs.connections.set(0, conns.leftNode); - } - if (conns.rightNode) { - joinNode.secondaryInputs.connections.set(1, conns.rightNode); - } - }, postDeserializeLate: (node) => { const joinNode = node as JoinNode; joinNode.onPrevNodesUpdated(); @@ -529,19 +450,6 @@ export function registerCoreNodes() { ), sqlModules, }), - deserializeConnections: (node, state, allNodes) => { - const createSlicesNode = node as CreateSlicesNode; - const conns = CreateSlicesNode.deserializeConnections( - allNodes, - state as CreateSlicesSerializedState, - ); - if (conns.startsNode) { - createSlicesNode.secondaryInputs.connections.set(0, conns.startsNode); - } - if (conns.endsNode) { - createSlicesNode.secondaryInputs.connections.set(1, conns.endsNode); - } - }, }); nodeRegistry.register('sort_node', { @@ -579,17 +487,6 @@ export function registerCoreNodes() { ...UnionNode.deserializeState(state as UnionSerializedState), sqlModules, }), - deserializeConnections: (node, state, allNodes) => { - const unionNode = node as UnionNode; - const conns = UnionNode.deserializeConnections( - allNodes, - state as UnionSerializedState, - ); - unionNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.inputNodes.length; i++) { - unionNode.secondaryInputs.connections.set(i, conns.inputNodes[i]); - } - }, }); nodeRegistry.register('limit_and_offset_node', { @@ -615,7 +512,6 @@ export function registerCoreNodes() { 'Define a trace-based metric with value column and dimensions.', icon: 'analytics', type: 'export', - hasPrimaryInput: true, nodeType: NodeType.kMetrics, allowedChildren: ['trace_summary'], factory: (state) => new MetricsNode(state as MetricsNodeState), @@ -668,7 +564,6 @@ export function registerCoreNodes() { icon: 'dashboard', type: 'export', nodeType: NodeType.kDashboard, - hasPrimaryInput: true, allowedChildren: [], factory: (state) => new DashboardNode(state), deserialize: (state) => @@ -694,20 +589,6 @@ export function registerCoreNodes() { ), trace, }), - deserializeConnections: (node, state, allNodes) => { - const traceSummaryNode = node as TraceSummaryNode; - const conns = TraceSummaryNode.deserializeConnections( - allNodes, - state as {secondaryInputNodeIds?: string[]}, - ); - traceSummaryNode.secondaryInputs.connections.clear(); - for (let i = 0; i < conns.secondaryInputNodes.length; i++) { - traceSummaryNode.secondaryInputs.connections.set( - i, - conns.secondaryInputNodes[i], - ); - } - }, }); // Set the default allowed children for all nodes. diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/legacy_connections.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/legacy_connections.ts new file mode 100644 index 00000000000..a718f175c2b --- /dev/null +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/legacy_connections.ts @@ -0,0 +1,100 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed 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. + +// Backward compatibility: restores secondary input connections for old saved +// graphs that stored connection IDs inside individual node state instead of +// at the graph level. Each entry maps a NodeType to the old field name pattern. +// +// New graphs use SerializedNode.secondaryInputIds and never reach this code. + +import {NodeType, QueryNode} from '../query_node'; + +// Describes how secondary input IDs were stored in old node state. +type LegacyConnectionSpec = + // string[] field → sequential ports 0, 1, 2, ... + | {type: 'array'; field: string} + // single string field → one specific port + | {type: 'singleField'; field: string; port: number} + // named string fields → specific ports (e.g. leftNodeId→0, rightNodeId→1) + | {type: 'namedFields'; fields: Array<{field: string; port: number}>}; + +// Map from NodeType to the legacy field(s) that held secondary input IDs. +const LEGACY_SECONDARY_INPUT_SPECS: Partial< + Record +> = { + [NodeType.kSqlSource]: {type: 'array', field: 'inputNodeIds'}, + [NodeType.kAddColumns]: { + type: 'singleField', + field: 'secondaryInputNodeId', + port: 0, + }, + [NodeType.kFilterDuring]: {type: 'array', field: 'secondaryInputNodeIds'}, + [NodeType.kFilterIn]: {type: 'array', field: 'secondaryInputNodeIds'}, + [NodeType.kIntervalIntersect]: {type: 'array', field: 'intervalNodes'}, + [NodeType.kJoin]: { + type: 'namedFields', + fields: [ + {field: 'leftNodeId', port: 0}, + {field: 'rightNodeId', port: 1}, + ], + }, + [NodeType.kCreateSlices]: { + type: 'namedFields', + fields: [ + {field: 'startsNodeId', port: 0}, + {field: 'endsNodeId', port: 1}, + ], + }, + [NodeType.kUnion]: {type: 'array', field: 'unionNodes'}, + [NodeType.kTraceSummary]: {type: 'array', field: 'secondaryInputNodeIds'}, +}; + +/** + * Restores secondary input connections from old-format node state. + * Called during deserialization when no graph-level secondaryInputIds are + * present (i.e. the graph was saved before the new format was introduced). + */ +export function restoreLegacySecondaryInputs( + node: QueryNode, + state: object, + allNodes: Map, +): void { + if (!node.secondaryInputs) return; + const spec = LEGACY_SECONDARY_INPUT_SPECS[node.type]; + if (spec === undefined) return; + + const s = state as Record; + node.secondaryInputs.connections.clear(); + + if (spec.type === 'array') { + const ids = s[spec.field] as string[] | undefined; + if (!ids) return; + for (let i = 0; i < ids.length; i++) { + const n = allNodes.get(ids[i]); + if (n) node.secondaryInputs.connections.set(i, n); + } + } else if (spec.type === 'singleField') { + const id = s[spec.field] as string | undefined; + if (!id) return; + const n = allNodes.get(id); + if (n) node.secondaryInputs.connections.set(spec.port, n); + } else { + for (const {field, port} of spec.fields) { + const id = s[field] as string | undefined; + if (!id) continue; + const n = allNodes.get(id); + if (n) node.secondaryInputs.connections.set(port, n); + } + } +} diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_registry.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_registry.ts index 566c12833e1..a653bf8d593 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_registry.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_registry.ts @@ -83,14 +83,6 @@ export interface NodeDescriptor { sqlModules: SqlModules, ) => QueryNode; - // Restore secondary/backward connections after all nodes are created. - // Primary input is restored automatically based on hasPrimaryInput. - deserializeConnections?: ( - node: QueryNode, - state: object, - allNodes: Map, - ) => void; - // Post-deserialization hook (phase 1). Called after all connections are // restored. Used for resolving internal references (e.g. column resolution). postDeserialize?: (node: QueryNode) => void; @@ -100,11 +92,6 @@ export interface NodeDescriptor { // fully resolved (e.g. onPrevNodesUpdated). postDeserializeLate?: (node: QueryNode) => void; - // Whether this node has a primary input (vertical connection from above). - // If true, primaryInputId from serialized state will be auto-restored. - // Default: true for 'modification' nodes, false for 'source'/'multisource'. - hasPrimaryInput?: boolean; - // Optional list of registry IDs that are allowed as children of this node. // Controls which nodes appear in the "+" menu and which connections are valid. // If undefined, falls back to the registry's default allowed children. diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/add_columns_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/add_columns_node.ts index 921be2476e0..b66110dff0d 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/add_columns_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/add_columns_node.ts @@ -1676,13 +1676,7 @@ export class AddColumnsNode implements QueryNode { } serializeState(): object { - // Get the secondary input node ID (the node connected to port 0) - const secondaryInputNodeId = - this.secondaryInputs.connections.get(0)?.nodeId; - return { - primaryInputId: this.primaryInput?.nodeId, - secondaryInputNodeId, selectedColumns: this.state.selectedColumns, leftColumn: this.state.leftColumn, rightColumn: this.state.rightColumn, diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/aggregation_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/aggregation_node.ts index 79d76bafec2..0b4760584cb 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/aggregation_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/aggregation_node.ts @@ -612,9 +612,8 @@ export class AggregationNode implements QueryNode { }); } - serializeState(): AggregationSerializedState & {primaryInputId?: string} { + serializeState(): AggregationSerializedState { return { - primaryInputId: this.primaryInput?.nodeId, groupByColumns: this.state.groupByColumns.map((c) => ({ name: c.name, checked: c.checked, diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node.ts index 548ac6bd519..11ea8232559 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node.ts @@ -179,9 +179,7 @@ export class CounterToIntervalsNode implements QueryNode { } serializeState(): object { - return { - primaryInputId: this.primaryInput?.nodeId, - }; + return {}; } static deserializeState( diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node_unittest.ts index c93de4fc50b..8655736af6c 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/counter_to_intervals_node_unittest.ts @@ -435,9 +435,7 @@ describe('CounterToIntervalsNode', () => { const serialized = node.serializeState(); - expect(serialized).toEqual({ - primaryInputId: inputNode.nodeId, - }); + expect(serialized).toEqual({}); }); it('should handle missing input gracefully', () => { @@ -445,9 +443,7 @@ describe('CounterToIntervalsNode', () => { const serialized = node.serializeState(); - expect(serialized).toEqual({ - primaryInputId: undefined, - }); + expect(serialized).toEqual({}); }); }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node.ts index 61cb587bbfb..0aec16f9557 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node.ts @@ -45,8 +45,6 @@ const COMPUTED_STARTS_END_TS_COLUMN = 'exp_tmp_starts_computed_end_ts'; const COMPUTED_ENDS_END_TS_COLUMN = 'exp_tmp_ends_computed_end_ts'; export interface CreateSlicesSerializedState { - startsNodeId: string; - endsNodeId: string; startsMode?: TimestampMode; endsMode?: TimestampMode; startsTsColumn: string; @@ -636,8 +634,6 @@ export class CreateSlicesNode implements QueryNode { serializeState(): CreateSlicesSerializedState { return { - startsNodeId: this.startsNode?.nodeId ?? '', - endsNodeId: this.endsNode?.nodeId ?? '', startsMode: this.state.startsMode ?? 'ts', endsMode: this.state.endsMode ?? 'ts', startsTsColumn: this.state.startsTsColumn, @@ -659,17 +655,4 @@ export class CreateSlicesNode implements QueryNode { endsDurColumn: state.endsDurColumn, }; } - - static deserializeConnections( - nodes: Map, - state: CreateSlicesSerializedState, - ): { - startsNode?: QueryNode; - endsNode?: QueryNode; - } { - return { - startsNode: nodes.get(state.startsNodeId), - endsNode: nodes.get(state.endsNodeId), - }; - } } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node_unittest.ts index 852e2ab39e7..0584662f3da 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/create_slices_node_unittest.ts @@ -774,8 +774,6 @@ describe('CreateSlicesNode', () => { const serialized = createSlicesNode.serializeState(); - expect(serialized.startsNodeId).toBe('starts_node_id'); - expect(serialized.endsNodeId).toBe('ends_node_id'); expect(serialized.startsTsColumn).toBe('acquire_ts'); expect(serialized.endsTsColumn).toBe('release_ts'); }); @@ -790,16 +788,14 @@ describe('CreateSlicesNode', () => { const serialized = createSlicesNode.serializeState(); - expect(serialized.startsNodeId).toBe(''); - expect(serialized.endsNodeId).toBe(''); + expect(serialized.startsTsColumn).toBe('ts'); + expect(serialized.endsTsColumn).toBe('ts'); }); }); describe('deserializeState', () => { it('should deserialize state correctly', () => { const serialized = { - startsNodeId: 'starts_node_id', - endsNodeId: 'ends_node_id', startsTsColumn: 'acquire_ts', endsTsColumn: 'release_ts', }; @@ -812,8 +808,6 @@ describe('CreateSlicesNode', () => { it('should use default values when fields are missing', () => { const serialized = { - startsNodeId: 'starts', - endsNodeId: 'ends', startsTsColumn: undefined!, endsTsColumn: undefined!, }; @@ -825,56 +819,6 @@ describe('CreateSlicesNode', () => { }); }); - describe('deserializeConnections', () => { - it('should deserialize connections correctly', () => { - const startsNode = createMockPrevNode('starts_id', []); - const endsNode = createMockPrevNode('ends_id', []); - const nodes = new Map([ - ['starts_id', startsNode], - ['ends_id', endsNode], - ]); - - const connections = CreateSlicesNode.deserializeConnections(nodes, { - startsNodeId: 'starts_id', - endsNodeId: 'ends_id', - startsTsColumn: 'ts', - endsTsColumn: 'ts', - }); - - expect(connections.startsNode).toBe(startsNode); - expect(connections.endsNode).toBe(endsNode); - }); - - it('should handle missing nodes gracefully', () => { - const nodes = new Map(); - - const connections = CreateSlicesNode.deserializeConnections(nodes, { - startsNodeId: 'missing_starts', - endsNodeId: 'missing_ends', - startsTsColumn: 'ts', - endsTsColumn: 'ts', - }); - - expect(connections.startsNode).toBeUndefined(); - expect(connections.endsNode).toBeUndefined(); - }); - - it('should handle partially missing nodes', () => { - const startsNode = createMockPrevNode('starts_id', []); - const nodes = new Map([['starts_id', startsNode]]); - - const connections = CreateSlicesNode.deserializeConnections(nodes, { - startsNodeId: 'starts_id', - endsNodeId: 'missing_ends', - startsTsColumn: 'ts', - endsTsColumn: 'ts', - }); - - expect(connections.startsNode).toBe(startsNode); - expect(connections.endsNode).toBeUndefined(); - }); - }); - describe('auto-selection based on column type', () => { it('should auto-select timestamp column based on type, not name', () => { // Create a node with a single timestamp column with a custom name diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node.ts index 9bf971e46b0..b98b1294403 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node.ts @@ -189,9 +189,8 @@ export class DashboardNode implements QueryNode { this.state.onchange?.(); } - serializeState(): DashboardSerializedState & {primaryInputId?: string} { + serializeState(): DashboardSerializedState { return { - primaryInputId: this.primaryInput?.nodeId, exportName: this.state.exportName, }; } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node_unittest.ts index 51ba4c926a2..2d55268ec1f 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/dashboard_node_unittest.ts @@ -191,20 +191,6 @@ describe('DashboardNode', () => { expect(serialized.exportName).toBeUndefined(); }); - test('serializes primaryInputId', () => { - const source = createMockSourceNode('source-123'); - const node = makeNode(); - connectNodes(source, node); - const serialized = node.serializeState(); - expect(serialized.primaryInputId).toBe('source-123'); - }); - - test('primaryInputId is undefined without input', () => { - const node = makeNode(); - const serialized = node.serializeState(); - expect(serialized.primaryInputId).toBeUndefined(); - }); - test('deserializeState restores exportName', () => { const state = DashboardNode.deserializeState({exportName: 'Restored'}); expect(state.exportName).toBe('Restored'); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node.ts index 4007a47e357..52651792015 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node.ts @@ -469,10 +469,6 @@ export class FilterDuringNode implements QueryNode { serializeState(): object { return { - primaryInputId: this.primaryInput?.nodeId, - secondaryInputNodeIds: Array.from( - this.secondaryInputs.connections.values(), - ).map((node) => node.nodeId), partitionColumns: this.state.partitionColumns, clipToIntervals: this.state.clipToIntervals, }; @@ -487,24 +483,6 @@ export class FilterDuringNode implements QueryNode { }; } - static deserializeConnections( - nodes: Map, - serializedState: {secondaryInputNodeIds?: string[]}, - ): {secondaryInputNodes: QueryNode[]} { - const secondaryInputNodes: QueryNode[] = []; - if (serializedState.secondaryInputNodeIds) { - for (const nodeId of serializedState.secondaryInputNodeIds) { - const node = nodes.get(nodeId); - if (node) { - secondaryInputNodes.push(node); - } - } - } - return { - secondaryInputNodes, - }; - } - // Called when a node is connected/disconnected to secondary inputs onPrevNodesUpdated(): void { // Validate and clean up partition columns diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node_unittest.ts index fe3ae437141..f193181c43f 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_during_node_unittest.ts @@ -519,8 +519,6 @@ describe('FilterDuringNode', () => { const serialized = node.serializeState(); expect(serialized).toEqual({ - primaryInputId: primaryNode.nodeId, - secondaryInputNodeIds: [secondaryNode.nodeId], partitionColumns: ['utid'], clipToIntervals: false, }); @@ -532,8 +530,6 @@ describe('FilterDuringNode', () => { const serialized = node.serializeState(); expect(serialized).toEqual({ - primaryInputId: undefined, - secondaryInputNodeIds: [], partitionColumns: undefined, clipToIntervals: undefined, }); @@ -903,8 +899,6 @@ describe('FilterDuringNode', () => { const serialized = node.serializeState(); expect(serialized).toEqual({ - primaryInputId: primaryNode.nodeId, - secondaryInputNodeIds: [secondaryNode.nodeId], partitionColumns: ['utid'], clipToIntervals: undefined, }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node.ts index 7ed3ff98bd3..2fa49dc9a46 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node.ts @@ -374,10 +374,6 @@ export class FilterInNode implements QueryNode { serializeState(): object { return { - primaryInputId: this.primaryInput?.nodeId, - secondaryInputNodeIds: Array.from( - this.secondaryInputs.connections.values(), - ).map((node) => node.nodeId), baseColumn: this.state.baseColumn, matchColumn: this.state.matchColumn, }; @@ -392,24 +388,6 @@ export class FilterInNode implements QueryNode { }; } - static deserializeConnections( - nodes: Map, - serializedState: {secondaryInputNodeIds?: string[]}, - ): {secondaryInputNodes: QueryNode[]} { - const secondaryInputNodes: QueryNode[] = []; - if (serializedState.secondaryInputNodeIds) { - for (const nodeId of serializedState.secondaryInputNodeIds) { - const node = nodes.get(nodeId); - if (node) { - secondaryInputNodes.push(node); - } - } - } - return { - secondaryInputNodes, - }; - } - private cleanupStaleColumns(): void { if (this.primaryInput !== undefined && this.state.baseColumn) { const primaryCols = new Set( diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node_unittest.ts index a19a78df005..d307a02716c 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_in_node_unittest.ts @@ -516,30 +516,6 @@ describe('FilterInNode', () => { expect(serialized.baseColumn).toBe('utid'); expect(serialized.matchColumn).toBe('id'); }); - - it('should serialize secondary input node IDs', () => { - const matchNode = createMockNode({nodeId: 'match-123'}); - - const node = new FilterInNode({}); - node.secondaryInputs.connections.set(0, matchNode); - - const serialized = node.serializeState() as { - secondaryInputNodeIds: string[]; - }; - - expect(serialized.secondaryInputNodeIds).toEqual(['match-123']); - }); - - it('should serialize primaryInputId', () => { - const primaryNode = createMockNode({nodeId: 'primary-456'}); - - const node = new FilterInNode({}); - node.primaryInput = primaryNode; - - const serialized = node.serializeState() as {primaryInputId: string}; - - expect(serialized.primaryInputId).toBe('primary-456'); - }); }); describe('deserializeState', () => { @@ -554,37 +530,6 @@ describe('FilterInNode', () => { }); }); - describe('deserializeConnections', () => { - it('should restore secondary input connections', () => { - const mockNode = createMockNode({nodeId: 'match-1'}); - const nodes = new Map([['match-1', mockNode]]); - - const result = FilterInNode.deserializeConnections(nodes, { - secondaryInputNodeIds: ['match-1'], - }); - - expect(result.secondaryInputNodes).toEqual([mockNode]); - }); - - it('should handle missing nodes gracefully', () => { - const nodes = new Map(); - - const result = FilterInNode.deserializeConnections(nodes, { - secondaryInputNodeIds: ['nonexistent'], - }); - - expect(result.secondaryInputNodes).toEqual([]); - }); - - it('should handle empty secondaryInputNodeIds', () => { - const nodes = new Map(); - - const result = FilterInNode.deserializeConnections(nodes, {}); - - expect(result.secondaryInputNodes).toEqual([]); - }); - }); - describe('getTitle', () => { it('should return Filter In', () => { const node = new FilterInNode({}); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_node.ts index ad442fc4637..759f7d63b03 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/filter_node.ts @@ -521,7 +521,6 @@ export class FilterNode implements QueryNode { serializeState(): object { return { - primaryInputId: this.primaryInput?.nodeId, filters: this.state.filters?.map((f) => { if ('value' in f) { return { diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node.ts index 41dc0e3dca7..3ad808511f0 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node.ts @@ -48,7 +48,6 @@ import {loadNodeDoc} from '../node_doc_loader'; import {getCommonColumns} from '../utils'; export interface IntervalIntersectSerializedState { - intervalNodes: string[]; comment?: string; partitionColumns?: string[]; // Columns to partition by during interval intersection tsDurSource?: 'intersection' | number; // Source for ts/dur: 'intersection' or input index @@ -749,10 +748,6 @@ export class IntervalIntersectNode implements QueryNode { serializeState(): IntervalIntersectSerializedState { return { - // Store ALL input node IDs (not just slice(1)) for reliable deserialization - intervalNodes: this.inputNodesList - .filter((n): n is QueryNode => n !== undefined) - .map((n) => n.nodeId), partitionColumns: this.state.partitionColumns, tsDurSource: this.state.tsDurSource, }; @@ -767,17 +762,4 @@ export class IntervalIntersectNode implements QueryNode { tsDurSource: state.tsDurSource, }; } - - static deserializeConnections( - nodes: Map, - state: IntervalIntersectSerializedState, - ): {inputNodes: QueryNode[]} { - // Resolve all input nodes from their IDs - const inputNodes = state.intervalNodes - .map((id) => nodes.get(id)) - .filter((node): node is QueryNode => node !== undefined); - return { - inputNodes, - }; - } } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node_unittest.ts index ef229c99bbf..b486d7fd762 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/interval_intersect_node_unittest.ts @@ -796,8 +796,6 @@ describe('IntervalIntersectNode', () => { const serialized = node.serializeState(); - // All input node IDs are now serialized - expect(serialized.intervalNodes).toEqual(['node1', 'node2', 'node3']); expect(serialized.partitionColumns).toEqual(['utid']); }); @@ -819,80 +817,29 @@ describe('IntervalIntersectNode', () => { const serialized = node.serializeState(); - // All input node IDs are now serialized - expect(serialized.intervalNodes).toEqual(['node1', 'node2']); expect(serialized.partitionColumns).toBeUndefined(); }); }); describe('deserializeState', () => { - it('should deserialize state correctly', () => { - const node1 = createMockPrevNode('node1', [ - createColumnInfo('id', 'INT'), - createColumnInfo('ts', 'INT64'), - createColumnInfo('dur', 'INT64'), - ]); - const node2 = createMockPrevNode('node2', [ - createColumnInfo('id', 'INT'), - createColumnInfo('ts', 'INT64'), - createColumnInfo('dur', 'INT64'), - ]); - const node3 = createMockPrevNode('node3', [ - createColumnInfo('id', 'INT'), - createColumnInfo('ts', 'INT64'), - createColumnInfo('dur', 'INT64'), - ]); - - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ['node3', node3], - ]); - + it('should deserialize partition columns', () => { const serialized = { - // All input node IDs are stored - intervalNodes: ['node1', 'node2', 'node3'], partitionColumns: ['utid'], }; - const deserialized = IntervalIntersectNode.deserializeConnections( - nodes, - serialized, - ); + const deserialized = IntervalIntersectNode.deserializeState(serialized); - expect(deserialized.inputNodes).toEqual([node1, node2, node3]); + expect(deserialized.partitionColumns).toEqual(['utid']); + expect(deserialized.inputNodes).toEqual([]); }); - it('should handle missing nodes gracefully', () => { - const node1 = createMockPrevNode('node1', [ - createColumnInfo('id', 'INT'), - createColumnInfo('ts', 'INT64'), - createColumnInfo('dur', 'INT64'), - ]); - const node2 = createMockPrevNode('node2', [ - createColumnInfo('id', 'INT'), - createColumnInfo('ts', 'INT64'), - createColumnInfo('dur', 'INT64'), - ]); - - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ]); + it('should handle missing partition columns', () => { + const serialized = {}; - const serialized = { - // Include a missing node ID to test graceful handling - intervalNodes: ['node1', 'node2', 'node_missing'], - partitionColumns: ['utid'], - }; - - const deserialized = IntervalIntersectNode.deserializeConnections( - nodes, - serialized, - ); + const deserialized = IntervalIntersectNode.deserializeState(serialized); - // Should only include found nodes (node_missing is filtered out) - expect(deserialized.inputNodes).toEqual([node1, node2]); + expect(deserialized.partitionColumns).toBeUndefined(); + expect(deserialized.inputNodes).toEqual([]); }); }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node.ts index 1be1d138a5e..2688c883db7 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node.ts @@ -42,8 +42,6 @@ import {JoinConditionSelector, JoinConditionDisplay} from '../join_widgets'; import {ResizableSqlEditor} from '../widgets'; export interface JoinSerializedState { - leftNodeId: string; - rightNodeId: string; leftQueryAlias: string; rightQueryAlias: string; conditionType: 'equality' | 'freeform'; @@ -521,8 +519,6 @@ export class JoinNode implements QueryNode { serializeState(): JoinSerializedState { return { - leftNodeId: this.leftNode?.nodeId ?? '', - rightNodeId: this.rightNode?.nodeId ?? '', leftQueryAlias: this.state.leftQueryAlias, rightQueryAlias: this.state.rightQueryAlias, conditionType: this.state.conditionType, @@ -578,17 +574,4 @@ export class JoinNode implements QueryNode { })) ?? [], }; } - - static deserializeConnections( - nodes: Map, - state: JoinSerializedState, - ): { - leftNode?: QueryNode; - rightNode?: QueryNode; - } { - return { - leftNode: nodes.get(state.leftNodeId), - rightNode: nodes.get(state.rightNodeId), - }; - } } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node_unittest.ts index 51c1a1f503f..8bf8a2561f7 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/join_node_unittest.ts @@ -908,8 +908,6 @@ describe('JoinNode', () => { const serialized = joinNode.serializeState(); - expect(serialized.leftNodeId).toBe('node1'); - expect(serialized.rightNodeId).toBe('node2'); expect(serialized.leftQueryAlias).toBe('left'); expect(serialized.rightQueryAlias).toBe('right'); expect(serialized.conditionType).toBe('equality'); @@ -921,8 +919,6 @@ describe('JoinNode', () => { describe('deserializeState', () => { it('should deserialize state correctly', () => { const serialized = { - leftNodeId: 'node1', - rightNodeId: 'node2', leftQueryAlias: 'left', rightQueryAlias: 'right', conditionType: 'equality' as const, @@ -944,8 +940,6 @@ describe('JoinNode', () => { it('should deserialize legacy string types into PerfettoSqlType', () => { const legacySerialized = { - leftNodeId: 'node1', - rightNodeId: 'node2', leftQueryAlias: 'left', rightQueryAlias: 'right', conditionType: 'equality' as const, @@ -973,8 +967,6 @@ describe('JoinNode', () => { it('should deserialize new PerfettoSqlType objects correctly', () => { const newSerialized: JoinSerializedState = { - leftNodeId: 'node1', - rightNodeId: 'node2', leftQueryAlias: 'left', rightQueryAlias: 'right', conditionType: 'equality', @@ -1007,51 +999,6 @@ describe('JoinNode', () => { }); }); - describe('deserializeConnections', () => { - it('should deserialize connections correctly', () => { - const node1 = createMockPrevNode('node1', []); - const node2 = createMockPrevNode('node2', []); - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ]); - - const connections = JoinNode.deserializeConnections(nodes, { - leftNodeId: 'node1', - rightNodeId: 'node2', - leftQueryAlias: 'left', - rightQueryAlias: 'right', - conditionType: 'equality', - joinType: 'INNER', - leftColumn: 'id', - rightColumn: 'id', - sqlExpression: '', - }); - - expect(connections.leftNode).toBe(node1); - expect(connections.rightNode).toBe(node2); - }); - - it('should handle missing nodes gracefully', () => { - const nodes = new Map(); - - const connections = JoinNode.deserializeConnections(nodes, { - leftNodeId: 'missing1', - rightNodeId: 'missing2', - leftQueryAlias: 'left', - rightQueryAlias: 'right', - conditionType: 'equality', - joinType: 'INNER', - leftColumn: 'id', - rightColumn: 'id', - sqlExpression: '', - }); - - expect(connections.leftNode).toBeUndefined(); - expect(connections.rightNode).toBeUndefined(); - }); - }); - describe('serialize/deserialize round-trip', () => { it('should preserve checked columns after serialization and deserialization', () => { // Create mock source nodes with columns @@ -1094,19 +1041,12 @@ describe('JoinNode', () => { // Deserialize the state const deserializedState = JoinNode.deserializeState(serialized); - // Create nodes map for connection deserialization - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ]); - const connections = JoinNode.deserializeConnections(nodes, serialized); - // Create a new join node from deserialized state - // This simulates what happens when loading from saved state + // Connections are now restored at the graph level by json_handler const restoredNode = new JoinNode({ ...deserializedState, - leftNode: connections.leftNode, - rightNode: connections.rightNode, + leftNode: node1, + rightNode: node2, }); // BUG: After deserialization and reconstruction, checked status is lost @@ -1165,18 +1105,12 @@ describe('JoinNode', () => { // Deserialize the state const deserializedState = JoinNode.deserializeState(serialized); - // Create nodes map for connection deserialization - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ]); - const connections = JoinNode.deserializeConnections(nodes, serialized); - // Create a new join node from deserialized state + // Connections are now restored at the graph level by json_handler const restoredNode = new JoinNode({ ...deserializedState, - leftNode: connections.leftNode, - rightNode: connections.rightNode, + leftNode: node1, + rightNode: node2, }); // BUG: Aliases and checked status are both lost after deserialization diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/limit_and_offset_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/limit_and_offset_node.ts index 9a5a11a4a7e..f785f9bbfb4 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/limit_and_offset_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/limit_and_offset_node.ts @@ -182,7 +182,6 @@ export class LimitAndOffsetNode implements QueryNode { // Only return serializable fields, excluding callbacks and objects // that might contain circular references return { - primaryInputId: this.primaryInput?.nodeId, limit: this.state.limit, offset: this.state.offset, }; diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node.ts index 10aae4464eb..455c2a65fef 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node.ts @@ -936,7 +936,7 @@ export class MetricsNode implements QueryNode { return templateSpec; } - serializeState(): MetricsSerializedState & {primaryInputId?: string} { + serializeState(): MetricsSerializedState { // Strip transient `expanded` from dimension configs before serializing. const dimensionConfigs: Record = {}; for (const [name, cfg] of Object.entries(this.state.dimensionConfigs)) { @@ -946,7 +946,6 @@ export class MetricsNode implements QueryNode { } } return { - primaryInputId: this.primaryInput?.nodeId, metricIdPrefix: this.state.metricIdPrefix, valueColumns: this.state.valueColumns.map(({expanded: _, ...vc}) => vc), dimensionConfigs: diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node_unittest.ts index c02bc7e4fe8..8e07c62cc1a 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/metrics_node_unittest.ts @@ -826,7 +826,6 @@ describe('MetricsNode', () => { expect(serialized.valueColumns?.[0].unit).toBe('BYTES'); expect(serialized.valueColumns?.[0].polarity).toBe('HIGHER_IS_BETTER'); expect(serialized.dimensionUniqueness).toBe('UNIQUE'); - expect(serialized.primaryInputId).toBe(inputNode.nodeId); }); it('should serialize multiple value columns', () => { @@ -845,14 +844,6 @@ describe('MetricsNode', () => { expect(serialized.valueColumns?.[0].column).toBe('cpu'); expect(serialized.valueColumns?.[1].column).toBe('mem'); }); - - it('should handle missing primary input', () => { - const node = new MetricsNode(makeState()); - - const serialized = node.serializeState(); - - expect(serialized.primaryInputId).toBeUndefined(); - }); }); describe('deserializeState', () => { @@ -1334,22 +1325,6 @@ describe('MetricsNode', () => { expect(restoredNode.state.valueColumns).toHaveLength(0); }); - - it('should include primaryInputId in serialized state', () => { - const inputCols = [createColumnInfo('value', 'int')]; - const inputNode = createMockNodeWithStructuredQuery( - 'input-123', - inputCols, - ); - (inputNode as {nodeId: string}).nodeId = 'input-123'; - - const node = new MetricsNode(makeState()); - connectNodes(inputNode, node); - - const serialized = node.serializeState(); - - expect(serialized.primaryInputId).toBe('input-123'); - }); }); }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/modify_columns_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/modify_columns_node.ts index b1d481104d8..dc41c45148a 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/modify_columns_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/modify_columns_node.ts @@ -415,7 +415,6 @@ export class ModifyColumnsNode implements QueryNode { serializeState(): ModifyColumnsSerializedState { return { - primaryInputId: this.primaryInput?.nodeId, selectedColumns: this.state.selectedColumns.map((c) => ({ name: c.name, type: c.column.type, diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sort_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sort_node.ts index ddf9b01ff4a..a769bf2b759 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sort_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sort_node.ts @@ -299,7 +299,6 @@ export class SortNode implements QueryNode { // Only return serializable fields, excluding callbacks and objects // that might contain circular references return { - primaryInputId: this.primaryInput?.nodeId, sortCriteria: this.state.sortCriteria, }; } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source.ts index 77d273c6868..54ae19c835a 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source.ts @@ -39,7 +39,6 @@ import {NodeTitle} from '../../node_styling_widgets'; export interface SqlSourceSerializedState { sql?: string; comment?: string; - inputNodeIds?: string[]; } export interface SqlSourceState extends QueryNodeState { @@ -233,28 +232,11 @@ export class SqlSourceNode implements QueryNode { } serializeState(): SqlSourceSerializedState { - // Serialize input node IDs in port order - const inputNodeIds = [...this.secondaryInputs.connections.entries()] - .sort(([a], [b]) => a - b) - .map(([, node]) => node.nodeId); - return { sql: this.state.sql, - inputNodeIds: inputNodeIds.length > 0 ? inputNodeIds : undefined, }; } - static deserializeConnections( - nodes: Map, - state: SqlSourceSerializedState, - ): {inputNodes: QueryNode[]} { - // Resolve input nodes from their IDs - const inputNodes = (state.inputNodeIds ?? []) - .map((id) => nodes.get(id)) - .filter((node): node is QueryNode => node !== undefined); - return {inputNodes}; - } - getStructuredQuery(): protos.PerfettoSqlStructuredQuery | undefined { // Build dependencies from connected input nodes // Each input can be referenced in SQL as $input_0, $input_1, etc. diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source_unittest.ts index 2be344e8b15..01828d80baf 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/sources/sql_source_unittest.ts @@ -446,78 +446,6 @@ describe('SqlSourceNode', () => { expect(serialized.sql).toBe('SELECT * FROM slice'); }); - - it('should not include inputNodeIds when no inputs connected', () => { - const node = new SqlSourceNode({ - sql: 'SELECT * FROM slice', - trace: mockTrace, - }); - - const serialized = node.serializeState(); - - expect(serialized.inputNodeIds).toBeUndefined(); - }); - - it('should serialize input node IDs in port order', () => { - const node = new SqlSourceNode({ - sql: 'SELECT * FROM $input_0 JOIN $input_1', - trace: mockTrace, - }); - - const input0 = createMockNodeWithSq('node0', []); - const input1 = createMockNodeWithSq('node1', []); - - // Connect in reverse order - connectSecondary(input1, node, 1); - connectSecondary(input0, node, 0); - - const serialized = node.serializeState(); - - expect(serialized.inputNodeIds).toEqual(['node0', 'node1']); - }); - }); - - describe('deserializeConnections', () => { - it('should return empty inputNodes when no inputNodeIds', () => { - const nodes = new Map(); - - const connections = SqlSourceNode.deserializeConnections(nodes, { - sql: 'SELECT * FROM slice', - }); - - expect(connections.inputNodes).toEqual([]); - }); - - it('should resolve input nodes from IDs', () => { - const node0 = createMockNodeWithSq('node0', []); - const node1 = createMockNodeWithSq('node1', []); - const nodes = new Map([ - ['node0', node0], - ['node1', node1], - ]); - - const connections = SqlSourceNode.deserializeConnections(nodes, { - sql: 'SELECT * FROM $input_0 JOIN $input_1', - inputNodeIds: ['node0', 'node1'], - }); - - expect(connections.inputNodes.length).toBe(2); - expect(connections.inputNodes[0]).toBe(node0); - expect(connections.inputNodes[1]).toBe(node1); - }); - - it('should filter out missing nodes gracefully', () => { - const node0 = createMockNodeWithSq('node0', []); - const nodes = new Map([['node0', node0]]); - - const connections = SqlSourceNode.deserializeConnections(nodes, { - sql: 'SELECT * FROM $input_0', - inputNodeIds: ['node0', 'missing_node'], - }); - - expect(connections.inputNodes.length).toBe(1); - expect(connections.inputNodes[0]).toBe(node0); - }); }); describe('nodeSpecificModify', () => { diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node.ts index 6b597e0732c..2bb582bd064 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node.ts @@ -42,9 +42,7 @@ import { buildEmbeddedQueryTree, } from '../query_builder_utils'; -export interface TraceSummarySerializedState { - secondaryInputNodeIds?: string[]; -} +export interface TraceSummarySerializedState {} export interface TraceSummaryNodeState extends QueryNodeState {} @@ -349,16 +347,7 @@ export class TraceSummaryNode implements QueryNode { } serializeState(): TraceSummarySerializedState { - const secondaryInputNodeIds: string[] = []; - for (const [, node] of [...this.secondaryInputs.connections.entries()].sort( - ([a], [b]) => a - b, - )) { - secondaryInputNodeIds.push(node.nodeId); - } - return { - secondaryInputNodeIds: - secondaryInputNodeIds.length > 0 ? secondaryInputNodeIds : undefined, - }; + return {}; } static deserializeState( @@ -366,20 +355,4 @@ export class TraceSummaryNode implements QueryNode { ): TraceSummaryNodeState { return {}; } - - static deserializeConnections( - allNodes: Map, - state: {secondaryInputNodeIds?: string[]}, - ): {secondaryInputNodes: QueryNode[]} { - const secondaryInputNodes: QueryNode[] = []; - if (state.secondaryInputNodeIds) { - for (const id of state.secondaryInputNodeIds) { - const node = allNodes.get(id); - if (node !== undefined) { - secondaryInputNodes.push(node); - } - } - } - return {secondaryInputNodes}; - } } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node_unittest.ts index 0b9f6af1101..cf20d0a230c 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node_unittest.ts @@ -201,59 +201,16 @@ describe('TraceSummaryNode', () => { }); describe('serialization', () => { - test('serializes secondary input node IDs', () => { - const {metrics: metrics1} = makeConnectedMetricsNode('m1'); - const {metrics: metrics2} = makeConnectedMetricsNode('m2'); - const node = new TraceSummaryNode({}); - connectSecondary(metrics1, node, 0); - connectSecondary(metrics2, node, 1); - - const serialized = node.serializeState(); - expect(serialized.secondaryInputNodeIds).toEqual([ - metrics1.nodeId, - metrics2.nodeId, - ]); - }); - - test('omits secondaryInputNodeIds when no inputs', () => { + test('serializeState returns empty object', () => { const node = new TraceSummaryNode({}); const serialized = node.serializeState(); - expect(serialized.secondaryInputNodeIds).toBeUndefined(); + expect(serialized).toEqual({}); }); test('deserializeState returns empty state', () => { const state = TraceSummaryNode.deserializeState({}); expect(state).toEqual({}); }); - - test('deserializeConnections restores secondary inputs', () => { - const metrics1 = makeMetricsNode({metricIdPrefix: 'm1'}); - const metrics2 = makeMetricsNode({metricIdPrefix: 'm2'}); - const allNodes = new Map>([ - [metrics1.nodeId, metrics1], - [metrics2.nodeId, metrics2], - ]); - - const result = TraceSummaryNode.deserializeConnections(allNodes, { - secondaryInputNodeIds: [metrics1.nodeId, metrics2.nodeId], - }); - - expect(result.secondaryInputNodes).toHaveLength(2); - }); - - test('deserializeConnections handles missing nodes', () => { - const allNodes = new Map(); - const result = TraceSummaryNode.deserializeConnections(allNodes, { - secondaryInputNodeIds: ['nonexistent'], - }); - expect(result.secondaryInputNodes).toHaveLength(0); - }); - - test('deserializeConnections handles undefined IDs', () => { - const allNodes = new Map(); - const result = TraceSummaryNode.deserializeConnections(allNodes, {}); - expect(result.secondaryInputNodes).toHaveLength(0); - }); }); describe('clone', () => { diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node.ts index bc423baf413..d1e7622fbe5 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node.ts @@ -37,7 +37,6 @@ import { } from '../node_styling_widgets'; export interface UnionSerializedState { - unionNodes: string[]; selectedColumns: ColumnInfo[]; comment?: string; } @@ -307,8 +306,6 @@ export class UnionNode implements QueryNode { serializeState(): UnionSerializedState { return { - // Store ALL input node IDs for reliable deserialization - unionNodes: this.inputNodesList.map((n) => n.nodeId), selectedColumns: this.state.selectedColumns, comment: this.comment, }; @@ -320,17 +317,4 @@ export class UnionNode implements QueryNode { selectedColumns: state.selectedColumns, }; } - - static deserializeConnections( - nodes: Map, - state: UnionSerializedState, - ): {inputNodes: QueryNode[]} { - // Resolve all input nodes from their IDs - const inputNodes = state.unionNodes - .map((id) => nodes.get(id)) - .filter((node): node is QueryNode => node !== undefined); - return { - inputNodes, - }; - } } diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node_unittest.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node_unittest.ts index 7e7e67c0df2..342e0d812a8 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node_unittest.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/union_node_unittest.ts @@ -607,7 +607,6 @@ describe('UnionNode', () => { const serialized = unionNode.serializeState(); - expect(serialized.unionNodes).toEqual(['node1', 'node2']); expect(serialized.selectedColumns.length).toBe(1); expect(serialized.selectedColumns[0].column.name).toBe('id'); expect(serialized.comment).toBe('test comment'); @@ -617,7 +616,6 @@ describe('UnionNode', () => { describe('deserializeState', () => { it('should deserialize state correctly', () => { const serialized = { - unionNodes: ['node1', 'node2'], selectedColumns: [createColumnInfo('id', 'int')], comment: 'test comment', }; @@ -629,41 +627,4 @@ describe('UnionNode', () => { expect(state.selectedColumns[0].column.name).toBe('id'); }); }); - - describe('deserializeConnections', () => { - it('should deserialize connections correctly', () => { - const node1 = createMockNodeWithSq('node1', []); - const node2 = createMockNodeWithSq('node2', []); - const node3 = createMockNodeWithSq('node3', []); - const nodes = new Map([ - ['node1', node1], - ['node2', node2], - ['node3', node3], - ]); - - const connections = UnionNode.deserializeConnections(nodes, { - unionNodes: ['node1', 'node2', 'node3'], - selectedColumns: [], - }); - - expect(connections.inputNodes.length).toBe(3); - expect(connections.inputNodes[0]).toBe(node1); - expect(connections.inputNodes[1]).toBe(node2); - expect(connections.inputNodes[2]).toBe(node3); - }); - - it('should handle missing nodes gracefully', () => { - const node1 = createMockNodeWithSq('node1', []); - const nodes = new Map([['node1', node1]]); - - const connections = UnionNode.deserializeConnections(nodes, { - unionNodes: ['node1', 'missing1', 'node2'], - selectedColumns: [], - }); - - // Should only include node1, filtering out undefined entries - expect(connections.inputNodes.length).toBe(1); - expect(connections.inputNodes[0]).toBe(node1); - }); - }); }); diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts index f108cdec0d6..c353f4a6e5d 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts @@ -833,7 +833,6 @@ export class VisualisationNode implements QueryNode { serializeState(): object { return { - primaryInputId: this.primaryInput?.nodeId, chartConfigs: this.state.chartConfigs.map((c) => ({ id: c.id, name: c.name,