1
0
forked from 0ad/0ad
0ad/source/tools/profiler2/profiler2.js
wraitii 3cbe96d24c Revamp Profiler2 to make it more usable.
Features include new graphs to compare runtime of functions and runtimes
across reports, as well as new profiling functions that only profile
spikes.

This was SVN commit r18423.
2016-06-22 13:38:05 +00:00

497 lines
17 KiB
JavaScript

// Copyright (c) 2016 Wildfire Games
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// This file is the main handler, which deals with loading reports and showing the analysis graphs
// the latter could probably be put in a separate module
// global array of Profiler2Report objects
var g_reports = [];
var g_main_thread = 0;
var g_current_report = 0;
var g_profile_path = null;
var g_active_elements = [];
var g_loading_timeout = null;
function save_as_file()
{
$.ajax({
url: 'http://127.0.0.1:8000/download',
success: function () {
},
error: function (jqXHR, textStatus, errorThrown) {
}
});
}
function get_history_data(report, thread, type)
{
var ret = {"time_by_frame":[], "max" : 0, "log_scale" : null};
var report_data = g_reports[report].data().threads[thread];
var interval_data = report_data.intervals;
let data = report_data.intervals_by_type_frame[type];
if (!data)
return ret;
let max = 0;
let avg = [];
let current_frame = 0;
for (let i = 0; i < data.length; i++)
{
ret.time_by_frame.push(0);
for (let p = 0; p < data[i].length; p++)
ret.time_by_frame[ret.time_by_frame.length-1] += interval_data[data[i][p]].duration;
}
// somehow JS sorts 0.03 lower than 3e-7 otherwise
let sorted = ret.time_by_frame.slice(0).sort((a,b) => a-b);
ret.max = sorted[sorted.length-1];
avg = sorted[Math.round(avg.length/2)];
if (ret.max > avg * 3)
ret.log_scale = true;
return ret;
}
function draw_frequency_graph()
{
let canvas = document.getElementById("canvas_frequency");
canvas._tooltips = [];
let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
let legend = document.getElementById("frequency_graph").querySelector("aside");
legend.innerHTML = "";
if (!g_active_elements.length)
return;
var series_data = {};
var use_log_scale = null;
var x_scale = 0;
var y_scale = 0;
var padding = 10;
var item_nb = 0;
var tooltip_helper = {};
for (let typeI in g_active_elements)
{
for (let rep in g_reports)
{
item_nb++;
let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]);
let name = rep + "/" + g_active_elements[typeI];
series_data[name] = data.time_by_frame.filter(a=>a).sort((a,b) => a-b);
if (series_data[name].length > x_scale)
x_scale = series_data[name].length;
if (data.max > y_scale)
y_scale = data.max;
if (use_log_scale === null && data.log_scale)
use_log_scale = true;
}
}
if (use_log_scale)
{
let legend_item = document.createElement("p");
legend_item.style.borderColor = "transparent";
legend_item.textContent = " -- log x scale -- ";
legend.appendChild(legend_item);
}
let id = 0;
for (let type in series_data)
{
let colour = graph_colour(id);
let time_by_frame = series_data[type];
let p = 0;
let last_val = 0;
let nb = document.createElement("p");
nb.style.borderColor = colour;
nb.textContent = type + " - n=" + time_by_frame.length;
legend.appendChild(nb);
for (var i = 0; i < time_by_frame.length; i++)
{
let x0 = i/time_by_frame.length*(canvas.width-padding*2) + padding;
if (i == 0)
x0 = 0;
let x1 = (i+1)/time_by_frame.length*(canvas.width-padding*2) + padding;
if (i == time_by_frame.length-1)
x1 = (time_by_frame.length-1)*canvas.width;
let y = time_by_frame[i]/y_scale;
if (use_log_scale)
y = Math.log10(1 + time_by_frame[i]/y_scale * 9);
context.globalCompositeOperation = "lighter";
context.beginPath();
context.strokeStyle = colour
context.lineWidth = 0.5;
context.moveTo(x0,canvas.height * (1 - last_val));
context.lineTo(x1,canvas.height * (1 - y));
context.stroke();
last_val = y;
if (!tooltip_helper[Math.floor(x0)])
tooltip_helper[Math.floor(x0)] = [];
tooltip_helper[Math.floor(x0)].push([y, type]);
}
id++;
}
for (let i in tooltip_helper)
{
let tooltips = tooltip_helper[i];
let text = "";
for (let j in tooltips)
if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1)
text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "<br>";
canvas._tooltips.push({
'x0': +i, 'x1': +i+1,
'y0': 0, 'y1': canvas.height,
'text': function(text) { return function() { return text; } }(text)
});
}
set_tooltip_handlers(canvas);
[0.02,0.05,0.1,0.25,0.5,0.75].forEach(function(y_val)
{
let y = y_val;
if (use_log_scale)
y = Math.log10(1 + y_val * 9);
context.beginPath();
context.lineWidth="1";
context.strokeStyle = "rgba(0,0,0,0.2)";
context.moveTo(0,canvas.height * (1- y));
context.lineTo(canvas.width,canvas.height * (1 - y));
context.stroke();
context.fillStyle = "gray";
context.font = "10px Arial";
context.textAlign="left";
context.fillText(time_label(y*y_scale,0), 2, canvas.height * (1 - y) - 2 );
});
}
function draw_history_graph()
{
let canvas = document.getElementById("canvas_history");
canvas._tooltips = [];
let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
let legend = document.getElementById("history_graph").querySelector("aside");
legend.innerHTML = "";
if (!g_active_elements.length)
return;
var series_data = {};
var use_log_scale = null;
var frames_nb = Infinity;
var x_scale = 0;
var y_scale = 0;
var item_nb = 0;
var tooltip_helper = {};
for (let typeI in g_active_elements)
{
for (let rep in g_reports)
{
if (g_reports[rep].data().threads[g_main_thread].frames.length < frames_nb)
frames_nb = g_reports[rep].data().threads[g_main_thread].frames.length;
item_nb++;
let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]);
series_data[rep + "/" + g_active_elements[typeI]] = smooth_1D_array(data.time_by_frame, 3);
if (data.max > y_scale)
y_scale = data.max;
if (use_log_scale === null && data.log_scale)
use_log_scale = true;
}
}
if (use_log_scale)
{
let legend_item = document.createElement("p");
legend_item.style.borderColor = "transparent";
legend_item.textContent = " -- log y scale -- ";
legend.appendChild(legend_item);
}
canvas.width = Math.max(frames_nb,600);
x_scale = frames_nb / canvas.width;
let id = 0;
for (let type in series_data)
{
let colour = graph_colour(id);
let legend_item = document.createElement("p");
legend_item.style.borderColor = colour;
legend_item.textContent = type;
legend.appendChild(legend_item);
let time_by_frame = series_data[type];
let last_val = 0;
for (var i = 0; i < frames_nb; i++)
{
let smoothed_time = time_by_frame[i];//smooth_1D(time_by_frame.slice(0), i, 3);
let y = smoothed_time/y_scale;
if (use_log_scale)
y = Math.log10(1 + smoothed_time/y_scale * 9);
if (item_nb === 1)
{
context.beginPath();
context.fillStyle = colour;
context.fillRect(i/x_scale,canvas.height,1/x_scale,-y*canvas.height);
}
else
{
if ( i == frames_nb-1)
continue;
context.globalCompositeOperation = "lighten";
context.beginPath();
context.strokeStyle = colour
context.lineWidth = 0.5;
context.moveTo(i/x_scale,canvas.height * (1 - last_val));
context.lineTo((i+1)/x_scale,canvas.height * (1 - y));
context.stroke();
}
last_val = y;
if (!tooltip_helper[Math.floor(i/x_scale)])
tooltip_helper[Math.floor(i/x_scale)] = [];
tooltip_helper[Math.floor(i/x_scale)].push([y, type]);
}
id++;
}
for (let i in tooltip_helper)
{
let tooltips = tooltip_helper[i];
let text = "Frame " + i*x_scale + "<br>";
for (let j in tooltips)
if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1)
text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "<br>";
canvas._tooltips.push({
'x0': +i, 'x1': +i+1,
'y0': 0, 'y1': canvas.height,
'text': function(text) { return function() { return text; } }(text)
});
}
set_tooltip_handlers(canvas);
[0.1,0.25,0.5,0.75].forEach(function(y_val)
{
let y = y_val;
if (use_log_scale)
y = Math.log10(1 + y_val * 9);
context.beginPath();
context.lineWidth="1";
context.strokeStyle = "rgba(0,0,0,0.2)";
context.moveTo(0,canvas.height * (1- y));
context.lineTo(canvas.width,canvas.height * (1 - y));
context.stroke();
context.fillStyle = "gray";
context.font = "10px Arial";
context.textAlign="left";
context.fillText(time_label(y*y_scale,0), 2, canvas.height * (1 - y) - 2 );
});
}
function compare_reports()
{
let section = document.getElementById("comparison");
section.innerHTML = "<h3>Report Comparison</h3>";
if (g_reports.length < 2)
{
section.innerHTML += "<p>Too few reports loaded</p>";
return;
}
if (g_active_elements.length != 1)
{
section.innerHTML += "<p>Too many of too few elements selected</p>";
return;
}
let frames_nb = g_reports[0].data().threads[g_main_thread].frames.length;
for (let rep in g_reports)
if (g_reports[rep].data().threads[g_main_thread].frames.length < frames_nb)
frames_nb = g_reports[rep].data().threads[g_main_thread].frames.length;
if (frames_nb != g_reports[0].data().threads[g_main_thread].frames.length)
section.innerHTML += "<p>Only the first " + frames_nb + " frames will be considered.</p>";
let reports_data = [];
for (let rep in g_reports)
{
let raw_data = get_history_data(rep, g_main_thread, g_active_elements[0]).time_by_frame;
reports_data.push({"time_data" : raw_data.slice(0,frames_nb), "sorted_data" : raw_data.slice(0,frames_nb).sort((a,b) => a-b)});
}
let table_output = "<table><tr><th>Profiler Variable</th><th>Median</th><th>Maximum</th><th>% better frames</th><th>time difference per frame</th></tr>"
for (let rep in reports_data)
{
let report = reports_data[rep];
table_output += "<tr><td>Report " + rep + (rep == 0 ? " (reference)":"") + "</td>";
// median
table_output += "<td>" + time_label(report.sorted_data[Math.floor(report.sorted_data.length/2)]) + "</td>"
// max
table_output += "<td>" + time_label(report.sorted_data[report.sorted_data.length-1]) + "</td>"
let frames_better = 0;
let frames_diff = 0;
for (let f in report.time_data)
{
if (report.time_data[f] <= reports_data[0].time_data[f])
frames_better++;
frames_diff += report.time_data[f] - reports_data[0].time_data[f];
}
table_output += "<td>" + (frames_better/frames_nb*100).toFixed(0) + "%</td><td>" + time_label((frames_diff/frames_nb),2) + "</td>";
table_output += "</tr>";
}
section.innerHTML += table_output + "</table>";
}
function recompute_choices(report, thread)
{
var choices = document.getElementById("choices").querySelector("nav");
choices.innerHTML = "<h3>Choices</h3>";
var types = {};
var data = report.data().threads[thread];
for (let i = 0; i < data.intervals.length; i++)
types[data.intervals[i].id] = 0;
var sorted_keys = Object.keys(types).sort();
for (let key in sorted_keys)
{
let type = sorted_keys[key];
let p = document.createElement("p");
p.textContent = type;
if (g_active_elements.indexOf(p.textContent) !== -1)
p.className = "active";
choices.appendChild(p);
p.onclick = function()
{
if (g_active_elements.indexOf(p.textContent) !== -1)
{
p.className = "";
g_active_elements = g_active_elements.filter( x => x != p.textContent);
update_analysis();
return;
}
g_active_elements.push(p.textContent);
p.className = "active";
update_analysis();
}
}
update_analysis();
}
function update_analysis()
{
compare_reports();
draw_history_graph();
draw_frequency_graph();
}
function load_report_from_file(evt)
{
var file = evt.target.files[0];
if (!file)
return;
load_report(false, file);
evt.target.value = null;
}
function load_report(trylive, file)
{
if (g_loading_timeout != undefined)
return;
let reportID = g_reports.length;
let nav = document.querySelector("header nav");
let newRep = document.createElement("p");
newRep.textContent = file.name;
newRep.className = "loading";
newRep.id = "Report" + reportID;
newRep.dataset.id = reportID;
nav.appendChild(newRep);
g_reports.push(Profiler2Report(on_report_loaded, trylive, file));
g_loading_timeout = setTimeout(function() { on_report_loaded(false); }, 5000);
}
function on_report_loaded(success)
{
let element = document.getElementById("Report" + (g_reports.length-1));
let report = g_reports[g_reports.length-1];
if (!success)
{
element.className = "fail";
setTimeout(function() { element.parentNode.removeChild(element); clearTimeout(g_loading_timeout); g_loading_timeout = null; }, 1000 );
g_reports = g_reports.slice(0,-1);
if (g_reports.length === 0)
g_current_report = null;
return;
}
clearTimeout(g_loading_timeout);
g_loading_timeout = null;
select_report(+element.dataset.id);
element.onclick = function() { select_report(+element.dataset.id);};
}
function select_report(id)
{
if (g_current_report != undefined)
document.getElementById("Report" + g_current_report).className = "";
document.getElementById("Report" + id).className = "active";
g_current_report = id;
// Load up our canvas
g_report_draw.rebuild_canvases(g_reports[id].raw_data());
g_report_draw.update_display(g_reports[id].data(),{"seconds":5});
recompute_choices(g_reports[id], g_main_thread);
}
window.onload = function()
{
// Try loading the report live
load_report(true, {"name":"live"});
// add new reports
document.getElementById('report_load_input').addEventListener('change', load_report_from_file, false);
}