From 1bc5860e7df6352beafce6e4148ccc34c10c5145 Mon Sep 17 00:00:00 2001 From: jlampar Date: Tue, 8 May 2018 16:07:45 +0200 Subject: [PATCH 01/22] adding heatmap and graph_type --- .../priv/src/actions/CollectingActions.js | 6 +- .../priv/src/actions/ExploringActions.js | 20 +- .../priv/src/actions/MonitoringActions.js | 12 +- .../priv/src/actions/TracingActions.js | 10 +- .../src/components/functions/Functions.jsx | 14 +- .../src/components/monitoring/Graph/Graph.jsx | 43 +- .../components/monitoring/Graph/Grid/Grid.jsx | 311 ++++++++++++++ .../monitoring/Graph/Grid/gridUtils.js | 395 ++++++++++++++++++ .../monitoring/GraphPanel/GraphPanel.jsx | 16 +- .../GraphPanelHeading/GraphPanelHeading.jsx | 8 +- .../monitoring/Monitoring/Monitoring.jsx | 8 +- .../tracing/CallsInput/CallsInput.jsx | 6 +- .../tracing/CallsPanel/CallsPanel.jsx | 12 +- .../components/tracing/CallsRow/CallsRow.jsx | 6 +- .../tracing/CallsSwitch/CallsSwitch.jsx | 6 +- .../tracing/CallsTable/CallsTable.jsx | 6 +- .../components/tracing/Tracing/Tracing.jsx | 8 +- .../MonitoringContainer.jsx | 8 +- .../TracingContainer/TracingContainer.jsx | 6 +- apps/xprof_gui/priv/src/utils/ActionsUtils.js | 34 +- apps/xprof_gui/src/xprof_gui_rest.erl | 17 +- apps/xprof_gui/test/xprof_http_e2e_SUITE.erl | 21 +- 22 files changed, 882 insertions(+), 91 deletions(-) create mode 100644 apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx create mode 100644 apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/gridUtils.js diff --git a/apps/xprof_gui/priv/src/actions/CollectingActions.js b/apps/xprof_gui/priv/src/actions/CollectingActions.js index ce017111..c606965f 100644 --- a/apps/xprof_gui/priv/src/actions/CollectingActions.js +++ b/apps/xprof_gui/priv/src/actions/CollectingActions.js @@ -29,11 +29,13 @@ export const getMonitoredFunctions = () => async (dispatch, getState) => { if (error) { console.log('ERROR: ', error); } else if (!isEqual(mfas, json)) { - const newMfas = json.filter(mfa => !mfas.map(f => f[3]).includes(mfa[3])); + const newMfas = json + .filter(mfa => !mfas.map(f => f.query) + .includes(mfa.query)); const newControls = newMfas.reduce( (control, mfa) => ({ ...control, - [mfa[3]]: { + [mfa.query]: { threshold: undefined, limit: undefined, collecting: false, diff --git a/apps/xprof_gui/priv/src/actions/ExploringActions.js b/apps/xprof_gui/priv/src/actions/ExploringActions.js index b16c3c35..9d1e1c61 100644 --- a/apps/xprof_gui/priv/src/actions/ExploringActions.js +++ b/apps/xprof_gui/priv/src/actions/ExploringActions.js @@ -22,13 +22,13 @@ export const calleeClick = callee => (dispatch) => { dispatch(startMonitoringFunction(callee)); }; -export const getCallees = mfa => async (dispatch) => { - const name = mfa[3]; +export const getCallees = m => async (dispatch) => { + const name = m.query; const { json, error } = await XProf.getFunctionsCallees( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], ); if (error) console.log('ERROR'); @@ -39,13 +39,13 @@ export const getCallees = mfa => async (dispatch) => { export const getCalleesForFunctions = mfas => async (dispatch) => { const callees = {}; - await Promise.all(mfas.map(async (mfa) => { - const fun = mfa[3]; + await Promise.all(mfas.map(async (m) => { + const fun = m.query; const { json, error } = await XProf.getFunctionsCallees( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], ); if (error) console.log('ERROR'); diff --git a/apps/xprof_gui/priv/src/actions/MonitoringActions.js b/apps/xprof_gui/priv/src/actions/MonitoringActions.js index db26b895..6f74908c 100644 --- a/apps/xprof_gui/priv/src/actions/MonitoringActions.js +++ b/apps/xprof_gui/priv/src/actions/MonitoringActions.js @@ -12,14 +12,18 @@ const stopMonitoringFunctionError = mfas => ({ mfas, }); -export const stopMonitoringFunction = mfa => async (dispatch, getState) => { +export const stopMonitoringFunction = m => async (dispatch, getState) => { const state = getState(); const mfas = getMfas(state); - const mfasReduced = mfas.filter(m => m[3] !== mfa[3]); + const mfasReduced = mfas.filter(f => f.query !== m.query); dispatch(stopMonitoringFunctionRequest(mfasReduced)); - const { error } = await XProf.stopMonitoringFunction(mfa[0], mfa[1], mfa[2]); + const { error } = await XProf.stopMonitoringFunction( + m.mfa[0], + m.mfa[1], + m.mfa[2], + ); if (error) { console.log('ERROR: ', error); dispatch(stopMonitoringFunctionError(mfas)); @@ -32,7 +36,7 @@ export const startMonitoringFunction = functionName => async ( ) => { const state = getState(); const mfas = getMfas(state); - const isMonitored = mfas.filter(mfa => mfa[3] === functionName).length; + const isMonitored = mfas.filter(mfa => mfa.query === functionName).length; if (!isMonitored) { const { error } = await XProf.startMonitoringFunction(functionName); diff --git a/apps/xprof_gui/priv/src/actions/TracingActions.js b/apps/xprof_gui/priv/src/actions/TracingActions.js index 802391cf..ba9372c0 100644 --- a/apps/xprof_gui/priv/src/actions/TracingActions.js +++ b/apps/xprof_gui/priv/src/actions/TracingActions.js @@ -22,7 +22,7 @@ const updateLastCallsForFunction = (functionName, sortedCalls) => ({ export const toggleExpandItem = (mfa, item) => (dispatch, getState) => { const state = getState(); - const functionName = mfa[3]; + const functionName = mfa.query; const lastCallsForFunction = getLastCallsForFunction(state, functionName); const updatedItems = lastCallsForFunction.sort.items.map((call) => { @@ -40,7 +40,7 @@ export const toggleExpandItem = (mfa, item) => (dispatch, getState) => { export const toggleCallsTracing = mfa => async (dispatch, getState) => { const state = getState(); - const functionName = mfa[3]; + const functionName = mfa.query; const control = getFunctionControl(state, functionName); const nextControl = await determineNextControlSwitch(control, mfa); dispatch(setCallsControl({ [functionName]: nextControl })); @@ -48,7 +48,7 @@ export const toggleCallsTracing = mfa => async (dispatch, getState) => { export const handleThresholdChange = (mfa, value) => (dispatch, getState) => { const state = getState(); - const functionName = mfa[3]; + const functionName = mfa.query; const { limit, collecting } = getFunctionControl(state, functionName); const nextControl = { threshold: value, @@ -60,7 +60,7 @@ export const handleThresholdChange = (mfa, value) => (dispatch, getState) => { export const handleLimitChange = (mfa, value) => (dispatch, getState) => { const state = getState(); - const functionName = mfa[3]; + const functionName = mfa.query; const { threshold, collecting } = getFunctionControl(state, functionName); const nextControl = { threshold, @@ -72,7 +72,7 @@ export const handleLimitChange = (mfa, value) => (dispatch, getState) => { export const sortCallsBy = (mfa, column) => (dispatch, getState) => { const state = getState(); - const functionName = mfa[3]; + const functionName = mfa.query; const lastCallsForFunction = getLastCallsForFunction(state, functionName); const order = lastCallsForFunction.sort.column === column && diff --git a/apps/xprof_gui/priv/src/components/functions/Functions.jsx b/apps/xprof_gui/priv/src/components/functions/Functions.jsx index a4508193..f60e54e6 100644 --- a/apps/xprof_gui/priv/src/components/functions/Functions.jsx +++ b/apps/xprof_gui/priv/src/components/functions/Functions.jsx @@ -5,7 +5,11 @@ import { FUNCTIONS_INTERVAL } from '../../constants'; const propTypes = { getMonitoredFunctions: PropTypes.func.isRequired, - mfas: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.any)).isRequired, + mfas: PropTypes.arrayOf(PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + })).isRequired, }; class Functions extends React.Component { @@ -27,10 +31,10 @@ class Functions extends React.Component { const { mfas } = this.props; return (
- {mfas.map((mfa, index) => ( -
- - + {mfas.map((m, index) => ( +
+ + {index < mfas.length - 1 ? (
) : null} diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx index d8aac088..8f8e108a 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx @@ -3,21 +3,42 @@ import React from 'react'; import C3Chart from 'react-c3js'; import 'c3/c3.css'; import { AXIS, DATA, GRID, POINT, TRANSITION } from '../../../constants'; +import Grid from './Grid/Grid'; const propTypes = { dps: PropTypes.arrayOf(PropTypes.object).isRequired, + type: PropTypes.string.isRequired, +}; +const Graph = ({ dps, type }) => { + const wrapper = document.getElementById('graphWrapper'); + if (type === 'grid') { + return ( +
+ {wrapper && } +
+ ); + } + return ( +
+ +
+ ); }; -const Graph = ({ dps }) => ( -
- -
-); Graph.propTypes = propTypes; export default Graph; diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx new file mode 100644 index 00000000..7fc40119 --- /dev/null +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx @@ -0,0 +1,311 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import * as d3 from 'd3'; +import { + gridData, + dataTransform, + getColor, + label, + spaceLabels, + colorTick, + renderTimeLabels, + getPositionY, + getPositionX, + getTooltip, + renderTooltip, + calcFont, +} from './gridUtils'; +// import scaleCluster from 'd3-scale-cluster'; + +class Grid extends React.Component { + constructor(props) { + super(props); + this.state = { + size: { + width: 0, + height: 0, + marginTop: 20, + marginRight: 0, + marginBottom: 70, + marginLeft: 0, + }, + }; + this.updateWindowDimensions = this.updateWindowDimensions.bind(this); + } + + componentWillMount() { + // Create tooltip - then it will be only relocated. + d3.select('#root').append('div') + .attr('id', 'tip') + .style('position', 'absolute') + .style('padding', '2px') + .style('font', '0.75em sans-serif') + .style('background', '#f5f5f5') + .style('border', '20px') + .style('border-radius', '2px') + .style('pointer-events', 'none') + .style('visibility', 'visible') + .style('opacity', 0.9) + .style('z-index', 99); + } + + componentDidMount() { + this.updateWindowDimensions(); + window.addEventListener('resize', this.updateWindowDimensions); + } + + componentWillReceiveProps(nextProps) { + /* + On every update the component is updating its size. + Without it - and upon very fast screen resizing - the event listener + was unable to catch the action and the chart was flowing outside + the container. + */ + if (nextProps !== this.props) { + this.updateWindowDimensions(); + } + } + + componentWillUpdate() { + /* + The chart is constantly redrawing itself + in a cycle of collapsing and self-creation. + */ + d3.select('#gridTable').remove(); + d3.select('#xAxis').remove(); + d3.select('#yAxis').remove(); + d3.select('#tip').style('visibility', 'hidden'); + } + + componentDidUpdate() { + const { data } = this.props; + const { size } = this.state; + const dataArray = dataTransform(data); + + const { + times, + names, + dataGrid, + } = dataArray; + + const r = names.length; + const c = times.length; + const w = size.width / c; + const h = size.height / r; + const gData = gridData(r, c, w, h); + + // Select Tooltip + const tooltip = d3.select('#tip'); + + // Append chart + const grid = d3.select('#grid') + .append('svg') + .attr('id', 'gridTable') + .attr('width', size.width) + .attr('height', size.height) + /* + The following line prevents the tooltip from being displayed + on user very rapidly moving the cursor outside the grid + - although the moving cursor outside the color-rectangle event + should do the trick, it sometimes does not. + We are double securing ourselves. + */ + .on('mouseout', () => tooltip.style('visibility', 'hidden')); + + // creating the abstract one row representation + const row = grid.selectAll('.row') + .data(gData) + .enter().append('g') + .attr('class', 'row'); + + // filling it with color-rectangles + row.selectAll('.rectangle') + .data(d => d) + .enter().append('rect') + .attr('class', 'rectangle') + .attr('id', d => d.id) + .attr('x', d => d.x) + .attr('y', d => d.y) + .attr('height', d => d.height) + .attr('width', d => d.width) + .style('fill', d => getColor(d, dataGrid)) + .style('stroke', d => getColor(d, dataGrid)) + .on('mouseover', (d) => { + const tooltipData = getTooltip(d, dataGrid); + renderTooltip(tooltip, tooltipData); + }) + .on('mouseout', () => tooltip.style('visibility', 'hidden')); + + // Append x axis + // creating the xAxis abstract container + const xAxis = d3.select('#x') + .append('svg') + .attr('id', 'xAxis') + .attr('width', size.width + 20) + .attr('height', size.marginBottom); + + /* + The axis won't be a common d3 axis as the heatmap looks better + with so-called bar-axis. Every color-rectangle will have + corresponding axis-rectangle below and on the right side of the grid. + That's why we are not calling the d3 axis method but instead we + create a single, special grid-row and grid-column. + */ + const xData = gridData(1, c, w, size.marginBottom); + + const xRow = xAxis.selectAll('.xRow') + .data(xData) + .enter() + .append('g') + .attr('class', 'xRow') + .attr('width', size.marginBottom); + + /* + In d3 it is impossible to have a nested inside the . + That is why we are creating the abstract group SVG container + with two children - each of them have to be positioned separately. + */ + const xRowG = xRow.selectAll('.xLabelSquare') + .data(d => d) + .enter().append('g') + .attr('id', d => `x${d.id}`); + + // Appending the rectangles and the labels separately. + xRowG.append('rect') + .attr('class', 'xLabelSquare') + .attr('id', d => `x${d.id}`) + .attr('x', d => d.x) + .attr('y', d => d.y) + .attr('height', d => d.height / 8) + .attr('width', d => d.width) + .style('fill', d => colorTick(spaceLabels(d.id, times.length, 10, 5))) + .style('stroke', 'white'); + xRowG.append('text') + .attr('class', 'xLabel') + .attr('id', (d, i) => `xl${i}`) + .attr('x', (d, i) => getPositionX(d, i, times)) + .attr( + 'y', + (d, i) => getPositionY(d, i, times), + ) + .style('fill', 'black') + .style('z-index', 1) + .style('font', () => calcFont('x')) + .text((d, i) => renderTimeLabels(d, i, times)); + + // Append y axis + + const yAxis = d3.select('#y') + .append('svg') + .attr('id', 'yAxis') + .attr('width', size.marginLeft) + .attr('height', size.height); + + const yData = gridData(r, 1, size.marginLeft, h); + + const yCol = yAxis.selectAll('.yCol') + .data(yData) + .enter() + .append('g') + .attr('class', 'yCol') + .attr('width', size.marginLeft); + + const yColG = yCol.selectAll('.yLabelSquare') + .data(d => d) + .enter().append('g') + .attr('id', d => `y${d.id}`); + + yColG.append('rect') + .attr('class', 'yLabelSquare') + .attr('id', d => `y${d.id}`) + .attr('x', d => d.x) + .attr('y', d => d.y) + .attr('height', d => d.height) + .attr('width', d => d.width) + .style('fill', '#f5f5f5') + .style('stroke', 'white'); + yColG.append('text') + .attr('class', 'yLabel') + .attr('x', d => d.x + 4) + .attr('y', d => d.y + (0.7 * d.height)) + .style('fill', 'black') + .style('font', () => calcFont('y')) + .text(d => names[label('y', d.id)]); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.updateWindowDimensions); + } + + updateWindowDimensions() { + const size = { ...this.state.size }; + + let heightFactor = 0; + if (window.innerWidth < 1030) { + heightFactor = 0.4; + } else { + heightFactor = 0.27; + } + + let leftFactor = 0; + let screenFactor = 0; + switch (true) { + case (window.innerWidth < 460): + leftFactor = 0.12; + screenFactor = 0.87; + break; + case (window.innerWidth < 1030): + leftFactor = 0.1; + screenFactor = 0.87; + break; + default: + leftFactor = 0.05; + screenFactor = 0.87; + break; + } + + size.width = this.props.outerWidth * screenFactor; + size.marginLeft = this.props.outerWidth * leftFactor; + size.height = size.width * heightFactor; + this.setState({ size }); + } + + render() { + return ( +
+
+
+
+
+
+
+ ); + } +} + +Grid.propTypes = { + data: PropTypes.shape({ + axes: {}, + colors: {}, + hide: [], + json: [], + keys: {}, + names: {}, + }).isRequired, + outerWidth: PropTypes.number.isRequired, +}; + +export default Grid; diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/gridUtils.js b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/gridUtils.js new file mode 100644 index 00000000..0f3878f1 --- /dev/null +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/gridUtils.js @@ -0,0 +1,395 @@ +import _ from 'lodash'; +import * as d3 from 'd3'; + +/* + Creates the desired rectangle grid data, + ready to be transformed into SVG representation. +*/ +export function gridData(rows, columns, width, height) { + const data = []; + let xpos = 1; + let ypos = 1; + + for (let row = 0; row < rows; row += 1) { + data.push([]); + for (let column = 0; column < columns; column += 1) { + data[row].push({ + x: xpos, + y: ypos, + width, + height, + id: `${row}:${column}`, + }); + xpos += width; + } + xpos = 1; + ypos += height; + } + return data; +} + +export function dataTransform(dataInput) { + const dataLocation = []; + const allValues = []; + const timeArray = []; + const allNames = []; + + /* + The input data produced by Erlang should be an array of objects. + The one of the key-value pairs should have a time moment stored. + E.g.: + + data = { + 0: { + bucket1: value1inTime0, + bucket2: value2inTime0, + bucket3: value3inTime0, + bucket4: value4inTime0, + time: time0 + }, + 1 : { + bucket1: value1inTime1, + bucket2: value2inTime1, + bucket3: value3inTime1, + bucket4: value4inTime1, + time: time1 + }, + . + . + . + N : { + ... + bucket_M: value_M_in_Time_N + ... + time: time_N + } + } + */ + Object.entries(dataInput.json).forEach((e, i) => { + const valuesRow = []; + let row = {}; + let time = 0; + + Object.entries(e[1]).forEach((f) => { + if (f[0] === 'time') { + time = f; + timeArray.push(f[1]); + } else if (f[0] !== 'memsize') { + valuesRow.push(f); + allValues.push(f[1]); + } + }); + + valuesRow.forEach((a, j) => { + row = { + row: j, + column: i, + time: time[1], + [a[0]]: a[1], // bucket-name: value + key: a[0], // key: bucket-name + }; + + dataLocation.push(row); + allNames.push(a[0]); + }); + }); + + const dataDomain = _.uniq(allValues).sort((a, b) => a - b); + + const colorRange = [ + '#ffffcc', + '#ffeda0', + '#fed976', + '#feb24c', + '#fd8d3c', + '#fc4e2a', + '#e31a1c', + '#bd0026', + '#800026', + ]; + + const colorScale = d3.scaleQuantile() + .domain(dataDomain) + .range(colorRange); + + const dataGrid = dataLocation.map((u) => { + const { key } = u; + // Adding the color field to the exisiting object. + return Object.assign(u, { color: colorScale(u[key]) }); + }); + const times = timeArray; + const names = _.uniq(allNames); + + return { + dataGrid, times, names, + }; + // dataGrid - the in-grid location of the data and the color. + // times - the unique list of sorted time values. + // names - the unique list of bucket names. +} + +export function getColor(d, arr) { + const dID = d.id.split(':'); // the dID holds the in-grid location + const dataRow = parseInt(dID[0], 10); // is representing the bucket + const dataCol = parseInt(dID[1], 10); // is representing the time moment + let hue = null; + arr.forEach((dg) => { + if (dg.column === dataCol && dg.row === dataRow) { + const { color } = dg; + hue = color; + } + }); + return hue; +} + +export function label(direction, id) { + const splitID = id.split(':'); + /* + Upon informing the function about what axis we are talking about, + it is returning the label index. + */ + if (direction === 'x') { + return splitID[1]; + } else if (direction === 'y') { + return splitID[0]; + } + return ''; +} + +export function spaceLabels(id, len, j, zero) { + /* + This function deals only with the x-axis. + It: + 1. Thinks about all the possible labels. + 2. Gets the desired number of labels. + 3. Figures out how the desired number of labels + should be evenly spaced in the whole labels chain + (e.g. how evenly space the 10 labels in a 120 labels chain). + + Its purpose is to store that information + and upon receiving the label to be rendered, + deciding whether it is the desired label or not. + */ + const splitID = parseInt(id.split(':')[1], 10); + const p = Math.ceil(len / j); + const ticks = []; + + for (let i = zero; i < len; i += p) { + ticks.push(i); + } + + if (ticks.includes(splitID)) { + return true; + } + + return false; +} + +export function colorTick(condition) { + if (condition) { + return 'darkgrey'; + } + return '#f5f5f5'; +} + +function printTick(l, condition) { + if (condition) { + return l; + } + return ''; +} + +export function renderTimeLabels(d, i, time) { + if (window.innerWidth < 690) { + /* + When the screen is very narrow, + we don't want the labels to be resized because they are already very long. + What we do is we're splitting the labels: + - the hour is at the begining of the axis, + - the minutes and the seconds are under their label bars. + This alows us to render slightly bigger labels. + The axis spans for 120 units which is 120 seconds. + There could be a situation where we are experiencing + the "changing of the time guards". + Then the first-bar-hour-label is expressed as the: + + H1 + H2 + + where: + H1 is the passing hour, + H2 is the incoming hour. + + Then the axis could look like this: + + |III|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIII ... + 10 :59:45 :59:57 :00:09 :00:21 :00:33 :00:45 :00:57 :01:09 ... + 11 + + Whereas after a while it would look like this: + + |III|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIIIII|IIIII ... + 11 :00:09 :00:21 :00:33 :00:45 :00:57 :01:09 :00:21 :00:33 ... + + And so on. + This function cares only about rendering. The label-atop-another + construction is done purely by the positioning functions. + */ + const hourFormat = d3.timeFormat('%H'); + const restFormat = d3.timeFormat(':%M:%S'); + const current = hourFormat(time[0]); + const incoming = hourFormat(time[time.length - 1]); + if (i === 0) { + return current; + } + if (current !== incoming) { + if (i === time.length - 1) { + return incoming; + } + } + return printTick( + restFormat(time[label('x', d.id)]), + spaceLabels(d.id, time.length, 10, 5), + ); + } + const wholeFormat = d3.timeFormat('%H:%M:%S'); + return printTick( + wholeFormat(time[label('x', d.id)]), + spaceLabels(d.id, time.length, 10, 5), + ); +} + +export function getPositionY(d, i, time) { + /* + Every label is postioned under its axis-bar except of the last one. + The last label in a chain is placed exactly under the first one. + When there will be an hour change within the axis, this strangely positioned + label will be made visible. We do not have to check if there is + an hour change, the last label is always there, ready to be displayed. + */ + const position = d.y + (d.height / 3); + if (i === time.length - 1) { + const l = Array.from(document.getElementsByClassName('xLabel'))[0]; + const h = parseFloat(getComputedStyle(l).fontSize); + return position + h; + } + return position; +} + +export function getPositionX(d, i, time) { + if (i === time.length - 1) { + return time[0].x; + } + return d.x; +} + +export function getTooltip(d, arr) { + const dID = d.id.split(':'); + const dataRow = parseInt(dID[0], 10); // is representing the bucket + const dataCol = parseInt(dID[1], 10); // is representing the time moment + const tooltip = []; + arr.forEach((dg) => { + /* + The function is not rendering the tooltip. + It only recovers its to-be-displayed data + from the data array. + */ + const { key } = dg; + if (dg.column === dataCol && dg.row === dataRow) { + tooltip.push(dg.time); // Time + tooltip.push(dg.key); // The bucket name + tooltip.push(dg[key]); // The bucket value + } + }); + return tooltip; +} + +export function renderTooltip(tooltipSelection, data) { + const baseStyle = 'border: 1px solid darkgrey; padding: 4px'; + const labelStyle = `style="${baseStyle}"`; + const dataStyle = `style="${baseStyle}; text-align: center"`; + /* When the browser window is very small and we are near the window contour, + we have to flip the tooltip, otherwise it will flow outside the window */ + const flipSide = d3.event.pageX > 0.7 * window.innerWidth ? -100 : 10; + tooltipSelection + .style('top', `${d3.event.pageY + 10}px`) + .style('left', `${d3.event.pageX + flipSide}px`) + .style('visibility', 'visible') + .html(` + + + + + + + + + + +
+ + Time + + + ${d3.timeFormat('%H:%M:%S')(data[0])} +
+ + ${data[1]} + + + ${data[2]} +
`); +} + +export function calcFont(axis) { + const w = window.innerWidth; + let fontSize = 0; + if (axis === 'y') { + switch (true) { + case (w < 560): + fontSize = 0.5; + break; + case (w < 660): + fontSize = 0.55; + break; + case (w < 1030): + fontSize = 0.8; + break; + case (w < 1110): + fontSize = 0.6; + break; + case (w < 1250): + fontSize = 0.7; + break; + case (w < 1450): + fontSize = 0.8; + break; + case (w < 1600): + fontSize = 0.89; + break; + default: + fontSize = 1; + break; + } + } else { + switch (true) { + case (w < 600): + fontSize = 0.6; + break; + case (w < 770): + fontSize = 0.7; + break; + case (w < 1180): + fontSize = 0.7; + break; + case (w < 1600): + fontSize = 0.89; + break; + default: + fontSize = 1; + break; + } + } + + return `${fontSize}em sans-serif`; +} diff --git a/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx b/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx index ac22e2c9..3897023c 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx @@ -10,7 +10,11 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, dps: PropTypes.arrayOf(PropTypes.object), stopMonitoringFunction: PropTypes.func.isRequired, callees: PropTypes.arrayOf(PropTypes.string), @@ -42,16 +46,16 @@ const GraphPanel = ({ stopMonitoringFunction={() => stopMonitoringFunction(mfa)} callees={callees} calleesVisibility={calleesVisibility} - showCallees={() => showCallees(mfa[3])} - hideCallees={() => hideCallees(mfa[3])} + showCallees={() => showCallees(mfa.query)} + hideCallees={() => hideCallees(mfa.query)} panelVisibility={panelVisibility} - expand={() => expand(mfa[3])} - shrink={() => shrink(mfa[3])} + expand={() => expand(mfa.query)} + shrink={() => shrink(mfa.query)} calleeClick={calleeClick} /> {panelVisibility ? (
- +
) : null}
diff --git a/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx b/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx index 77a97a6b..bd5dc535 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx @@ -9,7 +9,11 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, stopMonitoringFunction: PropTypes.func.isRequired, callees: PropTypes.arrayOf(PropTypes.string), calleesVisibility: PropTypes.bool, @@ -44,7 +48,7 @@ const GraphPanelHeading = ({ shrink={shrink} />

- {mfa[3]} + {mfa.query} - Monitoring

{calleesVisibility ? ( diff --git a/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx b/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx index f6cc667d..95411640 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx @@ -11,7 +11,11 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, getFunctionsData: PropTypes.func.isRequired, data: PropTypes.arrayOf(PropTypes.object), stopMonitoringFunction: PropTypes.func.isRequired, @@ -54,7 +58,7 @@ class Monitoring extends React.Component { return (
expand(mfa[3])} - shrink={() => shrink(mfa[3])} + expand={() => expand(mfa.query)} + shrink={() => shrink(mfa.query)} />

- {mfa[3]} + {mfa.query} - Slow calls tracing

diff --git a/apps/xprof_gui/priv/src/components/tracing/CallsRow/CallsRow.jsx b/apps/xprof_gui/priv/src/components/tracing/CallsRow/CallsRow.jsx index a7af4c3c..58d8fa9d 100644 --- a/apps/xprof_gui/priv/src/components/tracing/CallsRow/CallsRow.jsx +++ b/apps/xprof_gui/priv/src/components/tracing/CallsRow/CallsRow.jsx @@ -11,7 +11,11 @@ const propTypes = { expanded: PropTypes.bool, }).isRequired, toggleExpandItem: PropTypes.func.isRequired, - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, }; const CallsRow = ({ mfa, item, toggleExpandItem }) => { diff --git a/apps/xprof_gui/priv/src/components/tracing/CallsSwitch/CallsSwitch.jsx b/apps/xprof_gui/priv/src/components/tracing/CallsSwitch/CallsSwitch.jsx index fa0d22ba..5d38fb4b 100644 --- a/apps/xprof_gui/priv/src/components/tracing/CallsSwitch/CallsSwitch.jsx +++ b/apps/xprof_gui/priv/src/components/tracing/CallsSwitch/CallsSwitch.jsx @@ -7,7 +7,11 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, disabled: PropTypes.bool, collecting: PropTypes.bool, toggleCallsTracing: PropTypes.func.isRequired, diff --git a/apps/xprof_gui/priv/src/components/tracing/CallsTable/CallsTable.jsx b/apps/xprof_gui/priv/src/components/tracing/CallsTable/CallsTable.jsx index 149fe054..bfcf7a0e 100644 --- a/apps/xprof_gui/priv/src/components/tracing/CallsTable/CallsTable.jsx +++ b/apps/xprof_gui/priv/src/components/tracing/CallsTable/CallsTable.jsx @@ -7,7 +7,11 @@ const defaultProps = { sort: { items: [] }, }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, sort: PropTypes.objectOf(PropTypes.any), sortCallsBy: PropTypes.func.isRequired, toggleExpandItem: PropTypes.func.isRequired, diff --git a/apps/xprof_gui/priv/src/components/tracing/Tracing/Tracing.jsx b/apps/xprof_gui/priv/src/components/tracing/Tracing/Tracing.jsx index 0f1ceb3f..9680f879 100644 --- a/apps/xprof_gui/priv/src/components/tracing/Tracing/Tracing.jsx +++ b/apps/xprof_gui/priv/src/components/tracing/Tracing/Tracing.jsx @@ -10,7 +10,11 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.arrayOf(PropTypes.any).isRequired, + mfa: PropTypes.shape({ + graph_type: PropTypes.string, + mfa: PropTypes.arrayOf(PropTypes.any), + query: PropTypes.string, + }).isRequired, getFunctionsCalls: PropTypes.func.isRequired, calls: PropTypes.arrayOf(PropTypes.object), toggleCallsTracing: PropTypes.func.isRequired, @@ -52,7 +56,7 @@ class Tracing extends React.Component { return (
; const mapStateToProps = (state, ownProps) => ({ mfa: ownProps.mfa, - data: getFunctionData(state, ownProps.mfa[3]), - callees: getFunctionCallees(state, ownProps.mfa[3]), - calleesVisibility: getFunctionCalleesVisibility(state, ownProps.mfa[3]), - panelVisibility: getFunctionGraphVisibility(state, ownProps.mfa[3]), + data: getFunctionData(state, ownProps.mfa.query), + callees: getFunctionCallees(state, ownProps.mfa.query), + calleesVisibility: getFunctionCalleesVisibility(state, ownProps.mfa.query), + panelVisibility: getFunctionGraphVisibility(state, ownProps.mfa.query), }); const mapDispatchToProps = { diff --git a/apps/xprof_gui/priv/src/containers/TracingContainer/TracingContainer.jsx b/apps/xprof_gui/priv/src/containers/TracingContainer/TracingContainer.jsx index e17c2e72..dfbb6873 100644 --- a/apps/xprof_gui/priv/src/containers/TracingContainer/TracingContainer.jsx +++ b/apps/xprof_gui/priv/src/containers/TracingContainer/TracingContainer.jsx @@ -21,9 +21,9 @@ const TracingContainer = props => ; const mapStateToProps = (state, ownProps) => ({ mfa: ownProps.mfa, - calls: getFunctionCalls(state, ownProps.mfa[3]), - controls: getFunctionControl(state, ownProps.mfa[3]), - panelVisibility: getFunctionTracingVisibility(state, ownProps.mfa[3]), + calls: getFunctionCalls(state, ownProps.mfa.query), + controls: getFunctionControl(state, ownProps.mfa.query), + panelVisibility: getFunctionTracingVisibility(state, ownProps.mfa.query), }); const mapDispatchToProps = { diff --git a/apps/xprof_gui/priv/src/utils/ActionsUtils.js b/apps/xprof_gui/priv/src/utils/ActionsUtils.js index d0f583ca..d61b38f1 100644 --- a/apps/xprof_gui/priv/src/utils/ActionsUtils.js +++ b/apps/xprof_gui/priv/src/utils/ActionsUtils.js @@ -155,16 +155,16 @@ const determineIncomingDps = (dps, ts) => { export const determineNextData = async (mfas, data) => { const nextData = {}; - await Promise.all(mfas.map(async (mfa) => { - const completeFunName = mfa[3]; + await Promise.all(mfas.map(async (m) => { + const completeFunName = m.query; const currentDps = data[completeFunName]; const lastTs = currentDps && currentDps.length ? last(currentDps).time / 1000 : 0; const { json, error } = await XProf.getFunctionsSamples( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], lastTs, ); @@ -187,16 +187,16 @@ export const determineNextData = async (mfas, data) => { export const determineNextCalls = async (dispatch, state, mfas, calls) => { const nextCalls = {}; - await Promise.all(mfas.map(async (mfa) => { - const completeFunName = mfa[3]; + await Promise.all(mfas.map(async (m) => { + const completeFunName = m.query; const lastCalls = getLastCallsForFunction(state, completeFunName); const offset = lastCalls && lastCalls.items.length ? last(lastCalls.items).id : 0; const { json, error } = await XProf.getFunctionsCalls( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], offset, ); @@ -223,24 +223,24 @@ export const determineNextCalls = async (dispatch, state, mfas, calls) => { return nextCalls; }; -export const determineNextControlSwitch = async (control, mfa) => { +export const determineNextControlSwitch = async (control, m) => { const { threshold, limit, collecting } = control; const nextControl = { ...control }; if (collecting) { const { error } = await XProf.stopCapturingFunctionsCalls( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], ); if (error) console.log('ERROR: ', error); nextControl.collecting = false; } else { const { error } = await XProf.startCapturingFunctionsCalls( - mfa[0], - mfa[1], - mfa[2], + m.mfa[0], + m.mfa[1], + m.mfa[2], threshold, limit, ); diff --git a/apps/xprof_gui/src/xprof_gui_rest.erl b/apps/xprof_gui/src/xprof_gui_rest.erl index 3815cb84..4a3825dc 100644 --- a/apps/xprof_gui/src/xprof_gui_rest.erl +++ b/apps/xprof_gui/src/xprof_gui_rest.erl @@ -67,12 +67,14 @@ %%% %%% Returns: %%%
    -%%%
  • 200: [["mod", "fun", "arity", "query"]]
  • +%%%
  • 200: [{"mfa": ["mod", "fun", "arity"], +%%% "query": "queryvalue", +%%% "graph_type": "percentiles"/"grid"]]
  • %%%
%%% %%% Return list of monitored functions. %%% (The values of "mod", "fun" and "arity" can be used as params to calls to eg -%%% "/api/mon_stop" while "query" can be used to display the original query +%%% "/api/mon_stop" while "queryvalue" can be used to display the original query %%% string). %%% %%% === /api/data === @@ -87,7 +89,7 @@ %%% %%% Returns: %%%
    -%%%
  • 200: [{"time": timestamp, "hitkey": number}] (where "histkey" is one of: +%%%
  • 200: [{"time": timestamp, "histkey": number}] (where "histkey" is one of: %%% min, mean, median, max, stddev, %%% p25, p50, p75, p90, p99, p9999999, memsize, count)
  • %%%
  • 404: "" (the requested MFA is not monitored)
  • @@ -264,7 +266,13 @@ handle_req(<<"mon_stop">>, Params) -> handle_req(<<"mon_get_all">>, _Params) -> Funs = xprof_core:get_all_monitored(), - FunsArr = [[Mod, Fun, Arity, Query] + FunsArr = [{[{<<"mfa">>, [Mod, Fun, Arity]}, + {<<"query">>, Query}, + {<<"graph_type">>, case Mod of + ets -> <<"grid">>; + _ -> <<"percentiles">> + end} + ]} || {{Mod, Fun, Arity}, Query} <- Funs], Json = jsone:encode(FunsArr), {200, Json}; @@ -379,4 +387,3 @@ get_int(Key, Params, Default) -> _ -> Default end. - diff --git a/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl b/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl index 1bc444eb..bec97953 100644 --- a/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl +++ b/apps/xprof_gui/test/xprof_http_e2e_SUITE.erl @@ -170,19 +170,28 @@ try_to_start_monitoring_invalid_query(_Config) -> monitor_valid_query(_Config) -> {204, _} = make_get_request("api/mon_start", [{"query", "dict:new/0"}]), {200, Monitored} = make_get_request("api/mon_get_all"), - ?assertEqual([[<<"dict">>, <<"new">>, 0, <<"dict:new/0">>]], Monitored), + ?assertEqual([[{<<"mfa">>, [<<"dict">>, <<"new">>, 0]}, + {<<"query">>, <<"dict:new/0">>}, + {<<"graph_type">>, <<"percentiles">>}]], + Monitored), ok. monitor_valid_query_twice(_Config) -> {204, _} = make_get_request("api/mon_start", [{"query", "dict:new/0"}]), {204, _} = make_get_request("api/mon_start", [{"query", "dict:new/0"}]), {200, Monitored} = make_get_request("api/mon_get_all"), - ?assertEqual([[<<"dict">>, <<"new">>, 0, <<"dict:new/0">>]], Monitored), + ?assertEqual([[{<<"mfa">>, [<<"dict">>, <<"new">>, 0]}, + {<<"query">>, <<"dict:new/0">>}, + {<<"graph_type">>, <<"percentiles">>}]], + Monitored), ok. stop_monitoring(_Config) -> given_traced("dict:new/0"), - {200, [[<<"dict">>, <<"new">>, 0, <<"dict:new/0">>]]} = + {200, [[{<<"mfa">>, [<<"dict">>, <<"new">>, 0]}, + {<<"query">>, <<"dict:new/0">>}, + {<<"graph_type">>, <<"percentiles">>}] + ]} = make_get_request("api/mon_get_all"), {204, _} = make_get_request("api/mon_stop", [ {"mod", "dict"}, @@ -196,7 +205,10 @@ monitor_query_with_matchspec(_Config) -> Q = "lists:delete(_, [E]) -> true", ?assertMatch({204, _}, make_get_request("api/mon_start", [{"query", Q}])), {200, Monitored} = make_get_request("api/mon_get_all"), - ?assertEqual([[<<"lists">>, <<"delete">>, 2, list_to_binary(Q)]], + ?assertEqual([[{<<"mfa">>, [<<"lists">>, <<"delete">>, 2]}, + {<<"query">>, list_to_binary(Q)}, + {<<"graph_type">>, <<"percentiles">>}] + ], Monitored), ok. @@ -408,4 +420,3 @@ proplist_to_query_string([{K, V}]) -> K ++ "=" ++ http_uri:encode(V); proplist_to_query_string([{K, V} | Rest]) -> K ++ "=" ++ http_uri:encode(V) ++ "&" ++ proplist_to_query_string(Rest). - From e326cc4b15150ef0e0b38b7c24fc49d4388daa81 Mon Sep 17 00:00:00 2001 From: jlampar Date: Wed, 9 May 2018 10:27:56 +0200 Subject: [PATCH 02/22] fixed one-container bug --- .../src/components/monitoring/Graph/Graph.jsx | 17 +++++- .../components/monitoring/Graph/Grid/Grid.jsx | 58 ++++++++++--------- .../monitoring/GraphPanel/GraphPanel.jsx | 2 +- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx index 8f8e108a..22fc0292 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx @@ -5,16 +5,26 @@ import 'c3/c3.css'; import { AXIS, DATA, GRID, POINT, TRANSITION } from '../../../constants'; import Grid from './Grid/Grid'; +function composeID(q) { + const modFuncArity = q.split(':'); + const mod = modFuncArity[0]; + const func = modFuncArity[1].split('/')[0]; + const arity = modFuncArity[1].split('/')[1]; + return `${mod}${func}${arity}`; +} + const propTypes = { dps: PropTypes.arrayOf(PropTypes.object).isRequired, type: PropTypes.string.isRequired, + query: PropTypes.string.isRequired, }; -const Graph = ({ dps, type }) => { - const wrapper = document.getElementById('graphWrapper'); + +const Graph = ({ dps, type, query }) => { + const wrapper = document.getElementById(`graphWrapper-${composeID(query)}`); if (type === 'grid') { return (
    { {wrapper && }
    ); diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx index 7fc40119..42871d89 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Grid/Grid.jsx @@ -15,7 +15,6 @@ import { renderTooltip, calcFont, } from './gridUtils'; -// import scaleCluster from 'd3-scale-cluster'; class Grid extends React.Component { constructor(props) { @@ -35,18 +34,20 @@ class Grid extends React.Component { componentWillMount() { // Create tooltip - then it will be only relocated. - d3.select('#root').append('div') - .attr('id', 'tip') - .style('position', 'absolute') - .style('padding', '2px') - .style('font', '0.75em sans-serif') - .style('background', '#f5f5f5') - .style('border', '20px') - .style('border-radius', '2px') - .style('pointer-events', 'none') - .style('visibility', 'visible') - .style('opacity', 0.9) - .style('z-index', 99); + if (!document.getElementById('tip')) { + d3.select('#root').append('div') + .attr('id', 'tip') + .style('position', 'absolute') + .style('padding', '2px') + .style('font', '0.75em sans-serif') + .style('background', '#f5f5f5') + .style('border', '20px') + .style('border-radius', '2px') + .style('pointer-events', 'none') + .style('visibility', 'visible') + .style('opacity', 0.9) + .style('z-index', 99); + } } componentDidMount() { @@ -71,14 +72,15 @@ class Grid extends React.Component { The chart is constantly redrawing itself in a cycle of collapsing and self-creation. */ - d3.select('#gridTable').remove(); - d3.select('#xAxis').remove(); - d3.select('#yAxis').remove(); + const { graphID } = this.props; + d3.select(`#gridTable-${graphID}`).remove(); + d3.select(`#xAxis-${graphID}`).remove(); + d3.select(`#yAxis-${graphID}`).remove(); d3.select('#tip').style('visibility', 'hidden'); } componentDidUpdate() { - const { data } = this.props; + const { data, graphID } = this.props; const { size } = this.state; const dataArray = dataTransform(data); @@ -98,9 +100,9 @@ class Grid extends React.Component { const tooltip = d3.select('#tip'); // Append chart - const grid = d3.select('#grid') + const grid = d3.select(`#grid-${graphID}`) .append('svg') - .attr('id', 'gridTable') + .attr('id', `gridTable-${graphID}`) .attr('width', size.width) .attr('height', size.height) /* @@ -138,16 +140,16 @@ class Grid extends React.Component { // Append x axis // creating the xAxis abstract container - const xAxis = d3.select('#x') + const xAxis = d3.select(`#x-${graphID}`) .append('svg') - .attr('id', 'xAxis') + .attr('id', `xAxis-${graphID}`) .attr('width', size.width + 20) .attr('height', size.marginBottom); /* The axis won't be a common d3 axis as the heatmap looks better with so-called bar-axis. Every color-rectangle will have - corresponding axis-rectangle below and on the right side of the grid. + corresponding axis-rectangle below and on the left side of the grid. That's why we are not calling the d3 axis method but instead we create a single, special grid-row and grid-column. */ @@ -195,9 +197,9 @@ class Grid extends React.Component { // Append y axis - const yAxis = d3.select('#y') + const yAxis = d3.select(`#y-${graphID}`) .append('svg') - .attr('id', 'yAxis') + .attr('id', `yAxis-${graphID}`) .attr('width', size.marginLeft) .attr('height', size.height); @@ -271,20 +273,21 @@ class Grid extends React.Component { } render() { + const { graphID } = this.props; return (
    -
    +
    {panelVisibility ? (
    - +
    ) : null}
    From 2ba0fed28121e23e43e1a86a58927f0ae5e5bbb9 Mon Sep 17 00:00:00 2001 From: jlampar Date: Wed, 9 May 2018 10:58:54 +0200 Subject: [PATCH 03/22] minor Graph refactoring --- .../priv/src/components/monitoring/Graph/Graph.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx index 22fc0292..751aac38 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx @@ -20,11 +20,13 @@ const propTypes = { }; const Graph = ({ dps, type, query }) => { - const wrapper = document.getElementById(`graphWrapper-${composeID(query)}`); + const queryID = composeID(query); + const wrapperID = `graphWrapper-${queryID}`; + const wrapper = document.getElementById(wrapperID); if (type === 'grid') { return (
    { {wrapper && }
    ); From 663693ac7de87ed6b3aaf333a656e79b189c0555 Mon Sep 17 00:00:00 2001 From: jlampar Date: Thu, 10 May 2018 13:26:28 +0200 Subject: [PATCH 04/22] integrated chartID, renaming mfa/mfas, filtering --- .../priv/src/actions/CollectingActions.js | 38 ++++++++++-------- .../priv/src/actions/ExploringActions.js | 24 ++++++----- .../priv/src/actions/MonitoringActions.js | 34 +++++++++------- .../priv/src/actions/TracingActions.js | 39 +++++++++--------- .../src/components/functions/Functions.jsx | 14 +++---- .../src/components/monitoring/Graph/Graph.jsx | 8 +--- .../monitoring/GraphPanel/GraphPanel.jsx | 18 ++++----- .../GraphPanelHeading/GraphPanelHeading.jsx | 8 ++-- .../monitoring/Monitoring/Monitoring.jsx | 8 ++-- .../tracing/CallsInput/CallsInput.jsx | 10 ++--- .../tracing/CallsPanel/CallsPanel.jsx | 14 +++---- .../components/tracing/CallsRow/CallsRow.jsx | 6 +-- .../tracing/CallsSwitch/CallsSwitch.jsx | 6 +-- .../tracing/CallsTable/CallsTable.jsx | 16 ++++---- .../components/tracing/Tracing/Tracing.jsx | 8 ++-- .../FunctionsContainer/FunctionsContainer.jsx | 4 +- .../MonitoringContainer.jsx | 13 +++--- .../TracingContainer/TracingContainer.jsx | 11 +++-- .../xprof_gui/priv/src/reducers/monitoring.js | 4 +- .../xprof_gui/priv/src/selectors/Selectors.js | 2 +- apps/xprof_gui/priv/src/utils/ActionsUtils.js | 40 ++++++++++--------- 21 files changed, 170 insertions(+), 155 deletions(-) diff --git a/apps/xprof_gui/priv/src/actions/CollectingActions.js b/apps/xprof_gui/priv/src/actions/CollectingActions.js index c606965f..004a3d85 100644 --- a/apps/xprof_gui/priv/src/actions/CollectingActions.js +++ b/apps/xprof_gui/priv/src/actions/CollectingActions.js @@ -1,13 +1,13 @@ import { isEqual, isEmpty } from 'lodash'; import * as types from '../constants/ActionTypes'; import * as XProf from '../api'; -import { getMfas, getData, getCalls } from '../selectors'; +import { getAllMonitored, getData, getCalls } from '../selectors'; import { setCallsControl, getCalleesForFunctions } from './'; import { determineNextData, determineNextCalls } from '../utils'; -export const updateListMonitoringFunctions = mfas => ({ +export const updateListMonitoringFunctions = monitoredCollection => ({ type: types.UPDATE_MONITORED_FUNCTIONS, - mfas, + monitoredCollection, }); const updateData = data => ({ @@ -22,20 +22,21 @@ const updateCalls = calls => ({ export const getMonitoredFunctions = () => async (dispatch, getState) => { const state = getState(); - const mfas = getMfas(state); + const monitoredCollection = getAllMonitored(state); const { json, error } = await XProf.getAllMonitoredFunctions(); if (error) { console.log('ERROR: ', error); - } else if (!isEqual(mfas, json)) { - const newMfas = json - .filter(mfa => !mfas.map(f => f.query) - .includes(mfa.query)); - const newControls = newMfas.reduce( - (control, mfa) => ({ + } else if (!isEqual(monitoredCollection, json)) { + const queries = monitoredCollection.map(monitored => monitored.query); + const newMonitoredCollection = json + .filter(monitored => !queries + .includes(monitored.query)); + const newControls = newMonitoredCollection.reduce( + (control, monitored) => ({ ...control, - [mfa.query]: { + [monitored.query]: { threshold: undefined, limit: undefined, collecting: false, @@ -44,7 +45,7 @@ export const getMonitoredFunctions = () => async (dispatch, getState) => { {}, ); - dispatch(getCalleesForFunctions(newMfas)); + dispatch(getCalleesForFunctions(newMonitoredCollection)); dispatch(setCallsControl(newControls)); dispatch(updateListMonitoringFunctions(json)); } @@ -52,9 +53,9 @@ export const getMonitoredFunctions = () => async (dispatch, getState) => { export const getFunctionsData = () => async (dispatch, getState) => { const state = getState(); - const mfas = getMfas(state); + const monitoredCollection = getAllMonitored(state); const data = getData(state); - const nextData = await determineNextData(mfas, data); + const nextData = await determineNextData(monitoredCollection, data); if (!isEmpty(nextData)) { dispatch(updateData({ ...data, ...nextData })); @@ -63,9 +64,14 @@ export const getFunctionsData = () => async (dispatch, getState) => { export const getFunctionsCalls = () => async (dispatch, getState) => { const state = getState(); - const mfas = getMfas(state); + const monitoredCollection = getAllMonitored(state); const calls = getCalls(state); - const nextCalls = await determineNextCalls(dispatch, state, mfas, calls); + const nextCalls = await determineNextCalls( + dispatch, + state, + monitoredCollection, + calls, + ); if (!isEmpty(nextCalls)) { dispatch(updateCalls({ ...calls, ...nextCalls })); diff --git a/apps/xprof_gui/priv/src/actions/ExploringActions.js b/apps/xprof_gui/priv/src/actions/ExploringActions.js index 9d1e1c61..335426ac 100644 --- a/apps/xprof_gui/priv/src/actions/ExploringActions.js +++ b/apps/xprof_gui/priv/src/actions/ExploringActions.js @@ -22,13 +22,13 @@ export const calleeClick = callee => (dispatch) => { dispatch(startMonitoringFunction(callee)); }; -export const getCallees = m => async (dispatch) => { - const name = m.query; +export const getCallees = monitored => async (dispatch) => { + const name = monitored.query; const { json, error } = await XProf.getFunctionsCallees( - m.mfa[0], - m.mfa[1], - m.mfa[2], + monitored.mfa[0], + monitored.mfa[1], + monitored.mfa[2], ); if (error) console.log('ERROR'); @@ -36,16 +36,18 @@ export const getCallees = m => async (dispatch) => { else console.log('NO CALLES FOUND!'); }; -export const getCalleesForFunctions = mfas => async (dispatch) => { +export const getCalleesForFunctions = monitoredCollection => async ( + dispatch, +) => { const callees = {}; - await Promise.all(mfas.map(async (m) => { - const fun = m.query; + await Promise.all(monitoredCollection.map(async (monitored) => { + const fun = monitored.query; const { json, error } = await XProf.getFunctionsCallees( - m.mfa[0], - m.mfa[1], - m.mfa[2], + monitored.mfa[0], + monitored.mfa[1], + monitored.mfa[2], ); if (error) console.log('ERROR'); diff --git a/apps/xprof_gui/priv/src/actions/MonitoringActions.js b/apps/xprof_gui/priv/src/actions/MonitoringActions.js index 6f74908c..1498ed2b 100644 --- a/apps/xprof_gui/priv/src/actions/MonitoringActions.js +++ b/apps/xprof_gui/priv/src/actions/MonitoringActions.js @@ -1,32 +1,35 @@ -import { getMfas } from '../selectors'; +import { getAllMonitored } from '../selectors'; import * as types from '../constants/ActionTypes'; import * as XProf from '../api'; -const stopMonitoringFunctionRequest = mfas => ({ +const stopMonitoringFunctionRequest = monitoredCollection => ({ type: types.STOP_MONITORING_FUNCTION, - mfas, + monitoredCollection, }); -const stopMonitoringFunctionError = mfas => ({ +const stopMonitoringFunctionError = monitoredCollection => ({ type: types.STOP_MONITORING_FUNCTION_ERROR, - mfas, + monitoredCollection, }); -export const stopMonitoringFunction = m => async (dispatch, getState) => { +export const stopMonitoringFunction = monitored => async ( + dispatch, getState, +) => { const state = getState(); - const mfas = getMfas(state); - const mfasReduced = mfas.filter(f => f.query !== m.query); + const monitoredCollection = getAllMonitored(state); + const monitoredCollectionReduced = monitoredCollection + .filter(f => f.query !== monitored.query); - dispatch(stopMonitoringFunctionRequest(mfasReduced)); + dispatch(stopMonitoringFunctionRequest(monitoredCollectionReduced)); const { error } = await XProf.stopMonitoringFunction( - m.mfa[0], - m.mfa[1], - m.mfa[2], + monitored.mfa[0], + monitored.mfa[1], + monitored.mfa[2], ); if (error) { console.log('ERROR: ', error); - dispatch(stopMonitoringFunctionError(mfas)); + dispatch(stopMonitoringFunctionError(monitoredCollection)); } }; @@ -35,8 +38,9 @@ export const startMonitoringFunction = functionName => async ( getState, ) => { const state = getState(); - const mfas = getMfas(state); - const isMonitored = mfas.filter(mfa => mfa.query === functionName).length; + const monitoredCollection = getAllMonitored(state); + const isMonitored = monitoredCollection + .filter(monitored => monitored.query === functionName).length; if (!isMonitored) { const { error } = await XProf.startMonitoringFunction(functionName); diff --git a/apps/xprof_gui/priv/src/actions/TracingActions.js b/apps/xprof_gui/priv/src/actions/TracingActions.js index ba9372c0..bd8f1738 100644 --- a/apps/xprof_gui/priv/src/actions/TracingActions.js +++ b/apps/xprof_gui/priv/src/actions/TracingActions.js @@ -20,9 +20,9 @@ const updateLastCallsForFunction = (functionName, sortedCalls) => ({ sortedCalls, }); -export const toggleExpandItem = (mfa, item) => (dispatch, getState) => { +export const toggleExpandItem = (monitored, item) => (dispatch, getState) => { const state = getState(); - const functionName = mfa.query; + const functionName = monitored.query; const lastCallsForFunction = getLastCallsForFunction(state, functionName); const updatedItems = lastCallsForFunction.sort.items.map((call) => { @@ -38,29 +38,30 @@ export const toggleExpandItem = (mfa, item) => (dispatch, getState) => { dispatch(toggleExpand(functionName, updatedItems)); }; -export const toggleCallsTracing = mfa => async (dispatch, getState) => { +export const toggleCallsTracing = monitored => async (dispatch, getState) => { const state = getState(); - const functionName = mfa.query; + const functionName = monitored.query; const control = getFunctionControl(state, functionName); - const nextControl = await determineNextControlSwitch(control, mfa); + const nextControl = await determineNextControlSwitch(control, monitored); dispatch(setCallsControl({ [functionName]: nextControl })); }; -export const handleThresholdChange = (mfa, value) => (dispatch, getState) => { - const state = getState(); - const functionName = mfa.query; - const { limit, collecting } = getFunctionControl(state, functionName); - const nextControl = { - threshold: value, - limit, - collecting, +export const handleThresholdChange = (monitored, value) => + (dispatch, getState) => { + const state = getState(); + const functionName = monitored.query; + const { limit, collecting } = getFunctionControl(state, functionName); + const nextControl = { + threshold: value, + limit, + collecting, + }; + dispatch(setCallsControl({ [functionName]: nextControl })); }; - dispatch(setCallsControl({ [functionName]: nextControl })); -}; -export const handleLimitChange = (mfa, value) => (dispatch, getState) => { +export const handleLimitChange = (monitored, value) => (dispatch, getState) => { const state = getState(); - const functionName = mfa.query; + const functionName = monitored.query; const { threshold, collecting } = getFunctionControl(state, functionName); const nextControl = { threshold, @@ -70,9 +71,9 @@ export const handleLimitChange = (mfa, value) => (dispatch, getState) => { dispatch(setCallsControl({ [functionName]: nextControl })); }; -export const sortCallsBy = (mfa, column) => (dispatch, getState) => { +export const sortCallsBy = (monitored, column) => (dispatch, getState) => { const state = getState(); - const functionName = mfa.query; + const functionName = monitored.query; const lastCallsForFunction = getLastCallsForFunction(state, functionName); const order = lastCallsForFunction.sort.column === column && diff --git a/apps/xprof_gui/priv/src/components/functions/Functions.jsx b/apps/xprof_gui/priv/src/components/functions/Functions.jsx index f60e54e6..235c5ee7 100644 --- a/apps/xprof_gui/priv/src/components/functions/Functions.jsx +++ b/apps/xprof_gui/priv/src/components/functions/Functions.jsx @@ -5,7 +5,7 @@ import { FUNCTIONS_INTERVAL } from '../../constants'; const propTypes = { getMonitoredFunctions: PropTypes.func.isRequired, - mfas: PropTypes.arrayOf(PropTypes.shape({ + monitoredCollection: PropTypes.arrayOf(PropTypes.shape({ graph_type: PropTypes.string, mfa: PropTypes.arrayOf(PropTypes.any), query: PropTypes.string, @@ -28,14 +28,14 @@ class Functions extends React.Component { } render() { - const { mfas } = this.props; + const { monitoredCollection } = this.props; return (
    - {mfas.map((m, index) => ( -
    - - - {index < mfas.length - 1 ? ( + {monitoredCollection.map((monitored, index) => ( +
    + + + {index < monitoredCollection.length - 1 ? (
    ) : null}
    diff --git a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx index 751aac38..6aa44ca5 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Graph/Graph.jsx @@ -5,13 +5,7 @@ import 'c3/c3.css'; import { AXIS, DATA, GRID, POINT, TRANSITION } from '../../../constants'; import Grid from './Grid/Grid'; -function composeID(q) { - const modFuncArity = q.split(':'); - const mod = modFuncArity[0]; - const func = modFuncArity[1].split('/')[0]; - const arity = modFuncArity[1].split('/')[1]; - return `${mod}${func}${arity}`; -} +const composeID = q => `${q.replace(/[^A-Za-z0-9_-]/g, '-')}`; const propTypes = { dps: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx b/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx index 13578dfa..d9dc23f4 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/GraphPanel/GraphPanel.jsx @@ -10,7 +10,7 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.shape({ + monitored: PropTypes.shape({ graph_type: PropTypes.string, mfa: PropTypes.arrayOf(PropTypes.any), query: PropTypes.string, @@ -28,7 +28,7 @@ const propTypes = { }; const GraphPanel = ({ - mfa, + monitored, dps, stopMonitoringFunction, callees, @@ -42,20 +42,20 @@ const GraphPanel = ({ }) => (
    stopMonitoringFunction(mfa)} + monitored={monitored} + stopMonitoringFunction={() => stopMonitoringFunction(monitored)} callees={callees} calleesVisibility={calleesVisibility} - showCallees={() => showCallees(mfa.query)} - hideCallees={() => hideCallees(mfa.query)} + showCallees={() => showCallees(monitored.query)} + hideCallees={() => hideCallees(monitored.query)} panelVisibility={panelVisibility} - expand={() => expand(mfa.query)} - shrink={() => shrink(mfa.query)} + expand={() => expand(monitored.query)} + shrink={() => shrink(monitored.query)} calleeClick={calleeClick} /> {panelVisibility ? (
    - +
    ) : null}
    diff --git a/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx b/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx index bd5dc535..52e94d6a 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/GraphPanelHeading/GraphPanelHeading.jsx @@ -9,7 +9,7 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.shape({ + monitored: PropTypes.shape({ graph_type: PropTypes.string, mfa: PropTypes.arrayOf(PropTypes.any), query: PropTypes.string, @@ -26,7 +26,7 @@ const propTypes = { }; const GraphPanelHeading = ({ - mfa, + monitored, stopMonitoringFunction, callees, calleesVisibility, @@ -39,7 +39,7 @@ const GraphPanelHeading = ({ }) => (

    - {mfa.query} + {monitored.query} - Monitoring

    {calleesVisibility ? ( diff --git a/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx b/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx index 95411640..2eee8877 100644 --- a/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx +++ b/apps/xprof_gui/priv/src/components/monitoring/Monitoring/Monitoring.jsx @@ -11,7 +11,7 @@ const defaultProps = { }; const propTypes = { - mfa: PropTypes.shape({ + monitored: PropTypes.shape({ graph_type: PropTypes.string, mfa: PropTypes.arrayOf(PropTypes.any), query: PropTypes.string, @@ -43,7 +43,7 @@ class Monitoring extends React.Component { render() { const { - mfa, + monitored, data, stopMonitoringFunction, callees, @@ -58,8 +58,8 @@ class Monitoring extends React.Component { return (
    handleThresholdChange(mfa, e.target.value)} + onChange={e => handleThresholdChange(monitored, e.target.value)} disabled={collecting} /> @@ -78,7 +78,7 @@ const CallsInput = ({ className="form-control" placeholder={`1 - ${CALLS_LIMIT}`} value={limit} - onChange={e => handleLimitChange(mfa, e.target.value)} + onChange={e => handleLimitChange(monitored, e.target.value)} disabled={collecting} /> @@ -88,7 +88,7 @@ const CallsInput = ({ {error} expand(mfa.query)} - shrink={() => shrink(mfa.query)} + expand={() => expand(monitored.query)} + shrink={() => shrink(monitored.query)} />

    - {mfa.query} + {monitored.query} - Slow calls tracing

    {panelVisibility ? (
    { +const CallsRow = ({ monitored, item, toggleExpandItem }) => { const dir = item.expanded ? 'down' : 'right'; const rowType = item.expanded ? 'expanded' : 'normal'; @@ -26,7 +26,7 @@ const CallsRow = ({ mfa, item, toggleExpandItem }) => {