Merge pull request #91 from yldio/leak

spike a memory leak using our graphs
This commit is contained in:
Tom Gallacher 2016-11-28 10:15:02 +00:00 committed by GitHub
commit bd63ca3c56
69 changed files with 15957 additions and 109 deletions

View File

@ -0,0 +1,15 @@
{
"presets": [
"react",
"es2015"
],
"plugins": [
["transform-object-rest-spread", {
"useBuiltIns": true
}],
"add-module-exports",
"transform-es2015-modules-commonjs",
"react-hot-loader/babel"
],
"sourceMaps": "both"
}

View File

@ -0,0 +1,3 @@
/node_modules
coverage
.nyc_output

View File

@ -0,0 +1,29 @@
{
"extends": "semistandard",
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"babel",
"react"
],
"rules": {
"generator-star-spacing": 0,
"babel/generator-star-spacing": 1,
"space-before-function-paren": [2, "never"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"object-curly-newline": ["error", {
"minProperties": 1
}],
"sort-vars": ["error", {
"ignoreCase": true
}]
}
}

View File

@ -0,0 +1,4 @@
/node_modules
coverage
.nyc_output
npm-debug.log

View File

@ -0,0 +1,82 @@
const take = require('lodash.take');
const actions = {
'UPDATE_STATS': (state, action) => {
const data = (state[action.subscription] || {
cpu: [],
mem: [],
disk: []
});
const newData = ['cpu', 'mem', 'disk'].reduce((sum, key) => {
const item = {
...action.payload.stats[key],
when: action.payload.when
};
const prepended = [item].concat(data[key]);
return {
...sum,
[key]: take(prepended, state.windowSize)
};
}, {});
return {
...state,
[action.subscription]: newData
};
}
};
module.exports = (state, action) => {
return !actions[action.type] ? state : actions[action.type](state, action);
};
module.exports.subscribe = (id) => (dispatch, getState) => {
const {
ws
} = getState();
const p = new Promise((resolve, reject) => {
ws.subscribe(`/stats/${id}`, (update, flag) => {
dispatch({
type: 'UPDATE_STATS',
payload: update,
subscription: id
});
}, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return dispatch({
type: 'SUBSCRIBE',
payload: p
});
};
module.exports.unsubscribe = (id) => (dispatch, getState) => {
const {
ws
} = getState();
const p = new Promise((resolve, reject) => {
ws.unsubscribe(`/stats/${id}`, null, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return dispatch({
type: 'UNSUBSCRIBE',
payload: p
});
};

View File

@ -0,0 +1,76 @@
const buildArray = require('build-array');
const Chart = require('chart.js');
const React = require('react');
const whisker = require('../whisker');
whisker(Chart);
module.exports = React.createClass({
ref: function(name) {
this._refs = this._refs || {};
return (el) => {
this._refs[name] = el;
};
},
componentDidMount: function() {
const {
datasets = [],
labels = 0,
stacked = false,
xAxe = false,
yAxe = false,
legend = false,
max = 100,
min = 0
} = this.props;
const _labels = !Array.isArray(labels)
? buildArray(labels).map((v, i) => '')
: labels;
this._chart = new Chart(this._refs.component, {
type: 'whisker',
responsive: true,
options: {
scales: {
xAxes: [{
barPercentage: 1.0,
categoryPercentage: 1.0
}],
yAxes: [{
ticks: {
min: min,
max: max
}
}]
},
legend: {
display: true
}
},
data: {
labels: _labels,
datasets: datasets
}
});
},
componentWillReceiveProps: function(nextProps) {
const {
datasets = [],
labels = 0
} = this.props;
this._chart.data.datasets = datasets;
this._chart.data.labels = buildArray(labels).map((v, i) => '');
this._chart.update(0);
},
render: function() {
return (
<canvas
ref={this.ref('component')}
width='400'
height='400'
/>
);
}
});

View File

@ -0,0 +1,34 @@
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
user: 'rgb(255, 99, 132)',
sys: 'rgb(255, 159, 64)',
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = {},
windowSize
}) => {
const datasets = ['perc'].map((key) => {
return {
label: key,
backgroundColor: colors[key],
altBackgroundColor: colors['alt'],
data: buildArray(windowSize).map((v, i) => ((data[i] || {})[key] || { firstQuartile: 0, thirdQuartile: 0, median: 0, max: 0, min: 0 })).reverse()
};
});
return (
<Chart
datasets={datasets}
stacked={true}
labels={datasets[0].data.length}
legend={true}
/>
);
};

View File

@ -0,0 +1,30 @@
const pretty = require('prettysize');
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = [],
windowSize
}) => {
const datasets = [{
label: 'disk',
backgroundColor: colors['perc'],
altBackgroundColor: colors['alt'],
data: buildArray(windowSize).map((v, i) => ((data[i] || {})['perc'] || { firstQuartile: 0, thirdQuartile: 0, median: 0, max: 0, min: 0 })).reverse()
}];
return (
<Chart
datasets={datasets}
labels={datasets[0].data.length}
legend={true}
/>
);
};

View File

@ -0,0 +1,8 @@
module.exports = {
CPU: require('./cpu'),
cpu: require('./cpu'),
Mem: require('./mem'),
mem: require('./mem'),
Disk: require('./disk'),
disk: require('./disk')
};

View File

@ -0,0 +1,29 @@
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = [],
windowSize
}) => {
const datasets = [{
label: 'mem',
backgroundColor: colors['perc'],
altBackgroundColor: colors['alt'],
data: buildArray(windowSize).map((v, i) => ((data[i] || {}).perc || { firstQuartile: 0, thirdQuartile: 0, median: 0, max: 0, min: 0 })).reverse()
}];
return (
<Chart
datasets={datasets}
labels={datasets[0].data.length}
legend={true}
/>
);
};

View File

@ -0,0 +1,221 @@
'use strict';
module.exports = function(Chart) {
var globalOpts = Chart.defaults.global;
globalOpts.elements.rectangle = {
backgroundColor: globalOpts.defaultColor,
borderWidth: 0,
borderColor: globalOpts.defaultColor,
borderSkipped: 'bottom'
};
function isVertical(bar) {
return bar._view.width !== undefined;
}
/**
* Helper function to get the bounds of the bar regardless of the orientation
* @private
* @param bar {Chart.Element.Rectangle} the bar
* @return {Bounds} bounds of the bar
*/
function getBarBounds(bar) {
var vm = bar._view;
var x1, x2, y1, y2;
if (isVertical(bar)) {
// vertical
var halfWidth = vm.width / 2;
x1 = vm.x - halfWidth;
x2 = vm.x + halfWidth;
y1 = Math.min(vm.y, vm.base);
y2 = Math.max(vm.y, vm.base);
} else {
// horizontal bar
var halfHeight = vm.height / 2;
x1 = Math.min(vm.x, vm.base);
x2 = Math.max(vm.x, vm.base);
y1 = vm.y - halfHeight;
y2 = vm.y + halfHeight;
}
return {
left: x1,
top: y1,
right: x2,
bottom: y2
};
}
Chart.elements.Whisker = Chart.Element.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var halfWidth = vm.width / 2,
leftX = vm.x - halfWidth,
rightX = vm.x + halfWidth,
top = vm.base - (vm.base - vm.y),
halfStroke = vm.borderWidth / 2;
// Canvas doesn't allow us to stroke inside the width so we can
// adjust the sizes to fit if we're setting a stroke on the line
if (vm.borderWidth) {
leftX += halfStroke;
rightX -= halfStroke;
top += halfStroke;
}
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = vm.borderWidth;
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[leftX, vm.base],
[leftX, top],
[rightX, top],
[rightX, vm.base]
];
// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(vm.borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
}
function cornerAt(index) {
return corners[(startCorner + index) % 4];
}
// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
ctx.lineTo(corner[0], corner[1]);
}
ctx.fill();
if (vm.borderWidth) {
ctx.stroke();
}
ctx.closePath();
// Median line
ctx.beginPath();
ctx.moveTo(leftX, vm.median);
ctx.lineTo(rightX, vm.median);
ctx.lineWidth = 2;
// set line color
ctx.strokeStyle = 'rgb(54, 74, 205)';
ctx.stroke();
ctx.closePath();
// Top Whisker
// if (smaller than 5px then do not draw)
if (vm.median - vm.maxV > 10) {
ctx.beginPath();
ctx.moveTo((rightX - leftX) / 2 + leftX, vm.median - 1);
ctx.lineTo((rightX - leftX) / 2 + leftX, vm.maxV);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(245, 93, 93)';
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.arc((rightX - leftX) / 2 + leftX, vm.maxV, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(245, 93, 93)';
ctx.fill();
}
// Bottom Whisker
// if (smaller than 5px then do not draw)
if (vm.minV - vm.median > 10) {
ctx.beginPath();
ctx.moveTo((rightX - leftX) / 2 + leftX, vm.median + 1);
ctx.lineTo((rightX - leftX) / 2 + leftX, vm.minV);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(245, 93, 93)';
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.arc((rightX - leftX) / 2 + leftX, vm.minV, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(245, 93, 93)';
ctx.fill();
}
},
height: function() {
var vm = this._view;
return vm.base - vm.y;
},
inRange: function(mouseX, mouseY) {
var inRange = false;
if (this._view) {
var bounds = getBarBounds(this);
inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom;
}
return inRange;
},
inLabelRange: function(mouseX, mouseY) {
var me = this;
if (!me._view) {
return false;
}
var inRange = false;
var bounds = getBarBounds(me);
if (isVertical(me)) {
inRange = mouseX >= bounds.left && mouseX <= bounds.right;
} else {
inRange = mouseY >= bounds.top && mouseY <= bounds.bottom;
}
return inRange;
},
inXRange: function(mouseX) {
var bounds = getBarBounds(this);
return mouseX >= bounds.left && mouseX <= bounds.right;
},
inYRange: function(mouseY) {
var bounds = getBarBounds(this);
return mouseY >= bounds.top && mouseY <= bounds.bottom;
},
getCenterPoint: function() {
var vm = this._view;
var x, y;
if (isVertical(this)) {
x = vm.x;
y = (vm.y + vm.base) / 2;
} else {
x = (vm.x + vm.base) / 2;
y = vm.y;
}
return {x: x, y: y};
},
getArea: function() {
var vm = this._view;
return vm.width * Math.abs(vm.y - vm.base);
},
tooltipPosition: function() {
var vm = this._view;
return {
x: vm.x,
y: vm.y
};
}
});
};

View File

@ -0,0 +1,36 @@
const ReactDOM = require('react-dom');
const React = require('react');
const Store = require('./store');
const nes = require('nes/dist/client');
const {
Client
} = nes;
const client = new Client(`ws://${document.location.host}`);
client.connect((err) => {
if (err) {
throw err;
}
});
const store = Store({
ws: client,
windowSize: 20
});
const render = () => {
const Root = require('./root');
ReactDOM.render(
<Root store={store} />,
document.getElementById('root')
);
};
render();
if (module.hot) {
module.hot.accept('./root', render);
}

View File

@ -0,0 +1,89 @@
const React = require('react');
const buildArray = require('build-array');
const ReactRedux = require('react-redux');
const Chart = require('./chart');
const actions = require('./actions');
const {
connect
} = ReactRedux;
const {
subscribe,
unsubscribe
} = actions;
const mapStateToProps = (state, ownProps) => {
return {
windowSize: state.windowSize,
data: state[ownProps.id]
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
subscribe: () => {
return dispatch(subscribe(ownProps.id));
},
unsubscribe: () => {
return unsubscribe(ownProps.id);
}
}
};
const Row = connect(
mapStateToProps,
mapDispatchToProps,
)(React.createClass({
componentWillMount: function() {
this.props.subscribe();
},
componentWillUnmount: function() {
this.props.unsubscribe();
},
render: function() {
const {
data = {},
windowSize
} = this.props;
const charts = Object.keys(data).map((key, i, arr) => {
if (!Chart[key]) {
return null;
}
const chart = React.createElement(Chart[key], {
data: data[key],
windowSize
});
return (
<div key={key} className={`col-xs-${12 / arr.length}`}>
{chart}
</div>
);
});
return (
<div className='row'>
{charts}
</div>
);
}
}));
module.exports = ({
rows
}) => {
const _rows = buildArray(rows).map((v, i) => {
return (
<Row id={i} key={i} />
);
});
return (
<div>
{_rows}
</div>
);
};

View File

@ -0,0 +1,24 @@
const React = require('react');
const ReactHotLoader = require('react-hot-loader');
const ReactRedux = require('react-redux');
const Matrix = require('./matrix');
const {
AppContainer
} = ReactHotLoader;
const {
Provider
} = ReactRedux;
module.exports = ({
store
}) => {
return (
<AppContainer>
<Provider store={store}>
<Matrix rows={4} />
</Provider>
</AppContainer>
);
};

View File

@ -0,0 +1,21 @@
const createLogger = require('redux-logger');
const promiseMiddleware = require('redux-promise-middleware').default;
const thunk = require('redux-thunk').default;
const redux = require('redux');
const reducer = require('./actions');
const {
createStore,
compose,
applyMiddleware
} = redux;
module.exports = (state = Object.freeze({})) => {
return createStore(reducer, state, applyMiddleware(
createLogger({
predicate: (getState, action) => action.type !== 'UPDATE_STATS'
}),
promiseMiddleware(),
thunk
));
};

View File

@ -0,0 +1,276 @@
const whiskerElement = require('./element.whisker');
module.exports = function(Chart) {
whiskerElement(Chart);
var helpers = Chart.helpers;
Chart.defaults.whisker = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'category',
// Specific to Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}],
yAxes: [{
type: 'linear'
}]
}
};
Chart.controllers.whisker = Chart.DatasetController.extend({
dataElementType: Chart.elements.Whisker,
initialize: function(chart, datasetIndex) {
Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);
// Use this to indicate that this is a bar dataset.
this.getMeta().bar = true;
},
// Get the number of datasets that display bars. We use this to correctly calculate the bar width
getBarCount: function() {
var me = this;
var barCount = 0;
helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {
var meta = me.chart.getDatasetMeta(datasetIndex);
if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) {
++barCount;
}
}, me);
return barCount;
},
update: function(reset) {
var me = this;
helpers.each(me.getMeta().data, function(rectangle, index) {
me.updateElement(rectangle, index, reset);
}, me);
},
updateElement: function(rectangle, index, reset) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var yScale = me.getScaleForId(meta.yAxisID);
var scaleBase = yScale.getBasePixel();
var rectangleElementOptions = me.chart.options.elements.rectangle;
var custom = rectangle.custom || {};
var dataset = me.getDataset();
rectangle._xScale = xScale;
rectangle._yScale = yScale;
rectangle._datasetIndex = me.index;
rectangle._index = index;
var ruler = me.getRuler(index);
rectangle._model = {
x: me.calculateBarX(index, me.index, ruler),
y: reset ? scaleBase : me.boxTopValue(index, me.index),
// Tooltip
label: me.chart.data.labels[index],
datasetLabel: dataset.label,
// Appearance
median: reset ? scaleBase : me.medianValue(me.index, index),
maxV: reset ? scaleBase : me.maxValue(me.index, index),
minV: reset ? scaleBase : me.minValue(me.index, index),
base: reset ? scaleBase : me.boxBottomValue(me.index, index),
width: me.calculateBarWidth(ruler),
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(me.stddev(me.index, index) > 3 ? dataset.altBackgroundColor : dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
};
rectangle.pivot();
},
stddev: function(datasetIndex, index) {
var me = this;
var obj = me.getDataset().data[index];
var value = Number(obj.stddev);
return value;
},
minValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.min);
return yScale.getPixelForValue(value);
},
maxValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.max);
return yScale.getPixelForValue(value);
},
medianValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.median);
return yScale.getPixelForValue(value);
},
boxBottomValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.firstQuartile);
return yScale.getPixelForValue(value);
},
boxTopValue: function(index, datasetIndex) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.thirdQuartile);
return yScale.getPixelForValue(value);
},
getRuler: function(index) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var datasetCount = me.getBarCount();
var tickWidth;
if (xScale.options.type === 'category') {
tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);
} else {
// Average width
tickWidth = xScale.width / xScale.ticks.length;
}
var categoryWidth = tickWidth * xScale.options.categoryPercentage;
var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
var fullBarWidth = categoryWidth / datasetCount;
if (xScale.ticks.length !== me.chart.data.labels.length) {
var perc = xScale.ticks.length / me.chart.data.labels.length;
fullBarWidth = fullBarWidth * perc;
}
var barWidth = fullBarWidth * xScale.options.barPercentage;
var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);
return {
datasetCount: datasetCount,
tickWidth: tickWidth,
categoryWidth: categoryWidth,
categorySpacing: categorySpacing,
fullBarWidth: fullBarWidth,
barWidth: barWidth,
barSpacing: barSpacing
};
},
calculateBarWidth: function(ruler) {
var xScale = this.getScaleForId(this.getMeta().xAxisID);
if (xScale.options.barThickness) {
return xScale.options.barThickness;
}
return ruler.barWidth;
},
// Get bar index from the given dataset index accounting for the fact that not all bars are visible
getBarIndex: function(datasetIndex) {
var barIndex = 0;
var meta;
var j;
for (j = 0; j < datasetIndex; ++j) {
meta = this.chart.getDatasetMeta(j);
if (meta.bar && this.chart.isDatasetVisible(j)) {
++barIndex;
}
}
return barIndex;
},
calculateBarX: function(index, datasetIndex, ruler) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var barIndex = me.getBarIndex(datasetIndex);
var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;
return leftTick +
(ruler.barWidth / 2) +
ruler.categorySpacing +
(ruler.barWidth * barIndex) +
(ruler.barSpacing / 2) +
(ruler.barSpacing * barIndex);
},
draw: function(ease) {
var me = this;
var easingDecimal = ease || 1;
var metaData = me.getMeta().data;
var dataset = me.getDataset();
var i, len;
for (i = 0, len = metaData.length; i < len; ++i) {
var d = dataset.data[i];
if (d !== null && d !== undefined && typeof d === 'object' && !isNaN(d.median)) {
metaData[i].transition(easingDecimal).draw();
}
}
},
setHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor));
model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor));
model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth);
},
removeHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
var rectangleElementOptions = this.chart.options.elements.rectangle;
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor);
model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor);
model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth);
}
});
};

View File

@ -0,0 +1,11 @@
OldCpuSpeed=2712
NewCpuSpeedCount=0
NewCpuSpeed=0
RollingAverage=1000
RollingAverageIsFromV27=1
ComputerGUID=7fe43060180dc17e1be44afd3c056d72
CPUHours=1
RollingStartTime=0
Affinity=100
ThreadsPerTest=4
Pid=0

View File

@ -0,0 +1,70 @@
{
"name": "chartjs-matrix-spike",
"private": true,
"license": "private",
"main": "server/index.js",
"dependencies": {
"async": "^2.1.2",
"autoprefixer": "^6.5.1",
"babel-eslint": "^7.0.0",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.16.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babel-preset-react-hmre": "^1.1.1",
"babel-runtime": "^6.11.6",
"build-array": "^1.0.0",
"chart.js": "^2.3.0",
"classnames": "^2.2.5",
"component-emitter": "^1.2.1",
"cpu-percent": "^2.0.1",
"css-loader": "^0.25.0",
"d3": "^4.3.0",
"diskusage": "^0.1.5",
"hapi": "^15.2.0",
"hapi-webpack-dev-plugin": "^1.1.4",
"inert": "^4.0.2",
"lodash.take": "^4.1.1",
"metrics-os": "^1.0.1",
"nes": "^6.3.1",
"os-utils": "^0.0.14",
"pidusage": "^1.1.0",
"postcss-loader": "^1.0.0",
"postcss-modules-values": "^1.2.2",
"postcss-nested": "^1.0.0",
"prettysize": "0.0.3",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-hot-loader": "^3.0.0-beta.6",
"react-redux": "^4.4.5",
"redux": "^3.6.0",
"redux-logger": "^2.7.4",
"redux-promise-middleware": "^4.1.0",
"redux-thunk": "^2.1.0",
"require-dir": "^0.3.1",
"simple-statistics": "^2.2.0",
"style-loader": "^0.13.1",
"webpack": "^1.13.2",
"webpack-dev-server": "^1.16.2"
},
"devDependencies": {
"babel-register": "^6.16.3",
"eslint": "^3.8.1",
"eslint-config-semistandard": "^7.0.0",
"eslint-config-standard": "^6.2.0",
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-promise": "^3.3.0",
"eslint-plugin-react": "^6.4.1",
"eslint-plugin-standard": "^2.0.1",
"json-loader": "^0.5.4"
},
"ava": {
"require": [
"babel-register"
],
"babel": "inherit"
}
}

View File

@ -0,0 +1,14 @@
# ChartJS
- [x] Graphs should maintain aspect ration
- [x] Graphs should match Antonas' first draft designs
- [x] Should have 3 Graphs on each row
- [x] Should be a 3 x 4 matrix of graphs, showing different data
- [x] Graphs should not jitter, ideally smoothly move across the x axis
- [x] All graphs should be a bar graph
- [ ] Animations when a graph comes into view
## notes
- borderSkipped not working: https://github.com/chartjs/Chart.js/issues/3293

View File

@ -0,0 +1,29 @@
const requireDir = require('require-dir');
const plugins = require('./plugins');
const routes = requireDir('./routes');
const Hapi = require('hapi');
const path = require('path');
const fs = require('fs');
const server = new Hapi.Server();
server.connection({
host: 'localhost',
port: 8000
});
server.register(plugins, (err) => {
if (err) {
throw err;
}
Object.keys(routes).forEach((name) => {
routes[name](server);
});
server.start((err) => {
server.connections.forEach((conn) => {
console.log(`started at: ${conn.info.uri}`);
});
});
});

View File

@ -0,0 +1,120 @@
const async = require('async');
const disk = require('diskusage');
const os = require('os');
const cdm = {};
const osutils = require('os-utils');
const statistics = require('simple-statistics');
const getCPU = (fn) => {
return fn(null, {
user: os.cpus().reduce((sum, cpu) => sum + cpu.times.user, 0),
sys: os.cpus().reduce((sum, cpu) => sum + cpu.times.sys, 0)
});
};
const getPerc = (fn) => {
async.timesSeries(5, (n, next) => {
osutils.cpuUsage((p) => {
const percentage = p * 100;
next(null, percentage);
});
}, (err, sample) => {
fn(err, {
perc: {
firstQuartile: statistics.quantile(sample, 0.25),
median: statistics.median(sample),
thirdQuartile: statistics.quantile(sample, 0.75),
max: statistics.max(sample),
min: statistics.min(sample),
stddev: statistics.sampleStandardDeviation(sample)
}
});
});
};
const getMem = (fn) => {
async.timesSeries(10, (n, next) => {
const free = os.freemem();
const total = os.totalmem();
const using = total - free;
const perc = (using / total) * 100;
setTimeout(() => {
next(null, perc);
}, 500);
}, (err, sample) => {
fn(err, {
perc: {
firstQuartile: statistics.quantile(sample, 0.25),
median: statistics.median(sample),
thirdQuartile: statistics.quantile(sample, 0.75),
max: statistics.max(sample),
min: statistics.min(sample),
stddev: statistics.sampleStandardDeviation(sample)
}
});
});
};
const getDisk = (fn) => {
async.timesSeries(5, (n, next) => {
disk.check('/', (err, data) => {
setTimeout(() => {
const perc = (data.available / data.total) * 100;
next(err, perc);
}, 2000);
});
}, (err, sample) => {
fn(err, {
perc: {
firstQuartile: statistics.quantile(sample, 0.25),
median: statistics.median(sample),
thirdQuartile: statistics.quantile(sample, 0.75),
max: statistics.max(sample),
min: statistics.min(sample),
stddev: statistics.sampleStandardDeviation(sample)
}
});
});
};
const getStats = (fn) => {
async.parallel({
cpu: getPerc,
mem: getMem,
disk: getDisk
}, fn);
};
module.exports = (server) => ({
on: (id) => {
console.log('on', cdm[id]);
if (cdm[id] && (cdm[id].sockets > 0)) {
cdm[id].sockets += 1;
return;
}
let messageId = 0;
const interval = setInterval(() => {
console.log(`publishing /stats/${id}`);
getStats((err, stats) => {
server.publish(`/stats/${id}`, {
when: new Date().getTime(),
stats
});
});
}, 1000);
cdm[id] = {
interval,
sockets: 1
};
},
off: (id) => {
if (!(cdm[id].sockets -= 1)) {
clearInterval(cdm[id].interval);
}
}
});

View File

@ -0,0 +1,15 @@
const webpack = require('webpack');
const path = require('path');
const cfg = require('../webpack.config.js');
module.exports = [
require('inert'),
require('nes'), {
register: require('hapi-webpack-dev-plugin'),
options: {
compiler: webpack(cfg),
devIndex: path.join(__dirname, '../static')
}
}
];

View File

@ -0,0 +1,11 @@
const path = require('path');
module.exports = (server) => {
server.route({
method: 'GET',
path: '/',
handler: (request, reply) => {
reply.file(path.join(__dirname, '../../static/index.html'));
}
});
};

View File

@ -0,0 +1,18 @@
const Metric = require('../metric');
module.exports = (server) => {
const metric = Metric(server);
server.subscription('/stats/{id}', {
onSubscribe: (socket, path, params, next) => {
console.log('onSubscribe');
metric.on(params.id);
next();
},
onUnsubscribe: (socket, path, params, next) => {
console.log('onUnsubscribe');
metric.off(params.id);
next();
}
});
};

View File

@ -0,0 +1,15 @@
const path = require('path');
module.exports = (server) => {
// server.route({
// method: 'GET',
// path: '/{param*}',
// handler: {
// directory: {
// path: path.join(__dirname, '../../static'),
// redirectToSlash: true,
// index: true
// }
// }
// });
};

View File

@ -0,0 +1,973 @@
<!doctype html>
<html lang='en-US'>
<head>
<title>React Boilerplate</title>
<link rel="stylesheet" type="text/css" href="https://necolas.github.io/normalize.css/latest/normalize.css" />
<style>
.container-fluid,
.container {
margin-right: auto;
margin-left: auto;
}
.container-fluid {
padding-right: 2rem;
padding-left: 2rem;
}
.row {
box-sizing: border-box;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -0.5rem;
margin-left: -0.5rem;
}
.row.reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.col.reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.col-xs,
.col-xs-1,
.col-xs-2,
.col-xs-3,
.col-xs-4,
.col-xs-5,
.col-xs-6,
.col-xs-7,
.col-xs-8,
.col-xs-9,
.col-xs-10,
.col-xs-11,
.col-xs-12,
.col-xs-offset-0,
.col-xs-offset-1,
.col-xs-offset-2,
.col-xs-offset-3,
.col-xs-offset-4,
.col-xs-offset-5,
.col-xs-offset-6,
.col-xs-offset-7,
.col-xs-offset-8,
.col-xs-offset-9,
.col-xs-offset-10,
.col-xs-offset-11,
.col-xs-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-xs {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-xs-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-xs-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-xs-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-xs-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-xs-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-xs-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-xs-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-xs-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-xs-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-xs-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-xs-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-xs-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-xs-offset-0 {
margin-left: 0;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.start-xs {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-xs {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-xs {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-xs {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-xs {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-xs {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-xs {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-xs {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-xs {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-xs {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
@media only screen and (min-width: 48em) {
.container {
width: 49rem;
}
.col-sm,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-offset-0,
.col-sm-offset-1,
.col-sm-offset-2,
.col-sm-offset-3,
.col-sm-offset-4,
.col-sm-offset-5,
.col-sm-offset-6,
.col-sm-offset-7,
.col-sm-offset-8,
.col-sm-offset-9,
.col-sm-offset-10,
.col-sm-offset-11,
.col-sm-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-sm {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-sm-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-sm-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-sm-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-sm-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-sm-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-sm-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-sm-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-sm-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-sm-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-sm-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-sm-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-sm-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-sm-offset-0 {
margin-left: 0;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.start-sm {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-sm {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-sm {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-sm {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-sm {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-sm {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-sm {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-sm {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-sm {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-sm {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
@media only screen and (min-width: 64em) {
.container {
width: 65rem;
}
.col-md,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12,
.col-md-offset-0,
.col-md-offset-1,
.col-md-offset-2,
.col-md-offset-3,
.col-md-offset-4,
.col-md-offset-5,
.col-md-offset-6,
.col-md-offset-7,
.col-md-offset-8,
.col-md-offset-9,
.col-md-offset-10,
.col-md-offset-11,
.col-md-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-md {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-md-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-md-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-md-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-md-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-md-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-md-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-md-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-md-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-md-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-md-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-md-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-md-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-md-offset-0 {
margin-left: 0;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.start-md {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-md {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-md {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-md {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-md {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-md {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-md {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-md {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-md {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-md {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
@media only screen and (min-width: 75em) {
.container {
width: 76rem;
}
.col-lg,
.col-lg-1,
.col-lg-2,
.col-lg-3,
.col-lg-4,
.col-lg-5,
.col-lg-6,
.col-lg-7,
.col-lg-8,
.col-lg-9,
.col-lg-10,
.col-lg-11,
.col-lg-12,
.col-lg-offset-0,
.col-lg-offset-1,
.col-lg-offset-2,
.col-lg-offset-3,
.col-lg-offset-4,
.col-lg-offset-5,
.col-lg-offset-6,
.col-lg-offset-7,
.col-lg-offset-8,
.col-lg-offset-9,
.col-lg-offset-10,
.col-lg-offset-11,
.col-lg-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-lg {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-lg-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-lg-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-lg-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-lg-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-lg-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-lg-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-lg-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-lg-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-lg-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-lg-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-lg-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-lg-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-lg-offset-0 {
margin-left: 0;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.start-lg {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-lg {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-lg {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-lg {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-lg {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-lg {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-lg {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-lg {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-lg {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-lg {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
</style>
</head>
<body>
<div id='root'></div>
<script src='/static/bundle.js'></script>
</body>
</html>

View File

@ -0,0 +1,65 @@
const webpack = require('webpack');
const path = require('path');
const config = {
debug: true,
devtool: 'source-map',
context: path.join(__dirname, './client'),
app: path.join(__dirname, './client/index.js'),
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'react-hot-loader/patch',
'./index.js'
],
output: {
path: path.join(__dirname, './static'),
publicPath: '/static/',
filename: 'bundle.js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.ProvidePlugin({
'd3': 'd3'
})
],
module: {
loaders: [{
test: /js?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, './client')
],
loaders: ['babel']
}, {
test: /\.json?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, './client')
],
loaders: ['json']
}, {
test: /\.css$/,
exclude: /node_modules/,
include: [
path.join(__dirname, './client')
],
loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'
}]
}
};
const devServer = {
hot: true,
compress: true,
lazy: false,
publicPath: config.output.publicPath,
historyApiFallback: {
index: './static/index.html'
}
};
module.exports = Object.assign({
devServer
}, config);

File diff suppressed because it is too large Load Diff

15
spikes/leak/.babelrc Normal file
View File

@ -0,0 +1,15 @@
{
"presets": [
"react",
"es2015"
],
"plugins": [
["transform-object-rest-spread", {
"useBuiltIns": true
}],
"add-module-exports",
"transform-es2015-modules-commonjs",
"react-hot-loader/babel"
],
"sourceMaps": "both"
}

View File

@ -0,0 +1,3 @@
/node_modules
coverage
.nyc_output

29
spikes/leak/.eslintrc Normal file
View File

@ -0,0 +1,29 @@
{
"extends": "semistandard",
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"babel",
"react"
],
"rules": {
"generator-star-spacing": 0,
"babel/generator-star-spacing": 1,
"space-before-function-paren": [2, "never"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"object-curly-newline": ["error", {
"minProperties": 1
}],
"sort-vars": ["error", {
"ignoreCase": true
}]
}
}

View File

@ -1,10 +1,14 @@
FROM mhart/alpine-node:7 FROM mhart/alpine-node:7
WORKDIR /src
ADD . .
RUN npm install -g yarn RUN npm install -g yarn
WORKDIR /src
COPY package.json package.json
COPY yarn.lock yarn.lock
RUN yarn install --production --pure-lockfile --prefer-offline RUN yarn install --production --pure-lockfile --prefer-offline
COPY . .
EXPOSE 8000 EXPOSE 8000
CMD ["node", "start.js"] CMD ["node", "scripts/start.js"]

View File

@ -0,0 +1,9 @@
config:
target: "http://fast-node:8000"
phases:
- duration: 13200
arrivalRate: 1
scenarios:
- flow:
- get:
url: "/mem-fast"

View File

@ -13,6 +13,19 @@ services:
- MODE=fast - MODE=fast
depends_on: depends_on:
- fast-node - fast-node
another-fast-node:
build: .
environment:
- TYPE=node
ports:
- "8004:8000"
another-fast-artillery:
build: .
environment:
- TYPE=artillery
- MODE=another-fast
depends_on:
- another-fast-node
slow-node: slow-node:
build: . build: .
environment: environment:
@ -49,5 +62,6 @@ services:
- TYPE=telemetry - TYPE=telemetry
depends_on: depends_on:
- fast-node - fast-node
- another-fast-node
- slow-node - slow-node
- plain-node - plain-node

View File

@ -1 +0,0 @@
{}

3012
spikes/leak/metrics.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,17 +3,65 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "private", "license": "private",
"main": "src/index.js", "main": "src/server/index.js",
"dependencies": { "dependencies": {
"artillery": "^1.5.0-17", "artillery": "^1.5.0-17",
"clone": "^2.0.0", "async": "^2.1.4",
"build-array": "^1.0.0",
"chart.js": "^2.4.0",
"date.js": "^0.3.1",
"epimetheus": "^1.0.46", "epimetheus": "^1.0.46",
"force-array": "^3.1.0",
"good": "^7.0.2", "good": "^7.0.2",
"good-console": "^6.3.1", "good-console": "^6.3.1",
"good-squeeze": "^5.0.1", "good-squeeze": "^5.0.1",
"got": "^6.6.3",
"hapi": "^15.2.0", "hapi": "^15.2.0",
"hapi-webpack-dev-plugin": "1.1.4",
"inert": "^4.0.2",
"internet-timestamp": "^0.0.1",
"lodash.first": "^3.0.0",
"lodash.get": "^4.4.2",
"lodash.take": "^4.1.1",
"minimist": "^1.2.0",
"nes": "^6.3.1",
"pretty-hrtime": "^1.0.3", "pretty-hrtime": "^1.0.3",
"prom-client": "^6.1.2", "prom-client": "^6.1.2",
"require-dir": "^0.3.1" "qs": "^6.3.0",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"react-hot-loader": "^3.0.0-beta.6",
"react-redux": "^4.4.6",
"redux": "^3.6.0",
"redux-logger": "^2.7.4",
"redux-promise-middleware": "^4.1.0",
"redux-thunk": "^2.1.0",
"relative-date": "^1.1.3",
"require-dir": "^0.3.1",
"simple-statistics": "^2.2.0"
},
"devDependencies": {
"async": "^2.1.4",
"babel-core": "^6.18.2",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.8",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.18.0",
"babel-plugin-transform-object-rest-spread": "^6.19.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"diskusage": "^0.1.5",
"eslint": "^3.10.2",
"eslint-config-semistandard": "^7.0.0",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-standard": "^2.0.1",
"json-loader": "^0.5.4",
"os-utils": "^0.0.14",
"simple-statistics": "^2.2.0",
"webpack": "^1.13.3",
"webpack-dev-server": "^1.16.2"
} }
} }

View File

@ -1,16 +1,17 @@
scrape_configs: scrape_configs:
- job_name: 'leak-fast' - job_name: 'leak-fast'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 1s scrape_interval: 1s
static_configs: static_configs:
- targets: ['fast-node:8000'] - targets: ['fast-node:8000', 'another-fast-node:8000']
- job_name: 'leak-slow' - job_name: 'leak-slow'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 1s scrape_interval: 1s
static_configs: static_configs:
- targets: ['slow-node:8000'] - targets: ['slow-node:8000']
- job_name: 'no-leak' - job_name: 'no-leak'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 1s scrape_interval: 1s
static_configs: static_configs:
- targets: ['plain-node:8000'] - targets: ['plain-node:8000']
- job_name: 'leak'
scrape_interval: 1s
static_configs:
- targets: ['fast-node:8000', 'another-fast-node:8000', 'slow-node:8000', 'plain-node:8000']

View File

@ -1,16 +1,25 @@
GET /mem # leak
GET /cpu
cpu-node: - 1. Spawn a bunch of servers:
build: . - another-fast: a node with a linear memory leak
environment: - fast: a node with a linear memory leak
- TYPE=node - slow: a node with a memory leak that grows very slowly
ports: - plain: a node with no memory leak
- "8003:8000" - 2. Spawn an [artillery](https://artillery.io) for each node that loads it with a small but constant stream of requests
cpu-artillery: - 3. Spawn Prometheus that watches the cpu/memory of each node
build: .
environment: Then, locally we start the same server and we can see the different instances and an aggregate of the metrics for each job.
- TYPE=artillery
- MODE=cpu ## usage
depends_on:
- cpu-node ```
λ docker-compose up
λ node .
```
Go to http://127.0.0.1:8000/ and see the result.
The [Prometheus](https://prometheus.io) is also listening at http://127.0.0.1:9090/
## example
![](https://cldup.com/yxS380e1HN.png)

View File

@ -0,0 +1,155 @@
const forceArray = require('force-array');
const get = require('lodash.get');
const date = require('date.js');
const timestamp = require('internet-timestamp');
const got = require('got');
const url = require('url');
const qs = require('qs');
const transform = (res) => {
return forceArray(res).reduce((sum, r) => {
const {
data
} = JSON.parse(r.body);
const result = !Array.isArray(data)
? data.result
: data;
return result.reduce((sum, inst) => {
const metric = !inst.job
? inst.metric
: inst;
const {
values = [],
value = []
} = inst;
const {
instance,
job,
__name__
} = metric;
const oldJob = get(sum, job, {});
const oldQuery = get(sum, `${job}.${__name__}`, {});
const _value = values.length ? values : value
return Object.assign(sum, {
[job]: Object.assign(oldJob, {
[instance]: Object.assign(oldQuery, {
[__name__]: _value
})
})
})
}, sum);
}, {});
};
const range = module.exports.range = ({
query = [],
ago = '1h ago',
step = '1s',
hostname = 'localhost'
}) => {
const end = timestamp(new Date());
const start = timestamp(date(ago));
return Promise.all(query.map((query) => {
return got(url.format({
protocol: 'http:',
slashes: true,
port: '9090',
hostname: hostname,
pathname: '/api/v1/query_range',
query: {
query,
end,
step,
start
}
}));
}))
.then(transform);
};
const query = module.exports.query = ({
hostname = 'localhost',
query = []
}) => {
return Promise.all(query.map((query) => {
return got(url.format({
protocol: 'http:',
slashes: true,
port: '9090',
hostname: hostname,
pathname: '/api/v1/query',
query: {
query: query
}
}));
}))
.then(transform);
};
const tree = module.exports.tree = ({
hostname = 'localhost',
query = []
}) => {
return got(url.format({
protocol: 'http:',
slashes: true,
port: '9090',
hostname: hostname,
pathname: '/api/v1/series',
search: qs.stringify({
match: query
}, {
arrayFormat: 'brackets'
})
}))
.then(transform);
};
if (!module.parent) {
process.on('unhandledRejection', (reason) => {
throw reason
});
const usage = () => {
console.error(`
Usage: node metrics.js --type={type} --query={metric} --step={step} --ago={ago}
node metrics.js --type=range --query=node_memory_heap_used_bytes --query=node_memory_heap_total_bytes
`.trim());
return process.exit(1);
}
const argv = require('minimist')(process.argv.slice(2));
if (!argv.query || !argv.type) {
return usage();
}
const handlers = {
tree,
range,
query
};
if (!handlers[argv.type]) {
return usage();
}
const conf = {
query: argv.query,
ago: argv.ago,
step: argv.step,
hostname: argv.hostname
};
handlers[argv.type](conf).then((res) => {
console.log(JSON.stringify(res, null, 2));
});
}

View File

@ -1,4 +1,5 @@
const cp = require('child_process'); const cp = require('child_process');
const path = require('path');
const TYPE = process.env.TYPE; const TYPE = process.env.TYPE;
const MODE = process.env.MODE; const MODE = process.env.MODE;
@ -12,17 +13,18 @@ Usage: TYPE={type} node start.js
process.exit(1); process.exit(1);
} }
const handler = ({ const handler = ({
node: () => { node: () => {
console.log('node src/index.js'); const root = path.join(__dirname, '../');
return cp.exec('node src/index.js', { const script = path.join(root, 'src/server/index.js');
return cp.exec(`node ${script}`, {
cwd: __dirname cwd: __dirname
}) });
}, },
artillery: () => { artillery: () => {
console.log(`./node_modules/.bin/artillery run ${__dirname}/artillery-${MODE}.yml`); const conf = path.join(__dirname, '../artillery/artillery-${MODE}.yml');
return cp.exec(`./node_modules/.bin/artillery run ${__dirname}/artillery-${MODE}.yml`) const bin = path.join(__dirname, '../node_modules/.bin/artillery');
return cp.exec(`${bin} run ${conf}`);
} }
})[TYPE]; })[TYPE];

View File

@ -0,0 +1,109 @@
const take = require('lodash.take');
const get = require('lodash.get');
const actions = {
'UPDATE_STATS': (state, action) => {
const data = get(state, `data.${action.subscription}`, {
cpu: [],
mem: [],
disk: []
});
const newData = ['cpu', 'mem', 'disk'].reduce((sum, key) => {
const item = {
...action.payload.stats[key],
when: action.payload.when
};
const prepended = [item].concat(data[key]);
return {
...sum,
[key]: take(prepended, state.windowSize)
};
}, {});
return {
...state,
data: {
...state.data,
[action.subscription]: newData
}
};
},
'GET_JOB_TREE_FULFILLED': (state, action) => {
return {
...state,
tree: action.payload
};
}
};
module.exports = (state, action) => {
return !actions[action.type] ? state : actions[action.type](state, action);
};
module.exports.subscribe = (id) => (dispatch, getState) => {
const {
ws
} = getState();
const p = new Promise((resolve, reject) => {
ws.subscribe(`/stats/${id}`, (update, flag) => {
dispatch({
type: 'UPDATE_STATS',
payload: update,
subscription: id
});
}, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return dispatch({
type: 'SUBSCRIBE',
payload: p
});
};
module.exports.unsubscribe = (id) => (dispatch, getState) => {
const {
ws
} = getState();
const p = new Promise((resolve, reject) => {
ws.unsubscribe(`/stats/${id}`, null, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return dispatch({
type: 'UNSUBSCRIBE',
payload: p
});
};
module.exports.getTree = (id) => (dispatch, getState) => {
const {
ws
} = getState();
const p = new Promise((resolve, reject) => {
ws.request(`/job-tree`, (err, payload) => {
return err ? reject(err) : resolve(payload);
});
});
return dispatch({
type: 'GET_JOB_TREE',
payload: p
});
};

View File

@ -0,0 +1,85 @@
const buildArray = require('build-array');
const Chart = require('chart.js');
const React = require('react');
const whisker = require('../whisker');
whisker(Chart);
module.exports = React.createClass({
ref: function(name) {
this._refs = this._refs || {};
return (el) => {
this._refs[name] = el;
};
},
componentDidMount: function() {
const {
datasets = [],
labels = 0,
stacked = false,
xAxe = false,
yAxe = false,
legend = false,
max = 100,
min = 0
} = this.props;
const _labels = !Array.isArray(labels)
? buildArray(labels).map((v, i) => '')
: labels;
this._chart = new Chart(this._refs.component, {
type: 'whisker',
responsive: true,
options: {
scales: {
xAxes: [{
barPercentage: 1.0,
categoryPercentage: 1.0
}],
yAxes: [{
ticks: {
min: min,
max: max
}
}]
},
legend: {
display: true
}
},
data: {
labels: _labels,
datasets: datasets
}
});
},
componentWillReceiveProps: function(nextProps) {
const {
datasets = [],
labels = 0,
max,
min
} = this.props;
this._chart.data.datasets = datasets;
this._chart.data.labels = buildArray(labels).map((v, i) => '');
this._chart.config.options.scales.yAxes[0].ticks.max = max;
this._chart.config.options.scales.yAxes[0].ticks.min = min;
this._chart.update(0);
},
render: function() {
return (
<canvas
ref={this.ref('component')}
width='400'
height='400'
/>
);
}
});
/*
* datasets[{altbackgr, back, data[{max, min, ...}, label]}]
*/

View File

@ -0,0 +1,34 @@
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
user: 'rgb(255, 99, 132)',
sys: 'rgb(255, 159, 64)',
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = {},
windowSize
}) => {
const datasets = ['perc'].map((key) => {
return {
label: key,
backgroundColor: colors[key],
altBackgroundColor: colors['alt'],
data: buildArray(windowSize).map((v, i) => ((data[i] || {})[key] || { firstQuartile: 0, thirdQuartile: 0, median: 0, max: 0, min: 0 })).reverse()
};
});
return (
<Chart
datasets={datasets}
stacked={true}
labels={datasets[0].data.length}
legend={true}
/>
);
};

View File

@ -0,0 +1,29 @@
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = [],
windowSize
}) => {
const datasets = [{
label: 'disk',
backgroundColor: colors['perc'],
altBackgroundColor: colors['alt'],
data: buildArray(windowSize).map((v, i) => ((data[i] || {})['perc'] || { firstQuartile: 0, thirdQuartile: 0, median: 0, max: 0, min: 0 })).reverse()
}];
return (
<Chart
datasets={datasets}
labels={datasets[0].data.length}
legend={true}
/>
);
};

View File

@ -0,0 +1,8 @@
module.exports = {
CPU: require('./cpu'),
cpu: require('./cpu'),
Mem: require('./mem'),
mem: require('./mem'),
Disk: require('./disk'),
disk: require('./disk')
};

View File

@ -0,0 +1,54 @@
const first = require('lodash.first');
const get = require('lodash.get');
const buildArray = require('build-array');
const Chart = require('./base');
const React = require('react');
const colors = {
perc: 'rgba(54, 74, 205, 0.2)',
alt: 'rgba(245, 93, 93, 0.2)'
};
module.exports = ({
data = [],
windowSize,
aggregate = false,
name = 'mem',
max = 100
}) => {
const datasets = [{
label: name,
backgroundColor: colors.perc,
altBackgroundColor: colors.alt,
data: buildArray(windowSize).map((v, i) => {
const sample = get(data, `[${i}].perc`, {
firstQuartile: 0,
thirdQuartile: 0,
median: 0,
max: 0,
min: 0
});
return Object.keys(sample).reduce((sum, name) => {
// from bytes to mb
return {
...sum,
[name]: (sample[name] > 0)
? sample[name] / 1000000
: sample[name]
};
}, {});
}).reverse()
}];
return (
<Chart
datasets={datasets}
stacked={aggregate}
labels={first(datasets).data.length}
legend={true}
max={max/1000000}
/>
);
};

View File

@ -0,0 +1,221 @@
'use strict';
module.exports = function(Chart) {
var globalOpts = Chart.defaults.global;
globalOpts.elements.rectangle = {
backgroundColor: globalOpts.defaultColor,
borderWidth: 0,
borderColor: globalOpts.defaultColor,
borderSkipped: 'bottom'
};
function isVertical(bar) {
return bar._view.width !== undefined;
}
/**
* Helper function to get the bounds of the bar regardless of the orientation
* @private
* @param bar {Chart.Element.Rectangle} the bar
* @return {Bounds} bounds of the bar
*/
function getBarBounds(bar) {
var vm = bar._view;
var x1, x2, y1, y2;
if (isVertical(bar)) {
// vertical
var halfWidth = vm.width / 2;
x1 = vm.x - halfWidth;
x2 = vm.x + halfWidth;
y1 = Math.min(vm.y, vm.base);
y2 = Math.max(vm.y, vm.base);
} else {
// horizontal bar
var halfHeight = vm.height / 2;
x1 = Math.min(vm.x, vm.base);
x2 = Math.max(vm.x, vm.base);
y1 = vm.y - halfHeight;
y2 = vm.y + halfHeight;
}
return {
left: x1,
top: y1,
right: x2,
bottom: y2
};
}
Chart.elements.Whisker = Chart.Element.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var halfWidth = vm.width / 2,
leftX = vm.x - halfWidth,
rightX = vm.x + halfWidth,
top = vm.base - (vm.base - vm.y),
halfStroke = vm.borderWidth / 2;
// Canvas doesn't allow us to stroke inside the width so we can
// adjust the sizes to fit if we're setting a stroke on the line
if (vm.borderWidth) {
leftX += halfStroke;
rightX -= halfStroke;
top += halfStroke;
}
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = vm.borderWidth;
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[leftX, vm.base],
[leftX, top],
[rightX, top],
[rightX, vm.base]
];
// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(vm.borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
}
function cornerAt(index) {
return corners[(startCorner + index) % 4];
}
// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
ctx.lineTo(corner[0], corner[1]);
}
ctx.fill();
if (vm.borderWidth) {
ctx.stroke();
}
ctx.closePath();
// Median line
ctx.beginPath();
ctx.moveTo(leftX, vm.median);
ctx.lineTo(rightX, vm.median);
ctx.lineWidth = 2;
// set line color
ctx.strokeStyle = 'rgb(54, 74, 205)';
ctx.stroke();
ctx.closePath();
// Top Whisker
// if (smaller than 5px then do not draw)
if (vm.median - vm.maxV > 10) {
ctx.beginPath();
ctx.moveTo((rightX - leftX) / 2 + leftX, vm.median - 1);
ctx.lineTo((rightX - leftX) / 2 + leftX, vm.maxV);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(245, 93, 93)';
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.arc((rightX - leftX) / 2 + leftX, vm.maxV, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(245, 93, 93)';
ctx.fill();
}
// Bottom Whisker
// if (smaller than 5px then do not draw)
if (vm.minV - vm.median > 10) {
ctx.beginPath();
ctx.moveTo((rightX - leftX) / 2 + leftX, vm.median + 1);
ctx.lineTo((rightX - leftX) / 2 + leftX, vm.minV);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgb(245, 93, 93)';
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.arc((rightX - leftX) / 2 + leftX, vm.minV, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'rgb(245, 93, 93)';
ctx.fill();
}
},
height: function() {
var vm = this._view;
return vm.base - vm.y;
},
inRange: function(mouseX, mouseY) {
var inRange = false;
if (this._view) {
var bounds = getBarBounds(this);
inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom;
}
return inRange;
},
inLabelRange: function(mouseX, mouseY) {
var me = this;
if (!me._view) {
return false;
}
var inRange = false;
var bounds = getBarBounds(me);
if (isVertical(me)) {
inRange = mouseX >= bounds.left && mouseX <= bounds.right;
} else {
inRange = mouseY >= bounds.top && mouseY <= bounds.bottom;
}
return inRange;
},
inXRange: function(mouseX) {
var bounds = getBarBounds(this);
return mouseX >= bounds.left && mouseX <= bounds.right;
},
inYRange: function(mouseY) {
var bounds = getBarBounds(this);
return mouseY >= bounds.top && mouseY <= bounds.bottom;
},
getCenterPoint: function() {
var vm = this._view;
var x, y;
if (isVertical(this)) {
x = vm.x;
y = (vm.y + vm.base) / 2;
} else {
x = (vm.x + vm.base) / 2;
y = vm.y;
}
return {x: x, y: y};
},
getArea: function() {
var vm = this._view;
return vm.width * Math.abs(vm.y - vm.base);
},
tooltipPosition: function() {
var vm = this._view;
return {
x: vm.x,
y: vm.y
};
}
});
};

View File

@ -0,0 +1,44 @@
const ReactDOM = require('react-dom');
const React = require('react');
const Store = require('./store');
const nes = require('nes/dist/client');
const {
Client
} = nes;
const client = new Client(`ws://${document.location.host}`);
const store = Store({
windowSize: 20,
ws: client
});
client.connect((err) => {
if (err) {
throw err;
}
store.getState().wsReady = true;
render();
});
const render = () => {
const Root = require('./root');
if (!store.getState().wsReady) {
return;
}
ReactDOM.render(
<Root store={store} />,
document.getElementById('root')
);
};
render();
if (module.hot) {
module.hot.accept('./root', render);
}

View File

@ -0,0 +1,156 @@
const get = require('lodash.get');
const React = require('react');
const buildArray = require('build-array');
const ReactRedux = require('react-redux');
const Chart = require('./chart');
const actions = require('./actions');
const {
connect
} = ReactRedux;
const {
subscribe,
unsubscribe,
getTree
} = actions;
const Job = React.createClass({
componentWillMount: function() {
this.props.subscribe(this.props.name);
},
componentWillUnmount: function() {
this.props.unsubscribe(this.props.name);
},
render: function() {
const {
data,
instances = [],
name,
windowSize
} = this.props;
if (!data) {
return null;
}
if (instances.length < 2) {
return null;
}
let max = 0;
const charts = ['aggregate'].concat(instances.map((i) => {
return `instances.${i}`;
})).map((path) => {
const set = data.mem.map((sample) => {
const perc = get(sample, path);
if (perc.max > max) {
max = perc.max;
}
return {
perc: perc,
when: sample.when
};
});
return {
key: path,
data: set,
aggregate: path === 'aggregate',
windowSize
};
}).map((ctx, i, arr) => {
const chart = React.createElement(Chart.mem, {
data: ctx.data,
aggregate: ctx.aggregate,
windowSize: ctx.windowSize,
max: max,
name: ctx.key
});
return (
<div
key={ctx.key}
className={`col-xs-${12 / arr.length}`}
>
{chart}
</div>
);
});
return (
<div>
<p>{name}</p>
<div className='row'>
{charts}
</div>
</div>
);
}
});
const Jobs = React.createClass({
componentWillMount: function() {
this.props.getTree();
},
render: function() {
const {
subscribe,
unsubscribe,
tree = {},
data = {},
windowSize
} = this.props;
const jobs = Object.keys(tree).map((jobName) => {
return (
<Job
key={jobName}
windowSize={windowSize}
data={data[jobName]}
instances={Object.keys(tree[jobName])}
subscribe={subscribe}
unsubscribe={unsubscribe}
name={jobName}
/>
);
})
return (
<div>
{jobs}
</div>
);
}
});
const mapStateToProps = (state) => {
return {
tree: state.tree,
data: state.data,
windowSize: state.windowSize
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
subscribe: (name) => {
return dispatch(subscribe(name));
},
unsubscribe: (name) => {
return dispatch(unsubscribe(name));
},
getTree: () => {
return dispatch(getTree());
}
}
};
module.exports = connect(
mapStateToProps,
mapDispatchToProps,
)(Jobs);

View File

@ -0,0 +1,24 @@
const React = require('react');
const ReactHotLoader = require('react-hot-loader');
const ReactRedux = require('react-redux');
const Matrix = require('./matrix');
const {
AppContainer
} = ReactHotLoader;
const {
Provider
} = ReactRedux;
module.exports = ({
store
}) => {
return (
<AppContainer>
<Provider store={store}>
<Matrix />
</Provider>
</AppContainer>
);
};

View File

@ -0,0 +1,21 @@
const createLogger = require('redux-logger');
const promiseMiddleware = require('redux-promise-middleware').default;
const thunk = require('redux-thunk').default;
const redux = require('redux');
const reducer = require('./actions');
const {
createStore,
compose,
applyMiddleware
} = redux;
module.exports = (state = Object.freeze({})) => {
return createStore(reducer, state, applyMiddleware(
createLogger({
predicate: (getState, action) => action.type !== 'UPDATE_STATS'
}),
promiseMiddleware(),
thunk
));
};

View File

@ -0,0 +1,276 @@
const whiskerElement = require('./element.whisker');
module.exports = function(Chart) {
whiskerElement(Chart);
var helpers = Chart.helpers;
Chart.defaults.whisker = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'category',
// Specific to Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}],
yAxes: [{
type: 'linear'
}]
}
};
Chart.controllers.whisker = Chart.DatasetController.extend({
dataElementType: Chart.elements.Whisker,
initialize: function(chart, datasetIndex) {
Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);
// Use this to indicate that this is a bar dataset.
this.getMeta().bar = true;
},
// Get the number of datasets that display bars. We use this to correctly calculate the bar width
getBarCount: function() {
var me = this;
var barCount = 0;
helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {
var meta = me.chart.getDatasetMeta(datasetIndex);
if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) {
++barCount;
}
}, me);
return barCount;
},
update: function(reset) {
var me = this;
helpers.each(me.getMeta().data, function(rectangle, index) {
me.updateElement(rectangle, index, reset);
}, me);
},
updateElement: function(rectangle, index, reset) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var yScale = me.getScaleForId(meta.yAxisID);
var scaleBase = yScale.getBasePixel();
var rectangleElementOptions = me.chart.options.elements.rectangle;
var custom = rectangle.custom || {};
var dataset = me.getDataset();
rectangle._xScale = xScale;
rectangle._yScale = yScale;
rectangle._datasetIndex = me.index;
rectangle._index = index;
var ruler = me.getRuler(index);
rectangle._model = {
x: me.calculateBarX(index, me.index, ruler),
y: reset ? scaleBase : me.boxTopValue(index, me.index),
// Tooltip
label: me.chart.data.labels[index],
datasetLabel: dataset.label,
// Appearance
median: reset ? scaleBase : me.medianValue(me.index, index),
maxV: reset ? scaleBase : me.maxValue(me.index, index),
minV: reset ? scaleBase : me.minValue(me.index, index),
base: reset ? scaleBase : me.boxBottomValue(me.index, index),
width: me.calculateBarWidth(ruler),
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(me.stddev(me.index, index) > 3 ? dataset.altBackgroundColor : dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
};
rectangle.pivot();
},
stddev: function(datasetIndex, index) {
var me = this;
var obj = me.getDataset().data[index];
var value = Number(obj.stddev);
return value;
},
minValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.min);
return yScale.getPixelForValue(value);
},
maxValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.max);
return yScale.getPixelForValue(value);
},
medianValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.median);
return yScale.getPixelForValue(value);
},
boxBottomValue: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.firstQuartile);
return yScale.getPixelForValue(value);
},
boxTopValue: function(index, datasetIndex) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var obj = me.getDataset().data[index];
var value = Number(obj.thirdQuartile);
return yScale.getPixelForValue(value);
},
getRuler: function(index) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var datasetCount = me.getBarCount();
var tickWidth;
if (xScale.options.type === 'category') {
tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);
} else {
// Average width
tickWidth = xScale.width / xScale.ticks.length;
}
var categoryWidth = tickWidth * xScale.options.categoryPercentage;
var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
var fullBarWidth = categoryWidth / datasetCount;
if (xScale.ticks.length !== me.chart.data.labels.length) {
var perc = xScale.ticks.length / me.chart.data.labels.length;
fullBarWidth = fullBarWidth * perc;
}
var barWidth = fullBarWidth * xScale.options.barPercentage;
var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);
return {
datasetCount: datasetCount,
tickWidth: tickWidth,
categoryWidth: categoryWidth,
categorySpacing: categorySpacing,
fullBarWidth: fullBarWidth,
barWidth: barWidth,
barSpacing: barSpacing
};
},
calculateBarWidth: function(ruler) {
var xScale = this.getScaleForId(this.getMeta().xAxisID);
if (xScale.options.barThickness) {
return xScale.options.barThickness;
}
return ruler.barWidth;
},
// Get bar index from the given dataset index accounting for the fact that not all bars are visible
getBarIndex: function(datasetIndex) {
var barIndex = 0;
var meta;
var j;
for (j = 0; j < datasetIndex; ++j) {
meta = this.chart.getDatasetMeta(j);
if (meta.bar && this.chart.isDatasetVisible(j)) {
++barIndex;
}
}
return barIndex;
},
calculateBarX: function(index, datasetIndex, ruler) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var barIndex = me.getBarIndex(datasetIndex);
var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;
return leftTick +
(ruler.barWidth / 2) +
ruler.categorySpacing +
(ruler.barWidth * barIndex) +
(ruler.barSpacing / 2) +
(ruler.barSpacing * barIndex);
},
draw: function(ease) {
var me = this;
var easingDecimal = ease || 1;
var metaData = me.getMeta().data;
var dataset = me.getDataset();
var i, len;
for (i = 0, len = metaData.length; i < len; ++i) {
var d = dataset.data[i];
if (d !== null && d !== undefined && typeof d === 'object' && !isNaN(d.median)) {
metaData[i].transition(easingDecimal).draw();
}
}
},
setHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor));
model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor));
model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth);
},
removeHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
var rectangleElementOptions = this.chart.options.elements.rectangle;
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor);
model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor);
model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth);
}
});
};

View File

@ -1,17 +0,0 @@
module.exports = [{
register: require('good'),
options: {
reporters: {
console: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{
response: '*',
log: '*'
}]
}, {
module: 'good-console'
}, 'stdout']
}
}
}];

View File

@ -11,6 +11,8 @@ server.connection({
port: 8000 port: 8000
}); });
epimetheus.instrument(server);
server.register(plugins, (err) => { server.register(plugins, (err) => {
if (err) { if (err) {
throw err; throw err;
@ -20,8 +22,6 @@ server.register(plugins, (err) => {
routes[name](server); routes[name](server);
}); });
epimetheus.instrument(server);
server.start((err) => { server.start((err) => {
server.connections.forEach((conn) => { server.connections.forEach((conn) => {
console.log(`started at: ${conn.info.uri}`); console.log(`started at: ${conn.info.uri}`);

View File

@ -0,0 +1,103 @@
const relativeDate = require('relative-date');
const statistics = require('simple-statistics');
const prometheus = require('../../scripts/prometheus');
const async = require('async');
const cdm = {};
const calc = (sample) => {
return {
firstQuartile: statistics.quantile(sample, 0.25),
median: statistics.median(sample),
thirdQuartile: statistics.quantile(sample, 0.75),
max: statistics.max(sample),
min: statistics.min(sample),
stddev: statistics.sampleStandardDeviation(sample)
};
};
const getMem = ({
job
}, fn) => {
prometheus.query({
query: [`node_memory_heap_used_bytes{job="${job}"}`]
}).then((res) => {
if (!res || !res[job]) {
return null
}
const aggregate = calc(Object.keys(res[job]).map((inst) => {
return Number(res[job][inst].node_memory_heap_used_bytes[1]);
}));
const instances = Object.keys(res[job]).reduce((sum, inst) => {
return Object.assign(sum, {
[inst]: calc([Number(res[job][inst].node_memory_heap_used_bytes[1])])
})
}, {});
return {
raw: res[job],
aggregate,
instances
};
}).then((res) => {
return fn(null, res);
}).catch((err) => {
return fn(err);
});
};
const getStats = (ctx, fn) => {
async.parallel({
mem: async.apply(getMem, ctx)
}, fn);
};
module.exports = (server) => ({
on: (job) => {
console.log('on', job);
if (cdm[job] && (cdm[job].sockets > 0)) {
cdm[job].sockets += 1;
return;
}
let messageId = 0;
const update = () => {
console.log(`publishing /stats/${job}/${messageId += 1}`);
getStats({
job: job
}, (err, stats) => {
if (err) {
return console.error(err);
}
server.publish(`/stats/${job}`, {
when: new Date().getTime(),
stats
});
});
};
cdm[job] = {
interval: setInterval(update, 1000),
sockets: 1
};
},
off: (job) => {
console.log('off', job);
if (!(cdm[job].sockets -= 1)) {
clearInterval(cdm[job].interval);
}
}
});
module.exports.tree = (ctx) => {
return prometheus.tree({
query: ['node_memory_heap_used_bytes']
});
};

View File

@ -0,0 +1,30 @@
const webpack = require('webpack');
const path = require('path');
const cfg = require('../webpack.config.js');
module.exports = [
require('inert'),
require('nes'), {
register: require('good'),
options: {
reporters: {
console: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{
response: '*',
log: '*'
}]
}, {
module: 'good-console'
}, 'stdout']
}
}
}, {
register: require('hapi-webpack-dev-plugin'),
options: {
compiler: webpack(cfg),
devIndex: path.join(__dirname, '../static')
}
}];

View File

@ -0,0 +1,12 @@
const path = require('path');
module.exports = (server) => {
server.route({
method: 'GET',
path: '/',
handler: (request, reply) => {
reply.file(path.join(__dirname, '../../../static/index.html'));
}
});
};

View File

@ -1,5 +1,4 @@
const prettyHrtime = require('pretty-hrtime'); const prettyHrtime = require('pretty-hrtime');
const clone = require('clone');
// leak example from https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/ // leak example from https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/
let theLeak = null; let theLeak = null;
@ -20,10 +19,10 @@ module.exports = (server) => {
const start = process.hrtime(); const start = process.hrtime();
anotherLeak.push({ anotherLeak.push({
longStr: new Array(Math.ceil(anotherLeak.length * 1.5)).map((v, i) => i) longStr: new Array(Math.ceil(anotherLeak.length * 2)).map((v, i) => i)
}); });
console.log('mem-fast %d', Math.ceil(anotherLeak.length * 1.5)); console.log('mem-fast %d', Math.ceil(anotherLeak.length * 2));
const end = process.hrtime(start); const end = process.hrtime(start);
reply(prettyHrtime(end)); reply(prettyHrtime(end));

View File

@ -0,0 +1,26 @@
const Metric = require('../metric');
module.exports = (server) => {
const metric = Metric(server);
server.route({
method: 'GET',
path: '/job-tree',
config: {
handler: (request, reply) => reply(Metric.tree())
}
});
server.subscription('/stats/{id}', {
onSubscribe: (socket, path, params, next) => {
console.log('onSubscribe');
metric.on(params.id);
next();
},
onUnsubscribe: (socket, path, params, next) => {
console.log('onUnsubscribe');
metric.off(params.id);
next();
}
});
};

View File

@ -0,0 +1,18 @@
const Pkg = require('../../../package.json');
const internals = {
response: {
version: Pkg.version
}
};
module.exports = (server) => {
server.route({
method: 'GET',
path: '/ops/version',
config: {
description: 'Returns the version of the server',
handler: (request, reply) => reply(internals.response)
}
});
};

View File

@ -0,0 +1,58 @@
const webpack = require('webpack');
const path = require('path');
const config = {
debug: true,
devtool: 'source-map',
context: path.join(__dirname, './client'),
app: path.join(__dirname, './client/index.js'),
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'react-hot-loader/patch',
'./index.js'
],
output: {
path: path.join(__dirname, '../static'),
publicPath: '/static/',
filename: 'bundle.js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.ProvidePlugin({
'd3': 'd3'
})
],
module: {
loaders: [{
test: /js?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, './client')
],
loaders: ['babel-loader']
}, {
test: /\.json?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, './client')
],
loaders: ['json']
}]
}
};
const devServer = {
hot: true,
compress: true,
lazy: false,
publicPath: config.output.publicPath,
historyApiFallback: {
index: '../static/index.html'
}
};
module.exports = Object.assign({
devServer
}, config);

View File

@ -0,0 +1,973 @@
<!doctype html>
<html lang='en-US'>
<head>
<title>React Boilerplate</title>
<link rel="stylesheet" type="text/css" href="https://necolas.github.io/normalize.css/latest/normalize.css" />
<style>
.container-fluid,
.container {
margin-right: auto;
margin-left: auto;
}
.container-fluid {
padding-right: 2rem;
padding-left: 2rem;
}
.row {
box-sizing: border-box;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -0.5rem;
margin-left: -0.5rem;
}
.row.reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.col.reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.col-xs,
.col-xs-1,
.col-xs-2,
.col-xs-3,
.col-xs-4,
.col-xs-5,
.col-xs-6,
.col-xs-7,
.col-xs-8,
.col-xs-9,
.col-xs-10,
.col-xs-11,
.col-xs-12,
.col-xs-offset-0,
.col-xs-offset-1,
.col-xs-offset-2,
.col-xs-offset-3,
.col-xs-offset-4,
.col-xs-offset-5,
.col-xs-offset-6,
.col-xs-offset-7,
.col-xs-offset-8,
.col-xs-offset-9,
.col-xs-offset-10,
.col-xs-offset-11,
.col-xs-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-xs {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-xs-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-xs-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-xs-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-xs-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-xs-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-xs-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-xs-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-xs-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-xs-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-xs-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-xs-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-xs-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-xs-offset-0 {
margin-left: 0;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.start-xs {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-xs {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-xs {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-xs {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-xs {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-xs {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-xs {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-xs {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-xs {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-xs {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
@media only screen and (min-width: 48em) {
.container {
width: 49rem;
}
.col-sm,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm-offset-0,
.col-sm-offset-1,
.col-sm-offset-2,
.col-sm-offset-3,
.col-sm-offset-4,
.col-sm-offset-5,
.col-sm-offset-6,
.col-sm-offset-7,
.col-sm-offset-8,
.col-sm-offset-9,
.col-sm-offset-10,
.col-sm-offset-11,
.col-sm-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-sm {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-sm-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-sm-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-sm-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-sm-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-sm-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-sm-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-sm-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-sm-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-sm-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-sm-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-sm-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-sm-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-sm-offset-0 {
margin-left: 0;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.start-sm {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-sm {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-sm {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-sm {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-sm {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-sm {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-sm {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-sm {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-sm {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-sm {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
@media only screen and (min-width: 64em) {
.container {
width: 65rem;
}
.col-md,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12,
.col-md-offset-0,
.col-md-offset-1,
.col-md-offset-2,
.col-md-offset-3,
.col-md-offset-4,
.col-md-offset-5,
.col-md-offset-6,
.col-md-offset-7,
.col-md-offset-8,
.col-md-offset-9,
.col-md-offset-10,
.col-md-offset-11,
.col-md-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-md {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-md-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-md-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-md-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-md-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-md-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-md-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-md-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-md-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-md-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-md-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-md-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-md-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-md-offset-0 {
margin-left: 0;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.start-md {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-md {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-md {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-md {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-md {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-md {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-md {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-md {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-md {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-md {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
@media only screen and (min-width: 75em) {
.container {
width: 76rem;
}
.col-lg,
.col-lg-1,
.col-lg-2,
.col-lg-3,
.col-lg-4,
.col-lg-5,
.col-lg-6,
.col-lg-7,
.col-lg-8,
.col-lg-9,
.col-lg-10,
.col-lg-11,
.col-lg-12,
.col-lg-offset-0,
.col-lg-offset-1,
.col-lg-offset-2,
.col-lg-offset-3,
.col-lg-offset-4,
.col-lg-offset-5,
.col-lg-offset-6,
.col-lg-offset-7,
.col-lg-offset-8,
.col-lg-offset-9,
.col-lg-offset-10,
.col-lg-offset-11,
.col-lg-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.col-lg {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
-ms-flex-preferred-size: 0;
flex-basis: 0;
max-width: 100%;
}
.col-lg-1 {
-ms-flex-preferred-size: 8.33333333%;
flex-basis: 8.33333333%;
max-width: 8.33333333%;
}
.col-lg-2 {
-ms-flex-preferred-size: 16.66666667%;
flex-basis: 16.66666667%;
max-width: 16.66666667%;
}
.col-lg-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
.col-lg-4 {
-ms-flex-preferred-size: 33.33333333%;
flex-basis: 33.33333333%;
max-width: 33.33333333%;
}
.col-lg-5 {
-ms-flex-preferred-size: 41.66666667%;
flex-basis: 41.66666667%;
max-width: 41.66666667%;
}
.col-lg-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.col-lg-7 {
-ms-flex-preferred-size: 58.33333333%;
flex-basis: 58.33333333%;
max-width: 58.33333333%;
}
.col-lg-8 {
-ms-flex-preferred-size: 66.66666667%;
flex-basis: 66.66666667%;
max-width: 66.66666667%;
}
.col-lg-9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%;
}
.col-lg-10 {
-ms-flex-preferred-size: 83.33333333%;
flex-basis: 83.33333333%;
max-width: 83.33333333%;
}
.col-lg-11 {
-ms-flex-preferred-size: 91.66666667%;
flex-basis: 91.66666667%;
max-width: 91.66666667%;
}
.col-lg-12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
.col-lg-offset-0 {
margin-left: 0;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.start-lg {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: start;
}
.center-lg {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.end-lg {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: end;
}
.top-lg {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.middle-lg {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.bottom-lg {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.around-lg {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.between-lg {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.first-lg {
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
}
.last-lg {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
}
</style>
</head>
<body>
<div id='root'></div>
<script src='/static/bundle.js'></script>
</body>
</html>

File diff suppressed because it is too large Load Diff