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