mirror of https://github.com/grpc/grpc.git
[cleanup] Remove old microbenchmarking diff framework (#36952)
There's something new in the works, so it's time that this unmaintained & broken system got garbage collected. Closes #36952 PiperOrigin-RevId: 644184198pull/36960/head
parent
bc26f27c32
commit
13a8023268
16 changed files with 0 additions and 1694 deletions
@ -1,26 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
|
||||
# This script is invoked by Kokoro and runs a diff on the microbenchmarks |
||||
set -ex |
||||
|
||||
# Enter the gRPC repo root |
||||
cd $(dirname $0)/../../.. |
||||
|
||||
source tools/internal_ci/helper_scripts/prepare_build_linux_rc |
||||
|
||||
export DOCKERFILE_DIR=tools/dockerfile/test/cxx_debian11_x64 |
||||
export DOCKER_RUN_SCRIPT=tools/internal_ci/linux/grpc_microbenchmark_diff_in_docker.sh |
||||
exec tools/run_tests/dockerize/build_and_run_docker.sh |
@ -1,32 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
|
||||
set -ex |
||||
|
||||
# Enter the gRPC repo root |
||||
cd $(dirname $0)/../../.. |
||||
|
||||
# some extra pip packages are needed for the check_on_pr.py script to work |
||||
# TODO(jtattermusch): avoid needing to install these pip packages each time |
||||
time python3 -m pip install --user -r tools/internal_ci/helper_scripts/requirements.linux_perf.txt |
||||
|
||||
# List of benchmarks that provide good signal for analyzing performance changes in pull requests |
||||
BENCHMARKS_TO_RUN="bm_fullstack_unary_ping_pong bm_fullstack_streaming_ping_pong bm_fullstack_streaming_pump bm_closure bm_cq bm_chttp2_hpack" |
||||
|
||||
tools/run_tests/start_port_server.py |
||||
|
||||
tools/internal_ci/linux/run_if_c_cpp_modified.sh tools/profiling/microbenchmarks/bm_diff/bm_main.py \ |
||||
-d "origin/$KOKORO_GITHUB_PULL_REQUEST_TARGET_BRANCH" \ |
||||
-b $BENCHMARKS_TO_RUN |
@ -1,33 +0,0 @@ |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
|
||||
# Config file for the internal CI (in protobuf text format) |
||||
|
||||
# Location of the continuous shell script in repository. |
||||
build_file: "grpc/tools/internal_ci/linux/grpc_microbenchmark_diff.sh" |
||||
timeout_mins: 120 |
||||
before_action { |
||||
fetch_keystore { |
||||
keystore_resource { |
||||
keystore_config_id: 73836 |
||||
keyname: "grpc_checks_private_key" |
||||
} |
||||
} |
||||
} |
||||
action { |
||||
define_artifacts { |
||||
regex: "**/*sponge_log.*" |
||||
regex: "github/grpc/reports/**" |
||||
} |
||||
} |
@ -1,4 +0,0 @@ |
||||
Microbenchmarks |
||||
==== |
||||
|
||||
This directory contains helper scripts for the microbenchmark suites. |
@ -1,70 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
|
||||
# Convert google-benchmark json output to something that can be uploaded to |
||||
# BigQuery |
||||
|
||||
import csv |
||||
import json |
||||
import subprocess |
||||
import sys |
||||
|
||||
import bm_json |
||||
|
||||
columns = [] |
||||
|
||||
for row in json.loads( |
||||
# TODO(jtattermusch): make sure the dataset name is not hardcoded |
||||
subprocess.check_output( |
||||
["bq", "--format=json", "show", "microbenchmarks.microbenchmarks"] |
||||
) |
||||
)["schema"]["fields"]: |
||||
columns.append((row["name"], row["type"].lower())) |
||||
|
||||
SANITIZE = { |
||||
"integer": int, |
||||
"float": float, |
||||
"boolean": bool, |
||||
"string": str, |
||||
"timestamp": str, |
||||
} |
||||
|
||||
# TODO(jtattermusch): add proper argparse argument, rather than trying |
||||
# to emulate with manual argv inspection. |
||||
if sys.argv[1] == "--schema": |
||||
print(",\n".join("%s:%s" % (k, t.upper()) for k, t in columns)) |
||||
sys.exit(0) |
||||
|
||||
with open(sys.argv[1]) as f: |
||||
js = json.loads(f.read()) |
||||
|
||||
if len(sys.argv) > 2: |
||||
with open(sys.argv[2]) as f: |
||||
js2 = json.loads(f.read()) |
||||
else: |
||||
js2 = None |
||||
|
||||
# TODO(jtattermusch): write directly to a file instead of stdout |
||||
writer = csv.DictWriter(sys.stdout, [c for c, t in columns]) |
||||
|
||||
for row in bm_json.expand_json(js, js2): |
||||
sane_row = {} |
||||
for name, sql_type in columns: |
||||
if name in row: |
||||
if row[name] == "": |
||||
continue |
||||
sane_row[name] = SANITIZE[sql_type](row[name]) |
||||
writer.writerow(sane_row) |
@ -1,116 +0,0 @@ |
||||
The bm_diff Family |
||||
==== |
||||
|
||||
This family of python scripts can be incredibly useful for fast iteration over |
||||
different performance tweaks. The tools allow you to save performance data from |
||||
a baseline commit, then quickly compare data from your working branch to that |
||||
baseline data to see if you have made any performance wins. |
||||
|
||||
The tools operate with three concrete steps, which can be invoked separately, |
||||
or all together via the driver script, bm_main.py. This readme will describe |
||||
the typical workflow for these scripts, then it will include sections on the |
||||
details of every script for advanced usage. |
||||
|
||||
## Normal Workflow |
||||
|
||||
Let's say you are working on a performance optimization for grpc_error. You have |
||||
made some significant changes and want to see some data. From your branch, run |
||||
(ensure everything is committed first): |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_main.py -b bm_error -l 5 -d master` |
||||
|
||||
This will build the `bm_error` binary on your branch, and then it will checkout |
||||
master and build it there too. It will then run these benchmarks 5 times each. |
||||
Lastly it will compute the statistically significant performance differences |
||||
between the two branches. This should show the nice performance wins your |
||||
changes have made. |
||||
|
||||
If you have already invoked bm_main with `-d master`, you should instead use |
||||
`-o` for subsequent runs. This allows the script to skip re-building and |
||||
re-running the unchanged master branch. For example: |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_main.py -b bm_error -l 5 -o` |
||||
|
||||
This will only build and run `bm_error` on your branch. It will then compare |
||||
the output to the saved runs from master. |
||||
|
||||
## Advanced Workflow |
||||
|
||||
If you have a deeper knowledge of these scripts, you can use them to do more |
||||
fine tuned benchmark comparisons. For example, you could build, run, and save |
||||
the benchmark output from two different base branches. Then you could diff both |
||||
of these baselines against your working branch to see how the different metrics |
||||
change. The rest of this doc goes over the details of what each of the |
||||
individual modules accomplishes. |
||||
|
||||
## bm_build.py |
||||
|
||||
This scrips builds the benchmarks. It takes in a name parameter, and will |
||||
store the binaries based on that. Both `opt` and `counter` configurations |
||||
will be used. The `opt` is used to get cpu_time and real_time, and the |
||||
`counters` build is used to track other metrics like allocs, atomic adds, |
||||
etc etc etc. |
||||
|
||||
For example, if you were to invoke (we assume everything is run from the |
||||
root of the repo): |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_build.py -b bm_error -n baseline` |
||||
|
||||
then the microbenchmark binaries will show up under |
||||
`bm_diff_baseline/{opt,counters}/bm_error` |
||||
|
||||
## bm_run.py |
||||
|
||||
This script runs the benchmarks. It takes a name parameter that must match the |
||||
name that was passed to `bm_build.py`. The script then runs the benchmark |
||||
multiple times (default is 20, can be toggled via the loops parameter). The |
||||
output is saved as `<benchmark name>.<config>.<name>.<loop idx>.json` |
||||
|
||||
For example, if you were to run: |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_run.py -b bm_error -b baseline -l 5` |
||||
|
||||
Then an example output file would be `bm_error.opt.baseline.0.json` |
||||
|
||||
## bm_diff.py |
||||
|
||||
This script takes in the output from two benchmark runs, computes the diff |
||||
between them, and prints any significant improvements or regressions. It takes |
||||
in two name parameters, old and new. These must have previously been built and |
||||
run. |
||||
|
||||
For example, assuming you had already built and run a 'baseline' microbenchmark |
||||
from master, and then you also built and ran a 'current' microbenchmark from |
||||
the branch you were working on, you could invoke: |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_diff.py -b bm_error -o baseline -n current -l 5` |
||||
|
||||
This would output the percent difference between your branch and master. |
||||
|
||||
## bm_main.py |
||||
|
||||
This is the driver script. It uses the previous three modules and does |
||||
everything for you. You pass in the benchmarks to be run, the number of loops, |
||||
number of CPUs to use, and the commit to compare to. Then the script will: |
||||
* Build the benchmarks at head, then checkout the branch to compare to and |
||||
build the benchmarks there |
||||
* Run both sets of microbenchmarks |
||||
* Run bm_diff.py to compare the two, outputs the difference. |
||||
|
||||
For example, one might run: |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_main.py -b bm_error -l 5 -d master` |
||||
|
||||
This would compare the current branch's error benchmarks to master. |
||||
|
||||
This script is invoked by our infrastructure on every PR to protect against |
||||
regressions and demonstrate performance wins. |
||||
|
||||
However, if you are iterating over different performance tweaks quickly, it is |
||||
unnecessary to build and run the baseline commit every time. That is why we |
||||
provide a different flag in case you are sure that the baseline benchmark has |
||||
already been built and run. In that case use the --old flag to pass in the name |
||||
of the baseline. This will only build and run the current branch. For example: |
||||
|
||||
`tools/profiling/microbenchmarks/bm_diff/bm_main.py -b bm_error -l 5 -o old` |
||||
|
@ -1,98 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Python utility to build opt and counters benchmarks """ |
||||
|
||||
import argparse |
||||
import multiprocessing |
||||
import os |
||||
import shutil |
||||
import subprocess |
||||
|
||||
import bm_constants |
||||
|
||||
|
||||
def _args(): |
||||
argp = argparse.ArgumentParser(description="Builds microbenchmarks") |
||||
argp.add_argument( |
||||
"-b", |
||||
"--benchmarks", |
||||
nargs="+", |
||||
choices=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
default=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
help="Which benchmarks to build", |
||||
) |
||||
argp.add_argument( |
||||
"-j", |
||||
"--jobs", |
||||
type=int, |
||||
default=multiprocessing.cpu_count(), |
||||
help=( |
||||
"Deprecated. Bazel chooses number of CPUs to build with" |
||||
" automatically." |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"-n", |
||||
"--name", |
||||
type=str, |
||||
help=( |
||||
"Unique name of this build. To be used as a handle to pass to the" |
||||
" other bm* scripts" |
||||
), |
||||
) |
||||
args = argp.parse_args() |
||||
assert args.name |
||||
return args |
||||
|
||||
|
||||
def _build_cmd(cfg, benchmarks): |
||||
bazel_targets = [ |
||||
"//test/cpp/microbenchmarks:%s" % benchmark for benchmark in benchmarks |
||||
] |
||||
# --dynamic_mode=off makes sure that we get a monolithic binary that can be safely |
||||
# moved outside of the bazel-bin directory |
||||
return [ |
||||
"tools/bazel", |
||||
"build", |
||||
"--config=%s" % cfg, |
||||
"--dynamic_mode=off", |
||||
] + bazel_targets |
||||
|
||||
|
||||
def _build_config_and_copy(cfg, benchmarks, dest_dir): |
||||
"""Build given config and copy resulting binaries to dest_dir/CONFIG""" |
||||
subprocess.check_call(_build_cmd(cfg, benchmarks)) |
||||
cfg_dir = dest_dir + "/%s" % cfg |
||||
os.makedirs(cfg_dir) |
||||
subprocess.check_call( |
||||
["cp"] |
||||
+ [ |
||||
"bazel-bin/test/cpp/microbenchmarks/%s" % benchmark |
||||
for benchmark in benchmarks |
||||
] |
||||
+ [cfg_dir] |
||||
) |
||||
|
||||
|
||||
def build(name, benchmarks, jobs): |
||||
dest_dir = "bm_diff_%s" % name |
||||
shutil.rmtree(dest_dir, ignore_errors=True) |
||||
_build_config_and_copy("opt", benchmarks, dest_dir) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = _args() |
||||
build(args.name, args.benchmarks, args.jobs) |
@ -1,41 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Configurable constants for the bm_*.py family """ |
||||
|
||||
_AVAILABLE_BENCHMARK_TESTS = [ |
||||
"bm_fullstack_unary_ping_pong", |
||||
"bm_fullstack_streaming_ping_pong", |
||||
"bm_fullstack_streaming_pump", |
||||
"bm_closure", |
||||
"bm_cq", |
||||
"bm_chttp2_hpack", |
||||
] |
||||
|
||||
_INTERESTING = ( |
||||
"cpu_time", |
||||
"real_time", |
||||
"locks_per_iteration", |
||||
"allocs_per_iteration", |
||||
"writes_per_iteration", |
||||
"atm_cas_per_iteration", |
||||
"atm_add_per_iteration", |
||||
"nows_per_iteration", |
||||
"cli_transport_stalls_per_iteration", |
||||
"cli_stream_stalls_per_iteration", |
||||
"svr_transport_stalls_per_iteration", |
||||
"svr_stream_stalls_per_iteration", |
||||
"http2_pings_sent_per_iteration", |
||||
) |
@ -1,300 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Computes the diff between two bm runs and outputs significant results """ |
||||
|
||||
import argparse |
||||
import collections |
||||
import json |
||||
import os |
||||
import subprocess |
||||
import sys |
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), "..")) |
||||
|
||||
import bm_constants |
||||
import bm_json |
||||
import bm_speedup |
||||
import tabulate |
||||
|
||||
verbose = False |
||||
|
||||
|
||||
def _median(ary): |
||||
assert len(ary) |
||||
ary = sorted(ary) |
||||
n = len(ary) |
||||
if n % 2 == 0: |
||||
return (ary[(n - 1) // 2] + ary[(n - 1) // 2 + 1]) / 2.0 |
||||
else: |
||||
return ary[n // 2] |
||||
|
||||
|
||||
def _args(): |
||||
argp = argparse.ArgumentParser( |
||||
description="Perform diff on microbenchmarks" |
||||
) |
||||
argp.add_argument( |
||||
"-t", |
||||
"--track", |
||||
choices=sorted(bm_constants._INTERESTING), |
||||
nargs="+", |
||||
default=sorted(bm_constants._INTERESTING), |
||||
help="Which metrics to track", |
||||
) |
||||
argp.add_argument( |
||||
"-b", |
||||
"--benchmarks", |
||||
nargs="+", |
||||
choices=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
default=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
help="Which benchmarks to run", |
||||
) |
||||
argp.add_argument( |
||||
"-l", |
||||
"--loops", |
||||
type=int, |
||||
default=20, |
||||
help=( |
||||
"Number of times to loops the benchmarks. Must match what was" |
||||
" passed to bm_run.py" |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"-r", |
||||
"--regex", |
||||
type=str, |
||||
default="", |
||||
help="Regex to filter benchmarks run", |
||||
) |
||||
argp.add_argument("-n", "--new", type=str, help="New benchmark name") |
||||
argp.add_argument("-o", "--old", type=str, help="Old benchmark name") |
||||
argp.add_argument( |
||||
"-v", "--verbose", type=bool, help="Print details of before/after" |
||||
) |
||||
args = argp.parse_args() |
||||
global verbose |
||||
if args.verbose: |
||||
verbose = True |
||||
assert args.new |
||||
assert args.old |
||||
return args |
||||
|
||||
|
||||
def _maybe_print(str): |
||||
if verbose: |
||||
print(str) |
||||
|
||||
|
||||
class Benchmark: |
||||
def __init__(self): |
||||
self.samples = { |
||||
True: collections.defaultdict(list), |
||||
False: collections.defaultdict(list), |
||||
} |
||||
self.final = {} |
||||
self.speedup = {} |
||||
|
||||
def add_sample(self, track, data, new): |
||||
for f in track: |
||||
if f in data: |
||||
self.samples[new][f].append(float(data[f])) |
||||
|
||||
def process(self, track, new_name, old_name): |
||||
for f in sorted(track): |
||||
new = self.samples[True][f] |
||||
old = self.samples[False][f] |
||||
if not new or not old: |
||||
continue |
||||
mdn_diff = abs(_median(new) - _median(old)) |
||||
_maybe_print( |
||||
"%s: %s=%r %s=%r mdn_diff=%r" |
||||
% (f, new_name, new, old_name, old, mdn_diff) |
||||
) |
||||
s = bm_speedup.speedup(new, old, 1e-5) |
||||
self.speedup[f] = s |
||||
if abs(s) > 3: |
||||
if mdn_diff > 0.5: |
||||
self.final[f] = "%+d%%" % s |
||||
return self.final.keys() |
||||
|
||||
def skip(self): |
||||
return not self.final |
||||
|
||||
def row(self, flds): |
||||
return [self.final[f] if f in self.final else "" for f in flds] |
||||
|
||||
def speedup(self, name): |
||||
if name in self.speedup: |
||||
return self.speedup[name] |
||||
return None |
||||
|
||||
|
||||
def _read_json(filename, badjson_files, nonexistant_files): |
||||
stripped = ".".join(filename.split(".")[:-2]) |
||||
try: |
||||
with open(filename) as f: |
||||
r = f.read() |
||||
return json.loads(r) |
||||
except IOError as e: |
||||
if stripped in nonexistant_files: |
||||
nonexistant_files[stripped] += 1 |
||||
else: |
||||
nonexistant_files[stripped] = 1 |
||||
return None |
||||
except ValueError as e: |
||||
print(r) |
||||
if stripped in badjson_files: |
||||
badjson_files[stripped] += 1 |
||||
else: |
||||
badjson_files[stripped] = 1 |
||||
return None |
||||
|
||||
|
||||
def fmt_dict(d): |
||||
return "".join([" " + k + ": " + str(d[k]) + "\n" for k in d]) |
||||
|
||||
|
||||
def diff(bms, loops, regex, track, old, new): |
||||
benchmarks = collections.defaultdict(Benchmark) |
||||
|
||||
badjson_files = {} |
||||
nonexistant_files = {} |
||||
for bm in bms: |
||||
for loop in range(0, loops): |
||||
for line in subprocess.check_output( |
||||
[ |
||||
"bm_diff_%s/opt/%s" % (old, bm), |
||||
"--benchmark_list_tests", |
||||
"--benchmark_filter=%s" % regex, |
||||
] |
||||
).splitlines(): |
||||
line = line.decode("UTF-8") |
||||
stripped_line = ( |
||||
line.strip() |
||||
.replace("/", "_") |
||||
.replace("<", "_") |
||||
.replace(">", "_") |
||||
.replace(", ", "_") |
||||
) |
||||
js_new_opt = _read_json( |
||||
"%s.%s.opt.%s.%d.json" % (bm, stripped_line, new, loop), |
||||
badjson_files, |
||||
nonexistant_files, |
||||
) |
||||
js_old_opt = _read_json( |
||||
"%s.%s.opt.%s.%d.json" % (bm, stripped_line, old, loop), |
||||
badjson_files, |
||||
nonexistant_files, |
||||
) |
||||
if js_new_opt: |
||||
for row in bm_json.expand_json(js_new_opt): |
||||
name = row["cpp_name"] |
||||
if name.endswith("_mean") or name.endswith("_stddev"): |
||||
continue |
||||
benchmarks[name].add_sample(track, row, True) |
||||
if js_old_opt: |
||||
for row in bm_json.expand_json(js_old_opt): |
||||
name = row["cpp_name"] |
||||
if name.endswith("_mean") or name.endswith("_stddev"): |
||||
continue |
||||
benchmarks[name].add_sample(track, row, False) |
||||
|
||||
really_interesting = set() |
||||
for name, bm in benchmarks.items(): |
||||
_maybe_print(name) |
||||
really_interesting.update(bm.process(track, new, old)) |
||||
fields = [f for f in track if f in really_interesting] |
||||
|
||||
# figure out the significance of the changes... right now we take the 95%-ile |
||||
# benchmark delta %-age, and then apply some hand chosen thresholds |
||||
histogram = [] |
||||
_NOISY = ["BM_WellFlushed"] |
||||
for name, bm in benchmarks.items(): |
||||
if name in _NOISY: |
||||
print( |
||||
"skipping noisy benchmark '%s' for labelling evaluation" % name |
||||
) |
||||
if bm.skip(): |
||||
continue |
||||
d = bm.speedup["cpu_time"] |
||||
if d is None: |
||||
continue |
||||
histogram.append(d) |
||||
histogram.sort() |
||||
print("histogram of speedups: ", histogram) |
||||
if len(histogram) == 0: |
||||
significance = 0 |
||||
else: |
||||
delta = histogram[int(len(histogram) * 0.95)] |
||||
mul = 1 |
||||
if delta < 0: |
||||
delta = -delta |
||||
mul = -1 |
||||
if delta < 2: |
||||
significance = 0 |
||||
elif delta < 5: |
||||
significance = 1 |
||||
elif delta < 10: |
||||
significance = 2 |
||||
else: |
||||
significance = 3 |
||||
significance *= mul |
||||
|
||||
headers = ["Benchmark"] + fields |
||||
rows = [] |
||||
for name in sorted(benchmarks.keys()): |
||||
if benchmarks[name].skip(): |
||||
continue |
||||
rows.append([name] + benchmarks[name].row(fields)) |
||||
note = None |
||||
if len(badjson_files): |
||||
note = ( |
||||
"Corrupt JSON data (indicates timeout or crash): \n%s" |
||||
% fmt_dict(badjson_files) |
||||
) |
||||
if len(nonexistant_files): |
||||
if note: |
||||
note += ( |
||||
"\n\nMissing files (indicates new benchmark): \n%s" |
||||
% fmt_dict(nonexistant_files) |
||||
) |
||||
else: |
||||
note = ( |
||||
"\n\nMissing files (indicates new benchmark): \n%s" |
||||
% fmt_dict(nonexistant_files) |
||||
) |
||||
if rows: |
||||
return ( |
||||
tabulate.tabulate(rows, headers=headers, floatfmt="+.2f"), |
||||
note, |
||||
significance, |
||||
) |
||||
else: |
||||
return None, note, 0 |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = _args() |
||||
diff, note = diff( |
||||
args.benchmarks, |
||||
args.loops, |
||||
args.regex, |
||||
args.track, |
||||
args.old, |
||||
args.new, |
||||
args.counters, |
||||
) |
||||
print("%s\n%s" % (note, diff if diff else "No performance differences")) |
@ -1,182 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Runs the entire bm_*.py pipeline, and possible comments on the PR """ |
||||
|
||||
import argparse |
||||
import multiprocessing |
||||
import os |
||||
import random |
||||
import subprocess |
||||
import sys |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), "..", "..", "run_tests", "python_utils" |
||||
) |
||||
) |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), |
||||
"..", |
||||
"..", |
||||
"..", |
||||
"run_tests", |
||||
"python_utils", |
||||
) |
||||
) |
||||
|
||||
import bm_build |
||||
import bm_constants |
||||
import bm_diff |
||||
import bm_run |
||||
import check_on_pr |
||||
import jobset |
||||
|
||||
|
||||
def _args(): |
||||
argp = argparse.ArgumentParser( |
||||
description="Perform diff on microbenchmarks" |
||||
) |
||||
argp.add_argument( |
||||
"-t", |
||||
"--track", |
||||
choices=sorted(bm_constants._INTERESTING), |
||||
nargs="+", |
||||
default=sorted(bm_constants._INTERESTING), |
||||
help="Which metrics to track", |
||||
) |
||||
argp.add_argument( |
||||
"-b", |
||||
"--benchmarks", |
||||
nargs="+", |
||||
choices=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
default=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
help="Which benchmarks to run", |
||||
) |
||||
argp.add_argument( |
||||
"-d", |
||||
"--diff_base", |
||||
type=str, |
||||
help="Commit or branch to compare the current one to", |
||||
) |
||||
argp.add_argument( |
||||
"-o", |
||||
"--old", |
||||
default="old", |
||||
type=str, |
||||
help='Name of baseline run to compare to. Usually just called "old"', |
||||
) |
||||
argp.add_argument( |
||||
"-r", |
||||
"--regex", |
||||
type=str, |
||||
default="", |
||||
help="Regex to filter benchmarks run", |
||||
) |
||||
argp.add_argument( |
||||
"-l", |
||||
"--loops", |
||||
type=int, |
||||
default=10, |
||||
help=( |
||||
"Number of times to loops the benchmarks. More loops cuts down on" |
||||
" noise" |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"-j", |
||||
"--jobs", |
||||
type=int, |
||||
default=multiprocessing.cpu_count(), |
||||
help="Number of CPUs to use", |
||||
) |
||||
argp.add_argument( |
||||
"--pr_comment_name", |
||||
type=str, |
||||
default="microbenchmarks", |
||||
help="Name that Jenkins will use to comment on the PR", |
||||
) |
||||
args = argp.parse_args() |
||||
assert args.diff_base or args.old, "One of diff_base or old must be set!" |
||||
if args.loops < 3: |
||||
print("WARNING: This run will likely be noisy. Increase loops.") |
||||
return args |
||||
|
||||
|
||||
def eintr_be_gone(fn): |
||||
"""Run fn until it doesn't stop because of EINTR""" |
||||
|
||||
def inner(*args): |
||||
while True: |
||||
try: |
||||
return fn(*args) |
||||
except IOError as e: |
||||
if e.errno != errno.EINTR: |
||||
raise |
||||
|
||||
return inner |
||||
|
||||
|
||||
def main(args): |
||||
bm_build.build("new", args.benchmarks, args.jobs) |
||||
|
||||
old = args.old |
||||
if args.diff_base: |
||||
old = "old" |
||||
where_am_i = subprocess.check_output( |
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"] |
||||
).strip() |
||||
subprocess.check_call(["git", "checkout", args.diff_base]) |
||||
try: |
||||
bm_build.build(old, args.benchmarks, args.jobs) |
||||
finally: |
||||
subprocess.check_call(["git", "checkout", where_am_i]) |
||||
subprocess.check_call(["git", "submodule", "update"]) |
||||
|
||||
jobs_list = [] |
||||
jobs_list += bm_run.create_jobs( |
||||
"new", args.benchmarks, args.loops, args.regex |
||||
) |
||||
jobs_list += bm_run.create_jobs( |
||||
old, args.benchmarks, args.loops, args.regex |
||||
) |
||||
|
||||
# shuffle all jobs to eliminate noise from GCE CPU drift |
||||
random.shuffle(jobs_list, random.SystemRandom().random) |
||||
jobset.run(jobs_list, maxjobs=args.jobs) |
||||
|
||||
diff, note, significance = bm_diff.diff( |
||||
args.benchmarks, args.loops, args.regex, args.track, old, "new" |
||||
) |
||||
if diff: |
||||
text = "[%s] Performance differences noted:\n%s" % ( |
||||
args.pr_comment_name, |
||||
diff, |
||||
) |
||||
else: |
||||
text = ( |
||||
"[%s] No significant performance differences" % args.pr_comment_name |
||||
) |
||||
if note: |
||||
text = note + "\n\n" + text |
||||
print("%s" % text) |
||||
check_on_pr.check_on_pr("Benchmark", "```\n%s\n```" % text) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = _args() |
||||
main(args) |
@ -1,148 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Python utility to run opt and counters benchmarks and save json output """ |
||||
|
||||
import argparse |
||||
import itertools |
||||
import multiprocessing |
||||
import os |
||||
import random |
||||
import subprocess |
||||
import sys |
||||
|
||||
import bm_constants |
||||
import jobset |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), |
||||
"..", |
||||
"..", |
||||
"..", |
||||
"run_tests", |
||||
"python_utils", |
||||
) |
||||
) |
||||
|
||||
|
||||
def _args(): |
||||
argp = argparse.ArgumentParser(description="Runs microbenchmarks") |
||||
argp.add_argument( |
||||
"-b", |
||||
"--benchmarks", |
||||
nargs="+", |
||||
choices=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
default=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
help="Benchmarks to run", |
||||
) |
||||
argp.add_argument( |
||||
"-j", |
||||
"--jobs", |
||||
type=int, |
||||
default=multiprocessing.cpu_count(), |
||||
help="Number of CPUs to use", |
||||
) |
||||
argp.add_argument( |
||||
"-n", |
||||
"--name", |
||||
type=str, |
||||
help=( |
||||
"Unique name of the build to run. Needs to match the handle passed" |
||||
" to bm_build.py" |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"-r", |
||||
"--regex", |
||||
type=str, |
||||
default="", |
||||
help="Regex to filter benchmarks run", |
||||
) |
||||
argp.add_argument( |
||||
"-l", |
||||
"--loops", |
||||
type=int, |
||||
default=20, |
||||
help=( |
||||
"Number of times to loops the benchmarks. More loops cuts down on" |
||||
" noise" |
||||
), |
||||
) |
||||
argp.add_argument("--counters", dest="counters", action="store_true") |
||||
argp.add_argument("--no-counters", dest="counters", action="store_false") |
||||
argp.set_defaults(counters=True) |
||||
args = argp.parse_args() |
||||
assert args.name |
||||
if args.loops < 3: |
||||
print( |
||||
"WARNING: This run will likely be noisy. Increase loops to at " |
||||
"least 3." |
||||
) |
||||
return args |
||||
|
||||
|
||||
def _collect_bm_data(bm, cfg, name, regex, idx, loops): |
||||
jobs_list = [] |
||||
for line in subprocess.check_output( |
||||
[ |
||||
"bm_diff_%s/%s/%s" % (name, cfg, bm), |
||||
"--benchmark_list_tests", |
||||
"--benchmark_filter=%s" % regex, |
||||
] |
||||
).splitlines(): |
||||
line = line.decode("UTF-8") |
||||
stripped_line = ( |
||||
line.strip() |
||||
.replace("/", "_") |
||||
.replace("<", "_") |
||||
.replace(">", "_") |
||||
.replace(", ", "_") |
||||
) |
||||
cmd = [ |
||||
"bm_diff_%s/%s/%s" % (name, cfg, bm), |
||||
"--benchmark_filter=^%s$" % line, |
||||
"--benchmark_out=%s.%s.%s.%s.%d.json" |
||||
% (bm, stripped_line, cfg, name, idx), |
||||
"--benchmark_out_format=json", |
||||
] |
||||
jobs_list.append( |
||||
jobset.JobSpec( |
||||
cmd, |
||||
shortname="%s %s %s %s %d/%d" |
||||
% (bm, line, cfg, name, idx + 1, loops), |
||||
verbose_success=True, |
||||
cpu_cost=2, |
||||
timeout_seconds=60 * 60, |
||||
) |
||||
) # one hour |
||||
return jobs_list |
||||
|
||||
|
||||
def create_jobs(name, benchmarks, loops, regex): |
||||
jobs_list = [] |
||||
for loop in range(0, loops): |
||||
for bm in benchmarks: |
||||
jobs_list += _collect_bm_data(bm, "opt", name, regex, loop, loops) |
||||
random.shuffle(jobs_list, random.SystemRandom().random) |
||||
return jobs_list |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = _args() |
||||
jobs_list = create_jobs( |
||||
args.name, args.benchmarks, args.loops, args.regex, args.counters |
||||
) |
||||
jobset.run(jobs_list, maxjobs=args.jobs) |
@ -1,68 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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 math |
||||
|
||||
from scipy import stats |
||||
|
||||
_DEFAULT_THRESHOLD = 1e-10 |
||||
|
||||
|
||||
def scale(a, mul): |
||||
return [x * mul for x in a] |
||||
|
||||
|
||||
def cmp(a, b): |
||||
return stats.ttest_ind(a, b) |
||||
|
||||
|
||||
def speedup(new, old, threshold=_DEFAULT_THRESHOLD): |
||||
if (len(set(new))) == 1 and new == old: |
||||
return 0 |
||||
s0, p0 = cmp(new, old) |
||||
if math.isnan(p0): |
||||
return 0 |
||||
if s0 == 0: |
||||
return 0 |
||||
if p0 > threshold: |
||||
return 0 |
||||
if s0 < 0: |
||||
pct = 1 |
||||
while pct < 100: |
||||
sp, pp = cmp(new, scale(old, 1 - pct / 100.0)) |
||||
if sp > 0: |
||||
break |
||||
if pp > threshold: |
||||
break |
||||
pct += 1 |
||||
return -(pct - 1) |
||||
else: |
||||
pct = 1 |
||||
while pct < 10000: |
||||
sp, pp = cmp(new, scale(old, 1 + pct / 100.0)) |
||||
if sp < 0: |
||||
break |
||||
if pp > threshold: |
||||
break |
||||
pct += 1 |
||||
return pct - 1 |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
new = [0.0, 0.0, 0.0, 0.0] |
||||
old = [2.96608e-06, 3.35076e-06, 3.45384e-06, 3.34407e-06] |
||||
print(speedup(new, old, 1e-5)) |
||||
print(speedup(old, new, 1e-5)) |
@ -1,214 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
|
||||
# Utilities for manipulating JSON data that represents microbenchmark results. |
||||
|
||||
import os |
||||
|
||||
# template arguments and dynamic arguments of individual benchmark types |
||||
# Example benchmark name: "BM_UnaryPingPong<TCP, NoOpMutator, NoOpMutator>/0/0" |
||||
_BM_SPECS = { |
||||
"BM_UnaryPingPong": { |
||||
"tpl": ["fixture", "client_mutator", "server_mutator"], |
||||
"dyn": ["request_size", "response_size"], |
||||
}, |
||||
"BM_PumpStreamClientToServer": { |
||||
"tpl": ["fixture"], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_PumpStreamServerToClient": { |
||||
"tpl": ["fixture"], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_StreamingPingPong": { |
||||
"tpl": ["fixture", "client_mutator", "server_mutator"], |
||||
"dyn": ["request_size", "request_count"], |
||||
}, |
||||
"BM_StreamingPingPongMsgs": { |
||||
"tpl": ["fixture", "client_mutator", "server_mutator"], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_PumpStreamServerToClient_Trickle": { |
||||
"tpl": [], |
||||
"dyn": ["request_size", "bandwidth_kilobits"], |
||||
}, |
||||
"BM_PumpUnbalancedUnary_Trickle": { |
||||
"tpl": [], |
||||
"dyn": ["cli_req_size", "svr_req_size", "bandwidth_kilobits"], |
||||
}, |
||||
"BM_ErrorStringOnNewError": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_ErrorStringRepeatedly": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_ErrorGetStatus": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_ErrorGetStatusCode": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_ErrorHttpError": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_HasClearGrpcStatus": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_IsolatedFilter": { |
||||
"tpl": ["fixture", "client_mutator"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_HpackEncoderEncodeHeader": { |
||||
"tpl": ["fixture"], |
||||
"dyn": ["end_of_stream", "request_size"], |
||||
}, |
||||
"BM_HpackParserParseHeader": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_CallCreateDestroy": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_Zalloc": { |
||||
"tpl": [], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_PollEmptyPollset_SpeedOfLight": { |
||||
"tpl": [], |
||||
"dyn": ["request_size", "request_count"], |
||||
}, |
||||
"BM_StreamCreateSendInitialMetadataDestroy": { |
||||
"tpl": ["fixture"], |
||||
"dyn": [], |
||||
}, |
||||
"BM_TransportStreamSend": { |
||||
"tpl": [], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_TransportStreamRecv": { |
||||
"tpl": [], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
"BM_StreamingPingPongWithCoalescingApi": { |
||||
"tpl": ["fixture", "client_mutator", "server_mutator"], |
||||
"dyn": ["request_size", "request_count", "end_of_stream"], |
||||
}, |
||||
"BM_Base16SomeStuff": { |
||||
"tpl": [], |
||||
"dyn": ["request_size"], |
||||
}, |
||||
} |
||||
|
||||
|
||||
def numericalize(s): |
||||
"""Convert abbreviations like '100M' or '10k' to a number.""" |
||||
if not s: |
||||
return "" |
||||
if s[-1] == "k": |
||||
return float(s[:-1]) * 1024 |
||||
if s[-1] == "M": |
||||
return float(s[:-1]) * 1024 * 1024 |
||||
if 0 <= (ord(s[-1]) - ord("0")) <= 9: |
||||
return float(s) |
||||
assert "not a number: %s" % s |
||||
|
||||
|
||||
def parse_name(name): |
||||
cpp_name = name |
||||
if "<" not in name and "/" not in name and name not in _BM_SPECS: |
||||
return {"name": name, "cpp_name": name} |
||||
rest = name |
||||
out = {} |
||||
tpl_args = [] |
||||
dyn_args = [] |
||||
if "<" in rest: |
||||
tpl_bit = rest[rest.find("<") + 1 : rest.rfind(">")] |
||||
arg = "" |
||||
nesting = 0 |
||||
for c in tpl_bit: |
||||
if c == "<": |
||||
nesting += 1 |
||||
arg += c |
||||
elif c == ">": |
||||
nesting -= 1 |
||||
arg += c |
||||
elif c == ",": |
||||
if nesting == 0: |
||||
tpl_args.append(arg.strip()) |
||||
arg = "" |
||||
else: |
||||
arg += c |
||||
else: |
||||
arg += c |
||||
tpl_args.append(arg.strip()) |
||||
rest = rest[: rest.find("<")] + rest[rest.rfind(">") + 1 :] |
||||
if "/" in rest: |
||||
s = rest.split("/") |
||||
rest = s[0] |
||||
dyn_args = s[1:] |
||||
name = rest |
||||
assert name in _BM_SPECS, "_BM_SPECS needs to be expanded for %s" % name |
||||
assert len(dyn_args) == len(_BM_SPECS[name]["dyn"]) |
||||
assert len(tpl_args) == len(_BM_SPECS[name]["tpl"]) |
||||
out["name"] = name |
||||
out["cpp_name"] = cpp_name |
||||
out.update( |
||||
dict( |
||||
(k, numericalize(v)) |
||||
for k, v in zip(_BM_SPECS[name]["dyn"], dyn_args) |
||||
) |
||||
) |
||||
out.update(dict(zip(_BM_SPECS[name]["tpl"], tpl_args))) |
||||
return out |
||||
|
||||
|
||||
def expand_json(js): |
||||
if not js: |
||||
raise StopIteration() |
||||
for bm in js["benchmarks"]: |
||||
if bm["name"].endswith("_stddev") or bm["name"].endswith("_mean"): |
||||
continue |
||||
context = js["context"] |
||||
if "label" in bm: |
||||
labels_list = [ |
||||
s.split(":") |
||||
for s in bm["label"].strip().split(" ") |
||||
if len(s) and s[0] != "#" |
||||
] |
||||
for el in labels_list: |
||||
el[0] = el[0].replace("/iter", "_per_iteration") |
||||
labels = dict(labels_list) |
||||
else: |
||||
labels = {} |
||||
# TODO(jtattermusch): grabbing kokoro env values shouldn't be buried |
||||
# deep in the JSON conversion logic. |
||||
# Link the data to a kokoro job run by adding |
||||
# well known kokoro env variables as metadata for each row |
||||
row = { |
||||
"jenkins_build": os.environ.get("KOKORO_BUILD_NUMBER", ""), |
||||
"jenkins_job": os.environ.get("KOKORO_JOB_NAME", ""), |
||||
} |
||||
row.update(context) |
||||
row.update(bm) |
||||
row.update(parse_name(row["name"])) |
||||
row.update(labels) |
||||
yield row |
@ -1,191 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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. |
||||
""" Computes the diff between two qps runs and outputs significant results """ |
||||
|
||||
import argparse |
||||
import json |
||||
import multiprocessing |
||||
import os |
||||
import shutil |
||||
import subprocess |
||||
import sys |
||||
|
||||
import qps_scenarios |
||||
import tabulate |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), "..", "microbenchmarks", "bm_diff" |
||||
) |
||||
) |
||||
import bm_speedup |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), "..", "..", "run_tests", "python_utils" |
||||
) |
||||
) |
||||
import check_on_pr |
||||
|
||||
|
||||
def _args(): |
||||
argp = argparse.ArgumentParser(description="Perform diff on QPS Driver") |
||||
argp.add_argument( |
||||
"-d", |
||||
"--diff_base", |
||||
type=str, |
||||
help="Commit or branch to compare the current one to", |
||||
) |
||||
argp.add_argument( |
||||
"-l", |
||||
"--loops", |
||||
type=int, |
||||
default=4, |
||||
help=( |
||||
"Number of loops for each benchmark. More loops cuts down on noise" |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"-j", |
||||
"--jobs", |
||||
type=int, |
||||
default=multiprocessing.cpu_count(), |
||||
help="Number of CPUs to use", |
||||
) |
||||
args = argp.parse_args() |
||||
assert args.diff_base, "diff_base must be set" |
||||
return args |
||||
|
||||
|
||||
def _make_cmd(jobs): |
||||
return ["make", "-j", "%d" % jobs, "qps_json_driver", "qps_worker"] |
||||
|
||||
|
||||
def build(name, jobs): |
||||
shutil.rmtree("qps_diff_%s" % name, ignore_errors=True) |
||||
subprocess.check_call(["git", "submodule", "update"]) |
||||
try: |
||||
subprocess.check_call(_make_cmd(jobs)) |
||||
except subprocess.CalledProcessError as e: |
||||
subprocess.check_call(["make", "clean"]) |
||||
subprocess.check_call(_make_cmd(jobs)) |
||||
os.rename("bins", "qps_diff_%s" % name) |
||||
|
||||
|
||||
def _run_cmd(name, scenario, fname): |
||||
return [ |
||||
"qps_diff_%s/opt/qps_json_driver" % name, |
||||
"--scenarios_json", |
||||
scenario, |
||||
"--json_file_out", |
||||
fname, |
||||
] |
||||
|
||||
|
||||
def run(name, scenarios, loops): |
||||
for sn in scenarios: |
||||
for i in range(0, loops): |
||||
fname = "%s.%s.%d.json" % (sn, name, i) |
||||
subprocess.check_call(_run_cmd(name, scenarios[sn], fname)) |
||||
|
||||
|
||||
def _load_qps(fname): |
||||
try: |
||||
with open(fname) as f: |
||||
return json.loads(f.read())["qps"] |
||||
except IOError as e: |
||||
print(("IOError occurred reading file: %s" % fname)) |
||||
return None |
||||
except ValueError as e: |
||||
print(("ValueError occurred reading file: %s" % fname)) |
||||
return None |
||||
|
||||
|
||||
def _median(ary): |
||||
assert len(ary) |
||||
ary = sorted(ary) |
||||
n = len(ary) |
||||
if n % 2 == 0: |
||||
return (ary[(n - 1) / 2] + ary[(n - 1) / 2 + 1]) / 2.0 |
||||
else: |
||||
return ary[n / 2] |
||||
|
||||
|
||||
def diff(scenarios, loops, old, new): |
||||
old_data = {} |
||||
new_data = {} |
||||
|
||||
# collect data |
||||
for sn in scenarios: |
||||
old_data[sn] = [] |
||||
new_data[sn] = [] |
||||
for i in range(loops): |
||||
old_data[sn].append(_load_qps("%s.%s.%d.json" % (sn, old, i))) |
||||
new_data[sn].append(_load_qps("%s.%s.%d.json" % (sn, new, i))) |
||||
|
||||
# crunch data |
||||
headers = ["Benchmark", "qps"] |
||||
rows = [] |
||||
for sn in scenarios: |
||||
mdn_diff = abs(_median(new_data[sn]) - _median(old_data[sn])) |
||||
print( |
||||
"%s: %s=%r %s=%r mdn_diff=%r" |
||||
% (sn, new, new_data[sn], old, old_data[sn], mdn_diff) |
||||
) |
||||
s = bm_speedup.speedup(new_data[sn], old_data[sn], 10e-5) |
||||
if abs(s) > 3 and mdn_diff > 0.5: |
||||
rows.append([sn, "%+d%%" % s]) |
||||
|
||||
if rows: |
||||
return tabulate.tabulate(rows, headers=headers, floatfmt="+.2f") |
||||
else: |
||||
return None |
||||
|
||||
|
||||
def main(args): |
||||
build("new", args.jobs) |
||||
|
||||
if args.diff_base: |
||||
where_am_i = ( |
||||
subprocess.check_output( |
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"] |
||||
) |
||||
.decode() |
||||
.strip() |
||||
) |
||||
subprocess.check_call(["git", "checkout", args.diff_base]) |
||||
try: |
||||
build("old", args.jobs) |
||||
finally: |
||||
subprocess.check_call(["git", "checkout", where_am_i]) |
||||
subprocess.check_call(["git", "submodule", "update"]) |
||||
|
||||
run("new", qps_scenarios._SCENARIOS, args.loops) |
||||
run("old", qps_scenarios._SCENARIOS, args.loops) |
||||
|
||||
diff_output = diff(qps_scenarios._SCENARIOS, args.loops, "old", "new") |
||||
|
||||
if diff_output: |
||||
text = "[qps] Performance differences noted:\n%s" % diff_output |
||||
else: |
||||
text = "[qps] No significant performance differences" |
||||
print(("%s" % text)) |
||||
check_on_pr.check_on_pr("QPS", "```\n%s\n```" % text) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = _args() |
||||
main(args) |
@ -1,168 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# Copyright 2017 gRPC authors. |
||||
# |
||||
# 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 argparse |
||||
import html |
||||
import multiprocessing |
||||
import os |
||||
import subprocess |
||||
import sys |
||||
|
||||
import python_utils.jobset as jobset |
||||
import python_utils.start_port_server as start_port_server |
||||
|
||||
sys.path.append( |
||||
os.path.join( |
||||
os.path.dirname(sys.argv[0]), |
||||
"..", |
||||
"profiling", |
||||
"microbenchmarks", |
||||
"bm_diff", |
||||
) |
||||
) |
||||
import bm_constants |
||||
|
||||
flamegraph_dir = os.path.join(os.path.expanduser("~"), "FlameGraph") |
||||
|
||||
os.chdir(os.path.join(os.path.dirname(sys.argv[0]), "../..")) |
||||
if not os.path.exists("reports"): |
||||
os.makedirs("reports") |
||||
|
||||
start_port_server.start_port_server() |
||||
|
||||
|
||||
def fnize(s): |
||||
out = "" |
||||
for c in s: |
||||
if c in "<>, /": |
||||
if len(out) and out[-1] == "_": |
||||
continue |
||||
out += "_" |
||||
else: |
||||
out += c |
||||
return out |
||||
|
||||
|
||||
# index html |
||||
index_html = """ |
||||
<html> |
||||
<head> |
||||
<title>Microbenchmark Results</title> |
||||
</head> |
||||
<body> |
||||
""" |
||||
|
||||
|
||||
def heading(name): |
||||
global index_html |
||||
index_html += "<h1>%s</h1>\n" % name |
||||
|
||||
|
||||
def link(txt, tgt): |
||||
global index_html |
||||
index_html += '<p><a href="%s">%s</a></p>\n' % ( |
||||
html.escape(tgt, quote=True), |
||||
html.escape(txt), |
||||
) |
||||
|
||||
|
||||
def text(txt): |
||||
global index_html |
||||
index_html += "<p><pre>%s</pre></p>\n" % html.escape(txt) |
||||
|
||||
|
||||
def _bazel_build_benchmark(bm_name, cfg): |
||||
"""Build given benchmark with bazel""" |
||||
subprocess.check_call( |
||||
[ |
||||
"tools/bazel", |
||||
"build", |
||||
"--config=%s" % cfg, |
||||
"//test/cpp/microbenchmarks:%s" % bm_name, |
||||
] |
||||
) |
||||
|
||||
|
||||
def run_summary(bm_name, cfg, base_json_name): |
||||
_bazel_build_benchmark(bm_name, cfg) |
||||
cmd = [ |
||||
"bazel-bin/test/cpp/microbenchmarks/%s" % bm_name, |
||||
"--benchmark_out=%s.%s.json" % (base_json_name, cfg), |
||||
"--benchmark_out_format=json", |
||||
] |
||||
if args.summary_time is not None: |
||||
cmd += ["--benchmark_min_time=%d" % args.summary_time] |
||||
return subprocess.check_output(cmd).decode("UTF-8") |
||||
|
||||
|
||||
def collect_summary(bm_name, args): |
||||
# no counters, run microbenchmark and add summary |
||||
# both to HTML report and to console. |
||||
nocounters_heading = "Summary: %s" % bm_name |
||||
nocounters_summary = run_summary(bm_name, "opt", bm_name) |
||||
heading(nocounters_heading) |
||||
text(nocounters_summary) |
||||
print(nocounters_heading) |
||||
print(nocounters_summary) |
||||
|
||||
|
||||
collectors = { |
||||
"summary": collect_summary, |
||||
} |
||||
|
||||
argp = argparse.ArgumentParser(description="Collect data from microbenchmarks") |
||||
argp.add_argument( |
||||
"-c", |
||||
"--collect", |
||||
choices=sorted(collectors.keys()), |
||||
nargs="*", |
||||
default=sorted(collectors.keys()), |
||||
help="Which collectors should be run against each benchmark", |
||||
) |
||||
argp.add_argument( |
||||
"-b", |
||||
"--benchmarks", |
||||
choices=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
default=bm_constants._AVAILABLE_BENCHMARK_TESTS, |
||||
nargs="+", |
||||
type=str, |
||||
help="Which microbenchmarks should be run", |
||||
) |
||||
argp.add_argument( |
||||
"--bq_result_table", |
||||
default="", |
||||
type=str, |
||||
help=( |
||||
"Upload results from summary collection to a specified bigquery table." |
||||
), |
||||
) |
||||
argp.add_argument( |
||||
"--summary_time", |
||||
default=None, |
||||
type=int, |
||||
help="Minimum time to run benchmarks for the summary collection", |
||||
) |
||||
args = argp.parse_args() |
||||
|
||||
try: |
||||
for collect in args.collect: |
||||
for bm_name in args.benchmarks: |
||||
collectors[collect](bm_name, args) |
||||
finally: |
||||
if not os.path.exists("reports"): |
||||
os.makedirs("reports") |
||||
index_html += "</body>\n</html>\n" |
||||
with open("reports/index.html", "w") as f: |
||||
f.write(index_html) |
Loading…
Reference in new issue