aboutsummaryrefslogtreecommitdiff
path: root/catapult/experimental
diff options
context:
space:
mode:
authorChris Craik <ccraik@google.com>2016-03-28 13:54:49 -0700
committerChris Craik <ccraik@google.com>2016-03-28 13:57:49 -0700
commitcef7893435aa41160dd1255c43cb8498279738cc (patch)
treefaf334a3dad13ef724722ff7b1d2c673257bbafd /catapult/experimental
parent11c2fbcfb91714e0aac1b2e53db3a4490c89a091 (diff)
downloadchromium-trace-cef7893435aa41160dd1255c43cb8498279738cc.tar.gz
Change-Id: Ic610f2da8ecd564d5dd58cbc8b9a738ee74b2a06
Diffstat (limited to 'catapult/experimental')
-rw-r--r--catapult/experimental/OWNERS1
-rw-r--r--catapult/experimental/PRESUBMIT.py29
-rw-r--r--catapult/experimental/bisect_lib/README.md12
-rwxr-xr-xcatapult/experimental/bisect_lib/bin/run_py_tests27
-rwxr-xr-xcatapult/experimental/bisect_lib/bisect_helper.py25
-rw-r--r--catapult/experimental/bisect_lib/chromium_revisions.py56
-rw-r--r--catapult/experimental/bisect_lib/chromium_revisions_test.py95
-rw-r--r--catapult/experimental/bisect_lib/depot_map.py19
-rwxr-xr-xcatapult/experimental/bisect_lib/fetch_intervening_revisions.py99
-rwxr-xr-xcatapult/experimental/bisect_lib/fetch_intervening_revisions_test.py73
-rwxr-xr-xcatapult/experimental/bisect_lib/fetch_revision_info.py52
-rwxr-xr-xcatapult/experimental/bisect_lib/fetch_revision_info_test.py49
-rw-r--r--catapult/experimental/bisect_lib/test_data/MOCK_INFO_RESPONSE_FILE96
-rw-r--r--catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_1 (renamed from catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_FILE)0
-rw-r--r--catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_142
-rw-r--r--catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_241
-rw-r--r--catapult/experimental/buildbot/buildbot.py36
-rwxr-xr-xcatapult/experimental/buildbot/query.py14
-rwxr-xr-xcatapult/experimental/hardware.py21
-rw-r--r--catapult/experimental/statistical_analysis/__init__.py3
-rwxr-xr-xcatapult/experimental/statistical_analysis/compare_benchmark_results.py177
-rwxr-xr-xcatapult/experimental/statistical_analysis/results_stats.py327
-rwxr-xr-xcatapult/experimental/statistical_analysis/results_stats_unittest.py265
23 files changed, 1295 insertions, 264 deletions
diff --git a/catapult/experimental/OWNERS b/catapult/experimental/OWNERS
new file mode 100644
index 00000000..72e8ffc0
--- /dev/null
+++ b/catapult/experimental/OWNERS
@@ -0,0 +1 @@
+*
diff --git a/catapult/experimental/PRESUBMIT.py b/catapult/experimental/PRESUBMIT.py
new file mode 100644
index 00000000..c119c870
--- /dev/null
+++ b/catapult/experimental/PRESUBMIT.py
@@ -0,0 +1,29 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return _CommonChecks(input_api, output_api)
+
+
+def _CommonChecks(input_api, output_api):
+ results = []
+ results += input_api.RunTests(input_api.canned_checks.GetPylint(
+ input_api, output_api, extra_paths_list=_GetPathsToPrepend(input_api),
+ pylintrc='../pylintrc'))
+ return results
+
+
+def _GetPathsToPrepend(input_api):
+ project_dir = input_api.PresubmitLocalPath()
+ catapult_dir = input_api.os_path.join(project_dir, '..')
+ return [
+ project_dir,
+
+ input_api.os_path.join(catapult_dir, 'third_party', 'mock'),
+ ]
diff --git a/catapult/experimental/bisect_lib/README.md b/catapult/experimental/bisect_lib/README.md
index b76c3d95..120e0798 100644
--- a/catapult/experimental/bisect_lib/README.md
+++ b/catapult/experimental/bisect_lib/README.md
@@ -1,4 +1,3 @@
-
<!-- Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
@@ -13,15 +12,6 @@ recipes subsystem currently allows.
Secondary goals are:
- * Simplify code sharing with the related [Telemetry](https://www.chromium.org/developers/telemetry) and [Performance Dashboard](https://github.com/catapult-project/catapult/blob/master/dashboard/README.md) projects, also under catapult.
+ * Simplify code sharing with the related [Telemetry](/telemetry/README.md) and [Performance Dashboard](/dashboard/README.md) projects.
* Eventually move the bisect director role outside of buildbot/recipes and
into its own standalone application.
-
-These tools were created by Chromium developers for performance analysis,
-testing, and monitoring of Chrome, but they can also be used for analyzing and
-monitoring websites, and eventually Android apps.
-
-Contributing
-============
-Please see [our contributor's guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md)
-
diff --git a/catapult/experimental/bisect_lib/bin/run_py_tests b/catapult/experimental/bisect_lib/bin/run_py_tests
new file mode 100755
index 00000000..6b7daf96
--- /dev/null
+++ b/catapult/experimental/bisect_lib/bin/run_py_tests
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# Copyright (c) 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Runs all Python unit tests in catapult_build/."""
+
+import os
+import sys
+
+_CATAPULT = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.path.pardir, os.path.pardir, os.path.pardir))
+
+
+if __name__ == '__main__':
+ sys.path.append(_CATAPULT)
+
+ from hooks import install
+ if '--no-install-hooks' in sys.argv:
+ sys.argv.remove('--no-install-hooks')
+ else:
+ install.InstallHooks()
+
+ from catapult_build import run_with_typ
+ sys.exit(run_with_typ.Run(
+ os.path.join(_CATAPULT, 'experimental', 'bisect_lib'),
+ path=[_CATAPULT]))
diff --git a/catapult/experimental/bisect_lib/bisect_helper.py b/catapult/experimental/bisect_lib/bisect_helper.py
deleted file mode 100755
index 44baa7c8..00000000
--- a/catapult/experimental/bisect_lib/bisect_helper.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/python2.7
-
-# Copyright 2015 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import json
-import os
-import sys
-
-sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
-
-from bisect_lib import chromium_revisions
-
-
-def main(argv):
- if argv[1] == 'query_revision_info':
- print json.dumps(chromium_revisions.revision_info(argv[2]))
- elif argv[1] == 'revision_range':
- print json.dumps(chromium_revisions.revision_range(argv[2], argv[3]))
- return 0
-
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv))
diff --git a/catapult/experimental/bisect_lib/chromium_revisions.py b/catapult/experimental/bisect_lib/chromium_revisions.py
deleted file mode 100644
index 2c660fe0..00000000
--- a/catapult/experimental/bisect_lib/chromium_revisions.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import json
-import urllib2
-
-BASE_URL = 'https://chromium.googlesource.com/chromium/src/+'
-PADDING = ')]}\'\n' # Gitiles padding.
-
-def revision_info(revision):
- """Gets information about a chromium revision.
-
- Args:
- revision (str): The git commit hash of the revision to check.
-
- Returns:
- A dictionary containing the author, email, 'subject' (the first line of the
- commit message) the 'body' (the whole message) and the date in string format
- like "Sat Oct 24 00:33:21 2015".
- """
-
- url = '%s/%s?format=json' % (BASE_URL, revision)
- response = urllib2.urlopen(url).read()
- response = json.loads(response[len(PADDING):])
- message = response['message'].splitlines()
- subject = message[0]
- body = '\n'.join(message[1:])
- result = {
- 'author': response['author']['name'],
- 'email': response['author']['email'],
- 'subject': subject,
- 'body': body,
- 'date': response['committer']['time'],
- }
- return result
-
-
-def revision_range(first_revision, last_revision):
- """Gets the revisions in chromium between first and last including the latter.
-
- Args:
- first_revision (str): The git commit of the first revision in the range.
- last_revision (str): The git commit of the last revision in the range.
-
- Returns:
- A list of dictionaries, one for each revision after the first revision up to
- and including the last revision. For each revision, its dictionary will
- contain information about the author and the comitter and the commit itself
- analogously to the 'git log' command. See test_data/MOCK_RANGE_RESPONSE_FILE
- for an example.
- """
- url = '%slog/%s..%s?format=json' % (BASE_URL, first_revision, last_revision)
- response = urllib2.urlopen(url).read()
- response = json.loads(response[len(PADDING):])
- return response['log']
diff --git a/catapult/experimental/bisect_lib/chromium_revisions_test.py b/catapult/experimental/bisect_lib/chromium_revisions_test.py
deleted file mode 100644
index 19eb56e2..00000000
--- a/catapult/experimental/bisect_lib/chromium_revisions_test.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import os
-import sys
-import unittest
-
-_EXPERIMENTAL = os.path.join(os.path.dirname(__file__), os.pardir)
-_CATAPULT = os.path.join(_EXPERIMENTAL, os.pardir)
-
-# TODO(robertocn): Add these to sys.path conditionally.
-sys.path.append(os.path.join(_CATAPULT, 'third_party', 'mock'))
-sys.path.append(_EXPERIMENTAL)
-
-import mock
-
-from bisect_lib import chromium_revisions
-
-
-FIRST_REVISION = '53fc07eb478520a80af6bf8b62be259bb55db0f1'
-LAST_REVISION = 'c89130e28fd01062104e1be7f3a6fc3abbb80ca9'
-
-TEST_DATA_LOCATION = os.path.join(os.path.dirname(__file__),
- 'test_data')
-MOCK_INFO_RESPONSE_FILE = open(os.path.join(
- TEST_DATA_LOCATION, 'MOCK_INFO_RESPONSE_FILE'))
-MOCK_RANGE_RESPONSE_FILE = open(os.path.join(
- TEST_DATA_LOCATION, 'MOCK_RANGE_RESPONSE_FILE'))
-
-EXPECTED_INFO = {
- 'body':
- 'BUG=548160',
- 'date':
- 'Tue Oct 27 21:26:30 2015',
- 'subject':
- '[Extensions] Fix hiding browser actions without the toolbar redesign',
- 'email':
- 'rdevlin.cronin@chromium.org',
- 'author':
- 'rdevlin.cronin'
-}
-
-INTERVENING_REVISIONS = [
- '2e93263dc74f0496100435e1fd7232e9e8323af0',
- '6feaa73a54d0515ad2940709161ca0a5ad91d1f8',
- '3861789af25e2d3502f0fb7080da5785d31308aa',
- '8fcc8af20a3d41b0512e3b1486e4dc7de528a72b',
- 'f1c777e3f97a16cc6a3aa922a23602fa59412989',
- 'ee261f306c3c66e96339aa1026d62a6d953302fe',
- '7bd1741893bd4e233b5562a6926d7e395d558343',
- '4f81be50501fbc02d7e44df0d56032e5885e19b6',
- '8414732168a8867a5d6bd45eaade68a5820a9e34',
- '01542ac6d0fbec6aa78e33e6c7ec49a582072ea9',
- '66aeb2b7084850d09f3fccc7d7467b57e4da1882',
- '48c1471f1f503246dd66753a4c7588d77282d2df',
- '84f6037e951c21a3b00bd3ddd034f258da6839b5',
- 'ebd5f102ee89a4be5c98815c02c444fbf2b6b040',
- '5dbc149bebecea186b693b3d780b6965eeffed0f',
- '22e49fb496d6ffa122c470f6071d47ccb4ccb672',
- '07a6d9854efab6677b880defa924758334cfd47d',
- '32ce3b13924d84004a3e05c35942626cbe93cbbd',
-]
-
-
-class ChromiumRevisionsTest(unittest.TestCase):
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- def testRevisionInfo(self):
- with mock.patch('urllib2.urlopen', mock.MagicMock(
- return_value=MOCK_INFO_RESPONSE_FILE)):
- test_info = chromium_revisions.revision_info(LAST_REVISION)
- for key in EXPECTED_INFO:
- self.assertIn(EXPECTED_INFO[key], test_info[key])
-
- def testRevisionRange(self):
- with mock.patch('urllib2.urlopen', mock.MagicMock(
- return_value=MOCK_RANGE_RESPONSE_FILE)):
- rev_list = chromium_revisions.revision_range(
- FIRST_REVISION, LAST_REVISION)
- commits_only = [entry['commit'] for entry in rev_list]
- for r in INTERVENING_REVISIONS:
- self.assertIn(r, commits_only)
- self.assertIn(LAST_REVISION, commits_only)
- self.assertEqual(len(INTERVENING_REVISIONS) + 1,
- len(rev_list))
-
-if __name__ == '__main__':
- unittest.main()
-
diff --git a/catapult/experimental/bisect_lib/depot_map.py b/catapult/experimental/bisect_lib/depot_map.py
new file mode 100644
index 00000000..e514da8b
--- /dev/null
+++ b/catapult/experimental/bisect_lib/depot_map.py
@@ -0,0 +1,19 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module contains a mapping of depot names to paths on gitiles.
+
+This is used to fetch information from gitiles for different
+repositories supported by auto-bisect.
+"""
+
+# For each entry in this map, the key is a "depot" name (a Chromium dependency
+# in the DEPS file) and the value is a path used for the repo on gitiles; each
+# repo can be found at https://chromium.googlesource.com/<PATH>.
+DEPOT_PATH_MAP = {
+ 'chromium': 'chromium/src',
+ 'angle': 'angle/angle',
+ 'v8': 'v8/v8.git',
+ 'skia': 'skia',
+}
diff --git a/catapult/experimental/bisect_lib/fetch_intervening_revisions.py b/catapult/experimental/bisect_lib/fetch_intervening_revisions.py
new file mode 100755
index 00000000..9cc4d65c
--- /dev/null
+++ b/catapult/experimental/bisect_lib/fetch_intervening_revisions.py
@@ -0,0 +1,99 @@
+#!/usr/bin/python
+#
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Gets list of revisions between two commits and their commit positions.
+
+Example usage:
+ ./fetchInterveningRevisions.py 343b531d31 7b43807df3 chromium
+ ./fetchInterveningRevisions.py 235eff9574 1e4681c33f v8
+
+Note: Another implementation of this functionality can be found in
+findit/common/gitRepository.py (https://goo.gl/Rr8j9O).
+"""
+
+import argparse
+import json
+import urllib2
+
+from bisect_lib import depot_map
+
+_GITILES_PADDING = ')]}\'\n'
+_URL_TEMPLATE = ('https://chromium.googlesource.com/%s/+log/%s..%s'
+ '?format=json&n=%d')
+
+# Gitiles paginates the list of commits; since we want to get all of the
+# commits at once, the page size should be larger than the largest revision
+# range that we expect to get.
+_PAGE_SIZE = 512
+
+def FetchInterveningRevisions(start, end, depot_name):
+ """Fetches a list of revision in between two commits.
+
+ Args:
+ start (str): A git commit hash in the Chromium src repository.
+ end (str): Another git commit hash, after start.
+ depot_name (str): A respository name.
+
+ Returns:
+ A list of pairs (commit hash, commit position), from earliest to latest,
+ for all commits in between the two given commits, not including either
+ of the given commits.
+
+ Raises:
+ urllib2.URLError: The request to gitiles failed.
+ ValueError: The response wasn't valid JSON.
+ KeyError: The JSON didn't contain the expected data.
+ """
+ revisions = _FetchRangeFromGitiles(start, end, depot_name)
+ # The response from gitiles includes the end revision and is ordered
+ # from latest to earliest.
+ return [_CommitPair(r) for r in reversed(revisions[1:])]
+
+
+def _FetchRangeFromGitiles(start, end, depot_name):
+ """Fetches a list of revision dicts from gitiles.
+
+ Make multiple requests to get multiple pages, if necessary.
+ """
+ revisions = []
+ url = _URL_TEMPLATE % (
+ depot_map.DEPOT_PATH_MAP[depot_name], start, end, _PAGE_SIZE)
+ current_page_url = url
+ while True:
+ response = urllib2.urlopen(current_page_url).read()
+ response_json = response[len(_GITILES_PADDING):] # Remove padding.
+ response_dict = json.loads(response_json)
+ revisions.extend(response_dict['log'])
+ if 'next' not in response_dict:
+ break
+ current_page_url = url + '&s=' + response_dict['next']
+ return revisions
+
+
+def _CommitPair(commit_dict):
+ return (commit_dict['commit'],
+ _CommitPositionFromMessage(commit_dict['message']))
+
+
+def _CommitPositionFromMessage(message):
+ for line in reversed(message.splitlines()):
+ if line.startswith('Cr-Commit-Position:'):
+ return line.split('#')[1].split('}')[0]
+ return None
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('start')
+ parser.add_argument('end')
+ parser.add_argument('depot', choices=list(depot_map.DEPOT_PATH_MAP))
+ args = parser.parse_args()
+ revision_pairs = FetchInterveningRevisions(args.start, args.end, args.depot)
+ print json.dumps(revision_pairs)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/catapult/experimental/bisect_lib/fetch_intervening_revisions_test.py b/catapult/experimental/bisect_lib/fetch_intervening_revisions_test.py
new file mode 100755
index 00000000..ee01a0ba
--- /dev/null
+++ b/catapult/experimental/bisect_lib/fetch_intervening_revisions_test.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+import unittest
+
+_CATAPULT_PATH = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.path.pardir, os.path.pardir))
+sys.path.insert(0, os.path.join(_CATAPULT_PATH, 'third_party', 'mock'))
+
+import mock
+
+from bisect_lib import fetch_intervening_revisions
+
+_TEST_DATA = os.path.join(os.path.dirname(__file__), 'test_data')
+
+
+class FetchInterveningRevisionsTest(unittest.TestCase):
+
+ def testFetchInterveningRevisions(self):
+ response = open(os.path.join(_TEST_DATA, 'MOCK_RANGE_RESPONSE_1'))
+ with mock.patch('urllib2.urlopen', mock.MagicMock(return_value=response)):
+ revs = fetch_intervening_revisions.FetchInterveningRevisions(
+ '53fc07eb478520a80af6bf8b62be259bb55db0f1',
+ 'c89130e28fd01062104e1be7f3a6fc3abbb80ca9',
+ depot_name='chromium')
+ self.assertEqual(
+ revs, [
+ ('32ce3b13924d84004a3e05c35942626cbe93cbbd', '356382'),
+ ('07a6d9854efab6677b880defa924758334cfd47d', '356383'),
+ ('22e49fb496d6ffa122c470f6071d47ccb4ccb672', '356384'),
+ ('5dbc149bebecea186b693b3d780b6965eeffed0f', '356385'),
+ ('ebd5f102ee89a4be5c98815c02c444fbf2b6b040', '356386'),
+ ('84f6037e951c21a3b00bd3ddd034f258da6839b5', '356387'),
+ ('48c1471f1f503246dd66753a4c7588d77282d2df', '356388'),
+ ('66aeb2b7084850d09f3fccc7d7467b57e4da1882', '356389'),
+ ('01542ac6d0fbec6aa78e33e6c7ec49a582072ea9', '356390'),
+ ('8414732168a8867a5d6bd45eaade68a5820a9e34', '356391'),
+ ('4f81be50501fbc02d7e44df0d56032e5885e19b6', '356392'),
+ ('7bd1741893bd4e233b5562a6926d7e395d558343', '356393'),
+ ('ee261f306c3c66e96339aa1026d62a6d953302fe', '356394'),
+ ('f1c777e3f97a16cc6a3aa922a23602fa59412989', '356395'),
+ ('8fcc8af20a3d41b0512e3b1486e4dc7de528a72b', '356396'),
+ ('3861789af25e2d3502f0fb7080da5785d31308aa', '356397'),
+ ('6feaa73a54d0515ad2940709161ca0a5ad91d1f8', '356398'),
+ ('2e93263dc74f0496100435e1fd7232e9e8323af0', '356399')
+ ])
+
+ def testFetchInterveningRevisionsPagination(self):
+
+ def MockUrlopen(url):
+ if 's=' not in url:
+ return open(os.path.join(_TEST_DATA, 'MOCK_RANGE_RESPONSE_2_PAGE_1'))
+ return open(os.path.join(_TEST_DATA, 'MOCK_RANGE_RESPONSE_2_PAGE_2'))
+
+ with mock.patch('urllib2.urlopen', MockUrlopen):
+ revs = fetch_intervening_revisions.FetchInterveningRevisions(
+ '7bd1741893bd4e233b5562a6926d7e395d558343',
+ '3861789af25e2d3502f0fb7080da5785d31308aa',
+ depot_name='chromium')
+ self.assertEqual(
+ revs, [
+ ('ee261f306c3c66e96339aa1026d62a6d953302fe', '356394'),
+ ('f1c777e3f97a16cc6a3aa922a23602fa59412989', '356395'),
+ ('8fcc8af20a3d41b0512e3b1486e4dc7de528a72b', '356396'),
+ ])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/catapult/experimental/bisect_lib/fetch_revision_info.py b/catapult/experimental/bisect_lib/fetch_revision_info.py
new file mode 100755
index 00000000..e34977d1
--- /dev/null
+++ b/catapult/experimental/bisect_lib/fetch_revision_info.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Gets information about one commit from gitiles.
+
+Example usage:
+ ./fetch_revision_info.py 343b531d31 chromium
+ ./fetch_revision_info.py 17b4e7450d v8
+"""
+
+import argparse
+import json
+import urllib2
+
+from bisect_lib import depot_map
+
+_GITILES_PADDING = ')]}\'\n'
+_URL_TEMPLATE = 'https://chromium.googlesource.com/%s/+/%s?format=json'
+
+def FetchRevisionInfo(commit_hash, depot_name):
+ """Gets information about a chromium revision."""
+ path = depot_map.DEPOT_PATH_MAP[depot_name]
+ url = _URL_TEMPLATE % (path, commit_hash)
+ response = urllib2.urlopen(url).read()
+ response_json = response[len(_GITILES_PADDING):]
+ response_dict = json.loads(response_json)
+ message = response_dict['message'].splitlines()
+ subject = message[0]
+ body = '\n'.join(message[1:])
+ result = {
+ 'author': response_dict['author']['name'],
+ 'email': response_dict['author']['email'],
+ 'subject': subject,
+ 'body': body,
+ 'date': response_dict['committer']['time'],
+ }
+ return result
+
+
+def Main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('commit_hash')
+ parser.add_argument('depot', choices=list(depot_map.DEPOT_PATH_MAP))
+ args = parser.parse_args()
+ revision_info = FetchRevisionInfo(args.commit_hash, args.depot)
+ print json.dumps(revision_info)
+
+
+if __name__ == '__main__':
+ Main()
diff --git a/catapult/experimental/bisect_lib/fetch_revision_info_test.py b/catapult/experimental/bisect_lib/fetch_revision_info_test.py
new file mode 100755
index 00000000..e538129a
--- /dev/null
+++ b/catapult/experimental/bisect_lib/fetch_revision_info_test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+import unittest
+
+_CATAPULT_PATH = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.path.pardir, os.path.pardir))
+sys.path.insert(0, os.path.join(_CATAPULT_PATH, 'third_party', 'mock'))
+
+import mock
+
+from bisect_lib import fetch_revision_info
+
+_TEST_DATA_PATH = os.path.join(os.path.dirname(__file__), 'test_data')
+_MOCK_RESPONSE_PATH = os.path.join(_TEST_DATA_PATH, 'MOCK_INFO_RESPONSE_FILE')
+
+
+class ChromiumRevisionsTest(unittest.TestCase):
+
+ def testRevisionInfo(self):
+ commit_hash = 'c89130e28fd01062104e1be7f3a6fc3abbb80ca9'
+ with mock.patch('urllib2.urlopen', mock.MagicMock(
+ return_value=open(_MOCK_RESPONSE_PATH))):
+ revision_info = fetch_revision_info.FetchRevisionInfo(
+ commit_hash, depot_name='chromium')
+ self.assertEqual(
+ {
+ 'body': ('\nHiding actions without the toolbar redesign '
+ 'means removing them entirely, so if\nthey exist '
+ 'in the toolbar, they are considered \'visible\' '
+ '(even if they are in\nthe chevron).\n\n'
+ 'BUG=544859\nBUG=548160\n\nReview URL: '
+ 'https://codereview.chromium.org/1414343003\n\n'
+ 'Cr-Commit-Position: refs/heads/master@{#356400}'),
+ 'date': 'Tue Oct 27 21:26:30 2015',
+ 'subject': ('[Extensions] Fix hiding browser actions '
+ 'without the toolbar redesign'),
+ 'email': 'rdevlin.cronin@chromium.org',
+ 'author': 'rdevlin.cronin'
+ },
+ revision_info)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/catapult/experimental/bisect_lib/test_data/MOCK_INFO_RESPONSE_FILE b/catapult/experimental/bisect_lib/test_data/MOCK_INFO_RESPONSE_FILE
index cbc53c50..1df21354 100644
--- a/catapult/experimental/bisect_lib/test_data/MOCK_INFO_RESPONSE_FILE
+++ b/catapult/experimental/bisect_lib/test_data/MOCK_INFO_RESPONSE_FILE
@@ -1,48 +1,48 @@
-)]}'
-{
- "commit": "c89130e28fd01062104e1be7f3a6fc3abbb80ca9",
- "tree": "b6c8947321df51bd03ce118285e243ca4b14a751",
- "parents": [
- "2e93263dc74f0496100435e1fd7232e9e8323af0"
- ],
- "author": {
- "name": "rdevlin.cronin",
- "email": "rdevlin.cronin@chromium.org",
- "time": "Tue Oct 27 21:24:54 2015"
- },
- "committer": {
- "name": "Commit bot",
- "email": "commit-bot@chromium.org",
- "time": "Tue Oct 27 21:26:30 2015"
- },
- "message": "[Extensions] Fix hiding browser actions without the toolbar redesign\n\nHiding actions without the toolbar redesign means removing them entirely, so if\nthey exist in the toolbar, they are considered \u0027visible\u0027 (even if they are in\nthe chevron).\n\nBUG\u003d544859\nBUG\u003d548160\n\nReview URL: https://codereview.chromium.org/1414343003\n\nCr-Commit-Position: refs/heads/master@{#356400}\n",
- "tree_diff": [
- {
- "type": "modify",
- "old_id": "d97ee3abdf40b1ed6217780c38ac7122d4b3caac",
- "old_mode": 33188,
- "old_path": "chrome/browser/extensions/extension_context_menu_model.cc",
- "new_id": "27ef17bab7ebfb1796fdecb8ce0c35199442cf2f",
- "new_mode": 33188,
- "new_path": "chrome/browser/extensions/extension_context_menu_model.cc"
- },
- {
- "type": "modify",
- "old_id": "2150b00ab82755323c069757ae5d05a4a32b91f6",
- "old_mode": 33188,
- "old_path": "chrome/browser/extensions/extension_context_menu_model_unittest.cc",
- "new_id": "ecdd3ba5b65102b7c54f35bb3a6b5bc1510bf442",
- "new_mode": 33188,
- "new_path": "chrome/browser/extensions/extension_context_menu_model_unittest.cc"
- },
- {
- "type": "modify",
- "old_id": "963f4629354c504b34321029a2e63f87355672e0",
- "old_mode": 33188,
- "old_path": "chrome/browser/ui/views/toolbar/chevron_menu_button.cc",
- "new_id": "69b207a671aa2c80f4a9b3a6873c9db727f2e355",
- "new_mode": 33188,
- "new_path": "chrome/browser/ui/views/toolbar/chevron_menu_button.cc"
- }
- ]
-}
+ ]}'
+ {
+ "commit": "c89130e28fd01062104e1be7f3a6fc3abbb80ca9",
+ "tree": "b6c8947321df51bd03ce118285e243ca4b14a751",
+ "parents": [
+ "2e93263dc74f0496100435e1fd7232e9e8323af0"
+ ],
+ "author": {
+ "name": "rdevlin.cronin",
+ "email": "rdevlin.cronin@chromium.org",
+ "time": "Tue Oct 27 21:24:54 2015"
+ },
+ "committer": {
+ "name": "Commit bot",
+ "email": "commit-bot@chromium.org",
+ "time": "Tue Oct 27 21:26:30 2015"
+ },
+ "message": "[Extensions] Fix hiding browser actions without the toolbar redesign\n\nHiding actions without the toolbar redesign means removing them entirely, so if\nthey exist in the toolbar, they are considered \u0027visible\u0027 (even if they are in\nthe chevron).\n\nBUG\u003d544859\nBUG\u003d548160\n\nReview URL: https://codereview.chromium.org/1414343003\n\nCr-Commit-Position: refs/heads/master@{#356400}\n",
+ "tree_diff": [
+ {
+ "type": "modify",
+ "old_id": "d97ee3abdf40b1ed6217780c38ac7122d4b3caac",
+ "old_mode": 33188,
+ "old_path": "chrome/browser/extensions/extension_context_menu_model.cc",
+ "new_id": "27ef17bab7ebfb1796fdecb8ce0c35199442cf2f",
+ "new_mode": 33188,
+ "new_path": "chrome/browser/extensions/extension_context_menu_model.cc"
+ },
+ {
+ "type": "modify",
+ "old_id": "2150b00ab82755323c069757ae5d05a4a32b91f6",
+ "old_mode": 33188,
+ "old_path": "chrome/browser/extensions/extension_context_menu_model_unittest.cc",
+ "new_id": "ecdd3ba5b65102b7c54f35bb3a6b5bc1510bf442",
+ "new_mode": 33188,
+ "new_path": "chrome/browser/extensions/extension_context_menu_model_unittest.cc"
+ },
+ {
+ "type": "modify",
+ "old_id": "963f4629354c504b34321029a2e63f87355672e0",
+ "old_mode": 33188,
+ "old_path": "chrome/browser/ui/views/toolbar/chevron_menu_button.cc",
+ "new_id": "69b207a671aa2c80f4a9b3a6873c9db727f2e355",
+ "new_mode": 33188,
+ "new_path": "chrome/browser/ui/views/toolbar/chevron_menu_button.cc"
+ }
+ ]
+ }
diff --git a/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_FILE b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_1
index bbc00d96..bbc00d96 100644
--- a/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_FILE
+++ b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_1
diff --git a/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_1 b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_1
new file mode 100644
index 00000000..a0a6ccec
--- /dev/null
+++ b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_1
@@ -0,0 +1,42 @@
+)]}'
+{
+ "log": [
+ {
+ "commit": "3861789af25e2d3502f0fb7080da5785d31308aa",
+ "tree": "97c4006bdd97695879ff6e38b6905d628f3ec56a",
+ "parents": [
+ "8fcc8af20a3d41b0512e3b1486e4dc7de528a72b"
+ ],
+ "author": {
+ "name": "v8-autoroll",
+ "email": "v8-autoroll@chromium.org",
+ "time": "Tue Oct 27 21:16:25 2015"
+ },
+ "committer": {
+ "name": "Commit bot",
+ "email": "commit-bot@chromium.org",
+ "time": "Tue Oct 27 21:17:57 2015"
+ },
+ "message": "Update V8 to version 4.8.161.\n\nSummary of changes available at:\nhttps://chromium.googlesource.com/v8/v8/+log/bb6df7e1..1eef3579\n\nPlease follow these instructions for assigning/CC\u0027ing issues:\nhttps://code.google.com/p/v8-wiki/wiki/TriagingIssues\n\nPlease close rolling in case of a roll revert:\nhttps://v8-roll.appspot.com/\n\nTBR\u003dhablich@chromium.org,machenbach@chromium.org,yangguo@chromium.org,vogelheim@chromium.org\n\nReview URL: https://codereview.chromium.org/1430483003\n\nCr-Commit-Position: refs/heads/master@{#356397}\n"
+ },
+ {
+ "commit": "8fcc8af20a3d41b0512e3b1486e4dc7de528a72b",
+ "tree": "a1c05b121ec2e8694dcaca9ef54b9817801d40e6",
+ "parents": [
+ "f1c777e3f97a16cc6a3aa922a23602fa59412989"
+ ],
+ "author": {
+ "name": "oshima",
+ "email": "oshima@chromium.org",
+ "time": "Tue Oct 27 21:16:08 2015"
+ },
+ "committer": {
+ "name": "Commit bot",
+ "email": "commit-bot@chromium.org",
+ "time": "Tue Oct 27 21:16:50 2015"
+ },
+ "message": "Reland of Add dimming to the background during sign in (patchset #1 id:1 of https://codereview.chromium.org/1424513003/ )\n\nReason for revert:\nThe asan issue has been fixed in the new CL https://codereview.chromium.org/1426573004/\n\nOriginal issue\u0027s description:\n\u003e Revert of Add dimming to the background during sign in (patchset #4 id:160001 of https://codereview.chromium.org/1400153002/ )\n\u003e\n\u003e Reason for revert:\n\u003e https://code.google.com/p/chromium/issues/detail?id\u003d547178\n\u003e\n\u003e Original issue\u0027s description:\n\u003e \u003e Add dimming to the background during sign in\n\u003e \u003e\n\u003e \u003e * Add option to put the dim layer at the bottom. Login screen put this dim layer at the bottom of login container containers so that\n\u003e \u003e dim layer stays during login transition.\n\u003e \u003e\n\u003e \u003e BUG\u003d478438\n\u003e \u003e TEST\u003dScreenDimmer.DimAtBottom\n\u003e \u003e\n\u003e \u003e Committed: https://crrev.com/c527600749bfc6970ba39e4ed6b24404b0f7b256\n\u003e \u003e Cr-Commit-Position: refs/heads/master@{#355481}\n\u003e\n\u003e TBR\u003ddzhioev@chromium.org,alemate@chromium.org,oshima@chromium.org\n\u003e NOPRESUBMIT\u003dtrue\n\u003e NOTREECHECKS\u003dtrue\n\u003e NOTRY\u003dtrue\n\u003e BUG\u003d478438\n\u003e\n\u003e Committed: https://crrev.com/ccb7824284975a271967334f86b4fc7a17378de5\n\u003e Cr-Commit-Position: refs/heads/master@{#355885}\n\nTBR\u003ddzhioev@chromium.org,alemate@chromium.org,dalecurtis@chromium.org\nNOPRESUBMIT\u003dtrue\nNOTREECHECKS\u003dtrue\nNOTRY\u003dtrue\nBUG\u003d478438\n\nReview URL: https://codereview.chromium.org/1413523005\n\nCr-Commit-Position: refs/heads/master@{#356396}\n"
+ }
+ ],
+ "next": "f1c777e3f97a16cc6a3aa922a23602fa59412989"
+}
diff --git a/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_2 b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_2
new file mode 100644
index 00000000..3d06cf85
--- /dev/null
+++ b/catapult/experimental/bisect_lib/test_data/MOCK_RANGE_RESPONSE_2_PAGE_2
@@ -0,0 +1,41 @@
+)]}'
+{
+ "log": [
+ {
+ "commit": "f1c777e3f97a16cc6a3aa922a23602fa59412989",
+ "tree": "99b77b34eca35570c99966e478d58e5f24fdafe7",
+ "parents": [
+ "ee261f306c3c66e96339aa1026d62a6d953302fe"
+ ],
+ "author": {
+ "name": "mmenke",
+ "email": "mmenke@chromium.org",
+ "time": "Tue Oct 27 21:06:44 2015"
+ },
+ "committer": {
+ "name": "Commit bot",
+ "email": "commit-bot@chromium.org",
+ "time": "Tue Oct 27 21:07:26 2015"
+ },
+ "message": "Make NetErrorHelper more OOPIF-friendly.\n\nNow create one per frame instead of creating them only for main frames,\nand get the WebFrame directly from a WebFrame rather than by going\nthrough the RenderView.\n\nBUG\u003d543226,529976\n\nReview URL: https://codereview.chromium.org/1406303002\n\nCr-Commit-Position: refs/heads/master@{#356395}\n"
+ },
+ {
+ "commit": "ee261f306c3c66e96339aa1026d62a6d953302fe",
+ "tree": "d72d77753789a7832b3422d267fe40e805f886c9",
+ "parents": [
+ "7bd1741893bd4e233b5562a6926d7e395d558343"
+ ],
+ "author": {
+ "name": "asanka",
+ "email": "asanka@chromium.org",
+ "time": "Tue Oct 27 21:04:28 2015"
+ },
+ "committer": {
+ "name": "Commit bot",
+ "email": "commit-bot@chromium.org",
+ "time": "Tue Oct 27 21:06:01 2015"
+ },
+ "message": "[SafeBrowsing] Block dangerous unchecked downloads based on a Finch trial.\n\nBUG\u003d533579\n\nReview URL: https://codereview.chromium.org/1409003002\n\nCr-Commit-Position: refs/heads/master@{#356394}\n"
+ }
+ ]
+}
diff --git a/catapult/experimental/buildbot/buildbot.py b/catapult/experimental/buildbot/buildbot.py
index b29b7436..cb733f5f 100644
--- a/catapult/experimental/buildbot/buildbot.py
+++ b/catapult/experimental/buildbot/buildbot.py
@@ -81,6 +81,8 @@ def Update(master, builders):
class Builder(object):
+ # pylint: disable=too-many-instance-attributes
+
def __init__(self, master, name, data):
self._master = master
self._name = name
@@ -116,7 +118,7 @@ class Builder(object):
"""
build_numbers = tuple(build_number for build_number in build_numbers
if not (build_number in self._builds and
- self._builds[build_number].complete))
+ self._builds[build_number].complete))
if not build_numbers:
return ()
@@ -214,9 +216,11 @@ class Build(object):
self._complete = not ('currentStep' in data and data['currentStep'])
self._start_time, self._end_time = data['times']
- self._steps = {step_info['name']:
- Step(self._master, self._builder_name, self._number, step_info)
- for step_info in data['steps']}
+ self._steps = {
+ step_info['name']:
+ Step(self._master, self._builder_name, self._number, step_info)
+ for step_info in data['steps']
+ }
def __str__(self):
return str(self.number)
@@ -286,6 +290,8 @@ def _ParseTraceFromLog(log):
class Step(object):
+ # pylint: disable=too-many-instance-attributes
+
def __init__(self, master, builder_name, build_number, data):
self._master = master
self._builder_name = builder_name
@@ -308,15 +314,15 @@ class Step(object):
def __getstate__(self):
return {
- '_master': self._master,
- '_builder_name': self._builder_name,
- '_build_number': self._build_number,
- '_name': self._name,
- '_result': self._result,
- '_start_time': self._start_time,
- '_end_time': self._end_time,
- '_log_link': self._log_link,
- '_results_link': self._results_link,
+ '_master': self._master,
+ '_builder_name': self._builder_name,
+ '_build_number': self._build_number,
+ '_name': self._name,
+ '_result': self._result,
+ '_start_time': self._start_time,
+ '_end_time': self._end_time,
+ '_log_link': self._log_link,
+ '_results_link': self._results_link,
}
def __setstate__(self, state):
@@ -436,7 +442,3 @@ class Step(object):
if self._stack_trace is None:
self._stack_trace = _ParseTraceFromLog(self.log)
return self._stack_trace
-
- @property
- def chrome_stack_trace(self):
- raise NotImplementedError()
diff --git a/catapult/experimental/buildbot/query.py b/catapult/experimental/buildbot/query.py
index 06830367..7f1e938f 100755
--- a/catapult/experimental/buildbot/query.py
+++ b/catapult/experimental/buildbot/query.py
@@ -33,15 +33,15 @@ def QueryBuild(build):
trace_results = step.results['chart_data']['charts'][VALUE_NAME].iteritems()
for user_story_name, user_story_data in trace_results:
revision_data.append({
- 'user_story': user_story_name,
- 'start_time': step.start_time,
- 'end_time': step.end_time,
- 'values': user_story_data['values'],
+ 'user_story': user_story_name,
+ 'start_time': step.start_time,
+ 'end_time': step.end_time,
+ 'values': user_story_data['values'],
})
return {
- 'start_time': build.start_time,
- 'end_time': build.end_time,
- 'user_story_runs': revision_data,
+ 'start_time': build.start_time,
+ 'end_time': build.end_time,
+ 'user_story_runs': revision_data,
}
diff --git a/catapult/experimental/hardware.py b/catapult/experimental/hardware.py
index c67aa403..85d4ffca 100755
--- a/catapult/experimental/hardware.py
+++ b/catapult/experimental/hardware.py
@@ -7,13 +7,16 @@
import csv
import json
+import logging
import sys
import urllib2
_MASTERS = [
'chromium.perf',
+ 'client.catapult',
'tryserver.chromium.perf',
+ 'tryserver.client.catapult',
]
@@ -33,6 +36,7 @@ _KEYS = [
'android device 7',
]
_EXCLUDED_KEYS = frozenset([
+ 'architecture (userland)',
'b directory',
'last puppet run',
'uptime',
@@ -46,9 +50,10 @@ def main():
for master_name in _MASTERS:
master_data = json.load(urllib2.urlopen(
- 'http://build.chromium.org/p/%s/json/slaves' % master_name))
+ 'http://build.chromium.org/p/%s/json/slaves' % master_name))
- slaves = sorted(master_data.iteritems(), key=lambda x: x[1]['builders'])
+ slaves = sorted(master_data.iteritems(),
+ key=lambda x: (x[1]['builders'].keys(), x[0]))
for slave_name, slave_data in slaves:
for builder_name in slave_data['builders']:
row = {
@@ -65,16 +70,22 @@ def main():
if not line:
continue
key, value = line.split(': ')
- if key == 'osfamily':
- key = 'os family'
if key in _EXCLUDED_KEYS:
continue
row[key] = value
+ # Munge keys.
+ row = {key.replace('_', ' '): value for key, value in row.iteritems()}
+ if 'osfamily' in row:
+ row['os family'] = row.pop('osfamily')
if 'product name' not in row and slave_name.startswith('slave'):
row['product name'] = 'Google Compute Engine'
- writer.writerow(row)
+ try:
+ writer.writerow(row)
+ except ValueError:
+ logging.error(row)
+ raise
if __name__ == '__main__':
diff --git a/catapult/experimental/statistical_analysis/__init__.py b/catapult/experimental/statistical_analysis/__init__.py
new file mode 100644
index 00000000..ca3e206f
--- /dev/null
+++ b/catapult/experimental/statistical_analysis/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
diff --git a/catapult/experimental/statistical_analysis/compare_benchmark_results.py b/catapult/experimental/statistical_analysis/compare_benchmark_results.py
new file mode 100755
index 00000000..347c5420
--- /dev/null
+++ b/catapult/experimental/statistical_analysis/compare_benchmark_results.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Calculates statistical hypothesis test for given benchmark results.
+
+Evaluate two benchmark results given as Chart JSON files to determine how
+statistically significantly different they are. This evaluation should be run
+using Chart JSON files created by one of the available benchmarks in
+tools/perf/run_benchmark.
+
+A "benchmark" (e.g. startup.cold.blank_page) includes several "metrics" (e.g.
+first_main_frame_load_time).
+"""
+
+from __future__ import print_function
+import argparse
+import json
+import os
+import sys
+
+sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..')))
+from statistical_analysis import results_stats
+
+
+DEFAULT_SIGNIFICANCE_LEVEL = 0.05
+DEFAULT_STATISTICAL_TEST = results_stats.MANN
+
+
+def LoadJsonFromPath(json_path):
+ """Returns a JSON from specified location."""
+ with open(os.path.abspath(json_path)) as data_file:
+ return json.load(data_file)
+
+
+def PrintOutcomeLine(name, max_name_length, outcome, print_p_value):
+ """Prints a single output line, e.g. 'metric_1 True 0.03'."""
+ print('{:{}}{}'.format(name, max_name_length + 2, outcome[0]), end='')
+ if print_p_value:
+ print('\t{:.10f}'.format(outcome[1]), end='')
+ print()
+
+
+def PrintTestOutcome(test_outcome_dict, test_name, significance_level,
+ print_p_value):
+ """Prints the given test outcomes to the command line.
+
+ Will print the p-values for each metric's outcome if |print_p_value| is True
+ and also prints the name of the executed statistical test and the
+ significance level.
+ """
+ print('Statistical analysis results (True=Performance difference likely)\n'
+ '(Test: {}, Significance Level: {})\n'.format(test_name,
+ significance_level))
+
+ max_metric_name_len = max([len(metric_name) for metric_name in
+ test_outcome_dict])
+
+ for metric_name, outcome in test_outcome_dict.iteritems():
+ PrintOutcomeLine(metric_name, max_metric_name_len, outcome, print_p_value)
+
+
+def PrintPagesetTestOutcome(test_outcome_dict, test_name, significance_level,
+ print_p_value, print_details):
+ """Prints the given test outcomes to the command line.
+
+ Prints a summary combining the p-values of the pageset for each metric. Then
+ prints results for each metric/page combination if |print_details| is True.
+ """
+ print('Statistical analysis results (True=Performance difference likely)\n'
+ '(Test: {}, Significance Level: {})\n'.format(test_name,
+ significance_level))
+
+ # Print summarized version at the top.
+ max_metric_name_len = max([len(metric_name) for metric_name in
+ test_outcome_dict])
+ print('Summary (combined p-values for all pages in pageset):\n')
+ for metric_name, pageset in test_outcome_dict.iteritems():
+ combined_p_value = results_stats.CombinePValues([p[1] for p in
+ pageset.itervalues()])
+ outcome = (combined_p_value < significance_level, combined_p_value)
+ PrintOutcomeLine(metric_name, max_metric_name_len, outcome, print_p_value)
+ print()
+
+ if not print_details:
+ return
+
+ # Print outcome for every metric/page combination.
+ for metric_name, pageset in test_outcome_dict.iteritems():
+ max_page_name_len = max([len(page_name) for page_name in pageset])
+ print('{}:'.format(metric_name))
+ for page_name, page_outcome in pageset.iteritems():
+ PrintOutcomeLine(page_name, max_page_name_len, page_outcome,
+ print_p_value)
+ print()
+
+
+def main(args=None):
+ """Set up parser and run statistical test on given benchmark results.
+
+ Set up command line parser and its arguments. Then load Chart JSONs from
+ given paths, run the specified statistical hypothesis test on the results and
+ print the test outcomes.
+ """
+ if args is None:
+ args = sys.argv[1:]
+
+ parser = argparse.ArgumentParser(description="""Runs statistical significance
+ tests on two given Chart JSON benchmark
+ results produced by the telemetry
+ benchmarks.""")
+
+ parser.add_argument(dest='json_paths', nargs=2, help='JSON file location')
+
+ parser.add_argument('--significance', dest='significance_level',
+ default=DEFAULT_SIGNIFICANCE_LEVEL, type=float,
+ help="""The significance level is the type I error rate,
+ which is the probability of determining that the
+ benchmark results are different although they're not.
+ Default: {}, which is common in statistical hypothesis
+ testing.""".format(DEFAULT_SIGNIFICANCE_LEVEL))
+
+ parser.add_argument('--statistical-test', dest='statistical_test',
+ default=DEFAULT_STATISTICAL_TEST,
+ choices=results_stats.ALL_TEST_OPTIONS,
+ help="""Specifies the statistical hypothesis test that is
+ used. Choices are: Mann-Whitney U-test,
+ Kolmogorov-Smirnov, Welch's t-test. Default: Mann-Whitney
+ U-Test.""")
+
+ parser.add_argument('-p', action='store_true', dest='print_p_value',
+ help="""If the -p flag is set, the output will include
+ the p-value for each metric.""")
+
+ parser.add_argument('-d', action='store_true', dest='print_details',
+ help="""If the -d flag is set, the output will be more
+ detailed for benchmarks containing pagesets, giving
+ results for every metric/page combination after a summary
+ at the top.""")
+
+ args = parser.parse_args(args)
+
+ result_jsons = [LoadJsonFromPath(json_path) for json_path in args.json_paths]
+
+ if (results_stats.DoesChartJSONContainPageset(result_jsons[0]) and
+ results_stats.DoesChartJSONContainPageset(result_jsons[1])):
+ # Benchmark containing a pageset.
+ result_dict_1, result_dict_2 = (
+ [results_stats.CreatePagesetBenchmarkResultDict(result_json)
+ for result_json in result_jsons])
+ test_outcome_dict = results_stats.ArePagesetBenchmarkResultsDifferent(
+ result_dict_1, result_dict_2, args.statistical_test,
+ args.significance_level)
+
+ PrintPagesetTestOutcome(test_outcome_dict, args.statistical_test,
+ args.significance_level, args.print_p_value,
+ args.print_details)
+
+ else:
+ # Benchmark not containing a pageset.
+ # (If only one JSON contains a pageset, results_stats raises an error.)
+ result_dict_1, result_dict_2 = (
+ [results_stats.CreateBenchmarkResultDict(result_json)
+ for result_json in result_jsons])
+ test_outcome_dict = (
+ results_stats.AreBenchmarkResultsDifferent(result_dict_1, result_dict_2,
+ args.statistical_test,
+ args.significance_level))
+
+ PrintTestOutcome(test_outcome_dict, args.statistical_test,
+ args.significance_level, args.print_p_value)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/catapult/experimental/statistical_analysis/results_stats.py b/catapult/experimental/statistical_analysis/results_stats.py
new file mode 100755
index 00000000..7f437b81
--- /dev/null
+++ b/catapult/experimental/statistical_analysis/results_stats.py
@@ -0,0 +1,327 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Statistical hypothesis testing for comparing benchmark results."""
+
+try:
+ import numpy as np
+except ImportError:
+ np = None
+
+try:
+ from scipy import stats
+ import scipy.version
+except ImportError:
+ stats = None
+
+
+MANN = 'mann'
+KOLMOGOROV = 'kolmogorov'
+WELCH = 'welch'
+ALL_TEST_OPTIONS = [MANN, KOLMOGOROV, WELCH]
+
+
+class DictMismatchError(Exception):
+ """Provides exception for result dicts with mismatching keys/metrics."""
+ def __str__(self):
+ return ("Provided benchmark result dicts' keys/metrics do not match. "
+ "Check if they have been created by the same benchmark.")
+
+
+class SampleSizeError(Exception):
+ """Provides exception for sample sizes too small for Mann-Whitney U-test."""
+ def __str__(self):
+ return ('At least one sample size is smaller than 20, which is too small '
+ 'for Mann-Whitney U-test.')
+
+
+class NonNormalSampleError(Exception):
+ """Provides exception for samples that are not normally distributed."""
+ def __str__(self):
+ return ("At least one sample is not normally distributed as required by "
+ "Welch's t-test.")
+
+
+def IsScipyMannTestOneSided():
+ """Checks if Scipy version is < 0.17.0.
+
+ This is the version where stats.mannwhitneyu(...) is changed from returning
+ a one-sided to returning a two-sided p-value.
+ """
+ scipy_version = [int(num) for num in scipy.version.version.split('.')]
+ return scipy_version[0] < 1 and scipy_version[1] < 17
+
+
+def GetChartsFromBenchmarkResultJson(benchmark_result_json):
+ """Returns the 'charts' element from a given Chart JSON.
+
+ Excludes entries that are not list_of_scalar_values and empty entries. Also
+ raises errors for an invalid JSON format or empty 'charts' element.
+
+ Raises:
+ ValueError: Provided chart JSON is either not valid or 'charts' is empty.
+ """
+ try:
+ charts = benchmark_result_json['charts']
+ except KeyError:
+ raise ValueError('Invalid benchmark result format. Make sure input is a '
+ 'Chart-JSON.\nProvided JSON:\n',
+ repr(benchmark_result_json))
+ if not charts:
+ raise ValueError("Invalid benchmark result format. Dict entry 'charts' is "
+ "empty.")
+
+ def IsValidPageContent(page_content):
+ return (page_content['type'] == 'list_of_scalar_values' and
+ 'values' in page_content)
+
+ def CreatePageDict(metric_content):
+ return {page_name: page_content
+ for page_name, page_content in metric_content.iteritems()
+ if IsValidPageContent(page_content)}
+
+ charts_valid_entries_only = {}
+ for metric_name, metric_content in charts.iteritems():
+ inner_page_dict = CreatePageDict(metric_content)
+ if not inner_page_dict:
+ continue
+ charts_valid_entries_only[metric_name] = inner_page_dict
+
+ return charts_valid_entries_only
+
+
+def DoesChartJSONContainPageset(benchmark_result_json):
+ """Checks if given Chart JSON contains results for a pageset.
+
+ A metric in a benchmark NOT containing a pageset contains only two elements
+ ("Only_page_in_this_benchmark" and "Summary", as opposed to "Ex_page_1",
+ "Ex_page_2", ..., and "Summary").
+ """
+ charts = GetChartsFromBenchmarkResultJson(benchmark_result_json)
+
+ arbitrary_metric_in_charts = charts.itervalues().next()
+ return len(arbitrary_metric_in_charts) > 2
+
+
+def CreateBenchmarkResultDict(benchmark_result_json):
+ """Creates a dict of format {metric_name: list of benchmark results}.
+
+ Takes a raw result Chart-JSON produced when using '--output-format=chartjson'
+ for 'run_benchmark'.
+
+ Args:
+ benchmark_result_json: Benchmark result Chart-JSON produced by Telemetry.
+
+ Returns:
+ Dictionary of benchmark results.
+ Example dict entry: 'tab_load_time': [650, 700, ...].
+ """
+ charts = GetChartsFromBenchmarkResultJson(benchmark_result_json)
+
+ benchmark_result_dict = {}
+ for metric_name, metric_content in charts.iteritems():
+ benchmark_result_dict[metric_name] = metric_content['summary']['values']
+
+ return benchmark_result_dict
+
+
+def CreatePagesetBenchmarkResultDict(benchmark_result_json):
+ """Creates a dict of format {metric_name: {page_name: list of page results}}.
+
+ Takes a raw result Chart-JSON produced by 'run_benchmark' when using
+ '--output-format=chartjson' and when specifying a benchmark that has a
+ pageset (e.g. top25mobile). Run 'DoesChartJSONContainPageset' to check if
+ your Chart-JSON contains a pageset.
+
+ Args:
+ benchmark_result_json: Benchmark result Chart-JSON produced by Telemetry.
+
+ Returns:
+ Dictionary of benchmark results.
+ Example dict entry: 'tab_load_time': 'Gmail.com': [650, 700, ...].
+ """
+ charts = GetChartsFromBenchmarkResultJson(benchmark_result_json)
+
+ benchmark_result_dict = {}
+ for metric_name, metric_content in charts.iteritems():
+ benchmark_result_dict[metric_name] = {}
+ for page_name, page_content in metric_content.iteritems():
+ if page_name == 'summary':
+ continue
+ benchmark_result_dict[metric_name][page_name] = page_content['values']
+
+ return benchmark_result_dict
+
+
+def CombinePValues(p_values):
+ """Combines p-values from a number of tests using Fisher's Method.
+
+ The tests the p-values result from must test the same null hypothesis and be
+ independent.
+
+ Args:
+ p_values: List of p-values.
+
+ Returns:
+ combined_p_value: Combined p-value according to Fisher's method.
+ """
+ # TODO (wierichs): Update to use scipy.stats.combine_pvalues(p_values) when
+ # Scipy v0.15.0 becomes available as standard version.
+ if not np:
+ raise ImportError('This function requires Numpy.')
+
+ if not stats:
+ raise ImportError('This function requires Scipy.')
+
+ test_statistic = -2 * np.sum(np.log(p_values))
+ p_value = stats.chi2.sf(test_statistic, 2 * len(p_values))
+ return p_value
+
+
+def IsNormallyDistributed(sample, significance_level=0.05):
+ """Calculates Shapiro-Wilk test for normality for a single sample.
+
+ Note that normality is a requirement for Welch's t-test.
+
+ Args:
+ sample: List of values.
+ significance_level: The significance level the p-value is compared against.
+
+ Returns:
+ is_normally_distributed: Returns True or False.
+ p_value: The calculated p-value.
+ """
+ if not stats:
+ raise ImportError('This function requires Scipy.')
+
+ # pylint: disable=unbalanced-tuple-unpacking
+ _, p_value = stats.shapiro(sample)
+
+ is_normally_distributed = p_value >= significance_level
+ return is_normally_distributed, p_value
+
+
+def AreSamplesDifferent(sample_1, sample_2, test=MANN,
+ significance_level=0.05):
+ """Calculates the specified statistical test for the given samples.
+
+ The null hypothesis for each test is that the two populations that the
+ samples are taken from are not significantly different. Tests are two-tailed.
+
+ Raises:
+ ImportError: Scipy is not installed.
+ SampleSizeError: Sample size is too small for MANN.
+ NonNormalSampleError: Sample is not normally distributed as required by
+ WELCH.
+
+ Args:
+ sample_1: First list of values.
+ sample_2: Second list of values.
+ test: Statistical test that is used.
+ significance_level: The significance level the p-value is compared against.
+
+ Returns:
+ is_different: True or False, depending on the test outcome.
+ p_value: The p-value the test has produced.
+ """
+ if not stats:
+ raise ImportError('This function requires Scipy.')
+
+ if test == MANN:
+ if len(sample_1) < 20 or len(sample_2) < 20:
+ raise SampleSizeError()
+ try:
+ _, p_value = stats.mannwhitneyu(sample_1, sample_2, use_continuity=True)
+ except ValueError:
+ # If sum of ranks of values in |sample_1| and |sample_2| is equal,
+ # scipy.stats.mannwhitneyu raises ValueError. Treat this as a 1.0 p-value
+ # (indistinguishable).
+ return (False, 1.0)
+
+ if IsScipyMannTestOneSided():
+ p_value = p_value * 2 if p_value < 0.5 else 1
+
+ elif test == KOLMOGOROV:
+ _, p_value = stats.ks_2samp(sample_1, sample_2)
+
+ elif test == WELCH:
+ if not (IsNormallyDistributed(sample_1, significance_level)[0] and
+ IsNormallyDistributed(sample_2, significance_level)[0]):
+ raise NonNormalSampleError()
+ _, p_value = stats.ttest_ind(sample_1, sample_2, equal_var=False)
+ # TODO: Add k sample anderson darling test
+
+ is_different = p_value <= significance_level
+ return is_different, p_value
+
+
+def AssertThatKeysMatch(result_dict_1, result_dict_2):
+ """Raises an exception if benchmark dicts do not contain the same metrics."""
+ if result_dict_1.viewkeys() != result_dict_2.viewkeys():
+ raise DictMismatchError()
+
+
+def AreBenchmarkResultsDifferent(result_dict_1, result_dict_2, test=MANN,
+ significance_level=0.05):
+ """Runs the given test on the results of each metric in the benchmarks.
+
+ Checks if the dicts have been created from the same benchmark, i.e. if
+ metric names match (e.g. first_non_empty_paint_time). Then runs the specified
+ statistical test on each metric's samples to find if they vary significantly.
+
+ Args:
+ result_dict_1: Benchmark result dict of format {metric: list of values}.
+ result_dict_2: Benchmark result dict of format {metric: list of values}.
+ test: Statistical test that is used.
+ significance_level: The significance level the p-value is compared against.
+
+ Returns:
+ test_outcome_dict: Format {metric: (bool is_different, p-value)}.
+ """
+ AssertThatKeysMatch(result_dict_1, result_dict_2)
+
+ test_outcome_dict = {}
+ for metric in result_dict_1:
+ is_different, p_value = AreSamplesDifferent(result_dict_1[metric],
+ result_dict_2[metric],
+ test, significance_level)
+ test_outcome_dict[metric] = (is_different, p_value)
+
+ return test_outcome_dict
+
+
+def ArePagesetBenchmarkResultsDifferent(result_dict_1, result_dict_2, test=MANN,
+ significance_level=0.05):
+ """Runs the given test on the results of each metric/page combination.
+
+ Checks if the dicts have been created from the same benchmark, i.e. if metric
+ names and pagesets match (e.g. metric first_non_empty_paint_time and page
+ Google.com). Then runs the specified statistical test on each metric/page
+ combination's sample to find if they vary significantly.
+
+ Args:
+ result_dict_1: Benchmark result dict
+ result_dict_2: Benchmark result dict
+ test: Statistical test that is used.
+ significance_level: The significance level the p-value is compared against.
+
+ Returns:
+ test_outcome_dict: Format {metric: {page: (bool is_different, p-value)}}
+ """
+ AssertThatKeysMatch(result_dict_1, result_dict_2)
+
+ # Pagesets should also match.
+ for metric in result_dict_1.iterkeys():
+ AssertThatKeysMatch(result_dict_1[metric], result_dict_2[metric])
+
+ test_outcome_dict = {}
+ for metric in result_dict_1.iterkeys():
+ test_outcome_dict[metric] = {}
+ for page in result_dict_1[metric]:
+ is_different, p_value = AreSamplesDifferent(result_dict_1[metric][page],
+ result_dict_2[metric][page],
+ test, significance_level)
+ test_outcome_dict[metric][page] = (is_different, p_value)
+
+ return test_outcome_dict
diff --git a/catapult/experimental/statistical_analysis/results_stats_unittest.py b/catapult/experimental/statistical_analysis/results_stats_unittest.py
new file mode 100755
index 00000000..51d12021
--- /dev/null
+++ b/catapult/experimental/statistical_analysis/results_stats_unittest.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Tests for results_stats."""
+
+import os
+import sys
+
+import unittest
+
+try:
+ import numpy as np
+except ImportError:
+ np = None
+
+sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..')))
+from statistical_analysis import results_stats
+
+
+class StatisticalBenchmarkResultsAnalysisTest(unittest.TestCase):
+ """Unit testing of several functions in results_stats."""
+
+ def testGetChartsFromBenchmarkResultJson(self):
+ """Unit test for errors raised when getting the charts element.
+
+ Also makes sure that the 'trace' element is deleted if it exists.
+ """
+ input_json_wrong_format = {'charts_wrong': {}}
+ input_json_empty = {'charts': {}}
+ with self.assertRaises(ValueError):
+ (results_stats.GetChartsFromBenchmarkResultJson(input_json_wrong_format))
+ with self.assertRaises(ValueError):
+ (results_stats.GetChartsFromBenchmarkResultJson(input_json_empty))
+
+ input_json_with_trace = {'charts':
+ {'trace': {},
+ 'Ex_metric_1':
+ {'Ex_page_1': {'type': 'list_of_scalar_values',
+ 'values': [1, 2]},
+ 'Ex_page_2': {'type': 'histogram',
+ 'values': [1, 2]}},
+ 'Ex_metric_2':
+ {'Ex_page_1': {'type': 'list_of_scalar_values'},
+ 'Ex_page_2': {'type': 'list_of_scalar_values',
+ 'values': [1, 2]}}}}
+
+ output = (results_stats.
+ GetChartsFromBenchmarkResultJson(input_json_with_trace))
+ expected_output = {'Ex_metric_1':
+ {'Ex_page_1': {'type': 'list_of_scalar_values',
+ 'values': [1, 2]}},
+ 'Ex_metric_2':
+ {'Ex_page_2': {'type': 'list_of_scalar_values',
+ 'values': [1, 2]}}}
+ self.assertEqual(output, expected_output)
+
+ def testCreateBenchmarkResultDict(self):
+ """Unit test for benchmark result dict created from a benchmark json.
+
+ Creates a json of the format created by tools/perf/run_benchmark and then
+ compares the output dict against an expected predefined output dict.
+ """
+ metric_names = ['messageloop_start_time',
+ 'open_tabs_time',
+ 'window_display_time']
+ metric_values = [[55, 72, 60], [54, 42, 65], [44, 89]]
+
+ input_json = {'charts': {}}
+ for metric, metric_vals in zip(metric_names, metric_values):
+ input_json['charts'][metric] = {'summary':
+ {'values': metric_vals,
+ 'type': 'list_of_scalar_values'}}
+
+ output = results_stats.CreateBenchmarkResultDict(input_json)
+ expected_output = {'messageloop_start_time': [55, 72, 60],
+ 'open_tabs_time': [54, 42, 65],
+ 'window_display_time': [44, 89]}
+
+ self.assertEqual(output, expected_output)
+
+ def testCreatePagesetBenchmarkResultDict(self):
+ """Unit test for pageset benchmark result dict created from benchmark json.
+
+ Creates a json of the format created by tools/perf/run_benchmark when it
+ includes a pageset and then compares the output dict against an expected
+ predefined output dict.
+ """
+ metric_names = ['messageloop_start_time',
+ 'open_tabs_time',
+ 'window_display_time']
+ metric_values = [[55, 72, 60], [54, 42, 65], [44, 89]]
+ page_names = ['Ex_page_1', 'Ex_page_2']
+
+ input_json = {'charts': {}}
+ for metric, metric_vals in zip(metric_names, metric_values):
+ input_json['charts'][metric] = {'summary':
+ {'values': [0, 1, 2, 3],
+ 'type': 'list_of_scalar_values'}}
+ for page in page_names:
+ input_json['charts'][metric][page] = {'values': metric_vals,
+ 'type': 'list_of_scalar_values'}
+
+ output = results_stats.CreatePagesetBenchmarkResultDict(input_json)
+ expected_output = {'messageloop_start_time': {'Ex_page_1': [55, 72, 60],
+ 'Ex_page_2': [55, 72, 60]},
+ 'open_tabs_time': {'Ex_page_1': [54, 42, 65],
+ 'Ex_page_2': [54, 42, 65]},
+ 'window_display_time': {'Ex_page_1': [44, 89],
+ 'Ex_page_2': [44, 89]}}
+
+ self.assertEqual(output, expected_output)
+
+ def testCombinePValues(self):
+ """Unit test for Fisher's Method that combines multiple p-values."""
+ test_p_values = [0.05, 0.04, 0.10, 0.07, 0.01]
+
+ expected_output = 0.00047334256271885721
+ output = results_stats.CombinePValues(test_p_values)
+
+ self.assertEqual(output, expected_output)
+
+ def CreateRandomNormalDistribution(self, mean=0, size=30):
+ """Creates two pseudo random samples for testing in multiple methods."""
+ if not np:
+ raise ImportError('This function requires Numpy.')
+
+ np.random.seed(0)
+ sample = np.random.normal(loc=mean, scale=1, size=size)
+
+ return sample
+
+ def testIsNormallyDistributed(self):
+ """Unit test for values returned when testing for normality."""
+ if not np:
+ self.skipTest("Numpy is not installed.")
+
+ test_samples = [self.CreateRandomNormalDistribution(0),
+ self.CreateRandomNormalDistribution(1)]
+
+ expected_outputs = [(True, 0.5253966450691223),
+ (True, 0.5253913402557373)]
+ for sample, expected_output in zip(test_samples, expected_outputs):
+ output = results_stats.IsNormallyDistributed(sample)
+
+ self.assertEqual(output, expected_output)
+
+ def testAreSamplesDifferent(self):
+ """Unit test for values returned after running the statistical tests.
+
+ Creates two pseudo-random normally distributed samples to run the
+ statistical tests and compares the resulting answer and p-value against
+ their pre-calculated values.
+ """
+ test_samples = [3 * [0, 0, 2, 4, 4], 3 * [5, 5, 7, 9, 9]]
+ with self.assertRaises(results_stats.SampleSizeError):
+ results_stats.AreSamplesDifferent(test_samples[0], test_samples[1],
+ test=results_stats.MANN)
+ with self.assertRaises(results_stats.NonNormalSampleError):
+ results_stats.AreSamplesDifferent(test_samples[0], test_samples[1],
+ test=results_stats.WELCH)
+
+ test_samples_equal = (20 * [1], 20 * [1])
+ expected_output_equal = (False, 1.0)
+ output_equal = results_stats.AreSamplesDifferent(test_samples_equal[0],
+ test_samples_equal[1],
+ test=results_stats.MANN)
+ self.assertEqual(output_equal, expected_output_equal)
+
+ if not np:
+ self.skipTest("Numpy is not installed.")
+
+ test_samples = [self.CreateRandomNormalDistribution(0),
+ self.CreateRandomNormalDistribution(1)]
+ test_options = results_stats.ALL_TEST_OPTIONS
+
+ expected_outputs = [(True, 2 * 0.00068516628052438266),
+ (True, 0.0017459498829507842),
+ (True, 0.00084765230478226514)]
+
+ for test, expected_output in zip(test_options, expected_outputs):
+ output = results_stats.AreSamplesDifferent(test_samples[0],
+ test_samples[1],
+ test=test)
+ self.assertEqual(output, expected_output)
+
+ def testAssertThatKeysMatch(self):
+ """Unit test for exception raised when input dicts' metrics don't match."""
+ differing_input_dicts = [{'messageloop_start_time': [55, 72, 60],
+ 'display_time': [44, 89]},
+ {'messageloop_start_time': [55, 72, 60]}]
+ with self.assertRaises(results_stats.DictMismatchError):
+ results_stats.AssertThatKeysMatch(differing_input_dicts[0],
+ differing_input_dicts[1])
+
+ def testAreBenchmarkResultsDifferent(self):
+ """Unit test for statistical test outcome dict."""
+ test_input_dicts = [{'open_tabs_time':
+ self.CreateRandomNormalDistribution(0),
+ 'display_time':
+ self.CreateRandomNormalDistribution(0)},
+ {'open_tabs_time':
+ self.CreateRandomNormalDistribution(0),
+ 'display_time':
+ self.CreateRandomNormalDistribution(1)}]
+ test_options = results_stats.ALL_TEST_OPTIONS
+
+ expected_outputs = [{'open_tabs_time': (False, 2 * 0.49704973080841425),
+ 'display_time': (True, 2 * 0.00068516628052438266)},
+ {'open_tabs_time': (False, 1.0),
+ 'display_time': (True, 0.0017459498829507842)},
+ {'open_tabs_time': (False, 1.0),
+ 'display_time': (True, 0.00084765230478226514)}]
+
+ for test, expected_output in zip(test_options, expected_outputs):
+ output = results_stats.AreBenchmarkResultsDifferent(test_input_dicts[0],
+ test_input_dicts[1],
+ test=test)
+ self.assertEqual(output, expected_output)
+
+ def testArePagesetBenchmarkResultsDifferent(self):
+ """Unit test for statistical test outcome dict."""
+ distributions = (self.CreateRandomNormalDistribution(0),
+ self.CreateRandomNormalDistribution(1))
+ test_input_dicts = ({'open_tabs_time': {'Ex_page_1': distributions[0],
+ 'Ex_page_2': distributions[0]},
+ 'display_time': {'Ex_page_1': distributions[1],
+ 'Ex_page_2': distributions[1]}},
+ {'open_tabs_time': {'Ex_page_1': distributions[0],
+ 'Ex_page_2': distributions[1]},
+ 'display_time': {'Ex_page_1': distributions[1],
+ 'Ex_page_2': distributions[0]}})
+ test_options = results_stats.ALL_TEST_OPTIONS
+
+ expected_outputs = ({'open_tabs_time': # Mann.
+ {'Ex_page_1': (False, 2 * 0.49704973080841425),
+ 'Ex_page_2': (True, 2 * 0.00068516628052438266)},
+ 'display_time':
+ {'Ex_page_1': (False, 2 * 0.49704973080841425),
+ 'Ex_page_2': (True, 2 * 0.00068516628052438266)}},
+ {'open_tabs_time': # Kolmogorov.
+ {'Ex_page_1': (False, 1.0),
+ 'Ex_page_2': (True, 0.0017459498829507842)},
+ 'display_time':
+ {'Ex_page_1': (False, 1.0),
+ 'Ex_page_2': (True, 0.0017459498829507842)}},
+ {'open_tabs_time': # Welch.
+ {'Ex_page_1': (False, 1.0),
+ 'Ex_page_2': (True, 0.00084765230478226514)},
+ 'display_time':
+ {'Ex_page_1': (False, 1.0),
+ 'Ex_page_2': (True, 0.00084765230478226514)}})
+
+ for test, expected_output in zip(test_options, expected_outputs):
+ output = (results_stats.
+ ArePagesetBenchmarkResultsDifferent(test_input_dicts[0],
+ test_input_dicts[1],
+ test=test))
+ self.assertEqual(output, expected_output)
+
+
+if __name__ == '__main__':
+ sys.exit(unittest.main())