275 lines
7.9 KiB
OCaml
275 lines
7.9 KiB
OCaml
open Stdune
|
|
module Process = Dune_engine.Process
|
|
|
|
module Console = struct
|
|
include Dune_console
|
|
|
|
let printf fmt = printf ("[Bench] " ^^ fmt)
|
|
end
|
|
|
|
module Json = struct
|
|
include Chrome_trace.Json
|
|
include Dune_stats.Json
|
|
end
|
|
|
|
module Output = struct
|
|
type measurement =
|
|
[ `Int of int
|
|
| `Float of float
|
|
]
|
|
|
|
type bench =
|
|
{ name : string
|
|
; metrics : (string * [ measurement | `List of measurement list ] * string) list
|
|
}
|
|
|
|
let json_of_bench { name; metrics } : Json.t =
|
|
let metrics =
|
|
List.map metrics ~f:(fun (name, value, units) ->
|
|
let value =
|
|
match value with
|
|
| `Int i -> `Int i
|
|
| `Float f -> `Float f
|
|
| `List xs -> `List (xs :> Json.t list)
|
|
in
|
|
`Assoc [ "name", `String name; "value", value; "units", `String units ])
|
|
in
|
|
`Assoc [ "name", `String name; "metrics", `List metrics ]
|
|
;;
|
|
|
|
type t =
|
|
{ config : (string * Json.t) list
|
|
; version : int
|
|
; results : bench list
|
|
}
|
|
|
|
let to_json { config; version; results } : Json.t =
|
|
let assoc = [ "results", `List (List.map results ~f:json_of_bench) ] in
|
|
let assoc = ("version", `Int version) :: assoc in
|
|
let assoc =
|
|
match config with
|
|
| [] -> assoc
|
|
| _ :: _ -> ("config", `Assoc config) :: assoc
|
|
in
|
|
`Assoc assoc
|
|
;;
|
|
end
|
|
|
|
let git =
|
|
lazy
|
|
(let path = Env.get Env.initial "PATH" |> Option.value_exn |> Bin.parse_path in
|
|
Bin.which ~path "git" |> Option.value_exn)
|
|
;;
|
|
|
|
let dune = Path.of_string (Filename.concat Fpath.initial_cwd Sys.argv.(1))
|
|
let output_limit = Dune_engine.Execution_parameters.Action_output_limit.default
|
|
let make_stdout () = Process.Io.make_stdout ~output_on_success:Swallow ~output_limit
|
|
let make_stderr () = Process.Io.make_stderr ~output_on_success:Swallow ~output_limit
|
|
|
|
module Package = struct
|
|
type t =
|
|
{ org : string
|
|
; name : string
|
|
}
|
|
|
|
let uri { org; name } = sprintf "https://github.com/%s/%s" org name
|
|
let make org name = { org; name }
|
|
|
|
let clone t =
|
|
let stdout_to = make_stdout () in
|
|
let stderr_to = make_stderr () in
|
|
let stdin_from = Process.Io.(null In) in
|
|
Process.run
|
|
Strict
|
|
~display:Quiet
|
|
~stdout_to
|
|
~stderr_to
|
|
~stdin_from
|
|
(Lazy.force git)
|
|
[ "clone"; uri t ]
|
|
;;
|
|
end
|
|
|
|
let duniverse =
|
|
let pkg = Package.make in
|
|
[ pkg "ocaml-dune" "dune-bench" ]
|
|
;;
|
|
|
|
let prepare_workspace () =
|
|
Fiber.parallel_iter duniverse ~f:(fun (pkg : Package.t) ->
|
|
Fpath.rm_rf pkg.name;
|
|
Console.printf "cloning %s/%s" pkg.org pkg.name;
|
|
Fiber.finalize
|
|
(fun () -> Package.clone pkg)
|
|
~finally:(fun () ->
|
|
Fiber.return @@ Console.printf "finished cloning %s/%s" pkg.org pkg.name))
|
|
;;
|
|
|
|
let dune_build ~name ~sandbox =
|
|
let stdin_from = Process.(Io.null In) in
|
|
let stdout_to = make_stdout () in
|
|
let stderr_to = make_stderr () in
|
|
let gc_dump = Temp.create File ~prefix:"gc_stat" ~suffix:name in
|
|
let open Fiber.O in
|
|
(* Build with timings and gc stats *)
|
|
let+ times =
|
|
Process.run_with_times
|
|
Strict
|
|
dune
|
|
~display:Quiet
|
|
~stdin_from
|
|
~stdout_to
|
|
~stderr_to
|
|
([ "build"
|
|
; "@install"
|
|
; "--release"
|
|
; "--cache" (* explicitly disable cache *)
|
|
; "disabled"
|
|
; "--dump-gc-stats"
|
|
; Path.to_string gc_dump
|
|
]
|
|
@
|
|
match sandbox with
|
|
| `Yes -> [ "--sandbox"; "hardlink" ]
|
|
| `No -> [])
|
|
in
|
|
(* Read the gc stats from the dump file *)
|
|
Dune_lang.Parser.parse_string
|
|
~mode:Single
|
|
~fname:(Path.to_string gc_dump)
|
|
(Io.read_file gc_dump)
|
|
|> Dune_lang.Decoder.parse Dune_util.Gc.decode Univ_map.empty
|
|
|> Metrics.make times
|
|
;;
|
|
|
|
let run_bench ~sandbox =
|
|
let open Fiber.O in
|
|
let* clean = dune_build ~name:"clean" ~sandbox in
|
|
let+ zero =
|
|
let rec zero acc n =
|
|
if n = 0
|
|
then Fiber.return (List.rev acc)
|
|
else
|
|
let* time = dune_build ~name:("zero" ^ string_of_int n) ~sandbox in
|
|
zero (time :: acc) (pred n)
|
|
in
|
|
zero [] 5
|
|
in
|
|
clean, zero
|
|
;;
|
|
|
|
type ('float, 'int) bench_results =
|
|
{ size : int
|
|
; clean : ('float, 'int) Metrics.t
|
|
; zero : ('float, 'int) Metrics.t list
|
|
}
|
|
|
|
let tag_results { size; clean; zero } =
|
|
let tag data = Metrics.map ~f:(fun t -> `Float t) ~g:(fun t -> `Int t) data in
|
|
let list_tag data =
|
|
List.map data ~f:tag
|
|
|> Metrics.unzip
|
|
|> Metrics.map ~f:(fun x -> `List x) ~g:(fun x -> `List x)
|
|
in
|
|
`Int size, tag clean, list_tag zero
|
|
;;
|
|
|
|
(** Display all clean and null builds with a few exceptions:
|
|
|
|
- fragments - not consistent between builds
|
|
- stack_size - not very useful
|
|
- forced_collections - only available in OCaml >= 4.12 *)
|
|
let display_clean_and_zero_with_sandboxing
|
|
({ elapsed_time
|
|
; user_cpu_time
|
|
; system_cpu_time
|
|
; minor_words
|
|
; promoted_words
|
|
; major_words
|
|
; minor_collections
|
|
; major_collections
|
|
; heap_words
|
|
; heap_chunks
|
|
; live_words
|
|
; live_blocks
|
|
; free_words
|
|
; free_blocks
|
|
; largest_free
|
|
; fragments = _
|
|
; compactions
|
|
; top_heap_words
|
|
; stack_size = _
|
|
} :
|
|
_ Metrics.t)
|
|
(zero : _ Metrics.t)
|
|
=
|
|
let display what units clean zero =
|
|
{ Output.name = what
|
|
; metrics = [ "[Clean] " ^ what, clean, units; "[Null] " ^ what, zero, units ]
|
|
}
|
|
in
|
|
[ display "Build Time" "Seconds" elapsed_time zero.elapsed_time
|
|
; display "Minor Words" "Approx. Words" minor_words zero.minor_words
|
|
; display "Promoted Words" "Approx. Words" promoted_words zero.promoted_words
|
|
; display "Major Words" "Approx. Words" major_words zero.major_words
|
|
; display "Minor Collections" "Collections" minor_collections zero.minor_collections
|
|
; display "Major Collections" "Collections" major_collections zero.major_collections
|
|
; display "Heap Words" "Words" heap_words zero.heap_words
|
|
; display "Heap Chunks" "Chunks" heap_chunks zero.heap_chunks
|
|
; display "Live Words" "Words" live_words zero.live_words
|
|
; display "Live Blocks" "Blocks" live_blocks zero.live_blocks
|
|
; display "Free Words" "Words" free_words zero.free_words
|
|
; display "Free Blocks" "Blocks" free_blocks zero.free_blocks
|
|
; display "Largest Free" "Words" largest_free zero.largest_free
|
|
; display "Compactions" "Compactions" compactions zero.compactions
|
|
; display "Top Heap Words" "Words" top_heap_words zero.top_heap_words
|
|
; display "User CPU Time" "Seconds" user_cpu_time zero.user_cpu_time
|
|
; display "System CPU Time" "Seconds" system_cpu_time zero.system_cpu_time
|
|
]
|
|
;;
|
|
|
|
let format_results bench_results =
|
|
(* tagging data for json conversion *)
|
|
let size, clean, zero = tag_results bench_results in
|
|
(* bench results *)
|
|
[ { Output.name = "Misc"; metrics = [ "Size of _boot/dune.exe", size, "Bytes" ] } ]
|
|
@ display_clean_and_zero_with_sandboxing clean zero
|
|
;;
|
|
|
|
let () =
|
|
Dune_util.Log.init ~file:No_log_file ();
|
|
let dir = Temp.create Dir ~prefix:"dune" ~suffix:"bench" in
|
|
Sys.chdir (Path.to_string dir);
|
|
Path.as_external dir |> Option.value_exn |> Path.set_root;
|
|
Path.Build.set_build_dir (Path.Outside_build_dir.of_string "_build");
|
|
let module Scheduler = Dune_engine.Scheduler in
|
|
let config =
|
|
Dune_engine.Clflags.display := Quiet;
|
|
{ Scheduler.Config.concurrency = 10
|
|
; stats = None
|
|
; print_ctrl_c_warning = false
|
|
; watch_exclusions = []
|
|
}
|
|
in
|
|
let size =
|
|
let stat : Unix.stats = Path.stat_exn dune in
|
|
stat.st_size
|
|
in
|
|
let results =
|
|
Scheduler.Run.go config ~on_event:(fun _ _ -> ())
|
|
@@ fun () ->
|
|
let open Fiber.O in
|
|
(* Prepare the workspace *)
|
|
let* () = prepare_workspace () in
|
|
(* Build the clean and null builds *)
|
|
Console.printf "Building clean and null builds";
|
|
let+ clean, zero = run_bench ~sandbox:`No in
|
|
Console.printf "Finished building clean and null builds";
|
|
(* Return the bench results *)
|
|
format_results { size; clean; zero }
|
|
in
|
|
let version = 4 in
|
|
let output = { Output.config = []; version; results } in
|
|
print_string (Json.to_string (Output.to_json output));
|
|
flush stdout
|
|
;;
|