/** * @license * Copyright 2024 The Model Explorer Authors. All Rights Reserved. * * 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. * ============================================================================== */ /// import {Graph} from '../common/input_graph'; import {GroupNode, ModelGraph} from '../common/model_graph'; import {NodeDataProviderRunData, ShowOnNodeItemData} from '../common/types'; import {getDeepestExpandedGroupNodeIds, isGroupNode} from '../common/utils'; import {VisualizerConfig} from '../common/visualizer_config'; import { ExpandOrCollapseGroupNodeResponse, LocateNodeResponse, PreparePopupResponse, ProcessGraphResponse, ProcessingLabel, RelayoutGraphResponse, WorkerEvent, WorkerEventType, } from '../common/worker_events'; import {Dagre} from './dagre_types'; import {GraphExpander} from './graph_expander'; import {GraphLayout} from './graph_layout'; import {GraphProcessor} from './graph_processor'; import {IdenticalGroupsFinder} from './identical_groups_finder'; import {updateProcessingProgress} from './utils'; import '../../../../public/static_files/worker_deps.js'; declare var dagre: Dagre; // -> ModelGraph let MODEL_GRAPHS_CACHE: Record = {}; self.addEventListener('message', (event: Event) => { const workerEvent = (event as ExtendableMessageEvent).data as WorkerEvent; switch (workerEvent.eventType) { // Handle processing input graph. case WorkerEventType.PROCESS_GRAPH_REQ: { const modelGraph = handleProcessGraph( workerEvent.paneId, workerEvent.graph, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, workerEvent.config, workerEvent.groupNodeChildrenCountThreshold, workerEvent.flattenLayers, workerEvent.keepLayersWithASingleChild, workerEvent.initialLayout, ); cacheModelGraph(modelGraph, workerEvent.paneId); const resp: ProcessGraphResponse = { eventType: WorkerEventType.PROCESS_GRAPH_RESP, modelGraph, paneId: workerEvent.paneId, }; postMessage(resp); break; } case WorkerEventType.PREPARE_POPUP_REQ: { // Clone the model graph from the pane to a graph for the renderer id // (the renderer id for the model graph in the popup). const modelGraph = getCachedModelGraph( workerEvent.modelGraphId, workerEvent.paneId, ); const clonedModelGraph = JSON.parse( JSON.stringify(modelGraph), ) as ModelGraph; cacheModelGraph(clonedModelGraph, workerEvent.rendererId); const resp: PreparePopupResponse = { eventType: WorkerEventType.PREPARE_POPUP_RESP, modelGraph, paneId: workerEvent.paneId, rendererId: workerEvent.rendererId, groupNodeId: workerEvent.groupNodeId, initialPosition: workerEvent.initialPosition, }; postMessage(resp); break; } case WorkerEventType.EXPAND_OR_COLLAPSE_GROUP_NODE_REQ: { const modelGraph = getCachedModelGraph( workerEvent.modelGraphId, workerEvent.rendererId, ); let deepestExpandedGroupNodeIds: string[] = []; if (workerEvent.expand) { deepestExpandedGroupNodeIds = handleExpandGroupNode( modelGraph, workerEvent.groupNodeId, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, workerEvent.selectedNodeDataProviderRunId, workerEvent.all === true, workerEvent.config, ); } else { deepestExpandedGroupNodeIds = handleCollapseGroupNode( modelGraph, workerEvent.groupNodeId, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, workerEvent.selectedNodeDataProviderRunId, workerEvent.all === true, workerEvent.config, ); } cacheModelGraph(modelGraph, workerEvent.rendererId); const resp: ExpandOrCollapseGroupNodeResponse = { eventType: WorkerEventType.EXPAND_OR_COLLAPSE_GROUP_NODE_RESP, modelGraph, expanded: workerEvent.expand, groupNodeId: workerEvent.groupNodeId, rendererId: workerEvent.rendererId, deepestExpandedGroupNodeIds, }; postMessage(resp); break; } case WorkerEventType.RELAYOUT_GRAPH_REQ: { const modelGraph = getCachedModelGraph( workerEvent.modelGraphId, workerEvent.rendererId, ); handleReLayoutGraph( modelGraph, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, workerEvent.selectedNodeDataProviderRunId, workerEvent.targetDeepestGroupNodeIdsToExpand, workerEvent.clearAllExpandStates, workerEvent.config, ); cacheModelGraph(modelGraph, workerEvent.rendererId); const resp: RelayoutGraphResponse = { eventType: WorkerEventType.RELAYOUT_GRAPH_RESP, modelGraph, selectedNodeId: workerEvent.selectedNodeId, rendererId: workerEvent.rendererId, forRestoringUiState: workerEvent.forRestoringUiState, rectToZoomFit: workerEvent.rectToZoomFit, forRestoringSnapshotAfterTogglingFlattenLayers: workerEvent.forRestoringSnapshotAfterTogglingFlattenLayers, targetDeepestGroupNodeIdsToExpand: workerEvent.targetDeepestGroupNodeIdsToExpand, triggerNavigationSync: workerEvent.triggerNavigationSync, }; postMessage(resp); break; } case WorkerEventType.LOCATE_NODE_REQ: { const modelGraph = getCachedModelGraph( workerEvent.modelGraphId, workerEvent.rendererId, ); const deepestExpandedGroupNodeIds = handleLocateNode( modelGraph, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, workerEvent.selectedNodeDataProviderRunId, workerEvent.nodeId, workerEvent.config, ); cacheModelGraph(modelGraph, workerEvent.rendererId); const resp: LocateNodeResponse = { eventType: WorkerEventType.LOCATE_NODE_RESP, modelGraph, nodeId: workerEvent.nodeId, rendererId: workerEvent.rendererId, deepestExpandedGroupNodeIds, noNodeShake: workerEvent.noNodeShake, select: workerEvent.select, }; postMessage(resp); break; } case WorkerEventType.CLEANUP: { MODEL_GRAPHS_CACHE = {}; break; } default: break; } }); function handleProcessGraph( paneId: string, graph: Graph, showItemOnNodeTypes: Record, nodeDataProviderRuns: Record, config?: VisualizerConfig, groupNodeChildrenCountThreshold?: number, flattenLayers?: boolean, keepLayersWithASingleChild?: boolean, initialLayout?: boolean, ): ModelGraph { let error: string | undefined = undefined; // Processes the given input graph `Graph` into a `ModelGraph`. const processor = new GraphProcessor( paneId, graph, config, showItemOnNodeTypes, {}, groupNodeChildrenCountThreshold, false, flattenLayers, keepLayersWithASingleChild, ); const modelGraph = processor.process(); // Check nodes with empty ids. if (modelGraph.nodesById[''] != null) { error = 'Some nodes have empty strings as ids which will cause layout failures. See console for details.'; console.warn('Nodes with empty ids', modelGraph.nodesById['']); } // Do the initial layout. if (!error && initialLayout) { const layout = new GraphLayout( modelGraph, dagre, showItemOnNodeTypes, nodeDataProviderRuns, undefined, ); try { layout.layout(); } catch (e) { error = `Failed to layout graph: ${e}`; } } updateProcessingProgress( paneId, ProcessingLabel.LAYING_OUT_ROOT_LAYER, error, ); // Find identical groups. const identicalGroupsFinder = new IdenticalGroupsFinder(modelGraph); identicalGroupsFinder.markIdenticalGroups(); updateProcessingProgress(paneId, ProcessingLabel.FINDING_IDENTICAL_LAYERS); return modelGraph; } function handleExpandGroupNode( modelGraph: ModelGraph, groupNodeId: string | undefined, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, selectedNodeDataProviderRunId: string | undefined, all: boolean, config?: VisualizerConfig, ): string[] { const expander = new GraphExpander( modelGraph, dagre, showOnNodeItemTypes, nodeDataProviderRuns, selectedNodeDataProviderRunId, false, config, ); // Expane group node. if (groupNodeId != null) { let deepestExpandedGroupNodeId: string[] | undefined = undefined; const groupNode = modelGraph.nodesById[groupNodeId]; if (groupNode && isGroupNode(groupNode)) { groupNode.expanded = true; // Recursively expand child group node if there is only one child. let curGroupNode = groupNode; while (true) { const childrenIds = curGroupNode.nsChildrenIds || []; if (childrenIds.length === 1) { const child = modelGraph.nodesById[childrenIds[0]]; if (child && isGroupNode(child)) { child.expanded = true; curGroupNode = child; } else { break; } } else { break; } } // Get the deepest expanded group nodes from the curGroupNode and we will // be doing relayout from there. const ids: string[] = []; getDeepestExpandedGroupNodeIds(curGroupNode, modelGraph, ids); deepestExpandedGroupNodeId = ids.length === 0 ? [curGroupNode.id] : ids; // Clear layout data for all nodes under curGroupNode. // // This is necessary because the node overlay might have been changed so // we need to re-calculate the node sizes. for (const nodeId of curGroupNode.descendantsNodeIds || []) { const node = modelGraph.nodesById[nodeId]; node.width = undefined; node.height = undefined; } } if (all) { for (const childNodeId of (groupNode as GroupNode).descendantsNodeIds || []) { const node = modelGraph.nodesById[childNodeId]; if (isGroupNode(node)) { node.expanded = true; } } deepestExpandedGroupNodeId = undefined; } expander.reLayoutGraph(deepestExpandedGroupNodeId); const ids: string[] = []; getDeepestExpandedGroupNodeIds(undefined, modelGraph, ids); return ids; } // Expand all group nodes in the graph. else { return expander.expandAllGroups(); } } function handleCollapseGroupNode( modelGraph: ModelGraph, groupNodeId: string | undefined, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, selectedNodeDataProviderRunId: string | undefined, all: boolean, config?: VisualizerConfig, ): string[] { const expander = new GraphExpander( modelGraph, dagre, showOnNodeItemTypes, nodeDataProviderRuns, selectedNodeDataProviderRunId, false, config, ); if (groupNodeId != null) { if (all) { const groupNode = modelGraph.nodesById[groupNodeId] as GroupNode; for (const childNodeId of groupNode.descendantsNodeIds || []) { const node = modelGraph.nodesById[childNodeId]; if (isGroupNode(node)) { node.expanded = false; node.width = undefined; node.height = undefined; delete modelGraph.edgesByGroupNodeIds[node.id]; } } } return expander.collapseGroupNode(groupNodeId); } else { return expander.collapseAllGroup(); } } function handleReLayoutGraph( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, selectedNodeDataProviderRunId: string | undefined, targetDeepestGroupNodeIdsToExpand?: string[], clearAllExpandStates?: boolean, config?: VisualizerConfig, ) { const expander = new GraphExpander( modelGraph, dagre, showOnNodeItemTypes, nodeDataProviderRuns, selectedNodeDataProviderRunId, false, config, ); expander.reLayoutGraph( targetDeepestGroupNodeIdsToExpand, clearAllExpandStates, ); } function handleLocateNode( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, selectedNodeDataProviderRunId: string | undefined, nodeId: string, config?: VisualizerConfig, ): string[] { const expander = new GraphExpander( modelGraph, dagre, showOnNodeItemTypes, nodeDataProviderRuns, selectedNodeDataProviderRunId, false, config, ); return expander.expandToRevealNode(nodeId); } function cacheModelGraph(modelGraph: ModelGraph, rendererId: string) { MODEL_GRAPHS_CACHE[getModelGraphKey(modelGraph.id, rendererId)] = modelGraph; } function getCachedModelGraph( modelGraphId: string, rendererId: string, ): ModelGraph { const cachedModelGraph = MODEL_GRAPHS_CACHE[getModelGraphKey(modelGraphId, rendererId)]; if (cachedModelGraph == null) { throw new Error( `ModelGraph with id "${modelGraphId}" not found for rendererId "${rendererId}"`, ); } return cachedModelGraph; } function getModelGraphKey(modelGraphId: string, rendererId: string): string { return `${modelGraphId}___${rendererId}`; }