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