Create parser options, remove non typechecker options from tcopts, make parser_hack...
[hiphop-php.git] / hphp / hack / src / server / serverInit.ml
blobd14230f816f297481c9c15810808569f705956f7
1 (**
2 * Copyright (c) 2015, Facebook, Inc.
3 * All rights reserved.
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.
9 *)
11 open Core
12 open ServerEnv
13 open ServerCheckUtils
14 open Reordered_argument_collections
15 open Utils
16 open String_utils
18 open Result.Export
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
26 exception No_loader
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
43 * saved.
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 ->
60 (FindUtils.is_php 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"))
68 end in
69 let next_files_hhi = compose
70 (List.map ~f:(Relative_path.(create Hhi)))
71 (Find.make_next_files
72 ~name:"hhi" ~filter:hhi_filter hhi_root) in
73 fun () ->
74 match next_files_hhi () with
75 | [] -> next_files_root ()
76 | x -> x
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;
94 raise e
96 let check_json_obj_error kv =
97 match List.Assoc.find kv "error" with
98 | Some (Hh_json.JSON_String s) -> failwith s
99 | _ -> ()
101 (* Expected output from script:
102 * Two lines of JSON.
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
105 * was built
107 let load_state root saved_state_load_type cmd (_ic, oc) =
109 let load_script_log_file = ServerFiles.load_log root in
110 let cmd =
111 Printf.sprintf
112 "%s %s %s %s %s"
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
124 let is_cached =
125 Hh_json.get_bool_exn @@ List.Assoc.find_exn kv "is_cached" in
126 let deptable_fn =
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;
135 let to_recheck =
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)
139 with e ->
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 () ->
158 let log_file =
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
163 in fun () ->
164 let fn =
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;
173 with e ->
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
178 * through *)
179 (try Daemon.kill daemon with e -> Hh_logger.exc e);
180 raise e
181 in fun () ->
182 fn >>= fun fn ->
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
199 let indexing genv =
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
204 get_next, t
206 let parsing genv env ~get_next t =
207 let files_info, errorl, failed =
208 Parsing_service.go
209 genv.workers
210 Relative_path.Map.empty
211 ~get_next
212 env.popt in
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;
219 let env = { env with
220 files_info;
221 errorl = Errors.merge errorl env.errorl;
222 failed_parsing = Relative_path.Set.union env.failed_parsing failed;
223 } in
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
233 let naming env t =
234 let env =
235 Relative_path.Map.fold env.files_info ~f:begin fun k v env ->
236 let errorl, failed = NamingGlobal.ndecl_file k v in
237 { env with
238 errorl = Errors.merge errorl env.errorl;
239 failed_parsing = Relative_path.Set.union env.failed_parsing failed;
241 end ~init:env
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
256 let env = {
257 env with
258 errorl = Errors.merge errorl env.errorl;
259 failed_decl;
260 } in
261 env, t
263 let type_check genv env fast t =
264 if ServerArgs.ai_mode genv.options = None || not (is_check_mode genv.options)
265 then begin
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
269 let { Decl_service.
270 errs = failed;
271 lazy_decl_errs = lazy_decl_failed;
272 } = err_info in
273 let hs = SharedMem.heap_size () in
274 Hh_logger.log "Heap size: %d" hs;
275 HackEventLogger.type_check_end count t;
276 let env = { env with
277 errorl = Errors.merge errorl env.errorl;
278 failed_decl = Relative_path.Set.union env.failed_decl lazy_decl_failed;
279 failed_check = failed;
280 } in
281 env, (Hh_logger.log_duration "Type-check" t)
282 end else env, 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
289 match fast with
290 | Some fast -> Relative_path.Map.add acc ~key:fn ~data:fast
291 | None -> acc
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
308 deps
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
329 | Some ai_opt ->
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
336 end;
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
341 let env = { env with
342 errorl = Errors.merge errorl env.errorl;
343 failed_check = Relative_path.Set.union failed env.failed_check;
344 } in
345 env, (Hh_logger.log_duration "Ai" t)
346 | None -> env, 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 =
361 let targets =
362 List.map (BuildMain.get_live_targets env) (Relative_path.(concat Root)) in
363 Relative_path.set_of_list targets
365 (* entry point *)
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
382 let state_future =
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
400 let env, t =
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. *)
416 let dirty_files =
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
423 let env, t =
424 match state with
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
429 * errors. *)
430 let dirty_files =
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. *)
434 let env = { env with
435 failed_parsing =
436 Relative_path.Set.union env.failed_parsing changed_while_parsing;
437 } in
438 type_check_dirty genv env old_fast fast dirty_files t
439 | Error err ->
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;
444 end;
445 type_check genv env fast t
448 let env, _t = ai_check genv env.files_info env t in
450 SharedMem.init_done ();
451 print_hash_stats ();
452 env, Result.is_ok state