blob: 92c2dc99d09a2d69c2f33f49ae76caed6e87f1e5 [file] [log] [blame]
// Copyright 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview StateGraph class to generate d3 visualization of the service
* states, wifi network scans and system suspend/resume events.
*/
var stateGraph = {};
/**
* This method is responsible for creating the d3 elements needed for the
* service, scan and system suspend visualization. The visualization is built
* with the d3 library.
*
* @param {Node} node element that will the parent of the visualization in the
* resulting document.
* @param {Manager} manager instance containing the log state information.
*/
stateGraph.createGraph = function(node, manager) {
var lineData = [];
var scans = manager.scanDetails;
var suspends = manager.suspendDetails;
var darkSuspends = [];
for (var i = 0; i < manager.services.length; i++) {
if (manager.services[i].isActive) {
lineData.push(manager.services[i].graphData);
lineData[lineData.length - 1].serviceId = manager.services[i].serviceId;
}
}
if (lineData.length == 0)
return;
var color;
if (lineData.length < 10) {
color = d3.scale.category10();
} else if (lineData.length < 20) {
color = d3.scale.category20();
} else {
console.error('Too many active services to graph.');
return;
}
var states = [
'Failure',
'Idle',
'Associating',
'Configuring',
'Connected',
'Online',
'Portal'
];
var map = {};
map['Failure'] = 0;
map['Idle'] = 1;
map['Associating'] = 2;
map['Configuring'] = 3;
map['Connected'] = 4;
map['Online'] = 5;
map['Portal'] = 6;
// Creates a graph for a state machine. Returns a function that
// should be called when the timeline is "brushed" (i.e., when the
// user zooms in on a particular time).
function createStateGraph(uiStates, logStates, height, topMargin,
cssClass, graphName, statesList) {
var stateMap = {};
for (var i = 0; i < logStates.length; i++) {
stateMap[logStates[i]] = i;
}
console.log(stateMap);
console.log(statesList);
var margins = {
left: 80,
top: topMargin,
right: 20,
bottom: 40
};
var graphX = d3.scale.linear()
.range([0, width - margins.right - margins.left])
.domain(focusX.domain());
var graphY = d3.scale.linear()
.range([height, 0])
.domain([0, logStates.length - 1]);
var yAxis = d3.svg.axis()
.scale(graphY)
.tickSize(2)
.innerTickSize(-width + margins.left + margins.right)
.ticks(uiStates.length)
.orient('left')
.tickSubdivide(true)
.tickFormat(function(d) {
console.log(`need tick for ${d}`);
return uiStates[d];
});
var xAxis = d3.svg.axis()
.scale(graphX)
.tickSize(5)
.tickSubdivide(true)
.tickFormat(function(d) {
return getXTickLabels(d);
});
var area = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return graphX(d.x);
})
.y(function(d) {
return graphY(stateMap[d.y]);
});
var graphGroup = svg.append('g')
.attr('transform',
'translate(' + margins.left + ',' + margins.top + ')');
graphGroup.append('g')
.attr('class', 'x axis')
.attr('transform',
'translate(0,' + height + ')')
.call(xAxis);
graphGroup.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(0,0)')
.call(yAxis);
graphGroup.append('text')
.attr('x', -margins.left)
.attr('y', height + margins.bottom)
.attr('class', cssClass)
.text(graphName);
graphGroup.append('path')
.attr('fill', 'none')
.attr('class', 'supp_line')
.attr('clip-path', 'url(#clip)')
.attr('d', area(statesList));
var onBrushed = function(brush) {
graphX.domain(brush.empty() ? contextX.domain() : brush.extent());
graphGroup.select('path.supp_line').attr('d', area(statesList));
graphGroup.select('.x.axis').call(xAxis);
};
return onBrushed;
}
var label = 'manager_viz_' + manager.id;
var svg = d3.select(node).append('svg:svg')
.attr('id', label)
.attr('width', 1000)
.attr('height', 1200);
var width = 1000;
var focusHeight = 430;
var contextHeight = 40;
var focusMargin = {
top: 40,
right: 20,
bottom: 40,
left: 80
};
var contextMargin = {
top: 430,
right: 20,
bottom: 100,
left: 80
};
var focusX = d3.scale.linear()
.range([0, width - focusMargin.right - focusMargin.left])
.domain([0, manager.endTime - manager.startTime]);
var focusY = d3.scale.linear()
.range([focusHeight - focusMargin.bottom - focusMargin.top,
focusMargin.top])
.domain([0, 6]);
var contextX = d3.scale.linear()
.range([0, width - contextMargin.right - contextMargin.left])
.domain(focusX.domain());
var contextY = d3.scale.linear()
.range([contextHeight, 0])
.domain([0, 6]);
var xAxisFocus = d3.svg.axis()
.scale(focusX)
.tickSize(5)
.tickSubdivide(true)
.tickFormat(function(d) {
return getXTickLabels(d);
});
var yAxis = d3.svg.axis()
.scale(focusY)
.tickSize(5)
.innerTickSize(-width + focusMargin.left + focusMargin.right)
.ticks(6)
.orient('left')
.tickSubdivide(true)
.tickFormat(function(d) {
return states[d];
});
var xAxisContext = d3.svg.axis()
.scale(contextX)
.orient('bottom')
.tickFormat(function(d) {
return getXTickLabels(d);
});
var brush = d3.svg.brush()
.x(contextX)
.on('brush', brushed);
var focusArea = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return focusX(d.x);
})
.y(function(d) {
return focusY(map[d.y]);
});
var contextArea = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return contextX(d.x);
})
.y(function(d) {
return contextY(map[d.y]);
});
svg.selectAll('legend')
.data(lineData)
.enter()
.append('text')
.attr('class', 'legend')
.attr('x', function(d, i) {
return i * (4 * focusMargin.right);
})
.attr('y', (focusHeight - 14))
.attr('fill', function(d, i) {
return d.color = color(i);
})
.text(function(d) {
const serviceId = d.serviceId;
if (serviceId.match(/^\d+$/)) {
return 'service ' + serviceId;
}
return serviceId;
});
var haveScans = false;
var haveScansDefault = false;
var haveScansError = false;
var haveSuspends = false;
var haveSuspendsPartial = false;
var haveDarkSuspends = false;
if (scans.length > 0) {
for (var s = 0; s < scans.length; s++) {
var temp = scans[s];
if ((temp.start && temp.end) || (temp.start >= 0 && temp.end >= 0)) {
haveScans = true;
} else if (temp.end || temp.end == 0) {
haveScansDefault = true;
} else {
haveScansError = true;
}
}
}
if (suspends.length > 0) {
for (var s = 0; s < suspends.length; s++) {
if (suspends[s].start && suspends[s].end) {
haveSuspends = true;
} else {
haveSuspendsPartial = true;
}
if (suspends[s].darkSuspends != null) {
haveDarkSuspends = true;
Array.prototype.push.apply(darkSuspends, suspends[s].darkSuspends);
}
}
}
var colorLabels = [];
if (haveSuspends) {
colorLabels.push({label: 'complete', text: ['suspend']});
}
if (haveSuspendsPartial) {
colorLabels.push({label: 'partial', text: ['suspend', 'unbounded']});
}
if (haveDarkSuspends) {
colorLabels.push({label: 'dark', text: ['suspend', 'dark']});
}
if (haveScans) {
colorLabels.push({label: 'detailed', text: ['scan']});
}
if (haveScansDefault) {
colorLabels.push({label: 'default', text: ['scan', 'done']});
}
if (haveScansError) {
colorLabels.push({label: 'error', text: ['scan', 'error']});
}
if (colorLabels.length > 0) {
svg.selectAll('colorLegend')
.data(colorLabels)
.enter()
.append('rect')
.attr('width', 70)
.attr('height', 30)
.attr('x', function(d, i) {
return (width - (i + 1) * 75);
})
.attr('y', 2)
.attr('class', function(d) {
return d.label;
});
svg.selectAll('colorLabels')
.data(colorLabels)
.enter()
.append('text')
.attr('x', function(d, i) {
return (width - ((i + 1) * 75) + 2);
})
.attr('y', 18)
.text(function(d) {
return d.text[0];
});
svg.selectAll('colorLabels')
.data(colorLabels)
.enter()
.append('text')
.attr('x', function(d, i) {
return (width - ((i + 1) * 75) + 2);
})
.attr('y', 30)
.text(function(d) {
return d.text[1];
});
}
svg.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + focusMargin.left + ',0)')
.call(yAxis);
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width - focusMargin.left - focusMargin.right)
.attr('height', focusHeight - focusMargin.top - focusMargin.bottom);
var focus = svg.append('g')
.attr('class', 'focus')
.attr('transform', 'translate(' + focusMargin.left + ',0)');
var focusHoverTarget = svg.append('rect')
.attr('width', width)
.attr('height', focusHeight)
.attr('fill', 'rgba(0, 0, 0, 0)')
.attr('stroke', 'none')
.on('mouseover', focusMouseOver)
.on('mousemove', focusMouseMove)
.on('mouseout', focusMouseOut);
var context = svg.append('g')
.attr('class', 'context')
.attr('transform',
'translate(' + contextMargin.left + ',' + contextMargin.top + ')');
focus.append('g')
.attr('class', 'x axis')
.attr('transform',
'translate(0,' +
(focusHeight - focusMargin.bottom - focusMargin.top) + ')')
.call(xAxisFocus);
focus.selectAll('scanned')
.data(scans)
.enter()
.append('rect')
.attr('class', function(d, i) {
return getScanLabel(d, i);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('scan')
.data(scans)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return getScanLabel(d, i);
});
focus.selectAll('suspended')
.data(suspends)
.enter()
.append('rect')
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('suspend')
.data(suspends)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
});
focus.selectAll('darksuspended')
.data(darkSuspends)
.enter()
.append('rect')
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('darksuspend')
.data(darkSuspends)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
});
focus.selectAll('data')
.data(lineData)
.enter()
.append('path')
.attr('stroke', function(d, i) {
return d.color = color(i);
})
.attr('stroke-width', 2)
.attr('fill', 'none')
.attr('class', function(d, i) {
return 'area_' + i + ' data';
})
.attr('clip-path', 'url(#clip)')
.attr('d', focusArea);
context.selectAll('path')
.data(lineData)
.enter()
.append('path')
.attr('class', function(d, i) {
return 'line_' + i;
})
.attr('d', contextArea)
.attr('stroke', function(d, i) {
return d.color = color(i);
})
.attr('stroke-width', 2)
.attr('fill', 'none');
context.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + contextHeight + ')')
.call(xAxisContext);
context.append('g')
.attr('class', 'x brush')
.call(brush)
.selectAll('rect')
.attr('y', -6)
.attr('height', contextHeight + 7);
var brushCallbacks = [];
brushCallbacks.push(
createStateGraph([
'Disconnected',
'IntfDisabled',
'Inactive',
'Scanning',
'Authenticating',
'Associating',
'Associated',
'4WayHandsh',
'GroupHandsh',
'Completed'
], [
'DISCONNECTED',
'INTERFACE_DISABLED',
'INACTIVE',
'SCANNING',
'AUTHENTICATING',
'ASSOCIATING',
'ASSOCIATED',
'4WAY_HANDSHAKE',
'GROUP_HANDSHAKE',
'COMPLETED'
], 180, 550, 'supplicant', 'wpa_supplicant state',
manager.supplicantStates));
brushCallbacks.push(
createStateGraph([
'Unknown',
'Initialize',
'Disabled',
'Idle',
'Received',
'GetMethod',
'Method',
'SendResponse',
'Discard',
'Identity',
'Notification',
'Retransmit',
'Success',
'Failure'
], [
'UNKNOWN',
'INITIALIZE',
'DISABLED',
'IDLE',
'RECEIVED',
'GET_METHOD',
'METHOD',
'SEND_RESPONSE',
'DISCARD',
'IDENTITY',
'NOTIFICATION',
'RETRANSMIT',
'SUCCESS',
'FAILURE'
], 180, 850, 'supplicant', 'eap state',
manager.eapStates));
function brushed() {
focusX.domain(brush.empty() ? contextX.domain() : brush.extent());
focus.selectAll('path.data').attr('d', focusArea);
focus.selectAll('rect.suspend')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
});
focus.selectAll('rect.suspends')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
});
focus.selectAll('rect.scans')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return getScanLabel(d, i);
});
focus.select('.x.axis').call(xAxisFocus);
for (var i = 0; i < brushCallbacks.length; i++) {
brushCallbacks[i](brush);
}
}
function getXTickLabels(d) {
var format = d3.time.format.utc('%H:%M:%S');
var date = new Date(manager.startTime + d + manager.timeOffset);
return format(date);
}
function getX(d) {
if (d.start || d.start == 0) {
return d.start;
}
else {
return d.end;
}
}
function getWidth(d) {
var width = 2;
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
width = focusX(d.end) - focusX(d.start);
}
if (width < 2) {
return 2;
}
return width;
}
function getContextWidth(d) {
var width = 2;
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
width = contextX(d.end) - contextX(d.start);
}
if (width < 2) {
return 2;
}
return width;
}
function getScanLabel(d, i) {
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
return ('zoom_' + i + ' scans detailed');
}
if (d.end || d.end == 0) {
return ('zoom_' + i + ' scans default');
}
return ('zoom_' + i + ' scans error');
}
function getSuspendLabel(d, i) {
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
return ('zoom_' + i + ' suspends complete');
}
return ('zoom_' + i + ' suspends partial');
}
let timeHover = null;
function focusMouseOver(d, i) {
timeHover = document.createElement('div');
timeHover.style.position = 'absolute';
document.body.appendChild(timeHover);
}
function focusMouseMove(d, i) {
const bounds = this.getClientRects()[0];
const rawX = d3.mouse(this)[0];
const xpos = rawX + bounds.left + window.scrollX;
const rawY = d3.mouse(this)[1];
const mouseOffset = 30;
const ypos = rawY + bounds.top + window.scrollY - mouseOffset;
const text = getXTickLabels(focusX.invert(rawX - focusMargin.left));
timeHover.style.left = xpos + 'px';
timeHover.style.top = ypos + 'px';
timeHover.innerHTML = text;
}
function focusMouseOut(d, i) {
timeHover.remove();
timeHover = null;
}
}