blob: fd7ec03856ec19e82e4f72b8bf3c04a691065a12 [file] [log] [blame]
// This file is about the layout (Preproess() and Group()) of the IPMI time line view.
// The layout happens according to the following sequence that is very similar to how
// the layout is done for DBus messages:
//
// 1. User clicks any of the checkboxes for the grouping fields (NetFN, CMD)
// 2. OnGroupByConditionChanged() is called
// 3. OnGroupByConditionChanged() calls PreProcess() and Group()
// 4. PreProcess() takes the IPMI messages extracted from the DBus capture
// (g_ipmi_parsed_entries), and determines the start time.
// 5. Group() takes the IPMI messages, and the list of keys, and groups the messages
// by the keys. The output is picked up by GenerateTimeLine(), which writes the
// timeline data into the Intervals and Titles arrays. The draw loop immediately
// picks up the updated Intervals and Titles arrays and draws on the canvas
// accordingly.
const {dialog} = require('electron');
const {fs} = require('file-system');
const {util} = require('util');
const {exec} = require('child_process');
// Main view objects
var ipmi_timeline_view = new IPMITimelineView();
ipmi_timeline_view.IsTimeDistributionEnabled = true;
var btn_start_capture = document.getElementById('btn_start_capture');
var select_capture_mode = document.getElementById('select_capture_mode');
var capture_info = document.getElementById('capture_info');
var radio_open_file = document.getElementById('radio_open_file');
var radio_capture = document.getElementById('radio_capture');
var title_open_file = document.getElementById('title_open_file');
var title_capture = document.getElementById('title_capture');
// Set up Electron-related stuff here; Electron does not allow inlining button
// events
document.getElementById('c1').addEventListener(
'click', OnGroupByConditionChanged); // NetFN
document.getElementById('c2').addEventListener(
'click', OnGroupByConditionChanged); // CMD
// Zoom in button
document.getElementById('btn_zoom_in').addEventListener('click', function() {
ipmi_timeline_view.BeginZoomAnimation(0.5);
boost_asio_handler_timeline_view.BeginZoomAnimation(0.5);
});
// Zoom out button
document.getElementById('btn_zoom_out').addEventListener('click', function() {
ipmi_timeline_view.BeginZoomAnimation(-1);
boost_asio_handler_timeline_view.BeginZoomAnimation(-1);
});
// Pan left button
document.getElementById('btn_pan_left').addEventListener('click', function() {
ipmi_timeline_view.BeginPanScreenAnimaton(-0.5);
boost_asio_handler_timeline_view.BeginPanScreenAnimaton(-0.5);
});
// Pan right button
document.getElementById('btn_pan_right').addEventListener('click', function() {
ipmi_timeline_view.BeginPanScreenAnimaton(0.5);
boost_asio_handler_timeline_view.BeginPanScreenAnimaton(0.5);
});
// Reset zoom button
document.getElementById('btn_zoom_reset').addEventListener('click', function() {
ipmi_timeline_view.BeginSetBoundaryAnimation(
RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
dbus_timeline_view.BeginSetBoundaryAnimation(
RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
boost_asio_handler_timeline_view.BeginSetBoundaryAnimation(
RANGE_LEFT_INIT, RANGE_RIGHT_INIT)
})
// Generate replay
document.getElementById('gen_replay_ipmitool1')
.addEventListener('click', function() {
GenerateIPMIToolIndividualCommandReplay(HighlightedRequests);
});
document.getElementById('gen_replay_ipmitool2')
.addEventListener('click', function() {
GenerateIPMIToolExecListReplay(HighlightedRequests);
});
document.getElementById('gen_replay_ipmid_legacy')
.addEventListener('click', function() {
GenerateBusctlReplayLegacyInterface(HighlightedRequests);
});
document.getElementById('gen_replay_ipmid_new')
.addEventListener('click', function() {
GenerateBusctlReplayNewInterface(HighlightedRequests);
});
document.getElementById('btn_start_capture')
.addEventListener('click', function() {
let h = document.getElementById('text_hostname').value;
g_capture_state = 'started';
StartCapture(h);
});
// For capture mode
document.getElementById('btn_stop_capture')
.addEventListener('click', function() {
StopCapture();
});
document.getElementById('select_capture_mode')
.addEventListener('click', OnCaptureModeChanged);
radio_open_file.addEventListener('click', OnAppModeChanged);
radio_capture.addEventListener('click', OnAppModeChanged);
radio_open_file.click();
// App mode: open file or capture
function OnAppModeChanged() {
title_open_file.style.display = 'none';
title_capture.style.display = 'none';
if (radio_open_file.checked) {
title_open_file.style.display = 'block';
}
if (radio_capture.checked) {
title_capture.style.display = 'block';
}
}
// Capture mode: Live capture or staged capture
function OnCaptureModeChanged() {
let x = select_capture_mode;
let i = capture_info;
let desc = '';
switch (x.value) {
case 'live':
desc = 'Live: read BMC\'s dbus-monitor console output directly';
g_capture_mode = 'live';
break;
case 'staged':
desc =
'Staged, IPMI only: Store BMC\'s dbus-monitor output in a file and transfer back for display';
g_capture_mode = 'staged';
break;
case 'staged2':
desc =
'Staged, DBus + IPMI: Store BMC\'s busctl output in a file and transfer back for display';
g_capture_mode = 'staged2';
break;
}
i.textContent = desc;
}
// Data
var HistoryHistogram = [];
// var Data_IPMI = []
// =====================
let Intervals = [];
let Titles = [];
let HighlightedRequests = [];
let GroupBy = [];
let GroupByStr = '';
// (NetFn, Cmd) -> [ Bucket Indexes ]
// Normalized (0~1) bucket index for the currently highlighted IPMI requests
let IpmiVizHistHighlighted = {};
let HistogramThresholds = {};
function IsIntersected(i0, i1) {
return (!((i0[1] < i1[0]) || (i0[0] > i1[1])));
}
function IsIntersectedPixelCoords(i0, i1) {
if (i0[1] == undefined || isNaN(i0[1])) {
return (Math.abs(i0[0] - i1[0]) < 5);
} else {
return (IsIntersected(i0, i1));
}
}
var NetFnCmdToDescription = {
'6, 1': 'App-GetDeviceId',
'6, 3': 'App-WarmReset',
'10, 64': 'Storage-GetSelInfo',
'10, 35': 'Storage-GetSdr',
'4, 32': 'Sensor-GetDeviceSDRInfo',
'4, 34': 'Sensor-ReserveDeviceSDRRepo',
'4, 47': 'Sensor-GetSensorType',
'10, 34': 'Storage-ReserveSdrRepository',
'46, 50': 'OEM Extension',
'4, 39': 'Sensor-GetSensorThresholds',
'4, 45': 'Sensor-GetSensorReading',
'10, 67': 'Storage-GetSelEntry',
'58, 196': 'IBM_OEM',
'10, 32': 'Storage-GetSdrRepositoryInfo',
'4, 33': 'Sensor-GetDeviceSDR',
'6, 54': 'App-Get BT Interface Capabilities',
'10, 17': 'Storage-ReadFruData',
'10, 16': 'Storage-GetFruInventoryAreaInfo',
'4, 2': 'Sensor-PlatformEvent',
'4, 48': 'Sensor-SetSensor',
'6, 34': 'App-ResetWatchdogTimer'
};
const CANVAS_H = document.getElementById('my_canvas_ipmi').height;
const CANVAS_W = document.getElementById('my_canvas_ipmi').width;
var LowerBoundTime = RANGE_LEFT_INIT;
var UpperBoundTime = RANGE_RIGHT_INIT;
var LastTimeLowerBound;
var LastTimeUpperBound;
// Dirty flags for determining when to redraw the canvas
let IsCanvasDirty = true;
let IsHighlightDirty = false;
// Animating left and right boundaries
let IsAnimating = false;
let LowerBoundTimeTarget = LowerBoundTime;
let UpperBoundTimeTarget = UpperBoundTime;
// For keyboard interaction: arrow keys and Shift
let CurrDeltaX = 0; // Proportion of Canvas to scroll per frame.
let CurrDeltaZoom = 0; // Delta zoom per frame.
let CurrShiftFlag = false; // Whether the Shift key is depressed
// TODO: these variables are shared across all views but are now in ipmi_timeline_vis.js, need to move to some other location some time
const LEFT_MARGIN = 640
const RIGHT_MARGIN = 1390;
const LINE_HEIGHT = 15;
const LINE_SPACING = 17;
const YBEGIN = 22 + LINE_SPACING;
const TOP_HORIZONTAL_SCROLLBAR_HEIGHT = YBEGIN - LINE_HEIGHT / 2; // ybegin is the center of the 1st line of the text so need to minus line_height/2
const BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT = LINE_HEIGHT;
const TEXT_Y0 = 3;
const HISTOGRAM_W = 100, HISTOGRAM_H = LINE_SPACING;
const HISTOGRAM_X = 270;
// If some request's time is beyond the right tail, it's considered "too long"
// If some request's time is below the left tail it's considered "good"
// const HISTOGRAM_LEFT_TAIL_WIDTH = 0.05, HISTOGRAM_RIGHT_TAIL_WIDTH = 0.05;
// temporarily disabled for now
const HISTOGRAM_LEFT_TAIL_WIDTH = -1, HISTOGRAM_RIGHT_TAIL_WIDTH = -1;
const SCROLL_BAR_WIDTH = 16;
let IpmiVizHistogramImageData = {}; // Image data for rendered histogram
// Input is the data that's completed layout
// is_free_x: Should each histogram has its own X range or not
// num_buckets: # of buckets for histograms
// theta: top and bottom portion to cut
function ComputeHistogram(num_buckets = 30, is_free_x = true) {
let global_lb = Infinity, global_ub = -Infinity;
IpmiVizHistogramImageData = {};
// Global minimal and maximal values
for (let i = 0; i < Intervals.length; i++) {
let interval = Intervals[i];
let l = Math.min.apply(Math, interval.map(function(x) {
return x[1] - x[0];
}));
let u = Math.max.apply(Math, interval.map(function(x) {
return x[1] - x[0];
}));
global_lb = Math.min(l, global_lb);
global_ub = Math.max(u, global_ub);
}
HistoryHistogram = [];
for (let i = 0; i < Intervals.length; i++) {
let interval = Intervals[i];
let lb = global_lb, ub = global_ub;
if (is_free_x == true) {
lb = Math.min.apply(Math, interval.map(function(x) {
return x[1] - x[0];
}));
ub = Math.max.apply(Math, interval.map(function(x) {
return x[1] - x[0];
}));
}
const EPS = 1e-2;
if (lb == ub) ub = lb + EPS;
let line = [lb * 1000000, ub * 1000000]; // to usec
let buckets = [];
for (let j = 0; j < num_buckets; j++) buckets.push(0);
for (let j = 0; j < interval.length; j++) {
let t = interval[j][1] - interval[j][0];
let bucket_idx = parseInt(t / ((ub - lb) / num_buckets));
buckets[bucket_idx]++;
}
line.push(buckets);
HistoryHistogram[Titles[i].title] = line;
}
}
function Preprocess(data) {
preprocessed = [];
let StartingUsec_IPMI;
if (g_StartingSec == undefined) {
StartingUsec_IPMI = undefined;
} else {
StartingUsec_IPMI = g_StartingSec * 1000000;
}
for (let i = 0; i < data.length; i++) {
let entry = data[i].slice();
let lb = entry[2], ub = entry[3];
// Only when IPMI view is present (i.e. no DBus pcap is loaded)
if (i == 0 && StartingUsec_IPMI == undefined) {
StartingUsec_IPMI = lb;
}
entry[2] = lb - StartingUsec_IPMI;
entry[3] = ub - StartingUsec_IPMI;
preprocessed.push(entry);
}
return preprocessed;
}
let SHOW_BLOB_DETAILS = true;
function Group(data, groupBy) {
let grouped = {};
// If has netfn and cmd: use "NetFN, CMD" as key
// Otherwise, use "NetFN" as key
// This distinction is made if the user chooses to label operation on each
// blob individually
// Key: blob name
// Value: the commands that operate on the particular blob
let sid2blobid = {}
for (let n = 0; n < data.length; n++) {
const p = data[n];
const netfn = p[0], cmd = p[1], req = p[4], res = p[5];
if (netfn == 46 && cmd == 128) {
const oen = req[0] + req[1] * 256 + req[2] * 65536;
if (oen == 0xc2cf) { // Blob operations
const blobcmd =
req[3]; // Refer to https://github.com/openbmc/phosphor-ipmi-blobs
// The IPMI blob commands are visible on DBus, another WIP command-line tool that
// utilizes this fact to show information about blobs can be found here:
// https://gerrit.openbmc-project.xyz/c/openbmc/openbmc-tools/+/41451
let sid, blobid;
// layout of req
// 0 1 2 3 4 5 6 7 8 9 10 ...
// CF C2 00 CMD [CRC ] [ other stuff ]
// layout of res
// 0 1 2 3 4 5 6 7 8 ...
// CF C2 00 [CRC ] [other stuff]
// Determining blob id and session ID
switch (blobcmd) {
case 3:
case 4:
case 5:
case 6:
case 9:
case 10: {
const sid = req[6] + req[7] * 256;
blobid = sid2blobid[sid];
if (blobid != undefined) {
p.key = blobid;
}
break;
}
case 7:
case 8: {
blobid = '';
for (let i = 6; i < req.length; i++) {
blobid += String.fromCharCode(req[i]);
}
break;
}
}
switch (blobcmd) {
case 2: { // open
blobid = '';
for (let i = 8; i < req.length; i++) {
if (req[i] == 0)
break;
else
blobid += String.fromCharCode(req[i]);
}
p.key = blobid;
sid = res[5] + res[6] * 256; // session_id
sid2blobid[sid] = blobid;
break;
}
case 3: { // Read
break;
}
case 4: { // Write
const offset =
req[8] + req[9] * 256 + req[10] * 65536 + req[11] * 16777216;
p.offset = offset;
break;
}
case 5: { // Commit
break;
}
case 6: { // Close
break;
}
}
}
}
}
const idxes = {'NetFN': 0, 'CMD': 1};
//
for (let n = 0; n < data.length; n++) {
const p = data[n];
let key = '';
if (p.key != undefined)
key = p.key;
else if (p[0] != '' && p[1] != '') {
for (let i = 0; i < groupBy.length; i++) {
if (i > 0) {
key += ', ';
}
key += p[idxes[groupBy[i]]];
}
}
if (grouped[key] == undefined) {
grouped[key] = [];
}
grouped[key].push(p);
}
return grouped;
}
function GenerateTimeLine(grouped) {
const keys = Object.keys(grouped);
let sortedKeys = keys.slice();
// If NetFN and CMD are both selected, sort by NetFN then CMD
// In this case, all "keys" are string-encoded integer pairs
if (keys.length > 0 && ipmi_timeline_view.GroupBy.length == 2) {
sortedKeys = sortedKeys.sort(function(a, b) {
a = a.split(',');
b = b.split(',');
if (a.length == 2 && b.length == 2) {
let aa = parseInt(a[0]) * 256 + parseInt(a[1]);
let bb = parseInt(b[0]) * 256 + parseInt(b[1]);
return aa < bb ? -1 : (aa > bb ? 1 : 0);
} else {
return a < b ? -1 : (a > b ? 1 : 0);
}
});
}
Intervals = [];
Titles = [];
for (let i = 0; i < sortedKeys.length; i++) {
Titles.push({"header":false, "title":sortedKeys[i], "intervals_idxes":[i]});
line = [];
for (let j = 0; j < grouped[sortedKeys[i]].length; j++) {
let entry = grouped[sortedKeys[i]][j];
// Lower bound, Upper bound, and a reference to the original request
line.push([
parseFloat(entry[2]) / 1000000, parseFloat(entry[3]) / 1000000, entry,
'ok', 0
]);
}
Intervals.push(line);
}
ipmi_timeline_view.Intervals = Intervals.slice();
ipmi_timeline_view.Titles = Titles.slice();
ipmi_timeline_view.LayoutForOverlappingIntervals();
}
function OnGroupByConditionChanged() {
const tags = ['c1', 'c2'];
const v = ipmi_timeline_view;
v.GroupBy = [];
v.GroupByStr = '';
for (let i = 0; i < tags.length; i++) {
let cb = document.getElementById(tags[i]);
if (cb.checked) {
v.GroupBy.push(cb.value);
if (v.GroupByStr.length > 0) {
v.GroupByStr += ', ';
}
v.GroupByStr += cb.value;
}
}
let preproc = Preprocess(Data_IPMI);
grouped = Group(preproc, v.GroupBy);
GenerateTimeLine(grouped);
IsCanvasDirty = true;
ipmi_timeline_view.IsCanvasDirty = true;
}
function MapXCoord(x, left_margin, right_margin, rl, rr) {
let ret = left_margin + (x - rl) / (rr - rl) * (right_margin - left_margin);
if (ret < left_margin) {
ret = left_margin;
} else if (ret > right_margin) {
ret = right_margin;
}
return ret;
}
function draw_timeline(ctx) {
ipmi_timeline_view.Render(ctx);
}
window.addEventListener('keydown', function() {
if (event.keyCode == 37) { // Left Arrow
ipmi_timeline_view.CurrDeltaX = -0.004;
dbus_timeline_view.CurrDeltaX = -0.004;
} else if (event.keyCode == 39) { // Right arrow
ipmi_timeline_view.CurrDeltaX = 0.004;
dbus_timeline_view.CurrDeltaX = 0.004;
} else if (event.keyCode == 16) { // Shift
ipmi_timeline_view.CurrShiftFlag = true;
dbus_timeline_view.CurrShiftFlag = true;
} else if (event.keyCode == 38) { // Up arrow
ipmi_timeline_view.CurrDeltaZoom = 0.01;
dbus_timeline_view.CurrDeltaZoom = 0.01;
} else if (event.keyCode == 40) { // Down arrow
ipmi_timeline_view.CurrDeltaZoom = -0.01;
dbus_timeline_view.CurrDeltaZoom = -0.01;
}
});
window.addEventListener('keyup', function() {
if (event.keyCode == 37 || event.keyCode == 39) {
ipmi_timeline_view.CurrDeltaX = 0;
dbus_timeline_view.CurrDeltaX = 0;
} else if (event.keyCode == 16) {
ipmi_timeline_view.CurrShiftFlag = false;
dbus_timeline_view.CurrShiftFlag = false;
} else if (event.keyCode == 38 || event.keyCode == 40) {
ipmi_timeline_view.CurrDeltaZoom = 0;
dbus_timeline_view.CurrDeltaZoom = 0;
}
});
function MouseXToTimestamp(x) {
let ret = (x - LEFT_MARGIN) / (RIGHT_MARGIN - LEFT_MARGIN) *
(UpperBoundTime - LowerBoundTime) +
LowerBoundTime;
ret = Math.max(ret, LowerBoundTime);
ret = Math.min(ret, UpperBoundTime);
return ret;
}
let HighlightedRegion = {t0: -999, t1: -999};
function IsHighlighted() {
return (HighlightedRegion.t0 != -999 && HighlightedRegion.t1 != -999);
}
function Unhighlight() {
HighlightedRegion.t0 = -999;
HighlightedRegion.t1 = -999;
}
function UnhighlightIfEmpty() {
if (HighlightedRegion.t0 == HighlightedRegion.t1) {
Unhighlight();
return true;
}
return false;
}
let MouseState = {
hovered: true,
pressed: false,
x: 0,
y: 0,
hoveredVisibleLineIndex: -999,
hoveredSide: undefined,
IsHoveredOverHorizontalScrollbar: function() {
if (this.hoveredSide == "top_horizontal_scrollbar") return true;
else if (this.hoveredSide == "bottom_horizontal_scrollbar") return true;
else return false;
}
};
let Canvas = document.getElementById('my_canvas_ipmi');
Canvas.onmousemove = function(event) {
const v = ipmi_timeline_view;
v.MouseState.x = event.pageX - this.offsetLeft;
v.MouseState.y = event.pageY - this.offsetTop;
if (v.MouseState.pressed == true &&
v.MouseState.hoveredSide == 'timeline') { // Update highlighted area
v.HighlightedRegion.t1 = v.MouseXToTimestamp(v.MouseState.x);
}
v.OnMouseMove();
v.IsCanvasDirty = true;
v.linked_views.forEach(function(u) {
u.MouseState.x = event.pageX - Canvas.offsetLeft;
u.MouseState.y = 0; // Do not highlight any entry
if (u.MouseState.pressed == true &&
u.MouseState.hoveredSide == 'timeline') { // Update highlighted area
u.HighlightedRegion.t1 = u.MouseXToTimestamp(u.MouseState.x);
}
u.OnMouseMove();
u.IsCanvasDirty = true;
});
};
Canvas.onmouseover = function() {
ipmi_timeline_view.OnMouseMove();
};
Canvas.onmouseleave = function() {
ipmi_timeline_view.OnMouseLeave();
};
Canvas.onmousedown = function(event) {
if (event.button == 0) { // Left mouse button
ipmi_timeline_view.OnMouseDown();
}
};
Canvas.onmouseup = function(event) {
if (event.button == 0) {
ipmi_timeline_view.OnMouseUp();
// page-specific, not view-specific
let hint = document.getElementById('highlight_hint');
if (ipmi_timeline_view.UnhighlightIfEmpty()) {
hint.style.display = 'none';
} else {
hint.style.display = 'block';
}
}
};
Canvas.onwheel = function(event) {
ipmi_timeline_view.OnMouseWheel(event);
};
// This function is not specific to TimelineView so putting it here
function OnHighlightedChanged(reqs) {
let x = document.getElementById('ipmi_replay');
let i = document.getElementById('ipmi_replay_output');
let cnt = document.getElementById('highlight_count');
cnt.innerHTML = '' + reqs.length;
i.style.display = 'none';
if (reqs.length > 0) {
x.style.display = 'block';
} else
x.style.display = 'none';
let o = document.getElementById('ipmi_replay_output');
o.style.display = 'none';
o.textContent = '';
}
function ToHexString(bytes, prefix, sep) {
let ret = '';
for (let i = 0; i < bytes.length; i++) {
if (i > 0) {
ret += sep;
}
ret += prefix + bytes[i].toString(16);
}
return ret;
}
function ToASCIIString(bytes) {
ret = '';
for (let i = 0; i < bytes.length; i++) {
ret = ret + String.fromCharCode(bytes[i]);
}
return ret;
}
function ShowReplayOutputs(x, ncols) {
let o = document.getElementById('ipmi_replay_output');
o.cols = ncols;
o.style.display = 'block';
o.textContent = x;
}
function GenerateIPMIToolIndividualCommandReplay(reqs) {
let x = '';
for (let i = 0; i < reqs.length; i++) {
let req = reqs[i];
// [0]: NetFN, [1]: cmd, [4]: payload
// NetFN and cmd are DECIMAL while payload is HEXADECIMAL.
x = x + 'ipmitool raw ' + req[0] + ' ' + req[1] + ' ' +
ToHexString(req[4], '0x', ' ') + '\n';
}
ShowReplayOutputs(x, 80);
}
function GenerateIPMIToolExecListReplay(reqs) {
console.log(reqs.length);
let x = '';
for (let i = 0; i < reqs.length; i++) {
let req = reqs[i];
x = x + 'raw ' +
ToHexString([req[0]].concat([req[1]]).concat(req[4]), '0x', ' ') + '\n';
}
ShowReplayOutputs(x, 80);
}
function GenerateBusctlReplayLegacyInterface(reqs) {
console.log(reqs.length);
let serial = 0;
let x = '';
for (let i = 0; i < reqs.length; i++) {
let req = reqs[i];
x = x +
'busctl --system emit /org/openbmc/HostIpmi/1 org.openbmc.HostIpmi ReceivedMessage yyyyay ';
x = x + serial + ' ' + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' +
ToHexString(req[4], '0x', ' ') + '\n';
serial = (serial + 1) % 256;
}
ShowReplayOutputs(x, 120);
}
function GenerateBusctlReplayNewInterface(reqs) {
console.log(reqs.length);
let x = '';
for (let i = 0; i < reqs.length; i++) {
let req = reqs[i];
x = x +
'busctl --system call xyz.openbmc_project.Ipmi.Host /xyz/openbmc_project/Ipmi xyz.openbmc_project.Ipmi.Server execute yyyaya{sv} ';
x = x + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' +
ToHexString(req[4], '0x', ' ');
+' 0\n';
}
ShowReplayOutputs(x, 150);
}