packages feed

gipeda-0.1: 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]
};

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


});