223 lines
6.8 KiB
OCaml
223 lines
6.8 KiB
OCaml
(* Driver Benchmark Runner
|
|
|
|
Assumes the existence of a directory named "drivers" next to the benchmark
|
|
runner executable whose hierarchy is:
|
|
|
|
drivers
|
|
├── <driver1>
|
|
│ ├── driver.ml
|
|
│ ├── dune
|
|
│ └── inputs
|
|
│ ├── <input1>
|
|
│ ├── <input2>
|
|
│ └── ...
|
|
├── <driver2>
|
|
│ ├── driver.ml
|
|
│ ├── dune
|
|
│ └── inputs
|
|
│ ├── <input1>
|
|
│ ├── <input2>
|
|
│ └── ...
|
|
└── ...
|
|
|
|
This benchmark runner will invoke each driver on each of the input files
|
|
associated with it (ie. the files in its "inputs" directory).
|
|
*)
|
|
|
|
(* Run a program on a list of arguments, sending its output to /dev/null,
|
|
returning the wallclock duration of the program in seconds. *)
|
|
let time_run_blocking program args =
|
|
let args_arr = Array.of_list (program :: args) in
|
|
let dev_null = Unix.openfile "/dev/null" [ Unix.O_RDWR ] 0 in
|
|
let timestamp_before = Unix.gettimeofday () in
|
|
let child_pid =
|
|
Unix.create_process program args_arr dev_null dev_null dev_null
|
|
in
|
|
let got_pid, status = Unix.waitpid [] child_pid in
|
|
let timestamp_after = Unix.gettimeofday () in
|
|
Unix.close dev_null;
|
|
if got_pid <> child_pid then failwith "wait returned unexpected pid";
|
|
let () =
|
|
match status with
|
|
| Unix.WEXITED 0 -> ()
|
|
| _ ->
|
|
let command_string = String.concat " " (program :: args) in
|
|
failwith
|
|
(Printf.sprintf "`%s` did not exit successfully" command_string)
|
|
in
|
|
timestamp_after -. timestamp_before
|
|
|
|
(* Takes the path of a directory and returns a list of the full paths of the
|
|
files contained within it. Filters dot files and folders. *)
|
|
let readdir_full_paths dir =
|
|
Sys.readdir dir |> Array.to_list |> List.sort String.compare
|
|
|> List.filter (fun path -> path.[0] <> '.')
|
|
|> List.map (Filename.concat dir)
|
|
|
|
module Input = struct
|
|
type t = { path : string }
|
|
|
|
let name { path } = Filename.basename path
|
|
end
|
|
|
|
module Driver = struct
|
|
type t = { path : string }
|
|
|
|
let name { path } = Filename.basename (Filename.dirname path)
|
|
end
|
|
|
|
module Output = struct
|
|
module Metric = struct
|
|
type t = { name : string; value : Yojson.t; units : string }
|
|
|
|
let create ~name ~value ~units = { name; value; units }
|
|
|
|
let to_json { name; value; units } : Yojson.t =
|
|
`Assoc
|
|
[ ("name", `String name); ("value", value); ("units", `String units) ]
|
|
end
|
|
|
|
module Result = struct
|
|
type t = { name : string; metrics : Metric.t list }
|
|
|
|
let create ~name ~metrics = { name; metrics }
|
|
|
|
let to_json { name; metrics } : Yojson.t =
|
|
`Assoc
|
|
[
|
|
("name", `String name);
|
|
("metrics", `List (List.map Metric.to_json metrics));
|
|
]
|
|
end
|
|
|
|
module Benchmark = struct
|
|
type t = { name : string; results : Result.t list }
|
|
|
|
let create ~name ~results = { name; results }
|
|
|
|
let to_json { name; results } : Yojson.t =
|
|
`Assoc
|
|
[
|
|
("name", `String name);
|
|
("results", `List (List.map Result.to_json results));
|
|
]
|
|
end
|
|
end
|
|
|
|
module Stats = struct
|
|
let sum xs = List.fold_left ( +. ) 0.0 xs
|
|
let mean xs = sum xs /. Int.to_float (List.length xs)
|
|
|
|
let variance xs =
|
|
let xs_mean = mean xs in
|
|
List.map (fun x -> Float.pow (x -. xs_mean) 2.) xs |> mean
|
|
|
|
let stddev xs = Float.sqrt (variance xs)
|
|
end
|
|
|
|
module Benchmark = struct
|
|
type t = { driver : Driver.t; input : Input.t }
|
|
|
|
let create ~driver ~input = { driver; input }
|
|
|
|
let name { driver; input } =
|
|
Printf.sprintf "%s %s" (Driver.name driver) (Input.name input)
|
|
|
|
let time_run_blocking { driver; input } =
|
|
time_run_blocking driver.path [ input.path ]
|
|
|
|
let repeat n f = List.init n (fun _ -> f ())
|
|
|
|
let run_blocking t ~n_warmup ~n =
|
|
let run () = time_run_blocking t in
|
|
let _warmup = repeat n_warmup run in
|
|
let times = repeat n run in
|
|
let mean = Stats.mean times in
|
|
let stddev = Stats.stddev times in
|
|
let metrics =
|
|
[
|
|
Output.Metric.create ~name:"time mean" ~value:(`Float mean)
|
|
~units:"seconds";
|
|
Output.Metric.create ~name:"time stddev" ~value:(`Float stddev)
|
|
~units:"seconds";
|
|
]
|
|
in
|
|
Output.Result.create ~name:(name t) ~metrics
|
|
end
|
|
|
|
module Driver_dir = struct
|
|
type t = { path : string }
|
|
|
|
let driver_name = "driver.exe"
|
|
let inputs_dir_name = "inputs"
|
|
let driver_path { path } = Filename.concat path driver_name
|
|
let driver t = { Driver.path = driver_path t }
|
|
let inputs_path { path } = Filename.concat path inputs_dir_name
|
|
|
|
let of_path path =
|
|
let t = { path } in
|
|
if not (Sys.file_exists (driver_path t)) then
|
|
failwith (Printf.sprintf "failed to find %s in %s" driver_name path);
|
|
if not (Sys.file_exists (inputs_path t)) then
|
|
failwith (Printf.sprintf "failed to find %s in %s" inputs_dir_name path);
|
|
t
|
|
|
|
let inputs t =
|
|
readdir_full_paths (inputs_path t) |> List.map (fun path -> { Input.path })
|
|
|
|
let benchmarks t =
|
|
let driver = driver t in
|
|
inputs t |> List.map (fun input -> Benchmark.create ~driver ~input)
|
|
end
|
|
|
|
module Path_to_current_exe = struct
|
|
let via_procfs () =
|
|
(* Look up the current executable's path in the proc filesystem. *)
|
|
let pid = Unix.getpid () in
|
|
let proc_exe_path = Printf.sprintf "/proc/%d/exe" pid in
|
|
if Sys.file_exists proc_exe_path then Some (Unix.readlink proc_exe_path)
|
|
else None
|
|
|
|
let via_cwd () =
|
|
(* Assume the current working directory is the root of the project (as it
|
|
would be if this was run via `make bench`) and find the path to the
|
|
current exe relative to the project root *)
|
|
let cwd = Unix.getcwd () in
|
|
let relative_path = "_build/default/bench/bench.exe" in
|
|
let absolute_path = Filename.concat cwd relative_path in
|
|
if Sys.file_exists absolute_path then Some absolute_path else None
|
|
|
|
let methods = [ via_procfs; via_cwd ]
|
|
|
|
let get () =
|
|
let maybe_path = List.find_map (fun m -> m ()) methods in
|
|
match maybe_path with
|
|
| Some path -> path
|
|
| None -> failwith "couldn't determine the path to the current exe"
|
|
end
|
|
|
|
module Benchmark_suite = struct
|
|
let get_bench_dir () = Filename.dirname (Path_to_current_exe.get ())
|
|
let drivers_dir_name = "drivers"
|
|
|
|
(* Returns the list of ppxlib drivers that will be benchmarked *)
|
|
let get_driver_dirs () =
|
|
let bench_dir = get_bench_dir () in
|
|
readdir_full_paths (Filename.concat bench_dir drivers_dir_name)
|
|
|> List.map Driver_dir.of_path
|
|
|
|
let get_benchmarks () =
|
|
get_driver_dirs () |> List.concat_map Driver_dir.benchmarks
|
|
|
|
let run_benchmarks ~n_warmup ~n =
|
|
let benchmarks = get_benchmarks () in
|
|
let results = List.map (Benchmark.run_blocking ~n_warmup ~n) benchmarks in
|
|
Output.Benchmark.create ~name:"benchmarks" ~results
|
|
|> Output.Benchmark.to_json
|
|
end
|
|
|
|
let () =
|
|
let n_warmup = 10 in
|
|
let n = 100 in
|
|
Benchmark_suite.run_benchmarks ~n_warmup ~n
|
|
|> Yojson.pretty_to_string |> print_endline
|