// TODO: this code needs a load of cleaning up and documenting, // and feature additions and general improvement and unrubbishing // Item types returned by the engine var ITEM_EVENT = 1; var ITEM_ENTER = 2; var ITEM_LEAVE = 3; var ITEM_ATTRIBUTE = 4; function hslToRgb(h, s, l, a) { var r, g, b; if (s == 0) { r = g = b = l; } else { function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return 'rgba(' + Math.floor(r * 255) + ',' + Math.floor(g * 255) + ',' + Math.floor(b * 255) + ',' + a + ')'; } function new_colour(id) { var hs = [0, 1/3, 2/3, 1/4, 2/4, 3/4, 1/5, 3/5, 2/5, 4/5]; var ss = [1, 0.5]; var ls = [0.8, 0.6, 0.9, 0.7]; return hslToRgb(hs[id % hs.length], ss[Math.floor(id / hs.length) % ss.length], ls[Math.floor(id / (hs.length*ss.length)) % ls.length], 1); } var g_used_colours = {}; var g_data; function refresh() { if (1) refresh_live(); else refresh_jsonp('../../../binaries/system/profile2.jsonp'); } function concat_events(data) { var events = []; data.events.forEach(function(ev) { ev.pop(); // remove the dummy null markers Array.prototype.push.apply(events, ev); }); return events; } function refresh_jsonp(url) { var script = document.createElement('script'); window.profileDataCB = function(data) { script.parentNode.removeChild(script); var threads = []; data.threads.forEach(function(thread) { var canvas = $(''); threads.push({'name': thread.name, 'data': { 'events': concat_events(thread.data) }, 'canvas': canvas.get(0)}); }); g_data = { 'threads': threads }; var range = {'seconds': 0.05}; rebuild_canvases(); update_display(range); }; script.src = url; document.body.appendChild(script); } function refresh_live() { $.ajax({ url: 'http://127.0.0.1:8000/overview', dataType: 'json', success: function (data) { var threads = []; data.threads.forEach(function(thread) { var canvas = $(''); threads.push({'name': thread.name, 'canvas': canvas.get(0)}); }); var callback_data = { 'threads': threads, 'completed': 0 }; threads.forEach(function(thread) { refresh_thread(thread, callback_data); }); }, error: function (jqXHR, textStatus, errorThrown) { alert('Failed to connect to server ("'+textStatus+'")'); } }); } function refresh_thread(thread, callback_data) { $.ajax({ url: 'http://127.0.0.1:8000/query', dataType: 'json', data: { 'thread': thread.name }, success: function (data) { data.events = concat_events(data); thread.data = data; if (++callback_data.completed == callback_data.threads.length) { g_data = { 'threads': callback_data.threads }; //var range = {'numframes': 5}; var range = {'seconds': 0.05}; rebuild_canvases(); update_display(range); } }, error: function (jqXHR, textStatus, errorThrown) { alert('Failed to connect to server ("'+textStatus+'")'); } }); } function rebuild_canvases() { g_data.canvas_frames = $('').get(0); g_data.canvas_zoom = $('').get(0); g_data.text_output = $('
').get(0);
    
    set_frames_zoom_handlers(g_data.canvas_frames);
    set_tooltip_handlers(g_data.canvas_frames);

    $('#timelines').empty();
    $('#timelines').append(g_data.canvas_frames);
    g_data.threads.forEach(function(thread) {
        $('#timelines').append($(thread.canvas));
    });
    $('#timelines').append(g_data.canvas_zoom);
    $('#timelines').append(g_data.text_output);
}

function update_display(range)
{
    $(g_data.text_output).empty();

    var main_events = g_data.threads[0].data.events;

    var processed_main = g_data.threads[0].processed_events = compute_intervals(main_events, range);

//    display_top_items(main_events, g_data.text_output);

    display_frames(processed_main, g_data.canvas_frames);
    display_events(processed_main, g_data.canvas_frames);

    $(g_data.threads[0].canvas).unbind();
    $(g_data.canvas_zoom).unbind();
    display_hierarchy(processed_main, processed_main, g_data.threads[0].canvas, {}, undefined);
    set_zoom_handlers(processed_main, processed_main, g_data.threads[0].canvas, g_data.canvas_zoom);
    set_tooltip_handlers(g_data.threads[0].canvas);
    set_tooltip_handlers(g_data.canvas_zoom);

    g_data.threads.slice(1).forEach(function(thread) {
        var processed_data = compute_intervals(thread.data.events, {'tmin': processed_main.tmin, 'tmax': processed_main.tmax});

        $(thread.canvas).unbind();
        display_hierarchy(processed_main, processed_data, thread.canvas, {}, undefined);
        set_zoom_handlers(processed_main, processed_data, thread.canvas, g_data.canvas_zoom);
        set_tooltip_handlers(thread.canvas);
    });
 }

function display_top_items(data, output)
{
    var items = {};
    for (var i = 0; i < data.length; ++i)
    {
        var type = data[i][0];
        if (!(type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE))
            continue;
        var id = data[i][2];
        if (!items[id])
            items[id] = { 'count': 0 };
        items[id].count++;
    }

    var topitems = [];
    for (var k in items)
        topitems.push([k, items[k].count]);
    topitems.sort(function(a, b) {
        return b[1] - a[1];
    });
    topitems.splice(16);
    
    topitems.forEach(function(item) {
        output.appendChild(document.createTextNode(item[1] + 'x -- ' + item[0] + '\n'));
    });
    output.appendChild(document.createTextNode('(' + data.length + ' items)'));
}

function compute_intervals(data, range)
{
    var start, end;
    var tmin, tmax;
    
    var frames = [];
    var last_frame_time = undefined;
    for (var i = 0; i < data.length; ++i)
    {
        if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
        {
            var t = data[i][1];
            if (last_frame_time)
                frames.push({'t0': last_frame_time, 't1': t});
            last_frame_time = t;
        }
    }
    
    if (range.numframes)
    {
        for (var i = data.length - 1; i > 0; --i)
        {
            if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
            {
                end = i;
                break;
            }
        }
        
        var framesfound = 0;
        for (var i = end - 1; i > 0; --i)
        {
            if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
            {
                start = i;
                if (++framesfound == range.numframes)
                    break;
            }
        }
        
        tmin = data[start][1];
        tmax = data[end][1];
    }
    else if (range.seconds)
    {
        var end = data.length - 1;
        for (var i = end; i > 0; --i)
        {
            var type = data[i][0];
            if (type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE)
            {
                tmax = data[i][1];
                break;
            }
        }
        tmin = tmax - range.seconds;
        
        for (var i = end; i > 0; --i)
        {
            var type = data[i][0];
            if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmin)
                break;
            start = i;
        }
    }
    else
    {
        start = 0;
        end = data.length - 1;
        tmin = range.tmin;
        tmax = range.tmax;

        for (var i = data.length-1; i > 0; --i)
        {
            var type = data[i][0];
            if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmax)
            {
                end = i;
                break;
            }
        }

        for (var i = end; i > 0; --i)
        {
            var type = data[i][0];
            if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmin)
                break;
            start = i;
        }

        // Move the start/end outwards by another frame, so we don't lose data at the edges
        while (start > 0)
        {
            --start;
            if (data[start][0] == ITEM_EVENT && data[start][2] == '__framestart')
                break;
        }
        while (end < data.length-1)
        {
            ++end;
            if (data[end][0] == ITEM_EVENT && data[end][2] == '__framestart')
                break;
        }
    }

    var num_colours = 0;
    
    var events = [];

    // Read events for the entire data period (not just start..end)
    var lastWasEvent = false;
    for (var i = 0; i < data.length; ++i)
    {
        if (data[i][0] == ITEM_EVENT)
        {
            events.push({'t': data[i][1], 'id': data[i][2]});
            lastWasEvent = true;
        }
        else if (data[i][0] == ITEM_ATTRIBUTE)
        {
            if (lastWasEvent)
            {
                if (!events[events.length-1].attrs)
                    events[events.length-1].attrs = [];
                events[events.length-1].attrs.push(data[i][1]);
            }
        }
        else
        {
            lastWasEvent = false;
        }
    }
    
    
    var intervals = [];
    
    // Read intervals from the focused data period (start..end)
    var stack = [];
    var lastT = 0;
    var lastWasEvent = false;
    for (var i = start; i <= end; ++i)
    {
        if (data[i][0] == ITEM_EVENT)
        {
//            if (data[i][1] < lastT)
//                console.log('Time went backwards: ' + (data[i][1] - lastT));

            lastT = data[i][1];
            lastWasEvent = true;
        }
        else if (data[i][0] == ITEM_ENTER)
        {
//            if (data[i][1] < lastT)
//                console.log('Time went backwards: ' + (data[i][1] - lastT));

            stack.push({'t0': data[i][1], 'id': data[i][2]});

            lastT = data[i][1];
            lastWasEvent = false;
        }
        else if (data[i][0] == ITEM_LEAVE)
        {
//            if (data[i][1] < lastT)
//                console.log('Time went backwards: ' + (data[i][1] - lastT));

            lastT = data[i][1];
            lastWasEvent = false;
            
            if (!stack.length)
                continue;
            var interval = stack.pop();

            if (data[i][2] != interval.id && data[i][2] != '(ProfileStop)')
                alert('inconsistent interval ids ('+interval.id+' / '+data[i][2]+')');

            if (!g_used_colours[interval.id])
                g_used_colours[interval.id] = new_colour(num_colours++);
            
            interval.colour = g_used_colours[interval.id];
                
            interval.t1 = data[i][1];
            interval.duration = interval.t1 - interval.t0;
            interval.depth = stack.length;
            
            intervals.push(interval);
        }
        else if (data[i][0] == ITEM_ATTRIBUTE)
        {
            if (!lastWasEvent && stack.length)
            {
                if (!stack[stack.length-1].attrs)
                    stack[stack.length-1].attrs = [];
                stack[stack.length-1].attrs.push(data[i][1]);
            }
        }
    }

    return { 'frames': frames, 'events': events, 'intervals': intervals, 'tmin': tmin, 'tmax': tmax };
}

function time_label(t)
{
    if (t > 1e-3)
        return (t * 1e3).toFixed(2) + 'ms';
    else
        return (t * 1e6).toFixed(2) + 'us';
}

function display_frames(data, canvas)
{
    canvas._tooltips = [];

    var ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();

    var xpadding = 8;
    var padding_top = 40;
    var width = canvas.width - xpadding*2;
    var height = canvas.height - padding_top - 4;

    var tmin = data.frames[0].t0;
    var tmax = data.frames[data.frames.length-1].t1;
    var dx = width / (tmax-tmin);
    
    canvas._zoomData = {
        'x_to_t': function(x) {
            return tmin + (x - xpadding) / dx;
        },
        't_to_x': function(t) {
            return (t - tmin) * dx + xpadding;
        }
    };
    
//    var y_per_second = 1000;
    var y_per_second = 100;

    [16, 33, 200, 500].forEach(function(t) {
        var y1 = canvas.height;
        var y0 = y1 - t/1000*y_per_second;
        var y = Math.floor(y0) + 0.5;

        ctx.beginPath();
        ctx.moveTo(xpadding, y);
        ctx.lineTo(canvas.width - xpadding, y);
        ctx.strokeStyle = 'rgb(255, 0, 0)';
        ctx.stroke();
        ctx.fillStyle = 'rgb(255, 0, 0)';
        ctx.fillText(t+'ms', 0, y-2);
    });

    ctx.strokeStyle = 'rgb(0, 0, 0)';
    ctx.fillStyle = 'rgb(255, 255, 255)';
    for (var i = 0; i < data.frames.length; ++i)
    {
        var frame = data.frames[i];
        
        var duration = frame.t1 - frame.t0;
        var x0 = xpadding + dx*(frame.t0 - tmin);
        var x1 = x0 + dx*duration;
        var y1 = canvas.height;
        var y0 = y1 - duration*y_per_second;
        
        ctx.beginPath();
        ctx.rect(x0, y0, x1-x0, y1-y0);
        ctx.stroke();
        
        canvas._tooltips.push({
            'x0': x0, 'x1': x1,
            'y0': y0, 'y1': y1,
            'text': function(frame, duration) { return function() {
                var t = 'Frame
'; t += 'Length: ' + time_label(duration) + '
'; if (frame.attrs) { frame.attrs.forEach(function(attr) { t += attr + '
'; }); } return t; }} (frame, duration) }); } ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)'; ctx.fillStyle = 'rgba(128, 128, 255, 0.2)'; ctx.beginPath(); ctx.rect(xpadding + dx*(data.tmin - tmin), 0, dx*(data.tmax - data.tmin), canvas.height); ctx.fill(); ctx.stroke(); ctx.restore(); } function display_events(data, canvas) { var ctx = canvas.getContext('2d'); ctx.save(); var x_to_time = canvas._zoomData.x_to_t; var time_to_x = canvas._zoomData.t_to_x; for (var i = 0; i < data.events.length; ++i) { var event = data.events[i]; if (event.id == '__framestart') continue; if (event.id == 'gui event' && event.attrs && event.attrs[0] == 'type: mousemove') continue; var x = time_to_x(event.t); var y = 32; var x0 = x; var x1 = x; var y0 = y-4; var y1 = y+4; ctx.strokeStyle = 'rgb(255, 0, 0)'; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); canvas._tooltips.push({ 'x0': x0, 'x1': x1, 'y0': y0, 'y1': y1, 'text': function(event) { return function() { var t = '' + event.id + '
'; if (event.attrs) { event.attrs.forEach(function(attr) { t += attr + '
'; }); } return t; }} (event) }); } ctx.restore(); } function display_hierarchy(main_data, data, canvas, range, zoom) { canvas._tooltips = []; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.font = '12px sans-serif'; var xpadding = 8; var padding_top = 40; var width = canvas.width - xpadding*2; var height = canvas.height - padding_top - 4; var tmin, tmax, start, end; if (range.tmin) { tmin = range.tmin; tmax = range.tmax; } else { tmin = data.tmin; tmax = data.tmax; } canvas._hierarchyData = { 'range': range, 'tmin': tmin, 'tmax': tmax }; function time_to_x(t) { return xpadding + (t - tmin) / (tmax - tmin) * width; } function x_to_time(x) { return tmin + (x - xpadding) * (tmax - tmin) / width; } ctx.save(); ctx.textAlign = 'center'; ctx.strokeStyle = 'rgb(192, 192, 192)'; ctx.beginPath(); var precision = -3; while ((tmax-tmin)*Math.pow(10, 3+precision) < 25) ++precision; var ticks_per_sec = Math.pow(10, 3+precision); var major_tick_interval = 5; for (var i = 0; i < (tmax-tmin)*ticks_per_sec; ++i) { var major = (i % major_tick_interval == 0); var x = Math.floor(time_to_x(tmin + i/ticks_per_sec)); ctx.moveTo(x-0.5, padding_top - (major ? 4 : 2)); ctx.lineTo(x-0.5, padding_top + height); if (major) ctx.fillText((i*1000/ticks_per_sec).toFixed(precision), x, padding_top - 8); } ctx.stroke(); ctx.restore(); var BAR_SPACING = 16; for (var i = 0; i < data.intervals.length; ++i) { var interval = data.intervals[i]; if (interval.tmax <= tmin || interval.tmin > tmax) continue; var label = interval.id; if (interval.attrs) { if (/^\d+$/.exec(interval.attrs[0])) label += ' ' + interval.attrs[0]; else label += ' [...]'; } var x0 = Math.floor(time_to_x(interval.t0)); var x1 = Math.floor(time_to_x(interval.t1)); var y0 = padding_top + interval.depth * BAR_SPACING; var y1 = y0 + BAR_SPACING; ctx.fillStyle = interval.colour; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.rect(x0-0.5, y0-0.5, x1-x0, y1-y0); ctx.fill(); ctx.stroke(); ctx.fillStyle = 'black'; ctx.fillText(label, x0+2, y0+BAR_SPACING-4, Math.max(1, x1-x0-4)); canvas._tooltips.push({ 'x0': x0, 'x1': x1, 'y0': y0, 'y1': y1, 'text': function(interval) { return function() { var t = '' + interval.id + '
'; t += 'Length: ' + time_label(interval.duration) + '
'; if (interval.attrs) { interval.attrs.forEach(function(attr) { t += attr + '
'; }); } return t; }} (interval) }); } for (var i = 0; i < main_data.frames.length; ++i) { var frame = main_data.frames[i]; if (frame.t0 < tmin || frame.t0 > tmax) continue; var x = Math.floor(time_to_x(frame.t0)); ctx.save(); ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)'; ctx.beginPath(); ctx.moveTo(x+0.5, 0); ctx.lineTo(x+0.5, canvas.height); ctx.stroke(); ctx.fillText(((frame.t1 - frame.t0) * 1000).toFixed(0)+'ms', x+2, padding_top - 24); ctx.restore(); } if (zoom) { var x0 = time_to_x(zoom.tmin); var x1 = time_to_x(zoom.tmax); ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)'; ctx.fillStyle = 'rgba(128, 128, 255, 0.2)'; ctx.beginPath(); ctx.moveTo(x0+0.5, 0.5); ctx.lineTo(x1+0.5, 0.5); ctx.lineTo(x1+0.5 + 4, canvas.height-0.5); ctx.lineTo(x0+0.5 - 4, canvas.height-0.5); ctx.closePath(); ctx.fill(); ctx.stroke(); } ctx.restore(); } function set_frames_zoom_handlers(canvas0) { function do_zoom(event) { var zdata = canvas0._zoomData; var relativeX = event.pageX - this.offsetLeft; var relativeY = event.pageY - this.offsetTop; // var width = 0.001 + 0.5 * relativeY / canvas0.height; var width = 0.001 + 5 * relativeY / canvas0.height; var tavg = zdata.x_to_t(relativeX); var tmax = tavg + width/2; var tmin = tavg - width/2; var range = {'tmin': tmin, 'tmax': tmax}; update_display(range); } var mouse_is_down = false; $(canvas0).unbind(); $(canvas0).mousedown(function(event) { mouse_is_down = true; do_zoom.call(this, event); }); $(canvas0).mouseup(function(event) { mouse_is_down = false; }); $(canvas0).mousemove(function(event) { if (mouse_is_down) do_zoom.call(this, event); }); } function set_zoom_handlers(main_data, data, canvas0, canvas1) { function do_zoom(event) { var hdata = canvas0._hierarchyData; function x_to_time(x) { return hdata.tmin + x * (hdata.tmax - hdata.tmin) / canvas0.width; } var relativeX = event.pageX - this.offsetLeft; var relativeY = event.pageY - this.offsetTop; var width = 8 + 64 * relativeY / canvas0.height; var zoom = { tmin: x_to_time(relativeX-width/2), tmax: x_to_time(relativeX+width/2) }; display_hierarchy(main_data, data, canvas0, hdata.range, zoom); display_hierarchy(main_data, data, canvas1, zoom, undefined); } var mouse_is_down = false; $(canvas0).mousedown(function(event) { mouse_is_down = true; do_zoom.call(this, event); }); $(canvas0).mouseup(function(event) { mouse_is_down = false; }); $(canvas0).mousemove(function(event) { if (mouse_is_down) do_zoom.call(this, event); }); } function set_tooltip_handlers(canvas) { function do_tooltip(event) { var tooltips = canvas._tooltips; if (!tooltips) return; var relativeX = event.pageX - this.offsetLeft; var relativeY = event.pageY - this.offsetTop; var text = undefined; for (var i = 0; i < tooltips.length; ++i) { var t = tooltips[i]; if (t.x0-1 <= relativeX && relativeX <= t.x1+1 && t.y0 <= relativeY && relativeY <= t.y1) { text = t.text(); break; } } if (text) { if (text.length > 512) $('#tooltip').addClass('long'); else $('#tooltip').removeClass('long'); $('#tooltip').css('left', (event.pageX+16)+'px'); $('#tooltip').css('top', (event.pageY+8)+'px'); $('#tooltip').html(text); $('#tooltip').css('visibility', 'visible'); } else { $('#tooltip').css('visibility', 'hidden'); } } $(canvas).mousemove(function(event) { do_tooltip.call(this, event); }); } function search_regions(query) { var re = new RegExp(query); var data = g_data.threads[0].processed_events; var found = []; for (var i = 0; i < data.intervals.length; ++i) { var interval = data.intervals[i]; if (interval.id.match(re)) { found.push(interval); if (found.length > 100) break; } } var out = $('#regionsearchresult > tbody'); out.empty(); for (var i = 0; i < found.length; ++i) { out.append($('?' + found[i].id + '' + (found[i].duration*1000) + '')); } }