add daemon support, fix the rebuild cases by using an installer config Change-Id: I47cbb0dd401abf5e041e9936077e502f5e0cabf9
185 lines
6.5 KiB
Python
Executable File
185 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env nix-shell
|
|
#!nix-shell -i python3 -p python3 -p hyperfine -p "if stdenv.isLinux then linuxPackages.perf else null"
|
|
|
|
import argparse
|
|
import subprocess
|
|
import os
|
|
import json
|
|
import tempfile
|
|
import platform
|
|
import shlex
|
|
import textwrap
|
|
|
|
flake_args = ["--extra-experimental-features", "nix-command flakes"]
|
|
cases = {
|
|
"search": lambda build: [
|
|
f"{build}/bin/nix",
|
|
*flake_args,
|
|
"search",
|
|
"--no-eval-cache",
|
|
"github:nixos/nixpkgs/e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
|
|
"hello",
|
|
],
|
|
"rebuild": lambda build: [
|
|
f"{build}/bin/nix",
|
|
*flake_args,
|
|
"eval",
|
|
"--raw",
|
|
"--impure",
|
|
"--expr",
|
|
textwrap.dedent("""
|
|
(import <nixpkgs/nixos> {
|
|
configuration = ./bench/nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma6.nix;
|
|
}).config.system.build.toplevel
|
|
""").replace("\n", " "),
|
|
],
|
|
"rebuild_lh": lambda build: [
|
|
"GC_INITIAL_HEAP_SIZE=10g",
|
|
*cases['rebuild'](build),
|
|
],
|
|
"parse": lambda build: [
|
|
f"{build}/bin/nix",
|
|
*flake_args,
|
|
"eval",
|
|
"-f",
|
|
"bench/nixpkgs/pkgs/development/haskell-modules/hackage-packages.nix",
|
|
],
|
|
}
|
|
|
|
arg_parser = argparse.ArgumentParser()
|
|
# FIXME(jade, gilice): it is a reasonable use case to want to run a benchmark run
|
|
# on just one build. However, since we are using hyperfine in comparison
|
|
# mode, we would have to combine the JSON ourselves to support that, which
|
|
# would probably be better done by writing a benchmarking script in
|
|
# not-bash.
|
|
arg_parser.add_argument(
|
|
'builds',
|
|
nargs='+',
|
|
help="At least two build directories to compare, containing bin/nix",
|
|
)
|
|
arg_parser.add_argument(
|
|
'--cases',
|
|
type=str,
|
|
help="A comma-separated list of cases you want to run. Defaults to running all",
|
|
)
|
|
arg_parser.add_argument(
|
|
'--mode',
|
|
choices=[ "walltime" ] + [ "icount" ] if platform.system() == 'Linux' else [], # perf doesn't run on Darwin
|
|
default="walltime",
|
|
)
|
|
arg_parser.add_argument(
|
|
'--daemon',
|
|
action='store_true',
|
|
help='Run a temporary daemon for the benchmark instead of using a local store directly',
|
|
)
|
|
args = arg_parser.parse_args()
|
|
if len(args.builds) < 2:
|
|
raise ValueError("need at least two build directories to compare")
|
|
|
|
benchmarks: list[str] = []
|
|
if args.cases is None:
|
|
benchmarks = list(cases.keys())
|
|
else:
|
|
for case in args.cases.split(","):
|
|
if case not in cases:
|
|
raise ValueError(f"no such case: {case}")
|
|
benchmarks.append(case)
|
|
|
|
def make_full_command(build, case):
|
|
cmd = " ".join(map(shlex.quote, cases[case](build)))
|
|
if args.daemon:
|
|
return " ".join([
|
|
f"{build}/bin/nix --extra-experimental-features nix-command daemon &",
|
|
"trap 'kill %1' EXIT;",
|
|
f"NIX_REMOTE=daemon {cmd}",
|
|
])
|
|
else:
|
|
return cmd
|
|
|
|
def bench_walltime(env):
|
|
for case in benchmarks:
|
|
for build in args.builds:
|
|
subprocess.run([
|
|
"taskset", "-c", "2,3",
|
|
"chrt", "-f","50",
|
|
*[
|
|
"hyperfine", "--warmup", "2", "--runs", "10",
|
|
"--export-json", f"bench/bench-{case}-{build}.json",
|
|
"--export-markdown", f"bench/bench-{case}-{build}.md",
|
|
"--", make_full_command(build, case),
|
|
],
|
|
], env=env, check=True)
|
|
|
|
print("Benchmarks summary\n---\n")
|
|
for case in benchmarks:
|
|
results = []
|
|
for build in args.builds:
|
|
with open(f"bench/bench-{case}-{build}.json") as fd:
|
|
results.append(json.load(fd)["results"][0])
|
|
for result in results:
|
|
print(result["command"])
|
|
print("-" * min(80,len(result["command"])))
|
|
def attr_rounded(attr):
|
|
return f"{result[attr]:.3f}"
|
|
print(" mean: ", attr_rounded("mean"), "±", attr_rounded("stddev"))
|
|
print(" user:", attr_rounded("user"), "| system", attr_rounded("system"))
|
|
print(" median: ", attr_rounded("median"))
|
|
print(" range: ", attr_rounded("min") + "s.." + attr_rounded("max")+"s")
|
|
print(" relative:", f"{result["mean"]/results[0]["mean"]:.3f}")
|
|
print("\n")
|
|
|
|
|
|
def bench_icount(env):
|
|
perf_results_for: dict[str, list[tuple[str, float]]] = {}
|
|
for case in benchmarks:
|
|
for build in args.builds:
|
|
# the perf stat -j output (incorrectly) localizes numbers, which will trip up the json parser.
|
|
env["LC_ALL"]="C"
|
|
case_command = make_full_command(build, case)
|
|
commandline = [
|
|
"perf", "stat", "-o", f"bench/perf-{case}.json", "-j",
|
|
"sh", "-c", case_command,
|
|
]
|
|
print("running", case_command)
|
|
subprocess.run(commandline, env=env, check=True, stdout=subprocess.DEVNULL) # warmup run
|
|
subprocess.run(commandline, env=env, check=True, stdout=subprocess.DEVNULL)
|
|
perf_fd = open(f"bench/perf-{case}.json")
|
|
perf_data = [json.loads(x) for x in perf_fd.readlines()]
|
|
perf_fd.close()
|
|
|
|
instr = next(x for x in perf_data if x["event"] in ["instructions", "instructions:u"]) # an implementation of a find_first iterator
|
|
if case not in perf_results_for:
|
|
perf_results_for[case] = []
|
|
perf_results_for[case].append((case_command, float(instr["counter-value"])))
|
|
|
|
print("Benchmarks summary\n---\n")
|
|
for (case, entries) in perf_results_for.items():
|
|
for entry in entries:
|
|
cmd,instr = entry
|
|
print(cmd)
|
|
print("-" * min(80,len(cmd)))
|
|
print(" instructions: ", int(instr))
|
|
print(" relative instructions:", int(instr)/perf_results_for[case][0][1])
|
|
print("\n")
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
subprocess.run([
|
|
"nix", "build",
|
|
"--extra-experimental-features", "nix-command flakes",
|
|
"--impure", "--expr",'(builtins.getFlake "git+file:.").inputs.nixpkgs.outPath',
|
|
"-o","bench/nixpkgs"
|
|
], check=True)
|
|
subenv = os.environ.copy()
|
|
subenv["NIX_CONF_DIR"] = "/var/empty"
|
|
subenv["NIX_REMOTE"] = tmp_dir
|
|
subenv["NIX_PATH"] = ":".join([
|
|
"nixpkgs=bench/nixpkgs",
|
|
])
|
|
subenv["NIX_DAEMON_SOCKET_PATH"] = f"{tmp_dir}/daemon"
|
|
|
|
if args.mode == "walltime":
|
|
bench_walltime(subenv)
|
|
else:
|
|
bench_icount(subenv)
|