Offload simulation calculation to web worker.

This commit is contained in:
Kia Lam
2022-02-23 18:54:18 -08:00
parent 7fbab6760e
commit fd135caed5
4 changed files with 217 additions and 171 deletions

View File

@@ -4,11 +4,11 @@ import { t } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
EmptyState as PFEmptyState, EmptyState as PFEmptyState,
EmptyStateIcon, Progress,
ProgressMeasureLocation,
Text, Text,
TextContent, TextContent,
TextVariants, TextVariants,
Spinner,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { TopologyIcon as PFTopologyIcon } from '@patternfly/react-icons'; import { TopologyIcon as PFTopologyIcon } from '@patternfly/react-icons';
@@ -23,10 +23,16 @@ const TopologyIcon = styled(PFTopologyIcon)`
fill: #6a6e73; fill: #6a6e73;
`; `;
const ContentLoading = ({ className }) => ( const ContentLoading = ({ className, progress }) => (
<EmptyState variant="full" className={className}> <EmptyState variant="full" className={className}>
<TopologyIcon /> <TopologyIcon />
<TextContent> <Progress
value={progress}
measureLocation={ProgressMeasureLocation.inside}
aria-label={t`content-loading-in-progress`}
style={{ margin: '20px' }}
/>
<TextContent style={{ margin: '20px' }}>
<Text <Text
component={TextVariants.small} component={TextVariants.small}
style={{ fontWeight: 'bold', color: 'black' }} style={{ fontWeight: 'bold', color: 'black' }}
@@ -34,7 +40,6 @@ const ContentLoading = ({ className }) => (
{t`Please wait until the topology view is populated...`} {t`Please wait until the topology view is populated...`}
</Text> </Text>
</TextContent> </TextContent>
<EmptyStateIcon variant="container" component={Spinner} />
</EmptyState> </EmptyState>
); );

View File

@@ -19,7 +19,6 @@ import {
// getRandomInt, // getRandomInt,
} from './utils/helpers'; } from './utils/helpers';
import { import {
MESH_FORCE_LAYOUT,
DEFAULT_RADIUS, DEFAULT_RADIUS,
DEFAULT_NODE_COLOR, DEFAULT_NODE_COLOR,
DEFAULT_NODE_HIGHLIGHT_COLOR, DEFAULT_NODE_HIGHLIGHT_COLOR,
@@ -39,10 +38,12 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
const [isNodeSelected, setIsNodeSelected] = useState(false); const [isNodeSelected, setIsNodeSelected] = useState(false);
const [selectedNode, setSelectedNode] = useState(null); const [selectedNode, setSelectedNode] = useState(null);
const [nodeDetail, setNodeDetail] = useState(null); const [nodeDetail, setNodeDetail] = useState(null);
const [simulationProgress, setSimulationProgress] = useState(null);
const history = useHistory(); const history = useHistory();
// const data = generateRandomNodes(getRandomInt(4, 50)); // const data = generateRandomNodes(getRandomInt(4, 50));
const draw = () => { const draw = () => {
setShowZoomControls(false);
const width = getWidth(SELECTOR); const width = getWidth(SELECTOR);
const height = getHeight(SELECTOR); const height = getHeight(SELECTOR);
@@ -58,33 +59,54 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
const graph = data; const graph = data;
const simulation = d3 /* WEB WORKER */
.forceSimulation() const worker = new Worker(
.nodes(graph.nodes) new URL('./utils/workers/simulationWorker.js', import.meta.url)
.force( );
'charge',
d3.forceManyBody().strength(MESH_FORCE_LAYOUT.defaultForceStrength)
)
.force(
'link',
d3.forceLink(graph.links).id((d) => d.hostname)
)
.force(
'collide',
d3.forceCollide(MESH_FORCE_LAYOUT.defaultCollisionFactor)
)
.force('forceX', d3.forceX(MESH_FORCE_LAYOUT.defaultForceX))
.force('forceY', d3.forceY(MESH_FORCE_LAYOUT.defaultForceY))
.force('center', d3.forceCenter(width / 2, height / 2));
const link = mesh worker.postMessage({
nodes: graph.nodes,
links: graph.links,
});
worker.onmessage = function handleWorkerEvent(event) {
switch (event.data.type) {
case 'tick':
return ticked(event.data);
case 'end':
return ended(event.data);
default:
return false;
}
};
function ticked({ progress }) {
const calculatedPercent = Math.round(progress * 100);
setSimulationProgress(calculatedPercent);
}
function ended({ nodes, links }) {
// Remove loading screen
d3.select('.simulation-loader').style('visibility', 'hidden');
setShowZoomControls(true);
// Center the mesh
const simulation = d3
.forceSimulation(nodes)
.force('center', d3.forceCenter(width / 2, height / 2));
simulation.tick();
// Add links
mesh
.append('g') .append('g')
.attr('class', `links`) .attr('class', `links`)
.attr('data-cy', 'links') .attr('data-cy', 'links')
.selectAll('line') .selectAll('line')
.data(graph.links) .data(links)
.enter() .enter()
.append('line') .append('line')
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
.attr('class', (_, i) => `link-${i}`) .attr('class', (_, i) => `link-${i}`)
.attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`) .attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`)
.style('fill', 'none') .style('fill', 'none')
@@ -94,13 +116,13 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
.on('mouseover', function showPointer() { .on('mouseover', function showPointer() {
d3.select(this).transition().style('cursor', 'pointer'); d3.select(this).transition().style('cursor', 'pointer');
}); });
// add nodes
const node = mesh const node = mesh
.append('g') .append('g')
.attr('class', 'nodes') .attr('class', 'nodes')
.attr('data-cy', 'nodes') .attr('data-cy', 'nodes')
.selectAll('g') .selectAll('g')
.data(graph.nodes) .data(nodes)
.enter() .enter()
.append('g') .append('g')
.on('mouseenter', function handleNodeHover(_, d) { .on('mouseenter', function handleNodeHover(_, d) {
@@ -119,6 +141,8 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
node node
.append('circle') .append('circle')
.attr('r', DEFAULT_RADIUS) .attr('r', DEFAULT_RADIUS)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('class', (d) => d.node_type) .attr('class', (d) => d.node_type)
.attr('class', (d) => `id-${d.id}`) .attr('class', (d) => `id-${d.id}`)
.attr('fill', DEFAULT_NODE_COLOR) .attr('fill', DEFAULT_NODE_COLOR)
@@ -128,6 +152,8 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
node node
.append('text') .append('text')
.text((d) => renderNodeType(d.node_type)) .text((d) => renderNodeType(d.node_type))
.attr('x', (d) => d.x)
.attr('y', (d) => d.y)
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central') .attr('dominant-baseline', 'central')
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR); .attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR);
@@ -136,11 +162,12 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
const hostNames = node.append('g'); const hostNames = node.append('g');
hostNames hostNames
.append('text') .append('text')
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 40)
.text((d) => renderLabelText(d.node_state, d.hostname)) .text((d) => renderLabelText(d.node_state, d.hostname))
.attr('class', 'placeholder') .attr('class', 'placeholder')
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR) .attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR)
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('y', 40)
.each(function calculateLabelWidth() { .each(function calculateLabelWidth() {
// eslint-disable-next-line react/no-this-in-sfc // eslint-disable-next-line react/no-this-in-sfc
const bbox = this.getBBox(); const bbox = this.getBBox();
@@ -158,27 +185,12 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
svg.selectAll('text.placeholder').remove(); svg.selectAll('text.placeholder').remove();
hostNames hostNames
.append('text') .append('text')
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 38)
.text((d) => renderLabelText(d.node_state, d.hostname)) .text((d) => renderLabelText(d.node_state, d.hostname))
.attr('font-size', DEFAULT_FONT_SIZE) .attr('font-size', DEFAULT_FONT_SIZE)
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR) .attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR)
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle');
.attr('y', 38);
simulation.nodes(graph.nodes).on('tick', ticked);
simulation.force('link').links(graph.links);
function ticked() {
d3.select('.simulation-loader').style('visibility', 'visible');
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
node.attr('transform', (d) => `translate(${d.x},${d.y})`);
calculateAlphaDecay(simulation.alpha(), simulation.alphaMin(), 20);
}
svg.call(zoom); svg.call(zoom);
@@ -187,9 +199,10 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
svg svg
.select(`circle.id-${n.id}`) .select(`circle.id-${n.id}`)
.attr('fill', DEFAULT_NODE_HIGHLIGHT_COLOR); .attr('fill', DEFAULT_NODE_HIGHLIGHT_COLOR);
const immediate = graph.links.filter( const immediate = links.filter(
(l) => (l) =>
n.hostname === l.source.hostname || n.hostname === l.target.hostname n.hostname === l.source.hostname ||
n.hostname === l.target.hostname
); );
immediate.forEach((s) => { immediate.forEach((s) => {
svg svg
@@ -203,7 +216,7 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
function deselectSiblings(n) { function deselectSiblings(n) {
svg.select(`circle.id-${n.id}`).attr('fill', DEFAULT_NODE_COLOR); svg.select(`circle.id-${n.id}`).attr('fill', DEFAULT_NODE_COLOR);
const immediate = graph.links.filter( const immediate = links.filter(
(l) => (l) =>
n.hostname === l.source.hostname || n.hostname === l.target.hostname n.hostname === l.source.hostname || n.hostname === l.target.hostname
); );
@@ -233,14 +246,6 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
setIsNodeSelected(true); setIsNodeSelected(true);
setSelectedNode(n); setSelectedNode(n);
} }
function calculateAlphaDecay(a, aMin, x) {
setShowZoomControls(false);
const decayPercentage = Math.min((aMin / a) * 100);
if (decayPercentage >= x) {
d3.select('.simulation-loader').style('visibility', 'hidden');
setShowZoomControls(true);
}
} }
}; };
@@ -267,7 +272,7 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
redirectToDetailsPage(selectedNode, history) redirectToDetailsPage(selectedNode, history)
} }
/> />
<Loader className="simulation-loader" /> <Loader className="simulation-loader" progress={simulationProgress} />
</div> </div>
); );
} }

View File

@@ -3,8 +3,9 @@ export const PARENTSELECTOR = '.mesh-svg';
export const CHILDSELECTOR = '.mesh'; export const CHILDSELECTOR = '.mesh';
export const DEFAULT_RADIUS = 16; export const DEFAULT_RADIUS = 16;
export const MESH_FORCE_LAYOUT = { export const MESH_FORCE_LAYOUT = {
defaultCollisionFactor: DEFAULT_RADIUS * 2 + 20, defaultCollisionFactor: DEFAULT_RADIUS * 2 + 30,
defaultForceStrength: -30, defaultForceStrength: -50,
defaultForceBody: 15,
defaultForceX: 0, defaultForceX: 0,
defaultForceY: 0, defaultForceY: 0,
}; };

View File

@@ -0,0 +1,35 @@
import * as d3 from 'd3';
import { MESH_FORCE_LAYOUT } from '../../constants';
onmessage = function calculateLayout({ data: { nodes, links } }) {
const simulation = d3
.forceSimulation(nodes)
.force(
'charge',
d3
.forceManyBody(MESH_FORCE_LAYOUT.defaultForceBody)
.strength(MESH_FORCE_LAYOUT.defaultForceStrength)
)
.force(
'link',
d3.forceLink(links).id((d) => d.hostname)
)
.force('collide', d3.forceCollide(MESH_FORCE_LAYOUT.defaultCollisionFactor))
.force('forceX', d3.forceX(MESH_FORCE_LAYOUT.defaultForceX))
.force('forceY', d3.forceY(MESH_FORCE_LAYOUT.defaultForceY))
.stop();
for (
let i = 0,
n = Math.ceil(
Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())
);
i < n;
++i
) {
postMessage({ type: 'tick', progress: i / n });
simulation.tick();
}
postMessage({ type: 'end', nodes, links });
};