Skip to content

Commit

Permalink
Auto merge of #19207 - asajeffrey:test-perf-add-base-url-option, r=ed…
Browse files Browse the repository at this point in the history
…unham

Add --base option to test-perf.

<!-- Please describe your changes on the following line: -->

Add a `--base <URL>` option to test-perf, which is handy for two reasons: a) it reduces randomness in tests by allowing tests to be file URLs, and b) it doesn't require running the test suite as root (the tp5n manifest hardwires tests served from port 80 on localhost).

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: -->
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes do not require tests because this is test infrastructure

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/19207)
<!-- Reviewable:end -->
  • Loading branch information
bors-servo committed Nov 14, 2017
2 parents 0085666 + 109df33 commit 5f33d35
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 31 deletions.
34 changes: 34 additions & 0 deletions etc/ci/performance/README.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -66,6 +66,40 @@ Individual test can be run by `python -m pytest <filename>`:


# Advanced Usage # Advanced Usage


## Reducing variance

Running the same performance test results in a lot of variance, caused
by the OS the test is running on. Experimentally, the things which
seem to tame randomness the most are a) disbling CPU frequency
changes, b) increasing the priority of the tests, c) running one one
CPU core, d) loading files directly rather than via localhost http,
and e) serving files from memory rather than from disk.

First run the performance tests normally (this downloads the test suite):
```
./mach test-perf
```
Disable CPU frequency changes, e.g. on Linux:
```
sudo cpupower frequency-set --min 3.5GHz --max 3.5GHz
```
Copy the test files to a `tmpfs` file,
such as `/run/user/`, for example if your `uid` is `1000`:
```
cp -r etc/ci/performance /run/user/1000
```
Then run the test suite on one core, at high priority, using a `file://` base URL:
```
sudo nice --20 chrt -r 99 sudo -u *userid* taskset 1 ./mach test-perf --base file:///run/user/1000/performance/
```
These fixes seem to take down the variance for most tests to under 5% for individual
tests, and under 0.5% total.

(IRC logs:
[2017-11-09](https://mozilla.logbot.info/servo/20171109#c13829674) |
[2017-11-10](https://mozilla.logbot.info/servo/20171110#c13835736)
)

## Test Perfherder Locally ## Test Perfherder Locally


If you want to test the data submission code in `submit_to_perfherder.py` without getting a credential for the production server, you can setup a local treeherder VM. If you don't need to test `submit_to_perfherder.py`, you can skip this step. If you want to test the data submission code in `submit_to_perfherder.py` without getting a credential for the production server, you can setup a local treeherder VM. If you don't need to test `submit_to_perfherder.py`, you can skip this step.
Expand Down
4 changes: 2 additions & 2 deletions etc/ci/performance/gecko_driver.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ def generate_placeholder(testcase):
return [timings] return [timings]




def run_gecko_test(testcase, timeout, is_async): def run_gecko_test(testcase, url, timeout, is_async):
with create_gecko_session() as driver: with create_gecko_session() as driver:
driver.set_page_load_timeout(timeout) driver.set_page_load_timeout(timeout)
try: try:
driver.get(testcase) driver.get(url)
except TimeoutException: except TimeoutException:
print("Timeout!") print("Timeout!")
return generate_placeholder(testcase) return generate_placeholder(testcase)
Expand Down
57 changes: 41 additions & 16 deletions etc/ci/performance/runner.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import subprocess import subprocess
from functools import partial from functools import partial
from statistics import median, StatisticsError from statistics import median, StatisticsError
from urllib.parse import urlsplit, urlunsplit, urljoin




def load_manifest(filename): def load_manifest(filename):
Expand All @@ -31,6 +32,17 @@ def parse_manifest(text):
return output return output




def testcase_url(base, testcase):
# The tp5 manifest hardwires http://localhost/ as the base URL,
# which requires running the server as root in order to open
# the server on port 80. To allow non-root users to run the test
# case, we take the URL to be relative to a base URL.
(scheme, netloc, path, query, fragment) = urlsplit(testcase)
relative_url = urlunsplit(('', '', '.' + path, query, fragment))
absolute_url = urljoin(base, relative_url)
return absolute_url


def execute_test(url, command, timeout): def execute_test(url, command, timeout):
try: try:
return subprocess.check_output( return subprocess.check_output(
Expand All @@ -46,11 +58,11 @@ def execute_test(url, command, timeout):
return "" return ""




def run_servo_test(url, timeout, is_async): def run_servo_test(testcase, url, timeout, is_async):
if is_async: if is_async:
print("Servo does not support async test!") print("Servo does not support async test!")
# Return a placeholder # Return a placeholder
return parse_log("", url) return parse_log("", testcase, url)


ua_script_path = "{}/user-agent-js".format(os.getcwd()) ua_script_path = "{}/user-agent-js".format(os.getcwd())
command = [ command = [
Expand All @@ -71,11 +83,11 @@ def run_servo_test(url, timeout, is_async):
' '.join(command) ' '.join(command)
)) ))
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
print("Test FAILED due to timeout: {}".format(url)) print("Test FAILED due to timeout: {}".format(testcase))
return parse_log(log, url) return parse_log(log, testcase, url)




def parse_log(log, testcase): def parse_log(log, testcase, url):
blocks = [] blocks = []
block = [] block = []
copy = False copy = False
Expand Down Expand Up @@ -112,11 +124,11 @@ def parse_block(block):


return timing return timing


def valid_timing(timing, testcase=None): def valid_timing(timing, url=None):
if (timing is None or if (timing is None or
testcase is None or testcase is None or
timing.get('title') == 'Error response' or timing.get('title') == 'Error response' or
timing.get('testcase') != testcase): timing.get('testcase') != url):
return False return False
else: else:
return True return True
Expand Down Expand Up @@ -152,8 +164,15 @@ def create_placeholder(testcase):
"domComplete": -1, "domComplete": -1,
} }


valid_timing_for_case = partial(valid_timing, testcase=testcase) # Set the testcase field to contain the original testcase name,
timings = list(filter(valid_timing_for_case, map(parse_block, blocks))) # rather than the url.
def set_testcase(timing, testcase=None):
timing['testcase'] = testcase
return timing

valid_timing_for_case = partial(valid_timing, url=url)
set_testcase_for_case = partial(set_testcase, testcase=testcase)
timings = list(map(set_testcase_for_case, filter(valid_timing_for_case, map(parse_block, blocks))))


if len(timings) == 0: if len(timings) == 0:
print("Didn't find any perf data in the log, test timeout?") print("Didn't find any perf data in the log, test timeout?")
Expand All @@ -167,10 +186,11 @@ def create_placeholder(testcase):
return timings return timings




def filter_result_by_manifest(result_json, manifest): def filter_result_by_manifest(result_json, manifest, base):
filtered = [] filtered = []
for name, is_async in manifest: for name, is_async in manifest:
match = [tc for tc in result_json if tc['testcase'] == name] url = testcase_url(base, name)
match = [tc for tc in result_json if tc['testcase'] == url]
if len(match) == 0: if len(match) == 0:
raise Exception(("Missing test result: {}. This will cause a " raise Exception(("Missing test result: {}. This will cause a "
"discontinuity in the treeherder graph, " "discontinuity in the treeherder graph, "
Expand Down Expand Up @@ -201,9 +221,9 @@ def take_result_median(result_json, expected_runs):
return median_results return median_results




def save_result_json(results, filename, manifest, expected_runs): def save_result_json(results, filename, manifest, expected_runs, base):


results = filter_result_by_manifest(results, manifest) results = filter_result_by_manifest(results, manifest, base)
results = take_result_median(results, expected_runs) results = take_result_median(results, expected_runs)


if len(results) == 0: if len(results) == 0:
Expand Down Expand Up @@ -245,6 +265,10 @@ def main():
help="the test manifest in tp5 format") help="the test manifest in tp5 format")
parser.add_argument("output_file", parser.add_argument("output_file",
help="filename for the output json") help="filename for the output json")
parser.add_argument("--base",
type=str,
default='http://localhost:8000/',
help="the base URL for tests. Default: http://localhost:8000/")
parser.add_argument("--runs", parser.add_argument("--runs",
type=int, type=int,
default=20, default=20,
Expand All @@ -270,18 +294,19 @@ def main():
testcases = load_manifest(args.tp5_manifest) testcases = load_manifest(args.tp5_manifest)
results = [] results = []
for testcase, is_async in testcases: for testcase, is_async in testcases:
url = testcase_url(args.base, testcase)
for run in range(args.runs): for run in range(args.runs):
print("Running test {}/{} on {}".format(run + 1, print("Running test {}/{} on {}".format(run + 1,
args.runs, args.runs,
testcase)) url))
# results will be a mixure of timings dict and testcase strings # results will be a mixure of timings dict and testcase strings
# testcase string indicates a failed test # testcase string indicates a failed test
results += run_test(testcase, args.timeout, is_async) results += run_test(testcase, url, args.timeout, is_async)
print("Finished") print("Finished")
# TODO: Record and analyze other performance.timing properties # TODO: Record and analyze other performance.timing properties


print(format_result_summary(results)) print(format_result_summary(results))
save_result_json(results, args.output_file, testcases, args.runs) save_result_json(results, args.output_file, testcases, args.runs, args.base)


except KeyboardInterrupt: except KeyboardInterrupt:
print("Test stopped by user, saving partial result") print("Test stopped by user, saving partial result")
Expand Down
8 changes: 7 additions & 1 deletion etc/ci/performance/test_all.sh
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ set -o errexit
set -o nounset set -o nounset
set -o pipefail set -o pipefail


base="http://localhost:8000"

while (( "${#}" )) while (( "${#}" ))
do do
case "${1}" in case "${1}" in
Expand All @@ -22,6 +24,10 @@ case "${1}" in
--submit) --submit)
submit=1 submit=1
;; ;;
--base)
base="${2}"
shift
;;
*) *)
echo "Unknown option ${1}." echo "Unknown option ${1}."
exit exit
Expand All @@ -46,7 +52,7 @@ MANIFEST="page_load_test/example.manifest" # A manifest that excludes
PERF_FILE="output/perf-$(date +%s).json" PERF_FILE="output/perf-$(date +%s).json"


echo "Running tests" echo "Running tests"
python3 runner.py ${engine} --runs 3 --timeout "${timeout}" \ python3 runner.py ${engine} --runs 4 --timeout "${timeout}" --base "${base}" \
"${MANIFEST}" "${PERF_FILE}" "${MANIFEST}" "${PERF_FILE}"


if [[ "${submit:-}" ]]; if [[ "${submit:-}" ]];
Expand Down
11 changes: 1 addition & 10 deletions etc/ci/performance/test_perf.sh
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -35,13 +35,4 @@ mkdir -p servo
mkdir -p output # Test result will be saved to output/perf-<timestamp>.json mkdir -p output # Test result will be saved to output/perf-<timestamp>.json
./git_log_to_json.sh > servo/revision.json ./git_log_to_json.sh > servo/revision.json


if [[ "${#}" -eq 1 ]]; then ./test_all.sh --servo ${*}
if [[ "${1}" = "--submit" ]]; then
./test_all.sh --servo --submit
else
echo "Unrecognized argument: ${1}; Ignore and proceed without submitting"
./test_all.sh --servo
fi
else
./test_all.sh --servo
fi
8 changes: 6 additions & 2 deletions python/servo/testing_commands.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -173,14 +173,18 @@ def suite_for_path(self, path_arg):
@Command('test-perf', @Command('test-perf',
description='Run the page load performance test', description='Run the page load performance test',
category='testing') category='testing')
@CommandArgument('--submit', default=False, action="store_true", @CommandArgument('--base', default=None,
help="the base URL for testcases")
@CommandArgument('-submit', '-a', default=False, action="store_true",
help="submit the data to perfherder") help="submit the data to perfherder")
def test_perf(self, submit=False): def test_perf(self, base=None, submit=False):
self.set_software_rendering_env(True) self.set_software_rendering_env(True)


self.ensure_bootstrapped() self.ensure_bootstrapped()
env = self.build_env() env = self.build_env()
cmd = ["bash", "test_perf.sh"] cmd = ["bash", "test_perf.sh"]
if base:
cmd += ["--base", base]
if submit: if submit:
if not ("TREEHERDER_CLIENT_ID" in os.environ and if not ("TREEHERDER_CLIENT_ID" in os.environ and
"TREEHERDER_CLIENT_SECRET" in os.environ): "TREEHERDER_CLIENT_SECRET" in os.environ):
Expand Down

0 comments on commit 5f33d35

Please sign in to comment.