Skip to content

Commit b79caff

Browse files
committed
Start dependents once a module's interface is final
Why: A dependent only needs a module's .cmi to type check, but it waits until that module has also produced its lambda, JavaScript, .cmj and .cmt. On builds limited by a long dependency chain most job slots sit idle on the critical path: a 136 module library gets only 1.6x from 7 domains. When a slot would otherwise idle, a module can now start as soon as each unfinished dependency's interface is final: an implementation without .resi has computed the .cmi it is about to write (with no pending warning as error), or an interface file has compiled. The early request reads those .cmi files from memory and waits at a gate before writing any file or returning its result. The scheduler adopts it at the point master would have started it (dependencies complete, module still dirty), so its code generation sees the dependencies' .cmj files, and artifacts, diagnostics and compiler.log are unchanged. Otherwise it is abandoned without effects, together with anything that started on top of it. A redirected lookup matches directories by identity (device and inode, or the Windows file id). A lookup that might be another spelling of a published file, or a directory that can't be identified, waits for adoption instead of reading the disk. Modules with js-post-build commands or lowercase .cmi names don't release their dependents early, and with one domain nothing changes. Review: Can a dependent see an interface that differs from master's? It reads the exact bytes the dependency then writes, and it is only adopted after the dependency completed. If the dependency fails later, for example in code generation, the dependent is abandoned before it wrote or reported anything.
1 parent c10223e commit b79caff

30 files changed

Lines changed: 2312 additions & 124 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Build: start type checking a module as soon as the interfaces of its dependencies are final, instead of waiting for their JavaScript generation, which shortens builds limited by long dependency chains.

‎compiler/bsc/rescript_compiler_driver.ml‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,15 +569,15 @@ let start_request () =
569569
Predef.reset_for_request ();
570570
Env.reset_initial_for_request ()
571571

572-
let with_fresh_request_states ~cwd action =
572+
let with_fresh_request_states ?hooks ~cwd action =
573573
Predef.with_fresh (fun () ->
574574
Env.with_fresh_initial (fun () ->
575575
Ident.with_fresh (fun () ->
576576
Request_local.with_fresh (fun () ->
577-
Compiler_request_state.with_fresh ~cwd action))))
577+
Compiler_request_state.with_fresh ~cwd ?hooks action))))
578578

579-
let run_argv ?run_external ~capture ~cwd argv =
580-
with_fresh_request_states ~cwd (fun () ->
579+
let run_argv ?run_external ?hooks ~capture ~cwd argv =
580+
with_fresh_request_states ?hooks ~cwd (fun () ->
581581
start_request ();
582582
Cmt_format.set_args argv;
583583
let execute () =
@@ -635,8 +635,12 @@ let run_argv ?run_external ~capture ~cwd argv =
635635
in
636636
{exit_code; stdout = ""; stderr = ""})
637637

638-
let run_request ~run_external ~cwd ~argv ~input =
638+
let run_request_with_hooks ~hooks ~run_external ~cwd ~argv ~input =
639639
let logical_argv = Array.of_list ("bsc" :: (argv @ [input])) in
640-
run_argv ?run_external ~capture:true ~cwd logical_argv
640+
run_argv ?run_external ~hooks ~capture:true ~cwd logical_argv
641+
642+
let run_request ~run_external ~cwd ~argv ~input =
643+
run_request_with_hooks ~hooks:Compiler_request_state.no_hooks ~run_external
644+
~cwd ~argv ~input
641645

642646
let run argv = (run_argv ~capture:false ~cwd:(Sys.getcwd ()) argv).exit_code

‎compiler/bsc/rescript_compiler_driver.mli‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ val run_request :
2424
I/O against [cwd] without changing the process working directory. Request
2525
state is restored on success and failure. *)
2626

27+
val run_request_with_hooks :
28+
hooks:Compiler_request_state.hooks ->
29+
run_external:(string -> int * string * string) option ->
30+
cwd:string ->
31+
argv:string list ->
32+
input:string ->
33+
result
34+
(** {!run_request} with build-system callbacks attached to the request; see
35+
{!Compiler_request_state.hooks}. *)
36+
2737
val run : string array -> int
2838
(** Command-line entry point of the standalone [bsc]. It writes to the process's
2939
stdout and stderr as it goes. *)

‎compiler/core/bs_cmi_load.ml‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,20 @@ let load_cmi ~unit_name : Env.Persistent_signature.t option =
2929
Misc.find_map_in_path_uncap (Config.get_load_path ()) (unit_name ^ ".cmi")
3030
(fun filename ->
3131
let path = Compiler_request_state.resolve_path filename in
32-
match Unix.openfile path [Unix.O_RDONLY; Unix.O_CLOEXEC] 0 with
33-
| descriptor ->
34-
let channel = Unix.in_channel_of_descr descriptor in
35-
let cmi =
36-
Fun.protect
37-
(fun () -> Cmi_format.read_cmi_channel filename channel)
38-
~finally:(fun () -> close_in_noerr channel)
39-
in
32+
match Compiler_request_state.redirect_cmi path with
33+
| Some bytes ->
34+
let cmi = Cmi_format.read_cmi_string filename bytes in
4035
Some Env.Persistent_signature.{filename; cmi}
41-
| exception Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) -> None
42-
| exception Unix.Unix_error (error, _, _) ->
43-
raise (Sys_error (path ^ ": " ^ Unix.error_message error)))
36+
| None -> (
37+
match Unix.openfile path [Unix.O_RDONLY; Unix.O_CLOEXEC] 0 with
38+
| descriptor ->
39+
let channel = Unix.in_channel_of_descr descriptor in
40+
let cmi =
41+
Fun.protect
42+
(fun () -> Cmi_format.read_cmi_channel filename channel)
43+
~finally:(fun () -> close_in_noerr channel)
44+
in
45+
Some Env.Persistent_signature.{filename; cmi}
46+
| exception Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) -> None
47+
| exception Unix.Unix_error (error, _, _) ->
48+
raise (Sys_error (path ^ ": " ^ Unix.error_message error))))

‎compiler/ext/compiler_request_state.ml‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
type hooks = {
2+
before_artifact_write: unit -> unit;
3+
interface_computed: string -> (unit -> string) -> unit;
4+
redirect_cmi: string -> string option;
5+
}
6+
7+
let no_hooks =
8+
{
9+
before_artifact_write = ignore;
10+
interface_computed = (fun _ _ -> ());
11+
redirect_cmi = (fun _ -> None);
12+
}
13+
114
type t = {
215
active: bool;
16+
hooks: hooks;
317
cwd: string;
418
mutable input_name: string;
519
mutable command_runner: string -> int;
@@ -16,9 +30,10 @@ type t = {
1630
mutable load_path: string list;
1731
}
1832

19-
let create ~active cwd =
33+
let create ?(hooks = no_hooks) ~active cwd =
2034
{
2135
active;
36+
hooks;
2237
cwd;
2338
input_name = "_none_";
2439
command_runner = Sys.command;
@@ -47,13 +62,18 @@ let resolve_path path =
4762
Filename.concat state.cwd path
4863
else path
4964

50-
let with_fresh ?cwd:requested_cwd action =
65+
let before_artifact_write () = (current ()).hooks.before_artifact_write ()
66+
let interface_computed path bytes =
67+
(current ()).hooks.interface_computed path bytes
68+
let redirect_cmi path = (current ()).hooks.redirect_cmi path
69+
70+
let with_fresh ?cwd:requested_cwd ?hooks action =
5171
let previous = current () in
5272
let cwd =
5373
match requested_cwd with
5474
| None -> cwd ()
5575
| Some path when Filename.is_relative path -> Filename.concat (cwd ()) path
5676
| Some path -> path
5777
in
58-
Domain.DLS.set key (create ~active:true cwd);
78+
Domain.DLS.set key (create ?hooks ~active:true cwd);
5979
Fun.protect action ~finally:(fun () -> Domain.DLS.set key previous)

‎compiler/ext/compiler_request_state.mli‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1+
type hooks = {
2+
before_artifact_write: unit -> unit;
3+
(** Called before the request writes its first compiled artifact (.cmi,
4+
.cmt, .cmti). A build system that starts a request before its
5+
dependencies have finished uses it to wait for them; it may raise to
6+
abandon the request before anything is written. *)
7+
interface_computed: string -> (unit -> string) -> unit;
8+
(** Called with the path of a .cmi the request is about to write, and a
9+
function giving the file's bytes, when no error is pending. Called
10+
before {!before_artifact_write}. *)
11+
redirect_cmi: string -> string option;
12+
(** The bytes to read instead of the .cmi file at a resolved candidate
13+
path. The name the compiler records stays the candidate's. *)
14+
}
15+
(** Callbacks a build system attaches to one in-process request. *)
16+
17+
val no_hooks : hooks
18+
119
type t = {
220
active: bool;
21+
hooks: hooks;
322
cwd: string;
423
mutable input_name: string;
524
mutable command_runner: string -> int;
@@ -27,5 +46,9 @@ val resolve_path : string -> string
2746
(** Resolve a relative file path against the active request root. Outside a
2847
request, preserve the path so ordinary process-relative I/O is unchanged. *)
2948

30-
val with_fresh : ?cwd:string -> (unit -> 'a) -> 'a
49+
val before_artifact_write : unit -> unit
50+
val interface_computed : string -> (unit -> string) -> unit
51+
val redirect_cmi : string -> string option
52+
53+
val with_fresh : ?cwd:string -> ?hooks:hooks -> (unit -> 'a) -> 'a
3154
(** Run with new request state and restore the prior state on success or failure. *)

‎compiler/ext/warnings.mli‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ exception Errors
8888

8989
val check_fatal : unit -> unit
9090

91+
val nerrors : unit -> int
92+
(** Warnings reported as errors since the last {!check_fatal}. *)
93+
9194
val reset_fatal : unit -> unit
9295

9396
val help_warnings : unit -> unit

‎compiler/ml/cmi_format.ml‎

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,47 @@ let input_cmi ic =
3838
(* Earlier compilers wrote OCaml-style magic numbers *)
3939
let legacy_magic_prefix = "Caml1999I"
4040

41+
let check_magic filename buffer =
42+
if buffer <> Config.cmi_magic_number then
43+
let pre_len = String.length Config.cmi_magic_number - 3 in
44+
if
45+
String.sub buffer 0 pre_len = String.sub Config.cmi_magic_number 0 pre_len
46+
then
47+
let msg =
48+
if buffer < Config.cmi_magic_number then "an older" else "a newer"
49+
in
50+
raise (Error (Wrong_version_interface (filename, msg)))
51+
else if String.starts_with ~prefix:legacy_magic_prefix buffer then
52+
raise (Error (Wrong_version_interface (filename, "an older")))
53+
else raise (Error (Not_an_interface filename))
54+
55+
(* The same decoding as [read_cmi_channel], from the bytes of a .cmi file. *)
56+
let read_cmi_string filename bytes =
57+
let magic_length = String.length Config.cmi_magic_number in
58+
if String.length bytes < magic_length then
59+
raise (Error (Corrupted_interface filename));
60+
check_magic filename (String.sub bytes 0 magic_length);
61+
let value offset =
62+
if offset + Marshal.header_size > String.length bytes then
63+
raise (Error (Corrupted_interface filename));
64+
let size = Marshal.total_size (Bytes.unsafe_of_string bytes) offset in
65+
if offset + size > String.length bytes then
66+
raise (Error (Corrupted_interface filename));
67+
(Marshal.from_string bytes offset, offset + size)
68+
in
69+
try
70+
let (name, sign), offset = value magic_length in
71+
let crcs, offset = value offset in
72+
let flags, _ = value offset in
73+
{cmi_name = name; cmi_sign = sign; cmi_crcs = crcs; cmi_flags = flags}
74+
with Failure _ -> raise (Error (Corrupted_interface filename))
75+
4176
let read_cmi_channel filename ic =
4277
try
4378
let buffer =
4479
really_input_string ic (String.length Config.cmi_magic_number)
4580
in
46-
(if buffer <> Config.cmi_magic_number then
47-
let pre_len = String.length Config.cmi_magic_number - 3 in
48-
if
49-
String.sub buffer 0 pre_len
50-
= String.sub Config.cmi_magic_number 0 pre_len
51-
then
52-
let msg =
53-
if buffer < Config.cmi_magic_number then "an older" else "a newer"
54-
in
55-
raise (Error (Wrong_version_interface (filename, msg)))
56-
else if String.starts_with ~prefix:legacy_magic_prefix buffer then
57-
raise (Error (Wrong_version_interface (filename, "an older")))
58-
else raise (Error (Not_an_interface filename)));
81+
check_magic filename buffer;
5982
let cmi = input_cmi ic in
6083
cmi
6184
with End_of_file | Failure _ -> raise (Error (Corrupted_interface filename))
@@ -66,15 +89,31 @@ let read_cmi filename =
6689
(fun () -> read_cmi_channel filename ic)
6790
~finally:(fun () -> close_in_noerr ic)
6891

69-
(* Writes the .cmi file, keeping an existing one with the same contents
70-
when [check_exists] is given *)
71-
let create_cmi ?check_exists filename (cmi : cmi_infos) =
92+
type prepared = {content: string; crc: Digest.t}
93+
94+
let prepare_cmi (cmi : cmi_infos) =
7295
(* beware: the provided signature must have been substituted for saving *)
7396
let content =
7497
Config.cmi_magic_number ^ Marshal.to_string (cmi.cmi_name, cmi.cmi_sign) []
7598
(* checkout [output_value] in {!Pervasives} module *)
7699
in
77-
let crc = Digest.string content in
100+
{content; crc = Digest.string content}
101+
102+
(* The bytes [create_cmi] leaves in the file. An existing file it keeps has
103+
the same bytes: the same content, imports and flags. *)
104+
let prepared_bytes {content; crc} (cmi : cmi_infos) =
105+
content
106+
^ Marshal.to_string ((cmi.cmi_name, Some crc) :: cmi.cmi_crcs) []
107+
^ Marshal.to_string cmi.cmi_flags []
108+
109+
(* Writes the .cmi file, keeping an existing one with the same contents
110+
when [check_exists] is given *)
111+
let create_cmi ?check_exists ?prepared filename (cmi : cmi_infos) =
112+
let {content; crc} =
113+
match prepared with
114+
| Some prepared -> prepared
115+
| None -> prepare_cmi cmi
116+
in
78117
let cmi_infos =
79118
if
80119
check_exists <> None

‎compiler/ml/cmi_format.mli‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,27 @@ type cmi_infos = {
2222
cmi_flags: pers_flags list;
2323
}
2424

25-
val create_cmi : ?check_exists:unit -> string -> cmi_infos -> Digest.t
25+
type prepared
26+
(** A signature marshalled for saving, and its CRC. *)
27+
28+
val prepare_cmi : cmi_infos -> prepared
29+
30+
val prepared_bytes : prepared -> cmi_infos -> string
31+
(** The bytes of the .cmi file {!create_cmi} leaves for [cmi]. *)
32+
33+
val create_cmi :
34+
?check_exists:unit -> ?prepared:prepared -> string -> cmi_infos -> Digest.t
2635

2736
(* read the cmi information (the magic is supposed to have already been read) *)
2837
val input_cmi : in_channel -> cmi_infos
2938

3039
(* read a cmi from a filename, checking the magic *)
3140
val read_cmi : string -> cmi_infos
3241
val read_cmi_channel : string -> in_channel -> cmi_infos
42+
43+
val read_cmi_string : string -> string -> cmi_infos
44+
(** [read_cmi_string filename bytes] decodes the bytes of a .cmi file like
45+
{!read_cmi_channel}; [filename] names it in errors. *)
3346
(* Parse an already opened CMI. The caller retains ownership of the channel. *)
3447

3548
(* Error report *)

‎compiler/ml/cmt_format.ml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ include Cmt_format_common
33
let set_args = Cmt_format_persistence.set_args
44

55
let save_cmt filename modname binary_annots sourcefile initial_env =
6+
Compiler_request_state.before_artifact_write ();
67
Cmt_format_persistence.save_cmt filename modname binary_annots sourcefile
78
initial_env;
89
clear ()

0 commit comments

Comments
 (0)