/**
* Constructs a clusterer
*
* @classdesc
*
* A clusterer separates the executions in the input log into different groups
* based on a chosen metric. It then displays the results in the cluster tab in the
* left sidebar. The clustering mechanism is performed after the visualization has
* been drawn.
*
* The headingsToLabelsMap uses heading names as a key to get an array of corresponding
* subheadings and execution labels. The first item in the array is always the subheadings
* and the second item is always the execution labels.
*
* @todo cache cluster results
*
* @constructor
* @param {String} metric The chosen metric for clustering executions
*/
function Clusterer(metric, global) {
/** @private */
this.global = global;
/** @private */
this.metric = metric;
/** @private */
this.table = $("<td class='lines'></td>");
/** @private */
// An array of headings for clusters of executions, for example: "Same hosts as base"
this.headings = [];
/** @private */
this.headingsToLabelsMap = {};
}
/**
* This function delegates clustering to the appropriate helper function based on
* the metric that was set when the Clusterer was constructed
*
*/
Clusterer.prototype.cluster = function() {
// clear this Clusterer's arrays and the results table
$("#baseLabel, .clusterBase").hide();
this.clearResults();
// Create the clusters by calling helper functions
switch (this.metric) {
case "clusterNumProcess":
this.clusterByNumProcesses();
break;
case "clusterComparison":
this.clusterByExecComparison();
break;
}
};
/**
* This function starts by finding the minimum and maximum number of processes
* across all the views. If these values are the same, all executions are listed
* under one cluster heading. Otherwise, the midpoint between max and min is
* determined and executions with <= midpoint processes are grouped under
* one heading and ones with > midpoint processes are grouped into another
*/
Clusterer.prototype.clusterByNumProcesses = function() {
var views = this.global.getViews();
var headings = this.headings;
var numProcesses = [];
// Get the number of processes in each view and save the results in numProcesses
for (var i=0; i < views.length; i++) {
numProcesses[i] = views[i].getHosts().length;
}
var max = Math.max.apply(Math, numProcesses);
var min = Math.min.apply(Math, numProcesses);
// If all the executions have the same number of processes, they get grouped into a single cluster
if (max == min) {
headings.push("All executions have " + max + " processes:");
var labels = [];
for (var i=0; i < views.length; i++) {
labels.push(views[i].getLabel());
}
this.headingsToLabelsMap[headings[0]] = [[], [labels]];
}
// Otherwise, the midpoint is calculated and executions are sorted into two different clusters
else {
var mid = min + Math.floor((max-min)/2);
var lessThanMid = [];
var moreThanMid = [];
for (var i=0; i < numProcesses.length; i++) {
var currNumProcess = numProcesses[i]
var currLabel = views[i].getLabel();
if (currNumProcess <= mid) {
lessThanMid.push(currLabel);
} else {
moreThanMid.push(currLabel);
}
}
headings.push("Executions with " + mid + " or less processes:", "Executions with more than " + mid + " processes:");
this.headingsToLabelsMap[headings[0]] = [[], [lessThanMid]];
this.headingsToLabelsMap[headings[1]] = [[], [moreThanMid]];
}
this.drawClusterResults();
};
/**
* This function clusters executions into different groups by comparing them to a user-specified base execution.
* The two main headings are "Same hosts" and "Different hosts". The subheadings are "Same Events" and "Different Events".
*/
Clusterer.prototype.clusterByExecComparison = function() {
var context = this;
var global = this.global;
$("#baseLabel").show();
$(".clusterBase").show().find("option").not("#placeholder").remove();
// Set margin for base dropdown to zero initially
$(".clusterBase").removeClass("baseIndent fade");
global.getViews().forEach(function(view) {
var label = view.getLabel();
$(".clusterBase").append('<option value="' + label + '">' + label + '</option>');
});
$(".clusterBase").unbind().on("change", compareExecsToNewBase);
function compareExecsToNewBase() {
var base = global.getViewByLabel($(".clusterBase option:selected").val());
var noDiffExecs = [];
var sameHostsDiffEventsExecs = [];
var diffHostsSameEventsExecs = [];
var diffHostsDiffEventsExecs = [];
// Clear the table results whenever a new base is selected
context.clearResults();
var views = global.getViews();
for (var i=0; i < views.length; i++) {
var currView = views[i];
// Compare every view to the base
if (currView != base) {
var currViewLabel = currView.getLabel();
// Search for unique hosts and events in the non-base view
var uniqueHosts = [], uniqueEvents = [];
var hiddenHosts = currView.getTransformer().getHiddenHosts();
var sdt = new ShowDiffTransformation(base, uniqueHosts, hiddenHosts, uniqueEvents, false);
sdt.transform(currView.getVisualModel());
// Search for unique hosts and events in the base
var baseUniqueHosts = [], baseUniqueEvents = [];
var baseHiddenHosts = base.getTransformer().getHiddenHosts();
sdt = new ShowDiffTransformation(currView, baseUniqueHosts, baseHiddenHosts, baseUniqueEvents, false);
sdt.transform(base.getVisualModel());
if (baseUniqueHosts.length > 0 || uniqueHosts.length > 0) {
// The current view has different hosts and different events
if (baseUniqueEvents.length > 0 || uniqueEvents.length > 0) {
diffHostsDiffEventsExecs.push(currViewLabel);
}
// The current view has only different hosts
else {
diffHostsSameEventsExecs.push(currViewLabel);
}
} else {
// The current view has the same hosts but different events
if (baseUniqueEvents.length > 0 || uniqueEvents.length > 0) {
sameHostsDiffEventsExecs.push(currViewLabel);
}
// The current view has no differences
else {
noDiffExecs.push(currViewLabel);
}
}
}
}
// Determine which headings and subheadings should be drawn, map subheadings and execution labels to proper headings
if (noDiffExecs.length > 0 || sameHostsDiffEventsExecs.length > 0) {
context.headings.push("Same hosts as base:");
var sameHostsSubheadings = [];
var sameHostsExecLabels = [];
if (noDiffExecs.length > 0) {
sameHostsSubheadings.push("Same events as base:");
sameHostsExecLabels.push(noDiffExecs);
}
if (sameHostsDiffEventsExecs.length > 0) {
sameHostsSubheadings.push("Different events from base:");
sameHostsExecLabels.push(sameHostsDiffEventsExecs);
}
context.headingsToLabelsMap["Same hosts as base:"] = [sameHostsSubheadings, sameHostsExecLabels];
}
if (diffHostsSameEventsExecs.length > 0 || diffHostsDiffEventsExecs.length > 0) {
context.headings.push("Different hosts from base:");
var diffHostsSubheadings = [];
var diffHostsExecLabels = [];
if (diffHostsSameEventsExecs.length > 0) {
diffHostsSubheadings.push("Same events as base:");
diffHostsExecLabels.push(diffHostsSameEventsExecs);
}
if (diffHostsDiffEventsExecs.length > 0) {
diffHostsSubheadings.push("Different events from base:");
diffHostsExecLabels.push(diffHostsDiffEventsExecs);
}
context.headingsToLabelsMap["Different hosts from base:"] = [diffHostsSubheadings, diffHostsExecLabels];
}
context.drawClusterResults();
}
}
/**
* Sorts an array of execution labels based on the chosen metric for clustering:
* When clustering by the number of processes, the labels are ordered by increasing number of processes.
*
* @param {Array<String>} labels The execution labels to be sorted
*/
Clusterer.prototype.sortLabels = function(labels) {
var global = this.global;
switch (this.metric) {
case "clusterNumProcess":
labels.sort(function(a,b) {
var numA = global.getViewByLabel(a).getHosts().length;
var numB = global.getViewByLabel(b).getHosts().length;
return numA - numB;
});
break;
}
return labels;
}
/**
* Condenses the list of execution labels beloning to the given heading or subheading
*
* @param {jQuery.selection} heading A jQuery selection of the heading or subheading whose execution labels need to be condensed
*/
Clusterer.prototype.condenseExecLabels = function(heading) {
heading.nextAll("br.condense:first").nextUntil("br.stop:first").hide();
this.table.append("<br>", $("<a></a>").text("Show all").attr("href", heading).css("color","black"), "<br>");
}
/**
* Draws the given list of execution labels and calls condenseExecLabels() when appropriate
*
* @param {Array<String>} currLabels The execution labels to be drawn
* @param {jQuery.selection} currHeading A jQuery selection of the heading or subheading that currLabels is drawn under
*/
Clusterer.prototype.drawExecLabels = function(currLabels, currHeading) {
var table = this.table;
var global = this.global;
for (var index=0; index < currLabels.length; index++) {
var currLabel = currLabels[index];
// Create a breakpoint for condensing execution labels
if (index == 5) {
table.append($("<br class=condense>").hide());
}
// Include the number of processes beside the label when clustering by number of processes
if (this.metric == "clusterNumProcess" && this.headings.length > 1) {
var numProcess = global.getViewByLabel(currLabel).getHosts().length;
if (numProcess == 1) {
table.append($("<a></a>").text(currLabel + " - " + numProcess + " process").attr("href", currLabel), "<br>");
} else {
table.append($("<a></a>").text(currLabel + " - " + numProcess + " processes").attr("href", currLabel), "<br>");
}
} else {
table.append($("<a></a>").text(currLabel).attr("href", currLabel), "<br>");
}
}
table.append($("<br class=stop>").hide());
// Condense the list if there are more than 5 executions
if (currLabels.length > 5) {
this.condenseExecLabels(currHeading);
}
}
/**
* Draws the cluster headings and subheadings and passes the appropriate execution labels to drawExecLabels().
* Formats and binds events to results after all execution labels for all headings and subheadings have been drawn.
*/
Clusterer.prototype.drawClusterResults = function() {
var global = this.global;
var metric = this.metric;
var table = this.table;
var clusterer = this;
this.headings.forEach(function(heading) {
// Draw the cluster heading
var $currHeadingLabel = $("<p></p>").text(heading);
table.append($currHeadingLabel);
// Get the subheadings for this heading
var subheadings = clusterer.headingsToLabelsMap[heading][0];
// Get the array of executions labels for this heading
var execLabelsArray = clusterer.headingsToLabelsMap[heading][1];
var currLabels = [];
// If the subheadings array is empty, there's only one array of execution labels
if (subheadings.length == 0) {
currLabels = clusterer.sortLabels(execLabelsArray[0]);
clusterer.drawExecLabels(currLabels, $currHeadingLabel);
// Otherwise, draw the subheadings and the corresponding execution labels beneath them
} else {
for (var index=0; index < subheadings.length; index++) {
var $subheadingLabel = $("<p></p>").text(subheadings[index]).addClass("indent");
table.append($subheadingLabel);
currLabels = clusterer.sortLabels(execLabelsArray[index])
clusterer.drawExecLabels(currLabels, $subheadingLabel);
}
}
});
table.find("a").addClass("indent");
$("table.clusterResults").append(table);
$("#labelIconL, #selectIconL").show();
if (global.getPairwiseView()) {
$("#labelIconR, #selectIconR").show();
}
// For comparison clustering, by default, make the graph on the right the base execution
if (metric == "clusterComparison") {
// Shift the cluster results down to make room for the base label and dropdown
$("td.lines").addClass("shiftDown");
var baseExec = $(".clusterBase option:selected").val();
this.setView("R", baseExec);
// For other clustering options, make the graph on the right the second execution in the results
} else {
$("td.lines").removeClass("shiftDown");
var secondExec = $(".clusterResults a").first().nextAll("a:first").attr("href");
this.setView("R", secondExec);
}
// Change the left graph to be that of the first execution in the first cluster
var firstExec = $(".clusterResults a").first().attr("href");
this.setView("L", firstExec);
// Bind the click event to the cluster results
$(".clusterResults a").on("click", function(e) {
var anchorText = $(this).text();
var anchorHref = $(this).attr("href");
if (anchorText == "Show all") {
$(this).text("Condense");
$(this).prevAll("br.condense:first").nextUntil("br.stop").not("br.left, br.right").show();
} else if (anchorText == "Condense") {
$(this).text("Show all");
// Condense up to the nearest left or right arrow icon instead of all the way up
var prevCondense = $(this).prevAll("br.condense:first");
var prevLeft = $(this).prevAll("br.left");
var prevRight = $(this).prevAll("br.right");
if (prevCondense.nextUntil("br.stop", "br.left").length > 0) {
if (prevLeft.nextUntil("br.stop", "br.right").length > 0) {
prevRight.nextUntil("br.stop").hide();
} else {
prevLeft.nextUntil("br.stop").hide();
}
} else if (prevCondense.nextUntil("br.stop", "br.right").length > 0) {
prevRight.nextUntil("br.stop").hide();
} else {
prevCondense.nextUntil("br.stop").hide();
}
} else {
clusterer.setView("L", anchorHref);
}
e.preventDefault();
});
}
/**
* Sets the view in the given position to the execution with the label specified by anchorHref
*
* @param {String} position Either "L" or "R" to indicate the left or right view
* @param {String} anchorHref The label of the execution to be set
*
*/
Clusterer.prototype.setView = function(position, anchorHref) {
var global = this.global;
var leftView = global.getActiveViews()[0].getLabel();
var rightView = global.getActiveViews()[1].getLabel();
if ((position == "L" && anchorHref == rightView) || (position == "R" && global.getPairwiseView() && anchorHref == leftView)) {
global.swapViews();
} else {
// For logs with exactly two executions, there are no execution drop-downs so have to call drawClusterIcons directly
if (global.getPairwiseView() && global.getViews().length == 2) {
global.drawClusterIcons();
} else {
if (position == "L") {
$("#viewSelectL").children("option[value='" + anchorHref + "']").prop("selected", true).change();
} else {
$("#viewSelectR").children("option[value='" + anchorHref + "']").prop("selected", true).change();
}
}
}
}
/**
* Clears the results table as well as any existing headings, subheadings and executions labels
*/
Clusterer.prototype.clearResults = function() {
this.headings = [];
this.headingsToLabelsMap = {};
// clear the cluster results
$(".clusterResults td.lines").empty();
$(".clusterResults td:empty").remove();
}