[cost] Autoreleasepool size analysis

Summary:
This diff adds a new experimental checker for detecting size of objects in autorelease pool in ObjC. The basic mechanism is almost the same with the previous cost calculation:

* Autorelease pool size is increased at explicit `autorelease` call
* Autorelease pool size is set as zero by the `autoreleasepool` block.

While it only supports the explicit calls as of now, we will extend the checker to handle more cases in the following diffs.

Reviewed By: ezgicicek

Differential Revision: D23473145

fbshipit-source-id: 416488176
master
Sungkeun Cho 4 years ago committed by Facebook GitHub Bot
parent 4dbfb72260
commit cb4cf115e1

@ -279,6 +279,11 @@ OPTIONS
Activates: Enable cost and disable all other checkers (Conversely:
--no-cost-only) See also infer-analyze(1).
--cost-tests-only-autoreleasepool
Activates: [EXPERIMENTAL] Report only autoreleasepool size results
in cost tests (Conversely: --no-cost-tests-only-autoreleasepool)
See also infer-report(1) and infer-reportdiff(1).
--costs-current path
Costs report of the latest revision See also infer-reportdiff(1).

@ -39,6 +39,10 @@ OPTIONS
Write a list of cost issues in a format suitable for cost tests to
file
--cost-tests-only-autoreleasepool
Activates: [EXPERIMENTAL] Report only autoreleasepool size results
in cost tests (Conversely: --no-cost-tests-only-autoreleasepool)
--debug,-g
Activates: Debug mode (also sets --debug-level 2,
--developer-mode, --print-buckets, --print-types,

@ -20,6 +20,10 @@ DESCRIPTION
OPTIONS
--cost-tests-only-autoreleasepool
Activates: [EXPERIMENTAL] Report only autoreleasepool size results
in cost tests (Conversely: --no-cost-tests-only-autoreleasepool)
--costs-current path
Costs report of the latest revision

@ -279,6 +279,11 @@ OPTIONS
Activates: Enable cost and disable all other checkers (Conversely:
--no-cost-only) See also infer-analyze(1).
--cost-tests-only-autoreleasepool
Activates: [EXPERIMENTAL] Report only autoreleasepool size results
in cost tests (Conversely: --no-cost-tests-only-autoreleasepool)
See also infer-report(1) and infer-reportdiff(1).
--costs-current path
Costs report of the latest revision See also infer-reportdiff(1).

@ -112,6 +112,10 @@ module type S = sig
val nsArray_arrayWithObjectsCount : t
val objc_autorelease_pool_pop : t
val objc_autorelease_pool_push : t
val objc_cpp_throw : t
val pthread_create : t

@ -155,6 +155,10 @@ let nsArray_arrayWithObjectsCount =
create_objc_class_method "NSArray" "arrayWithObjects:count:" [None; None]
let objc_autorelease_pool_pop = create_procname "_objc_autoreleasePoolPop"
let objc_autorelease_pool_push = create_procname "_objc_autoreleasePoolPush"
let objc_cpp_throw = create_procname "__infer_objc_cpp_throw"
let pthread_create = create_procname "pthread_create"

@ -131,7 +131,9 @@ module Node = struct
; loc: Location.t (** location in the source code *)
; mutable preds: t list (** predecessor nodes in the cfg *)
; pname: Procname.t (** name of the procedure the node belongs to *)
; mutable succs: t list (** successor nodes in the cfg *) }
; mutable succs: t list (** successor nodes in the cfg *)
; mutable code_block_exit: t option
(** exit node corresponding to start node in a code block *) }
let exn_handler_kind = Stmt_node ExceptionHandler
@ -149,7 +151,8 @@ module Node = struct
; pname
; succs= []
; preds= []
; exn= [] }
; exn= []
; code_block_exit= None }
let compare node1 node2 = Int.compare node1.id node2.id
@ -401,6 +404,10 @@ module Node = struct
F.asprintf "%s@\n%a" str_kind (Instrs.pp pe) (get_instrs node)
let set_code_block_exit node ~code_block_exit = node.code_block_exit <- Some code_block_exit
let get_code_block_exit node = node.code_block_exit
(** simple key for a node: just look at the instructions *)
let simple_key node =
let add_instr instr =
@ -631,7 +638,8 @@ let create_node_from_not_reversed pdesc loc kind instrs =
; preds= []
; pname= pdesc.attributes.proc_name
; succs= []
; exn= [] }
; exn= []
; code_block_exit= None }
in
pdesc.nodes <- node :: pdesc.nodes ;
node

@ -162,6 +162,13 @@ module Node : sig
val get_wto_index : t -> int
val set_code_block_exit : t -> code_block_exit:t -> unit
(** Set an exit node corresponding to a start node of a code block. Using this, when there is a
code block, frontend can keep the correspondence between start/exit nodes of a code block. *)
val get_code_block_exit : t -> t option
(** Get an exit node corresponding to a start node of a code block. *)
val is_dangling : t -> bool
(** Returns true if the node is dangling, i.e. no successors and predecessors *)

@ -944,6 +944,12 @@ and costs_previous =
"Costs report of the base revision to use for comparison"
and cost_tests_only_autoreleasepool =
CLOpt.mk_bool ~long:"cost-tests-only-autoreleasepool"
~in_help:InferCommand.[(Report, manual_generic); (ReportDiff, manual_generic)]
"[EXPERIMENTAL] Report only autoreleasepool size results in cost tests"
and siof_check_iostreams =
CLOpt.mk_bool ~long:"siof-check-iostreams"
~in_help:InferCommand.[(Analyze, manual_siof)]
@ -2713,6 +2719,8 @@ and cost_scuba_logging = !cost_scuba_logging
and costs_previous = !costs_previous
and cost_tests_only_autoreleasepool = !cost_tests_only_autoreleasepool
and cxx = !cxx
and cxx_scope_guards = !cxx_scope_guards

@ -232,6 +232,8 @@ val cost_scuba_logging : bool
val costs_previous : string option
val cost_tests_only_autoreleasepool : bool
val cxx : bool
val cxx_scope_guards : Yojson.Basic.t

@ -7,6 +7,11 @@
open! IStd
type t = IsOnUIThread | Procedure | File | Cost | Trace [@@deriving equal]
type t = IsOnUIThread | Procedure | File | Cost | AutoreleasepoolSize | Trace [@@deriving equal]
let all_fields = [File; Procedure; Cost; IsOnUIThread; Trace]
let all_fields =
[ File
; Procedure
; (if Config.cost_tests_only_autoreleasepool then AutoreleasepoolSize else Cost)
; IsOnUIThread
; Trace ]

@ -7,6 +7,6 @@
open! IStd
type t = IsOnUIThread | Procedure | File | Cost | Trace [@@deriving equal]
type t = IsOnUIThread | Procedure | File | Cost | AutoreleasepoolSize | Trace [@@deriving equal]
val all_fields : t list

@ -960,6 +960,12 @@ let nsArray_arrayWithObjectsCount =
Builtin.register BuiltinDecl.nsArray_arrayWithObjectsCount execute_skip
let objc_autorelease_pool_pop = Builtin.register BuiltinDecl.objc_autorelease_pool_pop execute_skip
let objc_autorelease_pool_push =
Builtin.register BuiltinDecl.objc_autorelease_pool_push execute_skip
(* model throwing exception in objc/c++ as divergence *)
let objc_cpp_throw = Builtin.register BuiltinDecl.objc_cpp_throw execute_exit

@ -87,10 +87,6 @@ let nsobject_cl = "NSObject"
let nsstring_cl = "NSString"
let objc_autorelease_pool_pop = "_objc_autoreleasePoolPop"
let objc_autorelease_pool_push = "_objc_autoreleasePoolPush"
let objc_class = "objc_class"
let objc_object = "objc_object"

@ -81,10 +81,6 @@ val nsobject_cl : string
val nsstring_cl : string
val objc_autorelease_pool_pop : string
val objc_autorelease_pool_push : string
val objc_class : string
val objc_object : string

@ -3119,14 +3119,14 @@ module CTrans_funct (F : CModule_type.CFrontend) : CModule_type.CTranslation = s
CLocation.location_of_stmt_info trans_state.context.translation_unit_context.source_file
stmt_info
in
let mk_call_node fname =
let mk_call_node pname =
let ret = mk_fresh_void_id_typ () in
let pname = Procname.from_string_c_fun fname in
let instr = Sil.Call (ret, Const (Cfun pname), [], location, CallFlags.default) in
Procdesc.create_node procdesc location (Stmt_node (Call fname)) [instr]
Procdesc.create_node procdesc location (Stmt_node (Call (Procname.to_string pname))) [instr]
in
let push_node = mk_call_node CFrontend_config.objc_autorelease_pool_push in
let pop_node = mk_call_node CFrontend_config.objc_autorelease_pool_pop in
let push_node = mk_call_node BuiltinDecl.objc_autorelease_pool_push in
let pop_node = mk_call_node BuiltinDecl.objc_autorelease_pool_pop in
Procdesc.Node.set_code_block_exit push_node ~code_block_exit:pop_node ;
let res_trans_body = compoundStmt_trans {trans_state with succ_nodes= [pop_node]} stmts in
Procdesc.set_succs push_node ~normal:(Some res_trans_body.control.root_nodes) ~exn:None ;
Procdesc.set_succs pop_node ~normal:(Some trans_state.succ_nodes) ~exn:None ;

@ -51,6 +51,10 @@ module InstrBasicCostWithReason = struct
List.exists allocation_functions ~f:(fun f -> Procname.equal callee_pname f)
let is_autorelease_function callee_pname =
String.equal (Procname.get_method callee_pname) "autorelease"
let get_instr_cost_record tenv extras instr_node instr =
match instr with
| Sil.Call (ret, Exp.Const (Const.Cfun callee_pname), params, _, _) when Config.inclusive_cost
@ -105,6 +109,8 @@ module InstrBasicCostWithReason = struct
in
if is_allocation_function callee_pname then
CostDomain.plus CostDomain.unit_cost_allocation operation_cost
else if is_autorelease_function callee_pname then
CostDomain.plus CostDomain.unit_cost_autoreleasepool_size operation_cost
else operation_cost
| Sil.Call (_, Exp.Const (Const.Cfun _), _, _, _) ->
CostDomain.zero_record
@ -176,8 +182,15 @@ module WorstCaseCost = struct
let compute tenv extras cfg =
let init = CostDomain.zero_record in
let cost =
InstrCFG.fold_nodes cfg ~init ~f:(fun acc pair ->
exec_node tenv extras pair |> CostDomain.plus acc )
let nodes_in_autoreleasepool = CostUtils.get_nodes_in_autoreleasepool cfg in
InstrCFG.fold_nodes cfg ~init ~f:(fun acc ((node, _) as pair) ->
let cost = exec_node tenv extras pair in
let cost =
if Procdesc.NodeSet.mem node nodes_in_autoreleasepool then
CostDomain.set_autoreleasepool_size_zero cost
else cost
in
CostDomain.plus acc cost )
in
Option.iter (CostDomain.get_operation_cost cost).top_pname_opt ~f:(fun top_pname ->
ScubaLogging.cost_log_message ~label:"unmodeled_function_top_cost"

@ -87,6 +87,10 @@ let add_top_pname_opt kind cost_record top_pname_opt =
let get_operation_cost cost_record = get_cost_kind CostKind.OperationCost cost_record
let set_autoreleasepool_size_zero cost_record =
VariantCostMap.remove CostKind.AutoreleasepoolSize cost_record
let map ~f cost_record = VariantCostMap.map f cost_record
let zero_record = VariantCostMap.empty
@ -104,6 +108,10 @@ let unit_cost_atomic_operation = VariantCostMap.increment CostKind.OperationCost
let unit_cost_allocation = VariantCostMap.increment CostKind.AllocationCost zero_record
let unit_cost_autoreleasepool_size =
VariantCostMap.increment CostKind.AutoreleasepoolSize zero_record
let of_operation_cost operation_cost =
VariantCostMap.increase_by CostKind.OperationCost
{cost= operation_cost; top_pname_opt= None}

@ -62,6 +62,8 @@ val add_top_pname_opt : CostKind.t -> t -> Procname.t option -> t
val get_operation_cost : t -> BasicCostWithReason.t
val set_autoreleasepool_size_zero : t -> t
val map : f:(BasicCostWithReason.t -> BasicCostWithReason.t) -> t -> t
val zero_record : t
@ -79,6 +81,9 @@ val unit_cost_atomic_operation : t
val unit_cost_allocation : t
(** Map representing cost record \{OperationCost:0; AllocationCost:1; AutoreleasepoolSize:0\} *)
val unit_cost_autoreleasepool_size : t
(** Map representing cost record \{OperationCost:0; AllocationCost:0; AutoreleasepoolSize:1\} *)
val of_operation_cost : BasicCost.t -> t
(** Map representing cost record \{OperationCost:operation_cost; AllocationCost:0;
AutoreleasepoolSize:0\} *)

@ -6,6 +6,7 @@
*)
open! IStd
module L = Logging
module BasicCost = CostDomain.BasicCost
open BufferOverrunUtils.ModelEnv
@ -63,3 +64,35 @@ end
module CString : S = struct
let length exp inferbo_mem = BufferOverrunSemantics.eval_string_len exp inferbo_mem
end
let get_nodes_in_block ~block_start ~block_exit =
let rec accum_nodes_in_block ~from acc =
match from with
| [] ->
acc
| x :: tl ->
if Procdesc.Node.equal x block_exit || Procdesc.NodeSet.mem x acc then
accum_nodes_in_block ~from:tl acc
else
let from = Procdesc.Node.get_succs x @ tl in
accum_nodes_in_block ~from (Procdesc.NodeSet.add x acc)
in
accum_nodes_in_block ~from:[block_start] Procdesc.NodeSet.empty
let get_nodes_in_autoreleasepool cfg =
Procdesc.fold_instrs cfg ~init:Procdesc.NodeSet.empty ~f:(fun acc node -> function
| Sil.Call (_, Const (Cfun pname), _, _, _)
when Procname.equal pname BuiltinDecl.objc_autorelease_pool_push -> (
match Procdesc.Node.get_code_block_exit node with
| None ->
(* Each _objc_autoreleasePoolPush has a corresponding _objc_autoreleasePoolPop, so we
should always have a corresponding exit node of the autoreleasepool block. *)
L.internal_error
"Not found: block-exit node matching to block_start node(%a) is not found.@."
Procdesc.Node.pp node ;
assert false
| Some block_exit ->
get_nodes_in_block ~block_start:node ~block_exit |> Procdesc.NodeSet.union acc )
| _ ->
acc )

@ -20,10 +20,17 @@ let pp_custom_of_cost_report fmt report cost_fields =
F.fprintf fmt "%s%s" (comma_separator index) cost_item.loc.file
| Cost ->
F.fprintf fmt "%s%s" (comma_separator index) cost_item.exec_cost.hum.hum_polynomial
| AutoreleasepoolSize ->
F.fprintf fmt "%s%s" (comma_separator index)
cost_item.autoreleasepool_size.hum.hum_polynomial
| IsOnUIThread ->
F.fprintf fmt "%s OnUIThread:%b" (comma_separator index) cost_item.is_on_ui_thread
| Trace ->
IssuesTest.pp_trace fmt cost_item.exec_cost.trace (comma_separator index)
let trace =
if Config.cost_tests_only_autoreleasepool then cost_item.autoreleasepool_size.trace
else cost_item.exec_cost.trace
in
IssuesTest.pp_trace fmt trace (comma_separator index)
in
List.iteri ~f:pp_cost_field cost_fields ;
F.fprintf fmt "@."
@ -36,12 +43,14 @@ let cost_tests_jsonbug_compare (cost1 : Jsonbug_t.cost_item) (cost2 : Jsonbug_t.
[%compare: string * string * string * Caml.Digest.t * bool]
( cost1.loc.file
, cost1.procedure_id
, cost1.exec_cost.hum.hum_polynomial
, ( if Config.cost_tests_only_autoreleasepool then cost1.autoreleasepool_size.hum.hum_polynomial
else cost1.exec_cost.hum.hum_polynomial )
, cost1.hash
, cost1.is_on_ui_thread )
( cost2.loc.file
, cost2.procedure_id
, cost2.exec_cost.hum.hum_polynomial
, ( if Config.cost_tests_only_autoreleasepool then cost2.autoreleasepool_size.hum.hum_polynomial
else cost2.exec_cost.hum.hum_polynomial )
, cost2.hash
, cost2.is_on_ui_thread )

@ -0,0 +1,19 @@
# 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.
TESTS_DIR = ../../..
CLANG_OPTIONS = -c $(OBJC_CLANG_OPTIONS)
INFER_OPTIONS = --cost-only --debug-exceptions --project-root $(TESTS_DIR)
INFERPRINT_OPTIONS = --issues-tests
INFERPRINT_COST_OPTIONS = --cost-tests-only-autoreleasepool --cost-issues-tests
SOURCES = $(wildcard *.m)
include $(TESTS_DIR)/clang.make
include $(TESTS_DIR)/objc.make
include $(TESTS_DIR)/cost.make
infer-out/report.json: $(MAKEFILE_LIST)

@ -0,0 +1,96 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
@interface Basic : NSObject
@end
@implementation Basic
- (void)call_autorelease_constant {
NSString* x = [[NSString alloc] initWith:"test"];
return [x autorelease];
}
- (void)autoreleased_in_loop_linear:(int)n {
for (int i = 0; i < n; i++) {
[self call_autorelease_constant];
}
}
- (void)autoreleased_in_autoreleasepool_zero:(int)n {
for (int i = 0; i < n; i++) {
@autoreleasepool {
[self call_autorelease_constant];
}
}
}
/* This function is problematic because autoreleased objects inside the loop
will not be released until the loop terminates. However, the checker is not
measuring peak memory but size of autoreleasepool increased by the function.
Since we wrap the loop inside the autoreleasepool block, we will be setting
the size to 0 for the whole function. */
- (void)loop_in_autoreleasepool_zero:(int)n {
@autoreleasepool {
for (int i = 0; i < n; i++) {
[self call_autorelease_constant];
}
}
}
- (void)autoreleased_in_loop_nested_zero:(int)n {
@autoreleasepool {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
[self call_autorelease_constant];
}
@autoreleasepool {
[self call_autorelease_constant];
}
}
}
}
- (void)autoreleased_in_loop_sequential_constant:(int)n {
@autoreleasepool {
for (int i = 0; i < n; i++) {
[self call_autorelease_constant];
}
}
[self call_autorelease_constant];
@autoreleasepool {
for (int i = 0; i < n; i++) {
[self call_autorelease_constant];
}
}
}
- (void)autoreleased_in_loop_sequential_linear:(int)n {
@autoreleasepool {
[self call_autorelease_constant];
}
for (int i = 0; i < n; i++) {
[self call_autorelease_constant];
}
@autoreleasepool {
[self call_autorelease_constant];
}
}
- (void)call_no_autorelease_zero {
NSString* x = [[NSString alloc] initWith:"test"];
return x;
}
- (void)no_autoreleased_in_loop_zero:(int)n {
for (int i = 0; i < n; i++) {
[self call_no_autorelease];
}
}
@end

@ -0,0 +1,10 @@
codetoanalyze/objc/autoreleasepool/basic.m, Basic.autoreleased_in_autoreleasepool_zero:, 0, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.autoreleased_in_loop_linear:, n, OnUIThread:false, [{n},Loop]
codetoanalyze/objc/autoreleasepool/basic.m, Basic.autoreleased_in_loop_nested_zero:, 0, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.autoreleased_in_loop_sequential_constant:, 1, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.autoreleased_in_loop_sequential_linear:, n, OnUIThread:false, [{n},Loop]
codetoanalyze/objc/autoreleasepool/basic.m, Basic.call_autorelease_constant, 1, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.call_no_autorelease_zero, 0, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.dealloc, 0, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.loop_in_autoreleasepool_zero:, 0, OnUIThread:false, []
codetoanalyze/objc/autoreleasepool/basic.m, Basic.no_autoreleased_in_loop_zero:, 0, OnUIThread:false, []
Loading…
Cancel
Save