QueueStatusServer needs pages to display historical queue data
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Jan 2013 18:07:52 +0000 (18:07 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Jan 2013 18:07:52 +0000 (18:07 +0000)
https://bugs.webkit.org/show_bug.cgi?id=107659

Patch by Alan Cutter <alancutter@chromium.org> on 2013-01-29
Reviewed by Eric Seidel.

Created a /queue-charts/<queue-name> handler to present queue and patch data using Google Chart Tools.

* QueueStatusServer/app.yaml:
* QueueStatusServer/config/charts.py: Copied from Tools/QueueStatusServer/model/queuelog.py.
(get_time_unit):
* QueueStatusServer/filters/webkit_extras.py:
(webkit_linkify):
(webkit_bug_id):
(webkit_attachment_id):
(results_link):
(queue_status_link):
(queue_charts_link):
* QueueStatusServer/handlers/queuecharts.py: Added.
(QueueCharts):
(QueueCharts.get):
(QueueCharts._get_min_med_max):
(QueueCharts._get_patch_data):
(QueueCharts._get_patch_logs):
(QueueCharts._get_queue_data):
(QueueCharts._get_queue_logs):
(QueueCharts._get_time_unit):
(QueueCharts._get_timestamp):
(QueueCharts._get_view_range):
* QueueStatusServer/handlers/queuestatus.py:
(QueueStatus.get):
* QueueStatusServer/index.yaml:
* QueueStatusServer/main.py:
* QueueStatusServer/model/queuelog.py:
(QueueLog):
(QueueLog.create_key):
(QueueLog.get_at):
(QueueLog.get_current):
(QueueLog.get_or_create):
(QueueLog._get_or_create_txn):
* QueueStatusServer/stylesheets/charts.css: Added.
(.chart):
(.choices):
* QueueStatusServer/templates/queuecharts.html: Added.
* QueueStatusServer/templates/queuestatus.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@141129 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Tools/ChangeLog
Tools/QueueStatusServer/app.yaml
Tools/QueueStatusServer/config/charts.py [new file with mode: 0644]
Tools/QueueStatusServer/filters/webkit_extras.py
Tools/QueueStatusServer/handlers/queuecharts.py [new file with mode: 0644]
Tools/QueueStatusServer/handlers/queuestatus.py
Tools/QueueStatusServer/index.yaml
Tools/QueueStatusServer/main.py
Tools/QueueStatusServer/model/queuelog.py
Tools/QueueStatusServer/stylesheets/charts.css [new file with mode: 0644]
Tools/QueueStatusServer/templates/queuecharts.html [new file with mode: 0644]
Tools/QueueStatusServer/templates/queuestatus.html

index 3b1f36c..6e9516f 100644 (file)
@@ -1,3 +1,50 @@
+2013-01-29  Alan Cutter  <alancutter@chromium.org>
+
+        QueueStatusServer needs pages to display historical queue data
+        https://bugs.webkit.org/show_bug.cgi?id=107659
+
+        Reviewed by Eric Seidel.
+
+        Created a /queue-charts/<queue-name> handler to present queue and patch data using Google Chart Tools.
+
+        * QueueStatusServer/app.yaml:
+        * QueueStatusServer/config/charts.py: Copied from Tools/QueueStatusServer/model/queuelog.py.
+        (get_time_unit):
+        * QueueStatusServer/filters/webkit_extras.py:
+        (webkit_linkify):
+        (webkit_bug_id):
+        (webkit_attachment_id):
+        (results_link):
+        (queue_status_link):
+        (queue_charts_link):
+        * QueueStatusServer/handlers/queuecharts.py: Added.
+        (QueueCharts):
+        (QueueCharts.get):
+        (QueueCharts._get_min_med_max):
+        (QueueCharts._get_patch_data):
+        (QueueCharts._get_patch_logs):
+        (QueueCharts._get_queue_data):
+        (QueueCharts._get_queue_logs):
+        (QueueCharts._get_time_unit):
+        (QueueCharts._get_timestamp):
+        (QueueCharts._get_view_range):
+        * QueueStatusServer/handlers/queuestatus.py:
+        (QueueStatus.get):
+        * QueueStatusServer/index.yaml:
+        * QueueStatusServer/main.py:
+        * QueueStatusServer/model/queuelog.py:
+        (QueueLog):
+        (QueueLog.create_key):
+        (QueueLog.get_at):
+        (QueueLog.get_current):
+        (QueueLog.get_or_create):
+        (QueueLog._get_or_create_txn):
+        * QueueStatusServer/stylesheets/charts.css: Added.
+        (.chart):
+        (.choices):
+        * QueueStatusServer/templates/queuecharts.html: Added.
+        * QueueStatusServer/templates/queuestatus.html:
+
 2013-01-29  Mario Sanchez Prada  <mario.prada@samsung.com>
 
         [GTK] Missing build flags when building with Harfbuzz
index 1019461..53965be 100644 (file)
@@ -1,5 +1,5 @@
 application: webkit-commit-queue
-version: 107775 # Bugzilla bug ID of last major change
+version: 107659 # Bugzilla bug ID of last major change
 runtime: python
 api_version: 1
 
diff --git a/Tools/QueueStatusServer/config/charts.py b/Tools/QueueStatusServer/config/charts.py
new file mode 100644 (file)
index 0000000..3e38a85
--- /dev/null
@@ -0,0 +1,61 @@
+# Copyright (C) 2013 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+patch_log_limit = 500
+
+# All units are represented numerically as seconds.
+one_minute = 60.0
+one_hour = one_minute * 60.0
+one_day = one_hour * 24.0
+one_month = one_day * 30.0
+
+# How far back to view the history, specified in seconds.
+view_range_choices = [
+    {"name": "1 day", "view_range": one_day},
+    {"name": "1 week", "view_range": one_day * 7},
+    {"name": "1 month", "view_range": one_month},
+]
+
+default_view_range = one_day
+
+_time_units = [
+    #(threshold, time unit, name)
+    (0, one_hour, "hours"),
+    (4 * one_day, one_day, "days"),
+    (3 * one_month, one_month, "months"),
+]
+
+
+def get_time_unit(view_range):
+    current_threshold, current_time_unit, current_name = _time_units[0]
+    for threshold, time_unit, name in _time_units[1:]:
+        if view_range >= threshold:
+            current_time_unit, current_name = time_unit, name
+        else:
+            break
+    return current_time_unit, current_name
index b645f78..5b3c9b1 100644 (file)
@@ -36,6 +36,7 @@ register = webapp.template.create_template_register()
 bug_regexp = re.compile(r"bug (?P<bug_id>\d+)")
 patch_regexp = re.compile(r"patch (?P<patch_id>\d+)")
 
+
 @register.filter
 @stringfilter
 def webkit_linkify(value):
@@ -43,17 +44,32 @@ def webkit_linkify(value):
     value = patch_regexp.sub(r'<a href="https://bugs.webkit.org/attachment.cgi?id=\g<patch_id>&action=prettypatch">patch \g<patch_id></a>', value)
     return value
 
+
 @register.filter
 @stringfilter
 def webkit_bug_id(value):
     return '<a href="http://webkit.org/b/%s">%s</a>' % (value, value)
 
+
 @register.filter
 @stringfilter
 def webkit_attachment_id(value):
     return '<a href="https://bugs.webkit.org/attachment.cgi?id=%s&action=prettypatch">%s</a>' % (value, value)
 
+
 @register.filter
 @stringfilter
 def results_link(status_id):
     return '<a href="/results/%s">results</a>' % status_id
+
+
+@register.filter
+@stringfilter
+def queue_status_link(queue_name, text):
+    return '<a href="/queue-status/%s">%s</a>' % (queue_name, text)
+
+
+@register.filter
+@stringfilter
+def queue_charts_link(queue_name, text):
+    return '<a href="/queue-charts/%s">%s</a>' % (queue_name, text)
diff --git a/Tools/QueueStatusServer/handlers/queuecharts.py b/Tools/QueueStatusServer/handlers/queuecharts.py
new file mode 100644 (file)
index 0000000..6611027
--- /dev/null
@@ -0,0 +1,151 @@
+# Copyright (C) 2013 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import calendar
+from datetime import datetime
+import itertools
+from time import time
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+from config import logging, charts
+from model.patchlog import PatchLog
+from model.queues import Queue
+from model.queuelog import QueueLog
+
+
+class QueueCharts(webapp.RequestHandler):
+    def get(self, queue_name):
+        queue_name = queue_name.lower()
+        if not Queue.queue_with_name(queue_name):
+            self.error(404)
+            return
+
+        timestamp = self._get_timestamp()
+        view_range = self._get_view_range()
+        time_unit, time_unit_name = charts.get_time_unit(view_range)
+
+        all_queue_names = map(Queue.name, Queue.all())
+
+        template_values = {
+            "all_queue_names": all_queue_names,
+            "patch_data": self._get_patch_data(queue_name, timestamp, view_range),
+            "queue_data": self._get_queue_data(queue_name, timestamp, view_range),
+            "queue_name": queue_name,
+            "seconds_ago_min": 0,
+            "seconds_ago_max": view_range,
+            "time_unit_name": time_unit_name,
+            "time_unit": time_unit,
+            "timestamp": timestamp,
+            "view_range": view_range,
+            "view_range_choices": charts.view_range_choices,
+        }
+        self.response.out.write(template.render("templates/queuecharts.html", template_values))
+
+    @classmethod
+    def _get_min_med_max(cls, values, defaults=(0, 0, 0)):
+        if not values:
+            return defaults
+        length = len(values)
+        sorted_values = sorted(values)
+        return sorted_values[0], sorted_values[length / 2], sorted_values[length - 1]
+
+    def _get_patch_data(self, queue_name, timestamp, view_range):
+        patch_logs = self._get_patch_logs(queue_name, timestamp, view_range)
+        patch_data = []
+        for patch_log in patch_logs:
+            if patch_log.process_duration and patch_log.wait_duration:
+                patch_log_timestamp = calendar.timegm(patch_log.date.utctimetuple())
+                patch_data.append({
+                    "attachment_id": patch_log.attachment_id,
+                    "seconds_ago": timestamp - patch_log_timestamp,
+                    "process_duration": patch_log.process_duration / charts.one_minute,
+                    "retry_count": patch_log.retry_count,
+                    "status_update_count": patch_log.status_update_count,
+                    "wait_duration": patch_log.wait_duration / charts.one_minute,
+               })
+        return patch_data
+
+    def _get_patch_logs(self, queue_name, timestamp, view_range):
+        patch_log_query = PatchLog.all()
+        patch_log_query = patch_log_query.filter("queue_name =", queue_name)
+        patch_log_query = patch_log_query.filter("date >=", datetime.utcfromtimestamp(timestamp - view_range))
+        patch_log_query = patch_log_query.filter("date <=", datetime.utcfromtimestamp(timestamp))
+        patch_log_query = patch_log_query.order("date")
+        return patch_log_query.run(limit=charts.patch_log_limit)
+
+    def _get_queue_data(self, queue_name, timestamp, view_range):
+        queue_logs = self._get_queue_logs(queue_name, timestamp, view_range)
+        queue_data = []
+        for queue_log in queue_logs:
+            queue_log_timestamp = calendar.timegm(queue_log.date.utctimetuple())
+            p_min, p_med, p_max = self._get_min_med_max(queue_log.patch_process_durations)
+            w_min, w_med, w_max = self._get_min_med_max(queue_log.patch_wait_durations)
+            queue_data.append({
+                "bots_seen": len(queue_log.bot_ids_seen),
+                "seconds_ago": timestamp - queue_log_timestamp,
+                "patch_processing_min": p_min,
+                "patch_processing_med": p_med,
+                "patch_processing_max": p_max,
+                "patch_retry_count": queue_log.patch_retry_count,
+                "patch_waiting_min": w_min,
+                "patch_waiting_med": w_med,
+                "patch_waiting_max": w_max,
+                "patches_completed": len(queue_log.patch_process_durations),
+                "patches_waiting": queue_log.max_patches_waiting,
+                "status_update_count": queue_log.status_update_count,
+            })
+        return queue_data
+
+    def _get_queue_logs(self, queue_name, timestamp, view_range):
+        queue_logs = []
+        current_timestamp = timestamp - view_range
+        while current_timestamp <= timestamp:
+            queue_logs.append(QueueLog.get_at(queue_name, logging.queue_log_duration, current_timestamp))
+            current_timestamp += logging.queue_log_duration
+        return queue_logs
+
+    @classmethod
+    def _get_time_unit(cls, view_range):
+        if view_range > charts.one_day * 2:
+            return 
+
+    def _get_timestamp(self):
+        timestamp = self.request.get("timestamp")
+        try:
+            return int(timestamp)
+        except ValueError:
+            return int(time())
+
+    def _get_view_range(self):
+        view_range = self.request.get("view_range")
+        try:
+            return int(view_range)
+        except ValueError:
+            return charts.default_view_range
index 9054fca..4f4e2d2 100644 (file)
@@ -104,6 +104,7 @@ class QueueStatus(webapp.RequestHandler):
 
         statuses = self._fetch_statuses(queue, bot_id)
         template_values = {
+            "queue_name": queue_name,
             "page_title": self._page_title(queue, bot_id),
             "work_item_rows": self._rows_for_work_items(queue),
             "status_groups": self._build_status_groups(statuses),
index 34eb72e..694f77b 100644 (file)
@@ -10,6 +10,11 @@ indexes:
 # automatically uploaded to the admin console when you next deploy
 # your application using appcfg.py.
 
+- kind: PatchLog
+  properties:
+  - name: queue_name
+  - name: date
+
 - kind: QueueStatus
   properties:
   - name: active_patch_id
index e1155ff..7ba59de 100644 (file)
@@ -39,6 +39,7 @@ from handlers.gc import GC
 from handlers.nextpatch import NextPatch
 from handlers.patch import Patch
 from handlers.patchstatus import PatchStatus
+from handlers.queuecharts import QueueCharts
 from handlers.queuestatus import QueueStatus
 from handlers.recentstatus import QueuesOverview
 from handlers.releasepatch import ReleasePatch
@@ -63,6 +64,7 @@ routes = [
     (r'/results/(.*)', ShowResults),
     (r'/status-bubble/(.*)', StatusBubble),
     (r'/svn-revision/(.*)', SVNRevision),
+    (r'/queue-charts/(.*)', QueueCharts),
     (r'/queue-status/(.*)/bots/(.*)', QueueStatus),
     (r'/queue-status/(.*)', QueueStatus),
     (r'/next-patch/(.*)', NextPatch),
index 829b8a6..98e3b1e 100644 (file)
@@ -44,14 +44,29 @@ class QueueLog(db.Model):
     patch_retry_count = db.IntegerProperty(default=0)
     status_update_count = db.IntegerProperty(default=0)
 
+    @staticmethod
+    def create_key(queue_name, duration, timestamp):
+        return "%s-%s-%s" % (queue_name, duration, timestamp)
+
     @classmethod
-    def get_current(cls, queue_name, duration):
-        timestamp_now = time()
-        timestamp = int(timestamp_now / duration) * duration
+    def get_at(cls, queue_name, duration, timestamp):
+        timestamp = int(timestamp / duration) * duration
         date = datetime.utcfromtimestamp(timestamp)
         key = cls.create_key(queue_name, duration, timestamp)
-        return cls.get_or_insert(key, date=date, duration=duration, queue_name=queue_name)
+        return cls.get_or_create(key, date=date, duration=duration, queue_name=queue_name)
 
-    @staticmethod
-    def create_key(queue_name, duration, timestamp):
-        return "%s-%s-%s" % (queue_name, duration, timestamp)
+    @classmethod
+    def get_current(cls, queue_name, duration):
+        return cls.get_at(queue_name, duration, time())
+
+    # This is to prevent page requests from generating lots of rows in the database.
+    @classmethod
+    def get_or_create(cls, key_name, **kwargs):
+        return db.run_in_transaction(cls._get_or_create_txn, key_name, **kwargs)
+
+    @classmethod
+    def _get_or_create_txn(cls, key_name, **kwargs):
+        entity = cls.get_by_key_name(key_name, parent=kwargs.get('parent'))
+        if entity is None:
+            entity = cls(key_name=key_name, **kwargs)
+        return entity
diff --git a/Tools/QueueStatusServer/stylesheets/charts.css b/Tools/QueueStatusServer/stylesheets/charts.css
new file mode 100644 (file)
index 0000000..b841b2c
--- /dev/null
@@ -0,0 +1,8 @@
+.chart {
+    margin-bottom: 40px;
+    height: 200px;
+}
+
+.choices {
+    font-size: 0.75em;
+}
\ No newline at end of file
diff --git a/Tools/QueueStatusServer/templates/queuecharts.html b/Tools/QueueStatusServer/templates/queuecharts.html
new file mode 100644 (file)
index 0000000..953debd
--- /dev/null
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>{{ queue_name }} Charts</title>
+        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
+        <link type="text/css" rel="stylesheet" href="/stylesheets/charts.css" />
+        <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+        <script type="text/javascript">
+            google.load('visualization', '1.0', {'packages':['corechart']});
+            google.setOnLoadCallback(function () {
+
+                function secondsToString(seconds) {
+                    var oneSecond = 1;
+                    var oneMinute = 60;
+                    var oneHour = oneMinute * 60;
+                    var oneDay = oneHour * 24;
+                    var oneYear = oneDay * 365.25;
+                    var unitArray = [
+                        [oneYear, "year"],
+                        [oneDay, "day"],
+                        [oneHour, "hour"],
+                        [oneMinute, "minute"],
+                        [oneSecond, "second"],
+                    ];
+                    var result = "";
+                    for (var i = 0; i < unitArray.length; i++) {
+                        var unit = unitArray[i][0];
+                        if (seconds >= unit) {
+                            if (result !== "") {
+                                result += " ";
+                            }
+                            var name = unitArray[i][1];
+                            var number = Math.floor(seconds/unit);
+                            result += number + " " + name + (number > 1 ? "s" : "");
+                            seconds %= unit;
+                        }
+                    }
+                    if (result === "") {
+                        return "0 seconds";
+                    }
+                    return result;
+                }
+
+                var data, chart, options;
+
+                var timeString = new Date({{ timestamp }} * 1000).toString();
+                var timestampDiv = document.getElementById("timestamp");
+                timestampDiv.innerHTML = "Viewing from " + timeString;
+
+                options = {
+                    legend: {position: "top"},
+                    hAxis: {
+                        title: "{{ time_unit_name|capfirst }} Ago",
+                        direction: -1,
+                        viewWindow: {
+                            min: {{ seconds_ago_min }} / {{ time_unit }},
+                            max: {{ seconds_ago_max }} / {{ time_unit }},
+                        },
+                        gridlines: {
+                            count: 9,
+                        },
+                    },
+                    vAxis: {
+                        viewWindow: {min: 0 },
+                    },
+                    lineWidth: 3,
+                };
+
+                // CHART 1
+                options.colors = ["green", "red", "orange"];
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Patches Completed");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn("number", "Patches Waiting");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn("number", "Bots (visible to server)");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addRows([
+                    {% for queue_datum in queue_data %}
+                        [
+                            {{ queue_datum.seconds_ago }} / {{ time_unit }},
+                            {{ queue_datum.patches_completed }},
+                            "Patches Completed: " + {{ queue_datum.patches_completed }} + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                            {{ queue_datum.patches_waiting }},
+                            "Patches Waiting: " + {{ queue_datum.patches_waiting }} + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                            {{ queue_datum.bots_seen }},
+                            "Bots (visible to server): " + {{ queue_datum.bots_seen }} + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.LineChart(document.getElementById('chart1'));
+                chart.draw(data, options);
+
+                // CHART 2
+                options.colors = ["blue", "purple"];
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Status Updates");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn("number", "Patch Retries");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addRows([
+                    {% for queue_datum in queue_data %}
+                        [
+                            {{ queue_datum.seconds_ago }} / {{ time_unit }},
+                            {{ queue_datum.status_update_count }},
+                            "Status Updates: " + {{ queue_datum.status_update_count }} + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                            {{ queue_datum.patch_retry_count }},
+                            "Patch Retries: " + {{ queue_datum.patch_retry_count }} + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.LineChart(document.getElementById('chart2'));
+                chart.draw(data, options);
+
+                // CHART 3
+                options.colors = ["brown"];
+                options.vAxis.title = "Minutes";
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Patch Processing Times");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn({type: "number", role: "interval"});
+                data.addColumn({type: "number", role: "interval"});
+                data.addRows([
+                    {% for queue_datum in queue_data %}
+                        [
+                            {{ queue_datum.seconds_ago }} / {{ time_unit }},
+                            {{ queue_datum.patch_processing_med }} / 60,
+                            "Patch Processing Times\nMax: " + secondsToString({{ queue_datum.patch_processing_max }}) + "\nMedian: " + secondsToString({{ queue_datum.patch_processing_med }}) + "\nMin: " + secondsToString({{ queue_datum.patch_processing_min }}) + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                            {{ queue_datum.patch_processing_min }} / 60,
+                            {{ queue_datum.patch_processing_max }} / 60,
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.LineChart(document.getElementById('chart3'));
+                chart.draw(data, options);
+
+                // CHART 4
+                options.colors = ["red"];
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Patch Waiting Times");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn({type: "number", role: "interval"});
+                data.addColumn({type: "number", role: "interval"});
+                data.addRows([
+                    {% for queue_datum in queue_data %}
+                        [
+                            {{ queue_datum.seconds_ago }} / {{ time_unit }},
+                            {{ queue_datum.patch_waiting_med }} / 60,
+                            "Patch Waiting Times\nMax: " + secondsToString({{ queue_datum.patch_waiting_max }}) + "\nMedian: " + secondsToString({{ queue_datum.patch_waiting_med }}) + "\nMin: " + secondsToString({{ queue_datum.patch_waiting_min }}) + "\n" + secondsToString({{ queue_datum.seconds_ago }}) + " ago",
+                            {{ queue_datum.patch_waiting_min }} / 60,
+                            {{ queue_datum.patch_waiting_max }} / 60,
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.LineChart(document.getElementById('chart4'));
+                chart.draw(data, options);
+
+                function postPatchLink (selection) {
+                    if (selection.length > 0 && selection[0].row !== undefined) {
+                        var attachmentIdArray = [{% for patch_datum in patch_data %}{{ patch_datum.attachment_id }}, {% endfor %}];
+                        var attachmentId = attachmentIdArray[selection[0].row];
+                        var aTag = document.getElementById("selectedPatch");
+                        aTag.innerHTML = aTag.href = "//" + window.location.host + "/patch/" + attachmentId;
+                    }
+                }
+
+                // CHART 5
+                options.colors = ["brown", "red"];
+                options.hAxis.title = "{{ time_unit_name|capfirst }} Ago";
+                options.hAxis.viewWindow.min = {{ seconds_ago_min }} / {{ time_unit }};
+                options.hAxis.viewWindow.max = {{ seconds_ago_max }} / {{ time_unit }};
+                delete options.lineWidth;
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Process Duration");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn("number", "Wait Duration");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addRows([
+                    {% for patch_datum in patch_data %}
+                        [
+                            {{ patch_datum.seconds_ago }} / {{ time_unit }},
+                            {{ patch_datum.process_duration }},
+                            "Patch {{ patch_datum.attachment_id }}\n" + secondsToString({{ patch_datum.seconds_ago }}) + " ago",
+                            {{ patch_datum.wait_duration }},
+                            "Patch {{ patch_datum.attachment_id }}\n" + secondsToString({{ patch_datum.seconds_ago }}) + " ago",
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.ScatterChart(document.getElementById('chart5'));
+                var chart5 = chart;
+                google.visualization.events.addListener(chart, "select", function () {postPatchLink(chart5.getSelection());});
+                chart.draw(data, options);
+
+                // CHART 6
+                options.colors = ["blue", "purple"];
+                delete options.vAxis.title;
+                data = new google.visualization.DataTable();
+                data.addColumn("number", "<time>");
+                data.addColumn("number", "Status Updates");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addColumn("number", "Retries");
+                data.addColumn({type: "string", role: "tooltip"});
+                data.addRows([
+                    {% for patch_datum in patch_data %}
+                        [
+                            {{ patch_datum.seconds_ago }} / {{ time_unit }},
+                            {{ patch_datum.status_update_count }},
+                            "Patch {{ patch_datum.attachment_id }}\n" + secondsToString({{ patch_datum.seconds_ago }}) + " ago",
+                            {{ patch_datum.retry_count }},
+                            "Patch {{ patch_datum.attachment_id }}\n" + secondsToString({{ patch_datum.seconds_ago }}) + " ago",
+                        ],
+                    {% endfor %}
+                ]);
+                chart = new google.visualization.ScatterChart(document.getElementById('chart6'));
+                var chart6 = chart;
+                google.visualization.events.addListener(chart, "select", function () {postPatchLink(chart6.getSelection());});
+                chart.draw(data, options);
+            });
+
+            function setURLParameter (parameterName, newValue) {
+                var split;
+                split = window.location.href.split("?");
+                var url, parameterArray;
+                url = split[0];
+                if (split.length > 1) {
+                    parameterArray = split[1].split("&");
+                } else {
+                    parameterArray = [];
+                }
+                var setParameter = false;
+                for (var i = 0; i < parameterArray.length; i++) {
+                    var currentParameterName = decodeURIComponent(parameterArray[i].split("=")[0]);
+                    if (currentParameterName === parameterName) {
+                        parameterArray[i] = encodeURIComponent(parameterName) + "=" + encodeURIComponent(newValue);
+                        setParameter = true;
+                        break;
+                    }
+                }
+                if (!setParameter) {
+                    parameterArray.push(encodeURIComponent(parameterName) + "=" + encodeURIComponent(newValue));
+                }console.log(parameterArray);
+                window.location.href = url + "?" + parameterArray.join("&");
+            }
+        </script>
+    </head>
+
+    <body>
+        <div class="choices">
+            {% for single_queue_name in all_queue_names %}
+                {% if single_queue_name == queue_name %}
+                    {{ queue_name }}
+                {% else %}
+                    {{ single_queue_name|force_escape|queue_charts_link:single_queue_name|safe }}
+                {% endif %}
+                {% if not forloop.last %} | {% endif %}
+            {% endfor %}
+        </div>
+        <h1>{{ queue_name }} Charts</h1>
+        <div>[{{ queue_name|force_escape|queue_status_link:"status"|safe }}]</div>
+        <div id="timestamp"></div>
+        <div class="choices">Viewing range:
+            {% for view_range_choice in view_range_choices %}
+                {% if view_range_choice.view_range == view_range %}
+                    {{ view_range_choice.name }}
+                {% else %}
+                    <a href="javascript:setURLParameter('view_range', {{ view_range_choice.view_range }})">{{ view_range_choice.name }}</a>
+                {% endif %}
+                {% if not forloop.last %} | {% endif %}
+            {% endfor %}
+        </div>
+        <div class="chart" id="chart1"></div>
+        <div class="chart" id="chart2"></div>
+        <div class="chart" id="chart3"></div>
+        <div class="chart" id="chart4"></div>
+        <div class="chart" id="chart5"></div>
+        <div class="chart" id="chart6"></div>
+        Selected patch: <a id="selectedPatch">(None)</div>
+    </body>
+</html>
index f8eb61e..aafafbd 100644 (file)
@@ -9,6 +9,7 @@
 
 <h3>Summary</h3>
 <div>
+<div>[{{ queue_name|force_escape|queue_charts_link:"charts"|safe }}]</div>
 Last Pass: {% if last_pass %}{{ last_pass.date|timesince }} ago{% else %}never{% endif %}
 {% if not bot_id and last_pass.bot_id %}
 by <a href="/queue-status/{{last_pass.queue_name}}/bots/{{last_pass.bot_id}}">{{ last_pass.bot_id }}</a>