From d7e09cc8d0966f7e50f8131276aec44318bc5c0b Mon Sep 17 00:00:00 2001 From: "rniwa@webkit.org" Date: Thu, 26 Jan 2012 08:28:57 +0000 Subject: [PATCH] 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. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@105971 268f45cc-cd09-0410-ab3c-d52691b4dbfc --- ChangeLog | 65 ++++++++ Websites/webkit-perf.appspot.com/app.yaml | 50 ++++++ Websites/webkit-perf.appspot.com/create_handler.py | 117 +++++++++++++ .../webkit-perf.appspot.com/dashboard_handler.py | 59 +++++++ Websites/webkit-perf.appspot.com/index.yaml | 11 ++ Websites/webkit-perf.appspot.com/js/config.js | 78 +++++++++ Websites/webkit-perf.appspot.com/main.py | 46 ++++++ .../webkit-perf.appspot.com/manifest_handler.py | 94 +++++++++++ Websites/webkit-perf.appspot.com/models.py | 112 +++++++++++++ Websites/webkit-perf.appspot.com/report_handler.py | 184 +++++++++++++++++++++ Websites/webkit-perf.appspot.com/runs_handler.py | 94 +++++++++++ .../static/create-models.html | 69 ++++++++ .../static/manual-submit.html | 68 ++++++++ 13 files changed, 1047 insertions(+) create mode 100644 Websites/webkit-perf.appspot.com/app.yaml create mode 100644 Websites/webkit-perf.appspot.com/create_handler.py create mode 100644 Websites/webkit-perf.appspot.com/dashboard_handler.py create mode 100644 Websites/webkit-perf.appspot.com/index.yaml create mode 100644 Websites/webkit-perf.appspot.com/js/config.js create mode 100644 Websites/webkit-perf.appspot.com/main.py create mode 100644 Websites/webkit-perf.appspot.com/manifest_handler.py create mode 100644 Websites/webkit-perf.appspot.com/models.py create mode 100644 Websites/webkit-perf.appspot.com/report_handler.py create mode 100644 Websites/webkit-perf.appspot.com/runs_handler.py create mode 100644 Websites/webkit-perf.appspot.com/static/create-models.html create mode 100644 Websites/webkit-perf.appspot.com/static/manual-submit.html diff --git a/ChangeLog b/ChangeLog index ee5902f..723e8da 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,68 @@ +2012-01-24 Ryosuke Niwa + + 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 > 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 index 0000000..060f48a --- /dev/null +++ b/Websites/webkit-perf.appspot.com/app.yaml @@ -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 index 0000000..c20a56d --- /dev/null +++ b/Websites/webkit-perf.appspot.com/create_handler.py @@ -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 index 0000000..7973b02 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/dashboard_handler.py @@ -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 index 0000000..a3b9e05 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/index.yaml @@ -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 index 0000000..97db813 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/js/config.js @@ -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 index 0000000..76b222d --- /dev/null +++ b/Websites/webkit-perf.appspot.com/main.py @@ -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 index 0000000..4d487dc --- /dev/null +++ b/Websites/webkit-perf.appspot.com/manifest_handler.py @@ -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 index 0000000..da82091 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/models.py @@ -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 index 0000000..a1d3540 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/report_handler.py @@ -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 index 0000000..8793c28 --- /dev/null +++ b/Websites/webkit-perf.appspot.com/runs_handler.py @@ -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 index 0000000..fdf537a --- /dev/null +++ b/Websites/webkit-perf.appspot.com/static/create-models.html @@ -0,0 +1,69 @@ + + + +Create new models + + + +

Create new models

+ +

Key: canonicalized name used by build bots and storage. Name: human friendly name

+ +

Builder

+
+ + + +
+ +

Branch

+
+ + + +
+ +

Platform

+
+ + + +
+ +

Result:

+

Status code

+

+

Headers

+

+

Response

+

+
+
+
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
index 0000000..fe875b5
--- /dev/null
+++ b/Websites/webkit-perf.appspot.com/static/manual-submit.html
@@ -0,0 +1,68 @@
+
+
+
+Test submission of a build report
+
+
+
+

Test submission of a build report

+

Specify the payload and submit:

+
+
+ + +

Result:

+

Status code

+

+

Headers

+

+

Response

+

+
+
+
-- 
2.7.4