<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[15678] CalendarServer/trunk/calendarserver/tools/dashtime.py</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/15678">15678</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2016-06-16 13:34:40 -0700 (Thu, 16 Jun 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add tool to do time-based plots of various dashboard stats.</pre>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolsdashtimepy">CalendarServer/trunk/calendarserver/tools/dashtime.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolsdashtimepy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/calendarserver/tools/dashtime.py (0 => 15678)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/dashtime.py                                (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/dashtime.py        2016-06-16 20:34:40 UTC (rev 15678)
</span><span class="lines">@@ -0,0 +1,551 @@
</span><ins>+#!/usr/bin/env python
+##
+# Copyright (c) 2015-2016 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+&quot;&quot;&quot;
+Tool that extracts time series data from a dashcollect log.
+&quot;&quot;&quot;
+
+from bz2 import BZ2File
+from collections import OrderedDict, defaultdict
+import argparse
+import json
+import matplotlib.pyplot as plt
+import operator
+import os
+
+
+verbose = False
+def _verbose(log):
+    if verbose:
+        print(log)
+
+
+
+def safeDivision(value, total, factor=1):
+    return value * factor / total if total else 0
+
+
+
+class DataType(object):
+    &quot;&quot;&quot;
+    Base class for object that can process the different types of data in a
+    dashcollect log.
+    &quot;&quot;&quot;
+
+    allTypes = OrderedDict()
+    key = &quot;&quot;
+
+    # This indicates whether the class of data is based on a 1 minute average -
+    # which means the data represents a 60 second delay compared to the &quot;real-
+    # time&quot; value. If it is the average then setting this flag will cause the
+    # first 60 data items to be skipped.
+    skip60 = False
+
+
+    @staticmethod
+    def getTitle(measurement):
+        if &quot;-&quot; in measurement:
+            measurement, item = measurement.split(&quot;-&quot;, 1)
+        else:
+            item = &quot;&quot;
+        return DataType.allTypes[measurement].title(item)
+
+
+    @staticmethod
+    def skip(measurement):
+        if &quot;-&quot; in measurement:
+            measurement = measurement.split(&quot;-&quot;, 1)[0]
+        return DataType.allTypes[measurement].skip60
+
+
+    @staticmethod
+    def process(measurement, stats, host):
+        if &quot;-&quot; in measurement:
+            measurement, item = measurement.split(&quot;-&quot;, 1)
+        else:
+            item = &quot;&quot;
+        return DataType.allTypes[measurement].calculate(stats, item, host)
+
+
+    @staticmethod
+    def title(item):
+        raise NotImplementedError
+
+
+    @staticmethod
+    def maxY(stats, item):
+        raise NotImplementedError
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        &quot;&quot;&quot;
+        If hosts is L{None} then data from all hosts will be aggregated.
+
+        @param stats: per-Pod L{dict} of data from each host in the pod.
+        @type stats: L{dict}
+        @param item: additional L{dict} key for data of interest
+        @type item: L{str}
+        @param hosts: list of hosts to process
+        @type hosts: L{list}
+        &quot;&quot;&quot;
+        raise NotImplementedError
+
+
+
+class CPUDataType(DataType):
+    &quot;&quot;&quot;
+    CPU use.
+    &quot;&quot;&quot;
+
+    key = &quot;cpu&quot;
+
+    @staticmethod
+    def title(item):
+        return &quot;CPU Use %&quot;
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return 100 * len(stats) if host is None else 100
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        return sum([stats[onehost][&quot;stats_system&quot;][&quot;cpu use&quot;] for onehost in hosts])
+
+
+
+class RequestsDataType(DataType):
+    &quot;&quot;&quot;
+    Number of requests.
+    &quot;&quot;&quot;
+
+    key = &quot;reqs&quot;
+    skip60 = True
+
+    @staticmethod
+    def title(item):
+        return &quot;Requests/sec&quot;
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        return sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;requests&quot;] for onehost in hosts]) / 60.0
+
+
+
+class ResponseDataType(DataType):
+    &quot;&quot;&quot;
+    Average response time.
+    &quot;&quot;&quot;
+
+    key = &quot;respt&quot;
+    skip60 = True
+
+    @staticmethod
+    def title(item):
+        return &quot;Av. Response Time (ms)&quot;
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        tsum = sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;t&quot;] for onehost in hosts])
+        rsum = sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;requests&quot;] for onehost in hosts])
+        return safeDivision(tsum, rsum)
+
+
+
+class JobsCompletedDataType(DataType):
+    &quot;&quot;&quot;
+    Job completion count from job assignments.
+    &quot;&quot;&quot;
+
+    key = &quot;jcomp&quot;
+
+    lastCompleted = defaultdict(int)
+
+    @staticmethod
+    def title(item):
+        return &quot;Completed&quot;
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        result = 0
+        for onehost in hosts:
+            completed = sum(map(operator.itemgetter(2), stats[onehost][&quot;job_assignments&quot;][&quot;workers&quot;]))
+            result += completed - JobsCompletedDataType.lastCompleted[onehost] if JobsCompletedDataType.lastCompleted[onehost] else 0
+            JobsCompletedDataType.lastCompleted[onehost] = completed
+        return result
+
+
+
+class MethodCountDataType(DataType):
+    &quot;&quot;&quot;
+    Count of specified methods. L{item} should be set to the full name of the
+    &quot;decorated&quot; method seen in dashview.
+    &quot;&quot;&quot;
+
+    key = &quot;methodc&quot;
+    skip60 = True
+
+    @staticmethod
+    def title(item):
+        return item
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        return sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;method&quot;].get(item, 0) for onehost in hosts])
+
+
+
+class MethodResponseDataType(DataType):
+    &quot;&quot;&quot;
+    Average response time of specified methods. L{item} should be set to the
+    full name of the &quot;decorated&quot; method seen in dashview.
+    &quot;&quot;&quot;
+
+    key = &quot;methodr&quot;
+    skip60 = True
+
+    @staticmethod
+    def title(item):
+        return item
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        if hosts is None:
+            hosts = stats.keys()
+        tsum = sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;method-t&quot;].get(item, 0) for onehost in hosts])
+        rsum = sum([stats[onehost][&quot;stats&quot;][&quot;1m&quot;][&quot;method&quot;].get(item, 0) for onehost in hosts])
+        return safeDivision(tsum, rsum)
+
+
+
+class JobQueueDataType(DataType):
+    &quot;&quot;&quot;
+    Count of queued job items. L{item} should be set to the full name or prefix
+    of job types to process. Or if set to L{None}, all jobs are counted.
+    &quot;&quot;&quot;
+
+    key = &quot;jqueue&quot;
+
+    @staticmethod
+    def title(item):
+        return (&quot;JQ &quot; + &quot;_&quot;.join(map(operator.itemgetter(0), item.split(&quot;_&quot;)))) if item else &quot;Jobs Queued&quot;
+
+
+    @staticmethod
+    def maxY(stats, item, host):
+        return None
+
+
+    @staticmethod
+    def calculate(stats, item, hosts):
+        onehost = sorted(stats.keys())[0]
+
+        if item:
+            return sum(map(operator.itemgetter(&quot;queued&quot;), {k: v for k, v in stats[onehost][&quot;jobs&quot;].items() if k.startswith(item)}.values()))
+        else:
+            return sum(map(operator.itemgetter(&quot;queued&quot;), stats[onehost][&quot;jobs&quot;].values()))
+
+
+# Register the known L{DataType}s
+for dtype in DataType.__subclasses__():
+    DataType.allTypes[dtype.key] = dtype
+
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=&quot;Dashboard time series processor.&quot;,
+        epilog=&quot;cpu - CPU use\nreqs - requests per second\nrespt - average response time&quot;,
+    )
+    parser.add_argument(&quot;-l&quot;, help=&quot;Log file to process&quot;)
+    parser.add_argument(&quot;-p&quot;, help=&quot;Name of pod to analyze&quot;)
+    parser.add_argument(&quot;-s&quot;, help=&quot;Name of server to analyze&quot;)
+    parser.add_argument(&quot;-v&quot;, action=&quot;store_true&quot;, help=&quot;Verbose&quot;)
+    args = parser.parse_args()
+    if args.v:
+        global verbose
+        verbose = True
+
+    # Get the log file
+    try:
+        if args.l.endswith(&quot;.bz2&quot;):
+            logfile = BZ2File(os.path.expanduser(args.l))
+        else:
+            logfile = open(os.path.expanduser(args.l))
+    except:
+        print(&quot;Failed to open logfile {}&quot;.format(args.l))
+
+    # Start/end lines in log file to process
+    line_start = 0
+    line_count = 10000
+
+    # Plot arrays that will be generated
+    x = []
+    y = OrderedDict()
+    titles = {}
+    ymaxes = {}
+
+    def singleHost(valuekeys):
+        &quot;&quot;&quot;
+        Generate data for a single host only.
+
+        @param valuekeys: L{DataType} keys to process
+        @type valuekeys: L{list} or L{str}
+        &quot;&quot;&quot;
+        _plotHosts(valuekeys, (args.s,))
+
+
+    def combinedHosts(valuekeys):
+        &quot;&quot;&quot;
+        Generate data for all hosts.
+
+        @param valuekeys: L{DataType} keys to process
+        @type valuekeys: L{list} or L{str}
+        &quot;&quot;&quot;
+        _plotHosts(valuekeys, None)
+
+
+    def _plotHosts(valuekeys, hosts):
+        &quot;&quot;&quot;
+        Generate data for a the specified list of hosts.
+
+        @param valuekeys: L{DataType} keys to process
+        @type valuekeys: L{list} or L{str}
+        @param hosts: lists of hosts to process
+        @type hosts: L{list} or L{str}
+        &quot;&quot;&quot;
+
+        # Initialize the plot arrays
+        for measurement in valuekeys:
+            y[measurement] = []
+            titles[measurement] = DataType.getTitle(measurement)
+            ymaxes[measurement] = None
+
+        # For each log file line, process the data for each required measurement
+        with logfile:
+            line = logfile.readline()
+            ctr = 0
+            while line:
+                if ctr &lt; line_start:
+                    ctr += 1
+                    line = logfile.readline()
+                    continue
+
+                if line[0] == &quot;\x1e&quot;:
+                    line = line[1:]
+                jline = json.loads(line)
+
+                x.append(ctr)
+                ctr += 1
+
+                for measurement in valuekeys:
+                    stats = jline[&quot;pods&quot;][args.p]
+                    y[measurement].append(DataType.process(measurement, stats, hosts))
+
+                line = logfile.readline()
+                if ctr &gt; line_start + line_count:
+                    break
+
+        # Offset data that is averaged over the previous minute
+        for measurement in valuekeys:
+            if DataType.skip(measurement):
+                y[measurement] = y[measurement][60:]
+                y[measurement].extend([None] * 60)
+
+
+    def perHost(perhostkeys, combinedkeys):
+        &quot;&quot;&quot;
+        Generate a set of per-host plots, together we a set of plots for all-
+        host data.
+
+        @param perhostkeys: L{DataType} keys for per-host data to process
+        @type perhostkeys: L{list} or L{str}
+        @param combinedkeys: L{DataType} keys for all-host data to process
+        @type combinedkeys: L{list} or L{str}
+        &quot;&quot;&quot;
+
+        # For each log file line, process the data for each required measurement
+        with logfile:
+            line = logfile.readline()
+            ctr = 0
+            while line:
+                if ctr &lt; line_start:
+                    ctr += 1
+                    line = logfile.readline()
+                    continue
+
+                if line[0] == &quot;\x1e&quot;:
+                    line = line[1:]
+                jline = json.loads(line)
+
+                x.append(ctr)
+                ctr += 1
+
+                # Initialize the plot arrays when we know how many hosts there are
+                if len(y) == 0:
+                    hosts = sorted(jline[&quot;pods&quot;][args.p].keys())
+
+                    for host in hosts:
+                        for measurement in perhostkeys:
+                            ykey = &quot;{}={}&quot;.format(measurement, host)
+                            y[ykey] = []
+                            titles[ykey] = DataType.getTitle(measurement)
+                            ymaxes[ykey] = None
+
+                    for measurement in combinedkeys:
+                        y[measurement] = []
+                        titles[measurement] = DataType.getTitle(measurement)
+                        ymaxes[measurement] = None
+
+                # Get actual measurement data
+                for host in hosts:
+                    for measurement in perhostkeys:
+                        ykey = &quot;{}={}&quot;.format(measurement, host)
+                        stats = jline[&quot;pods&quot;][args.p]
+                        y[ykey].append(DataType.process(measurement, stats, (host,)))
+
+                for measurement in combinedkeys:
+                    stats = jline[&quot;pods&quot;][args.p]
+                    y[measurement].append(DataType.process(measurement, stats, hosts))
+
+                line = logfile.readline()
+                if ctr &gt; line_start + line_count:
+                    break
+
+        # Offset data that is averaged over the previous minute
+        for host in hosts:
+            for measurement in perhostkeys:
+                if DataType.skip(measurement):
+                    ykey = &quot;{}={}&quot;.format(measurement, host)
+                    y[ykey] = y[ykey][60:]
+                    y[ykey].extend([None] * 60)
+        for measurement in combinedkeys:
+            if DataType.skip(measurement):
+                y[measurement] = y[measurement][60:]
+                y[measurement].extend([None] * 60)
+
+
+#    singleHost((
+#        CPUDataType.key,
+#        RequestsDataType.key,
+#        ResponseDataType.key,
+#        JobsCompletedDataType.key,
+#        JobQueueDataType.key + &quot;-SCHEDULE&quot;,
+#        JobQueueDataType.key + &quot;-PUSH&quot;,
+#        JobQueueDataType.key,
+#    ))
+#    combinedHosts((
+#        CPUDataType.key,
+#        RequestsDataType.key,
+#        ResponseDataType.key,
+#        JobsCompletedDataType.key,
+#        JobQueueDataType.key + &quot;-SCHEDULE&quot;,
+#        JobQueueDataType.key + &quot;-PUSH&quot;,
+#        JobQueueDataType.key,
+#    ))
+    combinedHosts((
+        CPUDataType.key,
+        RequestsDataType.key,
+        ResponseDataType.key,
+        MethodCountDataType.key + &quot;-PUT ics&quot;,
+        MethodCountDataType.key + &quot;-REPORT cal-home-sync&quot;,
+        MethodCountDataType.key + &quot;-PROPFIND Calendar Home&quot;,
+        MethodCountDataType.key + &quot;-REPORT cal-sync&quot;,
+        MethodCountDataType.key + &quot;-PROPFIND Calendar&quot;,
+    ))
+#    perHost((
+#        RequestsDataType.key,
+#    ), (
+#        CPUDataType.key,
+#        JobQueueDataType.key,
+#    ))
+
+    # Generate a single stacked plot of the data
+    for plotnum, measurement in enumerate(y.keys()):
+        plt.subplot(len(y), 1, plotnum + 1)
+        plotSeries(titles[measurement], x, y[measurement], 0, ymaxes[measurement])
+    plt.show()
+
+
+
+def plotSeries(title, x, y, ymin=None, ymax=None):
+    &quot;&quot;&quot;
+    Plot the chosen dataset key for each scanned data file.
+
+    @param key: data set key to use
+    @type key: L{str}
+    @param ymin: minimum value for y-axis or L{None} for default
+    @type ymin: L{int} or L{float}
+    @param ymax: maximum value for y-axis or L{None} for default
+    @type ymax: L{int} or L{float}
+    &quot;&quot;&quot;
+
+    plt.plot(x, y)
+
+    plt.xlabel(&quot;Time&quot;)
+    plt.ylabel(title)
+    if ymin is not None:
+        plt.ylim(ymin=ymin)
+    if ymax is not None:
+        plt.ylim(ymax=ymax)
+    plt.minorticks_on()
+    plt.grid(True, &quot;major&quot;, &quot;x&quot;, alpha=0.5, linewidth=0.5)
+    plt.grid(True, &quot;minor&quot;, &quot;x&quot;, alpha=0.5, linewidth=0.5)
+
+if __name__ == &quot;__main__&quot;:
+    main()
</ins><span class="cx">Property changes on: CalendarServer/trunk/calendarserver/tools/dashtime.py
</span><span class="cx">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4>Added: svn:executable</h4></div>
</div>

</body>
</html>