diff --git a/Project.toml b/Project.toml index 1e6eeaf..db13d80 100644 --- a/Project.toml +++ b/Project.toml @@ -5,10 +5,12 @@ version = "0.5.12" [deps] ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] -julia = "1" ExprTools = "0.1.0" +Tables = "1" +julia = "1" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/README.md b/README.md index 41fb2ea..7699bc9 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,24 @@ julia> TimerOutputs.totallocated(to) 7632 ``` +## Exporting data + +The `TimerOutput` type implements the [Tables.jl](https://github.com/JuliaData/Tables.jl) +interface. Thus, it works with various data-processing packages such as DataFrames.jl. +For example, use `DataFrame(to)` (where `to isa TimerOutput`) to create a data frame +containing the timing and GC information. + +The output table has the following columns: + +* `name`: A tuple of strings. The i-th element is the label of the i-the level. +* `ncalls`: The number of times `@timeit` is evaluated. +* `time`: The time (nanoseconds) spent, excluding inner `@timeit` sections if any. +* `tottime`: The time (nanoseconds) spent, including inner `@timeit` sections if any. +* `allocated`: The number of bytes allocated, excluding inner `@timeit` sections if any. +* `totallocated`: The number of bytes allocated, including inner `@timeit` sections if any. +* `isleaf`: `true` if the corresponding `@timeit` does not have inner `@timeit`; + `false` otherwise. + ## Default Timer It is often the case that it is enough to only use one timer. For convenience, there is therefore a version of diff --git a/src/TimerOutputs.jl b/src/TimerOutputs.jl index 4979733..08e2254 100644 --- a/src/TimerOutputs.jl +++ b/src/TimerOutputs.jl @@ -1,6 +1,7 @@ module TimerOutputs using ExprTools +import Tables import Base: show, time_ns export TimerOutput, @timeit, @timeit_debug, reset_timer!, print_timer, timeit, @@ -17,12 +18,17 @@ else end end +if !@isdefined(fieldtypes) + fieldtypes(T) = Tuple([fieldtype(T, i) for i in 1:fieldcount(T)]) +end + using Printf include("TimerOutput.jl") include("show.jl") include("utilities.jl") +include("tables.jl") if Base.VERSION >= v"1.4.2" include("precompile.jl") diff --git a/src/tables.jl b/src/tables.jl new file mode 100644 index 0000000..21d6e88 --- /dev/null +++ b/src/tables.jl @@ -0,0 +1,43 @@ +Tables.istable(::Type{TimerOutput}) = true +Tables.rowaccess(::Type{TimerOutput}) = true + +struct TimerOutputRow + name::Tuple{String,Vararg{String}} + ncalls::Int + time::Int + allocated::Int + totallocated::Int + tottime::Int + isleaf::Bool +end + +function foreachrow(f, timer::TimerOutput, prefix::Tuple{Vararg{String}} = ()) + for k in sort!(collect(keys(timer.inner_timers))) + to = timer.inner_timers[k] + name = (prefix..., k) + row = TimerOutputRow( + name, + ncalls(to), + time(to), + allocated(to), + totallocated(to), + tottime(to), + isempty(to.inner_timers), + ) + f(row) + foreachrow(f, to, name) + end +end + +function Tables.rows(to::TimerOutput) + table = TimerOutputRow[] + foreachrow(to) do row + push!(table, row) + end + return table +end + +const TABLE_SCHEMA = + Tables.Schema{fieldnames(TimerOutputRow),Tuple{fieldtypes(TimerOutputRow)...}}() + +Tables.schema(::TimerOutput) = TABLE_SCHEMA diff --git a/test/runtests.jl b/test/runtests.jl index 02ece7f..c7e7c3d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using TimerOutputs using Test +using Tables import TimerOutputs: DEFAULT_TIMER, ncalls, flatten, prettytime, prettymemory, prettypercent, prettycount @@ -621,3 +622,51 @@ end @test !in("3.1.1", collect(keys(to32.inner_timers))) end + +@testset "Tables.jl interface" begin + to = TimerOutput() + + @timeit to "1" sleep(0.1) + + @timeit to "2" begin + @timeit to "2.1" sleep(0.1) + @timeit to "2.2" sleep(0.1) + @timeit to "2.3" sleep(0.1) + end + + @timeit to "3" begin + @timeit to "3.1" begin + @timeit to "3.1.1" sleep(0.1) + @timeit to "3.1.2" sleep(0.1) + @timeit to "3.1.3" sleep(0.1) + end + @timeit to "3.2" begin + @timeit to "3.2.1" sleep(0.1) + @timeit to "3.2.2" sleep(0.1) + @timeit to "3.2.3" sleep(0.1) + end + end + + table = Tables.columntable(to) + @test table.name == [ + ("1",), + ("2",), + ("2", "2.1"), + ("2", "2.2"), + ("2", "2.3"), + ("3",), + ("3", "3.1"), + ("3", "3.1", "3.1.1"), + ("3", "3.1", "3.1.2"), + ("3", "3.1", "3.1.3"), + ("3", "3.2"), + ("3", "3.2", "3.2.1"), + ("3", "3.2", "3.2.2"), + ("3", "3.2", "3.2.3"), + ] + @test all(==(1), table.ncalls) + @test table.isleaf == [ + match(r"^(1|2\.[0-9]|3\.[0-9]\.[0-9])$", name[end]) !== nothing for name in table.name + ] + @test Tables.schema(table) == Tables.schema(to) +end