wiki:DynamicCallAnalysis

Version 3 (modified by daniel_h, 12 years ago) (diff)

...

Running dynamic call analysis

Dynamic function calls are either dynamic MFA calls (those MFA calls whose callee is given by means of non-literal expressions) or apply calls (calls to the erlang:apply/3 built-in function).

For instance, the following calls inside f are dynamic:

f(IOModule) ->
    Foo = ["foo"],
    IOModule:format("foo"),
    apply(IOModule, format, Foo).

These constructs are likely difficult to be statically analysed, since their callee is determined only at execution-time. Fortunately, in many cases static code analysis is able to find potential callees, but unlike other semantic analyses in RefactorErl, dynamic call analysis would be very inefficient to be performed incrementally. Therefore, static analysis of dynamic function calls has to be triggered by the user on demand.

In order to analyse dynamic call constructs, load all the libraries and files you would like to work with. Then type in the RefactorErl shell:

ri:anal_dyn().

Dynamic call analysis begins and informs you about the process and its progress. Firstly, all function calls are gathered from the database. Secondly, dynamic ones are filtered and passed to the next step, where a parallel algorithm tries to identify the callee of each call. Finally, the results are merged and stored into the database.

For instance, executed on Erlang stdlib 1.17.4:

24012 function calls found in the database.
Looking for dynamic calls...
541 function calls seem to be dynamic.
Looking up dynamic calls... (533/541)
Identification of 8 dynamic calls timed out.

533 dynamic calls identified in the database.
Analysing dynamic calls... (533/533)

Analysis completed.

As you can see in this example, there may be function calls that are not identifiable in a reasonable time (currently the time limit is set to 5 seconds, and there were 8 calls whose analysis has been ignored). This is due to the complexity of some data-flow path analysis. Users may increase the time bound on their own risk in order to make the analysis process able to identify all the callees.

Please note that most database modifications (e.g. loading or refactoring a module) may invalidate the results of this dynamic call analysis. Currently this invalidation step is not automatically done, so the user has to invoke it. To clean the dynamic calls (and every corresponding entry) from the database, simply call:

ri:clean_dyn().

User-defined function references

The dynamic function analyser is equipped to handle user-defined function references. This functionality is basically a generalisation of the analysis of erlang:apply/3 calls, where the actual arguments of a function call determine another function which is, we say, symbolically referred to inside the original call. In this kind of extended reference analysis, the user can specify that: which functions, and in which of their arguments do refer to other functions.

In Erlang it is sometimes preferable to define functions that act as a wrapper to a simple apply. Typical use cases include load distribution among different Erlang nodes as well as practical implementations of callback functions. With user-defined function references, the wrapper function will actually refer to the function(s) that may be called inside its body.

How to specify user-defined references

The specification has to be put in a configuration file in the root directory of your instance of the tool (the file has to be named dynfunref.conf). It should contain tuples of the following form.

{
  {foo, bar},
  ['$1', '$2', '$3'],
  [
    {'$1', '$2', '$3'},
    {'$2', {'$1', foo, 0}}
  ]
}.

The first two elements determine the function whose arguments are to be inspected: the module name and the function name are given in the first element of the tuple, while the function arity is determined by the length of the second element. The list in the second element is a pattern (a simple version of Erlang match specifications) which allows its elements to be any Erlang term constructed of lists, tuples and atoms (atoms will be regarded as variable names, and they match with any term).

When analysing an appropriate function call, its actual parameters are matched with the pattern, and then the atoms become bound variable names within the third element of the specification tuple. The third element defines mappings: which arguments refer to which functions - if the mapping is a pair, the first element defines the source of the reference and the second determines the callee, if it is a 3-tuple, then a function reference will point from the original call expression to the specified function.

The first and second elements of callee specifications may be arbitrary atoms as they determine the module and function name of the callee: if the atom has been bound in the argument pattern, then the value of the actual argument will be used. The third element of the callee specification may be an integer or an atom and it determines the arity of the callee: if it is an atom, then the corresponding element in the argument match will be treated as a list and will be counted, otherwise the integer constant will be the arity of the callee.

Example

Consider the following 2 modules.

-module(router).
-export([route/2]).

route(ReqID, Args)->
    [Function | Args0] = tuple_to_list(Args),
    Master =self(),    
    Worker = fun()->
        Result = try
            apply(router_backend, Function, [ReqID]++Args0)
        catch
            _ -> error
        end,
        Master ! {ReqID, Result}
        end,
    spawn(Worker),
    receive
         {ReqID, Result} -> Result
    end.
    
ex_call()->
    route(1, {a}).

ex_call2()->
    route(2,{b,1}).
-module(router_backend).
-compile(export_all).

a(_)->
    2.

b(_,A)->
    A+1.

The configuration file:

%will link router_backend:a/1 to router:ex_call/0
{{router,route},
['_', {'$1'}],
[
    {router_backend, '$1', 1}
]}.

%will link router_backend:b/2 to router:ex_call2/0
{{router,route},
['_', {'$1','_'}],
[
    {router_backend, '$1', 2}
]}.

The result:

(refactorerl@localhost)26> ri:reset().                           
{ok,[]}
ok
(refactorerl@localhost)27> ri:add("/Users/V/erlang/mini_router").
| 1.12 kB/s >>>>>>>>>>>>>>>>>>>| [   4/   4] router.erl
| 1.18 kB/s >>>>>>>>>>>>>>>>>>>| [   4/   4] router_backend.erl
ok
(refactorerl@localhost)28> ri:anal_dyn().
Function reference patterns loaded (2, dynfunref.conf)

Analysing local funs... (1 done).                                              
Collecting function calls...
6 function calls found in the database.
Looking for dynamic references...
5 dynamic function references found.
Inspecting dynamic references... (5/5)
3 potential callee successfully spotted.
Storing dynamic references... (3/3)
Updating the call graph... (3/3)

Analysis completed.
ok
(refactorerl@localhost)29> ri:q("mods.funs.dyncalls").
router:ex_call/0
    router_backend:a/1
router:ex_call2/0
    router_backend:b/2
ok

Dynamic references were made to link router_backend:a/1 to router:ex_call/0 , and to link router_backend:b/2 to router:ex_call2/0.