Files
weave-scope/client/app/scripts/selectors/resource-view/layout.js
2017-10-17 19:04:41 +02:00

191 lines
8.3 KiB
JavaScript

import debug from 'debug';
import { times } from 'lodash';
import { fromJS, Map as makeMap } from 'immutable';
import { createSelector } from 'reselect';
import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles';
import {
RESOURCE_VIEW_MAX_LAYERS,
RESOURCE_VIEW_LAYERS,
TOPOLOGIES_WITH_CAPACITY,
} from '../../constants/resources';
import {
nodeParentDecoratorByTopologyId,
nodeMetricSummaryDecoratorByType,
nodeResourceViewColorDecorator,
nodeResourceBoxDecorator,
} from '../../decorators/node';
const log = debug('scope:nodes-layout');
// Used for ordering the resource nodes.
const resourceNodeConsumptionComparator = (node) => {
const metricSummary = node.get('metricSummary');
return metricSummary.get('showCapacity') ?
-metricSummary.get('relativeConsumption') :
-metricSummary.get('absoluteConsumption');
};
// A list of topologies shown in the resource view of the active topology (bottom to top).
export const layersTopologyIdsSelector = createSelector(
[
state => state.get('currentTopologyId'),
],
topologyId => fromJS(RESOURCE_VIEW_LAYERS[topologyId] || [])
);
// Calculates the resource view layer Y-coordinate for every topology in the resource view.
export const layerVerticalPositionByTopologyIdSelector = createSelector(
[
layersTopologyIdsSelector,
],
(topologiesIds) => {
let yPositions = makeMap();
let yCumulative = RESOURCES_LAYER_PADDING;
topologiesIds.forEach((topologyId) => {
yCumulative -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING;
yPositions = yPositions.set(topologyId, yCumulative);
});
return yPositions;
}
);
// Decorate and filter all the nodes to be displayed in the current resource view, except
// for the exact node horizontal offsets which are calculated from the data created here.
const decoratedNodesByTopologySelector = createSelector(
[
layersTopologyIdsSelector,
state => state.get('pinnedMetricType'),
// Generate the dependencies for this selector programmatically (because we want their
// number to be customizable directly by changing the constant). The dependency functions
// here depend on another selector, but this seems to work quite fine. For example, if
// layersTopologyIdsSelector = ['hosts', 'containers'] and RESOURCE_VIEW_MAX_LAYERS = 3,
// this code will generate:
// [
// state => state.getIn(['nodesByTopology', 'hosts'])
// state => state.getIn(['nodesByTopology', 'containers'])
// state => state.getIn(['nodesByTopology', undefined])
// ]
// which will all be captured by `topologiesNodes` and processed correctly (even for undefined).
...times(RESOURCE_VIEW_MAX_LAYERS, index => (
state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)])
))
],
(layersTopologyIds, pinnedMetricType, ...topologiesNodes) => {
let nodesByTopology = makeMap();
let parentLayerTopologyId = null;
topologiesNodes.forEach((topologyNodes, index) => {
const layerTopologyId = layersTopologyIds.get(index);
const parentTopologyNodes = nodesByTopology.get(parentLayerTopologyId, makeMap());
const showCapacity = TOPOLOGIES_WITH_CAPACITY.includes(layerTopologyId);
const isBaseLayer = (index === 0);
const nodeParentDecorator = nodeParentDecoratorByTopologyId(parentLayerTopologyId);
const nodeMetricSummaryDecorator =
nodeMetricSummaryDecoratorByType(pinnedMetricType, showCapacity);
// Color the node, deduce its anchor point, dimensions and info about its pinned metric.
const decoratedTopologyNodes = (topologyNodes || makeMap())
.map(nodeResourceViewColorDecorator)
.map(nodeMetricSummaryDecorator)
.map(nodeResourceBoxDecorator)
.map(nodeParentDecorator);
const filteredTopologyNodes = decoratedTopologyNodes
// Filter out the nodes with no parent in the topology of the previous layer, as their
// positions in the layout could not be determined. The exception is the base layer.
// TODO: Also make an exception for uncontained nodes (e.g. processes).
.filter(node => parentTopologyNodes.has(node.get('parentNodeId')) || isBaseLayer)
// Filter out the nodes with no metric summary data, which is needed to render the node.
.filter(node => node.get('metricSummary'))
// Filter out nodes with zero-width, as they will never be shown, no matter how much we
// zoom in. The example is every node consuming less than 0.005% CPU, as the 2-digit
// precision will round it down to zero.
.filter(node => node.get('width') > 0);
nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes);
parentLayerTopologyId = layerTopologyId;
});
return nodesByTopology;
}
);
// Calculate (and fix) the offsets for all the displayed resource nodes.
export const layoutNodesByTopologyIdSelector = createSelector(
[
layersTopologyIdsSelector,
decoratedNodesByTopologySelector,
],
(layersTopologyIds, nodesByTopology) => {
let layoutNodes = makeMap();
let parentTopologyId = null;
// Calculate the offsets bottom-to top as each layer needs to know exact offsets of its parents.
layersTopologyIds.forEach((layerTopologyId) => {
let positionedNodes = makeMap();
// Get the nodes in the current layer grouped by their parent nodes.
// Each of those buckets will be positioned and sorted independently.
const nodesByParent = nodesByTopology
.get(layerTopologyId, makeMap())
.groupBy(n => n.get('parentNodeId'));
nodesByParent.forEach((nodesBucket, parentNodeId) => {
// Set the initial offset to the offset of the parent (that has already been set).
// If there is no offset information, i.e. we're processing the base layer, set it to 0.
const parentNode = layoutNodes.getIn([parentTopologyId, parentNodeId], makeMap());
let currentOffset = parentNode.get('offset', 0);
// Sort the nodes in the current bucket and lay them down one after another.
nodesBucket.sortBy(resourceNodeConsumptionComparator).forEach((node, nodeId) => {
const positionedNode = node.set('offset', currentOffset);
positionedNodes = positionedNodes.set(nodeId, positionedNode);
currentOffset += node.get('width');
});
// TODO: This block of code checks for the overlaps which are caused by children
// consuming more resources than their parent node. This happens due to inconsistent
// data being sent from the backend and it needs to be fixed there.
const parentOffset = parentNode.get('offset', 0);
const parentWidth = parentNode.get('width', currentOffset);
const totalChildrenWidth = currentOffset - parentOffset;
// If the total width of the children exceeds the parent node box width, we have a problem.
// We fix it by shrinking all the children to by a factor to perfectly fit into the parent.
if (totalChildrenWidth > parentWidth) {
const shrinkFactor = parentWidth / totalChildrenWidth;
log(`Inconsistent data: Children of ${parentNodeId} reported to use more ` +
`resource than the node itself - shrinking by factor ${shrinkFactor}`);
// Shrink all the children.
nodesBucket.forEach((_, nodeId) => {
let node = positionedNodes.get(nodeId);
// Shrink the width of the resource box and update its relative offset.
node = node.merge(makeMap({
offset: ((node.get('offset') - parentOffset) * shrinkFactor) + parentOffset,
width: node.get('width') * shrinkFactor,
}));
// Update the metrics summary to reflect the adjusted dimensions for consistent data.
node = nodeMetricSummaryDecoratorByType(
node.getIn(['metricSummary', 'type']),
node.get('showCapacity'),
shrinkFactor
)(node);
// Update the node in the layout.
positionedNodes = positionedNodes.mergeIn([nodeId], node);
});
}
});
// Update the layout with the positioned node from the current layer.
layoutNodes = layoutNodes.mergeIn([layerTopologyId], positionedNodes);
parentTopologyId = layerTopologyId;
});
return layoutNodes;
}
);