| // 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; |
| } |
| |
| } |