Port Mozilla's Graph Server
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Jan 2012 08:28:57 +0000 (08:28 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Jan 2012 08:28:57 +0000 (08:28 +0000)
https://bugs.webkit.org/show_bug.cgi?id=76312

Reviewed by Adam Barth.

Add the app engine backend for the Mozilla's graph server used on perf-webkit.appspot.com.

To deploy webkit-perf.appspot.com, you also need to pull index.html, embed.html, graph.html, jq,
js (except config.js), and css (except title.png) from https://github.com/mozilla/graphs.

* Websites/perf-webkit.appspot.com: Added.
* Websites/perf-webkit.appspot.com/app.yaml: Added.
* Websites/perf-webkit.appspot.com/create_handler.py: Added.
(CreateHandler):
(CreateHandler.post):
(CreateHandler._createBuilder):
(CreateHandler._createBuilder.execute):
(CreateHandler._createBranch):
(CreateHandler._createBranch.execute):
(CreateHandler._createPlatform):
(CreateHandler._createPlatform.execute):
* Websites/perf-webkit.appspot.com/dashboard_handler.py: Added.
(DashboardHandler):
(DashboardHandler.get):
* Websites/perf-webkit.appspot.com/index.yaml: Added.
* Websites/perf-webkit.appspot.com/main.py: Added.
(main):
* Websites/perf-webkit.appspot.com/manifest_handler.py: Added.
(ManifestHandler):
(ManifestHandler.get):
* Websites/perf-webkit.appspot.com/models.py: Added.
(NumericIdHolder):
(NumericIdHolder.whose):
(createInTransactionWithNumericIdHolder):
(modelFromNumericId):
(Branch):
(Platform):
(Builder):
(Builder.authenticate):
(Builder.hashedPassword):
(Build):
(Test):
(TestResult):
(ReportLog):
* Websites/perf-webkit.appspot.com/report_handler.py: Added.
(ReportHandler):
(ReportHandler.post):
(ReportHandler._modelByKeyNameInBodyOrError):
(ReportHandler._integerInBody):
(ReportHandler._timestampInBody):
(ReportHandler._output):
(ReportHandler._resultsAreValid):
(ReportHandler._createBuildIfPossible):
(ReportHandler._createBuildIfPossible.execute):
(ReportHandler._addTestIfNeeded):
(ReportHandler._addTestIfNeeded.execute):
* Websites/perf-webkit.appspot.com/runs_handler.py: Added.
(RunsHandler):
(RunsHandler.get):
* Websites/perf-webkit.appspot.com/static: Added.
* Websites/perf-webkit.appspot.com/static/create-models.html: Added.
* Websites/perf-webkit.appspot.com/static/manual-submit.html: Added.

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

13 files changed:
ChangeLog
Websites/webkit-perf.appspot.com/app.yaml [new file with mode: 0644]
Websites/webkit-perf.appspot.com/create_handler.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/dashboard_handler.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/index.yaml [new file with mode: 0644]
Websites/webkit-perf.appspot.com/js/config.js [new file with mode: 0644]
Websites/webkit-perf.appspot.com/main.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/manifest_handler.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/models.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/report_handler.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/runs_handler.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/static/create-models.html [new file with mode: 0644]
Websites/webkit-perf.appspot.com/static/manual-submit.html [new file with mode: 0644]

index ee5902f..723e8da 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,68 @@
+2012-01-24  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Port Mozilla's Graph Server
+        https://bugs.webkit.org/show_bug.cgi?id=76312
+
+        Reviewed by Adam Barth.
+
+        Add the app engine backend for the Mozilla's graph server used on perf-webkit.appspot.com.
+
+        To deploy webkit-perf.appspot.com, you also need to pull index.html, embed.html, graph.html, jq,
+        js (except config.js), and css (except title.png) from https://github.com/mozilla/graphs.
+
+        * Websites/perf-webkit.appspot.com: Added.
+        * Websites/perf-webkit.appspot.com/app.yaml: Added.
+        * Websites/perf-webkit.appspot.com/create_handler.py: Added.
+        (CreateHandler):
+        (CreateHandler.post):
+        (CreateHandler._createBuilder):
+        (CreateHandler._createBuilder.execute):
+        (CreateHandler._createBranch):
+        (CreateHandler._createBranch.execute):
+        (CreateHandler._createPlatform):
+        (CreateHandler._createPlatform.execute):
+        * Websites/perf-webkit.appspot.com/dashboard_handler.py: Added.
+        (DashboardHandler):
+        (DashboardHandler.get):
+        * Websites/perf-webkit.appspot.com/index.yaml: Added.
+        * Websites/perf-webkit.appspot.com/main.py: Added.
+        (main):
+        * Websites/perf-webkit.appspot.com/manifest_handler.py: Added.
+        (ManifestHandler):
+        (ManifestHandler.get):
+        * Websites/perf-webkit.appspot.com/models.py: Added.
+        (NumericIdHolder):
+        (NumericIdHolder.whose):
+        (createInTransactionWithNumericIdHolder):
+        (modelFromNumericId):
+        (Branch):
+        (Platform):
+        (Builder):
+        (Builder.authenticate):
+        (Builder.hashedPassword):
+        (Build):
+        (Test):
+        (TestResult):
+        (ReportLog):
+        * Websites/perf-webkit.appspot.com/report_handler.py: Added.
+        (ReportHandler):
+        (ReportHandler.post):
+        (ReportHandler._modelByKeyNameInBodyOrError):
+        (ReportHandler._integerInBody):
+        (ReportHandler._timestampInBody):
+        (ReportHandler._output):
+        (ReportHandler._resultsAreValid):
+        (ReportHandler._createBuildIfPossible):
+        (ReportHandler._createBuildIfPossible.execute):
+        (ReportHandler._addTestIfNeeded):
+        (ReportHandler._addTestIfNeeded.execute):
+        * Websites/perf-webkit.appspot.com/runs_handler.py: Added.
+        (RunsHandler):
+        (RunsHandler.get):
+        * Websites/perf-webkit.appspot.com/static: Added.
+        * Websites/perf-webkit.appspot.com/static/create-models.html: Added.
+        * Websites/perf-webkit.appspot.com/static/manual-submit.html: Added.
+
 2012-01-25  Hajime Morita  <morrita@google.com>>
 
         ENABLE_SHADOW_DOM should be available via build-webkit --shadow-dom
diff --git a/Websites/webkit-perf.appspot.com/app.yaml b/Websites/webkit-perf.appspot.com/app.yaml
new file mode 100644 (file)
index 0000000..060f48a
--- /dev/null
@@ -0,0 +1,50 @@
+application: webkit-perf
+version: 8
+runtime: python27
+api_version: 1
+threadsafe: false
+
+handlers:
+- url: /favicon\.ico
+  static_files: favicon.ico
+  upload: favicon\.ico
+
+- url: /admin/((.+)\.html)
+  static_files: static/\1
+  upload: static
+  secure: always
+  login: admin
+
+- url: /
+  static_files: index.html
+  upload: index.html
+
+- url: /(.+\.html)
+  static_files: \1
+  upload: (.+\.html)
+
+- url: /css
+  static_dir: css
+
+- url: /js
+  static_dir: js
+
+- url: /jq
+  static_dir: jq
+
+- url: /api/test/report
+  script: main.py
+  secure: always
+
+- url: /admin/report
+  script: main.py
+  secure: always
+  login: admin
+
+- url: /api/create/(\w+)
+  script: main.py
+  secure: always
+  login: admin
+
+- url: .*
+  script: main.py
diff --git a/Websites/webkit-perf.appspot.com/create_handler.py b/Websites/webkit-perf.appspot.com/create_handler.py
new file mode 100644 (file)
index 0000000..c20a56d
--- /dev/null
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+from google.appengine.ext import db
+
+import json
+
+from models import Builder
+from models import Branch
+from models import NumericIdHolder
+from models import Platform
+from models import createInTransactionWithNumericIdHolder
+
+
+class CreateHandler(webapp2.RequestHandler):
+    def post(self, model):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
+
+        try:
+            payload = json.loads(self.request.body)
+            key = payload.get('key', '')
+            name = payload.get('name', '')
+            password = payload.get('password', '')
+        except:
+            self.response.out.write("Failed to parse the payload: %s" % self.request.body)
+            return
+
+        if model == 'builder':
+            error = self._createBuilder(name, password)
+        elif model == 'branch':
+            error = self._createBranch(key, name)
+        elif model == 'platform':
+            error = self._createPlatform(key, name)
+        else:
+            error = "Unknown model type: %s\n" % model
+
+        self.response.out.write(error + '\n' if error else 'OK')
+
+    def _createBuilder(self, name, password):
+        if not name or not password:
+            return 'Invalid name or password'
+
+        password = Builder.hashedPassword(password)
+
+        def execute():
+            message = None
+            bot = Builder.get_by_key_name(name)
+            if bot:
+                message = 'Updating the password since bot "%s" already exists' % name
+                bot.password = password
+            else:
+                bot = Builder(name=name, password=password, key_name=name)
+            bot.put()
+            return message
+
+        return db.run_in_transaction(execute)
+
+    def _createBranch(self, key, name):
+        if not key or not name:
+            return 'Invalid key or name'
+
+        error = [None]
+
+        def execute(id):
+            if Branch.get_by_key_name(key):
+                error[0] = 'Branch "%s" already exists' % key
+                return
+            branch = Branch(id=id, name=name, key_name=key)
+            branch.put()
+            return branch
+
+        createInTransactionWithNumericIdHolder(execute)
+        return error[0]
+
+    def _createPlatform(self, key, name):
+        if not key or not name:
+            return 'Invalid key name'
+
+        error = [None]
+
+        def execute(id):
+            if Platform.get_by_key_name(key):
+                error[0] = 'Platform "%s" already exists' % key
+                return
+            platform = Platform(id=id, name=name, key_name=key)
+            platform.put()
+            return platform
+
+        createInTransactionWithNumericIdHolder(execute)
+        return error[0]
diff --git a/Websites/webkit-perf.appspot.com/dashboard_handler.py b/Websites/webkit-perf.appspot.com/dashboard_handler.py
new file mode 100644 (file)
index 0000000..7973b02
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 webapp2
+
+import json
+
+from models import Builder
+from models import Branch
+from models import Platform
+from models import Test
+
+
+class DashboardHandler(webapp2.RequestHandler):
+    def get(self):
+        webkitTrunk = Branch.get_by_key_name('webkit-trunk')
+
+        # FIXME: Determine popular branches, platforms, and tests
+        dashboard = {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {webkitTrunk.name: webkitTrunk.id},
+            'platformToId': {},
+            'testToId': {},
+        }
+
+        for platform in Platform.all():
+            dashboard['platformToId'][platform.name] = platform.id
+
+        for test in Test.all():
+            dashboard['testToId'][test.name] = test.id
+
+        self.response.headers['Content-Type'] = 'application/json; charset=utf-8';
+        self.response.out.write(json.dumps(dashboard))
diff --git a/Websites/webkit-perf.appspot.com/index.yaml b/Websites/webkit-perf.appspot.com/index.yaml
new file mode 100644 (file)
index 0000000..a3b9e05
--- /dev/null
@@ -0,0 +1,11 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run.  If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED").  If you want to manage some indexes
+# manually, move them above the marker line.  The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
diff --git a/Websites/webkit-perf.appspot.com/js/config.js b/Websites/webkit-perf.appspot.com/js/config.js
new file mode 100644 (file)
index 0000000..97db813
--- /dev/null
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var USE_GENERATED_IMAGES_IN_DASHBOARD = false;
+var MAX_GRAPHS = 6;
+var MAX_CSETS = 100;
+var DAY = 86400000;
+
+var COLORS = ['#e7454c', '#6dba4b', '#4986cf', '#f5983d', '#884e9f', '#bf5c41'];
+
+// server for JSON performance data
+var SERVER = location.protocol.indexOf('http') == 0 ? location.protocol + '//' + location.host : 'http://webkit-perf.appspot.com';
+
+// server for static dashboard images
+var IMAGE_SERVER = SERVER;
+
+var LIGHT_COLORS = $.map(COLORS, function(color) {
+    return $.color.parse(color).add('a', -.5).toString();
+});
+
+var PLOT_OPTIONS = {
+    xaxis: { mode: 'time' },
+    yaxis: { min: 0 },
+    selection: { mode: 'x', color: '#97c6e5' },
+    series: { shadowSize: 0 },
+    lines: { show: false },
+    points: { show: true },
+    grid: {
+        color: '#cdd6df',
+        borderWidth: 2,
+        backgroundColor: '#fff',
+        hoverable: true,
+        clickable: true,
+        autoHighlight: false
+    }
+};
+
+var OVERVIEW_OPTIONS = {
+    xaxis: { mode: 'time' },
+    yaxis: { min: 0 },
+    selection: { mode: 'x', color: '#97c6e5' },
+    series: {
+        lines: { show: true, lineWidth: 1 },
+        shadowSize: 0
+    },
+    grid: {
+        color: '#cdd6df',
+        borderWidth: 2,
+        backgroundColor: '#fff',
+        tickColor: 'rgba(0,0,0,0)'
+    }
+};
+
+function urlForChangeset(branch, changeset)
+{
+    return 'http://trac.webkit.org/changeset/' + changeset;
+}
+
+function urlForChangesetList(branch, changesetList)
+{
+    var min = Math.min.apply(Math, changesetList);
+    var max = Math.max.apply(Math, changesetList);
+    return 'http://trac.webkit.org/log/?rev=' + max + '&stop_rev=' + min + '&verbose=on';
+}
+
+// FIXME move this back to dashboard.js once the bug 718925 is fixed
+function fetchDashboardManifest(callback)
+{
+    $.ajaxSetup({
+        'error': function(xhr, e, message) {
+            error('Could not download dashboard data from server', e);
+        },
+        cache: true,
+    });
+
+    $.getJSON(SERVER + '/api/test/dashboard', callback);
+}
diff --git a/Websites/webkit-perf.appspot.com/main.py b/Websites/webkit-perf.appspot.com/main.py
new file mode 100644 (file)
index 0000000..76b222d
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+#
+# Copyright 2007, 2011 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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.
+#
+
+import webapp2
+from google.appengine.ext.webapp import util
+
+import json
+
+from create_handler import CreateHandler
+from dashboard_handler import DashboardHandler
+from manifest_handler import ManifestHandler
+from report_handler import ReportHandler
+from report_handler import AdminReportHandler
+from runs_handler import RunsHandler
+
+routes = [
+    ('/api/create/(.*)', CreateHandler),
+    ('/api/test/?', ManifestHandler),
+    ('/api/test/report/?', ReportHandler),
+    ('/admin/report/?', AdminReportHandler),
+    ('/api/test/runs/?', RunsHandler),
+    ('/api/test/dashboard/?', DashboardHandler),
+]
+
+
+def main():
+    application = webapp2.WSGIApplication(routes, debug=True)
+    util.run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/Websites/webkit-perf.appspot.com/manifest_handler.py b/Websites/webkit-perf.appspot.com/manifest_handler.py
new file mode 100644 (file)
index 0000000..4d487dc
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 webapp2
+
+import json
+
+from models import Builder
+from models import Branch
+from models import Platform
+from models import Test
+
+
+class ManifestHandler(webapp2.RequestHandler):
+    def get(self):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
+        self.response.out.write('{"testMap":')
+
+        testMap = {}
+        platformIdMap = {}
+        branchIdMap = {}
+        for test in Test.all():
+            branchIds = [Branch.get(branchKey).id for branchKey in test.branches]
+            platformIds = [Platform.get(platformKey).id for platformKey in test.platforms]
+            testMap[test.id] = {
+                'name': test.name,
+                'branchIds': branchIds,
+                'platformIds': platformIds,
+            }
+
+            for platformId in platformIds:
+                platformIdMap.setdefault(platformId, {'tests': [], 'branches': []})
+                platformIdMap[platformId]['tests'].append(test.id)
+                platformIdMap[platformId]['branches'] += branchIds
+
+            for branchId in branchIds:
+                branchIdMap.setdefault(branchId, {'tests': [], 'platforms': []})
+                branchIdMap[branchId]['tests'].append(test.id)
+                branchIdMap[branchId]['platforms'] += platformIds
+
+        self.response.out.write(json.dumps(testMap))
+        self.response.out.write(',"platformMap":')
+
+        platformMap = {}
+        for platform in Platform.all():
+            if platform.id not in platformIdMap:
+                continue
+            platformMap[platform.id] = {
+                'name': platform.name,
+                'testIds': list(set(platformIdMap[platform.id]['tests'])),
+                'branchIds': list(set(platformIdMap[platform.id]['branches'])),
+            }
+
+        self.response.out.write(json.dumps(platformMap))
+        self.response.out.write(',"branchMap":')
+
+        branchMap = {}
+        for branch in Branch.all():
+            if branch.id not in branchIdMap:
+                continue
+            branchMap[branch.id] = {
+                'name': branch.name,
+                'testIds': list(set(branchIdMap[branch.id]['tests'])),
+                'platformIds': list(set(branchIdMap[branch.id]['platforms'])),
+            }
+
+        self.response.out.write(json.dumps(branchMap))
+        self.response.out.write('}')
diff --git a/Websites/webkit-perf.appspot.com/models.py b/Websites/webkit-perf.appspot.com/models.py
new file mode 100644 (file)
index 0000000..da82091
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 hashlib
+import re
+
+from google.appengine.ext import db
+
+
+class NumericIdHolder(db.Model):
+    owner = db.ReferenceProperty()
+    # Dummy class whose sole purpose is to generate key().id()
+
+
+def createInTransactionWithNumericIdHolder(callback):
+    idHolder = NumericIdHolder()
+    idHolder.put()
+    idHolder = NumericIdHolder.get(idHolder.key())
+    owner = db.run_in_transaction(callback, idHolder.key().id())
+    if owner:
+        idHolder.owner = owner
+        idHolder.put()
+    else:
+        idHolder.delete()
+    return owner
+
+
+def modelFromNumericId(id, expectedKind):
+    idHolder = NumericIdHolder.get_by_id(id)
+    return idHolder.owner if idHolder and idHolder.owner and isinstance(idHolder.owner, expectedKind) else None
+
+
+class Branch(db.Model):
+    id = db.IntegerProperty(required=True)
+    name = db.StringProperty(required=True)
+
+
+class Platform(db.Model):
+    id = db.IntegerProperty(required=True)
+    name = db.StringProperty(required=True)
+
+
+class Builder(db.Model):
+    name = db.StringProperty(required=True)
+    password = db.StringProperty(required=True)
+
+    def authenticate(self, rawPassword):
+        return self.password == hashlib.sha256(rawPassword).hexdigest()
+
+    @staticmethod
+    def hashedPassword(rawPassword):
+        return hashlib.sha256(rawPassword).hexdigest()
+
+
+class Build(db.Model):
+    branch = db.ReferenceProperty(Branch, required=True, collection_name='build_branch')
+    platform = db.ReferenceProperty(Platform, required=True, collection_name='build_platform')
+    builder = db.ReferenceProperty(Builder, required=True, collection_name='builder_key')
+    buildNumber = db.IntegerProperty(required=True)
+    revision = db.IntegerProperty(required=True)
+    timestamp = db.DateTimeProperty(required=True)
+
+
+# Used to generate TestMap in the manifest efficiently
+class Test(db.Model):
+    id = db.IntegerProperty(required=True)
+    name = db.StringProperty(required=True)
+    branches = db.ListProperty(db.Key)
+    platforms = db.ListProperty(db.Key)
+
+
+class TestResult(db.Model):
+    name = db.StringProperty(required=True)
+    build = db.ReferenceProperty(Build, required=True)
+    value = db.FloatProperty(required=True)
+    valueMedian = db.FloatProperty()
+    valueStdev = db.FloatProperty()
+    valueMin = db.FloatProperty()
+    valueMax = db.FloatProperty()
+
+
+# Temporarily log reports sent by bots
+class ReportLog(db.Model):
+    timestamp = db.DateTimeProperty(required=True)
+    headers = db.TextProperty()
+    payload = db.TextProperty()
diff --git a/Websites/webkit-perf.appspot.com/report_handler.py b/Websites/webkit-perf.appspot.com/report_handler.py
new file mode 100644 (file)
index 0000000..a1d3540
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+from google.appengine.ext import db
+
+import json
+import re
+import time
+from datetime import datetime
+
+from models import Builder
+from models import Branch
+from models import Build
+from models import NumericIdHolder
+from models import Platform
+from models import ReportLog
+from models import Test
+from models import TestResult
+from models import createInTransactionWithNumericIdHolder
+
+
+class ReportHandler(webapp2.RequestHandler):
+    def post(self):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+
+        headers = "\n".join([key + ': ' + value for key, value in self.request.headers.items()])
+
+        # Do as best as we can to remove the password
+        request_body_without_password = re.sub(r'"password"\s*:\s*".+?",', '', self.request.body)
+        log = ReportLog(timestamp=datetime.now(), headers=headers, payload=request_body_without_password)
+        log.put()
+
+        try:
+            self._body = json.loads(self.request.body)
+        except ValueError:
+            return self._output('Failed to parse the payload as a json. Report key: %d' % log.key().id())
+
+        builder = self._modelByKeyNameInBodyOrError(Builder, 'builder-name')
+        branch = self._modelByKeyNameInBodyOrError(Branch, 'branch')
+        platform = self._modelByKeyNameInBodyOrError(Platform, 'platform')
+        buildNumber = self._integerInBody('build-number')
+        revision = self._integerInBody('revision')
+        timestamp = self._timestampInBody()
+
+        failed = False
+        if builder and not (self.bypassAuthentication() or builder.authenticate(self._body.get('password', ''))):
+            self._output('Authentication failed')
+            failed = True
+
+        if not self._resultsAreValid():
+            self._output("The payload doesn't contain results or results are malformed")
+            failed = True
+
+        if not (builder and branch and platform and buildNumber and revision and timestamp) or failed:
+            return
+
+        build = self._createBuildIfPossible(builder, buildNumber, branch, platform, revision, timestamp)
+        if not build:
+            return
+
+        for test, result in self._body['results'].iteritems():
+            self._addTestIfNeeded(test, branch, platform)
+            if isinstance(result, dict):
+                TestResult(name=test, build=build, value=float(result.get('avg', 0)), valueMedian=float(result.get('median', 0)),
+                    valueStdev=float(result.get('stdev', 0)), valueMin=float(result.get('min', 0)), valueMax=float(result.get('max', 0))).put()
+            else:
+                TestResult(name=test, build=build, value=float(result)).put()
+
+        log = ReportLog.get(log.key())
+        log.delete()
+
+        return self._output('OK')
+
+    def _modelByKeyNameInBodyOrError(self, model, keyName):
+        key = self._body.get(keyName, '')
+        instance = key and model.get_by_key_name(key)
+        if not instance:
+            self._output('There are no %s named "%s"' % (model.__name__.lower(), key))
+        return instance
+
+    def _integerInBody(self, key):
+        value = self._body.get(key, '')
+        try:
+            return int(value)
+        except:
+            return self._output('Invalid %s: "%s"' % (key.replace('-', ' '), value))
+
+    def _timestampInBody(self):
+        value = self._body.get('timestamp', '')
+        try:
+            return datetime.fromtimestamp(int(value))
+        except:
+            return self._output('Failed to parse the timestamp: %s' % value)
+
+    def _output(self, message):
+        self.response.out.write(message + '\n')
+
+    def bypassAuthentication(self):
+        return False
+
+    def _resultsAreValid(self):
+
+        def _isFloatConvertible(value):
+            try:
+                float(value)
+                return True
+            except TypeError:
+                return False
+
+        if 'results' not in self._body or not isinstance(self._body['results'], dict):
+            return False
+
+        for testResult in self._body['results'].values():
+            if isinstance(testResult, dict):
+                for value in testResult.values():
+                    if not _isFloatConvertible(value):
+                        return False
+                if 'avg' not in testResult:
+                    return False
+                continue
+            if not _isFloatConvertible(testResult):
+                return False
+
+        return True
+
+    def _createBuildIfPossible(self, builder, buildNumber, branch, platform, revision, timestamp):
+        key_name = builder.name + ':' + str(int(time.mktime(timestamp.timetuple())))
+
+        def execute():
+            build = Build.get_by_key_name(key_name)
+            if build:
+                return self._output('The build at %s already exists for %s' % (str(timestamp), builder.name))
+
+            return Build(branch=branch, platform=platform, builder=builder, buildNumber=buildNumber,
+                timestamp=timestamp, revision=revision, key_name=key_name).put()
+        return db.run_in_transaction(execute)
+
+    def _addTestIfNeeded(self, testName, branch, platform):
+
+        def execute(id):
+            test = Test.get_by_key_name(testName)
+            returnValue = None
+            if not test:
+                test = Test(id=id, name=testName, key_name=testName)
+                returnValue = test
+            if branch.key() not in test.branches:
+                test.branches.append(branch.key())
+            if platform.key() not in test.platforms:
+                test.platforms.append(platform.key())
+            test.put()
+            return returnValue
+        createInTransactionWithNumericIdHolder(execute)
+
+
+class AdminReportHandler(ReportHandler):
+    def bypassAuthentication(self):
+        return True
diff --git a/Websites/webkit-perf.appspot.com/runs_handler.py b/Websites/webkit-perf.appspot.com/runs_handler.py
new file mode 100644 (file)
index 0000000..8793c28
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+
+import json
+from time import mktime
+from datetime import datetime
+
+from models import Build
+from models import Builder
+from models import Branch
+from models import NumericIdHolder
+from models import Platform
+from models import Test
+from models import TestResult
+from models import modelFromNumericId
+
+
+class RunsHandler(webapp2.RequestHandler):
+    def get(self):
+        try:
+            testId = int(self.request.get('id', 0))
+            branchId = int(self.request.get('branchid', 0))
+            platformId = int(self.request.get('platformid', 0))
+        except TypeError:
+            # FIXME: Output an error here
+            testId = 0
+            branchId = 0
+            platformId = 0
+
+        # FIXME: Just fetch builds specified by "days"
+        # days = self.request.get('days', 365)
+
+        builds = Build.all()
+        builds.filter('branch =', modelFromNumericId(branchId, Branch))
+        builds.filter('platform =', modelFromNumericId(platformId, Platform))
+
+        test = modelFromNumericId(testId, Test)
+        testName = test.name if test else None
+        test_runs = []
+        averages = {}
+        values = []
+        timestamps = []
+
+        for build in builds:
+            results = TestResult.all()
+            results.filter('name =', testName)
+            results.filter('build =', build)
+            for result in results:
+                builderId = build.builder.key().id()
+                posixTimestamp = mktime(build.timestamp.timetuple())
+                test_runs.append([result.key().id(),
+                    [build.key().id(), build.buildNumber, build.revision],
+                    posixTimestamp, result.value, 0, [], builderId])
+                # FIXME: Calculate the average; in practice, we wouldn't have more than one value for a given revision
+                averages[build.revision] = result.value
+                values.append(result.value)
+                timestamps.append(posixTimestamp)
+
+        self.response.headers['Content-Type'] = 'application/json; charset=utf-8';
+        self.response.out.write(json.dumps({
+            'test_runs': test_runs,
+            'averages': averages,
+            'min': min(values) if values else None,
+            'max': max(values) if values else None,
+            'date_range': [min(timestamps), max(timestamps)] if timestamps else None,
+            'stat': 'ok'}))
diff --git a/Websites/webkit-perf.appspot.com/static/create-models.html b/Websites/webkit-perf.appspot.com/static/create-models.html
new file mode 100644 (file)
index 0000000..fdf537a
--- /dev/null
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Create new models</title>
+<style type="text/css">
+em { font-style: normal; color: red; }
+pre {border: solid 1px black; padding: 5px;}
+h3 {font-size: 1em;}
+</style>
+</head>
+<body>
+<h1>Create new models</h1>
+
+<p>Key: canonicalized name used by build bots and storage. Name: human friendly name</p>
+
+<h2>Builder</h2>
+<form method="post" action="/api/create/builder" onsubmit="return submitByXHR(this, event)">
+<label for="name">Name/Key</label><input type="text" name="name">
+<label for="password">Password</label><input type="password" name="password">
+<button type="submit">Create</button>
+</form>
+
+<h2>Branch</h2>
+<form method="post" action="/api/create/branch" onsubmit="return submitByXHR(this, event);">
+<label for="key">Key</label><input type="text" name="key">
+<label for="name">Name</label><input type="text" name="name">
+<button type="submit">Create</button>
+</form>
+
+<h2>Platform</h2>
+<form method="post" action="/api/create/platform" onsubmit="return submitByXHR(this, event)">
+<label for="key">Key</label><input type="text" name="key">
+<label for="name">Name</label><input type="text" name="name">
+<button type="submit">Create</button>
+</form>
+
+<h2>Result:</h2>
+<h3>Status code</h3>
+<p id="status"></p>
+<h3>Headers</h3>
+<pre id="headers"></pre>
+<h3>Response</h3>
+<pre id="response" name="response"></pre>
+<script>
+
+$ = function (id) { return document.getElementById(id); }
+
+function submitByXHR(form, event) {
+    event.preventDefault();
+
+    var contents = {}
+    for (var i = 0; i < form.elements.length; i++)
+        contents[form.elements[i].name] = form.elements[i].value;
+
+    var xhr = new XMLHttpRequest;
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState != 4)
+            return;
+        $('status').innerText = xhr.status;
+        $('headers').innerText = xhr.getAllResponseHeaders();
+        $('response').innerHTML = xhr.responseText;
+    }
+    xhr.open(form.method, form.action, true);
+    xhr.send(JSON.stringify(contents));
+}
+
+</script>
+</body>
+</html>
diff --git a/Websites/webkit-perf.appspot.com/static/manual-submit.html b/Websites/webkit-perf.appspot.com/static/manual-submit.html
new file mode 100644 (file)
index 0000000..fe875b5
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test submission of a build report</title>
+<style type="text/css">
+em { font-style: normal; color: red; }
+pre {border: solid 1px black; padding: 5px;}
+h3 {font-size: 1em;}
+</style>
+</head>
+<body>
+<h1>Test submission of a build report</h1>
+<p>Specify the payload and submit:</p>
+<textarea id="data" name="data" rows="20" cols="100"></textarea><br>
+<em id="json_error"></em><br>
+<button type="submit" onclick="submit()">Submit</button>
+
+<h2>Result:</h2>
+<h3>Status code</h3>
+<p id="status"></p>
+<h3>Headers</h3>
+<pre id="headers"></pre>
+<h3>Response</h3>
+<pre id="response" name="response"></pre>
+<script>
+
+$ = function (id) { return document.getElementById(id); }
+
+$('data').oninput = function () {
+    var payload = $('data').value;
+    try {
+        JSON.parse(payload);
+        $('json_error').innerText = '';
+    } catch (error) {
+        $('json_error').innerText = error;
+    }
+}
+
+function submit() {
+    var xhr = new XMLHttpRequest;
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState != 4)
+            return;
+        $('status').innerText = xhr.status;
+        $('headers').innerText = xhr.getAllResponseHeaders();
+        $('response').innerHTML = xhr.responseText;
+    }
+    xhr.open('POST','/admin/report/', true);
+    xhr.send($('data').value);
+}
+
+$('data').value = JSON.stringify({
+    'branch': 'webkit-trunk',
+    'platform': 'chromium-mac',
+    'builder-name': 'google-mac-2',
+    'build-number': '123',
+    'timestamp': parseInt(Date.now() / 1000),
+    'revision': 104856,
+    'results':
+        {
+            'webkit_style_test': {'avg': 100, 'median': 102, 'stdev': 5, 'min': 90, 'max': 110},
+            'some_test': 54,
+        },
+}, null, '  ');
+
+</script>
+</body>
+</html>