dbus-vis: Initial commit

dbus-vis visualizes PCAP files captured by busctl as well as
text-format outputs from busctl.

dbus-vis displays the visualized results in horizontal
timelines, where each horizontal bar represents a DBus request, or
an IPMI request (an IPMI request is a specific kind of DBus request
sent to the IPMI daemon.)
The requests may be grouped by one or more fields including
type, sender, destination, path, interface, member.

In addition, if the BMC console is available, dbus-vis can connect
to the BMC console and capture DBus/IPMI requests in real time.

dbus-vis is in the form of a web page and is wrapped using Electron
to run as a desktop app.

Basic usage:
* Mouse click and drag:
  * If mouse is over time axis: drag timeline horizontally
  * If mouse is over vertical scroll bar: scroll up/down
  * Otherwise: select a region for measuring time & zooming in
* Mouse Scroll wheel:
  * If mouse is over time axis: zoom in/out
  * Otherwise: scroll up/down
* Click on the expandable headers will expand/collapse the entries under
that particular header.

Signed-off-by: Sui Chen <suichen@google.com>
Change-Id: I863c2ba80025d10efb44fd12868e37912fae9a47
diff --git a/dbus-vis/ipmi_parse.js b/dbus-vis/ipmi_parse.js
new file mode 100644
index 0000000..6323268
--- /dev/null
+++ b/dbus-vis/ipmi_parse.js
@@ -0,0 +1,479 @@
+// This file parses ASCII text-encoded dbus message dump.
+
+function extractUsec(line) {
+  let i0 = line.indexOf('time=');
+  if (i0 == -1) {
+    return BigInt(-1);
+  }
+  let line1 = line.substr(i0);
+  let i1 = line1.indexOf(' ');
+  if (i1 == -1) {
+    return BigInt(-1);
+  }
+  let line2 = line1.substr(5, i1 - 5);
+  let sp = line2.split('.');
+  return BigInt(sp[0]) * BigInt(1000000) + BigInt(sp[1]);
+}
+
+function extractInt(line, kw) {
+  let N = kw.length;
+  let i0 = line.indexOf(kw);
+  if (i0 == -1) {
+    return null;
+  }
+  let line1 = line.substr(i0);
+  let i1 = line1.indexOf(' ');
+  if (i1 == -1) {
+    i1 = line.length;
+  }
+  let line2 = line1.substr(N, i1 - N);
+  return parseInt(line2);
+}
+
+function extractSerial(line) {
+  return extractInt(line, 'serial=');
+}
+
+function extractReplySerial(line) {
+  return extractInt(line, 'reply_serial=');
+}
+
+// Returns [byte, i+1] if successful
+// Returns [null, i  ] if unsuccessful
+function munchByte(lines, i) {
+  if (i >= lines.length) {
+    return [null, i]
+  }
+  let l = lines[i];
+  let idx = l.indexOf('byte');
+  if (idx != -1) {
+    return [parseInt(l.substr(idx + 4), 10), i + 1];
+  } else {
+    return [null, i];
+  }
+}
+
+// array of bytes "@"
+function munchArrayOfBytes1(lines, i) {
+  let l = lines[i];
+  let idx = l.indexOf('array of bytes "');
+  if (idx != -1) {
+    let the_ch = l.substr(idx + 16, 1);
+    return [[the_ch.charCodeAt(0)], i + 1];
+  } else {
+    return [null, i];
+  }
+}
+
+function munchArrayOfBytes2(lines, i) {
+  let l = lines[i];
+  let idx = l.indexOf('array of bytes [');
+  if (idx == -1) {
+    idx = l.indexOf('array [');
+  }
+  if (idx != -1) {
+    let j = i + 1;
+    let payload = [];
+    while (true) {
+      if (j >= lines.length) {
+        break;
+      }
+      l = lines[j];
+      let sp = l.trim().split(' ');
+      let ok = true;
+      for (let k = 0; k < sp.length; k++) {
+        let b = parseInt(sp[k], 16);
+        if (isNaN(b)) {
+          ok = false;
+          break;
+        } else {
+          payload.push(b);
+        }
+      }
+      if (!ok) {
+        j--;
+        break;
+      } else
+        j++;
+    }
+    return [payload, j];
+  } else {
+    return [null, i];
+  }
+}
+
+function munchArrayOfBytes(lines, i) {
+  if (i >= lines.length) return [null, i];
+
+  let x = munchArrayOfBytes1(lines, i);
+  if (x[0] != null) {
+    return x;
+  }
+  x = munchArrayOfBytes2(lines, i);
+  if (x[0] != null) {
+    return x;
+  }
+  return [null, i];
+}
+
+// ReceivedMessage
+function munchLegacyMessageStart(lines, i) {
+  let entry = {
+    netfn: 0,
+    lun: 0,
+    cmd: 0,
+    serial: 0,
+    start_usec: 0,
+    end_usec: 0,
+    request: [],
+    response: []
+  };
+
+  let ts = extractUsec(lines[i]);
+  entry.start_usec = ts;
+
+  let x = munchByte(lines, i + 1);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.serial = x[0];
+  let j = x[1];
+
+  x = munchByte(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.netfn = x[0];
+  j = x[1];
+
+  x = munchByte(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.lun = x[0];
+  j = x[1];
+
+  x = munchByte(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.cmd = x[0];
+  j = x[1];
+
+  x = munchArrayOfBytes(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.request = x[0];
+  j = x[1];
+
+  return [entry, j];
+}
+
+function munchLegacyMessageEnd(lines, i, in_flight, parsed_entries) {
+  let ts = extractUsec(lines[i]);
+
+  let x = munchByte(lines, i + 1);
+  if (x[0] == null) {
+    return [null, i];
+  }  // serial
+  let serial = x[0];
+  let j = x[1];
+
+  let entry = null;
+  if (serial in in_flight) {
+    entry = in_flight[serial];
+    delete in_flight[serial];
+  } else {
+    return [null, i];
+  }
+
+  entry.end_usec = ts;
+
+  x = munchByte(lines, j);  // netfn
+  if (x[0] == null) {
+    return [null, i];
+  }
+  if (entry.netfn + 1 == x[0]) {
+  } else {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchByte(lines, j);  // lun (not used right now)
+  if (x[0] == null) {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchByte(lines, j);  // cmd
+  if (x[0] == null) {
+    return [null, i];
+  }
+  if (entry.cmd == x[0]) {
+  } else {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchByte(lines, j);  // cc
+  if (x[0] == null) {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchArrayOfBytes(lines, j);
+  if (x[0] == null) {
+    entry.response = [];
+  } else {
+    entry.response = x[0];
+  }
+  j = x[1];
+
+  parsed_entries.push(entry);
+
+  return [entry, j];
+}
+
+function munchNewMessageStart(lines, i, in_flight) {
+  let ts = extractUsec(lines[i]);
+  let serial = extractSerial(lines[i]);
+
+  let entry = {
+    netfn: 0,
+    lun: 0,
+    cmd: 0,
+    serial: -999,
+    start_usec: 0,
+    end_usec: 0,
+    request: [],
+    response: []
+  };
+  entry.start_usec = ts;
+  entry.serial = serial;
+
+  let x = munchByte(lines, i + 1);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.netfn = x[0];
+  let j = x[1];
+
+  x = munchByte(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.lun = x[0];
+  j = x[1];
+
+  x = munchByte(lines, j);
+  if (x[0] == null) {
+    return [null, i];
+  }
+  entry.cmd = x[0];
+  j = x[1];
+
+  x = munchArrayOfBytes(lines, j);
+  if (x[0] == null) {
+    entry.request = [];
+  }  // Truncated
+  entry.request = x[0];
+  j = x[1];
+
+  return [entry, j];
+}
+
+function munchNewMessageEnd(lines, i, in_flight, parsed_entries) {
+  let ts = extractUsec(lines[i]);
+  let reply_serial = extractReplySerial(lines[i]);
+
+  let entry = null;
+  if (reply_serial in in_flight) {
+    entry = in_flight[reply_serial];
+    delete in_flight[reply_serial];
+  } else {
+    return [null, i];
+  }
+
+  entry.end_usec = ts;
+
+  let x = munchByte(lines, i + 2);  // Skip "struct {"
+  if (x[0] == null) {
+    return [null, i];
+  }  // NetFN
+  if (entry.netfn + 1 != x[0]) {
+    return [null, i];
+  }
+  let j = x[1];
+
+  x = munchByte(lines, j);  // LUN
+  if (x[0] == null) {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchByte(lines, j);  // CMD
+  if (x[0] == null) {
+    return [null, i];
+  }
+  if (entry.cmd != x[0]) {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchByte(lines, j);  // cc
+  if (x[0] == null) {
+    return [null, i];
+  }
+  j = x[1];
+
+  x = munchArrayOfBytes(lines, j);
+  if (x[0] == null) {
+    entry.response = [];
+  } else {
+    entry.response = x[0];
+  }
+  j = x[1];
+
+  parsed_entries.push(entry);
+
+  return [entry, j];
+}
+
+// Parsing state
+let g_ipmi_parse_buf = '';
+let g_ipmi_parse_lines = [];
+let g_ipmi_in_flight = {};
+let g_ipmi_parsed_entries = [];
+function StartParseIPMIDump() {
+  g_ipmi_parse_lines = [];
+  g_ipmi_parsed_entries = [];
+  g_ipmi_in_flight = {};
+  g_ipmi_parse_buf = '';
+}
+function AppendToParseBuffer(x) {
+  g_ipmi_parse_buf += x;
+}
+function MunchLines() {
+  // 1. Extract all lines from the buffer
+  let chars_munched = 0;
+  while (true) {
+    let idx = g_ipmi_parse_buf.indexOf('\n');
+    if (idx == -1) break;
+    let l = g_ipmi_parse_buf.substr(0, idx);
+    g_ipmi_parse_lines.push(l);
+    g_ipmi_parse_buf = g_ipmi_parse_buf.substr(idx + 1);
+    chars_munched += (idx + 1);
+  }
+  console.log(chars_munched + ' chars munched');
+
+  // 2. Parse as many lines as possible
+  let lidx_last = 0;
+  let i = 0;
+  while (i < g_ipmi_parse_lines.length) {
+    let line = g_ipmi_parse_lines[i];
+    if (line.indexOf('interface=org.openbmc.HostIpmi') != -1 &&
+        line.indexOf('member=ReceivedMessage') != -1) {
+      let x = munchLegacyMessageStart(g_ipmi_parse_lines, i);
+      let entry = x[0];
+      if (i != x[1]) lidx_last = x[1];  // Munch success!
+      i = x[1];
+      if (entry != null) {
+        g_ipmi_in_flight[entry.serial] = entry;
+      }
+    } else if (
+        line.indexOf('interface=org.openbmc.HostIpmi') != -1 &&
+        line.indexOf('member=sendMessage') != -1) {
+      let x = munchLegacyMessageEnd(
+          g_ipmi_parse_lines, i, g_ipmi_in_flight, g_ipmi_parsed_entries);
+      if (i != x[1]) lidx_last = x[1];  // Munch success!
+      i = x[1];
+
+    } else if (
+        line.indexOf('interface=xyz.openbmc_project.Ipmi.Server') != -1 &&
+        line.indexOf('member=execute') != -1) {
+      let x = munchNewMessageStart(g_ipmi_parse_lines, i);
+      let entry = x[0];
+      if (i != x[1]) lidx_last = x[1];
+      i = x[1];
+      if (entry != null) {
+        g_ipmi_in_flight[entry.serial] = entry;
+      }
+    } else if (line.indexOf('method return') != -1) {
+      let x = munchNewMessageEnd(
+          g_ipmi_parse_lines, i, g_ipmi_in_flight, g_ipmi_parsed_entries);
+      if (i != x[1]) lidx_last = x[1];  // Munch success
+      i = x[1];
+    }
+    i++;
+  }
+  g_ipmi_parse_lines = g_ipmi_parse_lines.slice(
+      lidx_last,
+      g_ipmi_parse_lines.length);  // Remove munched lines
+  console.log(
+      lidx_last + ' lines munched, |lines|=' + g_ipmi_parse_lines.length +
+          ', |entries|=' + g_ipmi_parsed_entries.length,
+      ', |inflight|=' + Object.keys(g_ipmi_in_flight).length);
+}
+
+let last_update_time = 0;  // Millis since Unix Epoch
+function UpdateLayout(level) {
+  const this_update_time = new Date().getTime();
+  const over_1s = (this_update_time - last_update_time > 1000);
+  if (!over_1s) {
+    if (level > 0) {
+      setTimeout(function() {
+        UpdateLayout(level - 1);
+      }, 1000);
+    } else {
+      return;
+    }
+  }
+
+  if (g_ipmi_parsed_entries.length > 0) {
+    last_update_time = this_update_time;
+    // Write to Data_IPMI
+    let ts0 = g_ipmi_parsed_entries[0].start_usec;
+    let ts1 = g_ipmi_parsed_entries[g_ipmi_parsed_entries.length - 1].end_usec;
+
+    // When calling from DBus PCap loader, the following happens
+    // >> OnGroupByConditionChanged
+    //   >> Preprocess  <-- Time shift will happen here
+    // So, we don't do time-shifting here
+    let time_shift;
+    if (g_StartingSec != undefined) {
+      time_shift = BigInt(0);
+    } else {  // This is during live capture mode
+      time_shift = ts0;
+    }
+
+    Data_IPMI = [];
+    for (i = 0; i < g_ipmi_parsed_entries.length; i++) {
+      let entry = g_ipmi_parsed_entries[i];
+      let x = [
+        entry.netfn, entry.cmd, parseInt(entry.start_usec - time_shift),
+        parseInt(entry.end_usec - time_shift), entry.request, entry.response
+      ];
+      Data_IPMI.push(x);
+    }
+
+    // Re-calculate time range
+    RANGE_LEFT_INIT = 0;
+    RANGE_RIGHT_INIT =
+        parseInt((ts1 - ts0) / BigInt(1000000) / BigInt(10)) * 10 + 10;
+
+    IsCanvasDirty = true;
+    OnGroupByConditionChanged();
+    
+    ComputeHistogram();
+  } else {
+    console.log('No entries parsed');
+  }
+}
+
+function ParseIPMIDump(data) {
+  StartParseIPMIDump();
+  AppendToParseBuffer(data);
+  MunchLines();
+  UpdateLayout();
+}