-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
InstrumentFunctions.java
327 lines (296 loc) · 11 KB
/
InstrumentFunctions.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
* Copyright 2008 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.List;
/**
* Instruments functions for when functions first get called and defined.
*
* This pass be used to instrument code to:
* 1. Gather statistics from real users in the wild.
* 2. Incorporate utilization statistics into Selenium tests
* 3. Access utilization statistics from an app's debug UI
*
* By parametrizing the whole instrumentation process we expect to be
* able to support a wide variety of use cases and minimize the cost
* of developing new instrumentation schemes.
*
* TODO(user): This pass currently runs just before the variable and
* property renaming near the end of the optimization pass. I think
* Mark put it there to minimize the difference between the code
* generated with/without instrumentation; instrumentation makes
* several optimization passes do less, for example inline functions.
*
* My opinion is that we want utilization/profiling information for
* all function. This pass should run before most passes that modify
* the AST (exception being the localization pass, which makes
* assumptions about the structure of the AST). We should move the
* pass up, list inlined functions or give clients the option to
* instrument before or after optimization.
*
*/
class InstrumentFunctions implements CompilerPass {
private final AbstractCompiler compiler;
private final FunctionNames functionNames;
private final String appNameStr;
private final String initCodeSource;
private final String definedFunctionName;
private final String reportFunctionName;
private final String reportFunctionExitName;
private final String appNameSetter;
private final List<String> declarationsToRemove;
/**
* Creates an instrument functions compiler pass.
*
* @param compiler The JSCompiler
* @param functionNames Assigned function identifiers.
* @param template Instrumentation template; for use during error reporting only.
* @param appNameStr String to pass to appNameSetter.
*/
InstrumentFunctions(AbstractCompiler compiler,
FunctionNames functionNames,
Instrumentation template,
String appNameStr) {
this.compiler = compiler;
this.functionNames = functionNames;
this.appNameStr = appNameStr;
StringBuilder initCodeSourceBuilder = new StringBuilder();
for (String line : template.getInitList()) {
initCodeSourceBuilder.append(line).append("\n");
}
this.initCodeSource = initCodeSourceBuilder.toString();
this.definedFunctionName = template.getReportDefined();
this.reportFunctionName = template.getReportCall();
this.reportFunctionExitName = template.getReportExit();
this.appNameSetter = template.getAppNameSetter();
this.declarationsToRemove = ImmutableList.copyOf(
template.getDeclarationToRemoveList());
}
@Override
public void process(Node externs, Node root) {
Node initCode = null;
if (!initCodeSource.isEmpty()) {
Node initCodeRoot = compiler.parseSyntheticCode(
"template:init", initCodeSource);
if (initCodeRoot != null && initCodeRoot.getFirstChild() != null) {
initCode = initCodeRoot.removeChildren();
} else {
return; // parse failure
}
}
NodeTraversal.traverseEs6(compiler, root,
new RemoveCallback(declarationsToRemove));
NodeTraversal.traverseEs6(compiler, root, new InstrumentCallback());
if (!appNameSetter.isEmpty()) {
Node call = IR.call(
IR.name(appNameSetter),
IR.string(appNameStr));
call.putBooleanProp(Node.FREE_CALL, true);
Node expr = IR.exprResult(call);
Node addingRoot = compiler.getNodeForCodeInsertion(null);
addingRoot.addChildToFront(expr.useSourceInfoIfMissingFromForTree(addingRoot));
compiler.reportChangeToEnclosingScope(addingRoot);
}
if (initCode != null) {
Node addingRoot = compiler.getNodeForCodeInsertion(null);
addingRoot.addChildrenToFront(initCode);
compiler.reportChangeToEnclosingScope(addingRoot);
}
}
/**
* The application must refer to these variables to output them so the
* application must also declare these variables for the first
* {@link VarCheck} pass. These declarations must be removed before the
* second {@link VarCheck} pass. Otherwise, the second pass would warn about
* duplicate declarations.
*/
private static class RemoveCallback extends AbstractPostOrderCallback {
private final List<String> removable;
RemoveCallback(List<String> removable) {
this.removable = removable;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (NodeUtil.isVarDeclaration(n) && removable.contains(n.getString())) {
parent.removeChild(n);
if (!parent.hasChildren()) {
parent.getParent().removeChild(parent);
}
}
}
}
/**
* Traverse a function's body by instrument return sites by
* inserting calls to {@code reportFunctionExitName}. If the
* function is missing an explicit return statement in some control
* path, this pass inserts a call to {@code reportFunctionExitName}
* as the last statement in the function's body.
*
* Example:
* Input:
* function f() {
* if (pred) {
* return a;
* }
* }
*
* Template:
* reportFunctionExitName: "onExitFn"
*
* Output:
* function f() {
* if (pred) {
* return onExitFn(0, a, "f");
* }
* onExitFn(0, undefined, "f");
* }
*
**/
private class InstrumentReturns implements NodeTraversal.Callback {
private final int functionId;
private final String functionName;
/**
* @param functionId Function identifier computed by FunctionNames;
* used as first argument to {@code reportFunctionExitName}
* {@code reportFunctionExitName} must be a 3 argument function that
* returns it's second argument.
* @param functionName Function name.
*/
InstrumentReturns(int functionId, String functionName) {
this.functionId = functionId;
this.functionName = functionName;
}
/**
* @param function function with id == this.functionId
*/
void process(Node function) {
Node body = function.getLastChild();
NodeTraversal.traverseEs6(compiler, body, this);
if (!allPathsReturn(function)) {
Node call = newReportFunctionExitNode(function, null);
Node expr = IR.exprResult(call).useSourceInfoIfMissingFromForTree(function);
body.addChildToBack(expr);
compiler.reportChangeToEnclosingScope(body);
}
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
return !n.isFunction();
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isReturn()) {
return;
}
Node returnRhs = n.removeFirstChild();
Node call = newReportFunctionExitNode(n, returnRhs);
n.addChildToFront(call);
t.reportCodeChange();
}
private Node newReportFunctionExitNode(Node infoNode, Node returnRhs) {
Node call = IR.call(
IR.name(reportFunctionExitName),
IR.number(functionId),
(returnRhs != null) ? returnRhs : IR.name("undefined"),
IR.string(functionName));
call.putBooleanProp(Node.FREE_CALL, true);
call.useSourceInfoFromForTree(infoNode);
return call;
}
/**
* @return true if all paths from block must exit with an explicit return.
*/
private boolean allPathsReturn(Node function) {
// Computes the control flow graph.
ControlFlowAnalysis cfa = new ControlFlowAnalysis(
compiler, false, false);
cfa.process(null, function);
ControlFlowGraph<Node> cfg = cfa.getCfg();
Node returnPathsParent = cfg.getImplicitReturn().getValue();
for (DiGraphNode<Node, Branch> pred :
cfg.getDirectedPredNodes(returnPathsParent)) {
Node n = pred.getValue();
if (!n.isReturn()) {
return false;
}
}
return true;
}
}
private class InstrumentCallback extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isFunction()) {
return;
}
int id = functionNames.getFunctionId(n);
if (id < 0) {
// Function node was added during compilation; don't instrument.
return;
}
String name = functionNames.getFunctionName(n);
if (!reportFunctionName.isEmpty()) {
Node body = n.getLastChild();
Node call = IR.call(
IR.name(reportFunctionName),
IR.number(id),
IR.string(name),
IR.name("arguments"));
call.putBooleanProp(Node.FREE_CALL, true);
Node expr = IR.exprResult(call);
expr.useSourceInfoFromForTree(n);
body.addChildToFront(expr);
t.reportCodeChange();
}
if (!reportFunctionExitName.isEmpty()) {
(new InstrumentReturns(id, name)).process(n);
}
if (!definedFunctionName.isEmpty()) {
Node call = IR.call(
IR.name(definedFunctionName),
IR.number(id),
IR.string(name));
call.putBooleanProp(Node.FREE_CALL, true);
call.useSourceInfoFromForTree(n);
Node expr = NodeUtil.newExpr(call);
Node addingRoot = null;
if (NodeUtil.isFunctionDeclaration(n)) {
JSModule module = t.getModule();
addingRoot = compiler.getNodeForCodeInsertion(module);
addingRoot.addChildToFront(expr);
} else {
Node beforeChild = n;
for (Node ancestor : n.getAncestors()) {
Token type = ancestor.getToken();
if (type == Token.BLOCK || type == Token.SCRIPT) {
addingRoot = ancestor;
break;
}
beforeChild = ancestor;
}
addingRoot.addChildBefore(expr, beforeChild);
}
compiler.reportChangeToEnclosingScope(addingRoot);
}
}
}
}