<!DOCTYPE html>
<html>
<head>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/flatly/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.datatables.net/1.10.10/css/jquery.dataTables.min.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/1.1.1/ui-bootstrap.min.js"></script>
<script src="https://cdn.datatables.net/1.10.10/js/jquery.dataTables.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/0.2.8/pako_inflate.min.js"></script>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>perfwhiz {{info.label}}</title>
<style type="text/css">
body {
  font: 12px sans-serif;
  padding-top: 50px;
}

.bar {
  fill: steelblue;
}
div.tooltip {
    position: absolute;
    border: 0px;
    pointer-events: none;
}
td {
  text-align: right;
}
th {
  text-align: right;
}
.legend {
  font-size: 12px;
}
.grid .tick {
    stroke: lightgrey;
    opacity: 0.7;
}
.axis path,
.axis line {
    fill: none;
    stroke: lightgray;
    stroke-width: 1;
    shape-rendering: crispEdges;
}

.grid .tick {
    stroke: lightgrey;
    stroke-opacity: 0;
    shape-rendering: crispEdges;
}
.grid path {
          stroke-width: 0;
}
</style>
</head>
<body ng-app="pw-heatmaps" ng-controller="PwHeatMapsCtrl" ng-cloak>

<nav class="navbar navbar-default navbar-fixed-top">
  <div class="container-fluid">
      <div class="navbar-header">
      </div>
      <div class="collapse navbar-collapse">
           <ul class="nav navbar-nav">
                <li ng-repeat="mode in modes" ng-class="{active: current_index==$index}"
                    ng-click="handleEvent($event, $index)">
                    <a href="#"><span class="glyphicon glyphicon-{[mode.icon]}" aria-hidden="true"></span>
                    &nbsp;{[mode.title]}</a>
                </li>
          </ul>
      </div>
  </div>
</nav>

<div class="container-fluid">
    <h1>{[current_mode.title]}
     <div class="btn-group" uib-dropdown keyboard-nav ng-show="current_index < 2">
        <button id="simple-btn-keyboard-nav" type="button" class="btn btn-primary" uib-dropdown-toggle>
            {[current_mode.taskd[current_mode.task_index].task]} <span class="caret" ng-show="current_mode.taskd.length>1"></span>
        </button>
        <ul uib-dropdown-menu role="menu" aria-labelledby="simple-btn-keyboard-nav">
            <li ng-hide="current_mode.task_index==$index" ng-repeat="task in current_mode.taskd" role="menuitem" ng-click="selectTask($event, $index)"><a href="#">{[task.task]}</a></li>
        </ul>
     </div>
     <small>{{info.label}} ({{info.window}} msec window)</small></h1>
</div>
<div class="container-fluid" ng-show="current_index == 0">
    <svg ng-repeat="task in modes[0].taskd" id="svg-sw{[$index]}" ng-show="current_mode.task_index==$index" initsvg>
        <g> 
            <g ng-repeat="data in task.data" id="{[data.id]}" ng-show="show_sw_evt[$index]"></g>
        </g>
    </svg>
    <div class="checkbox">
      <label>
        <input type="checkbox" ng-model="show_sw_evt[0]"> Show "end of run" events
      </label>
    </div>
    <div class="checkbox">
      <label>
        <input type="checkbox" ng-model="show_sw_evt[1]"> Show "end of sleep" events
    </label>
    </div>
</div>
<div class="container-fluid" ng-show="current_index == 1">
    <svg ng-repeat="task in modes[1].taskd" id="svg-kvm{[$index]}" ng-show="current_mode.task_index==$index">
        <g> 
            <g ng-repeat="data in task.data" id="{[data.id]}" ng-show="show_kvm_evt[$index]"></g>
        </g>
    </svg>
    <div class="checkbox">
      <label>
        <input type="checkbox" ng-model="show_kvm_evt[0]"> Show kvm exit events (duration = guest user space before kvm exit)
      </label>
    </div>
    <div class="checkbox">
      <label>
        <input type="checkbox" ng-model="show_kvm_evt[1]"> Show kvm entry events (duration = root mode before kvm entry)
    </label>
    </div>
</div>
<div class="container-fluid" ng-show="current_index == 2">
    <svg id="svg-coreloc"></svg>
</div>

<br><br>
<div class="container-fluid">
  <small>
    Crafted on {{info.date}} by <a href="https://github.com/cisco-oss-eng/perfwhiz">perfwhiz</a> {{info.version}}
  </small>
</div>

<script>

var div = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);
var si_fmt = d3.format("s");
function usec_format(usec) {
    return si_fmt(usec/1000000)+"s";
}

function get_text_width(text_list) {
    // a fake svg to calculate max rendered width
    var tmp_svg = d3.select("body").append("svg").attr("width", 1000).attr("height", 1000);
    var tmp_lg = tmp_svg.append("g").attr("class", "g");
    var max_width = 0;
    tmp_lg.selectAll("text")
        .data(text_list)
        .enter().append("text")
        .text(function (d, i) { return d;});
    tmp_lg.selectAll("text")
        .each(function() {
            max_width = Math.max(this.getBBox().width, max_width);
        });
    tmp_svg.remove();
    return max_width;
}
function draw_legend(svg, text_list, color_list, width, align, margin, columns, shape, opacity) {
    var inner_margin = 5;
    var color_rect_size = 18;
    var inner_gap = 2;
    var row_height = color_rect_size + inner_gap;
    // calculate the width of the legend box
    var count = text_list.length;
    var col_width = get_text_width(text_list) + color_rect_size + inner_gap * 2;
    columns = Math.min(count, columns);
    var lb_width = (col_width * columns) + 2 * inner_margin;
    // calculate the x coordinates of the legend box based
    if (align == "left") {
        lb_x = margin;
    } else if (align == "center") {
        lb_x = width/2 - lb_width/2;
    } else {
        lb_x = width - margin - lb_width;
    }
    var count_per_column = Math.ceil(count / columns);
    var lb_height = count_per_column * row_height + inner_margin * 2 - inner_gap;
    var lbg = svg.append("g")
        .attr("class", "g")
        .attr("id", "legend_group")
        .attr("transform", "translate(" + lb_x + ", 0)");
    var legend_box = lbg.append("rect").
        attr("id","legend_box")
        .attr("height", lb_height)
        .attr("width", lb_width)
        .style("stroke-width", 1)
        .style("stroke", "#bbbbbb")
        .style("fill-opacity", 0.7)
        .style("fill", "white");
    var legend = lbg.selectAll(".legend")
        .data(color_list)
        .enter().append("g")
        .attr("class", "legend")
        .attr("transform", function(d, i) {
            return "translate(" +
                    (inner_margin + col_width * Math.floor(i/count_per_column)) +
                    "," +
                    (inner_margin + row_height * (i % count_per_column)) + ")"; });
    if (shape == "circle") {
        legend.append("circle")
            .attr("cx", color_rect_size/2)
            .attr("cy", color_rect_size/2)
            .attr("r", color_rect_size/3)
            .style("opacity", opacity)
            .style("fill", function(d, i) { return d; });
    } else {
        legend.append("rect")
            .attr("width", color_rect_size)
            .attr("height", color_rect_size)
            .style("opacity", opacity)
            .style("fill", function(d, i) { return d; });
    }
    legend.append("text")
        .attr('class','legend')
        .attr("x", row_height)
        .attr("dy", "1.2em")
        .style("text-anchor", "start")
        .text(function(d, i) { return text_list[i]; });
    return lb_height;
}

// ----- Context Switches and KVM events heatmaps
var swk_events = '{{swk_events}}';
swk_events = JSON.parse(pako.inflate(window.atob(swk_events), {to:'string'}));

function draw_swkvm(scope) {
    var task_index = scope.current_mode.task_index;
    var ctask = scope.current_mode.taskd[task_index];

    if (ctask.inited) {
        return;
    }
    ctask.inited = true;
    var total_width = 1100;
    var total_height = 800;
    // approximate left margin required based on longest task name
    var margin = {top: 30, right: 30, bottom: 50, left: 50},
        width = total_width - margin.left - margin.right,
        height = total_height - margin.top - margin.bottom;
    var x = d3.scale.linear()
        .range([0, width])
        .domain([swk_events.usecs_min, swk_events.usecs_max]);
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .tickFormat(usec_format)
        .tickSize(-height, 0, 0);
    var y = d3.scale.log()
        .range([height, 0])
        .domain([1, swk_events.usecs_duration_max]);
    var yAxis = d3.svg.axis()
        .scale(y)
        .tickFormat(usec_format)
        .tickSize(-width, 0, -width)
        .orient("left");
    var svg = d3.select("#svg-"+scope.current_mode.svg+task_index)
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
                .select("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    var xaxis = svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);
    xaxis
        .append("text")
        .attr("x", width-28)
        .attr("dy", "2em")
        .style("text-anchor", "middle")
        .text("time");
    var yaxis = svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
        .append("text")
          .attr("y", -10)
          .style("text-anchor", "middle")
          .text("duration");
    // create one g container per event type
    ctask.data.forEach(function(d,i){
        var state = svg.select("#"+d.id);
        state.selectAll("circle")
          .data(d.events)
        .enter().append("circle")
            .attr("cx", function(d) {return x(d[0]);})
            .attr("cy", function(d) {return y(d[1]);})
            .attr("r", 4)
            .style("fill", function(dd) {return d.color;})
            .style("opacity", 0.2);
    });
    draw_legend(svg,
        ctask.data.map(function(d){return d.desc + " (" + d.events.length + ")"}),
        ctask.data.map(function(d){return d.color}),
        width, "right", 0, 5, "circle", 0.2);
    draw_right_arrow(xaxis, width-10, 30, 10, 10, "red");
}

function draw_right_arrow(node, x, y, h, w, color) {
    var data = [[x, y-h/2], [x+w, y], [x, y+h/2]];
    var line = d3.svg.line()
                 .x(function(d) { return d[0];})
                 .y(function(d) { return d[1];})
                 .interpolate("linear");
    node.append("path")
        .attr("d", line(data))
        .style("stroke", color)
        .style("fill", color);
    node.append("path")
        .attr("d", line([[x-w*3,y], [x,y]]))
        .style("stroke", color)
        .style("stroke-width", 3)
        .style("fill", color);
}

var color = d3.scale.category20();
function get_task_color(index) {
    return color(index % 20);
}

// ----- Coremap
function draw_coreloc(scope) {
    var max_cores = {{info.max_cores}};
    var total_width = 1100;
    var total_height = 800;
    // approximate left margin required based on longest task name
    var margin = {top: 30, right: 30, bottom: 30, left: 50},
        width = total_width - margin.left - margin.right,
        height = total_height - margin.top - margin.bottom;
    var x = d3.scale.linear()
        .range([0, width]);
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .tickFormat(usec_format)
        .tickSize(-height, 0, 0);

    x.domain([swk_events.usecs_min, swk_events.usecs_max]);
    var y = d3.scale.ordinal()
        .rangeBands([height, 0], 0.01, 0.5);
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left")
        .tickSize(-width, 0, -width);
    var core_range = d3.range(max_cores);
    y.domain(core_range);

    var svg = d3.select("#svg-coreloc")
                .attr("width", width + margin.left + margin.right)
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    var axis = svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);
    axis
        .append("text")
        .attr("x", width-28)
        .attr("dy", "2em")
        .style("text-anchor", "middle")
        .text("time");
    draw_right_arrow(axis, width-10, 30, 10, 10, "red");
    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis);
    var yAxis2 = d3.svg.axis()
        .scale(y)
        .orient("right")
        .tickSize(0, 0, 0);
    axis = svg.append("g")
        .attr("class", "y axis")
        .attr("transform", "translate(" + (width) + ",0)")
        .call(yAxis2);
    var event_height = y.rangeBand() / 2;
    var y_offset = event_height / 2;
    var state = svg.selectAll(".state")
        .data(swk_events.task_events)
        .enter().append("g")
        .attr("class", "g")
        .attr("id", function(d, i) {d.color=get_task_color(i); return d.task;});
    state
        .selectAll("rect")
        // events is a list of [usec, duration, cpu] triplets
        .data(function(d) {return d.events['sched__sched_switch'];})
        .enter().append("rect")
        .attr("x", function(d) { var x0 = x((d[0] - d[1])); if (x0 < 0) { x0 = 0;} return x0;})
        .attr("y", function(d) { return y(d[2]) + y_offset;})
        .attr("height", event_height)
        .attr("width", function(d) {
            var x0 = x((d[0] - d[1]));
            var x1 = x(d[0]);
            if (x0 < 0) { return x1;} return x1 - x0;})
        .style("opacity", 0.8)
        .style("fill", function(d) { return this.parentNode.__data__.color;})
        .on("mouseover", function(d) {
            div.transition()
                .duration(100)
                .style("opacity", 1);
            var matrix = this.getScreenCTM()
                .translate(+this.getAttribute("x"),
                           +this.getAttribute("y"));
            div	.html('<button class="btn btn-primary" type="button">' + this.parentNode.__data__.task +
                      ' core ' + d[2] +
                      '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="badge">'+ d[1] + ' usec</span></button>')
                .style("top", (window.pageYOffset + matrix.f + y.rangeBand()/2 - 40) + "px")
                .style("left", (d3.event.pageX) + "px");
        })
        .on("mousemove", function(d) {
            div	.style("left", (d3.event.pageX) + "px");
        })
        .on("mouseout", function(d) {
            div.transition()
                .duration(500)
                .style("opacity", 0);
            d3.select(this).style("opacity", 0.8);
        });
    var text_list = swk_events.task_events.map(function(d) {return d.task;});
    var color_list = swk_events.task_events.map(function(d, i) {return get_task_color(i);});
    var lg_svg = svg.append("g")
        .attr("transform", "translate(0, " + (height+20) + ")")
    var legend_height = draw_legend(lg_svg, text_list, color_list, width, "center", 0, 5, "square", 0.5)
    d3.select("#svg-coreloc")
                .attr("height", total_height + legend_height);
}

/* ---------------------------------------- */
var app = angular.module('pw-heatmaps', ['ui.bootstrap']);
var modes = [
    {title:"Context switches", icon:"random", svg:"sw", init:draw_swkvm,
     task_index: 0,
     taskd: swk_events.task_events.map(function(d) { 
        return {task: d.task,
                inited: false,
                data: [
                    {id:"run", desc:"end of run", events:d.events['sched__sched_switch'], color:"blue"},
                    {id: "sleep", desc:"end of sleep", events:d.events['sched__sched_stat_sleep'], color:"red"}
                ]}; 
     })
    },
    {title:"KVM Exits", icon:"log-out", svg:"kvm", init:draw_swkvm,
     task_index: 0,
     taskd: swk_events.task_events.map(function(d) { 
        return {task: d.task,
                inited: false,
                data: [
                    {id:"run", desc:"user space run time (@kvm exit)", events:d.events['kvm_exit'], color:"blue"},
                    {id: "sleep", desc:"root mode duration (@kvm entry)", events:d.events['kvm_entry'], color:"red"}
                ]}; 
     })
    },
    {title:"Core locality over time", icon:"globe", svg:"coreloc", init:draw_coreloc}
];

app.config(['$interpolateProvider', function($interpolateProvider) {
  $interpolateProvider.startSymbol('{[');
  $interpolateProvider.endSymbol(']}');
}]);
app.controller('PwHeatMapsCtrl', PwHeatMapsCtrl);
app.directive('initsvg', function() {
    return {
        link: function($scope, element, attrs) {
            // Trigger when number of children changes,
            // including by directives like ng-repeat
            var watch = $scope.$watch(function() {
                return element.children().length;
            }, function() {
                // Wait for templates to render
                $scope.$evalAsync(function() {
                    // Finally, directives are evaluated
                    // and templates are renderer here
                    $scope.handleEvent(null, 0);
                });
            });
        },
    };
});

function PwHeatMapsCtrl($scope) {

    $scope.modes = modes;
    $scope.current_index = 0;
    $scope.handleEvent = function(event, index) {
        $scope.current_index = index;
        $scope.current_mode = $scope.modes[index];
        var initf = $scope.current_mode['init'];
        if (initf != null) {
            initf($scope);
            $scope.current_mode['init'] = null;
        }
    };
    $scope.show_sw_evt = [true, false];
    $scope.show_kvm_evt = [true, true];
    $scope.selectTask = function(event, index) {
        $scope.current_mode.task_index = index;
        draw_swkvm($scope);
    };
}
</script>

</body>
</html>