packages feed

gipeda (empty) → 0.1

raw patch · 23 files changed

+1985/−0 lines, 23 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, cassava, containers, directory, filepath, shake, split, text, unordered-containers, vector, yaml

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 2013 Joachim Breitner++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,108 @@+Gipeda -- the Git Performance Dashboard+=======================================++What is gipeda?+---------------++Gitpeda is a a tool that presents data from your program’s benchmark suite (or+any other source), with nice tables and shiny graphs.+++It is only a frontend and does not help with or care about collecting the data.+So it is up to you whether you have a polling shell script loop, a post-commit+hook or a elaborate jenkins setup. As long as the performance data ends up in+the `logs/` directory, gipeda is happy.++Gipeda produces static pages. In fact, the (single) html file and the+accompagning JavaScript code is completely static. Giepda just generates a+large number of json files. This has the advantage of easy deployment: Just put+gipeda in your webspace of copy the files to some static web hosting and you+are done. This putts very little load on your server, is cache friendly and has+no security problems.++Do you want to see it live? Check out these:++ * [Demo page], visualizing fairly boring stuff about gipedia itself.+ * [GHC’s gipeda installation].++[Demo page]: http://nomeata.github.io/gipeda/+[GHC’s gipeda installation]: https://perf.ghc.haskell.org/++Setting it up+-------------++ * Clone gipedia somewhere, possibly directly into your webspace.+ * Install a Haskell compiler, including the `cablal` tool.+ * Install the dependencies:++        cabal install --dependencies-only++ * Compile it:++        cabal install --bindir=.++ * Create a `settings.yaml`. You can look at the example file.+ * Clone the repository of your project into `repository/`. A bare clone is+   sufficient, e.g.++        git clone  --bare git://git.haskell.org/ghc.git repository++ * Download a bunch of JavaScript libraries by runing `./install-jslibs.sh`.	++Gipeda does not work without at least some logs, so lets add them.++Adding data+-----------++Gipeda expect simple CSV files for each revision, of the form++    benchmark1;1000+    benchmark2;20.123+    benchmark3;0++But likely your benchmark suite does not generate them in this format directly.+Hence, put whatever format you have (text base logs, JUnit reports, whatever)+into the directory `logs`, named `<gitrev>.log`, e.g.+`logs/0279a7d327a3b962ffa93a95d47ea5d9ee31e25c.log`.++Then create a script `log2csv` that expects the filename of such a log on on+the command line and produces the desired CSV file.++Running gipeda+--------------++With everything in place, you can now run++    ./gipeda++and it will create a bunch of JSON files in `site/out/`.  With `./gipda -j4`+you can parallize it.++You should do this everytime a new log file appears in `logs/`. You should also+make sure your repository is up-to-date, e.g. by running `git -C repository+pull` or, if it is a bare clone, `git -C repository fetch origin+"+refs/heads/*:refs/heads/*" --prune`.++Using gipedia+-------------++Finally, you simply point your browser to the `site/index.html`. The page+should be mostly self-explanatory. If you don’t see anything, it might be+because of the filter in the top-right corner. Try to enable all buttons, even+the `=`.++To host this on a webserver, just put the `site/` directory in your webspace.++Bugs, Code, Contact+-------------------++Please reports bugs and missing features at the [GitHub bugtracker]. This is+also where you can find the [source code].++Gipeda was written by [Joachim Breitner] and is licensed under a permissive MIT+[license].++[GitHub bugtracker]: https://github.com/nomeata/gipeda/issues+[source code]: https://github.com/nomeata/gipeda+[Joachim Breitner]: http://www.joachim-breitner.de/+[license]: https://github.com/nomeata/gipeda/blob/LICENSE
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ gipeda.cabal view
@@ -0,0 +1,81 @@+name:                gipeda+version:             0.1+category:            Development+synopsis:            Git Performance Dashboard+description:+ Gitpeda is a a tool that presents data from your program’s benchmark suite+ (or any other source), with nice tables and shiny graphs.+ .+ So it is up to you whether you have a polling shell script loop, a post-commit+ hook or a elaborate jenkins setup. As long as the performance data ends up in+ the `logs/` directory, gipeda is happy.+ .+ Gipeda produces static pages. In fact, the (single) html file and the+ accompagning JavaScript code is completely static. Giepda just generates a+ large number of json files. This has the advantage of easy deployment: Just put+ gipeda in your webspace of copy the files to some static web hosting and you+ are done. This putts very little load on your server, is cache friendly and has+ no security problems.+ .+ Do you want to see it live? Check out these:+ .+   * Demo page, visualizing fairly boring stuff about gipedia itself:+     http://nomeata.github.io/gipeda/+   * GHC’s gipeda installation:+     https://perf.ghc.haskell.org/+homepage:            https://github.com/nomeata/gipeda+license:             MIT+license-file:        LICENSE+author:              Joachim Breitner+maintainer:          mail@joachim-breitner.de+build-type:          Simple+extra-source-files:+  README.md,+  site/index.html,+  site/js/gipeda.js,+  install-jslibs.sh+cabal-version:       >=1.10++executable gipeda+  main-is:+      gipeda.hs++  other-modules:+    BenchmarkSettings,+    BenchmarksInCSV,+    BenchNames,+    GraphReport,+    IndexReport,+    JsonSettings,+    JsonUtils,+    ParentMap,+    Paths,+    ReadResult,+    ReportTypes,+    RevReport,+    Shake,+    Summary,+    WithLatestLogs++  build-depends:+      base                 >= 4.6  && <4.9,+      bytestring           >= 0.10 && <0.11,+      containers           >= 0.4  && <0.6,+      directory            >= 1.2  && <1.3,+      filepath             >= 1.3  && <1.4,+      shake                >= 0.13 && <0.15,+      text                 >= 0.11 && <1.3,+      unordered-containers >= 0.2  && <0.3,+      split                >= 0.2  && <0.3,+      vector               >= 0.10 && <0.11,+      cassava              >= 0.4  && <0.5,+      yaml                 >= 0.8  && <0.9,+      aeson                >= 0.7  && <0.9++  hs-source-dirs: src++  default-language: Haskell2010++source-repository head+  type:     git+  location: https://github.com/nomeat/gipeda
+ install-jslibs.sh view
@@ -0,0 +1,29 @@+#!/bin/bash++set -e+set -x++cd site/js+test -e signals.min.js ||+	wget -c https://raw.githubusercontent.com/millermedeiros/js-signals/master/dist/signals.min.js+test -e hasher.min.js ||+	wget -c https://raw.githubusercontent.com/millermedeiros/Hasher/master/dist/js/hasher.min.js+test -e handlebars-v2.0.0.js ||+	wget -c http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v2.0.0.js+test -e jquery-1.11.2.min.js ||+	wget -c http://code.jquery.com/jquery-1.11.2.min.js+test -e jquery.timeago.js ||+	wget -c http://timeago.yarp.com/jquery.timeago.js+test -d flot || {+	wget -c http://www.flotcharts.org/downloads/flot-0.8.3.zip+	unzip flot-0.8.3.zip+	rm -f flot-0.8.3.zip+}+test -d bootstrap || {+	mkdir bootstrap+	cd bootstrap+	wget -c https://github.com/twbs/bootstrap/releases/download/v3.3.1/bootstrap-3.3.1-dist.zip+	unzip bootstrap-3.3.1-dist.zip +	rm -f bootstrap-3.3.1-dist.zip +	cd ..+	}
+ site/index.html view
@@ -0,0 +1,375 @@+<!DOCTYPE html>+<html>+<head>+<meta charset="utf-8"/>+<meta http-equiv="X-UA-Compatible" content="IE=edge">+<meta name="viewport" content="width=device-width, initial-scale=1">++<title>Gipeda</title>+<script type="text/javascript" src="js/signals.min.js"></script>+<script type="text/javascript" src="js/hasher.min.js"></script>+<script type="text/javascript" src="js/handlebars-v2.0.0.js"></script>+<script src="js/jquery-1.11.2.min.js"></script>+<script src="js/jquery.timeago.js"></script>++<script src="js/flot/jquery.flot.min.js"></script>+<script src="js/flot/jquery.flot.resize.min.js"></script>++<link rel="stylesheet" href="js/bootstrap/dist/css/bootstrap.min.css">+<link rel="stylesheet" href="js/bootstrap/dist/css/bootstrap-theme.min.css">+<script src="js/bootstrap/dist/js/bootstrap.min.js"></script>+++<script type="text/javascript" src="js/gipeda.js"></script>++<style type="text/css">+html {+    overflow-y:scroll;+}++.panel-heading .accordion-toggle .indicator-untoggled {+    display: none;+    color: grey;+    padding-left: 1em;+}+.panel-heading .accordion-toggle .indicator-toggled {+    display: inline;+    color: grey;+    padding-left: 1em;+}+.panel-heading .accordion-toggle.collapsed .indicator-untoggled {+    display: inline;+}+.panel-heading .accordion-toggle.collapsed .indicator-toggled {+    display: none;+}++.summary-row-collapsed {+    display:none;+}++.benchmark-table {+    margin-bottom: 0px;+}++/* Trying to visualize collapsed rows */+.summary-table {+    border-collapse:collapse; +    +}+.summary-row-collapsed + .summary-row td {+    border-top: 4px dotted #DDD;++}+/* Couldn’t make it to work easily+.summary-row-collapsed td {+    background-color:grey;+    padding:1px;+    border: 0px;+    height:1px !important;+    line-height:0px;+    padding:0px !important;+    overflow:hidden !important;+}+*/++/* Order matters! */+.summary-improvement , .row-Improvement {+    background-color: #dff0d8;+}+.summary-regression, .row-Regression {+    background-color: #fcf8e3;+}++.benchmark-name a.graph-link {+    visibility:hidden;+}+.benchmark-name:hover a.graph-link {+    visibility:visible;+}++</style>++<script id="nothing" type="text/x-handlebars-template">+  <div class="jumbotron nothing-to-see" style="display:none">+   <p>Nothing selected... try the buttons in the top right corner.</p>+  </div>+</script>++<script id="nav"  type="text/x-handlebars-template">+ <nav class="navbar navbar-default">+  <div class="container">++    <!-- Inspired by http://stackoverflow.com/a/22978968/946226 -->+    <!-- Title -->+    <div class="navbar-header pull-left">+      <a class="navbar-brand" href="#">{{ settings.title }}</a>+    </div>++    <!-- 'Sticky' (non-collapsing) right-side menu item(s) -->+    <div class="navbar-header pull-right">+      <p class="navbar-text nav-loading pull-left">Loading data...</p>++      <ul class="nav pull-left">+       <li class="pull-left">+	<div class="btn-group" role="group">+	 <button type="button" class="btn btn-default navbar-btn active benchSelector" id="show-improvements" title="Show improvements">+	  <!-- <span class="glyphicon glyphicon-plus text-success"></span> -->+	  <span class="text-success" style="font-weight:bold">+</span>+	 </button>+	 <button type="button" class="btn btn-default navbar-btn benchSelector" id="show-boring" title="Show unchanged">+	  <span style="font-weight:bold">=</span>+	 </button>+	 <button type="button" class="btn btn-default navbar-btn active benchSelector" id="show-regressions" title="Show regressions">+	  <!-- <span class="glyphicon glyphicon-minus text-warning"></span> -->+	  <span class="text-warning" style="font-weight:bold">-</span>+	 </button>+	</div>+       </li>+      </ul>++      <!-- Required bootstrap placeholder for the collapsed menu -->+      <button type="button" data-toggle="collapse" data-target=".navbar-collapse" class="navbar-toggle"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button>+    </div>++    <!-- The Collapsing items            navbar-left or navbar-right -->+    <div class="collapse navbar-collapse navbar-left" id="bs-example-navbar-collapse-1">+      <ul class="nav navbar-nav pull-right">+      <!--                      pull-right keeps the drop-down in line -->+        <li><a href="{{indexLink}}">Revisions</a></li>+        <!-- <li><a href="{{allLink}}">All</a></li> -->+        <!-- <li><a href="{{revisionLink latest}}">Reports</a></li> -->+        <li><a href="{{graphIndexLink}}">Graphs</a></li>+      </ul>++    </div>+  </div>+ </nav>+</script>++<script id="summary-icons"  type="text/x-handlebars-template">+  <span  title="number of benchmarks">+   {{totalCount}}+   <span class="glyphicon glyphicon-stats"></span>+  </span>+  <span title="number of improvements"> +   {{improvementCount}}+   <span class="glyphicon glyphicon-plus text-success"></span>+  </span>+  <span title="number of regressions">+   {{regressionCount}}+   <span class="glyphicon glyphicon-minus text-warning"></span>+  </span>+</script>++<script id="summary-list"  type="text/x-handlebars-template">+  {{> nothing }}+  <table class="table summary-table">+   {{#each this}}+   {{#with summary}}+    <tr+     class="+       summary-row+       {{#if stats.improvementCount}}summary-improvement{{/if}}+       {{#if stats.regressionCount}}summary-regression{{/if}}+     ">+     <td class="col-md-2 text-right">+       <abbrv class="timeago" title="{{ iso8601 gitDate }}">{{ humanDate gitDate}}</abbrv>+     </td>+     <td class="col-md-1">+      <a href="{{revisionLink hash}}">+        <code>{{shortRev hash}}</code>+      </a>+     </td>+     <td class="col-md-7">+       {{ gitSubject }}+     </td>+     <td class="col-md-2 text-right">+      {{> summary-icons stats}}+     </td>+    </tr>+   {{/with}}+   {{/each}}+  </table>+</script>++<script id="revTooltip" type="text/x-handlebars-template">+ <a href="{{revisionLink hash}}"><code>{{shortRev hash}}</code></a>:+ {{ value }}<br/>+ {{ humanDate gitDate}}<br/>+ {{ gitSubject }}</br>+ {{> summary-icons stats}}+</script>++<script id="graphIndex" type="text/x-handlebars-template">+ {{> nav}}+ <div class="container">+  <h1>Benchmarks</h1>+ </div>+ <div class="container">+    <div class="panel-group" role="tablist">+     {{#each benchGroups}}+      <div class="panel panel-default">+       <div class="panel-heading" role="tab" id="heading-{{@index}}">+        <h4 class="panel-title">+         <a class="accordion-toggle" data-toggle="collapse" href="#table-{{@index}}">+         {{groupName}}+         </a>+        </h4>+       </div>++       <div id="table-{{@index}}" class="panel-collapse collapse in" role="tabpanel">+        <div class="panel-body">+         <table class="table table-condensed benchmark-table">+          <thead>+          <tr>+           <th class="col-md-6">Benchmark name</th>+          </tr>+          </thead>+          <tbody>+           {{#each groupMembers}}+            <tr>+            <td class="benchmark-name">+	     {{this}}+	     <a class="graph-link" title="Graphs" href="{{graphLink this}}">+	      <span class="glyphicon glyphicon-signal"/>+	     </a>+	    </td>+            </tr>+           {{/each}}+          </tbody>+         </table>+        </div>+       </div> +      </div>+     {{/each}}+    </div>+   </div>+</script>++<script id="index" type="text/x-handlebars-template">+ {{> nav}}+ <div class="container">+  <h1>Recent commits</h1>+  {{> summary-list (recentCommits revisions)}}+ </div>+ <div class="container">+  <p class="text-center">+    <a href="{{allLink}}">view older commits...</a>+  </p>+ </div>+</script>++<script id="complete" type="text/x-handlebars-template">+ {{> nav}}+ <div class="container">+  <h1>All commits</h1>+  {{> summary-list (allCommits revisions)}}+ </div>+</script>+++<script id="revision" type="text/x-handlebars-template">+ {{> nav}}++ <div class="container">+  <div class="row">+   <div class="col-md-6 col-md-push-6">+    {{#if rev.summary}}+    <h2>+     Commit {{shortRev rev.summary.hash}}+    </h2>+    <p>+    {{#if rev.summary.parents}}+    Displaying changes since:  +    {{#with (lookup rev.summary.parents 0)}}+      <a href="{{revisionLink this}}"><code>{{shortRev this}}</code></a> – +      <a href="{{diffLink this ../rev.summary.hash}}">View diff</a>+    {{/with}} –+      <a href="{{logLink rev.summary.hash}}">View buildlog</a> –+      {{> summary-icons rev.summary.stats }}+    {{else}}+    No parent commit found.+    {{/if}}+    </p> +    <pre>{{rev.gitLog}}</pre>+    {{/if}}+   </div>++   <div class="col-md-6 col-md-pull-6">+    {{> nothing }}+    <div class="panel-group" role="tablist">+     {{#each groups}}+      <div class="panel panel-default bench-panel">+       <div class="panel-heading" role="tab" id="heading-{{@index}}">+        <h4 class="panel-title">+         <a class="accordion-toggle" data-toggle="collapse" href="#table-{{@index}}">+         {{groupName}}+         <span class="stats pull-right">+          {{> summary-icons groupStats}}+          <span class="indicator-toggled glyphicon glyphicon-chevron-down text-grey"/>+          <span class="indicator-untoggled glyphicon glyphicon-chevron-right text-grey"/>+         </span>+         </a>+        </h4>+       </div>++       <div id="table-{{@index}}" class="panel-collapse collapse in" role="tabpanel">+        <div class="panel-body">+         <table class="table table-condensed benchmark-table">+          <thead>+          <tr>+          <th class="col-md-6">Benchmark name</th>+          <th class="col-md-2 text-right">previous</th>+          <th class="col-md-2 text-right"></th>+          <th class="col-md-2 text-right">value</th>+          </tr>+          </thead>+          <tbody>+           {{#each benchResults}}+            <tr class="row-result row-{{changeType}}">+            <td class="benchmark-name">+	     {{name}}+	     <a class="graph-link" title="Graphs" href="{{graphLink name ../../rev.summary.hash}}">+	      <span class="glyphicon glyphicon-signal"/>+	     </a>+	    </td>+            <td class="text-right">{{previous}}</td>+            <td class="text-right">{{change}}</td>+            <td class="text-right">{{value}}</td>+            </tr>+           {{/each}}+          </tbody>+         </table>+        </div>+       </div> +      </div>+     {{/each}}+    </div>+   </div>+  </div>+</script>++<script id="graph" type="text/x-handlebars-template">+ {{> nav}}++ <div class="container">+ <h2>{{benchName}}</h2>++ <div id="benchChart" style="width:100%; height:400px">+ </div>+</script>++</head>++<body>++<div id="main">+<div id="loading">+<p>+Loading...+</p>+<p>(If this does not disappear, then you do not have JavaScript enabled, or something else is broken.)</p>+</div>+</div>+</body>
+ site/js/gipeda.js view
@@ -0,0 +1,485 @@+// Main document+var data = {};+// Name of the current view+var view = 'index';+// Options of the current view+var viewData = {};++// Nonpersistent settings+var settings = {+    benchFilter: {+	improvements: true,+	boring: false,+	regressions: true,+    },+    collapsedGroups: [true, false, false, false]+};++// Signals++viewChanged = new signals.Signal()+dataChanged = new signals.Signal()+++// Routes++var routes = {+    index: +        { regex: /^$/,+          download: ['out/latest-summaries.json'],+          url: function () {return ""},+        },+    complete:+        { regex: /^all$/,+          download: ['out/all-summaries.json'],+          url: function () {return "all"},+        },+    graphIndex:+        { regex: /^graphs$/,+          download: ['out/benchNames.json'],+          url: function () {return "graphs"},+        },+    revision:+        { regex: /^revision\/([a-f0-9]+)$/,+          viewData: function (match) { return { hash: match[1] }; },+          download: function () {+            return ['out/benchNames.json','out/reports/' + viewData.hash + '.json'];+          },+          url: function (hash) { return "revision/" + hash; },+        },+    graph:+        { regex: /^graph\/(.*)$/,+          viewData: function (match, options) {+            var highlights = [];+            options.forEach(function (opt) {+                var m;+                if (m = /hl=(.*)/.exec(opt)) {+                    highlights.push(m[1]);+                }+            });+            return {+                benchName: match[1],+                highlights: highlights,+            };+          },+          download: function () {+            return ['out/latest-summaries.json','out/graphs/' + viewData.benchName + '.json'];+          },+          url: function (benchName, hls) {+            var comps = [ "graph/" + benchName ];+            $.merge(comps, hls.map(function (hl) { return "hl=" + hl }));+            return comps.join(';');+          },+        },+};++function parseRoute (path) {+    var options = path.split(";");+    var routePath = options.shift();++    $.each(routes, function (v,r) {+        var match = r.regex.exec(routePath);+        if (match) {+            view = v;+            if (r.viewData) viewData = r.viewData(match, options);+            return false;+        } else {+            return true;+        }+    });+    viewChanged.dispatch();+}++viewChanged.add(function () {+    if (routes[view].download) {+        d = routes[view].download;+        if ($.isFunction(d)) {d = d()}+        d.forEach(function (url) { getJSON(url) });+    }+});++function handleHashChange(newHash) {+    parseRoute(newHash);+}++// Data++function commitsFrom(revs, hash, count) {+  // Can happen during loading+  if (!revs) {return []};++  if (count == 0) return [];++  var rev = revs[hash];+  if (rev) {+    if (rev.summary.parents.length > 0) {+      var later = commitsFrom(revs, rev.summary.parents[0], count - 1);+      later.unshift(rev);+      return later;+    } else {+      return [ rev ];+    }+  } else {+    return []+  }+}+++// Template handling+var templates = {};++$(function ()  {+  var template_ids =  ["revision", "index", "complete", "graphIndex", "graph", "revTooltip"];+  template_ids.forEach(function(id) {+    var source = $("#" + id).html();+    templates[id] = Handlebars.compile(source);+  });++  var partials_ids =  ["nav", "summary-icons", "summary-list", "nothing"];+  partials_ids.forEach(function(id) {+    var source = $("#" + id).html();+    Handlebars.registerPartial(id, source);+  });++});++Handlebars.registerHelper('revisionLink', function(hash) {+  if (!hash) { return "#"; }+  return "#" + routes.revision.url(hash);+});+Handlebars.registerHelper('graphLink', function(benchName, hl) {+    if (hl) {+        return "#" + routes.graph.url(benchName,[hl]);+    } else {+        return "#" + routes.graph.url(benchName);+    }+});+Handlebars.registerHelper('diffLink', function(rev1, rev2) {+  return data.settings.cgitLink + "/commitdiff/" + rev2+});+Handlebars.registerHelper('logLink', function(rev) {+  return Handlebars.compile(data.settings.logLink)({rev:rev});+});+Handlebars.registerHelper('indexLink', function() {+  return "#" + routes.index.url();+});+Handlebars.registerHelper('allLink', function() {+  return "#" + routes.complete.url();+});+Handlebars.registerHelper('graphIndexLink', function() {+  return "#" + routes.graphIndex.url();+});+Handlebars.registerHelper('recentCommits', function(revisions) {+  return commitsFrom(revisions, data.latest, data.settings.limitRecent);+});+Handlebars.registerHelper('allCommits', function(revisions) {+  return commitsFrom(revisions, data.latest, -1);+});+function shortRev(rev) {+  if (!rev) { return ''; }+  return rev.substr(0,7);+}+Handlebars.registerHelper('shortRev', shortRev);+Handlebars.registerHelper('iso8601', function(timestamp) {+  if (!timestamp) { return '' };+  return new Date(timestamp*1000).toISOString();	   +});+Handlebars.registerHelper('humanDate', function(timestamp) {+  return new Date(timestamp*1000).toString();+});++// We cache everything+var jsonSeen = {};+function getJSON(url, callback, options) {+    var opts = {+        block: true,+    };+    $.extend(opts, options);+    if (jsonSeen[url]) {+	console.log("Not fetching "+url+" again.");+	if (callback) callback();+    } else {+	console.log("Fetching "+url+".");+        $.ajax(url, {+            success: function (newdata) {+		console.log("Fetched "+url+".");+	        jsonSeen[url] = true;+	        $.extend(true, data, newdata);+	        dataChanged.dispatch();+	        if (callback) callback();+            },+            dataType: 'json'+        });+    }+}+++// Views++function groupStats (benchResults) {+  return {+    totalCount: benchResults.length,+    improvementCount: benchResults.filter(function (br) { return br.changeType == 'Improvement' }).length,+    regressionCount:  benchResults.filter(function (br) { return br.changeType == 'Regression' }).length,+  }+}++// Some views require the data to be prepared in ways that are +// too complicated for the template, so lets do it here.+dataViewPrepare = {+  'revision': function (data, viewData) {+    if (!data.benchGroups || !data.revisions) return {};+    var hash = viewData.hash;+    var rev = data.revisions[hash];+    if (!rev) return {};+    if (!rev.benchResults) return {};++    var groups = data.benchGroups.map(function (group) {+      var benchmarks = group.groupMembers.map(function (bn) {+	return rev.benchResults[bn]+      }).filter(function (br) {return br});+      return {+	groupName: group.groupName,+	benchResults: benchmarks,+	groupStats: groupStats(benchmarks),+      };+    });+    return {+      rev : rev,+      groups : groups,+    };+  },+}++function load_template () {+    console.log('Rebuilding page');+    var context = {};+    $.extend(context, data, viewData, settings);+    if (dataViewPrepare[view]){+      $.extend(context, dataViewPrepare[view](data, viewData));+    }+    $('#main').html(templates[view](context));++    $(".nav-loading").toggle(jQuery.active > 0);++    updateBenchFilter();+    updateCollapsedGroups();+    $('abbrv.timeago').timeago();++    if ($('#benchChart').length) {+    	setupChart();+    }+}+viewChanged.add(load_template);+dataChanged.add(load_template);++function setupChart () {+    +    var commits = commitsFrom(data.revisions, data.latest, data.settings.limitRecent);+    var benchName = viewData.benchName;++    $("<div id='tooltip' class='panel alert-info'></div>").css({+		position: "absolute",+		display: "none",+		//border: "1px solid #fdd",+		padding: "2px",+		//"background-color": "#fee",+		// opacity: 0.80,+                width: '300px',+	}).appendTo("#main");++    var plot = $.plot("#benchChart",+	[{+	  lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" },+	  points: { show: true, fill: false },+	  label: benchName,+	  data: commits.map(function (x,i){+	    if (!x.benchResults) return;+	    if (!x.benchResults[benchName]) return;+	    return [commits.length - i, x.benchResults[benchName].value]+	  }),+	}],+	{	+	  legend: {+		position: 'nw',+	  },+	  grid: {+		hoverable: true,+		clickable: true,+	  },+	  xaxis: {+		// ticks: values.map(function (x,i){return [i,x[0]]}),+		tickFormatter: function (i,axis) {+			if (i > 0 && i <= commits.length) {+				var rev = commits[commits.length - i];+                                if (!rev) return "";+                                var date = new Date(rev.summary.gitDate * 1000);+                                return $.timeago(date);+			} else {+				return '';+			}+		}+	  }+	});++    viewData.highlights.forEach(function (hash) {+        commits.forEach(function (rev,i) {+            if (rev.summary.hash == hash)  {+                plot.highlight(0, i);+            };+        });+    });+++    $("#benchChart").bind("plothover", function (event, pos, item) {+	if (item) {+		var v = item.datapoint[1];+		var i = item.dataIndex;+                var rev = commits[i].summary.hash;+                var summary = commits[i].summary;++		var tooltipContext = $.extend({value: v}, summary);++                if ($("#tooltip").data('rev') != rev) {+                    $("#tooltip")+		    	.html(shortRev(rev))+			.data('rev',rev)+		        .html(templates.revTooltip(tooltipContext))+                        .fadeIn(200)+                        .css({top: item.pageY+10, left: item.pageX-100})+                        .show();+                    $("#benchChart").css('cursor','pointer');+                }+	} else {+		$("#tooltip").data('rev',null).hide();+		$("#benchChart").css('cursor','default');+	}+    });+    $("#benchChart").bind("plotclick", function (event, pos, item) {+	if (item) {+		var v = item.datapoint[1];+		var i = item.dataIndex;+                var hash = commits[i].summary.hash;+		+		goTo(routes.revision.url(hash));+	}+    });+}++function updateCollapsedGroups() {+    // Does not work yet...+    /*++    var list = settings.collapsedGroups;+    console.log(list);+    if ($(".panel-collapse").length) {+	$(".panel-collapse").each(function (i){+	    if (i < list.length) {+		console.log(i, list[i], this);+		$(this).toggleClass('in', !list[i]);++		$(this).collapse();+		if (list[i]) {+		    $(this).collapse('hide');+		} else {+		    $(this).collapse('show');+		}+	    }+	});+    }+    */+}++function updateBenchFilter() {+    var showRegressions  = settings.benchFilter.regressions;+    var showBoring       = settings.benchFilter.boring;+    var showImprovements = settings.benchFilter.improvements;++    $('#show-regressions').toggleClass('active', showRegressions);+    $('#show-boring').toggleClass('active', showBoring); +    $('#show-improvements').toggleClass('active', showImprovements);++    $('tr.row-Regression').toggle(showRegressions);+    $('tr.row-Boring').toggle(showBoring);+    $('tr.row-Improvement').toggle(showImprovements);++    $('.bench-panel').show().each(function() {+        if ($(this).has('tr.row-result:visible').length == 0) {+            $(this).hide();+        }+    });++    $('tr.summary-row').addClass('summary-row-collapsed');+    if (showBoring) {+        $('tr.summary-row:not(.summary-improvement):not(.summary-regression)')+            .removeClass('summary-row-collapsed');+    }+    if (showRegressions) {+        $('tr.summary-row.summary-regression')+            .removeClass('summary-row-collapsed');+    }+    if (showImprovements) {+        $('tr.summary-row.summary-improvement')+            .removeClass('summary-row-collapsed');+    }+    $('tr.summary-row').first()+            .removeClass('summary-row-collapsed');++    updateNothingToSee();+};++function updateNothingToSee() {+    $('.nothing-to-see')+	.toggle(jQuery.active == 0 && $('.bench-panel:visible, tr.summary-row:not(.summary-row-collapsed)').length == 0);+}++$(function (){+    $('body').on('click', '.benchSelector', function (event) {+        $(this).toggleClass('active');+	settings.benchFilter = {+	    regressions:  $('#show-regressions').hasClass('active'),+	    boring:       $('#show-boring').hasClass('active'), +	    improvements: $('#show-improvements').hasClass('active'), +	};+        updateBenchFilter();+    });+});++// Redirection++function goTo(path) {+    console.log("goTo " + path);+    hasher.setHash(path);+}+++// Main setup++$(function() {+    hasher.prependHash = '';++    $('#loading').hide();++    $(document).ajaxStart(function () {+	$(".nav-loading").show();+	updateNothingToSee();+    });+    $(document).ajaxStop(function () {+	$(".nav-loading").hide();+	updateNothingToSee();+    });++    // Load settins, then figure out what view to use.+    $.get("out/latest.txt", function (latest) {+        data.latest = latest;++        getJSON("out/settings.json", function (settings) {+	    $('title').html(data.settings.title + " – Gipeda");+            hasher.changed.add(handleHashChange);+            hasher.initialized.add(handleHashChange);+            hasher.init();+        });+    }, 'text');+++});
+ src/BenchNames.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+module BenchNames where++import qualified Data.ByteString.Lazy as BS+import Data.Aeson+import Control.Monad+import qualified Data.Map as M+import qualified Data.Text as T+import Data.List++import Paths+import JsonUtils+import ReportTypes+import qualified BenchmarkSettings as S+++benchNamesMain :: [S.BenchName] -> IO ()+benchNamesMain benchNames = do+    settings <- S.readSettings "settings.yaml"++    let groups =+            map (uncurry BenchGroup) $+            sort $+            M.toList $+            M.map sort $+            M.fromListWith (++)+                [ (S.group s, [bn])+                | bn <- benchNames+                , let s = S.benchSettings settings bn+                ]+    let doc = emptyGlobalReport { benchGroups = Just groups }++    BS.putStr (encode doc)
+ src/BenchmarkSettings.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE OverloadedStrings #-}+module BenchmarkSettings where++import Data.Yaml+import Data.Aeson+import qualified Data.Vector as V+import Data.Functor+import Control.Applicative+import Control.Monad+import Data.Monoid+import Data.Maybe+++data NumberType = IntegralNT | SmallIntegralNT | FloatingNT+    deriving Show++type BenchName = String+data BenchSettings = BenchSettings+    { smallerIsBetter :: Bool+    , unit :: String+    , numberType :: NumberType+    , group :: String+    , threshold :: Double+    }+    deriving Show++defaultBenchSettings :: BenchSettings+defaultBenchSettings = BenchSettings True "" IntegralNT "" 3++newtype S = S { unS :: BenchName -> BenchSettings }+newtype SM = SM (BenchName -> (BenchSettings -> BenchSettings))++instance Monoid SM where+    mempty = SM (const id)+    mappend (SM f) (SM g) = SM (\n -> g n . f n)++instance FromJSON NumberType where+    parseJSON = withText "type" $ \t -> case t of+        "small integral" -> return SmallIntegralNT+        "integral" -> return IntegralNT+        "float" -> return FloatingNT+        v -> fail $ "Unknown \"type\": " ++ show v++-- Very naive glob, * only at the end+matches :: String -> String -> Bool+matches [] [] = True+matches _ ('*':[]) = True+matches (x:xs) (m:ms) = x == m && matches xs ms++instance FromJSON SM where+    parseJSON (Object o) = do+        m <- o .: "match"+        mt <- o .:? "type"+        mu <- o .:? "unit"+        mg <- o .:? "group"+        ms <- o .:? "smallerIsBetter"+        mth <- o .:? "threshold"+        return $ SM $ \n b -> +            if n `matches` m then+               b { numberType      = fromMaybe (numberType b) mt+                 , unit            = fromMaybe (unit b) mu+                 , group           = fromMaybe (group b) mg+                 , smallerIsBetter = fromMaybe (smallerIsBetter b) ms+                 , threshold       = fromMaybe (threshold b) mth+                 }+            else b+    parseJSON _ = mzero++instance FromJSON S where+    parseJSON (Array a) = do+        mods <- mapM parseJSON (V.toList a)+        let SM sm = mconcat mods+        return $ S $ \n -> sm n defaultBenchSettings++data Settings = Settings+   { title :: String+   , cgitLink :: String+   , logLink :: String+   , limitRecent :: Integer+   , start :: String+   , benchSettings :: BenchName -> BenchSettings+   }++instance FromJSON Settings where+    parseJSON (Object v) =+        Settings <$> v .: "title"+                 <*> v .: "cgitLink"+                 <*> v .: "logLink"+                 <*> v .: "limitRecent"+                 <*> v .: "start"+                 <*> (unS <$> v.: "benchmarks")+    parseJSON _ = mzero+++readSettings :: FilePath -> IO Settings+readSettings fname = either (error.show) id <$> decodeFileEither fname
+ src/BenchmarksInCSV.hs view
@@ -0,0 +1,24 @@+module BenchmarksInCSV where++import Data.Csv hiding (encode)+import qualified Data.ByteString.Lazy as BS+import qualified Data.Vector as V+import Data.Char+import Data.Functor++type Benchmark = String++benchmarksInCSVFile :: FilePath -> IO [Benchmark]+benchmarksInCSVFile fname = benchmarksInCSV <$> BS.readFile fname++benchmarksInCSV :: BS.ByteString -> [Benchmark]+benchmarksInCSV s =+    [ n+    | (n,v) <- either error V.toList $ decodeWith ssv NoHeader s+    , const True (v::String)+    ]+    +ssv :: DecodeOptions+ssv = defaultDecodeOptions {+    decDelimiter = fromIntegral (ord ';')+}
+ src/GraphReport.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}+module GraphReport where++import Control.Monad+import qualified Data.Map as M+import qualified Data.ByteString.Lazy as BS+import qualified Data.Text as T+import Data.Aeson+import GHC.Generics++import Paths+import ReadResult+import ReportTypes++graphReportMain :: [String] -> IO ()+graphReportMain (bench:revs) = do+    g <- forM revs $ \rev -> do+        m <- readCSV rev+        let v = M.lookup bench m+        return $ T.pack rev .= object+            [ "benchResults" .= object+                [ T.pack bench .= object+                    [ "value" .= v ]+                ]+            ]+    let doc = object ["revisions" .= object g]++    BS.putStr (encode doc)+    
+ src/IndexReport.hs view
@@ -0,0 +1,19 @@+module IndexReport where++import qualified Data.ByteString.Lazy as BS+import Data.Aeson+import Control.Monad++import Paths+import JsonUtils+++indexReportsMain :: [FilePath] -> IO ()+indexReportsMain revs = do+    g <- forM revs $ \rev -> do+        json <- BS.readFile (summaryOf rev)+        case eitherDecode json of+            Left e -> fail e+            Right rep -> return (rep :: Value)++    BS.putStr (encode (merges g))
+ src/JsonSettings.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}++module JsonSettings where++import Data.Aeson+import qualified Data.ByteString.Lazy as BS++import BenchmarkSettings as S++jsonSettingsMain :: IO ()+jsonSettingsMain = do+    Settings {..} <- S.readSettings "settings.yaml"+    let o = object+            [ "settings" .= object+                [ "title" .= title+                , "cgitLink" .= cgitLink+                , "logLink" .= logLink+                , "limitRecent" .= limitRecent+                ]+            ]+    BS.putStr (encode o)++++    +
+ src/JsonUtils.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++module JsonUtils where++import qualified Data.Text as T+import qualified Data.HashMap.Strict as HM+import Data.Aeson+import Data.List (foldl')++mapObject :: (Object -> Object) -> Value -> Value+mapObject f (Object m) = Object (f m)+mapObject _ v =  error $ "Not an object " ++ show v++delete :: [T.Text] -> Value -> Value+delete []       = error "delete []"+delete ["*"]    = error "delete [\"*\"]"+delete [k]      = mapObject $ HM.delete k+delete ("*":ks) = mapObject $ HM.map (delete ks)+delete (k:ks)   = mapObject $ HM.adjust (delete ks) k++merge :: Value -> Value -> Value+merge (Object m1) (Object m2) = Object $ HM.unionWith merge m2 m1+merge v1 v2 = error $ "Cannot merge " ++ show v1 ++ " with " ++ show v2++merges :: [Value] -> Value+merges = foldl' merge (Object HM.empty)+++
+ src/ParentMap.hs view
@@ -0,0 +1,28 @@+module ParentMap where++import Data.Csv hiding (encode)+import qualified Data.Map as M+import Data.List.Split+import qualified Data.ByteString.Lazy as BS+import qualified Data.Vector as V+import Data.Char+import Data.Functor++type Hash = String+type ParentMap = M.Map Hash Hash++ssvFileToMap :: FilePath -> IO ParentMap+ssvFileToMap fname = ssvToMap <$> BS.readFile fname++ssvToMap :: BS.ByteString -> M.Map Hash Hash+ssvToMap s = M.fromList +    [ (k,p)+    | (k,parentList) <- either error V.toList $ decodeWith ssv NoHeader s+    , let parents = splitOn " " parentList+    , p:_ <- return parents+    ]+    +ssv :: DecodeOptions+ssv = defaultDecodeOptions {+    decDelimiter = fromIntegral (ord ';')+}
+ src/Paths.hs view
@@ -0,0 +1,16 @@+module Paths where++import System.FilePath++type Hash = String++out :: FilePath+out = "site" </> "out"++resultsOf, reportOf, summaryOf, logsOf, graphFile :: Hash -> FilePath++logsOf hash = "logs" </> hash <.> "log"+resultsOf hash = out </> "results" </> hash <.> "csv"+reportOf hash = out </> "reports" </> hash <.> "json"+summaryOf hash = out </> "summaries" </> hash <.> "json"+graphFile bench = out </> "graphs" </> bench <.> "json"
+ src/ReadResult.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE DeriveGeneric #-}++module ReadResult where++import Data.Csv hiding (encode)+import Data.Functor+import Control.Applicative+import qualified Data.Map as M+import qualified Data.ByteString.Lazy as BS+import qualified Data.Vector as V+import System.Directory+import Data.Aeson hiding (decode)+import GHC.Generics+import Data.Char++import Paths++type BenchName = String++data BenchValue =+    F Double |+    I Integer+ deriving (Show, Generic, Eq, Ord)++type ResultMap = M.Map String BenchValue++instance ToJSON BenchValue where +    toJSON (F d) = toJSON d+    toJSON (I i) = toJSON i+instance FromJSON BenchValue where +    parseJSON o = (I <$> parseJSON o) <|> (F <$> parseJSON o)++instance FromField BenchValue where+    parseField s = (I <$> parseField s) <|> (F <$> parseField s)++readCSV :: Hash -> IO ResultMap+readCSV hash = do+  ex <- doesFileExist (resultsOf hash)+  if not ex+     then return M.empty+     else do+            str <- BS.readFile (resultsOf hash)+            let rows = either error id $ decodeWith ssv NoHeader str+            return $ M.fromList $ V.toList $ rows++ssv :: DecodeOptions+ssv = defaultDecodeOptions {+    decDelimiter = fromIntegral (ord ';')+}+
+ src/ReportTypes.hs view
@@ -0,0 +1,215 @@+{-# LANGUAGE DeriveGeneric, ViewPatterns, RecordWildCards, OverloadedStrings #-}++module ReportTypes where++import qualified Data.Map as M+import Data.Aeson+import GHC.Generics+import Text.Printf+import Data.List++import Paths+import ReadResult+import qualified BenchmarkSettings as S++data ClientSettings = ClientSettings+   { title :: String+   , cgitLink :: String+   , logLink :: String+   }+ deriving (Generic)+instance ToJSON ClientSettings++data GlobalReport = GlobalReport+    { settings :: Maybe ClientSettings+    , benchmarks :: Maybe (M.Map BenchName ())+    , revisions :: Maybe (M.Map Hash RevReport)+    , benchGroups :: Maybe [BenchGroup]+    }++instance ToJSON GlobalReport where+    toJSON (GlobalReport {..}) = object $+        ( "settings"    .=? settings ) +++        ( "benchmarks"  .=? benchmarks ) +++        ( "revisions"   .=? revisions ) +++        ( "benchGroups" .=? benchGroups )+      where+        k .=? Just v  = [ k .= toJSON v ]+        _ .=? Nothing = []+++emptyGlobalReport :: GlobalReport+emptyGlobalReport = GlobalReport Nothing Nothing Nothing Nothing+++data SummaryStats = SummaryStats+    { totalCount :: Int+    , improvementCount :: Int+    , regressionCount :: Int+    }+ deriving (Show, Generic)+instance ToJSON SummaryStats+instance FromJSON SummaryStats++sumStats :: [SummaryStats] -> SummaryStats+sumStats = foldl' go (SummaryStats 0 0 0)+  where go (SummaryStats a b c) (SummaryStats a' b' c') =+            SummaryStats (a + a') (b + b') (c + c')++data Summary = Summary+    { hash :: Hash+    , gitDate :: Integer+    , gitSubject :: String+    , stats :: SummaryStats+    , parents :: [String]+    }+ deriving (Generic)+instance ToJSON Summary+instance FromJSON Summary+++data RevReport = RevReport+    { summary :: Summary+    , gitLog :: String+    , benchResults :: M.Map BenchName BenchResult+    }+ deriving (Generic)+instance ToJSON RevReport+instance FromJSON RevReport++data ChangeType = Improvement | Boring | Regression+ deriving (Generic)+instance ToJSON ChangeType+instance FromJSON ChangeType++data BenchGroup = BenchGroup+    { groupName :: String+    , groupMembers :: [BenchName]+    }+ deriving (Generic)+instance ToJSON BenchGroup+instance FromJSON BenchGroup++data BenchResult = BenchResult+    { name :: String+    , value :: BenchValue+    , previous :: Maybe BenchValue+    , change :: String+    , changeType :: ChangeType+    , unit :: String+    }+ deriving (Generic)+instance ToJSON BenchResult+instance FromJSON BenchResult++invertChangeType :: ChangeType -> ChangeType+invertChangeType Improvement = Regression+invertChangeType Boring = Boring+invertChangeType Regression = Improvement++type Explanation = (String, ChangeType)++noExplanation :: Explanation+noExplanation = ("", Boring)++equalExplanation :: Explanation+equalExplanation = ("=", Boring)++explainSmallInt :: S.BenchSettings -> Integer -> Integer -> Explanation+explainSmallInt _ i1 i2+    | i2 == i1 = equalExplanation +    | i2 > i1 = ("+ " ++ show (i2 - i1), Improvement)+    | i2 < i1 = ("- " ++ show (i1 - i2), Regression)++explainInt :: S.BenchSettings -> Integer -> Integer -> Explanation+explainInt s i1 i2 = explainFloat s (fromIntegral i1) (fromIntegral i2)++explainFloat :: S.BenchSettings -> Double -> Double -> Explanation+explainFloat _ 0 0 = equalExplanation+explainFloat _ 0 _ = ("+ ∞", Improvement)+explainFloat s f1 f2 = (change, typ)+  where+    change | abs perc < 0.01 = "="+           | perc  >= 0 = printf "+ %.2f%%" perc+           | perc  <  0 = printf "- %.2f%%" (-perc)+    typ | abs perc < th = Boring+        | perc  >= 0 = Improvement+        | perc  <  0 = Regression++    perc = 100 * ((f2 - f1) / f1)+    th = S.threshold s++toFloat :: BenchValue -> Double+toFloat (I i) = fromIntegral i+toFloat (F f) = f++explain :: S.BenchSettings -> BenchValue -> BenchValue -> (String, ChangeType)+explain s@(S.numberType -> S.SmallIntegralNT) (I i1) (I i2) = explainSmallInt s i1 i2+explain s@(S.numberType -> S.IntegralNT)      (I i1) (I i2) = explainInt s i1 i2+explain s@(S.numberType -> S.FloatingNT)      v1     v2     = explainFloat s (toFloat v1) (toFloat v2)+explain _ _ _ = noExplanation++toResult :: S.BenchSettings -> String -> BenchValue -> Maybe BenchValue -> BenchResult+toResult s name value prev = BenchResult+    { name = name+    , value = value+    , previous = prev+    , change = change+    , changeType = changeType+    , unit = S.unit s+    }+  where +    (change, changeType') =+        case prev of+            Just p -> explain s p value+            Nothing -> noExplanation+    changeType | S.smallerIsBetter s = invertChangeType changeType'+               | otherwise           =                  changeType'++toSummaryStats :: [BenchResult] -> SummaryStats+toSummaryStats res = SummaryStats+    { totalCount = length res+    , improvementCount = length [ () | BenchResult { changeType = Improvement } <- res ]+    , regressionCount =  length [ () | BenchResult { changeType = Regression } <- res ]+    }+++{-+toGroup :: String -> [BenchResult] -> BenchGroup+toGroup n res = BenchGroup+    { groupName = n+    , benchResults = res+    , groupStats = SummaryStats+        { totalCount = length res+        , improvementCount = length [ () | BenchResult { changeType = Improvement } <- res ]+        , regressionCount =  length [ () | BenchResult { changeType = Regression } <- res ]+        }+    }+-}++createReport ::+    S.Settings -> Hash -> [Hash] ->+    ResultMap -> ResultMap ->+    String -> String -> Integer ->+    RevReport+createReport settings this parents thisM parentM log subject date = RevReport +    { summary = Summary+        { hash = this+        , parents = parents+        , stats = toSummaryStats $ M.elems results+        , gitSubject = subject+        , gitDate = date+        }+    --, benchGroups = benchGroups+    , benchResults = results+    , gitLog = log+    }+  where+    results = M.fromList+        [ (name, toResult s name value (M.lookup name parentM))+        | (name, value) <- M.toAscList thisM+        , let s = S.benchSettings settings name+        ]++summarize :: RevReport -> Summary+summarize (RevReport {..}) =  summary 
+ src/RevReport.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DeriveGeneric #-}++module RevReport where++import Data.Functor+import qualified Data.Map as M+import qualified Data.ByteString.Lazy as BS+import Data.Aeson+import Development.Shake.Command++import ReportTypes+import ReadResult+import qualified BenchmarkSettings as S++git args = cmd (words "git -C repository" ++ args)++revReportMain :: [String] -> IO ()+revReportMain (this:parents) = do+    settings <- S.readSettings "settings.yaml"++    thisM <- readCSV this+    parentM <- case parents of+        p:_ -> readCSV (head parents)+        _    -> return M.empty++    log <- case parents of +        p:_ -> fromStdout <$> git ["log", p ++ ".."++ this]+        _   -> fromStdout <$> git ["show", "-s", this]++    msg <- fromStdout <$> git ["show", "--format=%s", "-s", this]+    date <- read . fromStdout <$> git ["show", "--format=%ct","-s",this]++    let rep = createReport settings this parents thisM parentM log msg date+    let doc = emptyGlobalReport { revisions = Just (M.singleton this rep) }++    BS.putStr (encode doc)+    
+ src/Shake.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving   #-}+module Shake where++import Prelude hiding ((*>))++import Development.Shake+import Development.Shake.FilePath+import Development.Shake.Classes+import Control.Monad+import qualified Data.Map as M+import Data.Functor+import Data.List++import Paths hiding (Hash)+import ParentMap+import BenchmarksInCSV+import qualified BenchmarkSettings as S++{- Global settings -}+cGRAPH_HISTORY :: Integer+cGRAPH_HISTORY = 50++git :: (CmdResult b) => String -> [String] -> Action b+git gitcmd args = do+    cmd (Traced $ "git " ++ gitcmd) (words "git -C repository" ++ gitcmd : args)++self :: (CmdResult b) => String -> [String] -> Action b+self name args = do+    -- orderOnly ["gipeda"]+    cmd (Traced name) "./gipeda" name args++gitRange :: Action String+gitRange = do+    s <- liftIO $ S.readSettings "settings.yaml"+    let first = S.start s+    [head] <- readFileLines "site/out/head.txt"+    return $ first ++ ".." ++ head++needIfThere :: [FilePath] -> Action [FilePath]+needIfThere files = do+    existing <- filterM doesFileExist files+    need existing+    return existing++findPred, findPredOrSelf :: ParentMap -> Hash -> Action (Maybe Hash)+findPredOrSelf m h = do+    ex <- doesFileExist (logsOf h)+    if ex then return (Just h)+          else findPred m h+findPred m h = case M.lookup h m of +    Just h' -> findPredOrSelf m h'+    Nothing -> return Nothing++findRecent :: ParentMap -> Integer -> FilePath -> Action [FilePath]+findRecent _ 0 _ = return []+findRecent m n h = do+    pM <- findPred m h+    (h:) <$> case pM of+        Nothing -> return []+        Just p ->  findRecent m (n-1) p++newtype LimitRecent = LimitRecent ()+    deriving (Show,Typeable,Eq,Hashable,Binary,NFData)++shakeMain :: IO ()+shakeMain = shakeArgs shakeOptions $ do++{-+    "gipeda" *> \out ->  do+        sources <- getDirectoryFiles "src" ["*.hs"]+        need (map ("src" </>) sources)+        cmd "ghc -isrc --make -O src/gipeda.hs -o" out+    want ["gipeda"]+-}++    getLimitRecent <- addOracle $ \(LimitRecent _) -> do+        need ["settings.yaml"]+        S.limitRecent <$> liftIO (S.readSettings "settings.yaml")++    "reports" ~> do+        range <- gitRange+        Stdout range <- git "log" ["--format=%H",range]+        let hashes = words range+        withLogs <- filterM (doesFileExist . logsOf) hashes+        need $ map reportOf withLogs+    want ["reports"]++    "summaries" ~> do+        range <- gitRange+        Stdout range <- git "log" ["--format=%H",range]+        let hashes = words range+        withLogs <- filterM (doesFileExist . logsOf) hashes+        need $ map summaryOf withLogs+    want ["summaries"]++    "site/out/head.txt" *> \ out -> do+        alwaysRerun+        Stdout stdout <- git "rev-parse" ["master"]+        writeFileChanged out stdout+     ++    "site/out/history.csv" *> \out -> do+        range <- gitRange+        Stdout stdout <- git "log" ["--format=%H;%P",range]+        writeFileChanged out stdout+    want ["site/out/history.csv"]++    history' <- newCache $ \() -> do+         orderOnly ["site/out/history.csv"]+         liftIO $ ssvFileToMap "site/out/history.csv"+    let history = history' ()+    let pred h = do { hist <- history; findPred hist h }+    let predOrSelf h = do { hist <- history; findPredOrSelf hist h }+    let recent n h = do { hist <- history; findRecent hist n h }++    "site/out/latest.txt" *> \ out -> do+        [head] <- readFileLines "site/out/head.txt"+        latestM <- predOrSelf head+        case latestM of+           Just latest -> +                writeFileChanged out latest+           Nothing -> +                fail "Head has no parent with logs?"++    "graphs" ~> do+        [latest] <- readFileLines "site/out/latest.txt"+        need [resultsOf latest]+        b <- liftIO $ benchmarksInCSVFile (resultsOf latest)+        need (map graphFile b)+    want ["graphs"]++    "site/out/results/*.csv" *> \out -> do+        let hash = takeBaseName out+        need [logsOf hash]+        Stdout csv <- cmd "./log2csv" (logsOf hash)+        writeFile' out csv++    "site/out/graphs//*.json" *> \out -> do+        let bench = dropDirectory1 (dropDirectory1 (dropDirectory1 (dropExtension out)))++        [latest] <- readFileLines "site/out/latest.txt"+        limitRecent <- getLimitRecent (LimitRecent ())+        r <- recent limitRecent latest+        need (map resultsOf r)++        Stdout json <- self "GraphReport" (bench : r)+        writeFile' out json++    "site/out/reports/*.json" *> \out -> do+        let hash = takeBaseName out+        need [resultsOf hash]++        pred <- pred hash+        need [resultsOf h | Just h <- return pred]++        Stdout json <- self "RevReport" (hash : [h | Just h <- return pred])+        writeFile' out json++    "site/out/summaries/*.json" *> \out -> do+        let hash = takeBaseName out+        need [reportOf hash]++        Stdout json <- self "Summary" [hash]+        writeFile' out json++    "site/out/latest-summaries.json" *> \out -> do+        [latest] <- readFileLines "site/out/latest.txt"+        r <- recent cGRAPH_HISTORY latest+        need (map summaryOf r)++        Stdout json <- self "IndexReport" r+        writeFile' out json+    want ["site/out/latest-summaries.json"]++    "site/out/benchNames.json" *> \out -> do+        [latest] <- readFileLines "site/out/latest.txt"+        need [resultsOf latest]+        b <- liftIO $ benchmarksInCSVFile (resultsOf latest)++        need ["settings.yaml"]++        Stdout json <- self "BenchNames" (nub b)+        writeFile' out json+    want ["site/out/benchNames.json"]+        +++    "site/out/all-summaries.json" *> \out -> do+        range <- gitRange+        Stdout range <- git "log" ["--format=%H",range]+        let hashes = words range+        withLogs <- filterM (doesFileExist . logsOf) hashes+        need (map summaryOf withLogs)++        Stdout json <- self "IndexReport" withLogs+        writeFile' out json+    want ["site/out/all-summaries.json"]++    "site/out/settings.json" *> \out -> do+        need ["settings.yaml"]++        Stdout json <- self "JsonSettings" []+        writeFile' out json+    want ["site/out/settings.json"]++    phony "clean" $ do+        removeFilesAfter "site/out" ["//*"]+
+ src/Summary.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE RecordWildCards, DeriveGeneric, OverloadedStrings #-}+module Summary where++import qualified Data.ByteString.Lazy as BS+import Data.Aeson++import Paths+import JsonUtils++summaryMain :: [FilePath] -> IO ()+summaryMain [rev] = do+    json <- BS.readFile (reportOf rev)+    rep <- case eitherDecode json of+        Left e -> fail e+        Right rep -> return rep+    let rep' = delete ["revisions","*","benchResults"] $+               delete ["revisions","*","gitLog"] $+               rep+    BS.putStr (encode rep')
+ src/WithLatestLogs.hs view
@@ -0,0 +1,19 @@+module WithLatestLogs where++import Development.Shake.Command+import System.Directory+import Control.Monad++import Paths++git args = cmd (words "git -C repository" ++ args)++withLatestLogsMain :: [String] -> IO ()+withLatestLogsMain (n:args) = do+    Stdout out <- git ["log", "--first-parent", "--format=%H", "master", "-n", n]+    let revs = words out+    +    logs <- filterM (doesFileExist . logsOf) revs++    () <- cmd args (map logsOf logs)+    return ()
+ src/gipeda.hs view
@@ -0,0 +1,38 @@+import System.Environment+import System.IO+import System.Directory+import Control.Monad++import Shake+import JsonSettings+import RevReport+import WithLatestLogs+import Summary+import IndexReport+import GraphReport+import BenchNames++{-+We want to build everything into one executable, bit still treat+it as multiple tools. Hence the main function+here calls the various real main functions, defaulting to the shake tool.+-}++main :: IO ()+main = do+    args <- getArgs++    ex <- doesFileExist "settings.yaml"+    unless ex $ do+        hPutStr stderr "Please run this from the same directory as the settings.yaml file."+++    case args of +        "IndexReport":opts    -> indexReportsMain opts+        "JsonSettings":_      -> jsonSettingsMain+        "Summary":opts        -> summaryMain opts+        "RevReport":opts      -> revReportMain opts+        "GraphReport":opts    -> graphReportMain opts+        "WithLatestLogs":opts -> withLatestLogsMain opts+        "BenchNames":opts     -> benchNamesMain opts+        _ -> shakeMain -- shake will take the arguments from getArgs