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.

879 lines
33 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 F = Format
module L = Logging
module BasicCost = CostDomain.BasicCost
let attrs_of_pname = Summary.OnDisk.proc_resolve_attributes
module Payload = SummaryPayload.Make (struct
type t = CostDomain.summary
let field = Payloads.Fields.cost
end)
(* CFG modules used in several other modules *)
module InstrCFG = ProcCfg.NormalOneInstrPerNode
module NodeCFG = ProcCfg.Normal
module Node = ProcCfg.DefaultNode
(* Map associating to each node a bound on the number of times it can be executed.
This bound is computed using environments (map: val -> values), using the following
observation: the number of environments associated with a program point is an upperbound
of the number of times the program point can be executed in any execution.
The size of an environment env is computed as:
|env| = |env(v1)| * ... * |env(n_k)|
where |env(v)| is the size of the interval associated to v by env.
Reference: see Stefan Bygde PhD thesis, 2010
*)
module BoundMap = struct
let print_upper_bound_map bound_map =
L.(debug Analysis Medium)
"@\n\n******* Bound Map : [node -> bound] ITV **** @\n %a @\n"
(Node.IdMap.pp ~pp_value:BasicCost.pp)
bound_map ;
L.(debug Analysis Medium) "@\n******* END Bound Map ITV **** @\n\n"
let filter_loc vars_to_keep = function
| AbsLoc.Loc.Var (Var.LogicalVar _) ->
None
| AbsLoc.Loc.Var var ->
Control.ControlMap.find_opt var vars_to_keep
| _ ->
None
let compute_upperbound_map node_cfg inferbo_invariant_map control_invariant_map loop_inv_map =
let compute_node_upper_bound bound_map node =
let node_id = NodeCFG.Node.id node in
match Procdesc.Node.get_kind node with
| Procdesc.Node.Exit_node ->
Node.IdMap.add node_id BasicCost.one bound_map
| _ -> (
let exit_state_opt =
let instr_node_id = InstrCFG.last_of_underlying_node node |> InstrCFG.Node.id in
BufferOverrunAnalysis.extract_post instr_node_id inferbo_invariant_map
in
match exit_state_opt with
| Some entry_mem ->
(* compute control vars, i.e. set of variables that affect the execution count *)
let control_map =
Control.compute_control_vars control_invariant_map loop_inv_map node
in
L.(debug Analysis Medium)
"@\n>>> All dependencies for node = %a : %a @\n\n" Procdesc.Node.pp node
(Control.ControlMap.pp ~pp_value:Location.pp)
control_map ;
(* bound = env(v1) *... * env(vn) *)
let bound =
match entry_mem with
| Bottom ->
L.debug Analysis Medium
"@\n\
[COST ANALYSIS INTERNAL WARNING:] No 'env' found. This location is \
unreachable returning cost 0 \n" ;
BasicCost.zero
| ExcRaised ->
BasicCost.one
| NonBottom mem ->
let cost =
BufferOverrunDomain.MemReach.range ~filter_loc:(filter_loc control_map)
~node_id mem
in
(* The zero cost of node does not make sense especially when the abstract memory
is non-bottom. *)
if BasicCost.is_zero cost then BasicCost.one else cost
in
L.(debug Analysis Medium)
"@\n>>>Setting bound for node = %a to %a@\n\n" Node.pp_id node_id BasicCost.pp
bound ;
Node.IdMap.add node_id bound bound_map
| _ ->
Node.IdMap.add node_id BasicCost.zero bound_map )
in
let bound_map =
NodeCFG.fold_nodes node_cfg ~f:compute_node_upper_bound ~init:Node.IdMap.empty
in
print_upper_bound_map bound_map ; bound_map
let upperbound bound_map nid =
match Node.IdMap.find_opt nid bound_map with
| Some bound ->
bound
| None ->
L.(debug Analysis Medium)
"@\n\n[WARNING] Bound not found for node %a, returning Top @\n" Node.pp_id nid ;
BasicCost.top
end
module ControlFlowCost = struct
(* A Control-flow cost represents the number of times the flow of control can go through a certain CFG item (a node or an edge),
or a sum of such things *)
module Item = struct
type t = [`Node of Node.id | `Edge of Node.id * Node.id]
let compare : t -> t -> int =
fun x y ->
match (x, y) with
| `Node id1, `Node id2 ->
Node.compare_id id1 id2
| `Node _, `Edge _ ->
-1
| `Edge _, `Node _ ->
1
| `Edge (f1, t1), `Edge (f2, t2) ->
[%compare: Node.id * Node.id] (f1, t1) (f2, t2)
let equal = [%compare.equal: t]
let pp : F.formatter -> t -> unit =
fun fmt -> function
| `Node id ->
F.fprintf fmt "Node(%a)" Node.pp_id id
| `Edge (f, t) ->
F.fprintf fmt "Edge(%a -> %a)" Node.pp_id f Node.pp_id t
let normalize ~(normalizer : t -> [> t]) (x : t) : t =
match normalizer x with #t as x -> x | _ -> assert false
end
module Sum = struct
type 'a set = (* non-empty sorted list *) 'a list
type t = [`Sum of int * Item.t set]
let of_list l =
let length = List.length l in
let set = List.sort ~compare:Item.compare l in
`Sum (length, set)
let compare : t -> t -> int =
fun (`Sum (l1, s1)) (`Sum (l2, s2)) -> [%compare: int * Item.t list] (l1, s1) (l2, s2)
let pp : F.formatter -> t -> unit = fun fmt (`Sum (_, set)) -> Pp.seq ~sep:" + " Item.pp fmt set
let items (`Sum (_, l)) = l
let normalized_items ~normalizer (`Sum (_, l)) =
let normalizer = (normalizer :> Item.t -> [> Item.t]) in
l |> List.rev_map ~f:(Item.normalize ~normalizer)
let normalize ~normalizer sum = sum |> normalized_items ~normalizer |> of_list
(* Given a sum and an item, remove one occurence of the item in the sum. Returns [None] if the item is not present in the sum.
[remove_one_item ~item:A (A + B)] = B
[remove_one_item ~item:A (A + B + C)] = B + C
[remove_one_item ~item:A (A + A + B)] = A + B
[remove_one_item ~item:A (B + C)] = None
*)
let remove_one_item ~item (`Sum (len, l)) =
match IList.remove_first l ~f:(Item.equal item) with
| None ->
None
| Some [e] ->
Some (e :> [Item.t | t])
| Some l ->
Some (`Sum (len - 1, l))
let cost ~of_item (`Sum (_, l)) =
List.fold l ~init:BasicCost.zero ~f:(fun cost item -> BasicCost.plus cost (of_item item))
end
type t = [Item.t | Sum.t]
let compare : t -> t -> int =
fun x y ->
match (x, y) with
| (#Item.t as x), (#Item.t as y) ->
Item.compare x y
| #Item.t, #Sum.t ->
-1
| #Sum.t, #Item.t ->
1
| (#Sum.t as x), (#Sum.t as y) ->
Sum.compare x y
let make_node node = `Node node
let make_pred_edge succ pred = `Edge (pred, succ)
let make_succ_edge pred succ = `Edge (pred, succ)
let pp : F.formatter -> t -> unit =
fun fmt -> function #Item.t as item -> Item.pp fmt item | #Sum.t as sum -> Sum.pp fmt sum
let sum : Item.t list -> t = function [] -> assert false | [e] -> (e :> t) | l -> Sum.of_list l
module Set = struct
type elt = t [@@deriving compare]
type t =
{ mutable size: int
; mutable items: Item.t ARList.t
; mutable sums: Sum.t ARList.t
; mutable cost: BasicCost.t }
let create e =
let items, sums =
match e with
| #Item.t as item ->
(ARList.singleton item, ARList.empty)
| #Sum.t as sum ->
(ARList.empty, ARList.singleton sum)
in
{size= 1; items; sums; cost= BasicCost.top}
let compare_size {size= size1} {size= size2} = Int.compare size1 size2
(* Invalidation is just a sanity check, union-find already takes care of it. *)
let is_valid {size} = size >= 1
let cost {cost} = cost
(* move semantics, should not be called with aliases *)
let merge ~from ~to_ =
assert (not (phys_equal from to_)) ;
assert (is_valid from) ;
assert (is_valid to_) ;
to_.size <- to_.size + from.size ;
to_.items <- ARList.append to_.items from.items ;
to_.sums <- ARList.append to_.sums from.sums ;
from.size <- 0
let pp_equalities fmt t =
ARList.append (t.items :> elt ARList.t) (t.sums :> elt ARList.t)
|> IContainer.to_rev_list ~fold:ARList.fold_unordered
|> List.sort ~compare |> Pp.seq ~sep:" = " pp fmt
let normalize_sums : normalizer:(elt -> elt) -> t -> unit =
fun ~normalizer t ->
t.sums <-
t.sums
|> IContainer.rev_map_to_list ~fold:ARList.fold_unordered ~f:(Sum.normalize ~normalizer)
|> List.dedup_and_sort ~compare:Sum.compare
|> ARList.of_list
let infer_equalities_by_removing_item ~on_infer t item =
t.sums
|> IContainer.rev_filter_map_to_list ~fold:ARList.fold_unordered
~f:(Sum.remove_one_item ~item)
|> IContainer.iter_consecutive ~fold:List.fold ~f:on_infer
let sum_items t =
t.sums
|> ARList.fold_unordered ~init:ARList.empty ~f:(fun acc sum ->
sum |> Sum.items |> ARList.of_list |> ARList.append acc )
|> IContainer.to_rev_list ~fold:ARList.fold_unordered
|> List.dedup_and_sort ~compare:Item.compare
let infer_equalities_from_sums :
on_infer:(elt -> elt -> unit) -> normalizer:(elt -> elt) -> t -> unit =
fun ~on_infer ~normalizer t ->
normalize_sums ~normalizer t ;
(* Keep in mind that [on_infer] can modify [t].
It happens only if we merge a node while infering equalities from it, i.e. in the case an item appears in an equality class both alone and in two sums, i.e. X = A + X = A + B.
This is not a problem here (we could stop if it happens but it is not necessary as existing equalities still remain true after merges) *)
(* Also keep in mind that the current version, in the worst-case scenario, is quadratic-ish in the size of the CFG *)
sum_items t |> List.iter ~f:(fun item -> infer_equalities_by_removing_item ~on_infer t item)
let init_cost : of_node:(Node.id -> BasicCost.t) -> t -> unit =
fun ~of_node t ->
let min_if_node cost item =
match item with `Node node -> BasicCost.min_default_left cost (of_node node) | _ -> cost
in
t.cost <- ARList.fold_unordered t.items ~init:t.cost ~f:min_if_node
let improve_cost_from_sums :
on_improve:(Sum.t -> BasicCost.t -> BasicCost.t -> unit)
-> of_item:(Item.t -> BasicCost.t)
-> t
-> unit =
fun ~on_improve ~of_item t ->
let f sum =
let cost_of_sum = Sum.cost ~of_item sum in
let new_cost = BasicCost.min_default_left t.cost cost_of_sum in
if not (BasicCost.leq ~lhs:t.cost ~rhs:new_cost) then (
on_improve sum cost_of_sum new_cost ;
t.cost <- new_cost )
in
Container.iter t.sums ~fold:ARList.fold_unordered ~f
let improve_cost_with t cost' =
let old_cost = t.cost in
let new_cost = BasicCost.min_default_left old_cost cost' in
if not (BasicCost.leq ~lhs:old_cost ~rhs:new_cost) then (
t.cost <- new_cost ;
Some old_cost )
else None
end
end
module ConstraintSolver = struct
type debug = {f: 'a. ('a, F.formatter, unit, unit) format4 -> 'a} [@@unboxed]
module Equalities = struct
include ImperativeUnionFind.Make (ControlFlowCost.Set)
let normalizer equalities e = (find equalities e :> ControlFlowCost.t)
let pp_repr fmt (repr : Repr.t) = ControlFlowCost.pp fmt (repr :> ControlFlowCost.t)
let pp_equalities fmt equalities =
let pp_item fmt (repr, set) =
F.fprintf fmt "%a --> %a" pp_repr repr ControlFlowCost.Set.pp_equalities set
in
IContainer.pp_collection ~fold:fold_sets ~pp_item fmt equalities
let pp_costs fmt equalities =
let pp_item fmt (repr, set) =
F.fprintf fmt "%a --> %a" pp_repr repr BasicCost.pp (ControlFlowCost.Set.cost set)
in
IContainer.pp_collection ~fold:fold_sets ~pp_item fmt equalities
let log_union ~debug equalities e1 e2 =
match union equalities e1 e2 with
| None ->
debug.f "[UF] Preexisting %a = %a@\n" ControlFlowCost.pp e1 ControlFlowCost.pp e2 ;
false
| Some (e1, e2) ->
debug.f "[UF] Union %a into %a@\n" ControlFlowCost.pp e1 ControlFlowCost.pp e2 ;
true
let try_to_improve ~debug ~on_improve ~f equalities ~max =
let f did_improve repr_set =
if did_improve then (
f ~did_improve:(fun () -> ()) repr_set ;
true )
else
let did_improve = ref false in
f ~did_improve:(fun () -> did_improve := true) repr_set ;
!did_improve
in
let rec loop max =
if fold_sets equalities ~init:false ~f then (
on_improve () ;
if max > 0 then loop (max - 1)
else debug.f "[ConstraintSolver] Maximum number of iterations reached@\n" )
in
loop max
(**
Infer equalities from sums, like this:
(1) A + sum1 = A + sum2 => sum1 = sum2
It does not try to saturate
(2) A = B + C /\ B = D + E => A = C + D + E
Nor combine more than 2 equations
(3) A = B + C /\ B = D + E /\ F = C + D + E => A = F
((3) is implied by (1) /\ (2))
Its complexity is unknown but I think it is bounded by nbNodes x nbEdges x max.
*)
let infer_equalities_from_sums ~debug equalities ~max =
let normalizer = normalizer equalities in
let f ~did_improve (_repr, set) =
let on_infer e1 e2 = if log_union equalities ~debug e1 e2 then did_improve () in
ControlFlowCost.Set.infer_equalities_from_sums ~on_infer ~normalizer set
in
let on_improve () = debug.f "[ConstraintSolver][EInfe] %a@\n" pp_equalities equalities in
try_to_improve ~debug ~on_improve ~f equalities ~max
let normalize_sums equalities =
let normalizer = normalizer equalities in
Container.iter ~fold:fold_sets equalities ~f:(fun (_repr, set) ->
ControlFlowCost.Set.normalize_sums ~normalizer set )
let union ~debug equalities e1 e2 =
let (_ : bool) = log_union ~debug equalities e1 e2 in
()
let init_costs bound_map equalities =
let of_node node_id = BoundMap.upperbound bound_map node_id in
Container.iter equalities ~fold:fold_sets ~f:(fun (_repr, set) ->
ControlFlowCost.Set.init_cost ~of_node set )
(**
From sums: if A = B + C, do cost(A) = min(cost(A), cost(B) + cost(C))
From inequalities: if A = B + C, then B <= A, do cost(B) = min(cost(B), cost(A))
*)
let improve_costs ~debug equalities ~max =
let of_item (item : ControlFlowCost.Item.t) =
(item :> ControlFlowCost.t)
|> find equalities |> find_set equalities
|> Option.value_map ~f:ControlFlowCost.Set.cost ~default:BasicCost.top
in
let f ~did_improve (repr, set) =
let on_improve sum cost_of_sum new_cost =
debug.f
"[ConstraintSolver][CImpr] Improved cost of %a using %a (cost: %a), from %a to %a@\n"
pp_repr repr ControlFlowCost.Sum.pp sum BasicCost.pp cost_of_sum BasicCost.pp
(ControlFlowCost.Set.cost set) BasicCost.pp new_cost ;
did_improve ()
in
ControlFlowCost.Set.improve_cost_from_sums ~on_improve ~of_item set ;
let try_from_inequality (sum_item : ControlFlowCost.Item.t) =
let sum_item_set =
(sum_item :> ControlFlowCost.t) |> find equalities |> find_create_set equalities
in
match
ControlFlowCost.Set.improve_cost_with sum_item_set (ControlFlowCost.Set.cost set)
with
| Some previous_cost ->
debug.f
"[ConstraintSolver][CImpr] Improved cost of %a <= %a (cost: %a), from %a to %a@\n"
ControlFlowCost.Item.pp sum_item pp_repr repr BasicCost.pp
(ControlFlowCost.Set.cost set) BasicCost.pp previous_cost BasicCost.pp
(ControlFlowCost.Set.cost sum_item_set) ;
did_improve ()
| None ->
()
in
ControlFlowCost.Set.sum_items set |> List.iter ~f:try_from_inequality
in
let on_improve () = debug.f "[ConstraintSolver][CImpr] %a@\n" pp_costs equalities in
try_to_improve ~debug ~on_improve ~f equalities ~max
end
let add_constraints ~debug equalities node get_nodes make =
match get_nodes node with
| [] ->
(* either start/exit node or dead node (broken CFG) *)
()
| nodes ->
let node_id = Node.id node in
let edges = List.rev_map nodes ~f:(fun other -> make node_id (Node.id other)) in
let sum = ControlFlowCost.sum edges in
Equalities.union ~debug equalities (ControlFlowCost.make_node node_id) sum
let collect_on_node ~debug equalities node =
add_constraints ~debug equalities node Procdesc.Node.get_preds ControlFlowCost.make_pred_edge ;
add_constraints ~debug equalities node Procdesc.Node.get_succs ControlFlowCost.make_succ_edge
let collect_constraints ~debug node_cfg =
let equalities = Equalities.create () in
Container.iter node_cfg ~fold:NodeCFG.fold_nodes ~f:(collect_on_node ~debug equalities) ;
debug.f "[ConstraintSolver] Procedure %a @@ %a@\n" Typ.Procname.pp
(Procdesc.get_proc_name node_cfg) Location.pp_file_pos (Procdesc.get_loc node_cfg) ;
debug.f "[ConstraintSolver][EInit] %a@\n" Equalities.pp_equalities equalities ;
Equalities.normalize_sums equalities ;
debug.f "[ConstraintSolver][ENorm] %a@\n" Equalities.pp_equalities equalities ;
Equalities.infer_equalities_from_sums equalities ~debug ~max:10 ;
debug.f "[ConstraintSolver][EInfe] %a@\n" Equalities.pp_equalities equalities ;
equalities
let compute_costs ~debug bound_map equalities =
Equalities.init_costs bound_map equalities ;
debug.f "[ConstraintSolver][CInit] %a@\n" Equalities.pp_costs equalities ;
Equalities.improve_costs equalities ~debug ~max:10 ;
debug.f "[ConstraintSolver][CImpr] %a@\n" Equalities.pp_costs equalities
let get_node_nb_exec equalities node_id =
let set =
node_id |> ControlFlowCost.make_node |> Equalities.find equalities
|> Equalities.find_set equalities
in
Option.value_exn set |> ControlFlowCost.Set.cost
end
type callee_summary_and_formals = CostDomain.summary * (Pvar.t * Typ.t) list
type extras_WorstCaseCost =
{ inferbo_invariant_map: BufferOverrunAnalysis.invariant_map
; integer_type_widths: Typ.IntegerWidths.t
; get_node_nb_exec: Node.id -> BasicCost.t
; get_callee_summary_and_formals: Typ.Procname.t -> callee_summary_and_formals option }
let instantiate_cost integer_type_widths ~inferbo_caller_mem ~callee_pname ~callee_formals ~params
~callee_cost ~loc =
let eval_sym =
BufferOverrunSemantics.mk_eval_sym_cost integer_type_widths callee_formals params
inferbo_caller_mem
in
BasicCost.subst callee_pname loc callee_cost eval_sym
module InstrBasicCost = struct
(*
Compute the cost for an instruction.
For example for basic operation we set it to 1 and for function call we take it from the spec of the function.
*)
let allocation_functions =
[ BuiltinDecl.__new
; BuiltinDecl.__new_array
; BuiltinDecl.__objc_alloc_no_fail
; BuiltinDecl.malloc
; BuiltinDecl.malloc_no_fail ]
let is_allocation_function callee_pname =
List.exists allocation_functions ~f:(fun f -> Typ.Procname.equal callee_pname f)
let get_instr_cost_record tenv extras instr_node instr =
match instr with
| Sil.Call (ret, Exp.Const (Const.Cfun callee_pname), params, _, _) ->
let {inferbo_invariant_map; integer_type_widths; get_callee_summary_and_formals} = extras in
let operation_cost =
match
BufferOverrunAnalysis.extract_pre (InstrCFG.Node.id instr_node) inferbo_invariant_map
with
| None ->
CostDomain.unit_cost_atomic_operation
| Some inferbo_mem -> (
let loc = InstrCFG.Node.loc instr_node in
let fun_arg_list =
List.map params ~f:(fun (exp, typ) ->
ProcnameDispatcher.Call.FuncArg.{exp; typ; arg_payload= ()} )
in
match CostModels.Call.dispatch tenv callee_pname fun_arg_list with
| Some model ->
let node_hash = InstrCFG.Node.hash instr_node in
let model_env =
BufferOverrunUtils.ModelEnv.mk_model_env callee_pname ~node_hash loc tenv
integer_type_widths
in
CostDomain.of_operation_cost (model model_env ~ret inferbo_mem)
| None -> (
match get_callee_summary_and_formals callee_pname with
| Some ({CostDomain.post= callee_cost_record}, callee_formals) ->
CostDomain.map callee_cost_record ~f:(fun callee_cost ->
instantiate_cost integer_type_widths ~inferbo_caller_mem:inferbo_mem
~callee_pname ~callee_formals ~params ~callee_cost ~loc )
| None ->
CostDomain.unit_cost_atomic_operation ) )
in
if is_allocation_function callee_pname then
CostDomain.plus CostDomain.unit_cost_allocation operation_cost
else operation_cost
| Sil.Load {id= lhs_id} when Ident.is_none lhs_id ->
(* dummy deref inserted by frontend--don't count as a step. In
JDK 11, dummy deref disappears and causes cost differences
otherwise. *)
CostDomain.zero_record
| Sil.Load _ | Sil.Store _ | Sil.Call _ | Sil.Prune _ ->
CostDomain.unit_cost_atomic_operation
| Sil.Metadata Skip -> (
match InstrCFG.Node.kind instr_node with
| Procdesc.Node.Start_node ->
CostDomain.unit_cost_atomic_operation
| _ ->
CostDomain.zero_record )
| Sil.Metadata (Abstract _ | ExitScope _ | Nullify _ | VariableLifetimeBegins _) ->
CostDomain.zero_record
let get_instr_node_cost_record tenv extras instr_node =
let instrs = InstrCFG.instrs instr_node in
let instr =
match IContainer.singleton_or_more instrs ~fold:Instrs.fold with
| Empty ->
Sil.skip_instr
| Singleton instr ->
instr
| More ->
assert false
in
let cost = get_instr_cost_record tenv extras instr_node instr in
if BasicCost.is_top (CostDomain.get_operation_cost cost) then
Logging.d_printfln_escaped "Statement cost became top at %a (%a)." InstrCFG.Node.pp_id
(InstrCFG.Node.id instr_node)
(Sil.pp_instr ~print_types:false Pp.text)
instr ;
cost
end
let compute_errlog_extras cost =
{ Jsonbug_t.cost_polynomial= Some (Format.asprintf "%a" BasicCost.pp_hum cost)
; cost_degree= BasicCost.degree cost |> Option.map ~f:Polynomials.Degree.encode_to_int }
module ThresholdReports = struct
type threshold_or_report =
| Threshold of BasicCost.t
| ReportOn of {location: Location.t; cost: BasicCost.t}
type t = threshold_or_report CostIssues.CostKindMap.t
let none : t = CostIssues.CostKindMap.empty
let config =
CostIssues.CostKindMap.fold
(fun kind kind_spec acc ->
match kind_spec with
| CostIssues.{threshold= Some threshold} ->
CostIssues.CostKindMap.add kind (Threshold (BasicCost.of_int_exn threshold)) acc
| _ ->
acc )
CostIssues.enabled_cost_map none
end
(** Calculate the final Worst Case Cost predicted for each cost field
and each WTO component. It is the dot product of the symbolic cost
of the node and how many times it is executed. *)
module WorstCaseCost = struct
type astate = {costs: CostDomain.t; reports: ThresholdReports.t}
(** We don't report when the cost is Top as it corresponds to
subsequent 'don't know's. Instead, we report Top cost only at
the top level per function. *)
let should_report_cost cost ~threshold =
(not (BasicCost.is_top cost)) && not (BasicCost.leq ~lhs:cost ~rhs:threshold)
let exec_node tenv {costs; reports} extras instr_node =
let {get_node_nb_exec} = extras in
let node_cost =
let instr_cost_record = InstrBasicCost.get_instr_node_cost_record tenv extras instr_node in
let node_id = InstrCFG.Node.underlying_node instr_node |> Node.id in
let nb_exec = get_node_nb_exec node_id in
if BasicCost.is_top nb_exec then
Logging.d_printfln_escaped "Node %a is analyzed to visit infinite (top) times." Node.pp_id
node_id ;
CostDomain.mult_by_scalar instr_cost_record nb_exec
in
let costs = CostDomain.plus costs node_cost in
let reports =
CostIssues.CostKindMap.merge
(fun _kind threshold_or_report_opt cost_opt ->
match (threshold_or_report_opt, cost_opt) with
| None, _ ->
None
| Some (ThresholdReports.Threshold threshold), Some cost
when should_report_cost cost ~threshold ->
Some (ThresholdReports.ReportOn {location= InstrCFG.Node.loc instr_node; cost})
| _ ->
threshold_or_report_opt )
reports costs
in
{costs; reports}
let rec exec_partition tenv astate extras
(partition : InstrCFG.Node.t WeakTopologicalOrder.Partition.t) =
match partition with
| Empty ->
astate
| Node {node; next} ->
let astate = exec_node tenv astate extras node in
exec_partition tenv astate extras next
| Component {head; rest; next} ->
let {costs; reports} = astate in
let {costs} = exec_partition tenv {costs; reports= ThresholdReports.none} extras rest in
(* Execute head after the loop body to always report at loop head *)
let astate = exec_node tenv {costs; reports} extras head in
exec_partition tenv astate extras next
let compute tenv extras instr_cfg_wto =
let initial = {costs= CostDomain.zero_record; reports= ThresholdReports.config} in
exec_partition tenv initial extras instr_cfg_wto
end
module Check = struct
let report_threshold proc_desc summary ~name ~location ~cost CostIssues.{expensive_issue}
~threshold ~is_on_ui_thread =
let pname = Procdesc.get_proc_name proc_desc in
let report_issue_type =
L.(debug Analysis Medium)
"@\n\n++++++ Checking error type for %a **** @\n" Typ.Procname.pp pname ;
let is_on_cold_start =
ExternalPerfData.in_profiler_data_map (Procdesc.get_proc_name proc_desc)
in
expensive_issue ~is_on_cold_start ~is_on_ui_thread
in
let bigO_str =
Format.asprintf ", %a"
(BasicCost.pp_degree ~only_bigO:true)
(BasicCost.get_degree_with_term cost)
in
let degree_str = BasicCost.degree_str cost in
let message =
F.asprintf
"%s from the beginning of the function up to this program point is likely above the \
acceptable threshold of %d (estimated cost %a%s)"
name threshold BasicCost.pp_hum cost degree_str
in
let cost_trace_elem =
let cost_desc =
F.asprintf "with estimated cost %a%s%s" BasicCost.pp_hum cost bigO_str degree_str
in
Errlog.make_trace_element 0 location cost_desc []
in
Reporting.log_error summary ~loc:location
~ltr:(cost_trace_elem :: BasicCost.polynomial_traces cost)
~extras:(compute_errlog_extras cost) report_issue_type message
let report_top_and_bottom proc_desc summary ~name ~cost CostIssues.{zero_issue; infinite_issue} =
let report issue suffix =
let message =
F.asprintf "%s of the function %a %s" name Typ.Procname.pp
(Procdesc.get_proc_name proc_desc)
suffix
in
let loc = Procdesc.get_start_node proc_desc |> Procdesc.Node.get_loc in
Reporting.log_error ~loc
~ltr:(BasicCost.polynomial_traces cost)
~extras:(compute_errlog_extras cost) summary issue message
in
if BasicCost.is_top cost then report infinite_issue "cannot be computed"
else if BasicCost.is_zero cost then report zero_issue "is zero"
let check_and_report ~is_on_ui_thread WorstCaseCost.{costs; reports} proc_desc summary =
let pname = Procdesc.get_proc_name proc_desc in
if not (Typ.Procname.is_java_access_method pname) then (
CostIssues.CostKindMap.iter2 CostIssues.enabled_cost_map reports
~f:(fun _kind (CostIssues.{name; threshold} as kind_spec) -> function
| ThresholdReports.Threshold _ ->
()
| ThresholdReports.ReportOn {location; cost} ->
report_threshold proc_desc summary ~name ~location ~cost kind_spec
~threshold:(Option.value_exn threshold) ~is_on_ui_thread ) ;
CostIssues.CostKindMap.iter2 CostIssues.enabled_cost_map costs
~f:(fun _kind (CostIssues.{name; top_and_bottom} as issue_spec) cost ->
if top_and_bottom then report_top_and_bottom proc_desc summary ~name ~cost issue_spec ) )
end
type bound_map = BasicCost.t Node.IdMap.t
type get_node_nb_exec = Node.id -> BasicCost.t
let compute_bound_map node_cfg inferbo_invariant_map control_dep_invariant_map loop_invmap :
bound_map =
BoundMap.compute_upperbound_map node_cfg inferbo_invariant_map control_dep_invariant_map
loop_invmap
let compute_get_node_nb_exec node_cfg bound_map : get_node_nb_exec =
let debug =
if Config.write_html then
let f fmt = L.d_printfln fmt in
{ConstraintSolver.f}
else
let f fmt = L.(debug Analysis Verbose) fmt in
{ConstraintSolver.f}
in
let start_node = NodeCFG.start_node node_cfg in
NodePrinter.with_session start_node
~pp_name:(fun fmt -> F.pp_print_string fmt "cost(constraints)")
~f:(fun () ->
let equalities = ConstraintSolver.collect_constraints ~debug node_cfg in
let () = ConstraintSolver.compute_costs ~debug bound_map equalities in
ConstraintSolver.get_node_nb_exec equalities )
let compute_worst_case_cost tenv integer_type_widths get_callee_summary_and_formals instr_cfg_wto
inferbo_invariant_map get_node_nb_exec =
let extras =
{inferbo_invariant_map; integer_type_widths; get_node_nb_exec; get_callee_summary_and_formals}
in
WorstCaseCost.compute tenv extras instr_cfg_wto
let get_cost_summary ~is_on_ui_thread astate =
CostDomain.{post= astate.WorstCaseCost.costs; is_on_ui_thread}
let report_errors ~is_on_ui_thread proc_desc astate summary =
Check.check_and_report ~is_on_ui_thread astate proc_desc summary
let checker {Callbacks.exe_env; summary} : Summary.t =
let proc_name = Summary.get_proc_name summary in
let tenv = Exe_env.get_tenv exe_env proc_name in
let integer_type_widths = Exe_env.get_integer_type_widths exe_env proc_name in
let proc_desc = Summary.get_proc_desc summary in
let inferbo_invariant_map =
BufferOverrunAnalysis.cached_compute_invariant_map summary tenv integer_type_widths
in
let node_cfg = NodeCFG.from_pdesc proc_desc in
(* computes reaching defs: node -> (var -> node set) *)
let reaching_defs_invariant_map = ReachingDefs.compute_invariant_map summary tenv in
(* collect all prune nodes that occur in loop guards, needed for ControlDepAnalyzer *)
let control_maps, loop_head_to_loop_nodes = Loop_control.get_loop_control_maps node_cfg in
(* computes the control dependencies: node -> var set *)
let control_dep_invariant_map = Control.compute_invariant_map summary tenv control_maps in
(* compute loop invariant map for control var analysis *)
let loop_inv_map =
let get_callee_purity callee_pname =
match Ondemand.analyze_proc_name ~caller_summary:summary callee_pname with
| Some {Summary.payloads= {Payloads.purity}} ->
purity
| _ ->
None
in
LoopInvariant.get_loop_inv_var_map tenv get_callee_purity reaching_defs_invariant_map
loop_head_to_loop_nodes
in
(* given the semantics computes the upper bound on the number of times a node could be executed *)
let bound_map =
compute_bound_map node_cfg inferbo_invariant_map control_dep_invariant_map loop_inv_map
in
let is_on_ui_thread = ConcurrencyModels.runs_on_ui_thread ~attrs_of_pname tenv proc_name in
let get_node_nb_exec = compute_get_node_nb_exec node_cfg bound_map in
let astate =
let get_callee_summary_and_formals callee_pname =
Ondemand.analyze_proc_name ~caller_summary:summary callee_pname
|> Option.bind ~f:(fun summary ->
Payload.of_summary summary
|> Option.map ~f:(fun payload ->
(payload, Summary.get_proc_desc summary |> Procdesc.get_pvar_formals) ) )
in
let instr_cfg = InstrCFG.from_pdesc proc_desc in
let instr_cfg_wto = InstrCFG.wto instr_cfg in
compute_worst_case_cost tenv integer_type_widths get_callee_summary_and_formals instr_cfg_wto
inferbo_invariant_map get_node_nb_exec
in
let () =
let exit_cost_record = astate.WorstCaseCost.costs in
L.(debug Analysis Verbose)
"@\n[COST ANALYSIS] PROCEDURE '%a' |CFG| = %i FINAL COST = %a @\n" Typ.Procname.pp proc_name
(Container.length ~fold:NodeCFG.fold_nodes node_cfg)
CostDomain.VariantCostMap.pp exit_cost_record
in
report_errors ~is_on_ui_thread proc_desc astate summary ;
Payload.update_summary (get_cost_summary ~is_on_ui_thread astate) summary