forked from KristofferC/TimerOutputs.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTimerOutput.jl
383 lines (332 loc) · 11.8 KB
/
TimerOutput.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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
############
# TimeData #
############
mutable struct TimeData
ncalls::Int
time::Int64
allocs::Int64
firstexec::Int64
end
TimeData(ncalls, time, allocs) = TimeData(ncalls, time, allocs, time)
Base.copy(td::TimeData) = TimeData(td.ncalls, td.time, td.allocs)
TimeData() = TimeData(0, 0, 0, time_ns())
function Base.:+(self::TimeData, other::TimeData)
TimeData(self.ncalls + other.ncalls,
self.time + other.time,
self.allocs + other.allocs,
min(self.firstexec, other.firstexec))
end
###############
# TimerOutput #
###############
mutable struct TimerOutput
start_data::TimeData
accumulated_data::TimeData
inner_timers::Dict{String,TimerOutput}
timer_stack::Vector{TimerOutput}
name::String
flattened::Bool
enabled::Bool
totmeasured::Tuple{Int64,Int64}
prev_timer_label::String
prev_timer::Union{TimerOutput,Nothing}
function TimerOutput(label::String = "root")
start_data = TimeData(0, time_ns(), gc_bytes())
accumulated_data = TimeData()
inner_timers = Dict{String,TimerOutput}()
timer_stack = TimerOutput[]
return new(start_data, accumulated_data, inner_timers, timer_stack, label, false, true, (0, 0), "", nothing)
end
# Jeez...
TimerOutput(start_data, accumulated_data, inner_timers, timer_stack, name, flattened, enabled, totmeasured, prev_timer_label,
prev_timer) = new(start_data, accumulated_data, inner_timers, timer_stack, name, flattened, enabled, totmeasured, prev_timer_label,
prev_timer)
end
Base.copy(to::TimerOutput) = TimerOutput(copy(to.start_data), copy(to.accumulated_data), copy(to.inner_timers),
copy(to.timer_stack), to.name, to.flattened, to.enabled, to.totmeasured, "", nothing)
const DEFAULT_TIMER = TimerOutput()
# push! and pop!
function Base.push!(to::TimerOutput, label::String)
if length(to.timer_stack) == 0 # Root section
current_timer = to
else # Not a root section
current_timer = to.timer_stack[end]
end
# Fast path
if current_timer.prev_timer_label == label
timer = current_timer.prev_timer
else
maybe_timer = get(current_timer.inner_timers, label, nothing)
# this could be implemented more elegant using
# get!(() -> TimerOutput(label), current_timer.inner_timers, label)
# however that causes lots of allocations in
# julia v1.3
if maybe_timer === nothing
timer = TimerOutput(label)
current_timer.inner_timers[label] = timer
else
timer = maybe_timer
end
end
current_timer.prev_timer_label = label
current_timer.prev_timer = timer
push!(to.timer_stack, timer)
return timer.accumulated_data
end
Base.pop!(to::TimerOutput) = pop!(to.timer_stack)
# Only sum the highest parents
function totmeasured(to::TimerOutput)
t, b = Int64(0), Int64(0)
for section in values(to.inner_timers)
timedata = section.accumulated_data
t += timedata.time
b += timedata.allocs
end
return t, b
end
function longest_name(to::TimerOutput, indent = 0)
m = textwidth(to.name) + indent
for inner_timer in values(to.inner_timers)
m = max(m, longest_name(inner_timer, indent + 2))
end
return m
end
# merging timer outputs
const merge_lock = ReentrantLock() # needed for merges of objects on different threads
Base.merge(others::TimerOutput...) = merge!(TimerOutput(), others...)
function Base.merge!(self::TimerOutput, others::TimerOutput...; tree_point = String[])
lock(merge_lock) do
for other in others
self.accumulated_data += other.accumulated_data
its = self.inner_timers
for point in tree_point
its = its[point].inner_timers
end
_merge(its, other.inner_timers)
end
return self
end
end
function _merge(self::Dict{String,TimerOutput}, other::Dict{String,TimerOutput})
for key in keys(other)
if haskey(self, key)
self[key].accumulated_data += other[key].accumulated_data
_merge(self[key].inner_timers, other[key].inner_timers)
else
self[key] = deepcopy(other[key])
end
end
end
#######
# API #
#######
# Accessors
ncalls(to::TimerOutput) = to.accumulated_data.ncalls
allocated(to::TimerOutput) = to.accumulated_data.allocs
time(to::TimerOutput) = to.accumulated_data.time
totallocated(to::TimerOutput) = totmeasured(to)[2]
tottime(to::TimerOutput) = totmeasured(to)[1]
time() = time(DEFAULT_TIMER)
ncalls() = ncalls(DEFAULT_TIMER)
allocated() = allocated(DEFAULT_TIMER)
totallocated() = totmeasured(DEFAULT_TIMER)[2]
tottime() = totmeasured(DEFAULT_TIMER)[1]
get_defaulttimer() = DEFAULT_TIMER
Base.@deprecate get_defaultimer get_defaulttimer
# Macro
macro timeit(args...)
return timer_expr(__module__, false, args...)
end
macro timeit_debug(args...)
if !isdefined(__module__, :timeit_debug_enabled)
Core.eval(__module__, :(timeit_debug_enabled() = false))
end
return timer_expr(__module__, true, args...)
end
function enable_debug_timings(m::Module)
if !getfield(m, :timeit_debug_enabled)()
Core.eval(m, :(timeit_debug_enabled() = true))
end
end
function disable_debug_timings(m::Module)
if getfield(m, :timeit_debug_enabled)()
Core.eval(m, :(timeit_debug_enabled() = false))
end
end
timer_expr(args...) = throw(ArgumentError("invalid macro usage for @timeit, use as @timeit [to] label codeblock"))
function is_func_def(f)
if isa(f, Expr) && (f.head === :function || Base.is_short_function_def(f))
return true
else
return false
end
end
function timer_expr(m::Module, is_debug::Bool, ex::Expr)
is_func_def(ex) && return timer_expr_func(m, is_debug, :($(TimerOutputs.DEFAULT_TIMER)), ex)
return esc(_timer_expr(m, is_debug, :($(TimerOutputs).DEFAULT_TIMER), ex))
end
function timer_expr(m::Module, is_debug::Bool, label_or_to, ex::Expr)
is_func_def(ex) && return timer_expr_func(m, is_debug, label_or_to, ex)
return esc(_timer_expr(m, is_debug, :($(TimerOutputs).DEFAULT_TIMER), label_or_to, ex))
end
function timer_expr(m::Module, is_debug::Bool, label::String, ex::Expr)
is_func_def(ex) && return timer_expr_func(m, is_debug, :($(TimerOutputs).DEFAULT_TIMER), ex, label)
return esc(_timer_expr(m, is_debug, :($(TimerOutputs).DEFAULT_TIMER), label, ex))
end
function timer_expr(m::Module, is_debug::Bool, to, label, ex::Expr)
is_func_def(ex) && return timer_expr_func(m, is_debug, to, ex, label)
return esc(_timer_expr(m, is_debug, to, label, ex))
end
function _timer_expr(m::Module, is_debug::Bool, to::Union{Symbol, Expr, TimerOutput}, label, ex::Expr)
@gensym local_to enabled accumulated_data b₀ t₀ val
timeit_block = quote
$local_to = $to
$enabled = $local_to.enabled
if $enabled
$accumulated_data = $(push!)($local_to, $label)
end
$b₀ = $(gc_bytes)()
$t₀ = $(time_ns)()
$(Expr(:tryfinally,
:($val = $ex),
quote
if $enabled
$(do_accumulate!)($accumulated_data, $t₀, $b₀)
$(pop!)($local_to)
end
end))
$val
end
if is_debug
return quote
if $m.timeit_debug_enabled()
$timeit_block
else
$ex
end
end
else
return timeit_block
end
end
function timer_expr_func(m::Module, is_debug::Bool, to, expr::Expr, label=nothing)
expr = macroexpand(m, expr)
def = splitdef(expr)
label === nothing && (label = string(def[:name]))
def[:body] = if is_debug
quote
@inline function inner()
$(def[:body])
end
$(_timer_expr(m, is_debug, to, label, :(inner())))
end
else
_timer_expr(m, is_debug, to, label, def[:body])
end
return esc(combinedef(def))
end
function do_accumulate!(accumulated_data, t₀, b₀)
accumulated_data.time += time_ns() - t₀
accumulated_data.allocs += gc_bytes() - b₀
accumulated_data.ncalls += 1
end
reset_timer!() = reset_timer!(DEFAULT_TIMER)
function reset_timer!(to::TimerOutput)
to.inner_timers = Dict{String,TimerOutput}()
to.start_data = TimeData(0, time_ns(), gc_bytes())
to.accumulated_data = TimeData()
to.prev_timer_label = ""
to.prev_timer = nothing
resize!(to.timer_stack, 0)
return to
end
# We can remove this now that the @timeit macro is exception safe.
# Doesn't hurt to keep it for a while though
timeit(f::Function, label::String) = timeit(f, DEFAULT_TIMER, label)
function timeit(f::Function, to::TimerOutput, label::String)
accumulated_data = push!(to, label)
b₀ = gc_bytes()
t₀ = time_ns()
local val
try
val = f()
finally
accumulated_data.time += time_ns() - t₀
accumulated_data.allocs += gc_bytes() - b₀
accumulated_data.ncalls += 1
pop!(to)
end
return val
end
Base.haskey(to::TimerOutput, name::String) = haskey(to.inner_timers, name)
Base.getindex(to::TimerOutput, name::String) = to.inner_timers[name]
function flatten(to::TimerOutput)
t, b = totmeasured(to)
inner_timers = Dict{String,TimerOutput}()
for inner_timer in values(to.inner_timers)
_flatten!(inner_timer, inner_timers)
end
toc = copy(to)
return TimerOutput(toc.start_data, toc.accumulated_data, inner_timers, TimerOutput[], "Flattened", true, true, (t, b), "", to)
end
function _flatten!(to::TimerOutput, inner_timers::Dict{String,TimerOutput})
for inner_timer in values(to.inner_timers)
_flatten!(inner_timer, inner_timers)
end
if haskey(inner_timers, to.name)
timer = inner_timers[to.name]
timer.accumulated_data += to.accumulated_data
else
toc = copy(to)
toc.inner_timers = Dict{String,TimerOutput}()
inner_timers[toc.name] = toc
end
end
enable_timer!(to::TimerOutput=DEFAULT_TIMER) = to.enabled = true
disable_timer!(to::TimerOutput=DEFAULT_TIMER) = to.enabled = false
# Macro to selectively disable timer for expression
macro notimeit(args...)
notimeit_expr(args...)
end
# Default function throws an error for the benefit of the user
notimeit_expr(args...) = throw(ArgumentError("invalid macro usage for @notimeit, use as @notimeit [to] codeblock"))
complement!() = complement!(DEFAULT_TIMER)
function complement!(to::TimerOutput)
if length(to.inner_timers) == 0
return nothing
end
tot_time = to.accumulated_data.time
tot_allocs = to.accumulated_data.allocs
for timer in values(to.inner_timers)
tot_time -= timer.accumulated_data.time
tot_allocs -= timer.accumulated_data.allocs
complement!(timer)
end
tot_time = max(tot_time, 0)
tot_allocs = max(tot_allocs, 0)
if !(to.name in ["root", "Flattened"])
name = string("~", to.name, "~")
timer = TimerOutput(to.start_data, TimeData(max(1,to.accumulated_data.ncalls), tot_time, tot_allocs), Dict{String,TimerOutput}(), TimerOutput[], name, false, true, (tot_time, tot_allocs), to.name, to)
to.inner_timers[name] = timer
end
return to
end
# If @notimeit was called without a TimerOutput instance, use default timer
notimeit_expr(ex::Expr) = notimeit_expr(:($(TimerOutputs.DEFAULT_TIMER)), ex)
# Disable timer, evaluate expression, restore timer to previous value, and return expression result
function notimeit_expr(to, ex::Expr)
return quote
local to = $(esc(to))
local enabled = to.enabled
$(disable_timer!)(to)
local val
$(Expr(:tryfinally,
:(val = $(esc(ex))),
quote
if enabled
$(enable_timer!)(to)
end
end))
val
end
end