2 * Copyright (c) 2015, Facebook, Inc.
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the "hack" directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
14 open Reordered_argument_collections
19 open Result.Monad_infix
21 module DepSet
= Typing_deps.DepSet
22 module Dep
= Typing_deps.Dep
23 module SLC
= ServerLocalConfig
24 module LSC
= LoadScriptConfig
27 exception Loader_timeout
of string
30 * hh_server can initialize either by typechecking the entire project (aka
31 * starting from a "fresh state") or by loading from a saved state and
32 * typechecking what has changed.
34 * If we start from a fresh state, we run the following phases:
36 * Parsing -> Naming -> Type-decl -> Type-check
38 * If we are loading a state, we do
40 * Run load script and parsing concurrently -> Naming -> Type-decl
42 * Then we typecheck only the files that have changed since the state was
45 * This is done in fairly similar manner to the incremental update
46 * code in ServerTypeCheck. The key difference is that incremental mode
47 * can compare the files that it has just parsed with their old versions,
48 * thereby (in theory) recomputing the least amount possible. OTOH,
49 * ServerInit only has the latest version of each file, so it has to make
50 * the most conservative estimate about what to recheck.
53 (* Return all the files that we need to typecheck *)
54 let make_next_files genv
: Relative_path.t
MultiWorker.nextlist
=
55 let next_files_root = compose
56 (List.map ~f
:(Relative_path.(create Root
)))
57 (genv
.indexer
ServerEnv.file_filter
) in
58 let hhi_root = Hhi.get_hhi_root
() in
59 let hhi_filter = begin fun s
->
61 (** If experimental disabled, we don't parse hhi files under
62 * the experimental directory. *)
63 && (TypecheckerOptions.experimental_feature_enabled
64 (ServerConfig.typechecker_options genv
.config
)
65 TypecheckerOptions.experimental_dict
66 || not
(FindUtils.has_ancestor s
"experimental"))
69 let next_files_hhi = compose
70 (List.map ~f
:(Relative_path.(create Hhi
)))
72 ~name
:"hhi" ~filter
:hhi_filter hhi_root) in
74 match next_files_hhi () with
75 | [] -> next_files_root ()
78 let save_state env fn
=
79 let t = Unix.gettimeofday
() in
80 if not
(Errors.is_empty env
.errorl
)
81 then failwith
"--save-mini only works if there are no type errors!";
82 let chan = Sys_utils.open_out_no_fail fn
in
83 let names = FileInfo.simplify_fast env
.files_info
in
84 Marshal.to_channel
chan names [];
85 Sys_utils.close_out_no_fail fn
chan;
86 SharedMem.save_dep_table
(fn^
".deptable");
87 ignore
@@ Hh_logger.log_duration
"Saving" t
89 let read_json_line ic
=
90 let output = input_line ic
in
91 try Hh_json.json_of_string
output
92 with Hh_json.Syntax_error _
as e
->
93 Hh_logger.log
"Failed to parse JSON: %s" output;
96 let check_json_obj_error kv
=
97 match List.Assoc.find kv
"error" with
98 | Some
(Hh_json.JSON_String s
) -> failwith s
101 (* Expected output from script:
103 * The first line indicates the path to the state file plus some metadata
104 * The second line is a list of the files that have changed since the state
107 let load_state root saved_state_load_type cmd
(_ic
, oc
) =
109 let load_script_log_file = ServerFiles.load_log root
in
113 (Filename.quote
(Path.to_string
cmd))
114 (Filename.quote
(Path.to_string root
))
115 (Filename.quote
Build_id.build_revision
)
116 (Filename.quote
load_script_log_file)
117 (Filename.quote saved_state_load_type
) in
118 Hh_logger.log
"Running load_mini script: %s\n%!" cmd;
119 let ic = Unix.open_process_in
cmd in
120 let json = read_json_line ic in
121 let kv = Hh_json.get_object_exn
json in
122 check_json_obj_error kv;
123 let state_fn = Hh_json.get_string_exn
@@ List.Assoc.find_exn
kv "state" in
125 Hh_json.get_bool_exn
@@ List.Assoc.find_exn
kv "is_cached" in
127 Hh_json.get_string_exn
@@ List.Assoc.find_exn
kv "deptable" in
128 SharedMem.load_dep_table
deptable_fn;
129 let end_time = Unix.gettimeofday
() in
130 Daemon.to_channel oc
@@ Ok
(`Fst
(state_fn, is_cached, end_time));
131 let json = read_json_line ic in
132 assert (Unix.close_process_in
ic = Unix.WEXITED
0);
133 let kv = Hh_json.get_object_exn
json in
134 check_json_obj_error kv;
136 Hh_json.get_array_exn
@@ List.Assoc.find_exn
kv "changes" in
137 let to_recheck = List.map
to_recheck Hh_json.get_string_exn
in
138 Daemon.to_channel oc
@@ Ok
(`Snd
to_recheck)
140 Hh_logger.exc ~prefix
:"Failed to load state: " e
;
141 Daemon.to_channel oc
@@ Error e
143 let with_loader_timeout timeout stage f
=
144 Result.try_with
@@ fun () ->
145 Timeout.with_timeout ~timeout ~do_
:(fun _
-> f
())
146 ~on_timeout
:(fun _
-> raise
@@ Loader_timeout stage
)
148 (* This generator-like function first runs the load script to download state
149 * and loads the downloaded dependency table into shared memory. It then
150 * waits for the load script to send it the list of files that have changed
151 * since the state was downloaded.
153 * The loading of the dependency table must not run concurrently with any
154 * operations that might write to the deptable. *)
155 let mk_state_future root saved_state_load_type
cmd =
156 let start_time = Unix.gettimeofday
() in
157 Result.try_with
@@ fun () ->
159 Sys_utils.make_link_of_timestamped
(ServerFiles.load_log root
) in
160 let log_fd = Daemon.fd_of_path
log_file in
161 let {Daemon.channels
= (ic, _oc
); pid
} as daemon
=
162 Daemon.fork
(log_fd, log_fd) (load_state root saved_state_load_type
) cmd
166 Daemon.from_channel
ic >>| function
167 | `Snd _
-> assert false
168 | `Fst
(fn, is_cached, end_time) ->
169 HackEventLogger.load_mini_worker_end ~
is_cached start_time end_time;
170 let time_taken = end_time -. start_time in
171 Hh_logger.log
"Loading mini-state took %.2fs" time_taken;
174 (* We have failed to load the saved state in the allotted time. Kill
175 * the daemon so it doesn't write to shared memory while the type-decl
176 * / type-check phases are running. The kill may fail if e.g. the
177 * daemon exited just after the timeout but before the kill signal goes
179 (try Daemon.kill daemon
with e
-> Hh_logger.exc e
);
183 Daemon.from_channel
ic >>| function
184 | `Fst _
-> assert false
185 | `Snd dirty_files
->
186 let _, status
= Unix.waitpid
[] pid
in
187 assert (status
= Unix.WEXITED
0);
188 let chan = open_in
fn in
189 let old_fast = Marshal.from_channel
chan in
190 let dirty_files = List.map
dirty_files Relative_path.(concat Root
) in
191 Relative_path.set_of_list
dirty_files, old_fast
193 let is_check_mode options
=
194 ServerArgs.check_mode options
&&
195 ServerArgs.convert options
= None
&&
196 (* Note: we need to run update_files to get an accurate saved state *)
197 ServerArgs.save_filename options
= None
200 let t = Unix.gettimeofday
() in
201 let get_next = make_next_files genv
in
202 HackEventLogger.indexing_end
t;
203 let t = Hh_logger.log_duration
"Indexing" t in
206 let parsing genv env ~
get_next t =
207 let files_info, errorl
, failed
=
210 Relative_path.Map.empty
213 let files_info = Relative_path.Map.union
files_info env
.files_info in
214 let hs = SharedMem.heap_size
() in
215 Hh_logger.log
"Heap size: %d" hs;
216 Stats.(stats
.init_parsing_heap_size
<- hs);
217 (* TODO: log a count of the number of files parsed... 0 is a placeholder *)
218 HackEventLogger.parsing_end
t hs ~parsed_count
:0;
221 errorl
= Errors.merge errorl
env.errorl
;
222 failed_parsing
= Relative_path.Set.union
env.failed_parsing failed
;
224 env, (Hh_logger.log_duration
"Parsing" t)
226 let update_files genv
files_info t =
227 if is_check_mode genv
.options
then t else begin
228 Typing_deps.update_files files_info;
229 HackEventLogger.updating_deps_end
t;
230 Hh_logger.log_duration
"Updating deps" t
235 Relative_path.Map.fold
env.files_info ~f
:begin fun k v
env ->
236 let errorl, failed
= NamingGlobal.ndecl_file k v
in
238 errorl = Errors.merge
errorl env.errorl;
239 failed_parsing
= Relative_path.Set.union
env.failed_parsing failed
;
243 let hs = SharedMem.heap_size
() in
244 Hh_logger.log
"Heap size: %d" hs;
245 env, (Hh_logger.log_duration
"Naming" t)
247 let type_decl genv
env fast
t =
248 let bucket_size = genv
.local_config
.SLC.type_decl_bucket_size
in
249 let errorl, failed_decl
=
250 Decl_service.go ~
bucket_size genv
.workers
env.tcopt fast
in
251 let hs = SharedMem.heap_size
() in
252 Hh_logger.log
"Heap size: %d" hs;
253 Stats.(stats
.init_heap_size
<- hs);
254 HackEventLogger.type_decl_end
t;
255 let t = Hh_logger.log_duration
"Type-decl" t in
258 errorl = Errors.merge
errorl env.errorl;
263 let type_check genv
env fast
t =
264 if ServerArgs.ai_mode genv
.options
= None
|| not
(is_check_mode genv
.options
)
266 let count = Relative_path.Map.cardinal fast
in
267 let errorl, err_info
=
268 Typing_check_service.go genv
.workers
env.tcopt fast
in
271 lazy_decl_errs
= lazy_decl_failed
;
273 let hs = SharedMem.heap_size
() in
274 Hh_logger.log
"Heap size: %d" hs;
275 HackEventLogger.type_check_end
count t;
277 errorl = Errors.merge
errorl env.errorl;
278 failed_decl
= Relative_path.Set.union
env.failed_decl lazy_decl_failed
;
279 failed_check
= failed
;
281 env, (Hh_logger.log_duration
"Type-check" t)
284 let get_dirty_fast old_fast fast dirty
=
285 Relative_path.Set.fold dirty ~f
:begin fun fn acc
->
286 let dirty_fast = Relative_path.Map.get fast
fn in
287 let dirty_old_fast = Relative_path.Map.get
old_fast fn in
288 let fast = Option.merge
dirty_old_fast dirty_fast FileInfo.merge_names
in
290 | Some
fast -> Relative_path.Map.add acc ~key
:fn ~data
:fast
292 end ~init
:Relative_path.Map.empty
294 let get_all_deps {FileInfo.n_funs
; n_classes
; n_types
; n_consts
} =
295 let add_deps_of_sset dep_ctor sset depset
=
296 SSet.fold sset ~init
:depset ~f
:begin fun n acc
->
297 let dep = dep_ctor n
in
298 let deps = Typing_deps.get_bazooka
dep in
299 DepSet.union
deps acc
302 let deps = add_deps_of_sset (fun n
-> Dep.Fun n
) n_funs
DepSet.empty
in
303 let deps = add_deps_of_sset (fun n
-> Dep.FunName n
) n_funs
deps in
304 let deps = add_deps_of_sset (fun n
-> Dep.Class n
) n_classes
deps in
305 let deps = add_deps_of_sset (fun n
-> Dep.Class n
) n_types
deps in
306 let deps = add_deps_of_sset (fun n
-> Dep.GConst n
) n_consts
deps in
307 let deps = add_deps_of_sset (fun n
-> Dep.GConstName n
) n_consts
deps in
310 (* We start of with a list of files that have changed since the state was saved
311 * (dirty_files), and two maps of the class / function declarations -- one made
312 * when the state was saved (old_fast) and one made for the current files in
313 * the repository (fast). We grab the declarations from both, to account for
314 * both the declaratons that were deleted and those that are newly created.
315 * Then we use the deptable to figure out the files that referred to them.
316 * Finally we recheck the lot. *)
317 let type_check_dirty genv
env old_fast fast dirty_files t =
318 let fast = get_dirty_fast old_fast fast dirty_files in
319 let names = Relative_path.Map.fold
fast ~f
:begin fun _k v acc
->
320 FileInfo.merge_names v acc
321 end ~init
:FileInfo.empty_names
in
322 let deps = get_all_deps names in
323 let to_recheck = Typing_deps.get_files
deps in
324 let fast = extend_fast
fast env.files_info to_recheck in
325 type_check genv
env fast t
327 let ai_check genv
files_info env t =
328 match ServerArgs.ai_mode genv
.options
with
330 let all_passed = List.for_all
331 [env.failed_parsing
; env.failed_decl
]
332 (fun m
-> Relative_path.Set.is_empty m
) in
333 if not
all_passed then begin
334 Hh_logger.log
"Cannot run AI because of errors in source";
335 Exit_status.exit
Exit_status.CantRunAI
337 let check_mode = ServerArgs.check_mode genv
.options
in
338 let errorl, failed
= Ai.go
339 Typing_check_utils.check_defs genv
.workers
files_info
340 env.tcopt ai_opt
check_mode in
342 errorl = Errors.merge
errorl env.errorl;
343 failed_check
= Relative_path.Set.union failed
env.failed_check
;
345 env, (Hh_logger.log_duration
"Ai" t)
348 let print_hash_stats () =
349 let {SharedMem.used_slots
; slots
; nonempty_slots
= _} = SharedMem.dep_stats
() in
350 let load_factor = float_of_int used_slots
/. float_of_int slots
in
351 Hh_logger.log
"Dependency table load factor: %d / %d (%.02f)"
352 used_slots slots
load_factor;
354 let {SharedMem.used_slots
; slots
; nonempty_slots
} = SharedMem.hash_stats
() in
355 let load_factor = float_of_int used_slots
/. float_of_int slots
in
356 Hh_logger.log
"Hashtable load factor: %d / %d (%.02f) with %d nonempty slots"
357 used_slots slots
load_factor nonempty_slots
;
360 let get_build_targets env =
362 List.map
(BuildMain.get_live_targets
env) (Relative_path.(concat Root
)) in
363 Relative_path.set_of_list
targets
366 let init ?load_mini_script genv
=
367 (* Log lazy declarations *)
368 let lazy_decl = genv
.local_config
.SLC.lazy_decl
369 && Option.is_none
(ServerArgs.ai_mode genv
.options
) in
370 let env = ServerEnvBuild.make_env genv
.config
in
371 let root = ServerArgs.root genv
.options
in
372 let saved_state_load_type =
373 LSC.saved_state_load_type_to_string
374 genv
.local_config
.SLC.load_script_config
in
376 (* Spawn this first so that it can run in the background while parsing is
377 * going on. The script can fail in a variety of ways, but the resolution
378 * is always the same -- we fall back to rechecking everything. Running it
379 * in the Result monad provides a convenient way to locate the error
380 * handling code in one place. *)
381 let load_mini_script = Result.of_option
load_mini_script ~error
:No_loader
in
383 load_mini_script >>= mk_state_future root saved_state_load_type in
385 let get_next, t = indexing genv
in
386 let env, t = parsing genv
env ~
get_next t in
388 let timeout = genv
.local_config
.SLC.load_mini_script_timeout
in
389 let state_future = state_future >>= fun f
->
390 with_loader_timeout timeout "wait_for_state" f
392 HackEventLogger.load_mini_state_end
t;
393 let t = Hh_logger.log_duration
"Loading mini-state" t in
395 let t = update_files genv
env.files_info t in
396 let env, t = naming env t in
397 let fast = FileInfo.simplify_fast
env.files_info in
398 let fast = Relative_path.Set.fold
env.failed_parsing
399 ~f
:(fun x m
-> Relative_path.Map.remove m x
) ~
init:fast in
401 if lazy_decl then env, t
402 else type_decl genv
env fast t in
404 let state = state_future >>= fun f
->
405 with_loader_timeout timeout "wait_for_changes" f
406 |> Result.join
>>= fun (dirty_files, old_fast) ->
407 genv
.wait_until_ready
();
408 let root = Path.to_string
root in
409 let updates = genv
.notifier
() in
410 let updates = SSet.filter
updates (fun p
->
411 string_starts_with p
root && ServerEnv.file_filter p
) in
412 let changed_while_parsing = Relative_path.(relativize_set Root
updates) in
413 (* Build targets are untracked by version control, so we must always
414 * recheck them. While we could query hg / git for the untracked files,
415 * it's much slower. *)
417 Relative_path.Set.union
dirty_files (get_build_targets env) in
418 Ok
(dirty_files, changed_while_parsing, old_fast)
420 HackEventLogger.vcs_changed_files_end
t;
421 let t = Hh_logger.log_duration
"Finding changed files" t in
425 | Ok
(dirty_files, changed_while_parsing, old_fast) ->
426 Hh_logger.log
"Successfully loaded mini-state";
427 (* If a file has changed while we were parsing, we may have parsed the
428 * new version, so we must treat it as possibly creating new type
431 Relative_path.Set.union
dirty_files changed_while_parsing in
432 (* But we still want to keep it in the set of things that need to be
433 * reparsed in the next round of incremental updates. *)
436 Relative_path.Set.union
env.failed_parsing
changed_while_parsing;
438 type_check_dirty genv
env old_fast fast dirty_files t
440 (* Fall back to type-checking everything *)
441 if err
<> No_loader
then begin
442 HackEventLogger.load_mini_exn err
;
443 Hh_logger.exc ~prefix
:"Could not load mini state: " err
;
445 type_check genv
env fast t
448 let env, _t
= ai_check genv
env.files_info env t in
450 SharedMem.init_done
();
452 env, Result.is_ok
state