-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
/
Copy pathprofile.jl
316 lines (296 loc) · 11.7 KB
/
profile.jl
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
_PROFILE_LINES = 1
_PROFILE_DESCEND = 2
_PROFILE_STATE = _PROFILE_LINES | _PROFILE_DESCEND # state is a bitfield
# Record of expressions to parse according to the current _PROFILE_STATE
_PROFILE_EXPR = {}
# To avoid the performance penalty of global variables, we design the
# macro to create local variables through a "let" block that shares
# the timing and count data with "reporting" and "clearing" functions.
_PROFILE_REPORTS = {} # list of reporting functions
_PROFILE_CLEARS = {} # list of clearing functions
_PROFILE_TAGS = {} # line #s for all timing variables
# Profile calibration, to compensate for the overhead of calling time()
_PROFILE_CALIB = 0
# Do it inside a let block, just like in real profiling, in case of
# extra overhead
let # tlast::Uint64 = 0x0, tnow::Uint64 = 0x0
global profile_calib
function profile_calib(n_iter)
trec = Array(Uint64, n_iter)
for i = 1:n_iter
tlast = time_ns()
tnow = time_ns()
trec[i] = tnow - tlast
end
return trec
end
end
_PROFILE_CALIB = min(profile_calib(100))
# Utilities
# Generic expression type testing
is_expr_head(ex::Expr, s::Symbol) = ex.head == s
is_expr_head(nonex, s::Symbol) = false
# Test whether an expression is a function declaration
function isfuncexpr(ex::LineNumberNode)
return false
end
function isfuncexpr(ex::Expr)
return ex.head == :function || (ex.head == :(=) && ex.args[1].head == :call)
end
# Get the "full syntax" of the function call
function funcsyntax(ex::Expr)
return ex.args[1]
end
# Get the symbol associated with the function call
function funcsym(ex::Expr)
tmp = funcsyntax(ex)
tmp = tmp.args[1]
if is_expr_head(tmp, :curly)
tmp = tmp.args[1]
end
return tmp
end
# Test for control-flow statements
is_cf_expr(ex::Expr) = contains([:for, :while, :if, :try], ex.head)
is_cf_expr(ex) = false
# General switchyard function
function insert_profile(ex::Expr, tlast, tnow, timers, counters, tags, indx::Int, retsym, rettest)
if ex.head == :block
insert_profile_block(ex, tlast, tnow, timers, counters, tags, indx, retsym, rettest)
elseif is_cf_expr(ex)
insert_profile_cf(ex, tlast, tnow, timers, counters, tags, indx, retsym)
else
error("Don't know what to do")
end
end
# Insert profiling statements into a code block
# rettest is a function with the following syntax:
# rettest(Expr, Int)
# and evaluates to true if the return value of Expr needs to be saved
# before inserting profiling statements.
function insert_profile_block(fblock::Expr, tlast, tnow, timers, counters, tags, indx::Int, retsym, rettest)
global _PROFILE_STATE, _PROFILE_DESCEND
if fblock.head != :block
println(fblock)
error("expression is not a block")
end
descend = _PROFILE_STATE & _PROFILE_DESCEND > 0
fblocknewargs = {}
for i = 1:length(fblock.args)
if isa(fblock.args[i],LineNumberNode) || is_expr_head(fblock.args[i], :line)
# This is a line expression, so no counters/timers required
push(fblocknewargs,fblock.args[i])
# ...but keep track of the line # for use during reporting
lasttag = fblock.args[i]
elseif descend && is_cf_expr(fblock.args[i])
# This is a control-flow statement, it requires special
# handling (recursive)
cfnew, indx = insert_profile_cf(fblock.args[i], tlast, tnow, timers, counters, tags, indx, retsym)
push(fblocknewargs, cfnew)
else
# This is an "ordinary" statement
saveret = rettest(fblock, i)
push(tags,lasttag)
if saveret
if is_expr_head(fblock.args[i], :return)
push(fblocknewargs, :($retsym = $fblock.args[i].args[1]))
else
push(fblocknewargs, :($retsym = $fblock.args[i]))
end
else
push(fblocknewargs, fblock.args[i])
end
# This next line inserts timing statements between two
# lines of code, equivalent to:
# timehr(_PROFILE_USE_CLOCK, tnow) # end time of prev
# timers[indx] += timehr_diff(tlast, tnow)
# counters[indx] += 1
# timehr(_PROFILE_USE_CLOCK, tlast) # start time for next
append!(fblocknewargs,{:($tnow = time_ns()), :(($timers)[($indx)] += $tnow - $tlast), :(($counters)[($indx)] += 1), :($tlast = time_ns())})
indx += 1
if saveret
push(fblocknewargs, :(return $retsym))
end
end
end
return expr(:block, fblocknewargs), indx
end
# Handling control-flow statements
function insert_profile_cf(ex::Expr, tlast, tnow, timers, counters, tags, indx::Int, retsym)
rettest = (ex, i) -> is_expr_head(ex, :return)
if length(ex.args) == 2
# This is a for, while, or 2-argument if or try block
block1, indx = insert_profile(ex.args[2], tlast, tnow, timers, counters, tags, indx, retsym, rettest)
return expr(ex.head, {ex.args[1], block1}), indx
elseif length(ex.args) == 3
# This is for a 3-argument if or try block
block1, indx = insert_profile(ex.args[2], tlast, tnow, timers, counters, tags, indx, retsym, rettest)
block2, indx = insert_profile(ex.args[3], tlast, tnow, timers, counters, tags, indx, retsym, rettest)
return expr(ex.head, {ex.args[1], block1, block2}), indx
else
error("Wrong number of arguments")
end
end
# Insert timing and counters into function body.
# Function bodies differ from blocks in these respects:
# - We need to initialize tlast
# - The final statement of a function should be returned, even if there
# is no explicit return statement
# - Functions can be defined in "short-form" (e.g.,
# "isempty(x) = numel(x)==0"), and the return value for these
# needs to be managed, too
function insert_profile_function(ex::Expr, tlast, tnow, timers, counters, tags, indx::Int, retsym)
fblock = ex.args[2]
if fblock.head != :block
error("Can't parse func expression")
end
# Prepare the test for whether we need to save the return value of
# a given line of code. We may need to store the return value
# because we need to run timing operations after computing the
# output.
# For a function, this will be true in three cases:
# - For a "f1(x) = x+1" type of function declaration
# - For an explicit return statement
# - For the last line of a function that does not have
# an explicit return statement in it.
if ex.head == :(=)
# This is a short-form function declaration
savefunc = (ex, i) -> true
else
# Long form, check to see if it's a return or the last line
savefunc = (ex, i) -> i == length(fblock.args) || is_expr_head(ex, :return)
end
# Insert the profiling statements in the function
fblocknewargs, indx = insert_profile_block(fblock, tlast, tnow, timers, counters, tags, indx, retsym, savefunc)
# Prepend the initialization of tlast
fblocknewargs = vcat({:($tlast = time_ns())}, fblocknewargs.args)
return expr(:function,{funcsyntax(ex),expr(:block,fblocknewargs)}), indx
end
function profile_parse(ex::Expr)
if _PROFILE_STATE & _PROFILE_LINES > 0
# Create the "let" variables for timing and counting
tlast, tnow, timers, counters = gensym(4)
# Keep track of line numbers
tags = {}
# Preserve return values
retsym = gensym()
# Create the symbols used for reporting and clearing the data
# for this block
funcreport, funcclear = gensym(2)
# Parse the block and insert instructions
indx = 1
coreargs = {}
if ex.head == :block
# This is a block which may contain many function declarations
for i = 1:length(ex.args)
if isfuncexpr(ex.args[i])
# Insert "global" statement for each function
push(coreargs,expr(:global,funcsym(ex.args[i])))
# Insert function-call counters
newfuncexpr, indx = insert_profile_function(ex.args[i], tlast, tnow, timers, counters, tags, indx, retsym)
push(coreargs, newfuncexpr)
else
push(coreargs,ex.args[i])
end
end
elseif isfuncexpr(ex)
# This is a single function declaration
push(coreargs,expr(:global,funcsym(ex)))
newfuncexpr, indx = insert_profile_function(ex, tlast, tnow, timers, counters, tags, indx, retsym)
push(coreargs, newfuncexpr)
else
error("Could not parse expression")
end
n_lines = indx-1
# Insert reporting function
# Because we're using a gensym for the function name, we can't
# quote the whole thing
push(coreargs, expr(:global, funcreport))
push(coreargs, expr(:function, {expr(:call, {funcreport}), expr(:block,{:(return $timers, $counters)})}))
# Insert clearing function
push(coreargs, expr(:global, funcclear))
push(coreargs, expr(:function, {expr(:call, {funcclear}), expr(:block,{:(fill!($timers,0)), :(fill!($counters,0))})}))
# Put all this inside a let block
excore = expr(:block,coreargs)
exlet = expr(:let,{expr(:block,excore), :($timers = zeros(Uint64, $n_lines)), :($counters = zeros(Int, $n_lines))})
return exlet, tags, funcreport, funcclear
else
return ex ,{}, :funcnoop, :funcnoop
end
end
function funcnoop()
end
function profile_parse_all()
del_all(_PROFILE_REPORTS)
del_all(_PROFILE_CLEARS)
del_all(_PROFILE_TAGS)
retargs = {}
for i = 1:length(_PROFILE_EXPR)
newblock, tags, funcreport, funcclear = profile_parse(_PROFILE_EXPR[i])
retargs = vcat(retargs, newblock.args)
if !isempty(tags)
push(_PROFILE_TAGS, tags)
push(_PROFILE_REPORTS, funcreport)
push(_PROFILE_CLEARS, funcclear)
end
end
push(retargs,:(return nothing))
return expr(:block,retargs)
end
function profile_report()
exret = cell(length(_PROFILE_REPORTS)+2)
ret = gensym()
exret[1] = :($ret = {})
for i = 1:length(_PROFILE_REPORTS)
exret[i+1] = :(push($ret,$expr(:call,{_PROFILE_REPORTS[i]})))
end
exret[end] = :(profile_print($ret))
return expr(:block,exret)
end
function profile_print(tc)
for i = 1:length(tc)
timers = tc[i][1]
counters = tc[i][2]
for j = 1:length(counters)
if counters[j] != 0
printf("%8d %f % f %s\n", counters[j], convert(Float64, timers[j])*1e-9, convert(Float64, timers[j] - counters[j]*_PROFILE_CALIB)*1e-9, _PROFILE_TAGS[i][j])
end
end
end
end
function profile_clear()
exret = cell(length(_PROFILE_CLEARS)+1)
for i = 1:length(_PROFILE_CLEARS)
exret[i] = expr(:call,{_PROFILE_CLEARS[i]})
end
exret[end] = :(return nothing)
return expr(:block,exret)
end
macro profile(ex)
global _PROFILE_STATE
if isa(ex,Symbol)
# State changes
if ex == :off
_PROFILE_STATE = 0
elseif ex == :on
_PROFILE_STATE = _PROFILE_LINES
elseif ex == :reset
elseif ex == :report
return profile_report()
elseif ex == :clear
return profile_clear()
else
error("Profile mode not recognized")
end
return profile_parse_all()
elseif isa(ex,Expr)
push(_PROFILE_EXPR,ex)
exret, tags, funcreport, funcclear = profile_parse(ex)
if !isempty(tags)
push(_PROFILE_TAGS, tags)
push(_PROFILE_REPORTS, funcreport)
push(_PROFILE_CLEARS, funcclear)
end
return exret
end
end