You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
14 KiB

(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module L = Logging
module F = Format
module Implementation = struct
let attribute_replace_statement =
(* The innermost SELECT returns either the current attributes_kind and source_file associated with
the given proc name, or default values of (-1,""). These default values have the property that
they are always "less than" any legit value. More precisely, MAX ensures that some value is
returned even if there is no row satisfying WHERE (we'll get NULL in that case, the value in
the row otherwise). COALESCE then returns the first non-NULL value, which will be either the
value of the row corresponding to that pname in the DB, or the default if no such row exists.
The next (second-outermost) SELECT filters out that value if it is "more defined" than the ones
we would like to insert (which will never be the case if the default values are returned). If
not, it returns a trivial row (consisting solely of NULL since we don't use its values) and the
INSERT OR REPLACE will proceed and insert or update the values stored into the DB for that
pname. *)
(* TRICK: use the source file to be more deterministic in case the same procedure name is defined
in several files *)
(* TRICK: older versions of sqlite (prior to version 3.15.0 (2016-10-14)) do not support row
values so the lexicographic ordering for (:akind, :sfile) is done by hand *)
ResultsDatabase.register_statement
{|
INSERT OR REPLACE INTO procedures
SELECT :pname, :proc_name_hum, :akind, :sfile, :pattr, :cfg, :callees
FROM (
SELECT NULL
FROM (
SELECT COALESCE(MAX(attr_kind),-1) AS attr_kind,
COALESCE(MAX(source_file),"") AS source_file
FROM procedures
WHERE proc_name = :pname )
WHERE attr_kind < :akind
OR (attr_kind = :akind AND source_file <= :sfile) )
|}
let replace_attributes ~pname_str ~pname ~akind ~source_file ~attributes ~proc_desc ~callees =
ResultsDatabase.with_registered_statement attribute_replace_statement ~f:(fun db replace_stmt ->
Sqlite3.bind replace_stmt 1 (* :pname *) pname
|> SqliteUtils.check_result_code db ~log:"replace bind pname" ;
Sqlite3.bind replace_stmt 2 (* :proc_name_hum *) (Sqlite3.Data.TEXT pname_str)
|> SqliteUtils.check_result_code db ~log:"replace bind proc_name_hum" ;
Sqlite3.bind replace_stmt 3 (* :akind *) (Sqlite3.Data.INT akind)
|> SqliteUtils.check_result_code db ~log:"replace bind attribute kind" ;
Sqlite3.bind replace_stmt 4 (* :sfile *) source_file
|> SqliteUtils.check_result_code db ~log:"replace bind source file" ;
Sqlite3.bind replace_stmt 5 (* :pattr *) attributes
|> SqliteUtils.check_result_code db ~log:"replace bind proc attributes" ;
Sqlite3.bind replace_stmt 6 (* :cfg *) proc_desc
|> SqliteUtils.check_result_code db ~log:"replace bind cfg" ;
Sqlite3.bind replace_stmt 7 (* :callees *) callees
|> SqliteUtils.check_result_code db ~log:"replace bind callees" ;
SqliteUtils.result_unit db ~finalize:false ~log:"Attributes.replace" replace_stmt )
let source_file_store_statement =
ResultsDatabase.register_statement
{|
INSERT OR REPLACE INTO source_files
VALUES (:source, :tenv, :integer_type_widths, :proc_names, :freshly_captured)
|}
let add_source_file ~source_file ~tenv ~integer_type_widths ~proc_names =
ResultsDatabase.with_registered_statement source_file_store_statement ~f:(fun db store_stmt ->
Sqlite3.bind store_stmt 1 source_file
(* :source *)
|> SqliteUtils.check_result_code db ~log:"store bind source file" ;
Sqlite3.bind store_stmt 2 tenv
(* :tenv *)
|> SqliteUtils.check_result_code db ~log:"store bind type environment" ;
Sqlite3.bind store_stmt 3 integer_type_widths
(* :integer_type_widths *)
|> SqliteUtils.check_result_code db ~log:"store bind integer type widths" ;
Sqlite3.bind store_stmt 4 proc_names
(* :proc_names *)
|> SqliteUtils.check_result_code db ~log:"store bind proc names" ;
Sqlite3.bind store_stmt 5 (Sqlite3.Data.INT Int64.one)
(* :freshly_captured *)
|> SqliteUtils.check_result_code db ~log:"store freshness" ;
SqliteUtils.result_unit ~finalize:false ~log:"Cfg.store" db store_stmt )
let mark_all_source_files_stale_statement =
ResultsDatabase.register_statement "UPDATE source_files SET freshly_captured = 0"
let mark_all_source_files_stale () =
ResultsDatabase.with_registered_statement mark_all_source_files_stale_statement
~f:(fun db stmt -> SqliteUtils.result_unit db ~finalize:false ~log:"mark_all_stale" stmt)
let merge_procedures_table ~db_file =
let db = ResultsDatabase.get_database () in
(* Do the merge purely in SQL for great speed. The query works by doing a left join between the
sub-table and the main one, and applying the same "more defined" logic as in Attributes in the
cases where a proc_name is present in both the sub-table and the main one (main.attr_kind !=
NULL). All the rows that pass this filter are inserted/updated into the main table. *)
Sqlite3.exec db
{|
INSERT OR REPLACE INTO memdb.procedures
SELECT sub.proc_name, sub.proc_name_hum, sub.attr_kind, sub.source_file, sub.proc_attributes, sub.cfg, sub.callees
FROM (
attached.procedures AS sub
LEFT OUTER JOIN memdb.procedures AS main
ON sub.proc_name = main.proc_name )
WHERE
main.attr_kind IS NULL
OR main.attr_kind < sub.attr_kind
OR (main.attr_kind = sub.attr_kind AND main.source_file < sub.source_file)
|}
|> SqliteUtils.check_result_code db
~log:(Printf.sprintf "copying procedures of database '%s'" db_file)
let merge_source_files_table ~db_file =
let db = ResultsDatabase.get_database () in
Sqlite3.exec db
{|
INSERT OR REPLACE INTO memdb.source_files
SELECT source_file, type_environment, integer_type_widths, procedure_names, 1
FROM attached.source_files
|}
|> SqliteUtils.check_result_code db
~log:(Printf.sprintf "copying source_files of database '%s'" db_file)
let copy_to_main db =
Sqlite3.exec db {| INSERT OR REPLACE INTO procedures SELECT * FROM memdb.procedures |}
|> SqliteUtils.check_result_code db ~log:"Copying procedures into main db" ;
Sqlite3.exec db {| INSERT OR REPLACE INTO source_files SELECT * FROM memdb.source_files |}
|> SqliteUtils.check_result_code db ~log:"Copying source_files into main db"
let merge_db infer_out_src =
let db_file = ResultsDirEntryName.get_path ~results_dir:infer_out_src CaptureDB in
let main_db = ResultsDatabase.get_database () in
Sqlite3.exec main_db (Printf.sprintf "ATTACH '%s' AS attached" db_file)
|> SqliteUtils.check_result_code main_db ~log:(Printf.sprintf "attaching database '%s'" db_file) ;
merge_procedures_table ~db_file ;
merge_source_files_table ~db_file ;
Sqlite3.exec main_db "DETACH attached"
|> SqliteUtils.check_result_code main_db ~log:(Printf.sprintf "detaching database '%s'" db_file)
let merge infer_deps_file =
let main_db = ResultsDatabase.get_database () in
Sqlite3.exec main_db "ATTACH ':memory:' AS memdb"
|> SqliteUtils.check_result_code main_db ~log:"attaching memdb" ;
ResultsDatabase.create_tables ~prefix:"memdb." main_db ;
Utils.iter_infer_deps ~project_root:Config.project_root ~f:merge_db infer_deps_file ;
copy_to_main main_db ;
Sqlite3.exec main_db "DETACH memdb"
|> SqliteUtils.check_result_code main_db ~log:"detaching memdb"
let canonicalize () =
let db = ResultsDatabase.get_database () in
SqliteUtils.exec db ~log:"running VACUUM" ~stmt:"VACUUM"
let reset_capture_tables () =
let db = ResultsDatabase.get_database () in
SqliteUtils.exec db ~log:"drop procedures table" ~stmt:"DROP TABLE procedures" ;
SqliteUtils.exec db ~log:"drop source_files table" ~stmt:"DROP TABLE source_files" ;
ResultsDatabase.create_tables db
end
module Command = struct
type t =
| ReplaceAttributes of
{ pname_str: string
; pname: Sqlite3.Data.t
; akind: int64
; source_file: Sqlite3.Data.t
; attributes: Sqlite3.Data.t
; proc_desc: Sqlite3.Data.t
; callees: Sqlite3.Data.t }
| AddSourceFile of
{ source_file: Sqlite3.Data.t
; tenv: Sqlite3.Data.t
; integer_type_widths: Sqlite3.Data.t
; proc_names: Sqlite3.Data.t }
| MarkAllSourceFilesStale
| Merge of {infer_deps_file: string}
| Vacuum
| ResetCaptureTables
| Handshake
| Terminate
let to_string = function
| ReplaceAttributes _ ->
"ReplaceAttributes"
| AddSourceFile _ ->
"AddSourceFile"
| MarkAllSourceFilesStale ->
"MarkAllSourceFilesStale"
| Merge _ ->
"Merge"
| Vacuum ->
"Vacuum"
| ResetCaptureTables ->
"ResetCaptureTables"
| Handshake ->
"Handshake"
| Terminate ->
"Terminate"
let pp fmt cmd = F.pp_print_string fmt (to_string cmd)
let execute = function
| ReplaceAttributes {pname_str; pname; akind; source_file; attributes; proc_desc; callees} ->
Implementation.replace_attributes ~pname_str ~pname ~akind ~source_file ~attributes
~proc_desc ~callees
| AddSourceFile {source_file; tenv; integer_type_widths; proc_names} ->
Implementation.add_source_file ~source_file ~tenv ~integer_type_widths ~proc_names
| MarkAllSourceFilesStale ->
Implementation.mark_all_source_files_stale ()
| Merge {infer_deps_file} ->
Implementation.merge infer_deps_file
| Vacuum ->
Implementation.canonicalize ()
| ResetCaptureTables ->
Implementation.reset_capture_tables ()
| Handshake ->
()
| Terminate ->
()
end
type response = Ack
module Server = struct
(* General comment about socket/channel destruction: closing the in_channel associated with the socket
will close the file descriptor too, so closing also the out_channel sometimes throws an exception.
That's why in all code below only the input channel is ever closed. *)
let socket_name = "sqlite_write_socket"
let socket_addr = Unix.ADDR_UNIX socket_name
let socket_domain = Unix.domain_of_sockaddr socket_addr
(** Unix socket *paths* have a historical length limit of ~100 chars (!?*\@&*$). However, this
only applies to the argument passed in the system call to create the socket, not to the actual
path. Thus a workaround is to cd into the parent dir of the socket and then use it, hence this
function. *)
let in_results_dir ~f = Utils.do_in_dir ~dir:Config.toplevel_results_dir ~f
let rec server_loop socket =
let client_sock, _client = Unix.accept socket in
let in_channel = Unix.in_channel_of_descr client_sock
and out_channel = Unix.out_channel_of_descr client_sock in
let command : Command.t = Marshal.from_channel in_channel in
L.debug Analysis Verbose "Sqlite write daemon: received command %a@." Command.pp command ;
Command.execute command ;
Marshal.to_channel out_channel Ack [] ;
Out_channel.flush out_channel ;
In_channel.close in_channel ;
L.debug Analysis Verbose "Sqlite write daemon: closing connection@." ;
match command with
| Terminate ->
L.debug Analysis Quiet "Sqlite write daemon: terminating@." ;
()
| _ ->
server_loop socket
let socket_exists () = in_results_dir ~f:(fun () -> Sys.file_exists_exn socket_name)
let server () =
L.debug Analysis Quiet "Sqlite write daemon: starting up@." ;
if socket_exists () then L.die InternalError "Sqlite write daemon: socket already exists@." ;
let socket = Unix.socket ~domain:socket_domain ~kind:Unix.SOCK_STREAM ~protocol:0 () in
in_results_dir ~f:(fun () -> Unix.bind socket ~addr:socket_addr) ;
(* [backlog] is (supposedly) the length of the queue for pending connections ;
there are no rules about the implied behaviour though. Here use optimistically
the number of workers, though even that is a guess. *)
Unix.listen socket ~backlog:Config.jobs ;
L.debug Analysis Quiet "Sqlite write daemon: set up complete, waiting for connections@." ;
let shutdown () =
in_results_dir ~f:(fun () ->
Unix.close socket ;
Unix.remove socket_name )
in
Utils.try_finally_swallow_timeout ~f:(fun () -> server_loop socket) ~finally:shutdown
let send cmd =
let in_channel, out_channel = in_results_dir ~f:(fun () -> Unix.open_connection socket_addr) in
Marshal.to_channel out_channel cmd [] ;
Out_channel.flush out_channel ;
let (Ack : response) = Marshal.from_channel in_channel in
In_channel.close in_channel
let rec retry ~pred ~timeout count =
if count < 0 then false
else if pred () then true
else (
Unix.nanosleep timeout |> ignore ;
retry ~pred ~timeout (count - 1) )
let start () =
match Unix.fork () with
| `In_the_child ->
ForkUtils.protect ~f:server () ;
L.exit 0
| `In_the_parent _child_pid ->
(* wait for socket to appear, try 5 times, with a 0.1 sec timeout each time ;
choice of numbers is completely arbitrary *)
if not (retry ~pred:socket_exists ~timeout:0.1 5) then
L.die InternalError "Sqlite write daemon never started@." ;
send Command.Handshake
end
let use_daemon = Config.((not (buck || genrule_mode)) && jobs > 1)
let perform cmd = if use_daemon then Server.send cmd else Command.execute cmd
let start () = Server.start ()
let stop () = Server.send Command.Terminate
let replace_attributes ~pname_str ~pname ~akind ~source_file ~attributes ~proc_desc ~callees =
Command.ReplaceAttributes {pname_str; pname; akind; source_file; attributes; proc_desc; callees}
|> perform
let add_source_file ~source_file ~tenv ~integer_type_widths ~proc_names =
Command.AddSourceFile {source_file; tenv; integer_type_widths; proc_names} |> perform
let mark_all_source_files_stale () = perform Command.MarkAllSourceFilesStale
let merge ~infer_deps_file = Command.Merge {infer_deps_file} |> perform
let canonicalize () = perform Command.Vacuum
let reset_capture_tables () = perform Command.ResetCaptureTables