Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 1 | // This file is about the layout (Preproess() and Group()) of the IPMI time line view. |
| 2 | // The layout happens according to the following sequence that is very similar to how |
| 3 | // the layout is done for DBus messages: |
| 4 | // |
| 5 | // 1. User clicks any of the checkboxes for the grouping fields (NetFN, CMD) |
| 6 | // 2. OnGroupByConditionChanged() is called |
| 7 | // 3. OnGroupByConditionChanged() calls PreProcess() and Group() |
| 8 | // 4. PreProcess() takes the IPMI messages extracted from the DBus capture |
| 9 | // (g_ipmi_parsed_entries), and determines the start time. |
| 10 | // 5. Group() takes the IPMI messages, and the list of keys, and groups the messages |
| 11 | // by the keys. The output is picked up by GenerateTimeLine(), which writes the |
| 12 | // timeline data into the Intervals and Titles arrays. The draw loop immediately |
| 13 | // picks up the updated Intervals and Titles arrays and draws on the canvas |
| 14 | // accordingly. |
| 15 | |
Sui Chen | b53fa1b | 2022-05-26 00:16:42 -0700 | [diff] [blame] | 16 | const {dialog} = require('electron'); |
Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 17 | const {fs} = require('file-system'); |
| 18 | const {util} = require('util'); |
| 19 | const {exec} = require('child_process'); |
| 20 | |
| 21 | // Main view objects |
| 22 | var ipmi_timeline_view = new IPMITimelineView(); |
| 23 | ipmi_timeline_view.IsTimeDistributionEnabled = true; |
| 24 | |
| 25 | var btn_start_capture = document.getElementById('btn_start_capture'); |
| 26 | var select_capture_mode = document.getElementById('select_capture_mode'); |
| 27 | var capture_info = document.getElementById('capture_info'); |
| 28 | |
| 29 | var radio_open_file = document.getElementById('radio_open_file'); |
| 30 | var radio_capture = document.getElementById('radio_capture'); |
| 31 | var title_open_file = document.getElementById('title_open_file'); |
| 32 | var title_capture = document.getElementById('title_capture'); |
| 33 | |
| 34 | // Set up Electron-related stuff here; Electron does not allow inlining button |
| 35 | // events |
| 36 | document.getElementById('c1').addEventListener( |
| 37 | 'click', OnGroupByConditionChanged); // NetFN |
| 38 | document.getElementById('c2').addEventListener( |
| 39 | 'click', OnGroupByConditionChanged); // CMD |
| 40 | |
| 41 | // Zoom in button |
| 42 | document.getElementById('btn_zoom_in').addEventListener('click', function() { |
| 43 | ipmi_timeline_view.BeginZoomAnimation(0.5); |
| 44 | boost_asio_handler_timeline_view.BeginZoomAnimation(0.5); |
| 45 | }); |
| 46 | |
| 47 | // Zoom out button |
| 48 | document.getElementById('btn_zoom_out').addEventListener('click', function() { |
| 49 | ipmi_timeline_view.BeginZoomAnimation(-1); |
| 50 | boost_asio_handler_timeline_view.BeginZoomAnimation(-1); |
| 51 | }); |
| 52 | |
| 53 | // Pan left button |
| 54 | document.getElementById('btn_pan_left').addEventListener('click', function() { |
| 55 | ipmi_timeline_view.BeginPanScreenAnimaton(-0.5); |
| 56 | boost_asio_handler_timeline_view.BeginPanScreenAnimaton(-0.5); |
| 57 | }); |
| 58 | |
| 59 | // Pan right button |
| 60 | document.getElementById('btn_pan_right').addEventListener('click', function() { |
| 61 | ipmi_timeline_view.BeginPanScreenAnimaton(0.5); |
| 62 | boost_asio_handler_timeline_view.BeginPanScreenAnimaton(0.5); |
| 63 | }); |
| 64 | |
| 65 | // Reset zoom button |
| 66 | document.getElementById('btn_zoom_reset').addEventListener('click', function() { |
| 67 | ipmi_timeline_view.BeginSetBoundaryAnimation( |
| 68 | RANGE_LEFT_INIT, RANGE_RIGHT_INIT) |
| 69 | dbus_timeline_view.BeginSetBoundaryAnimation( |
| 70 | RANGE_LEFT_INIT, RANGE_RIGHT_INIT) |
| 71 | boost_asio_handler_timeline_view.BeginSetBoundaryAnimation( |
| 72 | RANGE_LEFT_INIT, RANGE_RIGHT_INIT) |
| 73 | }) |
| 74 | |
| 75 | // Generate replay |
| 76 | document.getElementById('gen_replay_ipmitool1') |
| 77 | .addEventListener('click', function() { |
| 78 | GenerateIPMIToolIndividualCommandReplay(HighlightedRequests); |
| 79 | }); |
| 80 | document.getElementById('gen_replay_ipmitool2') |
| 81 | .addEventListener('click', function() { |
| 82 | GenerateIPMIToolExecListReplay(HighlightedRequests); |
| 83 | }); |
| 84 | document.getElementById('gen_replay_ipmid_legacy') |
| 85 | .addEventListener('click', function() { |
| 86 | GenerateBusctlReplayLegacyInterface(HighlightedRequests); |
| 87 | }); |
| 88 | document.getElementById('gen_replay_ipmid_new') |
| 89 | .addEventListener('click', function() { |
| 90 | GenerateBusctlReplayNewInterface(HighlightedRequests); |
| 91 | }); |
| 92 | document.getElementById('btn_start_capture') |
| 93 | .addEventListener('click', function() { |
| 94 | let h = document.getElementById('text_hostname').value; |
| 95 | g_capture_state = 'started'; |
| 96 | StartCapture(h); |
| 97 | }); |
| 98 | |
| 99 | // For capture mode |
| 100 | document.getElementById('btn_stop_capture') |
| 101 | .addEventListener('click', function() { |
| 102 | StopCapture(); |
| 103 | }); |
| 104 | document.getElementById('select_capture_mode') |
| 105 | .addEventListener('click', OnCaptureModeChanged); |
| 106 | radio_open_file.addEventListener('click', OnAppModeChanged); |
| 107 | radio_capture.addEventListener('click', OnAppModeChanged); |
| 108 | |
| 109 | radio_open_file.click(); |
| 110 | |
| 111 | // App mode: open file or capture |
| 112 | function OnAppModeChanged() { |
| 113 | title_open_file.style.display = 'none'; |
| 114 | title_capture.style.display = 'none'; |
| 115 | if (radio_open_file.checked) { |
| 116 | title_open_file.style.display = 'block'; |
| 117 | } |
| 118 | if (radio_capture.checked) { |
| 119 | title_capture.style.display = 'block'; |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // Capture mode: Live capture or staged capture |
| 124 | function OnCaptureModeChanged() { |
| 125 | let x = select_capture_mode; |
| 126 | let i = capture_info; |
| 127 | let desc = ''; |
| 128 | switch (x.value) { |
| 129 | case 'live': |
| 130 | desc = 'Live: read BMC\'s dbus-monitor console output directly'; |
| 131 | g_capture_mode = 'live'; |
| 132 | break; |
| 133 | case 'staged': |
| 134 | desc = |
| 135 | 'Staged, IPMI only: Store BMC\'s dbus-monitor output in a file and transfer back for display'; |
| 136 | g_capture_mode = 'staged'; |
| 137 | break; |
| 138 | case 'staged2': |
| 139 | desc = |
| 140 | 'Staged, DBus + IPMI: Store BMC\'s busctl output in a file and transfer back for display'; |
| 141 | g_capture_mode = 'staged2'; |
| 142 | break; |
| 143 | } |
| 144 | i.textContent = desc; |
| 145 | } |
| 146 | |
| 147 | // Data |
| 148 | var HistoryHistogram = []; |
| 149 | // var Data_IPMI = [] |
| 150 | |
| 151 | // ===================== |
| 152 | |
| 153 | let Intervals = []; |
| 154 | let Titles = []; |
| 155 | let HighlightedRequests = []; |
| 156 | let GroupBy = []; |
| 157 | let GroupByStr = ''; |
| 158 | |
| 159 | // (NetFn, Cmd) -> [ Bucket Indexes ] |
| 160 | // Normalized (0~1) bucket index for the currently highlighted IPMI requests |
| 161 | let IpmiVizHistHighlighted = {}; |
| 162 | let HistogramThresholds = {}; |
| 163 | |
| 164 | function IsIntersected(i0, i1) { |
| 165 | return (!((i0[1] < i1[0]) || (i0[0] > i1[1]))); |
| 166 | } |
| 167 | |
| 168 | function IsIntersectedPixelCoords(i0, i1) { |
| 169 | if (i0[1] == undefined || isNaN(i0[1])) { |
| 170 | return (Math.abs(i0[0] - i1[0]) < 5); |
| 171 | } else { |
| 172 | return (IsIntersected(i0, i1)); |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | var NetFnCmdToDescription = { |
| 177 | '6, 1': 'App-GetDeviceId', |
| 178 | '6, 3': 'App-WarmReset', |
| 179 | '10, 64': 'Storage-GetSelInfo', |
| 180 | '10, 35': 'Storage-GetSdr', |
| 181 | '4, 32': 'Sensor-GetDeviceSDRInfo', |
| 182 | '4, 34': 'Sensor-ReserveDeviceSDRRepo', |
| 183 | '4, 47': 'Sensor-GetSensorType', |
| 184 | '10, 34': 'Storage-ReserveSdrRepository', |
| 185 | '46, 50': 'OEM Extension', |
| 186 | '4, 39': 'Sensor-GetSensorThresholds', |
| 187 | '4, 45': 'Sensor-GetSensorReading', |
| 188 | '10, 67': 'Storage-GetSelEntry', |
| 189 | '58, 196': 'IBM_OEM', |
| 190 | '10, 32': 'Storage-GetSdrRepositoryInfo', |
| 191 | '4, 33': 'Sensor-GetDeviceSDR', |
| 192 | '6, 54': 'App-Get BT Interface Capabilities', |
| 193 | '10, 17': 'Storage-ReadFruData', |
| 194 | '10, 16': 'Storage-GetFruInventoryAreaInfo', |
| 195 | '4, 2': 'Sensor-PlatformEvent', |
| 196 | '4, 48': 'Sensor-SetSensor', |
| 197 | '6, 34': 'App-ResetWatchdogTimer' |
| 198 | }; |
| 199 | |
| 200 | const CANVAS_H = document.getElementById('my_canvas_ipmi').height; |
| 201 | const CANVAS_W = document.getElementById('my_canvas_ipmi').width; |
| 202 | |
| 203 | var LowerBoundTime = RANGE_LEFT_INIT; |
| 204 | var UpperBoundTime = RANGE_RIGHT_INIT; |
| 205 | var LastTimeLowerBound; |
| 206 | var LastTimeUpperBound; |
| 207 | // Dirty flags for determining when to redraw the canvas |
| 208 | let IsCanvasDirty = true; |
| 209 | let IsHighlightDirty = false; |
| 210 | // Animating left and right boundaries |
| 211 | let IsAnimating = false; |
| 212 | let LowerBoundTimeTarget = LowerBoundTime; |
| 213 | let UpperBoundTimeTarget = UpperBoundTime; |
| 214 | // For keyboard interaction: arrow keys and Shift |
| 215 | let CurrDeltaX = 0; // Proportion of Canvas to scroll per frame. |
| 216 | let CurrDeltaZoom = 0; // Delta zoom per frame. |
| 217 | let CurrShiftFlag = false; // Whether the Shift key is depressed |
| 218 | |
| 219 | // 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 |
| 220 | const LEFT_MARGIN = 640 |
| 221 | const RIGHT_MARGIN = 1390; |
| 222 | const LINE_HEIGHT = 15; |
| 223 | const LINE_SPACING = 17; |
| 224 | const YBEGIN = 22 + LINE_SPACING; |
| 225 | 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 |
| 226 | const BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT = LINE_HEIGHT; |
| 227 | const TEXT_Y0 = 3; |
| 228 | const HISTOGRAM_W = 100, HISTOGRAM_H = LINE_SPACING; |
| 229 | const HISTOGRAM_X = 270; |
| 230 | // If some request's time is beyond the right tail, it's considered "too long" |
| 231 | // If some request's time is below the left tail it's considered "good" |
| 232 | // const HISTOGRAM_LEFT_TAIL_WIDTH = 0.05, HISTOGRAM_RIGHT_TAIL_WIDTH = 0.05; |
| 233 | // temporarily disabled for now |
| 234 | const HISTOGRAM_LEFT_TAIL_WIDTH = -1, HISTOGRAM_RIGHT_TAIL_WIDTH = -1; |
| 235 | const SCROLL_BAR_WIDTH = 16; |
| 236 | |
| 237 | let IpmiVizHistogramImageData = {}; // Image data for rendered histogram |
| 238 | |
| 239 | // Input is the data that's completed layout |
| 240 | // is_free_x: Should each histogram has its own X range or not |
| 241 | // num_buckets: # of buckets for histograms |
| 242 | // theta: top and bottom portion to cut |
| 243 | function ComputeHistogram(num_buckets = 30, is_free_x = true) { |
| 244 | let global_lb = Infinity, global_ub = -Infinity; |
| 245 | IpmiVizHistogramImageData = {}; |
| 246 | // Global minimal and maximal values |
| 247 | for (let i = 0; i < Intervals.length; i++) { |
| 248 | let interval = Intervals[i]; |
| 249 | let l = Math.min.apply(Math, interval.map(function(x) { |
| 250 | return x[1] - x[0]; |
| 251 | })); |
| 252 | let u = Math.max.apply(Math, interval.map(function(x) { |
| 253 | return x[1] - x[0]; |
| 254 | })); |
| 255 | global_lb = Math.min(l, global_lb); |
| 256 | global_ub = Math.max(u, global_ub); |
| 257 | } |
| 258 | |
| 259 | HistoryHistogram = []; |
| 260 | for (let i = 0; i < Intervals.length; i++) { |
| 261 | let interval = Intervals[i]; |
| 262 | let lb = global_lb, ub = global_ub; |
| 263 | if (is_free_x == true) { |
| 264 | lb = Math.min.apply(Math, interval.map(function(x) { |
| 265 | return x[1] - x[0]; |
| 266 | })); |
| 267 | ub = Math.max.apply(Math, interval.map(function(x) { |
| 268 | return x[1] - x[0]; |
| 269 | })); |
| 270 | } |
| 271 | const EPS = 1e-2; |
| 272 | if (lb == ub) ub = lb + EPS; |
| 273 | let line = [lb * 1000000, ub * 1000000]; // to usec |
| 274 | let buckets = []; |
| 275 | for (let j = 0; j < num_buckets; j++) buckets.push(0); |
| 276 | for (let j = 0; j < interval.length; j++) { |
| 277 | let t = interval[j][1] - interval[j][0]; |
| 278 | let bucket_idx = parseInt(t / ((ub - lb) / num_buckets)); |
| 279 | buckets[bucket_idx]++; |
| 280 | } |
| 281 | line.push(buckets); |
| 282 | HistoryHistogram[Titles[i].title] = line; |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | function Preprocess(data) { |
| 287 | preprocessed = []; |
| 288 | let StartingUsec_IPMI; |
| 289 | |
| 290 | if (g_StartingSec == undefined) { |
| 291 | StartingUsec_IPMI = undefined; |
| 292 | } else { |
| 293 | StartingUsec_IPMI = g_StartingSec * 1000000; |
| 294 | } |
| 295 | |
| 296 | for (let i = 0; i < data.length; i++) { |
| 297 | let entry = data[i].slice(); |
| 298 | let lb = entry[2], ub = entry[3]; |
| 299 | |
| 300 | // Only when IPMI view is present (i.e. no DBus pcap is loaded) |
| 301 | if (i == 0 && StartingUsec_IPMI == undefined) { |
| 302 | StartingUsec_IPMI = lb; |
| 303 | } |
| 304 | |
| 305 | entry[2] = lb - StartingUsec_IPMI; |
| 306 | entry[3] = ub - StartingUsec_IPMI; |
| 307 | preprocessed.push(entry); |
| 308 | } |
| 309 | return preprocessed; |
| 310 | } |
| 311 | |
| 312 | let SHOW_BLOB_DETAILS = true; |
| 313 | function Group(data, groupBy) { |
| 314 | let grouped = {}; |
| 315 | |
| 316 | // If has netfn and cmd: use "NetFN, CMD" as key |
| 317 | // Otherwise, use "NetFN" as key |
| 318 | // This distinction is made if the user chooses to label operation on each |
| 319 | // blob individually |
| 320 | |
| 321 | // Key: blob name |
| 322 | // Value: the commands that operate on the particular blob |
| 323 | let sid2blobid = {} |
| 324 | |
| 325 | for (let n = 0; n < data.length; n++) { |
| 326 | const p = data[n]; |
| 327 | const netfn = p[0], cmd = p[1], req = p[4], res = p[5]; |
| 328 | if (netfn == 46 && cmd == 128) { |
| 329 | const oen = req[0] + req[1] * 256 + req[2] * 65536; |
| 330 | if (oen == 0xc2cf) { // Blob operations |
| 331 | const blobcmd = |
| 332 | req[3]; // Refer to https://github.com/openbmc/phosphor-ipmi-blobs |
| 333 | |
| 334 | // The IPMI blob commands are visible on DBus, another WIP command-line tool that |
| 335 | // utilizes this fact to show information about blobs can be found here: |
| 336 | // https://gerrit.openbmc-project.xyz/c/openbmc/openbmc-tools/+/41451 |
| 337 | |
| 338 | let sid, blobid; |
| 339 | |
| 340 | // layout of req |
| 341 | // 0 1 2 3 4 5 6 7 8 9 10 ... |
| 342 | // CF C2 00 CMD [CRC ] [ other stuff ] |
| 343 | |
| 344 | // layout of res |
| 345 | // 0 1 2 3 4 5 6 7 8 ... |
| 346 | // CF C2 00 [CRC ] [other stuff] |
| 347 | |
| 348 | // Determining blob id and session ID |
| 349 | switch (blobcmd) { |
| 350 | case 3: |
| 351 | case 4: |
| 352 | case 5: |
| 353 | case 6: |
| 354 | case 9: |
| 355 | case 10: { |
| 356 | const sid = req[6] + req[7] * 256; |
| 357 | blobid = sid2blobid[sid]; |
| 358 | if (blobid != undefined) { |
| 359 | p.key = blobid; |
| 360 | } |
| 361 | break; |
| 362 | } |
| 363 | case 7: |
| 364 | case 8: { |
| 365 | blobid = ''; |
| 366 | for (let i = 6; i < req.length; i++) { |
| 367 | blobid += String.fromCharCode(req[i]); |
| 368 | } |
| 369 | break; |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | switch (blobcmd) { |
| 374 | case 2: { // open |
| 375 | blobid = ''; |
| 376 | for (let i = 8; i < req.length; i++) { |
| 377 | if (req[i] == 0) |
| 378 | break; |
| 379 | else |
| 380 | blobid += String.fromCharCode(req[i]); |
| 381 | } |
| 382 | p.key = blobid; |
| 383 | sid = res[5] + res[6] * 256; // session_id |
| 384 | sid2blobid[sid] = blobid; |
| 385 | break; |
| 386 | } |
| 387 | case 3: { // Read |
| 388 | |
| 389 | break; |
| 390 | } |
| 391 | case 4: { // Write |
| 392 | const offset = |
| 393 | req[8] + req[9] * 256 + req[10] * 65536 + req[11] * 16777216; |
| 394 | p.offset = offset; |
| 395 | break; |
| 396 | } |
| 397 | case 5: { // Commit |
| 398 | break; |
| 399 | } |
| 400 | case 6: { // Close |
| 401 | break; |
| 402 | } |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | const idxes = {'NetFN': 0, 'CMD': 1}; |
| 409 | |
| 410 | // |
| 411 | for (let n = 0; n < data.length; n++) { |
| 412 | const p = data[n]; |
| 413 | let key = ''; |
| 414 | if (p.key != undefined) |
| 415 | key = p.key; |
| 416 | else if (p[0] != '' && p[1] != '') { |
| 417 | for (let i = 0; i < groupBy.length; i++) { |
| 418 | if (i > 0) { |
| 419 | key += ', '; |
| 420 | } |
| 421 | key += p[idxes[groupBy[i]]]; |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | if (grouped[key] == undefined) { |
| 426 | grouped[key] = []; |
| 427 | } |
| 428 | grouped[key].push(p); |
| 429 | } |
| 430 | |
| 431 | return grouped; |
| 432 | } |
| 433 | |
| 434 | function GenerateTimeLine(grouped) { |
| 435 | const keys = Object.keys(grouped); |
| 436 | let sortedKeys = keys.slice(); |
| 437 | // If NetFN and CMD are both selected, sort by NetFN then CMD |
| 438 | // In this case, all "keys" are string-encoded integer pairs |
| 439 | if (keys.length > 0 && ipmi_timeline_view.GroupBy.length == 2) { |
| 440 | sortedKeys = sortedKeys.sort(function(a, b) { |
| 441 | a = a.split(','); |
| 442 | b = b.split(','); |
| 443 | if (a.length == 2 && b.length == 2) { |
| 444 | let aa = parseInt(a[0]) * 256 + parseInt(a[1]); |
| 445 | let bb = parseInt(b[0]) * 256 + parseInt(b[1]); |
| 446 | return aa < bb ? -1 : (aa > bb ? 1 : 0); |
| 447 | } else { |
| 448 | return a < b ? -1 : (a > b ? 1 : 0); |
| 449 | } |
| 450 | }); |
| 451 | } |
| 452 | |
| 453 | Intervals = []; |
| 454 | Titles = []; |
| 455 | for (let i = 0; i < sortedKeys.length; i++) { |
| 456 | Titles.push({"header":false, "title":sortedKeys[i], "intervals_idxes":[i]}); |
| 457 | line = []; |
| 458 | for (let j = 0; j < grouped[sortedKeys[i]].length; j++) { |
| 459 | let entry = grouped[sortedKeys[i]][j]; |
| 460 | // Lower bound, Upper bound, and a reference to the original request |
| 461 | line.push([ |
| 462 | parseFloat(entry[2]) / 1000000, parseFloat(entry[3]) / 1000000, entry, |
| 463 | 'ok', 0 |
| 464 | ]); |
| 465 | } |
| 466 | Intervals.push(line); |
| 467 | } |
| 468 | |
| 469 | ipmi_timeline_view.Intervals = Intervals.slice(); |
| 470 | ipmi_timeline_view.Titles = Titles.slice(); |
| 471 | ipmi_timeline_view.LayoutForOverlappingIntervals(); |
| 472 | } |
| 473 | |
| 474 | function OnGroupByConditionChanged() { |
| 475 | const tags = ['c1', 'c2']; |
| 476 | const v = ipmi_timeline_view; |
| 477 | v.GroupBy = []; |
| 478 | v.GroupByStr = ''; |
| 479 | for (let i = 0; i < tags.length; i++) { |
| 480 | let cb = document.getElementById(tags[i]); |
| 481 | if (cb.checked) { |
| 482 | v.GroupBy.push(cb.value); |
| 483 | if (v.GroupByStr.length > 0) { |
| 484 | v.GroupByStr += ', '; |
| 485 | } |
| 486 | v.GroupByStr += cb.value; |
| 487 | } |
| 488 | } |
| 489 | let preproc = Preprocess(Data_IPMI); |
| 490 | grouped = Group(preproc, v.GroupBy); |
| 491 | GenerateTimeLine(grouped); |
| 492 | |
| 493 | IsCanvasDirty = true; |
| 494 | ipmi_timeline_view.IsCanvasDirty = true; |
| 495 | } |
| 496 | |
| 497 | function MapXCoord(x, left_margin, right_margin, rl, rr) { |
| 498 | let ret = left_margin + (x - rl) / (rr - rl) * (right_margin - left_margin); |
| 499 | if (ret < left_margin) { |
| 500 | ret = left_margin; |
| 501 | } else if (ret > right_margin) { |
| 502 | ret = right_margin; |
| 503 | } |
| 504 | return ret; |
| 505 | } |
| 506 | |
| 507 | function draw_timeline(ctx) { |
| 508 | ipmi_timeline_view.Render(ctx); |
| 509 | } |
| 510 | |
| 511 | |
| 512 | window.addEventListener('keydown', function() { |
| 513 | if (event.keyCode == 37) { // Left Arrow |
| 514 | ipmi_timeline_view.CurrDeltaX = -0.004; |
| 515 | dbus_timeline_view.CurrDeltaX = -0.004; |
| 516 | } else if (event.keyCode == 39) { // Right arrow |
| 517 | ipmi_timeline_view.CurrDeltaX = 0.004; |
| 518 | dbus_timeline_view.CurrDeltaX = 0.004; |
| 519 | } else if (event.keyCode == 16) { // Shift |
| 520 | ipmi_timeline_view.CurrShiftFlag = true; |
| 521 | dbus_timeline_view.CurrShiftFlag = true; |
| 522 | } else if (event.keyCode == 38) { // Up arrow |
| 523 | ipmi_timeline_view.CurrDeltaZoom = 0.01; |
| 524 | dbus_timeline_view.CurrDeltaZoom = 0.01; |
| 525 | } else if (event.keyCode == 40) { // Down arrow |
| 526 | ipmi_timeline_view.CurrDeltaZoom = -0.01; |
| 527 | dbus_timeline_view.CurrDeltaZoom = -0.01; |
| 528 | } |
| 529 | }); |
| 530 | |
| 531 | window.addEventListener('keyup', function() { |
| 532 | if (event.keyCode == 37 || event.keyCode == 39) { |
| 533 | ipmi_timeline_view.CurrDeltaX = 0; |
| 534 | dbus_timeline_view.CurrDeltaX = 0; |
| 535 | } else if (event.keyCode == 16) { |
| 536 | ipmi_timeline_view.CurrShiftFlag = false; |
| 537 | dbus_timeline_view.CurrShiftFlag = false; |
| 538 | } else if (event.keyCode == 38 || event.keyCode == 40) { |
| 539 | ipmi_timeline_view.CurrDeltaZoom = 0; |
| 540 | dbus_timeline_view.CurrDeltaZoom = 0; |
| 541 | } |
| 542 | }); |
| 543 | |
| 544 | function MouseXToTimestamp(x) { |
| 545 | let ret = (x - LEFT_MARGIN) / (RIGHT_MARGIN - LEFT_MARGIN) * |
| 546 | (UpperBoundTime - LowerBoundTime) + |
| 547 | LowerBoundTime; |
| 548 | ret = Math.max(ret, LowerBoundTime); |
| 549 | ret = Math.min(ret, UpperBoundTime); |
| 550 | return ret; |
| 551 | } |
| 552 | |
| 553 | let HighlightedRegion = {t0: -999, t1: -999}; |
| 554 | |
| 555 | function IsHighlighted() { |
| 556 | return (HighlightedRegion.t0 != -999 && HighlightedRegion.t1 != -999); |
| 557 | } |
| 558 | |
| 559 | function Unhighlight() { |
| 560 | HighlightedRegion.t0 = -999; |
| 561 | HighlightedRegion.t1 = -999; |
| 562 | } |
| 563 | |
| 564 | function UnhighlightIfEmpty() { |
| 565 | if (HighlightedRegion.t0 == HighlightedRegion.t1) { |
| 566 | Unhighlight(); |
| 567 | return true; |
| 568 | } |
| 569 | return false; |
| 570 | } |
| 571 | |
| 572 | let MouseState = { |
| 573 | hovered: true, |
| 574 | pressed: false, |
| 575 | x: 0, |
| 576 | y: 0, |
| 577 | hoveredVisibleLineIndex: -999, |
Sui Chen | 27cf933 | 2021-11-03 16:20:28 -0700 | [diff] [blame] | 578 | hoveredSide: undefined, |
| 579 | IsHoveredOverHorizontalScrollbar: function() { |
| 580 | if (this.hoveredSide == "top_horizontal_scrollbar") return true; |
| 581 | else if (this.hoveredSide == "bottom_horizontal_scrollbar") return true; |
| 582 | else return false; |
| 583 | } |
Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 584 | }; |
| 585 | let Canvas = document.getElementById('my_canvas_ipmi'); |
| 586 | |
| 587 | Canvas.onmousemove = function(event) { |
| 588 | const v = ipmi_timeline_view; |
| 589 | v.MouseState.x = event.pageX - this.offsetLeft; |
| 590 | v.MouseState.y = event.pageY - this.offsetTop; |
Sui Chen | 27cf933 | 2021-11-03 16:20:28 -0700 | [diff] [blame] | 591 | if (v.MouseState.pressed == true && |
| 592 | v.MouseState.hoveredSide == 'timeline') { // Update highlighted area |
Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 593 | v.HighlightedRegion.t1 = v.MouseXToTimestamp(v.MouseState.x); |
| 594 | } |
| 595 | v.OnMouseMove(); |
| 596 | v.IsCanvasDirty = true; |
| 597 | |
| 598 | v.linked_views.forEach(function(u) { |
| 599 | u.MouseState.x = event.pageX - Canvas.offsetLeft; |
| 600 | u.MouseState.y = 0; // Do not highlight any entry |
Sui Chen | 27cf933 | 2021-11-03 16:20:28 -0700 | [diff] [blame] | 601 | if (u.MouseState.pressed == true && |
| 602 | u.MouseState.hoveredSide == 'timeline') { // Update highlighted area |
Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 603 | u.HighlightedRegion.t1 = u.MouseXToTimestamp(u.MouseState.x); |
| 604 | } |
| 605 | u.OnMouseMove(); |
| 606 | u.IsCanvasDirty = true; |
| 607 | }); |
| 608 | }; |
| 609 | |
| 610 | Canvas.onmouseover = function() { |
| 611 | ipmi_timeline_view.OnMouseMove(); |
| 612 | }; |
| 613 | |
| 614 | Canvas.onmouseleave = function() { |
| 615 | ipmi_timeline_view.OnMouseLeave(); |
| 616 | }; |
| 617 | |
| 618 | Canvas.onmousedown = function(event) { |
| 619 | if (event.button == 0) { // Left mouse button |
| 620 | ipmi_timeline_view.OnMouseDown(); |
| 621 | } |
| 622 | }; |
| 623 | |
| 624 | Canvas.onmouseup = function(event) { |
| 625 | if (event.button == 0) { |
| 626 | ipmi_timeline_view.OnMouseUp(); |
| 627 | // page-specific, not view-specific |
| 628 | let hint = document.getElementById('highlight_hint'); |
| 629 | if (ipmi_timeline_view.UnhighlightIfEmpty()) { |
| 630 | hint.style.display = 'none'; |
| 631 | } else { |
| 632 | hint.style.display = 'block'; |
| 633 | } |
| 634 | } |
| 635 | }; |
| 636 | |
| 637 | Canvas.onwheel = function(event) { |
| 638 | ipmi_timeline_view.OnMouseWheel(event); |
| 639 | }; |
| 640 | |
| 641 | // This function is not specific to TimelineView so putting it here |
| 642 | function OnHighlightedChanged(reqs) { |
| 643 | let x = document.getElementById('ipmi_replay'); |
| 644 | let i = document.getElementById('ipmi_replay_output'); |
| 645 | let cnt = document.getElementById('highlight_count'); |
| 646 | cnt.innerHTML = '' + reqs.length; |
| 647 | i.style.display = 'none'; |
| 648 | if (reqs.length > 0) { |
| 649 | x.style.display = 'block'; |
| 650 | } else |
| 651 | x.style.display = 'none'; |
| 652 | let o = document.getElementById('ipmi_replay_output'); |
| 653 | o.style.display = 'none'; |
| 654 | o.textContent = ''; |
| 655 | } |
| 656 | |
| 657 | function ToHexString(bytes, prefix, sep) { |
| 658 | let ret = ''; |
| 659 | for (let i = 0; i < bytes.length; i++) { |
| 660 | if (i > 0) { |
| 661 | ret += sep; |
| 662 | } |
| 663 | ret += prefix + bytes[i].toString(16); |
| 664 | } |
| 665 | return ret; |
| 666 | } |
| 667 | |
| 668 | function ToASCIIString(bytes) { |
| 669 | ret = ''; |
| 670 | for (let i = 0; i < bytes.length; i++) { |
| 671 | ret = ret + String.fromCharCode(bytes[i]); |
| 672 | } |
| 673 | return ret; |
| 674 | } |
| 675 | |
| 676 | function ShowReplayOutputs(x, ncols) { |
| 677 | let o = document.getElementById('ipmi_replay_output'); |
| 678 | o.cols = ncols; |
| 679 | o.style.display = 'block'; |
| 680 | o.textContent = x; |
| 681 | } |
| 682 | |
| 683 | function GenerateIPMIToolIndividualCommandReplay(reqs) { |
| 684 | let x = ''; |
| 685 | for (let i = 0; i < reqs.length; i++) { |
| 686 | let req = reqs[i]; |
| 687 | // [0]: NetFN, [1]: cmd, [4]: payload |
| 688 | // NetFN and cmd are DECIMAL while payload is HEXADECIMAL. |
| 689 | x = x + 'ipmitool raw ' + req[0] + ' ' + req[1] + ' ' + |
| 690 | ToHexString(req[4], '0x', ' ') + '\n'; |
| 691 | } |
| 692 | ShowReplayOutputs(x, 80); |
| 693 | } |
| 694 | |
| 695 | function GenerateIPMIToolExecListReplay(reqs) { |
| 696 | console.log(reqs.length); |
| 697 | let x = ''; |
| 698 | for (let i = 0; i < reqs.length; i++) { |
| 699 | let req = reqs[i]; |
| 700 | x = x + 'raw ' + |
| 701 | ToHexString([req[0]].concat([req[1]]).concat(req[4]), '0x', ' ') + '\n'; |
| 702 | } |
| 703 | ShowReplayOutputs(x, 80); |
| 704 | } |
| 705 | |
| 706 | function GenerateBusctlReplayLegacyInterface(reqs) { |
| 707 | console.log(reqs.length); |
| 708 | let serial = 0; |
| 709 | let x = ''; |
| 710 | for (let i = 0; i < reqs.length; i++) { |
| 711 | let req = reqs[i]; |
| 712 | x = x + |
| 713 | 'busctl --system emit /org/openbmc/HostIpmi/1 org.openbmc.HostIpmi ReceivedMessage yyyyay '; |
| 714 | x = x + serial + ' ' + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' + |
| 715 | ToHexString(req[4], '0x', ' ') + '\n'; |
| 716 | serial = (serial + 1) % 256; |
| 717 | } |
| 718 | ShowReplayOutputs(x, 120); |
| 719 | } |
| 720 | |
| 721 | function GenerateBusctlReplayNewInterface(reqs) { |
| 722 | console.log(reqs.length); |
| 723 | let x = ''; |
| 724 | for (let i = 0; i < reqs.length; i++) { |
| 725 | let req = reqs[i]; |
| 726 | x = x + |
| 727 | 'busctl --system call xyz.openbmc_project.Ipmi.Host /xyz/openbmc_project/Ipmi xyz.openbmc_project.Ipmi.Server execute yyyaya{sv} '; |
| 728 | x = x + req[0] + ' 0 ' + req[1] + ' ' + req[4].length + ' ' + |
| 729 | ToHexString(req[4], '0x', ' '); |
| 730 | +' 0\n'; |
| 731 | } |
| 732 | ShowReplayOutputs(x, 150); |
| 733 | } |