Hack to work around handlebars wrongly indenting stuff
[gipeda.git] / site / js / gipeda.js
1 // Main document
2 var data = {};
3 // Name of the current view
4 var view = 'index';
5 // Options of the current view
6 var viewData = {};
7
8 // Nonpersistent settings
9 var settings = {
10     benchFilter: {
11         improvements: true,
12         boring: false,
13         regressions: true,
14     },
15     collapsedGroups: [true, false, false, false]
16 };
17
18 // Signals
19
20 viewChanged = new signals.Signal()
21 dataChanged = new signals.Signal()
22
23
24 // Routes
25
26 var routes = {
27     index:
28         { regex: /^$/,
29           download: ['out/latest-summaries.json'],
30           url: function () {return ""},
31         },
32     complete:
33         { regex: /^all$/,
34           download: ['out/all-summaries.json'],
35           url: function () {return "all"},
36         },
37     graphIndex:
38         { regex: /^graphs$/,
39           download: ['out/benchNames.json', 'out/graph-summaries.json'],
40           url: function () {return "graphs"},
41         },
42     revision:
43         { regex: /^revision\/([a-f0-9]+)$/,
44           viewData: function (match) { return { hash: match[1] }; },
45           download: function () {
46             return ['out/benchNames.json','out/reports/' + viewData.hash + '.json'];
47           },
48           url: function (hash) { return "revision/" + hash; },
49         },
50     compare:
51         { regex: /^compare\/([a-f0-9]+)\/([a-f0-9]+)$/,
52           viewData: function (match) { return { hash1: match[1], hash2: match[2] }; },
53           download: function () {
54             return ['out/benchNames.json',
55                     'out/reports/' + viewData.hash1 + '.json',
56                     'out/reports/' + viewData.hash2 + '.json'
57                     ];
58           },
59           url: function (hash1, hash2) { return "compare/" + hash1 + "/" + hash2; },
60         },
61     graph:
62         { regex: /^graph\/(.*)$/,
63           viewData: function (match, options) {
64             var highlights = [];
65             options.forEach(function (opt) {
66                 var m;
67                 if (m = /hl=(.*)/.exec(opt)) {
68                     highlights.push(m[1]);
69                 }
70             });
71             return {
72                 benchName: match[1],
73                 highlights: highlights,
74             };
75           },
76           download: function () {
77             return ['out/latest-summaries.json','out/graphs/' + viewData.benchName + '.json'];
78           },
79           url: function (benchName, hls) {
80             var comps = [ "graph/" + benchName ];
81             $.merge(comps, hls.map(function (hl) { return "hl=" + hl }));
82             return comps.join(';');
83           },
84         },
85 };
86
87 function parseRoute (path) {
88     var options = path.split(";");
89     var routePath = options.shift();
90
91     $.each(routes, function (v,r) {
92         var match = r.regex.exec(routePath);
93         if (match) {
94             view = v;
95             if (r.viewData) viewData = r.viewData(match, options);
96             return false;
97         } else {
98             return true;
99         }
100     });
101     viewChanged.dispatch();
102 }
103
104 viewChanged.add(function () {
105     if (routes[view].download) {
106         d = routes[view].download;
107         if ($.isFunction(d)) {d = d()}
108         d.forEach(function (url) { getJSON(url) });
109     }
110 });
111
112 function handleHashChange(newHash) {
113     parseRoute(newHash);
114 }
115
116 // Data
117
118 function commitsFrom(revs, hash, count) {
119   // Can happen during loading
120   if (!revs) {return []};
121
122   if (count == 0) return [];
123
124   var rev = revs[hash];
125   if (rev) {
126     if (rev.summary.parents.length > 0) {
127       var later = commitsFrom(revs, rev.summary.parents[0], count - 1);
128       later.unshift(rev);
129       return later;
130     } else {
131       return [ rev ];
132     }
133   } else {
134     return []
135   }
136 }
137
138
139 // Template handling
140 var templates = {};
141
142 $(function ()  {
143   var template_ids =  ["revision", "compare", "index", "complete", "graphIndex", "graph", "revTooltip"];
144   template_ids.forEach(function(id) {
145     var source = $("#" + id).html();
146     templates[id] = Handlebars.compile(source);
147   });
148
149   var partials_ids =  ["nav", "summary-icons", "summary-list", "nothing"];
150   partials_ids.forEach(function(id) {
151     var source = $("#" + id).html();
152     Handlebars.registerPartial(id, source);
153   });
154
155 });
156
157 Handlebars.registerHelper('revisionLink', function(hash) {
158   if (!hash) { return "#"; }
159   return "#" + routes.revision.url(hash);
160 });
161 Handlebars.registerHelper('compareLink', function(hash1,hash2) {
162   if (!hash1) { return "#"; }
163   if (!hash2) { return "#"; }
164   return "#" + routes.compare.url(hash1,hash2);
165 });
166 Handlebars.registerHelper('graphLink', function(benchName, hl1, hl2) {
167     hls = [];
168     if (hl1) {hls.push(hl1)};
169     if (hl2) {hls.push(hl2)};
170     return "#" + routes.graph.url(benchName,hls);
171 });
172 Handlebars.registerHelper('diffLink', function(rev1, rev2) {
173   return data.settings.cgitLink + "/commitdiff/" + rev2
174 });
175 Handlebars.registerHelper('logLink', function(rev, options) {
176   if (data.settings.logLink) {
177     var link = Handlebars.compile(data.settings.logLink)({rev: rev});
178     $.extend(this,{link:link});
179     return options.fn(this);
180   }
181 });
182 Handlebars.registerHelper('indexLink', function() {
183   return "#" + routes.index.url();
184 });
185 Handlebars.registerHelper('allLink', function() {
186   return "#" + routes.complete.url();
187 });
188 Handlebars.registerHelper('graphIndexLink', function() {
189   return "#" + routes.graphIndex.url();
190 });
191 Handlebars.registerHelper('recentCommits', function(revisions) {
192   return commitsFrom(revisions, data.latest, data.settings.limitRecent);
193 });
194 Handlebars.registerHelper('allCommits', function(revisions) {
195   return commitsFrom(revisions, data.latest, -1);
196 });
197 function shortRev(rev) {
198   if (!rev) { return ''; }
199   return rev.substr(0,7);
200 }
201 Handlebars.registerHelper('shortRev', shortRev);
202 Handlebars.registerHelper('id', function (text) {
203     lines = text.split(/\r?\n/);
204     return new Handlebars.SafeString(lines.map(Handlebars.escapeExpression).join('
'))
205 });
206 Handlebars.registerHelper('iso8601', function(timestamp) {
207   if (!timestamp) { return '' };
208   return new Date(timestamp*1000).toISOString();
209 });
210 Handlebars.registerHelper('humanDate', function(timestamp) {
211   return new Date(timestamp*1000).toString();
212 });
213
214 // We cache everything
215 var jsonSeen = {};
216 var jsonFetching = {};
217 function getJSON(url, callback, options) {
218     var opts = {
219         block: true,
220     };
221     $.extend(opts, options);
222     if (jsonSeen[url]) {
223         console.log("Not fetching "+url+" again.");
224         if (callback) callback();
225     } else if (jsonFetching[url]) {
226         console.log("Already fetching "+url+".");
227         if (callback) jsonFetching[url].push(callback);
228     } else {
229         console.log("Fetching "+url+".");
230         jsonFetching[url] = [];
231         if (callback) jsonFetching[url].push(callback);
232         $.ajax(url, {
233             success: function (newdata) {
234                 console.log("Fetched "+url+".");
235                 jsonSeen[url] = true;
236                 $.extend(true, data, newdata);
237                 dataChanged.dispatch();
238                 $.each(jsonFetching[url], function (i,c) {c()});
239                 delete jsonFetching[url];
240             },
241             error: function (e) {
242                 console.log("Failure fetching "+url,e);
243             },
244             cache: false,
245             dataType: 'json',
246         });
247     }
248 }
249
250
251 // Views
252
253 function groupStats (benchResults) {
254   return {
255     totalCount: benchResults.length,
256     improvementCount: benchResults.filter(function (br) { return br.changeType == 'Improvement' }).length,
257     regressionCount:  benchResults.filter(function (br) { return br.changeType == 'Regression' }).length,
258   }
259 }
260
261 function benchmark_name_matches(pattern, name) {
262     if (pattern[pattern.length-1] == "*") {
263         return pattern.substr(0, pattern.length-1) == name.substr(0, pattern.length-1);
264     } else {
265         return pattern == name;
266     }
267 }
268
269 // The following logic should be kept in sync with BenchmarkSettings.hs
270 function setting_for(name) {
271     var benchSettings = {
272         smallerIsBetter: true,
273         unit: "",
274         type: "integral",
275         group: "",
276         threshold: 3,
277         important: true
278     };
279
280     data.settings.benchmarks.map(function (s) {
281         if (benchmark_name_matches(s.match, name)) {
282             $.extend(benchSettings, s);
283         }
284     })
285
286     return benchSettings;
287 }
288
289
290 // The following logic should be kept in sync with toResult in ReportTypes.hs
291 function compareResults (res1, res2) {
292     if (!res1 && !res2) { return };
293
294     name = res1? res1.name : res2.name;
295     s = setting_for(name);
296
297     res = {
298         name: name,
299         previous:    res1 ? res1.value : null,
300         value:       res2 ? res2.value : null,
301         unit:        s.unit,
302         important:   s.important,
303         changeType: "Boring",
304         change: "foobar",
305     };
306
307     if (res1 && res2){
308         if (s.type == "integral" || s.type == "float"){
309             if (res1.value == 0 && res2.value == 0) {
310                 res.change = "=";
311             } else if (res1.value == 0) {
312                 res.change = "+  ∞";
313                 res.changeType = "Improvement";
314             } else {
315                 var perc = 100.0 * (res2.value - res1.value) / res1.value;
316                 var percS = Math.round (perc * 100.0) / 100;
317                 if (Math.abs(perc) < 0.01) {
318                     res.change = "=";
319                 } else if (perc >= 0) {
320                     res.change = "+ " + percS + "%";
321                 } else {
322                     res.change = "- " + (-percS) + "%";
323                 }
324
325                 if (Math.abs(perc) >= s.threshold) {
326                     if (perc >= 0) {
327                         s.changeType = "Improvement";
328                     } else {
329                         s.changeType = "Regression";
330                     }
331                 }
332             }
333         } else if (s.type == "small integral") {
334             if (res1.value == res2.value) {
335                 res.change = "=";
336             } else if (res2.value > res1.value) {
337                 res.change = "+" + (res2.value - res1.value);
338                 res.changeType = "Improvement";
339             } else if (res1.value > res2.value) {
340                 res.change = "-" + (res1.value - res2.value);
341                 res.changeType = "Regression";
342             }
343         }
344
345         if (s.smallerIsBetter) {
346             if (res.changeType == "Improvement") {
347                 res.changeType = "Regression";
348             } else if (res.changeType == "Regression") {
349                 res.changeType = "Improvement";
350             }
351         }
352     }
353
354     return res;
355 }
356
357 // Some views require the data to be prepared in ways that are 
358 // too complicated for the template, so lets do it here.
359 dataViewPrepare = {
360   'revision': function (data, viewData) {
361     if (!data.benchGroups || !data.revisions) return {};
362     var hash = viewData.hash;
363     var rev = data.revisions[hash];
364     if (!rev) return {};
365     if (!rev.benchResults) return {};
366
367     var groups = data.benchGroups.map(function (group) {
368       var benchmarks = group.groupMembers.map(function (bn) {
369         return rev.benchResults[bn]
370       }).filter(function (br) {return br});
371       return {
372         groupName: group.groupName,
373         benchResults: benchmarks,
374         groupStats: groupStats(benchmarks),
375       };
376     });
377     return {
378       rev : rev,
379       groups : groups,
380     };
381   },
382   'compare': function (data, viewData) {
383     if (!data.benchGroups || !data.revisions) return {};
384     var hash1 = viewData.hash1;
385     var hash2 = viewData.hash2;
386     var rev1 = data.revisions[hash1];
387     var rev2 = data.revisions[hash2];
388     if (!rev1) return {};
389     if (!rev1.benchResults) return {};
390     if (!rev2) return {};
391     if (!rev2.benchResults) return {};
392
393     var groups = data.benchGroups.map(function (group) {
394       var benchmarks = group.groupMembers.map(function (bn) {
395         var r1 = rev1.benchResults[bn];
396         var r2 = rev2.benchResults[bn];
397         return compareResults(r1,r2);
398       }).filter(function (br) {return br});
399       return {
400         groupName: group.groupName,
401         benchResults: benchmarks,
402         groupStats: groupStats(benchmarks),
403       };
404     });
405     return {
406       rev1 : rev1,
407       rev2 : rev2,
408       groups : groups,
409     };
410   },
411 }
412
413 function load_template () {
414     console.log('Rebuilding page');
415     var context = {};
416     $.extend(context, data, viewData, settings);
417     if (dataViewPrepare[view]){
418       $.extend(context, dataViewPrepare[view](data, viewData));
419     }
420     $('#main').html(templates[view](context));
421
422     $(".nav-loading").toggle(jQuery.active > 0);
423
424     updateBenchFilter();
425     updateCollapsedGroups();
426     $('abbrv.timeago').timeago();
427
428     if ($('#benchChart').length) {
429         setupChart();
430     }
431 }
432 viewChanged.add(load_template);
433 dataChanged.add(load_template);
434
435 function setupChart () {
436     
437     var commits = commitsFrom(data.revisions, data.latest, data.settings.limitRecent);
438     var benchName = viewData.benchName;
439
440     $("<div id='tooltip' class='panel alert-info'></div>").css({
441                 position: "absolute",
442                 display: "none",
443                 //border: "1px solid #fdd",
444                 padding: "2px",
445                 //"background-color": "#fee",
446                 // opacity: 0.80,
447                 width: '300px',
448         }).appendTo("#main");
449
450     var plot_series = {
451           lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" },
452           points: { show: true, fill: false },
453           label: benchName,
454           data: commits.map(function (x,i){
455             if (!x.benchResults) return;
456             if (!x.benchResults[benchName]) return;
457             return [commits.length - i, x.benchResults[benchName].value]
458           }),
459         };
460
461     var plot_options = {
462           legend: {
463                 position: 'nw',
464           },
465           grid: {
466                 hoverable: true,
467                 clickable: true,
468           },
469           yaxis: {},
470           xaxis: {
471                 // ticks: values.map(function (x,i){return [i,x[0]]}),
472                 tickFormatter: function (i,axis) {
473                         if (i > 0 && i <= commits.length) {
474                                 var rev = commits[commits.length - i];
475                                 if (!rev) return "";
476                                 var date = new Date(rev.summary.gitDate * 1000);
477                                 return $.timeago(date);
478                         } else {
479                                 return '';
480                         }
481                 }
482           }
483         };
484
485     var numberType;
486     if (data.benchmarkSettings && data.benchmarkSettings[benchName]) {
487         numberType = data.benchmarkSettings[benchName].numberType;
488     }
489     if (numberType == "integral" || numberType == "small integral") {
490         plot_options.yaxis.minTickSize = 1;
491         plot_options.yaxis.tickDecimals = 0;
492     }
493
494     var plot = $.plot("#benchChart", [plot_series], plot_options);
495
496     viewData.highlights.forEach(function (hash) {
497         commits.forEach(function (rev,i) {
498             if (rev.summary.hash == hash)  {
499                 plot.highlight(0, i);
500             };
501         });
502     });
503
504
505     $("#benchChart").bind("plothover", function (event, pos, item) {
506         if (item) {
507                 var v = item.datapoint[1];
508                 var i = item.dataIndex;
509                 var rev = commits[i].summary.hash;
510                 var summary = commits[i].summary;
511
512                 var tooltipContext = $.extend({value: v}, summary);
513
514                 if ($("#tooltip").data('rev') != rev) {
515                     $("#tooltip")
516                         .html(shortRev(rev))
517                         .data('rev',rev)
518                         .html(templates.revTooltip(tooltipContext))
519                         .fadeIn(200)
520                         .css({top: item.pageY+10, left: item.pageX-100})
521                         .show();
522                     $("#benchChart").css('cursor','pointer');
523                 }
524         } else {
525                 $("#tooltip").data('rev',null).hide();
526                 $("#benchChart").css('cursor','default');
527         }
528     });
529     $("#benchChart").bind("plotclick", function (event, pos, item) {
530         if (item) {
531                 var v = item.datapoint[1];
532                 var i = item.dataIndex;
533                 var hash = commits[i].summary.hash;
534                 
535                 goTo(routes.revision.url(hash));
536         }
537     });
538 }
539
540 function updateCollapsedGroups() {
541     // Does not work yet...
542     /*
543
544     var list = settings.collapsedGroups;
545     console.log(list);
546     if ($(".panel-collapse").length) {
547         $(".panel-collapse").each(function (i){
548             if (i < list.length) {
549                 console.log(i, list[i], this);
550                 $(this).toggleClass('in', !list[i]);
551
552                 $(this).collapse();
553                 if (list[i]) {
554                     $(this).collapse('hide');
555                 } else {
556                     $(this).collapse('show');
557                 }
558             }
559         });
560     }
561     */
562 }
563
564 function updateBenchFilter() {
565     var showRegressions  = settings.benchFilter.regressions;
566     var showBoring       = settings.benchFilter.boring;
567     var showImprovements = settings.benchFilter.improvements;
568
569     $('#show-regressions').toggleClass('active', showRegressions);
570     $('#show-boring').toggleClass('active', showBoring); 
571     $('#show-improvements').toggleClass('active', showImprovements);
572
573     $('tr.row-Regression').toggle(showRegressions);
574     $('tr.row-Boring').toggle(showBoring);
575     $('tr.row-Improvement').toggle(showImprovements);
576
577     $('.bench-panel').show().each(function() {
578         if ($(this).has('tr.row-result:visible').length == 0) {
579             $(this).hide();
580         }
581     });
582
583     $('tr.summary-row').addClass('summary-row-collapsed');
584     if (showBoring) {
585         $('tr.summary-row:not(.summary-improvement):not(.summary-regression)')
586             .removeClass('summary-row-collapsed');
587     }
588     if (showRegressions) {
589         $('tr.summary-row.summary-regression')
590             .removeClass('summary-row-collapsed');
591     }
592     if (showImprovements) {
593         $('tr.summary-row.summary-improvement')
594             .removeClass('summary-row-collapsed');
595     }
596     // Always show first entry in the history
597     $('.summary-table tr.summary-row').first()
598             .removeClass('summary-row-collapsed');
599
600     $('.graph-list-panel').show().each(function() {
601         if ($(this).has('tr.summary-row:visible').length == 0) {
602             $(this).hide();
603         }
604     });
605
606     updateNothingToSee();
607 };
608
609 function updateNothingToSee() {
610     $('.nothing-to-see')
611         .toggle(jQuery.active == 0 && $('.bench-panel:visible, tr.summary-row:not(.summary-row-collapsed)').length == 0);
612 }
613
614 $(function (){
615     $('body').on('click', '.benchSelector', function (event) {
616         $(this).toggleClass('active');
617         settings.benchFilter = {
618             regressions:  $('#show-regressions').hasClass('active'),
619             boring:       $('#show-boring').hasClass('active'), 
620             improvements: $('#show-improvements').hasClass('active'), 
621         };
622         updateBenchFilter();
623     });
624 });
625
626 // Redirection
627
628 function goTo(path) {
629     console.log("goTo " + path);
630     hasher.setHash(path);
631 }
632
633
634 // Main setup
635
636 $(function() {
637     hasher.prependHash = '';
638
639     $('#loading').hide();
640
641     $(document).ajaxStart(function () {
642         $(".nav-loading").show();
643         updateNothingToSee();
644     });
645     $(document).ajaxStop(function () {
646         $(".nav-loading").hide();
647         updateNothingToSee();
648     });
649
650     // Load settins, then figure out what view to use.
651     $.get("out/latest.txt", function (latest) {
652         data.latest = latest;
653
654         getJSON("out/settings.json", function (settings) {
655             $('title').html(data.settings.title + " – Gipeda");
656             hasher.changed.add(handleHashChange);
657             hasher.initialized.add(handleHashChange);
658             hasher.init();
659         });
660     }, 'text');
661
662
663 });