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/dbus_timeline_vis.js b/dbus-vis/dbus_timeline_vis.js
new file mode 100644
index 0000000..8d4f583
--- /dev/null
+++ b/dbus-vis/dbus_timeline_vis.js
@@ -0,0 +1,327 @@
+// This file deals with preprocessing the parsed DBus timeline data.
+// Data and Timestamps are separate b/c dbus-pcap does not include
+// timestamps in JSON output so we need to export both formats
+// (JSON and text)
+var Data_DBus = [];
+var Timestamps_DBus = [];
+
+// Main view object
+var dbus_timeline_view = new DBusTimelineView();
+var sensors_timeline_view = new DBusTimelineView(); // Same DBusTimelineView type, just that it will have only sensor propertieschanged events
+
+// group-by condition changes
+{
+  const tags = [
+    'dbus_column1', 'dbus_column2', 'dbus_column3', 'dbus_column4',
+    'dbus_column5', 'dbus_column6', 'dbus_column7'
+  ];
+  for (let i = 0; i < 7; i++) {
+    document.getElementById(tags[i]).addEventListener(
+        'click', OnGroupByConditionChanged_DBus);
+  }
+}
+
+// Called from renderer.Render()
+function draw_timeline_sensors(ctx) {
+  sensors_timeline_view.Render(ctx);
+}
+
+// Called from renderer.Render()
+function draw_timeline_dbus(ctx) {
+  dbus_timeline_view.Render(ctx);
+}
+
+let Canvas_DBus = document.getElementById('my_canvas_dbus');
+
+const IDXES = {
+  'Type': 0,
+  'Timestamp': 1,
+  'Serial': 2,
+  'Sender': 3,
+  'Destination': 4,
+  'Path': 5,
+  'Interface': 6,
+  'Member': 7
+};
+
+// This "group" is based on the content of the DBus
+// It is independent of the "group_by" of the meta-data (sender/destination/
+// path/interface/member) of a DBus message
+//
+// Input is processed message and some basic statistics needed for categorizing
+//
+const DBusMessageContentKey = function(msg, cxn_occ) {
+  let ret = undefined;
+  const type = msg[IDXES["Type"]];
+  const dest = msg[IDXES["Destination"]];
+  const path = msg[IDXES["Path"]];
+  const iface = msg[IDXES["Interface"]];
+  const member = msg[IDXES["Member"]];
+  const sender = msg[IDXES["Sender"]];
+
+  if (sender == "s" || sender == "sss") {
+    console.log(msg)
+  }
+  
+  if (type == "sig") {
+    if (path.indexOf("/xyz/openbmc_project/sensors/") != -1 &&
+        iface == "org.freedesktop.DBus.Properties" &&
+        member == "PropertiesChanged") {
+      ret = "Sensor PropertiesChanged Signals";
+    }
+  } else if (type == "mc") {
+    if (dest == "xyz.openbmc_project.Ipmi.Host" &&
+        path == "/xyz/openbmc_project/Ipmi" &&
+        iface == "xyz.openbmc_project.Ipmi.Server" &&
+        member == "execute") {
+      ret = "IPMI Daemon";
+    }
+  }
+  
+  if (ret == undefined && cxn_occ[sender] <= 10) {
+    ret = "Total 10 messages or less"
+  }
+
+  if (ret == undefined && type == "mc") {
+    if (path.indexOf("/xyz/openbmc_project/sensors/") == 0 &&
+    iface == "org.freedesktop.DBus.Properties" &&
+    (member.startsWith("Get") || member.startsWith("Set"))) {
+      ret = "Sensor Get/Set";
+    }
+  }
+
+  if (ret == undefined) {
+    ret = "Uncategorized";
+  }
+
+  return ret;
+}
+
+function Group_DBus(preprocessed, group_by) {
+  let grouped = {};  // [content key][sort key] -> packet
+
+  let cxn_occ = {}; // How many times have a specific service appeared?
+  preprocessed.forEach((pp) => {
+    const cxn = pp[IDXES["Sender"]];
+    if (cxn_occ[cxn] == undefined) {
+      cxn_occ[cxn] = 0;
+    }
+    cxn_occ[cxn]++;
+  });
+
+  for (var n = 0; n < preprocessed.length; n++) {
+    var key = '';
+    for (var i = 0; i < group_by.length; i++) {
+      if (i > 0) key += ' ';
+      key += ('' + preprocessed[n][IDXES[group_by[i]]]);
+    }
+
+    // "Content Key" is displayed on the "Column Headers"
+    const content_group = DBusMessageContentKey(preprocessed[n], cxn_occ);
+
+    // Initialize the "Collapsed" array here
+    // TODO: this should ideally not be specific to the dbus_interface_view instance
+    if (dbus_timeline_view.HeaderCollapsed[content_group] == undefined) {
+      dbus_timeline_view.HeaderCollapsed[content_group] = false;  // Not collapsed by default
+    }
+
+    if (grouped[content_group] == undefined) {
+      grouped[content_group] = [];
+    }
+    let grouped1 = grouped[content_group];
+
+    if (grouped1[key] == undefined) grouped1[key] = [];
+    grouped1[key].push(preprocessed[n]);
+  }
+  return grouped;
+}
+
+function OnGroupByConditionChanged_DBus() {
+  var tags = [
+    'dbus_column1', 'dbus_column2', 'dbus_column3', 'dbus_column4',
+    'dbus_column5', 'dbus_column6', 'dbus_column7'
+  ];
+  const v = dbus_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_DBusPcap(
+      Data_DBus, Timestamps_DBus);  // should be from dbus_pcap
+  let grouped = Group_DBus(preproc, v.GroupBy);
+  GenerateTimeLine_DBus(grouped);
+  dbus_timeline_view.IsCanvasDirty = true;
+}
+
+// Todo: put g_StartingSec somewhere that's common between sensors and non-sensors
+function GenerateTimeLine_DBus(grouped) {
+  let intervals = [];
+  let titles = [];
+  g_StartingSec = undefined;
+
+  // First, turn "content keys" into headers in the flattened layout
+  const content_keys = Object.keys(grouped);
+
+  const keys = Object.keys(grouped);
+  let sortedKeys = keys.slice();
+
+  let interval_idx = 0;  // The overall index into the intervals array
+
+  for (let x=0; x<content_keys.length; x++) {
+    const content_key = content_keys[x];
+    // Per-content key
+    const grouped1 = grouped[content_key];
+    const keys1 = Object.keys(grouped1);
+
+    let the_header = { "header":true, "title":content_key, "intervals_idxes":[] };
+    titles.push(the_header);
+    // TODO: this currently depends on the dbus_timeline_view instance
+    const collapsed = dbus_timeline_view.HeaderCollapsed[content_key];
+
+    for (let i = 0; i < keys1.length; i++) {
+      // The Title array controls which lines are drawn. If we con't push the header
+      // it will not be drawn (thus giving a "collapsed" visual effect.)
+      if (!collapsed) {
+        titles.push({ "header":false, "title":keys1[i], "intervals_idxes":[interval_idx] });
+      }
+
+
+      line = [];
+      for (let j = 0; j < grouped1[keys1[i]].length; j++) {
+        let entry = grouped1[keys1[i]][j];
+        let t0 = parseFloat(entry[1]) / 1000.0;
+        let t1 = parseFloat(entry[8]) / 1000.0;
+
+        // Modify time shift delta if IPMI dataset is loaded first
+        if (g_StartingSec == undefined) {
+          g_StartingSec = t0;
+        }
+        g_StartingSec = Math.min(g_StartingSec, t0);
+        const outcome = entry[9];
+        line.push([t0, t1, entry, outcome, 0]);
+      }
+
+      the_header.intervals_idxes.push(interval_idx);  // Keep the indices into the intervals array for use in rendering
+      intervals.push(line);
+      interval_idx ++;
+    }
+
+    // Compute a set of "merged intervals" for each content_key
+    let rise_fall_edges = [];
+    the_header.intervals_idxes.forEach((i) => {
+      intervals[i].forEach((t0t1) => {
+        if (t0t1[0] <= t0t1[1]) {  // For errored-out method calls, the end time will be set to a value smaller than the start tiem
+          rise_fall_edges.push([t0t1[0], 0]);  // 0 is a rising edge
+          rise_fall_edges.push([t0t1[1], 1]);  // 1 is a falling edge
+        }
+      })
+    });
+
+    let merged_intervals = [], 
+        current_interval = [undefined, undefined, 0];  // start, end, weight
+    rise_fall_edges.sort();
+    let i = 0, level = 0;
+    while (i<rise_fall_edges.length) {
+      let timestamp = rise_fall_edges[i][0];
+      while (i < rise_fall_edges.length && timestamp == rise_fall_edges[i][0]) {
+        switch (rise_fall_edges[i][1]) {
+          case 0: {  // rising edge
+            if (level == 0) {
+              current_interval[0] = timestamp;
+              current_interval[2] ++;
+            }
+            level ++;
+            break;
+          }
+          case 1: {  // falling edge
+            level --;
+            if (level == 0) {
+              current_interval[1] = timestamp;
+              merged_intervals.push(current_interval);
+              current_interval = [undefined, undefined, 0];
+            }
+            break;
+          }
+        }
+        i++;
+      }
+    }
+    the_header.merged_intervals = merged_intervals;
+  }
+
+  // Time shift
+  for (let i = 0; i < intervals.length; i++) {
+    for (let j = 0; j < intervals[i].length; j++) {
+      let x = intervals[i][j];
+      x[0] -= g_StartingSec;
+      x[1] -= g_StartingSec;
+    }
+  }
+  // merged intervals should be time-shifted as well
+  titles.forEach((t) => {
+    if (t.header == true) {
+      t.merged_intervals.forEach((mi) => {
+        mi[0] -= g_StartingSec;
+        mi[1] -= g_StartingSec;
+      })
+    }
+  })
+
+  dbus_timeline_view.Intervals = intervals.slice();
+  dbus_timeline_view.Titles    = titles.slice();
+  dbus_timeline_view.LayoutForOverlappingIntervals();
+}
+
+Canvas_DBus.onmousemove =
+    function(event) {
+  const v = dbus_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_DBus.offsetLeft;
+    u.MouseState.y = undefined;                  // Do not highlight any entry or the horizontal scroll bars
+    if (u.MouseState.pressed == true &&
+      v.MouseState.hoveredSide == 'timeline') {  // Update highlighted area
+      u.HighlightedRegion.t1 = u.MouseXToTimestamp(u.MouseState.x);
+    }
+    u.OnMouseMove();
+    u.IsCanvasDirty = true;
+  });
+}
+
+Canvas_DBus.onmousedown = function(event) {
+  if (event.button == 0) {
+    dbus_timeline_view.OnMouseDown();
+  }
+};
+
+Canvas_DBus.onmouseup =
+    function(event) {
+  if (event.button == 0) {
+    dbus_timeline_view.OnMouseUp();
+  }
+}
+
+Canvas_DBus.onmouseleave = 
+    function(event) {
+  dbus_timeline_view.OnMouseLeave();
+}
+
+Canvas_DBus.onwheel = function(event) {
+  dbus_timeline_view.OnMouseWheel(event);
+}