TimerOutputs.jl

repository·master·Indexed 20 days ago

https://github.com/kristofferc/timeroutputs.jl

A Julia package for profiling code by assigning labels to execution sections. It generates hierarchical tables showing time spent, memory allocations, and call counts using the `@timeit` macro. Features include line profiling with `@timeit_all`, test suite profiling via `@timed_testset`, and support for merging timers in multithreaded environments. It integrates with Tables.jl for data export and FlameGraphs for visualization.

Tokens
4.3K
Snippets
21
Records
24
Agent score
22%

What's inside TimerOutputs.jl

  1. Overview of TimerOutputs.jl

    master
    TimerOutputs.jl is a Julia package used to generate formatted reports of execution timings and memory allocations across different sections of a program. It provides the @timeit macro, which functions similarly to Base @time but allows you to assign a specific label to the timed code block. Data for the same label within the same scope is automatically accumulated, allowing for easy profiling of repeated operations.
  2. How to time multithreaded code safely

    master

    A single TimerOutput instance is not thread-safe for concurrent timing. Do not attempt to use the same TimerOutput object across multiple threads or tasks simultaneously, as this will cause race conditions.

    Correct Pattern:

    1. Create one TimerOutput per thread/task.
    2. Use merge!(main_timer, thread_timer, tree_point = [...]) at a synchronization point (like a join) to combine the results into the main timer. merge! is thread-safe.
  3. Achieve zero-overhead with NoTimerOutput

    master

    For a mechanism that selects timing per timer object (rather than per module) and avoids recompilation trickery, use NoTimerOutput.

    When the compiler knows the timer is a NoTimerOutput (e.g., it is stored as a const or a type parameter in a struct), all @timeit calls using that timer are compiled away entirely. Note that switching from a TimerOutput to a NoTimerOutput requires reconstructing the object that holds the timer.

    struct Solver{Timer}
        to::Timer
    end
    
    solve(s::Solver) = @timeit s.to "solve" begin 
        # ... code ...
    end
    
    # This version is timed:
    Solver(TimerOutput())   
    
    # This version has timing compiled away entirely:
    Solver(NoTimerOutput()) 
  4. Use the default global timer

    master

    For simple use cases, you can use versions of the functions and macros that do not require an explicit TimerOutput instance. These use a global timer shared among all users of the package. You can reset this global timer using reset_timer!() and retrieve it via TimerOutputs.get_defaulttimer().

    reset_timer!()
    
    @timeit "section" sleep(0.02)
    @timeit "section2" sleep(0.1)
    
    print_timer()
  5. Measure untimed code using the complement option

    master

    To see the time and allocations consumed by operations occurring outside of your @timeit blocks, use the complement = true argument with print_timer(). This adds gray rows to the output: a ~untimed~ row for total wall time/allocations outside all sections, and a ~name~ row under each section representing the portion of that section not covered by its subsections.

    print_timer(to; complement = true)
  6. Minimize overhead using @timeit_debug

    master

    If you want to instrument a package with timing macros without incurring overhead during normal operation, use the @timeit_debug macro.

    By default, @timeit_debug is disabled and the conditional is optimized away for zero overhead. You can enable timings for a specific module using TimerOutputs.enable_debug_timings(<module>). This triggers a recompilation of the module to enable the macros.

    • Recursive behavior: By default, enabling timings for a module also enables them for all submodules. To prevent this, pass recursive = false to enable_debug_timings.
    • Disabling: Use TimerOutputs.disable_debug_timings(<module>) to turn them off again.
    # To enable timings for a module (and its submodules):
    TimerOutputs.enable_debug_timings(MyModule)
    
    # To enable timings for ONLY the specific module (no submodules):
    TimerOutputs.enable_debug_timings(MyModule, recursive = false)
    
    # To turn them off:
    TimerOutputs.disable_debug_timings(MyModule)
  7. Visualize TimerOutputs with FlameGraphs

    master

    You can use the FlameGraphs extension to create an alternative visualization of your timing data. This integrates with ProfileView.jl to display the hierarchy of timed sections.

    By default, the graph includes the span of the root TimerOutput. If you want to crop the graph so it only shows the children of the root (rather than the duration the root object was open), use the crop_root=true option in the flamegraph function.

    using TimerOutputs, FlameGraphs, ProfileView
    to = TimerOutput()
    @timeit to "foo" begin
        sleep(0.1)
        @timeit to "bar" begin
            sleep(0.1)
            @timeit to "baz" begin
                sleep(0.1)
            end
        end
    end
    # Standard view
    ProfileView.view(flamegraph(to))
    
    # View cropped to children
    ProfileView.view(flamegraph(to, crop_root=true))
  8. Export timer data via Tables.jl

    master

    Timers implement the Tables.jl interface. You can pass a TimerOutput instance directly to any package that consumes Tables, such as DataFrames.jl or CSV.jl, to convert the measurements into a table or file. The resulting table contains one row per section in depth-first order.

    using DataFrames, CSV
    # Convert to a DataFrame
    df = DataFrame(to)
    
    # Write to a CSV file
    CSV.write("timings.csv", to)
  9. Profile lines and functions with @timeit_all and to()

    master

    For more granular profiling, you can use @timeit_all to profile every statement within a block, or instrument existing functions.

    Line Profiling

    Use @timeit_all to profile every line in a function body:

    @timeit_all to function line_profile(n)
        x = 0
        for i in 1:n
            x += i
        end
        x
    end

    Instrumenting existing functions

    You can wrap an existing function to create a timed version using the to(func) syntax:

    foo(x) = x + 1
    timed_foo = to(foo)
    timed_foo(5)
    # Line profiling
    @timeit_all to function line_profile(n)
        x = 0
        for i in 1:n
            x += i
        end
        x
    end
    
    # Instrumenting
    foo(x) = x + 1
    timed_foo = to(foo)
    timed_foo(5)
  10. Use shared named timers with get_timer

    master

    You can maintain a collection of named timers that are shared across different parts of a program using get_timer(timer_name::String). This retrieves an existing timer or creates a new one if it doesn't exist.

    Warnings:

    • Do not call get_timer from top-level in a package that is being precompiled, as the retrieved timer will not be shared with other users.
    • Avoid extensive use in libraries to prevent namespace collisions, as the timer names are shared globally.
    module UseTimer
    using TimerOutputs: @timeit, get_timer
    
    function foo()
        to = get_timer("Shared")
        @timeit get_timer("Shared") "foo" sleep(0.1)
    end
    end
    
    @timeit get_timer("Shared") "section1" begin
        UseTimer.foo()
        sleep(0.01)
    end
    
    print_timer(get_timer("Shared"))
  11. Manually manage timed sections

    master

    If you need fine-grained control over when a section starts and ends (e.g., across non-contiguous lines of code), use the manual start/stop functions.

    section = begin_timed_section!(to, "my section")
    # ... perform work ...
    end_timed_section!(to, section)