[perf-web] Allow finding out more information about commits



commit c507590ea3566cd568148128b228378d10cbca66
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Wed Sep 17 20:50:53 2014 -0400

    Allow finding out more information about commits
    
    When in the day view, allow double-clicking on a data point to find out
    what changed with the commit that the measurement corresponds to - a pane
    is opened at the bottom of the screen. Add keybindings for navigating to
    adjacent points.

 metrics/views.py               |   28 ++-
 static/images/README.txt       |   19 ++
 static/images/go-next.png      |  Bin 0 -> 231 bytes
 static/images/go-previous.png  |  Bin 0 -> 235 bytes
 static/images/window-close.png |  Bin 0 -> 315 bytes
 static/main.js                 |  452 +++++++++++++++++++++++++++++++++++++---
 static/perf.css                |  133 ++++++++++++
 templates/metrics/chart.html   |    1 +
 templates/metrics/common.html  |   12 +
 templates/metrics/home.html    |    1 +
 templates/metrics/metric.html  |    1 +
 templates/metrics/target.html  |    1 +
 12 files changed, 618 insertions(+), 30 deletions(-)
---
diff --git a/metrics/views.py b/metrics/views.py
index afd3e3d..cd33ef9 100644
--- a/metrics/views.py
+++ b/metrics/views.py
@@ -21,11 +21,11 @@ _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
 # timedelta.total_seconds added in 2.7
 if hasattr(timedelta, 'total_seconds'):
     def unix_time(dt):
-        return (dt - _EPOCH).total_seconds()
+        return int(round((dt - _EPOCH).total_seconds()))
 else:
     def unix_time(dt):
         td = dt - _EPOCH
-        return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+        return int(round(float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6))
 
 def home(request):
     t = loader.get_template('metrics/home.html')
@@ -148,10 +148,13 @@ def values(request):
     if not group in _SUMMARY_CLASSES:
         return HttpResponseBadRequest("Invalid group type")
 
-    result = []
+    result = {}
+    result['metrics'] = metrics = []
 
     summaryCls = _SUMMARY_CLASSES[group]
     if summaryCls is None:
+        target_map = {}
+
         qs = Value.objects.all()
         qs = Value.filter_and_order(qs, start, end, metric, target)
 
@@ -166,11 +169,12 @@ def values(request):
                     'name': metric_name,
                     'targets': []
                 }
-                result.append(metric_data)
+                metrics.append(metric_data)
                 last_metric = metric_name
                 last_target = None
 
             target_name = value.report.target.name
+
             if target_name != last_target:
                 target_data = {
                     'name': target_name,
@@ -179,10 +183,24 @@ def values(request):
                 metric_data['targets'].append(target_data)
                 last_target = target_name
 
+                if target_name in target_map:
+                    revisions = target_map[target_name]
+                else:
+                    revisions = target_map[target_name] = {}
+
+            pull_time_str = str(unix_time(value.report.pull_time))
+            if not pull_time_str in revisions:
+                revisions[pull_time_str] = value.report.revision
+
             target_data['values'].append({
                 'time': unix_time(value.report.pull_time),
                 'value': value.value
             })
+
+        result['targets'] = targets = []
+        for name, revisions in target_map.iteritems():
+            targets.append({'name': name,
+                            'revisions': revisions});
     else:
         summaries = summaryCls.get_summaries(start, end, target, metric)
 
@@ -197,7 +215,7 @@ def values(request):
                     'name': metric_name,
                     'targets': []
                 }
-                result.append(metric_data)
+                metrics.append(metric_data)
                 last_metric = metric_name
                 last_target = None
 
diff --git a/static/images/README.txt b/static/images/README.txt
new file mode 100644
index 0000000..5645551
--- /dev/null
+++ b/static/images/README.txt
@@ -0,0 +1,19 @@
+These icons are selected from the GNOME Project's Adwaita Icon theme,
+which is licensed under the following terms:
+
+===
+This work is licenced under the terms of either the GNU LGPL v3 or
+Creative Commons Attribution-Share Alike 3.0 United States License.
+
+To view a copy of the CC-BY-SA licence, visit
+http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
+Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA.
+
+When attributing the artwork, using "GNOME Project" is enough.
+Please link to http://www.gnome.org where available.
+===
+
+The source files for this artwork can be found in the adwaiata-icon-theme
+module:
+
+ https://git.gnome.org/browse/adwaita-icon-theme
diff --git a/static/images/go-next.png b/static/images/go-next.png
new file mode 100644
index 0000000..e1504a1
Binary files /dev/null and b/static/images/go-next.png differ
diff --git a/static/images/go-previous.png b/static/images/go-previous.png
new file mode 100644
index 0000000..2dea27a
Binary files /dev/null and b/static/images/go-previous.png differ
diff --git a/static/images/window-close.png b/static/images/window-close.png
new file mode 100644
index 0000000..0c02038
Binary files /dev/null and b/static/images/window-close.png differ
diff --git a/static/main.js b/static/main.js
index f77f64d..ae27bf3 100644
--- a/static/main.js
+++ b/static/main.js
@@ -84,9 +84,11 @@ function Chart(svg) {
     this.body = $( svg ).find( ".chart-body" ).get(0);
     this.grid = $( svg ).find( ".chart-grid" ).get(0);
     this.chartData = $( svg ).find( ".chart-data" ).get(0);
+    this.highlights = $( svg ).find( ".chart-highlights" ).get(0);
 
     this.polylines = [];
     this.pointPaths = [];
+    this.highlightMarkers = [];
     this.xGridLines = [];
     this.yGridLines = [];
     this.xLabelTexts = [];
@@ -94,13 +96,8 @@ function Chart(svg) {
     this.top = 10;
     this.bottom = 10;
 
-    svg.addEventListener("dblclick", function(event) {
-        var offset = $( this.body ).offset();
-        var x = event.pageX - offset.left;
-        var centerTime = this.time(x);
-
-        theDisplay.zoomIn(centerTime);
-    }.bind(this));
+    svg.addEventListener("dblclick", this.onDblClick.bind(this));
+    svg.addEventListener("mousemove", this.onMouseMove.bind(this));
 }
 
 Chart.prototype.x = function(time) {
@@ -115,6 +112,80 @@ Chart.prototype.y = function(value) {
     return this.bodyHeight * (1 - (value - this.bottom) / (this.top - this.bottom));
 }
 
+Chart.prototype.setHover = function(target, time) {
+    var topY = this.y(this.topValue);
+    var bottomY = this.y(this.bottomValue);
+    var values = target.values;
+    for (var j = 0; j < 2 * values.length; j++) {
+        if (values.data[j] == time) {
+            var offset = $( this.body ).offset();
+            var x = this.x(time);
+            var y = this.y(values.data[j + 1]);
+
+            theDisplay.setHover({
+                x: offset.left + x,
+                y: offset.top + y,
+                above: y < (topY + bottomY) / 2 - 3,
+                target: target.target,
+                metric: this.metric,
+                units: this.units,
+                time: time,
+                revision: theDisplay.allTargets[target.target].revisions[time],
+                value: values.data[j + 1] * this.multiplier,
+            });
+
+            return;
+        }
+    }
+}
+
+Chart.prototype.onMouseMove = function(event) {
+    var offset = $( this.body ).offset();
+    var x = event.pageX - offset.left;
+    var y = event.pageY - offset.top;
+
+    var targets = this.getTargets();
+    var closestTarget = null;
+    var closetDist2, closestTime;
+
+    for (var i = 0; i < targets.length; i++) {
+        var values = targets[i].values;
+
+        var j;
+        for (j = 0; j < 2 * values.length; j++) {
+            var pointX = this.x(values.data[j]);
+            var pointY = this.y(values.data[j + 1]);
+            var dist2 = (pointX - x) * (pointX - x) + (pointY - y) * (pointY - y);
+            if (dist2 < 25 && (closestTarget == null || dist2 < closestDist2)) {
+                closestTarget = targets[i];
+                closestTime = values.data[j];
+                closestDist2 = dist2;
+
+            }
+        }
+    }
+
+    if (closestTarget)
+        this.setHover(closestTarget, closestTime);
+    else
+        theDisplay.setHover(null);
+}
+
+Chart.prototype.onDblClick = function(event) {
+    var offset = $( this.body ).offset();
+    var x = event.pageX - offset.left;
+    var centerTime = this.time(x);
+
+    if (theDisplay.rangeType != 'day') {
+        theDisplay.zoomIn(centerTime);
+    } else {
+        if (theDisplay.hoverInfo)
+            theDisplay.setDetails({ time: theDisplay.hoverInfo.time,
+                                    metric: theDisplay.hoverInfo.metric,
+                                    revision: theDisplay.hoverInfo.revision });
+    }
+}
+
 function createElement(name, cls) {
     var element = document.createElementNS("http://www.w3.org/2000/svg";, name);
     if (cls != null)
@@ -172,9 +243,19 @@ Chart.prototype.removeExtraElements = function(elements, nToSave) {
 Chart.prototype.drawTarget = function(targetData) {
     var values = targetData.values;
     var index = targetData.index;
+    var highlightTime = null;
+    var highlightX, highlightY;
 
-    if (values.length == 0)
+    if (theDisplay.detailsInfo)
+        highlightTime = theDisplay.detailsInfo.time;
+
+    if (values.length == 0) {
+        if (this.highlightMarkers[index]) {
+            this.highlights.removeChild(this.highlightMarkers[index]);
+            delete this.highlightMarkers[index];
+        }
         return;
+    }
 
     var path;
     if (theDisplay.loadedGroup != 'none') {
@@ -225,16 +306,37 @@ Chart.prototype.drawTarget = function(targetData) {
 
     path = "";
     for (var i = 0; i < 2 * values.length; i += 2) {
-        var x = this.x(values.data[i]);
+        var time = values.data[i];
+        var x = this.x(time);
         if (x + 2 <= 0 || x - 2 >= this.bodyWidth)
             continue;
 
         var y = this.y(values.data[i + 1]);
 
-        path += "M" + (x - 2) + " " + (y - 2) + " " + "h4v4h-4z"
+        path += "M" + (x - 2) + " " + (y - 2) + " " + "h4v4h-4z";
+
+        if (time == highlightTime) {
+            highlightX = x;
+            highlightY = y;
+        }
     }
 
     pointPath.setAttribute("d", path);
+
+    if (highlightX) {
+        var rect = this.highlightMarkers[index];
+        if (rect == null) {
+            this.highlightMarkers[index] = rect = createElement("rect", "chart-highlight");
+            this.highlights.appendChild(rect);
+        }
+
+        allocateElement(rect, highlightX - 2.5, highlightY - 2.5, 5, 5);
+    } else {
+        if (this.highlightMarkers[index]) {
+            this.highlights.removeChild(this.highlightMarkers[index]);
+            delete this.highlightMarkers[index];
+        }
+    }
 }
 
 Chart.prototype.getTargets = function() {
@@ -426,6 +528,9 @@ Chart.prototype.drawYLabels = function(targets) {
     // Always include the X-axis
     var bottom = 0;
     var top = 0;
+    // But we also want the real-range for tooltip positioning
+    var realBottom = null;
+    var realTop = null;
 
     for (var i = 0; i < targets.length; i++) {
         var targetData = targets[i];
@@ -439,9 +544,23 @@ Chart.prototype.drawYLabels = function(targets) {
                 bottom = Math.min(value, bottom);
                 top = Math.max(value, top);
             }
+
+            if (realTop == null) {
+                realTop = realBottom = value;
+            } else {
+                realBottom = Math.min(value, realBottom);
+                realTop = Math.max(value, realTop);
+            }
         }
     }
 
+    if (realTop == null) {
+        this.bottomValue = this.topValue = 0;
+    } else {
+        this.bottomValue = realBottom;
+        this.topValue = realTop;
+    }
+
     // We want to find a multiple of 2, 5, or 10 that covers the range
     // bottom to top in about 5 steps
     var step;
@@ -487,24 +606,30 @@ Chart.prototype.drawYLabels = function(targets) {
     var displayTop = top;
     var displayBottom = bottom;
 
-    units = this.svg.getAttribute('data-metric-units');
+    var units = this.svg.getAttribute('data-metric-units');
     if (units == 's' || units == 'ms' || units == 'us') {
-        if (units == 's') {
-            displayTop *= 1000000;
-            displayBottom *= 1000000;
-        } else if (units == 'ms') {
-            displayTop *= 1000;
-            displayBottom *= 1000;
-        }
+        if (units == 's')
+            multiplier = 1000000;
+        else if (units == 'ms')
+            multiplier = 1000;
+        else
+            multiplier = 1;
 
         if (bottom <= -1000000 || top >= 1000000) {
-            displayTop /= 1000000;
-            displayBottom /= 1000000;
+            multiplier /= 1000000;
+            units = 's';
         } else if (bottom <= -100 || top >= 1000) {
-            displayTop /= 1000;
-            displayBottom /= 1000;
+            multiplier /= 1000;
+            units = 'ms';
+        } else {
+            units = 'us';
         }
+
+        displayTop *= multiplier;
+        displayBottom *= multiplier;
     }
+    this.units = units;
+    this.multiplier = multiplier;
 
     $( this.upperText ).empty();
     this.upperText.appendChild(document.createTextNode(displayTop));
@@ -555,6 +680,12 @@ Chart.prototype.draw = function() {
             delete this.polylines[index];
         }
     }
+    for (index in this.highlightMarkers) {
+        if (!(index in usedIndices)) {
+            this.highlights.removeChild(this.highlightMarkers[index]);
+            delete this.highlightMarkers[index];
+        }
+    }
 }
 
 ////////////////////////////////////////////////////////////////////////
@@ -1057,6 +1188,9 @@ function PerfDisplay(target, metric, dataMinTime, dataMaxTime, centerTime, range
     this.loadedGroup = null;
     this.loadedRanges = new TimeRanges();
 
+    this.revisions = {};
+    this.loadedRevisions = {};
+
     this.setPositionAndRange(centerTime, rangeType, false);
     $(window).load(this.onWindowLoaded.bind(this));
 }
@@ -1065,6 +1199,11 @@ PerfDisplay.prototype.setPositionAndRange = function(centerTime, rangeType, clam
     var rangeTypeChanged = (this.rangeType != rangeType);
     this.rangeType = rangeType;
 
+    if (rangeType != 'day')
+        this.setDetails(null);
+    if (rangeTypeChanged)
+        this.setHover(null);
+
     switch (rangeType) {
         case 'day':
            this.rangeSeconds = DAY_MSECS / 1000;
@@ -1253,8 +1392,9 @@ PerfDisplay.prototype._loadRange = function(group, start, end) {
 
                   this.loadedRanges.add(start, end);
 
-                  for (var i = 0; i < data.length; i++) {
-                      var metricData = data[i];
+                  var metrics = data.metrics;
+                  for (var i = 0; i < metrics.length; i++) {
+                      var metricData = metrics[i];
                       if (!(metricData.name in this.data))
                           this.data[metricData.name] = {};
 
@@ -1286,7 +1426,22 @@ PerfDisplay.prototype._loadRange = function(group, start, end) {
                           else
                               this.data[metricData.name][targetData.name] = values;
 
-                          this.allTargets[targetData.name] = 1;
+                          if (!(targetData.name in this.allTargets))
+                              this.allTargets[targetData.name] = { revisions: {} };
+                      }
+                  }
+
+                  if (data.targets != null) {
+                      var targets = data.targets;
+                      for (var i = 0; i < targets.length; i++) {
+                          var targetData = targets[i];
+                          if (!(targetData.name in this.allTargets))
+                              this.allTargets[targetData.name] = { revisions: {} };
+
+                          var revisions = this.allTargets[targetData.name].revisions;
+                          var newRevisions = targetData.revisions;
+                          for (var time in newRevisions)
+                              revisions[time] = newRevisions[time];
                       }
                   }
 
@@ -1314,6 +1469,9 @@ PerfDisplay.prototype.updateElementsForRange = function() {
 }
 
 PerfDisplay.prototype.scrollByDeltaX = function(startTime, deltaX) {
+    if (deltaX > 3)
+        this.setHover(null);
+
     var chart = $( ".chart" ).first().get(0);
     var chartBodyWidth = chart.parentNode.clientWidth - (MARGIN_LEFT + MARGIN_RIGHT + 2);
     var newPos = startTime + deltaX * this.rangeSeconds / chartBodyWidth;
@@ -1406,6 +1564,18 @@ PerfDisplay.prototype.onWindowLoaded = function() {
         this.scrollByDeltaX(this.centerTime, 10 * event.deltaX);
     }.bind(this));
 
+    $("#detailsClose").click(function() {
+        this.setDetails(null);
+    }.bind(this));
+
+    $("#detailsPrevious").click(function() {
+        this.moveDetails(-1);
+    }.bind(this));
+
+    $("#detailsNext").click(function() {
+        this.moveDetails(+1);
+    }.bind(this));
+
     this.scrollHandler = new ScrollHandler({
         exclusive: true,
         source: mainLeft,
@@ -1421,6 +1591,9 @@ PerfDisplay.prototype.onWindowLoaded = function() {
     });
 
     $( document.body ).keypress(function(e) {
+        if (e.ctrlKey || e.altKey || e.metaKey)
+            return;
+
         if (e.which == 43) { /* + */
             e.preventDefault();
             this.zoomIn();
@@ -1430,6 +1603,22 @@ PerfDisplay.prototype.onWindowLoaded = function() {
         }
     }.bind(this));
 
+    $( document.body ).keydown(function(e) {
+        if (e.ctrlKey || e.altKey || e.metaKey)
+            return;
+
+        if (e.which == 37) { /* left */
+            e.preventDefault();
+            this.moveDetails(-1);
+        } else if (e.which == 39) { /* right */
+            e.preventDefault();
+            this.moveDetails(+1);
+        } else if (e.which == 27) { /* esc */
+            e.preventDefault();
+            this.setDetails(null);
+        }
+    }.bind(this));
+
     $( window ).resize(function() {
         this.refresh();
     }.bind(this));
@@ -1438,6 +1627,219 @@ PerfDisplay.prototype.onWindowLoaded = function() {
         this.refresh();
 }
 
+PerfDisplay.prototype.setHover = function(hoverInfo)
+{
+    var hover = $( "#hover" ).get(0);
+    this.hoverInfo = hoverInfo;
+    if (hoverInfo) {
+        var date = new Date(hoverInfo.time * 1000.);
+        var dateStr =  (date.getUTCFullYear() + '-' + pad(date.getUTCMonth() + 1) + '-' + 
pad(date.getUTCDate()) + ' ' +
+                        pad(date.getUTCHours()) + ':' + pad(date.getUTCMinutes()) + ':' + 
pad(date.getUTCSeconds()));
+        $( "#hover" ).text (hoverInfo.value.toPrecision(4) + hoverInfo.units);
+        var x = Math.max(0, hoverInfo.x - hover.offsetWidth / 2);
+        var y;
+        if (hoverInfo.above)
+            y = hoverInfo.y - hover.offsetHeight - 10;
+        else
+            y = hoverInfo.y + 10;
+        $( hover ).css('top', y).css('left', x).show();
+    } else {
+        $( hover ).hide();
+    }
+}
+
+PerfDisplay.prototype.setDetails = function(detailsInfo)
+{
+    this.detailsInfo = detailsInfo;
+    if (this.detailsInfo) {
+        this.refreshDetails();
+        $( "body" ).addClass("details-showing");
+    } else {
+        $( "body" ).removeClass("details-showing");
+    }
+
+    this.queueRefresh();
+}
+
+PerfDisplay.prototype.moveDetails = function(direction)
+{
+    if (this.detailsInfo == null)
+        return;
+
+    var chart;
+    for (var i = 0; i < this.charts.length; i++) {
+        if (this.charts[i].metric == this.detailsInfo.metric) {
+            chart = this.charts[i];
+            break;
+        }
+    }
+
+    var targets = chart.getTargets();
+    var newTime = null;
+    var newTarget = null;
+    var newRevision = null;
+    var oldTime = this.detailsInfo.time;
+
+    for (var i = 0; i < targets.length; i++) {
+        var target = targets[i];
+        var targetRevisions = this.allTargets[target.target].revisions;
+        var targetTime = null;
+
+        if (direction > 0) {
+            for (var j = 0; j <= 2 * target.values.length; j += 2) {
+                var time = target.values.data[j];
+                if (time > oldTime &&
+                    targetRevisions[time] != this.detailsInfo.revision) {
+                    targetTime = time;
+                    break;
+                }
+            }
+
+            if (targetTime != null && (newTime == null || targetTime < newTime)) {
+                newTime = targetTime;
+                newRevision = targetRevisions[targetTime];
+                newTarget = target;
+            }
+        } else {
+            for (var j = 2 * target.values.length - 2; j >= 0; j -= 2) {
+                var time = target.values.data[j];
+                if (time < oldTime &&
+                    targetRevisions[time] != this.detailsInfo.revision) {
+                    targetTime = time;
+                    break;
+                }
+            }
+
+            if (targetTime != null && (newTime == null || targetTime > newTime)) {
+                newTime = targetTime;
+                newRevision = targetRevisions[targetTime];
+                newTarget = target;
+            }
+        }
+    }
+
+    if (newTime != null) {
+        chart.setHover(newTarget, newTime);
+
+        this.detailsInfo.time = newTime;
+        this.detailsInfo.revision = newRevision;
+        this.refreshDetails();
+        this.queueRefresh();
+    }
+}
+
+PerfDisplay.prototype.refreshDetails = function() {
+    if (!this.detailsInfo)
+        return;
+
+    $( "#detailsDetails" ).empty();
+    $( "#detailsLink" ).empty();
+
+    if (this.detailsInfo.time < this.startSeconds + this.rangeSeconds * 0.1)
+        this.setPositionAndRange(this.detailsInfo.time + this.rangeSeconds * 0.4, this.rangeType, true);
+    else if (this.detailsInfo.time > this.startSeconds + this.rangeSeconds * 0.9)
+        this.setPositionAndRange(this.detailsInfo.time - this.rangeSeconds * 0.4, this.rangeType, true);
+
+    if (this.detailsInfo.revision in this.revisions) {
+        this.updateDetails();
+        return;
+    }
+
+    var date = new Date(this.detailsInfo.time * 1000.);
+    this.fetchRevisions(date);
+
+    if (date.getUTCHours() == 23)
+        this.fetchRevisions(new Date(date.getTime() + DAY_MSECS));
+    else if (date.getUTCHours() == 0)
+        this.fetchRevisions(new Date(date.getTime() - DAY_MSECS));
+}
+
+PerfDisplay.prototype.updateDetails = function() {
+    if (!this.detailsInfo)
+        return;
+
+    var revision = this.detailsInfo.revision;
+    var build = this.revisions[this.detailsInfo.revision];
+    if (build == null)
+        return;
+
+    var parts = build.split("/");
+    var id = parts[0] + parts[1] + parts[2] + "." + parts[3];
+    $( "#detailsLink" )
+        .attr('href', 'http://build.gnome.org/#/build/' + id)
+        .text(id);
+
+    var url = 'http://build.gnome.org/continuous/work/builds/' + build + '/bdiff.json';
+    $.ajax({datatype: "json",
+            url: url,
+            success:
+            function(data) {
+                if (!this.detailsInfo || this.detailsInfo.revision != revision)
+                    return;
+
+                this.fillDetails(data);
+            }.bind(this)});
+}
+
+PerfDisplay.prototype.fillDetails = function(data) {
+    $( "#detailsDetails" ).empty();
+
+    for (var i = 0; i < data.modified.length; i++) {
+        var modified = data.modified[i];
+        var name = modified.latest.name;
+        var src = modified.latest.src;
+
+        var cgitBase = null;
+
+        var m;
+        m = /^git:git:\/\/git.gnome.org\/(.*)/.exec(src);
+        if (m)
+            cgitBase = 'http://git.gnome.org/browse/' + m[1];
+        m = /^git:git:\/\/anongit.freedesktop.org\/git\/(.*)/.exec(src);
+        if (m)
+            cgitBase = 'http://cgit.freedesktop.org/' + m[1];
+
+        var divQ = $( "<div class='module-detail'></div> ").appendTo("#detailsDetails");
+        $( "<div class='module-title' /> ").appendTo(divQ).text(name);
+
+
+        for (var j = 0; j < modified.gitlog.length; j++) {
+            var log = modified.gitlog[j];
+            var subject;
+            if (log.Subject.length > 60)
+                subject = log.Subject.substring(0, 57) + "...";
+            else
+                subject = log.Subject;
+            if (cgitBase) {
+                var commitQ = $( "<div class='module-commit' />" ).appendTo(divQ);
+                var url = cgitBase + '/commit/?id=' + log.Checksum;
+                $( "<a />" ).attr('href', url).attr('target', 
'commitDetails').text(subject).appendTo(commitQ);
+            } else {
+                $( "<div class='module-commit' /> ").appendTo(divQ).text(subject);
+            }
+        }
+    }
+}
+
+PerfDisplay.prototype.fetchRevisions = function(date) {
+    var datePath = date.getUTCFullYear() + '/' + pad(date.getUTCMonth() + 1) + '/' +  pad(date.getUTCDate());
+    if (datePath in this.loadedRevisions)
+        return;
+
+    this.loadedRevisions[datePath] = 1;
+
+    var url = 'http://build.gnome.org/continuous/work/builds/' + datePath + '/index.json';
+    $.ajax({datatype: "json",
+            url: url,
+            success:
+            function(data) {
+                var targetMap = data.targetMap;
+                for (var revision in targetMap)
+                    this.revisions[revision] = datePath + '/' + targetMap[revision][0];
+                this.updateDetails();
+            }.bind(this)});
+}
+
 //////////////////////////////////////////////////////////////////////////////////
 
 function setRange(e, a, rangeType) {
diff --git a/static/perf.css b/static/perf.css
index 58e13c5..e3f876c 100644
--- a/static/perf.css
+++ b/static/perf.css
@@ -12,6 +12,10 @@ body {
     cursor: grabbing;
 }
 
+.clear {
+    clear: both;
+}
+
 html, body {
     margin: 0px;
     padding: 0px;
@@ -132,6 +136,11 @@ h1 {
     fill: #aaaaaa;
 }
 
+.chart-highlight {
+    fill: none;
+    stroke: red;
+}
+
 a {
     color: #3465A4;
 }
@@ -160,6 +169,10 @@ a {
     overflow: scroll;
 }
 
+body.details-showing #mainLeft {
+    bottom: 100px;
+}
+
 #mainLeft, #mainRight {
     top: 160px;
 }
@@ -177,6 +190,10 @@ a {
     z-index: 0;
 }
 
+body.details-showing #mainRight {
+    bottom: 100px;
+}
+
 #mainRight table {
     font-size: 11px;
     table-layout: fixed;
@@ -296,6 +313,10 @@ a {
     bottom: 100px;
 }
 
+#metricPage.details-showing .chart-div {
+    bottom: 0px;
+}
+
 #metricPage .chart {
     height: 100%;
 }
@@ -303,3 +324,115 @@ a {
 #metricPage #mainLeft {
     overflow: hidden;
 }
+
+#hover {
+    display: none;
+    font-size: 10px;
+    padding: 2px;
+    position: fixed;
+    background: rgba(0, 0, 0, 0.5);
+    color: white;
+    left: 0px;
+}
+
+#details {
+    position: fixed;
+    display: none;
+    height: 100px;
+    bottom: 0px;
+    left: 0px;
+    right: 0px;
+    border-top: 1px solid #aaaaaa;
+    overflow-y: scroll;
+    font-size: 11px;
+}
+
+#details a {
+    text-decoration: none;
+}
+
+#detailsTitle {
+    font-size: 12px;
+    margin-left: 3px;
+    margin-bottom: 3px;
+}
+
+body.details-showing #details {
+    display: block;
+}
+
+#detailsButtons {
+    position: absolute;
+    right: 0px;
+    top: 0px;
+    width: 30px;
+    height: 10px;
+}
+
+#detailsButtons div {
+    position: absolute;
+    top: 0px;
+    width: 10px;
+    height: 10px;
+    background-size: contain;
+    box-shadow: inset -1px -1px 3px -1px #aaaaaa;
+    border-bottom: 1px solid #aaaaaa;
+    border-left: 1px solid #aaaaaa;
+}
+
+#detailsButtons div:active {
+    box-shadow: inset 1px 1px 3px -1px #888888;
+}
+
+#detailsPrevious {
+    left: 0px;
+    background: url(/static/images/go-previous.png),#eeeeee;
+}
+
+#detailsPrevious:hover {
+    background: url(/static/images/go-previous.png);
+    background-size: contain;
+}
+
+#detailsPrevious:active {
+    background: url(/static/images/go-previous.png),#aaaaaa;
+    background-size: contain;
+}
+
+#detailsNext {
+    left: 10px;
+    background: url(/static/images/go-next.png),#eeeeee;
+}
+
+#detailsNext:hover {
+    background: url(/static/images/go-next.png);
+    background-size: contain;
+}
+
+#detailsNext:active {
+    background: url(/static/images/go-next.png),#aaaaaa;
+    background-size: contain;
+}
+
+#detailsClose {
+    left: 20px;
+    background: url(/static/images/window-close.png),#eeeeee;
+}
+
+#detailsClose:hover {
+    background: url(/static/images/window-close.png);
+    background-size: contain;
+}
+
+#detailsClose:active {
+    background: url(/static/images/window-close.png),#aaaaaa;
+    background-size: contain;
+}
+
+.module-detail {
+    float: left;
+    border: 1px solid #aaaaaa;
+    padding: 2px;
+    margin-right: 3px;
+    margin-bottom: 3px;
+}
diff --git a/templates/metrics/chart.html b/templates/metrics/chart.html
index 7068262..357fc70 100644
--- a/templates/metrics/chart.html
+++ b/templates/metrics/chart.html
@@ -10,6 +10,7 @@
         <svg class="chart-body">
           <g class="chart-grid" />
           <g class="chart-data" />
+          <g class="chart-highlights" />
         </svg>
       </svg>
     </div>
diff --git a/templates/metrics/common.html b/templates/metrics/common.html
new file mode 100644
index 0000000..45e998e
--- /dev/null
+++ b/templates/metrics/common.html
@@ -0,0 +1,12 @@
+  <div id="hover">
+  </div>
+  <div id="details">
+    <div id="detailsTitle"><a id="detailsLink" target="buildDetails"></a></div>
+    <div id="detailsDetails"></div>
+    <div class="clear"></div>
+    <div id="detailsButtons">
+      <div id="detailsPrevious"> </div>
+      <div id="detailsNext"> </div>
+      <div id="detailsClose"> </div>
+    </div>
+  </div>
diff --git a/templates/metrics/home.html b/templates/metrics/home.html
index c44ba8f..5ba9d89 100644
--- a/templates/metrics/home.html
+++ b/templates/metrics/home.html
@@ -36,5 +36,6 @@
   {% endfor %}
     </ul>
   </div>
+  {% include "metrics/common.html" %}
 </body>
 </html>
diff --git a/templates/metrics/metric.html b/templates/metrics/metric.html
index 03ab43a..171bbb9 100644
--- a/templates/metrics/metric.html
+++ b/templates/metrics/metric.html
@@ -82,5 +82,6 @@
       </table>
     </div>
   </div>
+  {% include "metrics/common.html" %}
 </body>
 </html>
diff --git a/templates/metrics/target.html b/templates/metrics/target.html
index 1947dcf..362f89e 100644
--- a/templates/metrics/target.html
+++ b/templates/metrics/target.html
@@ -101,5 +101,6 @@
       </table>
     </div>
   </div>
+  {% include "metrics/common.html" %}
 </body>
 </html>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]