Skip to content

Commit

Permalink
flow cycle subcommand
Browse files Browse the repository at this point in the history
Summary:
Dependency cycles can be bad for Flow performance. This diff introduces a new
subcommand that makes it easier to extract useful information from Flow's
dependency information.

In particular, users can point to any file in a cycle and the command will
output a DOT file to stdout. This file can be fed into a program like Gephi to
to further analysis.

Reviewed By: gabelevi

Differential Revision: D6202923

fbshipit-source-id: 94f6c7862443f0cf0ee8fbfdd22e79504e584dda
  • Loading branch information
samwgoldman authored and facebook-github-bot committed Dec 7, 2017
1 parent 622bbc4 commit e6a10b1
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/commands/cycleCommand.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
(**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)

open CommandUtils

let print_endlinef = Utils_js.print_endlinef
let prerr_endlinef = Utils_js.prerr_endlinef

let spec = {
CommandSpec.
name = "cycle";
doc = "Output .dot file for cycle containing the given file";
usage = Printf.sprintf
"Usage: %s cycle [OPTION]...\n\n\
e.g. %s cycle path/to/file.js \n"
Utils_js.exe_name
Utils_js.exe_name;
args = CommandSpec.ArgSpec.(
empty
|> server_flags
|> root_flag
|> strip_root_flag
|> anon "FILE..." (required string)
~doc:"File contained in the cycle"
)
}

let main option_values root strip_root file () =
let file = expand_path file in
let root = guess_root root in
let strip_root f =
if strip_root
then Files.relative_path (Path.to_string root) f
else f
in
(* connect to server *)
let request = ServerProt.Request.CYCLE file in
match connect_and_make_request option_values root request with
| ServerProt.Response.CYCLE (Error msg) ->
prerr_endline msg
| ServerProt.Response.CYCLE (Ok dep_graph) ->
(* print .dot file to stdout *)
print_endline "digraph {";
List.iter (fun (f, dep_fs) ->
List.iter (fun dep_f ->
print_endlinef " \"%s\" -> \"%s\""
(strip_root f)
(strip_root dep_f)
) dep_fs
) dep_graph;
print_endline "}"
| response -> failwith_bad_response ~request ~response

let command = CommandSpec.command spec main
1 change: 1 addition & 0 deletions src/flow.ml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ end = struct
CheckContentsCommand.command;
ConfigCommands.Init.command;
CoverageCommand.command;
CycleCommand.command;
DumpTypesCommand.command;
FindModuleCommand.command;
FindRefsCommand.command;
Expand Down
45 changes: 45 additions & 0 deletions src/server/commandHandler.ml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,45 @@ let coverage ~options ~workers ~env ~force file_input =
Type_info_service.coverage ~options ~workers ~env ~force file content
end

let get_cycle ~workers ~env fn =
(* Re-calculate SCC *)
let parsed = FilenameSet.elements !env.ServerEnv.files in
let dependency_graph = Dep_service.calc_dependency_graph workers parsed in
let partition = Sort_js.topsort dependency_graph in
let component_map = Sort_js.component_map partition in

(* Get component for target file *)
let leader_map =
FilenameMap.fold (fun file component acc ->
List.fold_left (fun acc file_ ->
FilenameMap.add file_ file acc
) acc component
) component_map FilenameMap.empty
in
let leader = FilenameMap.find_unsafe fn leader_map in
let component = FilenameMap.find_unsafe leader component_map in

(* Restrict dep graph to only in-cycle files *)
let subgraph = List.fold_left (fun acc f ->
Option.fold (FilenameMap.get f dependency_graph) ~init:acc ~f:(fun acc deps ->
let subdeps = FilenameSet.filter (fun f -> List.mem f component) deps in
if FilenameSet.is_empty subdeps
then acc
else FilenameMap.add f subdeps acc
)
) FilenameMap.empty component in

(* Convert from map/set to lists for serialization to client. *)
let subgraph = FilenameMap.fold (fun f dep_fs acc ->
let f = File_key.to_string f in
let dep_fs = FilenameSet.fold (fun dep_f acc ->
(File_key.to_string dep_f)::acc
) dep_fs [] in
(f, dep_fs)::acc
) subgraph [] in

Ok subgraph

let suggest =
let suggest_for_file ~options ~workers ~env result_map (file, region) =
SMap.add file (try_with begin fun () ->
Expand Down Expand Up @@ -303,6 +342,12 @@ let handle_ephemeral_unsafe
ServerProt.Response.COVERAGE (
coverage ~options ~workers ~env ~force fn: ServerProt.Response.coverage_response
) |> respond
| ServerProt.Request.CYCLE fn ->
let file_options = Options.file_options options in
let fn = Files.filename_from_string ~options:file_options fn in
ServerProt.Response.CYCLE (
get_cycle ~workers ~env fn: ServerProt.Response.cycle_response
) |> respond
| ServerProt.Request.DUMP_TYPES (fn) ->
ServerProt.Response.DUMP_TYPES (dump_types ~options ~workers ~env fn)
|> respond
Expand Down
8 changes: 8 additions & 0 deletions src/server/protocol/serverProt.ml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Request = struct
bool * (* force *)
bool (* include_warnings *)
| COVERAGE of File_input.t * bool (* force *)
| CYCLE of string
| DUMP_TYPES of File_input.t
| FIND_MODULE of string * string
| FIND_REFS of File_input.t * int * int * bool (* filename, line, char, global *)
Expand All @@ -37,6 +38,8 @@ module Request = struct
Printf.sprintf "check %s" (File_input.filename_of_file_input fn)
| COVERAGE (fn, _) ->
Printf.sprintf "coverage %s" (File_input.filename_of_file_input fn)
| CYCLE fn ->
Printf.sprintf "cycle %s" fn
| DUMP_TYPES (fn) ->
Printf.sprintf "dump-types %s" (File_input.filename_of_file_input fn)
| FIND_MODULE (moduleref, filename) ->
Expand Down Expand Up @@ -119,6 +122,9 @@ module Response = struct
(* map of files to `Ok (line, col, annotation)` or `Error msg` *)
type suggest_response = ((int * int * string) list, string) result SMap.t

type cycle_response = (cycle_response_subgraph, string) result
and cycle_response_subgraph = (string * string list) list

type gen_flow_files_error =
| GenFlowFiles_TypecheckError of {errors: Errors.ErrorSet.t; warnings: Errors.ErrorSet.t}
| GenFlowFiles_UnexpectedError of string
Expand Down Expand Up @@ -149,6 +155,7 @@ module Response = struct
| AUTOCOMPLETE of autocomplete_response
| CHECK_FILE of check_file_response
| COVERAGE of coverage_response
| CYCLE of cycle_response
| DUMP_TYPES of dump_types_response
| FIND_MODULE of find_module_response
| FIND_REFS of find_refs_response
Expand All @@ -165,6 +172,7 @@ module Response = struct
| AUTOCOMPLETE _ -> "autocomplete response"
| CHECK_FILE _ -> "check_file response"
| COVERAGE _ -> "coverage response"
| CYCLE _ -> "cycle reponse"
| DUMP_TYPES _ -> "dump_types response"
| FIND_MODULE _ -> "find_module response"
| FIND_REFS _ -> "find_refs response"
Expand Down

0 comments on commit e6a10b1

Please sign in to comment.