blob: b2fffbd96d1cfb64d7cd13633c948d8782dcde4a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var htrace = htrace || {};
htrace.SearchResultsView = Backbone.View.extend({
// The minimum time span we will allow between begin and end.
MINIMUM_TIME_SPAN: 100,
begin: 0,
end: this.MINIMUM_TIME_SPAN,
processNameFraction: 0.2,
initialize: function(options) {
this.searchResults = options.searchResults;
this.el = options.el;
var view = this;
// Re-render the canvas when the window size changes.
// Add a debouncer delay to avoid spamming render requests.
$(window).on("resize", _.debounce(function() {
view.render();
}, 250));
},
// Get the canvas X coordinate of a mouse click from the absolute event
// coordinate.
getCanvasX: function(e) {
return e.pageX - $("#resultsCanvas").offset().left;
},
// Get the canvas Y coordinate of a mouse click from the absolute event
// coordinate.
getCanvasY: function(e) {
return e.pageY - $("#resultsCanvas").offset().top;
},
handleMouseDown: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseDown",
x: this.getCanvasX(e),
y: this.getCanvasY(e),
raw: e
});
this.draw();
},
handleMouseUp: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseUp",
x: this.getCanvasX(e),
y: this.getCanvasY(e),
raw: e
});
this.draw();
},
handleMouseOut: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseOut"
});
this.draw();
},
handleMouseMove: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseMove",
x: this.getCanvasX(e),
y: this.getCanvasY(e),
raw: e
});
this.draw();
},
handleDblclick: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "dblclick",
x: this.getCanvasX(e),
y: this.getCanvasY(e),
raw: e
});
this.draw();
},
render: function() {
console.log("SearchResultsView#render.");
$(this.el).html(_.template($("#search-results-view-template").html()));
$('#selectedTime').attr('readonly', 'readonly');
this.canvas = $("#resultsCanvas");
this.ctx = this.canvas.get(0).getContext("2d");
this.setupCoordinates();
this.setupWidgets();
this.scaleCanvas();
this.draw();
this.attachEvents();
return this;
},
/*
* Compute the ratio to use between the size of the canvas (i.e.
* canvas.ctx.width, canvas.ctx.height) and the size in "HTML5 pixels." Note
* that 'HTML5 pixels" don't actually correspond to screen pixels. A line 1
* "HTML5 pixel" wide actually takes up multiple scren pixels, etc.
*
* TODO: fix this to be sharper
*/
computeScaleFactor: function() {
var backingStoreRatio = this.ctx.backingStorePixelRatio ||
this.ctx.mozBackingStorePixelRatio ||
this.ctx.msBackingStorePixelRatio ||
this.ctx.webkitBackingStorePixelRatio ||
this.ctx.oBackingStorePixelRatio ||
this.ctx.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStoreRatio;
},
// Sets up the canvas size and scaling.
scaleCanvas: function() {
var ratio = this.computeScaleFactor();
//console.log("scaleCanvas: cssX=" + cssX + ", cssY=" + cssY + ", ratio=" + ratio);
$('#searchView').css('height', this.canvasY + "px");
$('#resultsView').css('height', this.canvasY + "px");
$('#resultsCanvas').css('height', this.canvasY + "px");
this.ctx.canvas.width = this.viewX * ratio;
this.ctx.canvas.height = this.canvasY * ratio;
this.ctx.scale(ratio, ratio);
},
//
// Set up the screen coordinates.
//
// 0 xB xD xS viewX
// +--------------+----------+--------------------+-----------+
// |TracerId | Buttons | Span Description | Scrollbar |
// +--------------+----------+--------------------+-----------+
//
setupCoordinates: function() {
this.viewX = this.canvas.parent().innerWidth();
this.viewY = $(window).innerHeight() - $("#header").innerHeight() - 50;
this.xB = Math.floor(this.viewX * this.processNameFraction);
this.xD = this.xB + Math.min(75, Math.floor(this.viewX / 20));
var scrollBarWidth = Math.min(50, Math.floor(this.viewX / 10));
this.xS = this.viewX - scrollBarWidth;
this.canvasY = this.viewY;
},
setupWidgets: function() {
var searchResultsView = this;
this.widgetManager = new htrace.WidgetManager({searchResultsView: this});
var partitionWidgetWidth = Math.max(5, Math.floor(this.viewX / 300));
// Create a SpanWidget for each span we know about
var spanWidgetHeight = Math.min(25, Math.floor(this.viewY / 32));
var numResults = this.searchResults.size();
var groupY = 0;
for (var i = 0; i < numResults; i++) {
var widget = new htrace.SpanGroupWidget({
manager: this.widgetManager,
ctx: this.ctx,
span: this.searchResults.at(i),
x0: 0,
xB: this.xB + partitionWidgetWidth,
xD: this.xD,
xF: this.xS,
y0: groupY,
begin: this.begin,
end: this.end,
spanWidgetHeight: spanWidgetHeight
});
groupY = widget.yF;
}
if (this.canvasY < groupY) {
this.canvasY = groupY;
}
// Create the draggable horizontal parition between process names and span
// names.
new htrace.PartitionWidget({
el: '#resultsCanvas',
manager: this.widgetManager,
ctx: this.ctx,
x0: this.xB,
xF: this.xB + partitionWidgetWidth,
xMin: Math.floor(this.viewX * 0.10),
xMax: Math.floor(this.viewX * 0.90),
y0: 0,
yF: groupY,
releaseHandler: function(x) {
searchResultsView.processNameFraction = (x / searchResultsView.viewX);
console.log("htrace#PartitionWidget setting processNameFraction to " +
searchResultsView.processNameFraction);
searchResultsView.render();
}
});
// Create the time cursor widget.
var selectedTime = this.begin;
if (this.timeCursor != null) {
selectedTime = this.timeCursor.selectedTime;
}
this.timeCursor = new htrace.TimeCursor({
manager: this.widgetManager,
selectedTime: selectedTime,
el: "#selectedTime"
});
this.timeCursor.ctx = this.ctx;
this.timeCursor.x0 = this.xD;
this.timeCursor.xF = this.xS;
this.timeCursor.y0 = 0;
this.timeCursor.yF = this.canvasY;
this.timeCursor.begin = this.begin;
this.timeCursor.end = this.end;
},
draw: function() {
if (this.checkCanvasTooSmall()) {
return;
}
// Set the background to white.
this.ctx.save();
this.ctx.fillStyle="#ffffff";
this.ctx.strokeStyle="#000000";
this.ctx.fillRect(0, 0, this.viewX, this.canvasY);
this.ctx.restore();
// Draw all the widgets.
this.widgetManager.handle({type: "draw"});
},
checkCanvasTooSmall: function() {
if ((this.viewX < 200) || (this.viewY < 200)) {
this.ctx.fillStyle="#cccccc";
this.ctx.strokeStyle="#000000";
this.ctx.fillRect(0, 0, this.viewX, this.viewY);
this.ctx.font = "24px serif";
this.ctx.fillStyle="#000000";
this.ctx.fillText("Canvas too small!", 0, 24);
return true;
}
return false;
},
attachEvents: function() {
// Use jquery to capture mouse events on the canvas.
// For some reason using backbone doesn't work for getting these events.
var view = this;
$("#resultsCanvas").off("mousedown");
$("#resultsCanvas").on("mousedown", function(e) {
$("#resultsCanvas").focus();
view.handleMouseDown(e);
});
$("#resultsCanvas").off("mouseup");
$("#resultsCanvas").on("mouseup", function(e) {
view.handleMouseUp(e);
});
$("#resultsCanvas").off("mouseout");
$("#resultsCanvas").on("mouseout", function(e) {
view.handleMouseOut(e);
});
$("#resultsCanvas").off("mousemove");
$("#resultsCanvas").on("mousemove", function(e) {
view.handleMouseMove(e);
});
$("#resultsCanvas").off("dblclick");
$("#resultsCanvas").on("dblclick", function(e) {
view.handleDblclick(e);
});
// Keyboard events. These events only fire if the canvas has focus. So if
// you press delete when entering a time in the time dialog box, this will
// not fire. Etc.
$("#resultsCanvas").off("keyup");
$("#resultsCanvas").on("keyup", function(e) {
if (e.keyCode == 46) { // delete key
view.clearHandler(false);
return false;
} else if (e.keyCode == 90) { // z key
view.zoomHandler();
return false;
} else {
return true;
}
});
$("#resultsCanvas").off("contextmenu");
$("#resultsCanvas").on("contextmenu", function(e) {
return false;
});
},
remove: function() {
$(window).off("resize");
$("#resultsCanvas").off("mousedown");
$("#resultsCanvas").off("mouseup");
$("#resultsCanvas").off("mouseout");
$("#resultsCanvas").off("mousemove");
$("#resultsCanvas").off("dblclick");
$("#resultsCanvas").off("keyup");
$("#resultsCanvas").off("contextmenu");
Backbone.View.prototype.remove.apply(this, arguments);
},
handleBeginOrEndChange: function(e, type) {
e.preventDefault();
var text = $(e.target).val().trim();
var d = null;
try {
d = htrace.parseDate(text);
} catch(err) {
$("#begin").val(htrace.dateToString(this.begin));
$("#end").val(htrace.dateToString(this.end));
htrace.showModalWarning("Timeline " + type + " Format Error",
"Please enter a valid time in the timeline " + type + " field.<p/>" +
err);
return null;
}
if (type === "begin") {
this.setTimes({begin: d.valueOf()});
} else if (type === "end") {
this.setTimes({end: d.valueOf()});
} else {
throw "invalid type for handleBeginOrEndChange: expected begin or end.";
}
this.render();
},
setTimes: function(params) {
if (params["begin"]) {
this.begin = params["begin"];
}
if (params["end"]) {
this.end = params["end"];
}
if (this.end < this.begin) {
var b = this.begin;
this.begin = this.end;
this.end = b;
}
var delta = this.end - this.begin;
if (delta < this.MINIMUM_TIME_SPAN) {
var needed = this.MINIMUM_TIME_SPAN - delta;
this.begin -= (needed / 2);
this.end += (needed / 2);
}
$("#begin").val(htrace.dateToString(this.begin));
$("#end").val(htrace.dateToString(this.end));
// caller should invoke render()
},
clearHandler: function(clearAllIfNoneSelected) {
console.log("invoking clearHandler.");
var toDelete = []
var noneSelected = true;
for (var i = 0; i < this.searchResults.length; i++) {
var resultSelected = false;
var model = this.searchResults.at(i);
htrace.treeTraverseDepthFirstPre(model,
htrace.getReifiedChildren, 0,
function(node, depth) {
if (noneSelected) {
if (node.get("selected")) {
resultSelected = true;
}
}
});
htrace.treeTraverseDepthFirstPre(model,
htrace.getReifiedParents, 0,
function(node, depth) {
if (node.get("selected")) {
resultSelected = true;
}
});
if (resultSelected) {
if (noneSelected) {
toDelete = [];
noneSelected = false;
}
toDelete.push(model);
} else if (noneSelected) {
toDelete.push(model);
}
}
if (noneSelected && (!clearAllIfNoneSelected)) {
return;
}
ids = [];
for (var i = 0; i < toDelete.length; i++) {
ids.push(toDelete[i].get("spanId"));
}
console.log("clearHandler: removing " + JSON.stringify(ids));
this.searchResults.remove(toDelete);
this.render();
},
getSelectedSpansOrAllSpans: function() {
// Get the list of selected spans.
// If there are no spans selected, we return all spans.
var ret = [];
var noneSelected = true;
this.applyToAllSpans(function(span) {
if (span.get("selected")) {
if (noneSelected) {
ret = [];
noneSelected = false;
}
ret.push(span);
} else if (noneSelected) {
ret.push(span);
}
});
return ret;
},
zoomHandler: function() {
var zoomSpans = this.getSelectedSpansOrAllSpans();
var numResults = zoomSpans.length;
if (numResults == 0) {
this.setTimes({begin:0, end:this.MINIMUM_TIME_SPAN});
this.render();
return;
}
var minStart = 4503599627370496;
var maxEnd = 0;
for (var i = 0; i < numResults; i++) {
var begin = zoomSpans[i].getEarliestBegin();
if (begin < minStart) {
minStart = begin;
}
var end = zoomSpans[i].getLatestEnd();
if (end > maxEnd) {
maxEnd = end;
}
}
this.setTimes({begin: minStart, end: maxEnd});
this.render();
},
// Apply a function to all spans
applyToAllSpans: function(cb) {
for (var i = 0; i < this.searchResults.length; i++) {
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
htrace.getReifiedChildren, 0,
function(node, depth) {
cb(node);
});
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
htrace.getReifiedParents, 0,
function(node, depth) {
if (depth > 0) {
cb(node);
}
});
}
}
});