packages feed

gipeda-0.2: site/js/gipeda.js

// 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],
    compare: {
	from: null,
	to: null,
    }
};

// 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', 'out/graph-summaries.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; },
        },
    compare:
        { regex: /^compare\/([a-f0-9]+)\/([a-f0-9]+)$/,
          viewData: function (match) { return { hash1: match[1], hash2: match[2] }; },
          download: function () {
            return ['out/benchNames.json',
	            'out/reports/' + viewData.hash1 + '.json',
	            'out/reports/' + viewData.hash2 + '.json'
		    ];
          },
          url: function (hash1, hash2) { return "compare/" + hash1 + "/" + hash2; },
        },
    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", "compare", "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", "rev-id", "nothing", "tags", "branches"];
  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('compareLink', function(hash1,hash2) {
  if (!hash1) { return "#"; }
  if (!hash2) { return "#"; }
  return "#" + routes.compare.url(hash1,hash2);
});
Handlebars.registerHelper('graphLink', function(benchName, hl1, hl2) {
    hls = [];
    if (hl1 && typeof(hl1) == 'string') {hls.push(hl1)};
    if (hl2 && typeof(hl2) == 'string') {hls.push(hl2)};
    return "#" + routes.graph.url(benchName,hls);
});
Handlebars.registerHelper('diffLink', function(rev1, rev2) {
    if (data.settings.diffLink) {
	return Handlebars.compile(data.settings.diffLink)({base: rev1, rev: rev2});
    } else {
	return 'javascript:alert("No diffLink defined in settings")';
    }
});
Handlebars.registerHelper('revisionInfo', function(revSummary) {
    var ctxt = {};
    ctxt.rev = revSummary.hash;
    if (revSummary.parents) {
	ctxt.base=revSummary.parents[0];
    }
    var result = Handlebars.compile(data.settings.revisionInfo)(ctxt);
    return new Handlebars.SafeString(result);
});
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('id', function (text) {
    if (text) {
	lines = text.split(/\r?\n/);
	return new Handlebars.SafeString(lines.map(Handlebars.escapeExpression).join('
'))
    }
});
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();
});
// inspired by http://stackoverflow.com/a/17935019/946226
Handlebars.registerHelper('each_naturally', function(context,options){
    var output = '';
    if (context) {
	console.log(context);
	var keys = jQuery.map(context, function(v,k) {return k});
	var sorted_keys = keys.sort(naturalSort);
	sorted_keys.map(function (k,i) {
	    output += options.fn(context[k], {data: {key: k, index: i}});
	});
    }
    return output;
});
Handlebars.registerHelper('each_unnaturally', function(context,options){
    var output = '';
    if (context) {
	console.log(context);
	var keys = jQuery.map(context, function(v,k) {return k});
        // needs https://github.com/overset/javascript-natural-sort/issues/21 fixed
	//var sorted_keys = keys.sort(naturalSort).reverse();
	var sorted_keys = keys.sort().reverse();
	sorted_keys.map(function (k,i) {
	    output += options.fn(context[k], {data: {key: k, index: i}});
	});
    }
    return output;
});

// We cache everything
var jsonSeen = {};
var jsonFetching = {};
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 if (jsonFetching[url]) {
	console.log("Already fetching "+url+".");
	if (callback) jsonFetching[url].push(callback);
    } else {
	console.log("Fetching "+url+".");
	jsonFetching[url] = [];
	if (callback) jsonFetching[url].push(callback);
        $.ajax(url, {
            success: function (newdata) {
		console.log("Fetched "+url+".");
	        jsonSeen[url] = true;
	        $.extend(true, data, newdata);
	        dataChanged.dispatch();
		$.each(jsonFetching[url], function (i,c) {c()});
		delete jsonFetching[url];
            },
            error: function (e) {
                console.log("Failure fetching "+url,e);
            },
	    cache: false,
            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,
  }
}

function benchmark_name_matches(pattern, name) {
    if (pattern[pattern.length-1] == "*") {
        return pattern.substr(0, pattern.length-1) == name.substr(0, pattern.length-1);
    } else {
        return pattern == name;
    }
}

// The following logic should be kept in sync with BenchmarkSettings.hs
function setting_for(name) {
    var benchSettings = {
        smallerIsBetter: true,
        unit: "",
        type: "integral",
        group: "",
        threshold: 3,
        important: true
    };

    data.settings.benchmarks.map(function (s) {
        if (benchmark_name_matches(s.match, name)) {
            $.extend(benchSettings, s);
        }
    })

    return benchSettings;
}


// The following logic should be kept in sync with toResult in ReportTypes.hs
function compareResults (res1, res2) {
    if (!res1 && !res2) { return };

    name = res1? res1.name : res2.name;
    s = setting_for(name);

    res = {
        name: name,
        previous:    res1 ? res1.value : null,
        value:       res2 ? res2.value : null,
        unit:        s.unit,
        important:   s.important,
        changeType: "Boring",
        change: "foobar",
    };

    if (res1 && res2){
        if (s.type == "integral" || s.type == "float"){
            if (res1.value == 0 && res2.value == 0) {
                res.change = "=";
            } else if (res1.value == 0) {
                res.change = "+  ∞";
                res.changeType = "Improvement";
            } else {
                var perc = 100.0 * (res2.value - res1.value) / res1.value;
                var percS = Math.round (perc * 100.0) / 100;
                if (Math.abs(perc) < 0.01) {
                    res.change = "=";
                } else if (perc >= 0) {
                    res.change = "+ " + percS + "%";
                } else {
                    res.change = "- " + (-percS) + "%";
                }

                if (Math.abs(perc) >= s.threshold) {
                    if (perc >= 0) {
                        res.changeType = "Improvement";
                    } else {
                        res.changeType = "Regression";
                    }
                }
            }
        } else if (s.type == "small integral") {
            if (res1.value == res2.value) {
                res.change = "=";
            } else if (res2.value > res1.value) {
                res.change = "+" + (res2.value - res1.value);
                res.changeType = "Improvement";
            } else if (res1.value > res2.value) {
                res.change = "-" + (res1.value - res2.value);
                res.changeType = "Regression";
            }
        }

        if (s.smallerIsBetter) {
            if (res.changeType == "Improvement") {
                res.changeType = "Regression";
            } else if (res.changeType == "Regression") {
                res.changeType = "Improvement";
            }
        }
    }

    return res;
}

// 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,
    };
  },
  'compare': function (data, viewData) {
    if (!data.benchGroups || !data.revisions) return {};
    var hash1 = viewData.hash1;
    var hash2 = viewData.hash2;
    var rev1 = data.revisions[hash1];
    var rev2 = data.revisions[hash2];
    if (!rev1) return {};
    if (!rev1.benchResults) return {};
    if (!rev2) return {};
    if (!rev2.benchResults) return {};

    var groups = data.benchGroups.map(function (group) {
      var benchmarks = group.groupMembers.map(function (bn) {
	var r1 = rev1.benchResults[bn];
	var r2 = rev2.benchResults[bn];
	return compareResults(r1,r2);
      }).filter(function (br) {return br});
      return {
	groupName: group.groupName,
	benchResults: benchmarks,
	groupStats: groupStats(benchmarks),
      };
    });
    return {
      rev1 : rev1,
      rev2 : rev2,
      groups : groups,
    };
  },
}

function remember_from_to() {
    settings.compare.from = $('#compare-from').data('rev');
    settings.compare.to = $('#compare-to').data('rev');
}

function recall_from_to() {
    if (settings.compare.from) {
	$('#compare-from').data('rev', settings.compare.from);
	$('#compare-from').text(shortRev(settings.compare.from));
    }
    if (settings.compare.to) {
	$('#compare-to').data('rev', settings.compare.to);
	$('#compare-to').text(shortRev(settings.compare.to));
    }
    $('#go-to-compare').attr('href',current_compare_link());
}

function current_compare_link () {
    var rev1 = settings.compare.from;
    var rev2 = settings.compare.to;
    if (rev1 && rev2) {
	return "#" + routes.compare.url(rev1, rev2);
    } else {
	return 'javascript:alert("Please drag two revisions here to compare them")';
    }
}

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();

    // Code to implement the compare-revision-drag'n'drop interface
    $('.rev-draggable').draggable({
        revert: true,
	revertDuration: 0,
    });
    $('#compare-from, #compare-to').droppable({
	accept: ".rev-draggable",
        activeClass: "ui-state-highlight",
        hoverClass: "ui-state-active",
        drop: function( event, ui ) {
          $( this ).text(ui.draggable.text());
          $( this ).data('rev',ui.draggable.data('rev'));
	  remember_from_to();
	  $('#go-to-compare').attr('href',current_compare_link());
      }
    });
    recall_from_to();

    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_series = {
	  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]
	  }),
	};

    var plot_options = {
	  legend: {
		position: 'nw',
	  },
	  grid: {
		hoverable: true,
		clickable: true,
	  },
          yaxis: {},
	  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 '';
			}
		}
	  }
	};

    var numberType;
    if (data.benchmarkSettings && data.benchmarkSettings[benchName]) {
        numberType = data.benchmarkSettings[benchName].numberType;
    }
    if (numberType == "integral" || numberType == "small integral") {
        plot_options.yaxis.minTickSize = 1;
        plot_options.yaxis.tickDecimals = 0;
    }

    var plot = $.plot("#benchChart", [plot_series], plot_options);

    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');
    }
    // Always show first entry in the history
    $('.summary-table tr.summary-row').first()
            .removeClass('summary-row-collapsed');

    $('.graph-list-panel').show().each(function() {
        if ($(this).has('tr.summary-row:visible').length == 0) {
            $(this).hide();
        }
    });

    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');


});