mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
Performance tweaks
Add debug.html to show toolbar Perfjankie test runner Playing w/ the pure mixin for perf. improvements * Works well! Smoother zooming/panning when things have settled. * Extract node movement to node-container, make nodes pure Extracted node chart elements into own components Keep control objects immutable while in components Keep layout state objects alive Made other components pure, removed mixin from stateless components Remove font size adjustment from scaling Fix zoomscale Move node transform to node * makes more sense there because the coords are rounded in the container dynamic coords precision based on topology size Make edge points immutable Remove nodes maximum for layout engine Dont send all canvas state down to next component moving layout handling back to nodes-chart.js Omit some props for edges/nodes, dont animate edges on low precision Moved AppStore access out of lower components
This commit is contained in:
2
Makefile
2
Makefile
@@ -20,7 +20,7 @@ CODECGEN_EXE=$(CODECGEN_DIR)/bin/codecgen_$(shell go env GOHOSTOS)_$(shell go en
|
||||
GET_CODECGEN_DEPS=$(shell find $(1) -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' -not -name '*.codecgen.go' -not -name '*.generated.go')
|
||||
CODECGEN_TARGETS=report/report.codecgen.go render/render.codecgen.go render/detailed/detailed.codecgen.go
|
||||
RM=--rm
|
||||
RUN_FLAGS=-ti
|
||||
RUN_FLAGS=-i
|
||||
BUILD_IN_CONTAINER=true
|
||||
GO_ENV=GOGC=off
|
||||
GO=env $(GO_ENV) go
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
app/scripts/vendor/term.js
|
||||
test/
|
||||
|
||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
||||
build/app.js
|
||||
build/*[woff2?|ttf|eot|svg]
|
||||
coverage/
|
||||
test/*png
|
||||
|
||||
96
client/app/scripts/charts/edge-container.js
Normal file
96
client/app/scripts/charts/edge-container.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
|
||||
import Edge from './edge';
|
||||
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const pointCount = 30;
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(d => d.x)
|
||||
.y(d => d.y);
|
||||
|
||||
const buildPath = (points, layoutPrecision) => {
|
||||
const extracted = [];
|
||||
_.each(points, (value, key) => {
|
||||
const axis = key[0];
|
||||
const index = key.slice(1);
|
||||
if (!extracted[index]) {
|
||||
extracted[index] = {};
|
||||
}
|
||||
extracted[index][axis] = d3.round(value, layoutPrecision);
|
||||
});
|
||||
return extracted;
|
||||
};
|
||||
|
||||
export default class EdgeContainer extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
pointsMap: makeMap()
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.preparePoints(this.props.points);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.preparePoints(nextProps.points);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutPrecision, points } = this.props;
|
||||
const other = _.omit(this.props, 'points');
|
||||
|
||||
if (layoutPrecision === 0) {
|
||||
const path = line(points.toJS());
|
||||
return <Edge {...other} path={path} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion style={this.state.pointsMap.toJS()}>
|
||||
{(interpolated) => {
|
||||
// convert points to path string, because that lends itself to
|
||||
// JS-equality checks in the child component
|
||||
const path = line(buildPath(interpolated, layoutPrecision));
|
||||
return <Edge {...other} path={path} />;
|
||||
}}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
preparePoints(nextPoints) {
|
||||
// Spring needs constant field count, hoping that dagre will insert never more than `pointCount`
|
||||
let { pointsMap } = this.state;
|
||||
|
||||
// filling up the map with copies of the first point
|
||||
const filler = nextPoints.first();
|
||||
const missing = pointCount - nextPoints.size;
|
||||
let index = 0;
|
||||
if (missing > 0) {
|
||||
while (index < missing) {
|
||||
pointsMap = pointsMap.set(`x${index}`, spring(filler.get('x'), animConfig));
|
||||
pointsMap = pointsMap.set(`y${index}`, spring(filler.get('y'), animConfig));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
nextPoints.forEach((point, i) => {
|
||||
pointsMap = pointsMap.set(`x${index + i}`, spring(point.get('x'), animConfig));
|
||||
pointsMap = pointsMap.set(`y${index + i}`, spring(point.get('y'), animConfig));
|
||||
});
|
||||
|
||||
this.setState({ pointsMap });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reactMixin.onClass(EdgeContainer, PureRenderMixin);
|
||||
@@ -1,102 +1,45 @@
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { enterEdge, leaveEdge } from '../actions/app-actions';
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(d => d.x)
|
||||
.y(d => d.y);
|
||||
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
|
||||
const flattenPoints = points => {
|
||||
const flattened = {};
|
||||
points.forEach((point, i) => {
|
||||
flattened[`x${i}`] = spring(point.x, animConfig);
|
||||
flattened[`y${i}`] = spring(point.y, animConfig);
|
||||
});
|
||||
return flattened;
|
||||
};
|
||||
|
||||
const extractPoints = points => {
|
||||
const extracted = [];
|
||||
_.each(points, (value, key) => {
|
||||
const axis = key[0];
|
||||
const index = key.slice(1);
|
||||
if (!extracted[index]) {
|
||||
extracted[index] = {};
|
||||
}
|
||||
extracted[index][axis] = value;
|
||||
});
|
||||
return extracted;
|
||||
};
|
||||
|
||||
export default class Edge extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
this.state = {
|
||||
points: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.ensureSameLength(this.props.points);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.ensureSameLength(nextProps.points);
|
||||
}
|
||||
|
||||
render() {
|
||||
const classNames = ['edge'];
|
||||
const points = flattenPoints(this.props.points);
|
||||
const props = this.props;
|
||||
const handleMouseEnter = this.handleMouseEnter;
|
||||
const handleMouseLeave = this.handleMouseLeave;
|
||||
const { hasSelectedNode, highlightedEdgeIds, id, layoutPrecision,
|
||||
path, selectedNodeId, source, target } = this.props;
|
||||
|
||||
if (this.props.highlighted) {
|
||||
const classNames = ['edge'];
|
||||
if (highlightedEdgeIds.has(id)) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
if (this.props.blurred) {
|
||||
if (hasSelectedNode
|
||||
&& source !== selectedNodeId
|
||||
&& target !== selectedNodeId) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
if (hasSelectedNode && layoutPrecision === 0
|
||||
&& (source === selectedNodeId || target === selectedNodeId)) {
|
||||
classNames.push('focused');
|
||||
}
|
||||
const classes = classNames.join(' ');
|
||||
|
||||
return (
|
||||
<Motion style={points}>
|
||||
{(interpolated) => {
|
||||
const path = line(extractPoints(interpolated));
|
||||
return (
|
||||
<g className={classes} onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave} id={props.id}>
|
||||
<path d={path} className="shadow" />
|
||||
<path d={path} className="link" />
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<g className={classes} onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave} id={id}>
|
||||
<path d={path} className="shadow" />
|
||||
<path d={path} className="link" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
ensureSameLength(points) {
|
||||
// Spring needs constant list length, hoping that dagre will insert never more than 30
|
||||
const length = 30;
|
||||
let missing = length - points.length;
|
||||
|
||||
while (missing > 0) {
|
||||
points.unshift(points[0]);
|
||||
missing = length - points.length;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
handleMouseEnter(ev) {
|
||||
enterEdge(ev.currentTarget.id);
|
||||
}
|
||||
@@ -105,3 +48,5 @@ export default class Edge extends React.Component {
|
||||
leaveEdge(ev.currentTarget.id);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Edge, PureRenderMixin);
|
||||
|
||||
27
client/app/scripts/charts/node-container.js
Normal file
27
client/app/scripts/charts/node-container.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import Node from './node';
|
||||
|
||||
export default function NodeContainer(props) {
|
||||
const { dx, dy, focused, layoutPrecision, zoomScale } = props;
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const scaleFactor = focused ? (2 / zoomScale) : 1;
|
||||
const other = _.omit(props, 'dx', 'dy');
|
||||
|
||||
return (
|
||||
<Motion style={{
|
||||
x: spring(dx, animConfig),
|
||||
y: spring(dy, animConfig),
|
||||
f: spring(scaleFactor, animConfig)
|
||||
}}>
|
||||
{interpolated => {
|
||||
const transform = `translate(${d3.round(interpolated.x, layoutPrecision)},`
|
||||
+ `${d3.round(interpolated.y, layoutPrecision)})`;
|
||||
return <Node {...other} transform={transform} scaleFactor={interpolated.f} />;
|
||||
}}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
|
||||
import { getNodeColor } from '../utils/color-utils';
|
||||
@@ -33,7 +35,18 @@ function getNodeShape({shape, stack}) {
|
||||
return stack ? stackedShape(nodeShape) : nodeShape;
|
||||
}
|
||||
|
||||
function ellipsis(text, fontSize, maxWidth) {
|
||||
const averageCharLength = fontSize / 1.5;
|
||||
const allowedChars = maxWidth / averageCharLength;
|
||||
let truncatedText = text;
|
||||
if (text && text.length > allowedChars) {
|
||||
truncatedText = `${text.slice(0, allowedChars)}...`;
|
||||
}
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
export default class Node extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||
@@ -42,95 +55,54 @@ export default class Node extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const nodeScale = props.focused ? props.selectedNodeScale : props.nodeScale;
|
||||
const zoomScale = this.props.zoomScale;
|
||||
let scaleFactor = 1;
|
||||
if (props.focused) {
|
||||
scaleFactor = 1.25 / zoomScale;
|
||||
} else if (props.blurred) {
|
||||
scaleFactor = 0.75;
|
||||
}
|
||||
const { blurred, focused, highlighted, label, nodeScale, pseudo, rank,
|
||||
subLabel, scaleFactor, transform, zoomScale } = this.props;
|
||||
|
||||
const color = getNodeColor(rank, label, pseudo);
|
||||
const labelText = ellipsis(label, 14, nodeScale(4 * scaleFactor));
|
||||
const subLabelText = ellipsis(subLabel, 12, nodeScale(4 * scaleFactor));
|
||||
|
||||
let labelOffsetY = 18;
|
||||
let subLabelOffsetY = 35;
|
||||
const color = getNodeColor(this.props.rank, this.props.label,
|
||||
this.props.pseudo);
|
||||
const onMouseEnter = this.handleMouseEnter;
|
||||
const onMouseLeave = this.handleMouseLeave;
|
||||
const onMouseClick = this.handleMouseClick;
|
||||
const classNames = ['node'];
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const label = this.ellipsis(props.label, 14, nodeScale(4 * scaleFactor));
|
||||
const subLabel = this.ellipsis(props.subLabel, 12, nodeScale(4 * scaleFactor));
|
||||
let labelFontSize = 14;
|
||||
let subLabelFontSize = 12;
|
||||
|
||||
if (props.focused) {
|
||||
// render focused nodes in normal size
|
||||
if (focused) {
|
||||
labelFontSize /= zoomScale;
|
||||
subLabelFontSize /= zoomScale;
|
||||
labelOffsetY /= zoomScale;
|
||||
subLabelOffsetY /= zoomScale;
|
||||
}
|
||||
if (this.props.highlighted) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
if (this.props.blurred) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
if (this.props.pseudo) {
|
||||
classNames.push('pseudo');
|
||||
}
|
||||
|
||||
const classes = classNames.join(' ');
|
||||
const className = classNames({
|
||||
node: true,
|
||||
highlighted,
|
||||
blurred,
|
||||
pseudo
|
||||
});
|
||||
|
||||
const NodeShapeType = getNodeShape(this.props);
|
||||
|
||||
return (
|
||||
<Motion style={{
|
||||
x: spring(this.props.dx, animConfig),
|
||||
y: spring(this.props.dy, animConfig),
|
||||
f: spring(scaleFactor, animConfig),
|
||||
labelFontSize: spring(labelFontSize, animConfig),
|
||||
subLabelFontSize: spring(subLabelFontSize, animConfig),
|
||||
labelOffsetY: spring(labelOffsetY, animConfig),
|
||||
subLabelOffsetY: spring(subLabelOffsetY, animConfig)
|
||||
}}>
|
||||
{(interpolated) => {
|
||||
const transform = `translate(${interpolated.x},${interpolated.y})`;
|
||||
return (
|
||||
<g className={classes} transform={transform} id={props.id}
|
||||
onClick={onMouseClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<NodeShapeType
|
||||
size={nodeScale(interpolated.f)}
|
||||
color={color}
|
||||
{...props} />
|
||||
<text className="node-label" textAnchor="middle"
|
||||
style={{fontSize: interpolated.labelFontSize}}
|
||||
x="0" y={interpolated.labelOffsetY + nodeScale(0.5 * interpolated.f)}>
|
||||
{label}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle"
|
||||
style={{fontSize: interpolated.subLabelFontSize}}
|
||||
x="0" y={interpolated.subLabelOffsetY + nodeScale(0.5 * interpolated.f)}>
|
||||
{subLabel}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<g className={className} transform={transform} onClick={this.handleMouseClick}
|
||||
onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<NodeShapeType
|
||||
size={nodeScale(scaleFactor)}
|
||||
color={color}
|
||||
{...this.props} />
|
||||
<text className="node-label" textAnchor="middle" style={{fontSize: labelFontSize}}
|
||||
x="0" y={labelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||
{labelText}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle" style={{fontSize: subLabelFontSize}}
|
||||
x="0" y={subLabelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||
{subLabelText}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
ellipsis(text, fontSize, maxWidth) {
|
||||
const averageCharLength = fontSize / 1.5;
|
||||
const allowedChars = maxWidth / averageCharLength;
|
||||
let truncatedText = text;
|
||||
if (text && text.length > allowedChars) {
|
||||
truncatedText = `${text.slice(0, allowedChars)}...`;
|
||||
}
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
handleMouseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
@@ -144,3 +116,5 @@ export default class Node extends React.Component {
|
||||
leaveNode(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Node, PureRenderMixin);
|
||||
|
||||
16
client/app/scripts/charts/nodes-chart-edges.js
Normal file
16
client/app/scripts/charts/nodes-chart-edges.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import EdgeContainer from './edge-container';
|
||||
|
||||
export default function NodesChartEdges({hasSelectedNode, highlightedEdgeIds,
|
||||
layoutEdges, layoutPrecision, selectedNodeId}) {
|
||||
return (
|
||||
<g className="nodes-chart-edges">
|
||||
{layoutEdges.toIndexedSeq().map(edge => <EdgeContainer key={edge.get('id')}
|
||||
id={edge.get('id')} source={edge.get('source')} target={edge.get('target')}
|
||||
points={edge.get('points')} layoutPrecision={layoutPrecision}
|
||||
highlightedEdgeIds={highlightedEdgeIds} hasSelectedNode={hasSelectedNode}
|
||||
selectedNodeId={selectedNodeId} />)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
23
client/app/scripts/charts/nodes-chart-elements.js
Normal file
23
client/app/scripts/charts/nodes-chart-elements.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import NodesChartEdges from './nodes-chart-edges';
|
||||
import NodesChartNodes from './nodes-chart-nodes';
|
||||
|
||||
export default function NodesChartElements(props) {
|
||||
return (
|
||||
<g className="nodes-chart-elements" transform={props.transform}>
|
||||
<NodesChartEdges layoutEdges={props.edges} selectedNodeId={props.selectedNodeId}
|
||||
highlightedEdgeIds={props.highlightedEdgeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
<NodesChartNodes layoutNodes={props.nodes} selectedNodeId={props.selectedNodeId}
|
||||
selectedMetric={props.selectedMetric}
|
||||
highlightedNodeIds={props.highlightedNodeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
adjacentNodes={props.adjacentNodes}
|
||||
nodeScale={props.nodeScale} onNodeClick={props.onNodeClick}
|
||||
scale={props.scale} selectedNodeScale={props.selectedNodeScale}
|
||||
topologyId={props.topologyId} layoutPrecision={props.layoutPrecision} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
68
client/app/scripts/charts/nodes-chart-nodes.js
Normal file
68
client/app/scripts/charts/nodes-chart-nodes.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
import NodeContainer from './node-container';
|
||||
|
||||
export default function NodesChartNodes({adjacentNodes, highlightedNodeIds,
|
||||
layoutNodes, layoutPrecision, nodeScale, onNodeClick, scale,
|
||||
selectedMetric, selectedNodeScale, selectedNodeId, topologyId}) {
|
||||
const zoomScale = scale;
|
||||
|
||||
// highlighter functions
|
||||
const setHighlighted = node => node.set('highlighted',
|
||||
highlightedNodeIds.has(node.get('id')) || selectedNodeId === node.get('id'));
|
||||
const setFocused = node => node.set('focused', selectedNodeId
|
||||
&& (selectedNodeId === node.get('id')
|
||||
|| (adjacentNodes && adjacentNodes.includes(node.get('id')))));
|
||||
const setBlurred = node => node.set('blurred', selectedNodeId && !node.get('focused'));
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
const sortNodes = node => {
|
||||
if (node.get('blurred')) {
|
||||
return 0;
|
||||
}
|
||||
if (node.get('highlighted')) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
// TODO: think about pulling this up into the store.
|
||||
const metric = node => (
|
||||
node.get('metrics') && node.get('metrics')
|
||||
.filter(m => m.get('id') === selectedMetric)
|
||||
.first()
|
||||
);
|
||||
|
||||
const nodesToRender = layoutNodes.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setFocused)
|
||||
.map(setBlurred)
|
||||
.sortBy(sortNodes);
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-nodes">
|
||||
{nodesToRender.map(node => <NodeContainer
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
topologyId={topologyId}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
onClick={onNodeClick}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
pseudo={node.get('pseudo')}
|
||||
nodeCount={node.get('nodeCount')}
|
||||
subLabel={node.get('subLabel')}
|
||||
metric={metric(node)}
|
||||
rank={node.get('rank')}
|
||||
layoutPrecision={layoutPrecision}
|
||||
selectedNodeScale={selectedNodeScale}
|
||||
nodeScale={nodeScale}
|
||||
zoomScale={zoomScale}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')} />)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,17 @@ import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { Map as makeMap, fromJS, is as isDeepEqual } from 'immutable';
|
||||
import timely from 'timely';
|
||||
|
||||
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
import Edge from './edge';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import Node from './node';
|
||||
import NodesError from './nodes-error';
|
||||
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||
import Logo from '../components/logo';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import NodesChartElements from './nodes-chart-elements';
|
||||
|
||||
const log = debug('scope:nodes-chart');
|
||||
|
||||
@@ -29,20 +28,22 @@ const radiusDensity = d3.scale.threshold()
|
||||
.domain([3, 6]).range([2.5, 3.5, 3]);
|
||||
|
||||
export default class NodesChart extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||
this.zoomed = this.zoomed.bind(this);
|
||||
|
||||
this.state = {
|
||||
nodes: makeMap(),
|
||||
edges: makeMap(),
|
||||
panTranslate: [0, 0],
|
||||
scale: 1,
|
||||
nodes: makeMap(),
|
||||
nodeScale: d3.scale.linear(),
|
||||
panTranslateX: 0,
|
||||
panTranslateY: 0,
|
||||
scale: 1,
|
||||
selectedNodeScale: d3.scale.linear(),
|
||||
hasZoomed: false,
|
||||
maxNodesExceeded: false
|
||||
hasZoomed: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,18 +52,6 @@ export default class NodesChart extends React.Component {
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// distinguish pan/zoom from click
|
||||
this.isZooming = false;
|
||||
|
||||
this.zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 2])
|
||||
.on('zoom', this.zoomed);
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.call(this.zoom);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// gather state, setState should be called only once here
|
||||
const state = _.assign({}, this.state);
|
||||
@@ -91,9 +80,20 @@ export default class NodesChart extends React.Component {
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// distinguish pan/zoom from click
|
||||
this.isZooming = false;
|
||||
|
||||
this.zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 2])
|
||||
.on('zoom', this.zoomed);
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.call(this.zoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// undoing .call(zoom)
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.on('mousedown.zoom', null)
|
||||
.on('onwheel', null)
|
||||
@@ -102,170 +102,74 @@ export default class NodesChart extends React.Component {
|
||||
.on('touchstart.zoom', null);
|
||||
}
|
||||
|
||||
renderGraphNodes(nodes, nodeScale) {
|
||||
const hasSelectedNode = this.props.selectedNodeId
|
||||
&& this.props.nodes.has(this.props.selectedNodeId);
|
||||
const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null;
|
||||
const onNodeClick = this.props.onNodeClick;
|
||||
const zoomScale = this.state.scale;
|
||||
const selectedNodeScale = this.state.selectedNodeScale;
|
||||
|
||||
// highlighter functions
|
||||
const setHighlighted = node => {
|
||||
const highlighted = this.props.highlightedNodeIds.has(node.get('id'))
|
||||
|| this.props.selectedNodeId === node.get('id');
|
||||
return node.set('highlighted', highlighted);
|
||||
};
|
||||
const setFocused = node => {
|
||||
const focused = hasSelectedNode
|
||||
&& (this.props.selectedNodeId === node.get('id') || adjacency.includes(node.get('id')));
|
||||
return node.set('focused', focused);
|
||||
};
|
||||
const setBlurred = node => node.set('blurred', hasSelectedNode && !node.get('focused'));
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
const sortNodes = node => {
|
||||
if (node.get('blurred')) {
|
||||
return 0;
|
||||
}
|
||||
if (node.get('highlighted')) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
// TODO: think about pulling this up into the store.
|
||||
const metric = node => (
|
||||
node.get('metrics') && node.get('metrics')
|
||||
.filter(m => m.get('id') === this.props.selectedMetric)
|
||||
.first()
|
||||
);
|
||||
|
||||
return nodes
|
||||
.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setFocused)
|
||||
.map(setBlurred)
|
||||
.sortBy(sortNodes)
|
||||
.map(node => <Node
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
topologyId={this.props.topologyId}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
onClick={onNodeClick}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
pseudo={node.get('pseudo')}
|
||||
nodeCount={node.get('nodeCount')}
|
||||
subLabel={node.get('subLabel')}
|
||||
metric={metric(node)}
|
||||
rank={node.get('rank')}
|
||||
selectedNodeScale={selectedNodeScale}
|
||||
nodeScale={nodeScale}
|
||||
zoomScale={zoomScale}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')}
|
||||
/>);
|
||||
}
|
||||
|
||||
renderGraphEdges(edges) {
|
||||
const selectedNodeId = this.props.selectedNodeId;
|
||||
const hasSelectedNode = selectedNodeId && this.props.nodes.has(selectedNodeId);
|
||||
|
||||
const setHighlighted = edge => edge.set('highlighted', this.props.highlightedEdgeIds.has(
|
||||
edge.get('id')));
|
||||
|
||||
const setBlurred = edge => edge.set('blurred', hasSelectedNode
|
||||
&& edge.get('source') !== selectedNodeId
|
||||
&& edge.get('target') !== selectedNodeId);
|
||||
|
||||
return edges
|
||||
.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setBlurred)
|
||||
.map(edge => <Edge key={edge.get('id')} id={edge.get('id')}
|
||||
points={edge.get('points')}
|
||||
blurred={edge.get('blurred')} highlighted={edge.get('highlighted')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMaxNodesError(show) {
|
||||
const errorHint = 'We\u0027re working on it, but for now, try a different view?';
|
||||
return (
|
||||
<NodesError faIconClass="fa-ban" hidden={!show}>
|
||||
<div className="centered">Too many nodes to show in the browser.<br />{errorHint}</div>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmptyTopologyError(show) {
|
||||
return (
|
||||
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
|
||||
<div className="heading">Nothing to show. This can have any of these reasons:</div>
|
||||
<ul>
|
||||
<li>We haven't received any reports from probes recently.
|
||||
Are the probes properly configured?</li>
|
||||
<li>There are nodes, but they're currently hidden. Check the view options
|
||||
in the bottom-left if they allow for showing hidden nodes.</li>
|
||||
<li>Containers view only: you're not running Docker,
|
||||
or you don't have any containers.</li>
|
||||
</ul>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
|
||||
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
|
||||
const scale = this.state.scale;
|
||||
const { edges, nodes, panTranslateX, panTranslateY, scale } = this.state;
|
||||
|
||||
const translate = this.state.panTranslate;
|
||||
// not passing translates into child components for perf reasons, use getTranslate instead
|
||||
const translate = [panTranslateX, panTranslateY];
|
||||
const transform = `translate(${translate}) scale(${scale})`;
|
||||
const svgClassNames = this.state.maxNodesExceeded || nodeElements.size === 0 ? 'hide' : '';
|
||||
const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty());
|
||||
const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded);
|
||||
const svgClassNames = this.props.isEmpty ? 'hide' : '';
|
||||
|
||||
return (
|
||||
<div className="nodes-chart">
|
||||
{errorEmpty}
|
||||
{errorMaxNodesExceeded}
|
||||
<svg width="100%" height="100%" id="nodes-chart-canvas"
|
||||
className={svgClassNames} onClick={this.handleMouseClick}>
|
||||
<g transform="translate(24,24) scale(0.25)">
|
||||
<Logo />
|
||||
</g>
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
</g>
|
||||
<g className="nodes">
|
||||
{nodeElements}
|
||||
</g>
|
||||
</g>
|
||||
<NodesChartElements
|
||||
edges={edges}
|
||||
nodes={nodes}
|
||||
transform={transform}
|
||||
adjacentNodes={this.props.adjacentNodes}
|
||||
layoutPrecision={this.props.layoutPrecision}
|
||||
selectedMetric={this.props.selectedMetric}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
highlightedEdgeIds={this.props.highlightedEdgeIds}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
hasSelectedNode={this.props.hasSelectedNode}
|
||||
nodeScale={this.state.nodeScale}
|
||||
scale={this.state.scale}
|
||||
selectedNodeScale={this.state.selectedNodeScale}
|
||||
topologyId={this.props.topologyId} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
initNodes(topology) {
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
}
|
||||
|
||||
initNodes(topology, stateNodes) {
|
||||
let nextStateNodes = stateNodes;
|
||||
|
||||
// remove nodes that have dissappeared
|
||||
stateNodes.forEach((node, id) => {
|
||||
if (!topology.has(id)) {
|
||||
nextStateNodes = nextStateNodes.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
// copy relevant fields to state nodes
|
||||
return topology.map((node, id) => makeMap({
|
||||
id,
|
||||
label: node.get('label'),
|
||||
pseudo: node.get('pseudo'),
|
||||
metrics: node.get('metrics'),
|
||||
subLabel: node.get('label_minor'),
|
||||
nodeCount: node.get('node_count'),
|
||||
rank: node.get('rank'),
|
||||
shape: node.get('shape'),
|
||||
stack: node.get('stack'),
|
||||
x: 0,
|
||||
y: 0
|
||||
}));
|
||||
topology.forEach((node, id) => {
|
||||
nextStateNodes = nextStateNodes.mergeIn([id], makeMap({
|
||||
id,
|
||||
label: node.get('label'),
|
||||
pseudo: node.get('pseudo'),
|
||||
subLabel: node.get('label_minor'),
|
||||
nodeCount: node.get('node_count'),
|
||||
rank: node.get('rank'),
|
||||
shape: node.get('shape'),
|
||||
stack: node.get('stack')
|
||||
}));
|
||||
});
|
||||
|
||||
return nextStateNodes;
|
||||
}
|
||||
|
||||
initEdges(topology, stateNodes) {
|
||||
@@ -309,10 +213,10 @@ export default class NodesChart extends React.Component {
|
||||
return {};
|
||||
}
|
||||
|
||||
const adjacency = AppStore.getAdjacentNodes(props.selectedNodeId);
|
||||
const adjacentNodes = props.adjacentNodes;
|
||||
const adjacentLayoutNodeIds = [];
|
||||
|
||||
adjacency.forEach(adjacentId => {
|
||||
adjacentNodes.forEach(adjacentId => {
|
||||
// filter loopback
|
||||
if (adjacentId !== props.selectedNodeId) {
|
||||
adjacentLayoutNodeIds.push(adjacentId);
|
||||
@@ -321,7 +225,7 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
// move origin node to center of viewport
|
||||
const zoomScale = state.scale;
|
||||
const translate = state.panTranslate;
|
||||
const translate = [state.panTranslateX, state.panTranslateY];
|
||||
const centerX = (-translate[0] + (props.width + MARGINS.left
|
||||
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
|
||||
const centerY = (-translate[1] + (props.height + MARGINS.top) / 2) / zoomScale;
|
||||
@@ -356,10 +260,10 @@ export default class NodesChart extends React.Component {
|
||||
|| _.includes(adjacentLayoutNodeIds, edge.get('target'))) {
|
||||
const source = stateNodes.get(edge.get('source'));
|
||||
const target = stateNodes.get(edge.get('target'));
|
||||
return edge.set('points', [
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]);
|
||||
]));
|
||||
}
|
||||
return edge;
|
||||
});
|
||||
@@ -374,18 +278,10 @@ export default class NodesChart extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
}
|
||||
|
||||
restoreLayout(state) {
|
||||
// undo any pan/zooming that might have happened
|
||||
this.zoom.scale(state.scale);
|
||||
this.zoom.translate(state.panTranslate);
|
||||
this.zoom.translate([state.panTranslateX, state.panTranslateY]);
|
||||
|
||||
const nodes = state.nodes.map(node => node.merge({
|
||||
x: node.get('px'),
|
||||
@@ -399,7 +295,7 @@ export default class NodesChart extends React.Component {
|
||||
return edge;
|
||||
});
|
||||
|
||||
return { edges, nodes};
|
||||
return { edges, nodes };
|
||||
}
|
||||
|
||||
updateGraphState(props, state) {
|
||||
@@ -412,9 +308,10 @@ export default class NodesChart extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
let stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||
let stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||
const stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||
const stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||
const nodeScale = this.getNodeScale(props);
|
||||
const nextState = { nodeScale };
|
||||
|
||||
const options = {
|
||||
width: props.width,
|
||||
@@ -431,21 +328,17 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
log(`graph layout took ${timedLayouter.time}ms`);
|
||||
|
||||
// layout was aborted
|
||||
if (!graph) {
|
||||
return {maxNodesExceeded: true};
|
||||
}
|
||||
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => makeMap({
|
||||
metrics: node.get('metrics')
|
||||
})));
|
||||
stateEdges = graph.edges;
|
||||
|
||||
// save coordinates for restore
|
||||
stateNodes = stateNodes.map(node => node.merge({
|
||||
px: node.get('x'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
stateEdges = stateEdges.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
// inject metrics and save coordinates for restore
|
||||
const layoutNodes = graph.nodes
|
||||
.mergeDeep(stateNodes.map(node => makeMap({
|
||||
metrics: node.get('metrics')
|
||||
})))
|
||||
.map(node => node.merge({
|
||||
px: node.get('x'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
const layoutEdges = graph.edges
|
||||
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
@@ -453,19 +346,21 @@ export default class NodesChart extends React.Component {
|
||||
const zoomFactor = Math.min(xFactor, yFactor);
|
||||
let zoomScale = this.state.scale;
|
||||
|
||||
if (this.zoom && !this.state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
if (!this.props.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
// saving in d3's behavior cache
|
||||
this.zoom.scale(zoomFactor);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: stateNodes,
|
||||
edges: stateEdges,
|
||||
scale: zoomScale,
|
||||
nodeScale,
|
||||
maxNodesExceeded: false
|
||||
};
|
||||
nextState.scale = zoomScale;
|
||||
if (!isDeepEqual(stateNodes, state.nodes)) {
|
||||
nextState.nodes = layoutNodes;
|
||||
}
|
||||
if (!isDeepEqual(stateEdges, state.edges)) {
|
||||
nextState.edges = layoutEdges;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
getNodeScale(props) {
|
||||
@@ -483,9 +378,12 @@ export default class NodesChart extends React.Component {
|
||||
if (!this.props.selectedNodeId) {
|
||||
this.setState({
|
||||
hasZoomed: true,
|
||||
panTranslate: d3.event.translate.slice(),
|
||||
panTranslateX: d3.event.translate[0],
|
||||
panTranslateY: d3.event.translate[1],
|
||||
scale: d3.event.scale
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChart, PureRenderMixin);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import dagre from 'dagre';
|
||||
import debug from 'debug';
|
||||
import { Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { updateNodeDegrees } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:nodes-layout');
|
||||
|
||||
const MAX_NODES = 100;
|
||||
const topologyCaches = {};
|
||||
const DEFAULT_WIDTH = 800;
|
||||
const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||
@@ -51,11 +50,6 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
||||
let nodes = imNodes;
|
||||
let edges = imEdges;
|
||||
|
||||
if (nodes.size > MAX_NODES) {
|
||||
log(`Too many nodes for graph layout engine. Limit: ${MAX_NODES}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = opts || {};
|
||||
const scale = options.scale || DEFAULT_SCALE;
|
||||
const ranksep = scale(RANK_SEPARATION_FACTOR);
|
||||
@@ -122,13 +116,13 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
||||
graph.edges().forEach(graphEdge => {
|
||||
const graphEdgeMeta = graph.edge(graphEdge);
|
||||
const edge = edges.get(graphEdgeMeta.id);
|
||||
const points = graphEdgeMeta.points;
|
||||
let points = fromJS(graphEdgeMeta.points);
|
||||
|
||||
// set beginning and end points to node coordinates to ignore node bounding box
|
||||
const source = nodes.get(fromGraphNodeId(edge.get('source')));
|
||||
const target = nodes.get(fromGraphNodeId(edge.get('target')));
|
||||
points[0] = {x: source.get('x'), y: source.get('y')};
|
||||
points[points.length - 1] = {x: target.get('x'), y: target.get('y')};
|
||||
points = points.mergeIn([0], {x: source.get('x'), y: source.get('y')});
|
||||
points = points.mergeIn([points.size - 1], {x: target.get('x'), y: target.get('y')});
|
||||
|
||||
edges = edges.setIn([graphEdgeMeta.id, 'points'], points);
|
||||
});
|
||||
@@ -251,13 +245,12 @@ function shiftLayoutToCenter(layout, opts) {
|
||||
y: node.get('y') + offsetY
|
||||
}));
|
||||
|
||||
result.edges = layout.edges.map(edge => {
|
||||
const points = edge.get('points').map(point => ({
|
||||
x: point.x + offsetX,
|
||||
y: point.y + offsetY
|
||||
}));
|
||||
return edge.set('points', points);
|
||||
});
|
||||
result.edges = layout.edges.map(edge => edge.update('points',
|
||||
points => points.map(point => point.merge({
|
||||
x: point.get('x') + offsetX,
|
||||
y: point.get('y') + offsetY
|
||||
}))
|
||||
));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -271,10 +264,10 @@ function shiftLayoutToCenter(layout, opts) {
|
||||
function setSimpleEdgePoints(edge, nodeCache) {
|
||||
const source = nodeCache.get(edge.get('source'));
|
||||
const target = nodeCache.get(edge.get('target'));
|
||||
return edge.set('points', [
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,14 +293,14 @@ export function hasUnseenNodes(nodes, cache) {
|
||||
*/
|
||||
function hasSameEndpoints(cachedEdge, nodes) {
|
||||
const oldPoints = cachedEdge.get('points');
|
||||
const oldSourcePoint = oldPoints[0];
|
||||
const oldTargetPoint = oldPoints[oldPoints.length - 1];
|
||||
const oldSourcePoint = oldPoints.first();
|
||||
const oldTargetPoint = oldPoints.last();
|
||||
const newSource = nodes.get(cachedEdge.get('source'));
|
||||
const newTarget = nodes.get(cachedEdge.get('target'));
|
||||
return (oldSourcePoint.x === newSource.get('x')
|
||||
&& oldSourcePoint.y === newSource.get('y')
|
||||
&& oldTargetPoint.x === newTarget.get('x')
|
||||
&& oldTargetPoint.y === newTarget.get('y'));
|
||||
return (oldSourcePoint.get('x') === newSource.get('x')
|
||||
&& oldSourcePoint.get('y') === newSource.get('y')
|
||||
&& oldTargetPoint.get('x') === newTarget.get('x')
|
||||
&& oldTargetPoint.get('y') === newTarget.get('y'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import Logo from './logo';
|
||||
import AppStore from '../stores/app-store';
|
||||
@@ -26,9 +28,11 @@ const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C';
|
||||
const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E';
|
||||
const keyPressLog = debug('scope:app-key-press');
|
||||
|
||||
/* make sure these can all be shallow-checked for equality for PureRenderMixin */
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||
adjacentNodes: AppStore.getAdjacentNodes(AppStore.getSelectedNodeId()),
|
||||
controlStatus: AppStore.getControlStatus(),
|
||||
controlPipe: AppStore.getControlPipe(),
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
@@ -47,6 +51,7 @@ function getStateFromStores() {
|
||||
selectedMetric: AppStore.getSelectedMetric(),
|
||||
topologies: AppStore.getTopologies(),
|
||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||
topologyEmpty: AppStore.isTopologyEmpty(),
|
||||
updatePaused: AppStore.isUpdatePaused(),
|
||||
updatePausedAt: AppStore.getUpdatePausedAt(),
|
||||
version: AppStore.getVersion(),
|
||||
@@ -136,6 +141,8 @@ export default class App extends React.Component {
|
||||
selectedMetric={this.state.selectedMetric}
|
||||
forceRelayout={this.state.forceRelayout}
|
||||
topologyOptions={this.state.activeTopologyOptions}
|
||||
topologyEmpty={this.state.topologyEmpty}
|
||||
adjacentNodes={this.state.adjacentNodes}
|
||||
topologyId={this.state.currentTopologyId} />
|
||||
|
||||
<Sidebar>
|
||||
@@ -157,3 +164,5 @@ export default class App extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(App, PureRenderMixin);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint react/jsx-no-bind: "off" */
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import Perf from 'react-addons-perf';
|
||||
|
||||
import debug from 'debug';
|
||||
const log = debug('scope:debug-panel');
|
||||
@@ -91,6 +92,19 @@ function addAllMetricVariants() {
|
||||
}
|
||||
|
||||
|
||||
function stopPerf() {
|
||||
Perf.stop();
|
||||
const measurements = Perf.getLastMeasurements();
|
||||
Perf.printInclusive(measurements);
|
||||
Perf.printWasted(measurements);
|
||||
}
|
||||
|
||||
function startPerf(delay) {
|
||||
Perf.start();
|
||||
setTimeout(stopPerf, delay * 1000);
|
||||
}
|
||||
|
||||
|
||||
function addNodes(n) {
|
||||
const ns = AppStore.getNodes();
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
@@ -110,9 +124,9 @@ function addNodes(n) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function showingDebugToolbar() {
|
||||
return 'debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar);
|
||||
return (('debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar))
|
||||
|| location.pathname.indexOf('debug') > -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +210,13 @@ export class DebugToolbar extends React.Component {
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<label>Measure React perf for </label>
|
||||
<button onClick={() => startPerf(2)}>2s</button>
|
||||
<button onClick={() => startPerf(5)}>5s</button>
|
||||
<button onClick={() => startPerf(10)}>10s</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodeDetails from './node-details';
|
||||
import { DETAILS_PANEL_WIDTH as WIDTH, DETAILS_PANEL_OFFSET as OFFSET,
|
||||
@@ -51,3 +53,5 @@ export default class DetailsCard extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(DetailsCard, PureRenderMixin);
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Details({controlStatus, details, nodes}) {
|
||||
<div className="details">
|
||||
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
||||
index={index} cardCount={details.size} nodes={nodes}
|
||||
nodeControlStatus={controlStatus[obj.id]} {...obj} />
|
||||
nodeControlStatus={controlStatus.get(obj.id)} {...obj} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -135,7 +135,8 @@ export default class NodeDetails extends React.Component {
|
||||
const details = this.props.details;
|
||||
const showControls = details.controls && details.controls.length > 0;
|
||||
const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo);
|
||||
const {error, pending} = (this.props.nodeControlStatus || {});
|
||||
const {error, pending} = this.props.nodeControlStatus
|
||||
? this.props.nodeControlStatus.toJS() : {};
|
||||
const tools = this.renderTools();
|
||||
const styles = {
|
||||
controls: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
@@ -25,3 +27,5 @@ export default class NodeDetailsRelativesLink extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsRelativesLink, PureRenderMixin);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
@@ -32,3 +34,5 @@ export default class NodeDetailsTableNodeLink extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsTableNodeLink, PureRenderMixin);
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodesChart from '../charts/nodes-chart';
|
||||
import NodesError from '../charts/nodes-error';
|
||||
|
||||
const navbarHeight = 160;
|
||||
const marginTop = 0;
|
||||
|
||||
/**
|
||||
* dynamic coords precision based on topology size
|
||||
*/
|
||||
function getLayoutPrecision(nodesCount) {
|
||||
let precision;
|
||||
if (nodesCount >= 50) {
|
||||
precision = 0;
|
||||
} else if (nodesCount > 20) {
|
||||
precision = 1;
|
||||
} else if (nodesCount > 10) {
|
||||
precision = 2;
|
||||
} else {
|
||||
precision = 3;
|
||||
}
|
||||
|
||||
return precision;
|
||||
}
|
||||
|
||||
export default class Nodes extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -24,8 +45,37 @@ export default class Nodes extends React.Component {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
renderEmptyTopologyError(show) {
|
||||
return (
|
||||
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
|
||||
<div className="heading">Nothing to show. This can have any of these reasons:</div>
|
||||
<ul>
|
||||
<li>We haven't received any reports from probes recently.
|
||||
Are the probes properly configured?</li>
|
||||
<li>There are nodes, but they're currently hidden. Check the view options
|
||||
in the bottom-left if they allow for showing hidden nodes.</li>
|
||||
<li>Containers view only: you're not running Docker,
|
||||
or you don't have any containers.</li>
|
||||
</ul>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <NodesChart {...this.props} {...this.state} />;
|
||||
const { nodes, selectedNodeId, topologyEmpty } = this.props;
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
const hasSelectedNode = selectedNodeId && nodes.has(selectedNodeId);
|
||||
const errorEmpty = this.renderEmptyTopologyError(topologyEmpty);
|
||||
|
||||
return (
|
||||
<div className="nodes-wrapper">
|
||||
{topologyEmpty && errorEmpty}
|
||||
<NodesChart {...this.props} {...this.state}
|
||||
layoutPrecision={layoutPrecision}
|
||||
hasSelectedNode={hasSelectedNode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
@@ -39,3 +89,5 @@ export default class Nodes extends React.Component {
|
||||
this.setState({height, width});
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Nodes, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
export default class ShowMore extends React.Component {
|
||||
|
||||
@@ -28,3 +30,5 @@ export default class ShowMore extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(ShowMore, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickTopology } from '../actions/app-actions';
|
||||
|
||||
@@ -69,3 +71,5 @@ export default class Topologies extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Topologies, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { changeTopologyOption } from '../actions/app-actions';
|
||||
|
||||
@@ -26,3 +28,5 @@ export default class TopologyOptionAction extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptionAction, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import TopologyOptionAction from './topology-option-action';
|
||||
|
||||
@@ -29,3 +31,5 @@ export default class TopologyOptions extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptions, PureRenderMixin);
|
||||
|
||||
@@ -142,9 +142,10 @@ export class AppStore extends Store {
|
||||
|
||||
// keep at the top
|
||||
getAppState() {
|
||||
const cp = this.getControlPipe();
|
||||
return {
|
||||
controlPipe: this.getControlPipe(),
|
||||
nodeDetails: this.getNodeDetailsState(),
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: this.getNodeDetailsState().toJS(),
|
||||
selectedNodeId,
|
||||
pinnedMetricType,
|
||||
topologyId: currentTopologyId,
|
||||
@@ -190,12 +191,11 @@ export class AppStore extends Store {
|
||||
}
|
||||
|
||||
getControlStatus() {
|
||||
return controlStatus.toJS();
|
||||
return controlStatus;
|
||||
}
|
||||
|
||||
getControlPipe() {
|
||||
const cp = controlPipes.last();
|
||||
return cp && cp.toJS();
|
||||
return controlPipes.last();
|
||||
}
|
||||
|
||||
getCurrentTopology() {
|
||||
@@ -240,7 +240,7 @@ export class AppStore extends Store {
|
||||
getNodeDetailsState() {
|
||||
return nodeDetails.toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
})).toJS();
|
||||
}));
|
||||
}
|
||||
|
||||
getTopCardNodeId() {
|
||||
|
||||
@@ -333,7 +333,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.nodes > .node {
|
||||
.nodes-chart-nodes > .node {
|
||||
cursor: pointer;
|
||||
transition: opacity .5s @base-ease;
|
||||
|
||||
@@ -370,6 +370,10 @@ h2 {
|
||||
opacity: @edge-opacity-blurred;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
animation: focusing 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: @edge-link-stroke-width;
|
||||
@@ -1007,7 +1011,7 @@ h2 {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.fa {
|
||||
margin-left: 4px;
|
||||
color: darkred;
|
||||
@@ -1058,6 +1062,16 @@ h2 {
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
@keyframes focusing {
|
||||
0% {
|
||||
opacity: 0;
|
||||
} 33% {
|
||||
opacity: 0.2;
|
||||
} 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinking {
|
||||
0%, 100% {
|
||||
opacity: 1.0;
|
||||
|
||||
20
client/build/debug.html
Normal file
20
client/build/debug.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Weave Scope</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 10]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<div class="wrap">
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
|
||||
<script src="vendors.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,6 +25,7 @@
|
||||
"react-addons-update": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-motion": "0.3.1",
|
||||
"react-mixin": "^3.0.3",
|
||||
"reqwest": "~2.0.5",
|
||||
"timely": "0.1.0"
|
||||
},
|
||||
@@ -50,14 +51,17 @@
|
||||
"less": "~2.6.1",
|
||||
"less-loader": "2.2.2",
|
||||
"postcss-loader": "0.8.2",
|
||||
"react-addons-perf": "^0.14.0",
|
||||
"style-loader": "0.13.0",
|
||||
"url": "0.11.0",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "~1.12.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"browser-perf": "^1.4.5",
|
||||
"express": "~4.13.3",
|
||||
"http-proxy": "^1.12.0",
|
||||
"perfjankie": "^2.1.0",
|
||||
"react-hot-loader": "~1.3.0",
|
||||
"webpack-dev-server": "~1.14.1"
|
||||
},
|
||||
|
||||
118
client/test/actions/90-nodes-select.js
Normal file
118
client/test/actions/90-nodes-select.js
Normal file
@@ -0,0 +1,118 @@
|
||||
var fs = require('fs');
|
||||
var debug = require('debug')('scope:test:action:90-nodes-select');
|
||||
|
||||
function clickIfVisible(list, index) {
|
||||
var el = list[index++];
|
||||
el.isDisplayed(function(err, visible) {
|
||||
if (err) {
|
||||
debug(err);
|
||||
} else if (visible) {
|
||||
el.click();
|
||||
} else {
|
||||
if (index < list.length) {
|
||||
clickIfVisible(list, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(cfg) {
|
||||
|
||||
var startUrl = 'http://' + cfg.host + '/debug.html';
|
||||
var selectedUrl = 'http://' + cfg.host + '/debug.html#!/state/{"nodeDetails":[{"id":"zing11","label":"zing11","topologyId":"containers"}],"selectedNodeId":"zing11","topologyId":"containers","topologyOptions":{"processes":{"unconnected":"hide"},"processes-by-name":{"unconnected":"hide"},"containers":{"system":"hide","stopped":"hide"},"containers-by-hostname":{"system":"hide","stopped":"hide"},"containers-by-image":{"system":"hide","stopped":"hide"}}}';
|
||||
|
||||
// cfg - The configuration object. args, from the example above.
|
||||
return function(browser) {
|
||||
// browser is created using wd.promiseRemote()
|
||||
// More info about wd at https://github.com/admc/wd
|
||||
return browser.get('http://' + cfg.host + '/debug.html')
|
||||
.then(function() {
|
||||
debug('starting run ' + cfg.run);
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.elementByCssSelector('.debug-panel div:nth-child(2) button:nth-child(9)');
|
||||
})
|
||||
.then(function(el) {
|
||||
debug('debug-panel found');
|
||||
return el.click(function() {
|
||||
el.click(function() {
|
||||
el.click();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000, function() {
|
||||
debug('scenario done');
|
||||
});
|
||||
})
|
||||
.fail(function(err) {
|
||||
debug('exception. taking screenshot', err);
|
||||
browser.takeScreenshot(function(err, data) {
|
||||
if (err) {
|
||||
debug(err);
|
||||
} else {
|
||||
var base64Data = data.replace(/^data:image\/png;base64,/,"");
|
||||
fs.writeFile('90-nodes-select-' + cfg.run + '.png', base64Data, 'base64', function(err) {
|
||||
if(err) debug(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
9
client/test/browser-perf/main.js
Normal file
9
client/test/browser-perf/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var browserPerf = require('browser-perf');
|
||||
var options = {
|
||||
selenium: 'http://local.docker:4444/wd/hub',
|
||||
actions: [require('./custom-action.js')()]
|
||||
}
|
||||
browserPerf('http://local.docker:4040/debug.html', function(err, res){
|
||||
console.error(err);
|
||||
console.log(res);
|
||||
}, options);
|
||||
55
client/test/perfjankie/main.js
Normal file
55
client/test/perfjankie/main.js
Normal file
@@ -0,0 +1,55 @@
|
||||
var perfjankie = require('perfjankie');
|
||||
|
||||
var run = process.env.COMMIT || 'commit#Hash'; // A hash for the commit, displayed in the x-axis in the dashboard
|
||||
var time = process.env.DATE || new Date().getTime() // Used to sort the data when displaying graph. Can be the time when a commit was made
|
||||
var scenario = process.env.ACTIONS || '90-nodes-select';
|
||||
var host = process.env.HOST || 'localhost:4040';
|
||||
var actions = require('../actions/' + scenario)({host: host, run: run});
|
||||
|
||||
perfjankie({
|
||||
/* The next set of values identify the test */
|
||||
suite: 'Scope',
|
||||
name: scenario, // A friendly name for the URL. This is shown as component name in the dashboard
|
||||
time: time,
|
||||
run: run,
|
||||
repeat: 10, // Run the tests 10 times. Default is 1 time
|
||||
|
||||
/* Identifies where the data and the dashboard are saved */
|
||||
couch: {
|
||||
server: 'http://local.docker:5984',
|
||||
database: 'performance'
|
||||
// updateSite: !process.env.CI, // If true, updates the couchApp that shows the dashboard. Set to false in when running Continuous integration, run this the first time using command line.
|
||||
// onlyUpdateSite: false // No data to upload, just update the site. Recommended to do from dev box as couchDB instance may require special access to create views.
|
||||
},
|
||||
|
||||
callback: function(err, res) {
|
||||
if (err)
|
||||
console.log(err);
|
||||
// The callback function, err is falsy if all of the following happen
|
||||
// 1. Browsers perf tests ran
|
||||
// 2. Data has been saved in couchDB
|
||||
// err is not falsy even if update site fails.
|
||||
},
|
||||
|
||||
/* OPTIONS PASSED TO BROWSER-PERF */
|
||||
// Properties identifying the test environment */
|
||||
browsers: [{ // This can also be a ['chrome', 'firefox'] or 'chrome,firefox'
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
perfLoggingPrefs: {
|
||||
'traceCategories': 'toplevel,disabled-by-default-devtools.timeline.frame,blink.console,disabled-by-default-devtools.timeline,benchmark'
|
||||
},
|
||||
args: ['--enable-gpu-benchmarking', '--enable-thread-composting']
|
||||
},
|
||||
loggingPrefs: {
|
||||
performance: 'ALL'
|
||||
}
|
||||
}], // See browser perf browser configuration for all options.
|
||||
actions: actions,
|
||||
|
||||
selenium: {
|
||||
hostname: 'local.docker', // or localhost or hub.browserstack.com
|
||||
port: 4444,
|
||||
}
|
||||
|
||||
});
|
||||
16
client/test/run-jankie.sh
Executable file
16
client/test/run-jankie.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# run jankie on one commit
|
||||
|
||||
set -x
|
||||
|
||||
HOST=$1
|
||||
COMMIT=$2
|
||||
DATE=$3
|
||||
|
||||
git checkout $COMMIT
|
||||
make SUDO= -C ../..
|
||||
../../scope stop && ../../scope launch
|
||||
|
||||
sleep 5
|
||||
|
||||
COMMIT=$COMMIT DATE=$DATE HOST=$HOST DEBUG=scope* node ./perfjankie/main.js
|
||||
Reference in New Issue
Block a user