Skip to content

Commit e1b7fb7

Browse files
authored
Continuous benchmarking in CI (#7100)
* Syntax benchmark: cleanup * Output benchmark results as JSON * Output JSON lazily * Increment opam cache key * Build benchmarks only for OCaml >= 4.14.0 * Continuous benchmarking in CI * Permissions * Beautify * Cleanup and fix benchmark * More cleanup * Do not attempt to run action for PRs created from other repos * Fix caml_mach_absolute_time for Linux * Add job summary
1 parent 5f5917e commit e1b7fb7

File tree

6 files changed

+116
-88
lines changed

6 files changed

+116
-88
lines changed

.github/workflows/ci.yml

+33-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ on:
1111
pull_request:
1212
branches: [master, 11.0_release]
1313

14+
permissions:
15+
# allow posting comments to pull request
16+
pull-requests: write
17+
1418
concurrency:
1519
group: ci-${{ github.ref }}-1
1620
# Cancel previous builds for pull requests only.
@@ -90,8 +94,9 @@ jobs:
9094
ocaml_compiler: ocaml-variants.5.2.0+options,ocaml-option-static
9195
upload_binaries: true
9296
upload_libs: true
93-
# Build the playground compiler on the fastest runner
97+
# Build the playground compiler and run the benchmarks on the fastest runner
9498
build_playground: true
99+
benchmarks: true
95100
- os: buildjet-2vcpu-ubuntu-2204-arm # ARM
96101
ocaml_compiler: ocaml-variants.5.2.0+options,ocaml-option-static
97102
upload_binaries: true
@@ -150,7 +155,7 @@ jobs:
150155
# matrix.ocaml_compiler may contain commas
151156
- name: Get OPAM cache key
152157
shell: bash
153-
run: echo "opam_cache_key=opam-env-v3-${{ matrix.os }}-${{ matrix.ocaml_compiler }}-${{ hashFiles('dune-project') }}" | sed 's/,/-/g' >> $GITHUB_ENV
158+
run: echo "opam_cache_key=opam-env-v4-${{ matrix.os }}-${{ matrix.ocaml_compiler }}-${{ hashFiles('dune-project') }}" | sed 's/,/-/g' >> $GITHUB_ENV
154159

155160
- name: Restore OPAM environment
156161
id: cache-opam-env
@@ -320,6 +325,32 @@ jobs:
320325
if: runner.os != 'Windows'
321326
run: make -C tests/gentype_tests/typescript-react-example clean test
322327

328+
- name: Run syntax benchmarks
329+
if: matrix.benchmarks
330+
run: ./_build/install/default/bin/syntax_benchmarks | tee tests/benchmark-output.json
331+
332+
- name: Download previous benchmark data
333+
if: matrix.benchmarks
334+
uses: actions/cache@v4
335+
with:
336+
path: ./tests/benchmark-cache
337+
key: syntax-benchmark-v1
338+
339+
- name: Store benchmark result
340+
# Do not run for PRs created from other repos as those won't be able to write to the pull request
341+
if: ${{ matrix.benchmarks && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.event.repository.full_name) }}
342+
uses: benchmark-action/github-action-benchmark@v1
343+
with:
344+
name: Syntax Benchmarks
345+
tool: customSmallerIsBetter
346+
output-file-path: tests/benchmark-output.json
347+
external-data-json-path: ./tests/benchmark-cache/benchmark-data.json
348+
github-token: ${{ secrets.GITHUB_TOKEN }}
349+
alert-threshold: "150%"
350+
comment-always: true
351+
comment-on-alert: true
352+
summary-always: true
353+
323354
- name: Build playground compiler
324355
if: matrix.build_playground
325356
run: |

dune-project

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
(and
2525
:with-test
2626
(= 0.26.2)))
27+
(yojson
28+
(and
29+
:with-test
30+
(= 2.2.2)))
2731
(ocaml-lsp-server
2832
(and
2933
:with-dev-setup

rescript.opam

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ bug-reports: "https://github.com/rescript-lang/rescript-compiler/issues"
99
depends: [
1010
"ocaml" {>= "4.10"}
1111
"ocamlformat" {with-test & = "0.26.2"}
12+
"yojson" {with-test & = "2.2.2"}
1213
"ocaml-lsp-server" {with-dev-setup & = "1.19.0"}
1314
"cppo" {= "1.6.9"}
1415
"js_of_ocaml" {= "5.8.1"}

tests/syntax_benchmarks/Benchmark.ml

+75-84
Original file line numberDiff line numberDiff line change
@@ -75,59 +75,31 @@ end = struct
7575
end
7676

7777
module Benchmark : sig
78-
type t
78+
type test_result = {ms_per_run: float; allocs_per_run: int}
7979

80-
val make : name:string -> f:(t -> unit) -> unit -> t
81-
val launch : t -> unit
82-
val report : t -> unit
80+
val run : (unit -> unit) -> num_iterations:int -> test_result
8381
end = struct
8482
type t = {
85-
name: string;
8683
mutable start: Time.t;
87-
mutable n: int; (* current iterations count *)
88-
mutable duration: Time.t;
89-
bench_func: t -> unit;
84+
mutable n: int; (* current iteration count *)
85+
mutable total_duration: Time.t;
86+
bench_func: unit -> unit;
9087
mutable timer_on: bool;
91-
(* mutable result: benchmarkResult; *)
92-
(* The initial states *)
9388
mutable start_allocs: float;
94-
mutable start_bytes: float;
95-
(* The net total of this test after being run. *)
96-
mutable net_allocs: float;
97-
mutable net_bytes: float;
89+
mutable total_allocs: float;
9890
}
9991

100-
let report b =
101-
print_endline (Format.sprintf "Benchmark: %s" b.name);
102-
print_endline (Format.sprintf "Nbr of iterations: %d" b.n);
103-
print_endline
104-
(Format.sprintf "Benchmark ran during: %fms" (Time.print b.duration));
105-
print_endline
106-
(Format.sprintf "Avg time/op: %fms"
107-
(Time.print b.duration /. float_of_int b.n));
108-
print_endline
109-
(Format.sprintf "Allocs/op: %d"
110-
(int_of_float (b.net_allocs /. float_of_int b.n)));
111-
print_endline
112-
(Format.sprintf "B/op: %d"
113-
(int_of_float (b.net_bytes /. float_of_int b.n)));
114-
115-
(* return (float64(r.Bytes) * float64(r.N) / 1e6) / r.T.Seconds() *)
116-
print_newline ();
117-
()
92+
type test_result = {ms_per_run: float; allocs_per_run: int}
11893

119-
let make ~name ~f () =
94+
let make f =
12095
{
121-
name;
12296
start = Time.zero;
12397
n = 0;
12498
bench_func = f;
125-
duration = Time.zero;
99+
total_duration = Time.zero;
126100
timer_on = false;
127101
start_allocs = 0.;
128-
start_bytes = 0.;
129-
net_allocs = 0.;
130-
net_bytes = 0.;
102+
total_allocs = 0.;
131103
}
132104

133105
(* total amount of memory allocated by the program since it started in words *)
@@ -139,79 +111,74 @@ end = struct
139111
if not b.timer_on then (
140112
let allocated_words = mallocs () in
141113
b.start_allocs <- allocated_words;
142-
b.start_bytes <- allocated_words *. 8.;
143114
b.start <- Time.now ();
144115
b.timer_on <- true)
145116

146117
let stop_timer b =
147118
if b.timer_on then (
148119
let allocated_words = mallocs () in
149120
let diff = Time.diff b.start (Time.now ()) in
150-
b.duration <- Time.add b.duration diff;
151-
b.net_allocs <- b.net_allocs +. (allocated_words -. b.start_allocs);
152-
b.net_bytes <- b.net_bytes +. ((allocated_words *. 8.) -. b.start_bytes);
121+
b.total_duration <- Time.add b.total_duration diff;
122+
b.total_allocs <- b.total_allocs +. (allocated_words -. b.start_allocs);
153123
b.timer_on <- false)
154124

155125
let reset_timer b =
156126
if b.timer_on then (
157127
let allocated_words = mallocs () in
158128
b.start_allocs <- allocated_words;
159-
b.net_allocs <- allocated_words *. 8.;
160-
b.start <- Time.now ());
161-
b.net_allocs <- 0.;
162-
b.net_bytes <- 0.
129+
b.start <- Time.now ())
163130

164131
let run_iteration b n =
165132
Gc.full_major ();
166133
b.n <- n;
167134
reset_timer b;
168135
start_timer b;
169-
b.bench_func b;
136+
b.bench_func ();
170137
stop_timer b
171138

172-
let launch b =
173-
(* 150 runs * all the benchmarks means around 1m of benchmark time *)
174-
for n = 1 to 150 do
139+
let run f ~num_iterations =
140+
let b = make f in
141+
for n = 1 to num_iterations do
175142
run_iteration b n
176-
done
143+
done;
144+
{
145+
ms_per_run = Time.print b.total_duration /. float_of_int b.n;
146+
allocs_per_run = int_of_float (b.total_allocs /. float_of_int b.n);
147+
}
177148
end
178149

179150
module Benchmarks : sig
180151
val run : unit -> unit
181152
end = struct
182153
type action = Parse | Print
154+
183155
let string_of_action action =
184156
match action with
185-
| Parse -> "parser"
186-
| Print -> "printer"
187-
188-
(* TODO: we could at Reason here *)
189-
type lang = Rescript
190-
let string_of_lang lang =
191-
match lang with
192-
| Rescript -> "rescript"
157+
| Parse -> "Parse"
158+
| Print -> "Print"
193159

194160
let parse_rescript src filename =
195161
let p = Parser.make src filename in
196162
let structure = ResParser.parse_implementation p in
197163
assert (p.diagnostics == []);
198164
structure
199165

200-
let benchmark filename lang action =
201-
let src = IO.read_file filename in
202-
let name =
203-
filename ^ " " ^ string_of_lang lang ^ " " ^ string_of_action action
204-
in
166+
let data_dir = "tests/syntax_benchmarks/data"
167+
let num_iterations = 150
168+
169+
let benchmark (filename, action) =
170+
let path = Filename.concat data_dir filename in
171+
let src = IO.read_file path in
205172
let benchmark_fn =
206-
match (lang, action) with
207-
| Rescript, Parse ->
208-
fun _ ->
209-
let _ = Sys.opaque_identity (parse_rescript src filename) in
173+
match action with
174+
| Parse ->
175+
fun () ->
176+
let _ = Sys.opaque_identity (parse_rescript src path) in
210177
()
211-
| Rescript, Print ->
212-
let p = Parser.make src filename in
178+
| Print ->
179+
let p = Parser.make src path in
213180
let ast = ResParser.parse_implementation p in
214-
fun _ ->
181+
fun () ->
215182
let _ =
216183
Sys.opaque_identity
217184
(let cmt_tbl = CommentTable.make () in
@@ -221,21 +188,45 @@ end = struct
221188
in
222189
()
223190
in
224-
let b = Benchmark.make ~name ~f:benchmark_fn () in
225-
Benchmark.launch b;
226-
Benchmark.report b
191+
Benchmark.run benchmark_fn ~num_iterations
192+
193+
let specs =
194+
[
195+
("RedBlackTree.res", Parse);
196+
("RedBlackTree.res", Print);
197+
("RedBlackTreeNoComments.res", Print);
198+
("Napkinscript.res", Parse);
199+
("Napkinscript.res", Print);
200+
("HeroGraphic.res", Parse);
201+
("HeroGraphic.res", Print);
202+
]
227203

228204
let run () =
229-
let data_dir = "tests/syntax_benchmarks/data" in
230-
benchmark (Filename.concat data_dir "RedBlackTree.res") Rescript Parse;
231-
benchmark (Filename.concat data_dir "RedBlackTree.res") Rescript Print;
232-
benchmark
233-
(Filename.concat data_dir "RedBlackTreeNoComments.res")
234-
Rescript Print;
235-
benchmark (Filename.concat data_dir "Napkinscript.res") Rescript Parse;
236-
benchmark (Filename.concat data_dir "Napkinscript.res") Rescript Print;
237-
benchmark (Filename.concat data_dir "HeroGraphic.res") Rescript Parse;
238-
benchmark (Filename.concat data_dir "HeroGraphic.res") Rescript Print
205+
List.to_seq specs
206+
|> Seq.flat_map (fun spec ->
207+
let filename, action = spec in
208+
let test_name = string_of_action action ^ " " ^ filename in
209+
let {Benchmark.ms_per_run; allocs_per_run} = benchmark spec in
210+
[
211+
`Assoc
212+
[
213+
("name", `String (Format.sprintf "%s - time/run" test_name));
214+
("unit", `String "ms");
215+
("value", `Float ms_per_run);
216+
];
217+
`Assoc
218+
[
219+
("name", `String (Format.sprintf "%s - allocs/run" test_name));
220+
("unit", `String "words");
221+
("value", `Int allocs_per_run);
222+
];
223+
]
224+
|> List.to_seq)
225+
|> Seq.iteri (fun i json ->
226+
print_endline (if i == 0 then "[" else ",");
227+
print_string (Yojson.to_string json));
228+
print_newline ();
229+
print_endline "]"
239230
end
240231

241232
let () = Benchmarks.run ()

tests/syntax_benchmarks/dune

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
(enabled_if
1010
(and
1111
(<> %{profile} browser)
12+
(>= %{ocaml_version} "4.14.0")
1213
(or
1314
(= %{system} macosx)
1415
; or one of Linuxes (see https://github.com/ocaml/ocaml/issues/10613)
@@ -22,6 +23,6 @@
2223
(foreign_stubs
2324
(language c)
2425
(names time))
25-
(libraries syntax))
26+
(libraries syntax yojson))
2627

2728
(data_only_dirs data)

tests/syntax_benchmarks/time.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ CAMLprim value caml_mach_absolute_time(value unit) {
3737
#elif defined(__linux__)
3838
struct timespec now;
3939
clock_gettime(CLOCK_MONOTONIC, &now);
40-
result = now.tv_sec * 1000 + now.tv_nsec / 1000000;
40+
result = now.tv_sec * 1000000000 + now.tv_nsec;
4141
#endif
4242

4343
return caml_copy_int64(result);

0 commit comments

Comments
 (0)