#!/usr/bin/env perl

=head1 NAME

devel_module_trace_result_server - webserver daemon to present result in html

=head1 DESCRIPTION

This script runs a webserver to present the result as web page.

=head1 SYNOPSIS

=over 4

  devel_module_trace_result_server <result file>

=back

=cut

use warnings;
use strict;
use POSIX;
use Template;
use Mojolicious::Lite;
use JSON::XS;

my $file = $ARGV[0] or die("usage: $0 <result file>");

################################################################################
get '/' => sub {
  my $c   = shift;
  $c->render(text => _get_output());
};
app->start('daemon');
exit;


################################################################################
# read result file
sub _get_output {
    my($data) = _read_results($file);
    my $stash = {
        'sprintf'     => \&CORE::sprintf,
        'int'         => \&CORE::int,
        'strftime'    => sub { return(POSIX::strftime($_[0], localtime($_[1]))) },
        'json_encode' => sub { return(JSON::XS->new->encode($_[0])) },
        'script'      => $data->{'script'},
        'filter'      => $data->{'filter'},
    };

    # flatten our result
    my $flattened = _flatten_results($data->{'result'});
    for my $mod (@{$flattened}) {
        delete $mod->{'parent'};
    }
    $stash->{'timeline'} = $flattened;
    # calculate percentages
    my $start_time = $flattened->[0]->{'time'};
    my $end_time   = $start_time;
    for my $mod (@{$flattened}) {
        my $end = $mod->{'time'} + $mod->{'elapsed'};
        if($end > $end_time) { $end_time = $end; }
    }
    my $total_time = $end_time - $start_time;
    $stash->{'start_time'} = $start_time;
    $stash->{'end_time'}   = $end_time;
    $stash->{'total_time'} = $total_time;
    for my $mod (@{$flattened}) {
        $mod->{'offset_x'} = ($mod->{'time'} - $start_time) / $total_time;
        $mod->{'width'}    = $mod->{'elapsed'} / $total_time;
    }

    # get most used modules
    my $most = {};
    for my $mod (@{$flattened}) {
        if(!defined $most->{$mod->{'name'}}) {
            $most->{$mod->{'name'}} = {
                name     => $mod->{'name'},
                count    => 0,
                first    => $mod,
                others   => [],
                duration => 0,
            }
        } else {
            push @{$most->{$mod->{'name'}}->{'others'}}, $mod;
        }
        $most->{$mod->{'name'}}->{'count'}++;
        $most->{$mod->{'name'}}->{'duration'} += $mod->{'elapsed'};
    }
    $stash->{'most'}    = [sort { $b->{count} <=> $a->{count} || $a->{name} cmp $b->{name} } values %{$most}];
    $stash->{'modules'} = $most;

    # get slowest used modules
    $stash->{'slowest'} = [sort { $b->{duration} <=> $a->{duration} || $a->{name} cmp $b->{name} } values %{$most}];

    my $template = _get_template();
    my $output;
    my $author = -f '.author' ? 1 : 0;
    my $tt = Template->new({
                  PRE_CHOMP          => 1,
                  POST_CHOMP         => 1,
                  TRIM               => 1,
                  STRICT             => $author ? 1 : 0,
                  render_die         => 1,
    });
    $tt->process(\$template, $stash, \$output) || die $tt->error();
    return($output);
}

################################################################################
sub _flatten_results {
    my($mods) = @_;
    my $flat = [];
    for my $mod (@{$mods}) {
        $mod->{'num_childs'} = 0 unless $mod->{'num_childs'};
        push @{$flat}, $mod;
        if($mod->{'sub'}) {
            for my $submod (@{$mod->{'sub'}}) {
                $submod->{'parent'} = $mod;
            }
            my $subs = _flatten_results($mod->{'sub'});
            push @{$flat}, @{$subs};
            $mod->{'num_childs'} += scalar @{$subs};
        }
        my($time, $milliseconds) = split(/\./mx, $mod->{'time'});
        $mod->{'human_starttime'} =
            POSIX::strftime("%H:%M:", localtime($time)).
            POSIX::strftime("%S", localtime($time)).'.'.$milliseconds;
        ($time, $milliseconds) = split(/\./mx, ($mod->{'time'}+$mod->{'elapsed'}));
        $mod->{'human_endtime'} =
            POSIX::strftime("%H:%M:", localtime($time)).
            POSIX::strftime("%S", localtime($time)).'.'.$milliseconds;

    }
    return($flat);
}

################################################################################
sub _get_template {
    our $data    = "";
    my $template = "";
    if(-e 'templates/index.tt') {
        open(my $fh, '<', 'templates/index.tt') or die("cannot read index.tt: $!");
        while(<$fh>) { $template .= $_; }
        close($fh);
    } else {
        if(!$data) {
            while(<DATA>) { $template .= $_; }
        }
        $template = $data;
    }
    return($template);
}

################################################################################
sub _read_results {
    my($file) = @_;
    open(my $fh, '<', $file) or die("cannot open $file: $!");
    my $content = "";
    while(<$fh>) { $content .= $_; }
    close($fh);
    if($content !~ m/^\$VAR1/mx) {
        die("unknown result file format");
    }
    my($VAR1);
    ## no critic
    eval($content);
    ## use critic
    die($@) if($@);
    return($VAR1);
}

################################################################################

__DATA__
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Devel::Module::Trace</title>
    <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" />
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
    <style type='text/css'>
      BODY {
        padding-top: 25px;
      }
      TABLE.table TD, TABLE.table TH {
        white-space: nowrap;
      }
      .node {
        white-space: nowrap;
        height: 13px;
        position: absolute;
        background: #5bc0de;
        line-height: 13px;
        font-size: x-small;
      }
      .node.direct {
        background: #4cae4c;
      }
      .node_border {
        position: absolute;
        border: 1px solid grey;
        border-radius: 2px;
      }
      .nodetooltip {
        text-align: left;
        white-space: nowrap;
      }
      .nodetooltip TD {
        maring-left: 2px;
        padding-right: 3px;
      }
      .legend {
        height: 20px;
        position: relative;
        margin-right: 5px;
        padding: 0 3px;
        border: 1px solid grey;
        top: -1px;
        border-radius: 2px;
      }
    </style>
  </head>
  <body>

<!-- Navigation -->
<a name="top"></a>
<nav class="navbar navbar-default navbar-fixed-top">
  <div class="container">
    <div id="navbar" class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a href="#stats">Statistics</a></li>
        <li><a href="#timeline">Timeline</a></li>
        <li><a href="#slowest">Slowest Modules</a></li>
        <li><a href="#most-used">Most Used Modules</a></li>
      </ul>
    </div>
  </div>
</nav>


<div class="container">

<!-- Statistics -->
<a name="stats"></a>
<div class="page-header">
    <h1>Statistics</h1>
</div>
<p>
  Recorded [% timeline.size %] module loads from '[% script %]' at [% strftime("%c", start_time) %].<br />
  Found [% modules.keys.size %] uniq modules which took [% sprintf("%.5f", total_time) %] seconds to load in total.
</p>


<!-- Timeline -->
<a name="timeline"></a>
<div class="page-header">
    <h1>Timeline</h1>
</div>
<p>
  Show in which order the modules have been loaded. The width of the bars reflects the time which the module took to load.<br />
  The x position is the relative time from start. Hover over a bar to get more information.<br /><br />
  Legend: <span class="node direct legend">direct dependency</span><span class="node legend">indirect dependency</span><br />

  <div style="position: relative;">
  <div style="position: absolute; top: -8px; left: -3px;">0s</div>
  <div style="position: absolute; top: -8px; right: -3px;" id="endmark">1s</div>
  <div style="position: relative; border: 1px solid grey; top: 10px;" id="timeline"></div>
  </div>
</p>


<!-- Slowest Modules -->
<a name="slowest"></a>
<div class="page-header">
    <h1>Slowest Modules</h1>
</div>
<p>
  Lists the modules ordered by duration.<br />
  <table class="table" id="slowest-table">
  <tr>
    <th># Loaded</th>
    <th>Module</th>
    <th>Total Duration</th>
    <th>First used in</th>
  </tr>
  </table>
</p>


<!-- Most Used -->
<a name="most-used"></a>
<div class="page-header">
    <h1>Most Used Modules</h1>
</div>
<p>
  Lists the modules ordered by the times loaded.<br />
  <table class="table" id="most-table">
  <tr>
    <th># Loaded</th>
    <th>Module</th>
    <th>Total Duration</th>
    <th>First used in</th>
  </tr>
  </table>
</p>


</div>

<script type="text/javascript">
var slowest  = [% json_encode(slowest) %];
var most     = [% json_encode(most) %];
var modules  = [% json_encode(modules) %];
var filter   = [% json_encode(filter) %];
var timeline = [% json_encode(timeline) %];

/* redraw all dynamic tables and graphs */
function renew_all() {
    renew_timeline();
    renew_most_table();
    renew_slowest_table();
    $('[data-toggle="tooltip"]').tooltip({html: true});
}

/* redraw the most used table */
function renew_most_table() {
    $('#most-table').children('tr:not(:first)').remove();
    $.each(most, function(i, r) {
      if(isFiltered(r)) { return; }
      $('#most-table tr:last').after(
          '<tr>'
         +'<td>'+r.count+'</td>'
         +'<td>'+r.name+'</td>'
         +'<td>'+Number(r.duration).toFixed(5)+'s</td>'
         +'<td>'+r.first.caller+'</td>'
         +'</tr>'
      );
    });
}

/* redraw the slowest modules table */
function renew_slowest_table() {
    $('#slowest-table').children('tr:not(:first)').remove();
    $.each(slowest, function(i, r) {
      if(isFiltered(r)) { return; }
      $('#slowest-table tr:last').after(
          '<tr>'
         +'<td>'+r.count+'</td>'
         +'<td>'+r.name+'</td>'
         +'<td>'+Number(r.duration).toFixed(5)+'s</td>'
         +'<td>'+r.first.caller+'</td>'
         +'</tr>'
      );
    });
}

/* redraw the timeline graph */
function renew_timeline() {
    $('#timeline').children().remove();
    var total_width = $('#timeline').width();
    var y           = -1;
    $.each(timeline, function(i, r) {
      if(isFiltered(r)) { return; }
      var title = '<table class=\'nodetooltip\'>'
          +'<tr><td>Name:</td><td>'+r.name+'</td></tr>'
          +'<tr><td>Started:</td><td>'+r.human_starttime+'</td></tr>'
          +'<tr><td>Started:</td><td>'+r.human_endtime+'</td></tr>'
          +'<tr><td>Duration:</td><td>'+Number(r.elapsed).toFixed(5)+'s</td></tr>'
          +'<tr><td>Caller:</td><td>'+r.caller+'</td></tr>'
          +'<tr><td># Loaded:</td><td>'+modules[r.name].count+'</td></tr>'
          +'<tr><td>Total Duration:</td><td>'+Number(modules[r.name].duration).toFixed(5)+'s</td></tr>'
          +'</table>';
      var width = Math.floor(total_width*r.width)-2;
      if(width < 0) { width = 0; }
      $('#timeline').append(
        '<span class="node_border" style="width: '+(Math.floor(total_width*r.width))+'px; left: '+(Math.floor(total_width*r.offset_x)-1)+'px; top: '+y+'px; height: 15px;">'
       +  '<span class="node'+("[% script %]" == r.caller_f ? ' direct' : '')+'"'
       +  'data-toggle="tooltip"'
       +  'data-placement="bottom"'
       +  'style="width: '+width+'px; left: 0; top: 0;"'
       +  'title="'+title+'"'
       +  '>'+r.name+'</span>'
       +'</span>'
      );
      y += 14;
    });
    $('#timeline').css({height: (y+2)+'px'});
    $('#endmark').html('[% sprintf('%.4f', total_time) %]s');
}

/* return true if given module matches a filter */
function isFiltered(mod) {
    var found = false;
    $.each(filter, function(i, f) {
        if(mod.name.match(f)) {
            found = true;
            return false;
        }
        if(f == 'perl' && mod.name.match(/^[\d\.]+$/)) {
            found = true;
            return false;
        }
    });
    return(found);
}

var renewTimer;
$(document).ready(function(){
    renew_all();

    $(window).resize(function() {
        window.clearTimeout(renewTimer);
        renewTimer = window.setTimeout(function() {
            renew_all();
        },150);
    });
});
</script>
  </body>
</html>
