[PATCH] testbot/web: Add a statistics page.

Francois Gouget fgouget at codeweavers.com
Thu Dec 28 17:23:48 CST 2017


This can help figuring out how busy the TestBot is, whether the VMs must
be rebalanced to lighten the load on a VM host, whether reverts are
getting slow, etc.

Signed-off-by: Francois Gouget <fgouget at codeweavers.com>
---
 testbot/lib/WineTestBot/Activity.pm     | 165 ++++++++++++++-
 testbot/lib/WineTestBot/CGI/PageBase.pm |   1 +
 testbot/web/Stats.pl                    | 361 ++++++++++++++++++++++++++++++++
 testbot/web/WineTestBot.css             |   2 +
 4 files changed, 528 insertions(+), 1 deletion(-)
 create mode 100644 testbot/web/Stats.pl

diff --git a/testbot/lib/WineTestBot/Activity.pm b/testbot/lib/WineTestBot/Activity.pm
index c5d3b532..5a5d2328 100644
--- a/testbot/lib/WineTestBot/Activity.pm
+++ b/testbot/lib/WineTestBot/Activity.pm
@@ -35,7 +35,7 @@ use vars qw (@ISA @EXPORT);
 
 require Exporter;
 @ISA = qw(Exporter);
- at EXPORT = qw(&GetActivity);
+ at EXPORT = qw(&GetActivity &GetStatistics);
 
 
 =pod
@@ -289,4 +289,167 @@ sub GetActivity($)
   return ($Activity, $Counters);
 }
 
+sub _AddFullStat($$$;$)
+{
+  my ($Stats, $StatKey, $Value, $Source) = @_;
+
+  $Stats->{"$StatKey.count"}++;
+  $Stats->{$StatKey} += $Value;
+  my $MaxKey = "$StatKey.max";
+  if (!exists $Stats->{$MaxKey} or $Stats->{$MaxKey} < $Value)
+  {
+    $Stats->{$MaxKey} = $Value;
+    $Stats->{"$MaxKey.source"} = $Source if ($Source);
+  }
+}
+
+sub GetStatistics($)
+{
+  my ($VMs) = @_;
+
+  my ($GlobalStats, $HostsStats, $VMsStats) = ({}, {}, {});
+
+  my @JobTimes;
+  my $Jobs = CreateJobs();
+  foreach my $Job (@{$Jobs->GetItems()})
+  {
+    $GlobalStats->{"jobs.count"}++;
+
+    my $IsSpecialJob;
+    my $Steps = $Job->Steps;
+    foreach my $Step (@{$Steps->GetItems()})
+    {
+      my $StepType = $Step->Type;
+      $IsSpecialJob = 1 if ($StepType =~ /^(?:reconfig|suite)$/);
+
+      my $Tasks = $Step->Tasks;
+      foreach my $Task (@{$Tasks->GetItems()})
+      {
+        $GlobalStats->{"tasks.count"}++;
+        if ($Task->Started and $Task->Ended and
+            $Task->Status !~ /^(?:queued|running|canceled)$/)
+        {
+          my $Time = $Task->Ended - $Task->Started;
+          _AddFullStat($GlobalStats, "$StepType.time", $Time, $Task);
+        }
+        if ($IsSpecialJob)
+        {
+          my $ReportFileName = $Task->GetDir() . "/log";
+          if (-f $ReportFileName)
+          {
+            my $ReportSize = -s $ReportFileName;
+            _AddFullStat($GlobalStats, "$StepType.size", $ReportSize, $Task);
+            if ($VMs->ItemExists($Task->VM->GetKey()))
+            {
+              my $VMStats = ($VMsStats->{items}->{$Task->VM->Name} ||= {});
+              _AddFullStat($VMStats, "report.size", $ReportSize, $Task);
+            }
+          }
+        }
+      }
+    }
+
+    if (!$IsSpecialJob and$Job->Ended and
+        $Job->Status !~ /^(?:queued|running|canceled)$/)
+    {
+      my $Time = $Job->Ended - $Job->Submitted;
+      _AddFullStat($GlobalStats, "jobs.time", $Time, $Job);
+      push @JobTimes, $Time;
+
+      if (!exists $GlobalStats->{start} or $GlobalStats->{start} > $Job->Submitted)
+      {
+        $GlobalStats->{start} = $Job->Submitted;
+      }
+      if (!exists $GlobalStats->{end} or $GlobalStats->{end} < $Job->Ended)
+      {
+        $GlobalStats->{end} = $Job->Ended;
+      }
+    }
+  }
+
+  my $JobCount = $GlobalStats->{"jobs.time.count"};
+  if ($JobCount)
+  {
+    @JobTimes = sort { $a <=> $b } @JobTimes;
+    $GlobalStats->{"jobs.time.p10"} = $JobTimes[int($JobCount * 0.1)];
+    $GlobalStats->{"jobs.time.p50"} = $JobTimes[int($JobCount * 0.5)];
+    $GlobalStats->{"jobs.time.p90"} = $JobTimes[int($JobCount * 0.9)];
+    @JobTimes = (); # free early
+  }
+
+  my ($Activity, $Counters) = GetActivity($VMs);
+  $GlobalStats->{"recordgroups.count"} = $Counters->{recordgroups};
+  $GlobalStats->{"records.count"} = $Counters->{records};
+  foreach my $Group (values %$Activity)
+  {
+    if (!$VMsStats->{start} or $VMsStats->{start} > $Group->{start})
+    {
+      $VMsStats->{start} = $Group->{start};
+    }
+    if (!$VMsStats->{end} or $VMsStats->{end} < $Group->{end})
+    {
+      $VMsStats->{end} = $Group->{end};
+    }
+    next if (!$Group->{statusvms});
+
+    my ($IsGroupBusy, %IsHostBusy);
+    foreach my $VM (@{$VMs->GetItems()})
+    {
+      my $VMStatus = $Group->{statusvms}->{$VM->Name};
+      my $Host = $VMStatus->{vmstatus}->{host} || $VM->GetHost();
+      my $HostStats = ($HostsStats->{items}->{$Host} ||= {});
+
+      if (!$VMStatus->{merged})
+      {
+        my $VMStats = ($VMsStats->{items}->{$VM->Name} ||= {});
+        my $Status = $VMStatus->{status};
+
+        my $Time = $VMStatus->{end} - $VMStatus->{start};
+        _AddFullStat($VMStats, "$Status.time", $Time);
+        _AddFullStat($HostStats, "$Status.time", $Time);
+        if ($Status =~ /^(?:reverting|sleeping|running|dirty)$/)
+        {
+          $VMStats->{"busy.elapsed"} += $Time;
+        }
+
+        if ($VMStatus->{result} =~ /^(?:boterror|error|timeout)$/)
+        {
+          $VMStats->{"$VMStatus->{result}.count"}++;
+          $HostStats->{"$VMStatus->{result}.count"}++;
+          $GlobalStats->{"$VMStatus->{result}.count"}++;
+        }
+        elsif ($VMStatus->{task} and
+               ($VMStatus->{result} eq "completed" or
+                $VMStatus->{result} eq "failed"))
+        {
+          my $StepType = $VMStatus->{step}->Type;
+          _AddFullStat($VMStats, "$StepType.time", $Time, $VMStatus->{task});
+          _AddFullStat($HostStats, "$StepType.time", $Time, $VMStatus->{task});
+        }
+      }
+
+      $VMStatus = $VMStatus->{vmstatus};
+      if (!$IsHostBusy{$Host} and
+          $VMStatus->{status} =~ /^(?:reverting|sleeping|running|dirty)$/)
+      {
+        # Note that we cannot simply sum the VMs busy wall clock times to get
+        # the host busy wall clock time because this would count periods where
+        # more than one VM is busy multiple times.
+        $HostStats->{"busy.elapsed"} += $Group->{end} - $Group->{start};
+        $IsHostBusy{$Host} = 1;
+        $IsGroupBusy = 1;
+      }
+    }
+    if ($IsGroupBusy)
+    {
+      $GlobalStats->{"busy.elapsed"} += $Group->{end} - $Group->{start};
+    }
+  }
+  $GlobalStats->{elapsed} = $GlobalStats->{end} - $GlobalStats->{start};
+  $HostsStats->{elapsed} =
+      $VMsStats->{elapsed} = $VMsStats->{end} - $VMsStats->{start};
+
+  return { global => $GlobalStats, hosts => $HostsStats, vms => $VMsStats };
+}
+
 1;
diff --git a/testbot/lib/WineTestBot/CGI/PageBase.pm b/testbot/lib/WineTestBot/CGI/PageBase.pm
index fe03a47e..14900904 100644
--- a/testbot/lib/WineTestBot/CGI/PageBase.pm
+++ b/testbot/lib/WineTestBot/CGI/PageBase.pm
@@ -266,6 +266,7 @@ EOF
     print "        <li><p><a href='", MakeSecureURL("/Submit.pl"),
           "'>Submit job</a></p></li>\n";
     print "        <li><p><a href='/Activity.pl'>Activity</a></p></li>\n";
+    print "        <li><p><a href='/Stats.pl'>Statistics</a></p></li>\n";
     print "        <li class='divider'> </li>\n";
     print "        <li><p><a href='", MakeSecureURL("/Logout.pl"), "'>Log out";
     if (defined($Session))
diff --git a/testbot/web/Stats.pl b/testbot/web/Stats.pl
new file mode 100644
index 00000000..424d33e0
--- /dev/null
+++ b/testbot/web/Stats.pl
@@ -0,0 +1,361 @@
+# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*-
+# Shows TestBot statistics
+#
+# Copyright 2017 Francois Gouget
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+use strict;
+
+package StatsPage;
+
+use ObjectModel::CGI::Page;
+use ObjectModel::Collection;
+use WineTestBot::Config;
+use WineTestBot::Activity;
+use WineTestBot::Log;
+use WineTestBot::VMs;
+
+ at StatsPage::ISA = qw(ObjectModel::CGI::Page);
+
+sub _initialize($$$)
+{
+  my ($self, $Request, $RequiredRole) = @_;
+
+  $self->{start} = Time();
+  $self->SUPER::_initialize($Request, $RequiredRole);
+}
+
+sub _GetDuration($;$)
+{
+  my ($Secs, $Raw) = @_;
+
+  return "n/a" if (!defined $Secs);
+
+  my @Parts;
+  if (!$Raw)
+  {
+    my $Mins = int($Secs / 60);
+    my $Hours = int($Mins / 60);
+    my $Days = int($Hours / 24);
+    push @Parts, "${Days}d" if ($Days);
+    my $Part = $Hours - 24 * $Days;
+    push @Parts, "${Part}h" if ($Part);
+    $Part = $Mins - 60 * $Hours;
+    push @Parts, "${Part}m" if ($Part);
+    $Secs = $Secs - 60 * $Mins;
+  }
+  push @Parts, (@Parts or int($Secs) == $Secs) ?
+               int($Secs) ."s" :
+               sprintf('%.1fs', $Secs);
+  return join(" ", @Parts);
+}
+
+sub _CompareVMs()
+{
+  my ($aHost, $bHost) = ($a->GetHost(), $b->GetHost());
+  if ($PrettyHostNames)
+  {
+    $aHost = $PrettyHostNames->{$aHost} || $aHost;
+    $bHost = $PrettyHostNames->{$bHost} || $bHost;
+  }
+  return $aHost cmp $bHost || $a->Name cmp $b->Name;
+}
+
+sub _AddRate($$;$)
+{
+  my ($Stats, $StatKey, $AllStats) = @_;
+
+  my $RateKey = $StatKey;
+  $RateKey =~ s/(?:\.time)?\.count$/.rate/;
+  $AllStats ||= $Stats;
+  $Stats->{$RateKey} = $AllStats->{elapsed} ?
+        3600 * $Stats->{$StatKey} / $AllStats->{elapsed} :
+      "n/a";
+}
+
+sub _GetAverage($$)
+{
+  my ($Stats, $Key) = @_;
+  return "n/a" if (!$Stats->{"$Key.count"});
+  return $Stats->{$Key} / $Stats->{"$Key.count"};
+}
+
+my $NO_AVERAGE = 1;
+my $NO_PERCENTAGE = 2;
+my $NO_TIME = 4;
+
+sub _GetStatStr($$;$$)
+{
+  my ($Stats, $StatKey, $AllStats, $Flags) = @_;
+
+  if ($StatKey =~ /\.time$/ and !($Flags & $NO_AVERAGE) and
+      exists $Stats->{"$StatKey.count"})
+  {
+    my $Avg = _GetAverage($Stats, $StatKey);
+    return $Avg eq "n/a" ? "n/a" : _GetDuration($Avg, $Flags & $NO_TIME);
+  }
+
+  if ($StatKey =~ /\.size$/ and !($Flags & $NO_AVERAGE) and
+      exists $Stats->{"$StatKey.count"})
+  {
+    my $Avg = _GetAverage($Stats, $StatKey);
+    return $Avg eq "n/a" ? "n/a" : int($Avg);
+  }
+
+  my $Value = $Stats->{$StatKey};
+  if ($StatKey =~ /\.elapsed$/ and !($Flags & $NO_PERCENTAGE))
+  {
+    $AllStats ||= $Stats;
+    return "n/a" if (!$AllStats->{elapsed});
+    return sprintf('%.1f%', 100 * $Value / $AllStats->{elapsed});
+  }
+  if ($StatKey =~ /(?:\belapsed|\.time)\b/)
+  {
+    return _GetDuration($Value, $Flags & $NO_TIME);
+  }
+  if ($StatKey =~ /\.rate$/)
+  {
+    return sprintf('%.1f / h', $Value);
+  }
+  return "0" if (!exists $Stats->{$StatKey});
+  return $Value if ($Value == int($Value));
+  return sprintf('%.1f', $Value);
+}
+
+sub _GetStatHtml($$;$$)
+{
+  my ($Stats, $StatKey, $AllStats, $Flags) = @_;
+
+  my $Value = _GetStatStr($Stats, $StatKey, $AllStats, $Flags);
+  return $Value if (!$Stats->{"$StatKey.source"});
+
+  my $SrcObj = $Stats->{"$StatKey.source"};
+  my ($JobId, $StepNo, $TaskNo) = ObjectModel::Collection::SplitKey(undef, $SrcObj->GetFullKey());
+  if (defined $TaskNo)
+  {
+    my $Key = "$JobId#k". ($StepNo * 100 + $TaskNo);
+    return "<a href='/JobDetails.pl?Key=$Key'>$Value</a>";
+  }
+  return "<a href='/index.pl#job$JobId'>$Value</a>";
+}
+
+sub _GenGlobalLine($$$;$$)
+{
+  my ($Stats, $StatKey, $Label, $Description, $Flags) = @_;
+
+  my $Value = _GetStatHtml($Stats, $StatKey, undef, $Flags);
+  print "<tr><td>$Label</td><td>$Value</td><td>$Description</td></tr>\n";
+}
+
+sub _GenStatsLine($$$$;$)
+{
+  my ($RowStats, $StatKey, $Label, $ColumnKeys, $Flags) = @_;
+
+  print "<tr><td>$Label</td>\n";
+  foreach my $Col (@$ColumnKeys)
+  {
+    my $Stats = $RowStats->{items}->{$Col};
+    my $Value = _GetStatHtml($Stats, $StatKey, $RowStats, $Flags);
+    print "<td>$Value</td>\n";
+  }
+  print "</tr>\n";
+}
+
+sub GenerateBody($)
+{
+  my ($self) = @_;
+
+  print "<h1>${ProjectName} Test Bot activity statistics</h1>\n";
+  print "<div class='Content'>\n";
+
+  ### Get the sorted VMs list
+
+  my $VMs = CreateVMs();
+  $VMs->FilterEnabledRole();
+  my @SortedVMs = sort _CompareVMs @{$VMs->GetItems()};
+  my $Stats = GetStatistics($VMs);
+
+  ### Show global statistics
+
+  my $GlobalStats = $Stats->{global};
+  print "<h2>General statistics</h2>\n";
+  print "<div class='CollectionBlock'><table>\n";
+
+  print "<thead><tr><th>Stat</th><th>Value</th><th>Description</th></thead>\n";
+  print "<tbody>\n";
+
+  _GenGlobalLine($GlobalStats, "elapsed", "Job history", "How far back the job history goes.");
+
+  _GenGlobalLine($GlobalStats, "jobs.count", "Job count", "The number of jobs in the job history.");
+  _AddRate($GlobalStats, "jobs.count");
+  _GenGlobalLine($GlobalStats, "jobs.rate", "Job rate", "How fast new jobs are coming in.");
+  _GenGlobalLine($GlobalStats, "tasks.count", "Task count", "The number of tasks.");
+  _AddRate($GlobalStats, "tasks.count");
+  _GenGlobalLine($GlobalStats, "tasks.rate", "Task rate", "How fast new tasks are coming in.");
+  _GenGlobalLine($GlobalStats, "busy.elapsed", "Busy time", "How much wall clock time was spent running jobs.", $NO_PERCENTAGE);
+  _GenGlobalLine($GlobalStats, "busy.elapsed", "Busy \%", "The percentage of wall clock time where the TestBot was busy running jobs.");
+
+  print "<tr><td class='StatSeparator'>Job times</td><td colspan='2'><hr></td></tr>\n";
+  _GenGlobalLine($GlobalStats, "jobs.time.p10", "10%", "10% of the jobs completed within this time.");
+  _GenGlobalLine($GlobalStats, "jobs.time.p50", "50%", "50% of the jobs completed within this time.");
+  _GenGlobalLine($GlobalStats, "jobs.time.p90", "90%", "90% of the jobs completed within this time.");
+  _GenGlobalLine($GlobalStats, "jobs.time.max", "Max", "The slowest job took this long. Note that this is heavily influenced by test storms.");
+
+  print "<tr><td class='StatSeparator'>Average times</td><td colspan='2'><hr></td></tr>\n";
+  _GenGlobalLine($GlobalStats, "jobs.time", "Job completion", "How long it takes to complete a  regular job (excluding canceled ones). Note that this is heavily influenced by test storms.");
+  _GenGlobalLine($GlobalStats, "reconfig.time", "Wine update", "How long the daily Wine update takes.");
+  _GenGlobalLine($GlobalStats, "suite.time", "WineTest", "Average time for a WineTest run.");
+  _GenGlobalLine($GlobalStats, "build.time", "Build", "Average patch build time.");
+  _GenGlobalLine($GlobalStats, "single.time", "Test", "Average test run time. Note that this very much depends on the tests and how many time out on a given day.");
+
+  print "<tr><td class='StatSeparator'>WineTest reports</td><td colspan='2'><hr></td></tr>\n";
+  _GenGlobalLine($GlobalStats, "suite.size", "Average size", "Average WineTest report size.");
+  _GenGlobalLine($GlobalStats, "suite.size.max", "Max size", "Maximum WineTest report size.");
+
+  print "<tr><td class='StatSeparator'>Errors</td><td colspan='2'><hr></td></tr>\n";
+  _GenGlobalLine($GlobalStats, "timeout.count", "Timeouts", "How many timeouts occurred, either because of a test bug or a TestBot performance issue.");
+  _GenGlobalLine($GlobalStats, "boterror.count", "TestBot errors", "How many tasks failed due to a TestBot error.");
+  _GenGlobalLine($GlobalStats, "error.count", "Transient errors", "How many transient (network?) errors happened and caused the task to be re-run.");
+
+  print "<tr><td class='StatSeparator'>Activity</td><td colspan='2'><hr></td></tr>\n";
+  my $VMsStats = $Stats->{vms};
+  _GenGlobalLine($VMsStats, "elapsed", "Activity history", "How far the activity records go. This is used for the VM and VM host tables.");
+  _GenGlobalLine($GlobalStats, "records.count", "Record count", "The number of activity records.");
+
+  print "</tbody></table></div>\n";
+
+  ### Generate a table with the VM host statistics
+
+  print "<p></p>\n";
+  print "<h2>VM host statistics</h2>\n";
+  print "<div class='CollectionBlock'><table>\n";
+
+  print "<thead><tr><th>Stat</th>\n";
+  my $HostsStats = $Stats->{hosts};
+  my $SortedHosts = [ sort keys %{$Stats->{hosts}->{items}} ];
+  foreach my $Host (@$SortedHosts)
+  {
+    my $DisplayHost = $Host;
+    if ($PrettyHostNames and defined $PrettyHostNames->{$Host})
+    {
+      $DisplayHost = $PrettyHostNames->{$Host};
+    }
+    $DisplayHost ||= "localhost";
+    print "<th>$DisplayHost</th>\n";
+
+    _AddRate($HostsStats->{items}->{$Host}, "reverting.time.count", $HostsStats);
+    _AddRate($HostsStats->{items}->{$Host}, "running.time.count", $HostsStats);
+  }
+  print "</tr></thead>\n";
+
+  print "<tbody>\n";
+  _GenStatsLine($HostsStats, "reverting.time.count", "Revert count", $SortedHosts);
+  _GenStatsLine($HostsStats, "reverting.rate", "Revert rate", $SortedHosts);
+  _GenStatsLine($HostsStats, "running.time.count", "Task count", $SortedHosts);
+  _GenStatsLine($HostsStats, "running.rate", "Task rate", $SortedHosts);
+  _GenStatsLine($HostsStats, "busy.elapsed", "Busy time", $SortedHosts, $NO_PERCENTAGE);
+  _GenStatsLine($HostsStats, "busy.elapsed", "Busy \%", $SortedHosts);
+
+  print "<tr><td class='StatSeparator'>Average times</td><td colspan='", scalar(@$SortedHosts),"'><hr></td></tr>\n";
+  _GenStatsLine($HostsStats, "reverting.time", "Revert", $SortedHosts);
+  _GenStatsLine($HostsStats, "sleeping.time", "Sleep", $SortedHosts);
+  _GenStatsLine($HostsStats, "running.time", "Run", $SortedHosts);
+  _GenStatsLine($HostsStats, "dirty.time", "Dirty", $SortedHosts);
+  _GenStatsLine($HostsStats, "offline.time", "Offline", $SortedHosts);
+  _GenStatsLine($HostsStats, "suite.time", "WineTest", $SortedHosts);
+
+  print "<tr><td class='StatSeparator'>Maximum times</td><td colspan='", scalar(@$SortedHosts),"'><hr></td></tr>\n";
+  _GenStatsLine($HostsStats, "reverting.time.max", "Revert", $SortedHosts);
+  _GenStatsLine($HostsStats, "sleeping.time.max", "Sleep", $SortedHosts);
+  _GenStatsLine($HostsStats, "running.time.max", "Run", $SortedHosts);
+  _GenStatsLine($HostsStats, "dirty.time.max", "Dirty", $SortedHosts);
+  _GenStatsLine($HostsStats, "offline.time.max", "Offline", $SortedHosts);
+  _GenStatsLine($HostsStats, "suite.time.max", "WineTest", $SortedHosts);
+
+  print "<tr><td class='StatSeparator'>Errors</td><td colspan='", scalar(@$SortedHosts),"'><hr></td></tr>\n";
+  _GenStatsLine($HostsStats, "timeout.count", "Timeouts", $SortedHosts);
+  _GenStatsLine($HostsStats, "boterror.count", "TestBot errors", $SortedHosts);
+  _GenStatsLine($HostsStats, "error.count", "Transient errors", $SortedHosts);
+
+  print "</tbody></table></div>\n";
+
+  ### Generate a table with the VM statistics
+
+  print "<p></p>\n";
+  print "<h2>VM statistics</h2>\n";
+  print "<div class='CollectionBlock'><table>\n";
+
+  print "<thead><tr><th>Stat</th>\n";
+  my $SortedVMKeys;
+  foreach my $VM (@SortedVMs)
+  {
+    my $Host = $VM->GetHost();
+    if ($PrettyHostNames and defined $PrettyHostNames->{$Host})
+    {
+      $Host = $PrettyHostNames->{$Host};
+    }
+    $Host = " on $Host" if ($Host ne "");
+    print "<th>", $VM->Name, "$Host</th>\n";
+    push @$SortedVMKeys, $VM->Name;
+
+    _AddRate($VMsStats->{items}->{$VM->Name}, "reverting.time.count", $VMsStats);
+    _AddRate($VMsStats->{items}->{$VM->Name}, "running.time.count", $VMsStats);
+  }
+  print "</tr></thead>\n";
+
+  print "<tbody>\n";
+  _GenStatsLine($VMsStats, "reverting.time.count", "Revert count", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "reverting.rate", "Revert rate", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "running.time.count", "Task count", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "running.rate", "Task rate", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "busy.elapsed", "Busy time", $SortedVMKeys, $NO_PERCENTAGE);
+  _GenStatsLine($VMsStats, "busy.elapsed", "Busy \%", $SortedVMKeys);
+
+  print "<tr><td class='StatSeparator'>Average times</td><td colspan='", scalar(@$SortedVMKeys),"'><hr></td></tr>\n";
+  _GenStatsLine($VMsStats, "reverting.time", "Revert", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "sleeping.time", "Sleep", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "running.time", "Run", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "dirty.time", "Dirty", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "offline.time", "Offline", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "suite.time", "WineTest", $SortedVMKeys);
+
+  print "<tr><td class='StatSeparator'>Maximum times</td><td colspan='", scalar(@$SortedVMKeys),"'><hr></td></tr>\n";
+  _GenStatsLine($VMsStats, "reverting.time.max", "Revert", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "sleeping.time.max", "Sleep", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "running.time.max", "Run", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "dirty.time.max", "Dirty", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "offline.time.max", "Offline", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "suite.time.max", "WineTest", $SortedVMKeys);
+
+  print "<tr><td class='StatSeparator'>WineTest/Reconfig reports</td><td colspan='", scalar(@$SortedVMKeys),"'><hr></td></tr>\n";
+  _GenStatsLine($VMsStats, "report.size", "Average size", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "report.size.max", "Max size", $SortedVMKeys);
+
+  print "<tr><td class='StatSeparator'>Errors</td><td colspan='", scalar(@$SortedVMKeys),"'><hr></td></tr>\n";
+  _GenStatsLine($VMsStats, "timeout.count", "Timeouts", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "boterror.count", "TestBot errors", $SortedVMKeys);
+  _GenStatsLine($VMsStats, "error.count", "Transient errors", $SortedVMKeys);
+
+  print "</tbody></table></div>\n";
+  print "<p class='GeneralFooterText'>Generated in ", Elapsed($self->{start}), " s</p>\n";
+}
+
+package main;
+
+my $Request = shift;
+
+my $StatsPage = StatsPage->new($Request, "wine-devel");
+$StatsPage->GeneratePage();
diff --git a/testbot/web/WineTestBot.css b/testbot/web/WineTestBot.css
index 4afbe31c..2ccce246 100644
--- a/testbot/web/WineTestBot.css
+++ b/testbot/web/WineTestBot.css
@@ -376,3 +376,5 @@ td.Record { text-align: center; }
 .Record.Record-error { border-left: thick solid #990000; border-right: thick solid #990000; }
 .Record.Record-timeout { border-left: thick solid blue; border-right: thick solid blue; }
 .Record.Record-miss { border-top: thick dashed #ff6600; }
+
+td.StatSeparator { color: #601919; font-weight: bold; }
-- 
2.15.1



More information about the wine-devel mailing list