-
Notifications
You must be signed in to change notification settings - Fork 5
Function Instrumentation
When tracking application execution for the Metis project, three different instruction types are of interest. Function declarations, function calls, and return statements must all be detected and instrumented in order to accurately track the flow of execution within a JavaScript-based application. Moreover, each of the aforementioned three instructions were instrumented differently.
The instrumentation of all JavaScript files was done using the open-source JavaScript engine Rhino. Rhino is a project developed by the Mozilla Foundation which transforms JavaScript source code into an abstract syntax tree . Such a tree may be traversed for function nodes, return statement nodes, etc. Presented on this page are descriptions of each instrumentation process as well as the challenges faced.
Multiple function nodes are found when searching through the rhino-produced trees of our test applications. In order to properly instrument each existing function declaration, code was added to both the beginning and end of each declaration.
Specifically, the proxy server is notified whenever a subroutine is entered (beginning of declaration). Such a notification includes the subroutine's name, its available arguments, and its line number. A subroutine ends execution when a return statement is reached or when the end of its body is reached. The proxy is notified when the latter occurs indicating the end of the function.
89 function startClock(delay) {
90 send(JSON.stringify({messageType: "FUNCTION_ENTER", targetFunction: "startClock", lineNo: 89, args: [{"label": "delay", "value": delay}]}));
91 document.getElementById("displayTime").innerHTML = delay;
92 setTimeout(updateClock, 1000);
93 send(JSON.stringify({messageType: "FUNCTION_EXIT", targetFunction: "startClock", lineNo: 93}));
94 }
When manipulating function declarations for tracing, it was important to insert our instrumentation code at the proper locations. In order to not alter the line numbers of other points of interest (return statements, function calls), the target JavaScript code was instrumented from the bottom up (in terms of line number). That is, for a given function declaration the end of its body was instrumented first before the beginning.
[Diagram TBW]
As mentioned earlier, a subroutine may end execution when a return statement is reached. Such an occurrence is important to our function trace and is recorded using a return-statement wrapper. This simple wrapper is injected into a web application's source code whenever a return statement is detected within the abstract syntax tree created by Rhino.
Below, the injection of the return statement wrapper (RSW()
) is seen on line 12 of the code segment. In this example original return statement does not return any value to its calling function. Therefore, the wrapper is simply passed the line number of the return statement. This information is relayed to the proxy for recording and the return statement is executed as usual on line 11.
Before Instrumentation:
10 if(emailAddr.indexOf("@") == -1) {
11 loginDiv.innerHTML = "Incorrect email format! ";
12 return;
13 }
After Instrumentation:
10 if (emailAddr[FCW("indexOf", 9)]("@") == -1) {
11 loginDiv.innerHTML = "Incorrect email format! ";
12 return RSW(11);
13 }
However, in the case that the return statement did return a value, such as true
or false
, this value would be passed to RSW()
for recording before being returned to the calling function:
return RSW(true, 77);
.
.
.
function RSW() {
if (arguments.length > 1)
{
// Notify proxy of return statement with return value and lineno
} else {
// Notify proxy of return statement with only statement lineno
}
return arguments[0]; // return the first argument
}
The injection of the function call wrapper FCW()
on line 10 of the above code segment relates to the next point of interest, function calls.
When instrumenting return statements within JavaScript code it is important to consider the case where there is no return value. The decision was made to only relay the return statement's line number to the proxy server. Also, the behavior of the application must not be altered by the injected wrappers.
Lastly, the instrumentation of function calls is necessary in order to capture the relationship between subroutines. By instrumenting both DOM events and function declarations, the Metis project is able to infer which functions were called as the result of a specific DOM event (e.g. click, page load, etc.). However, the instrumentation of function calls provides greater granularity when capturing JavaScript execution. Specifically, injecting the function call wrapper FCW()
allows Metis to determine the origin of a function call. This can be particularly useful when a single function is called numerous times by another function, as seen below.
1 function foo(flag) {
2
3 if (flag == true) {
4 startClock(1000);
5 } else {
6 startClock(2000);
7 }
8 }
Without wrapping the calls to startClock()
on lines 4 and 6 it would be difficult to trace the execution path in detail. That is, the tester would know that startClock()
is called from within foo()
but he/she would not know the entire context of the call.
When wrapping function calls within JavaScript it is important to preserve the scope of each call. Additionally, the order of execution must be represented within our trace. To achieve this, a wrapper must be created for function calls as multiple call may occur on a single line of code. The below example illustrates an alternate method for instrumenting function calls which does not capture the order of execution:
Coming soon...