# TimerOutputs.jl gives Julia code sections a name, a tree and a report

> TimerOutputs is a small Julia package whose @timeit macro times a labelled block, accumulates repeated calls under that label, and prints a formatted table of time, allocations and call counts at the end. It is the reporting layer Julia's own @time does not provide, and it trades speed of instrumentation for a readable summary.

**KristofferC/TimerOutputs.jl** — Formatted output of timed sections in Julia

- Repository: https://github.com/KristofferC/TimerOutputs.jl
- Stars: 723 · Forks: 59
- Language: Julia
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/kristofferc-timeroutputs-jl

## The same idea as @time, with a label attached

TimerOutputs is described as a small Julia package used to generate formatted output from timings made in different sections of a program. The README is clear about the relationship to the standard library: the main functionality is the `@timeit` macro, similar to the `@time` macro in Base, except that it also assigns a label to the code section being timed.

That label is the whole idea. With `@time` you get a line per call and no memory of what you saw. With `@timeit` the measurement is attached to a name, and the package keeps the record. The README states the accumulation rule: multiple calls to code sections with the same label, and in the same scope, accumulate the data for that label. So a loop that calls the same section a hundred times produces one row with a call count of one hundred, not a hundred rows.

The unit of work is the `TimerOutput`, the main type that keeps track of everything. You create one, pass it to each `@timeit` call, and print it at the end:

```julia
# Create a TimerOutput, this is the main type that keeps track of everything.
const to = TimerOutput()
```

```julia
# Time a section code with the label "sleep" to the `TimerOutput` named "to"
@timeit to "sleep" sleep(0.02)
```

A single call is enough to show the shape. The example output in the README comes from a finite element simulation and shows a header row with the total time and allocations since the timer was created, the percentage of those totals spent inside timed sections, and then one row per section with the call count, the time, the share of the total, the average per call, and the same set of figures for allocations.

## Time and allocations share one table

The report has two halves. The left side is time, the right side is allocations, and the README describes what each row means. The `Tot / % measured` row shows the total wall time passed and the allocations made since the start of the timer, plus the percentage of those totals spent inside timed sections. The following lines give the data for each timed section, with the section label first and the number of calls made to that section next, and finally the total time elapsed or allocations made in that section together with the percentage of the total and the average per call.

Two details make the table readable. There are bar columns that visualise each section's share of the total, and in colour capable terminals they are coloured from blue for cheap to red for expensive, so the expensive section is visible without reading numbers. And a `%par` column showing the percentage of the enclosing section is available through the `columns` printing option.

That last point is the reason to care about the format rather than the numbers. A flat profile tells you that something is slow. A profile with parent context tells you which part of a slow stage is responsible, and the README's own example output shows the tree with connector characters in front of nested labels and indentation that grows with depth.

The output is also cuttable. The README says the output can be customized as to only show the things you are interested in, so you are not forced to keep the allocation columns if you only care about time.

## Nested sections form a tree, not a flat list

Nesting is the part of the design that has a real rule attached to it. The README's example puts a block with the label `nest 1` around three calls to `level 2.1` and one to `level 2.2`, and then a separate `nest 2` block with one call to each of the same two labels. The comment in the example says it directly: nested sections with the same name are not accumulated if they have different parents.

So the identity of a measurement is the pair, label and parent. `level 2.1` under `nest 1` and `level 2.1` under `nest 2` are two different rows with two different averages, even though the label text is identical. If you expect a flat table where the same label always merges, this behaviour will surprise you the first time you restructure a function.

The payoff is that the tree mirrors the code. A section that calls a helper many times shows the helper as a child row, and the percentage of the enclosing section is available through the `columns` option, which is the number you want when a stage is slow and you need to know which of its children is responsible.

Instrumentation is exception safe as well. The README's example defines a function whose timed block sleeps and then throws an error, and the timing of that block is still recorded when the exception propagates.

## Switching the timer off when it gets in the way

You will want sections that are not measured, either because they are setup work or because the measurement itself is the thing you are studying. The package gives three ways to do that.

`disable_timer!` turns the timer off selectively, and `enable_timer!` turns it on again:

```julia
# Use disable_timer! to selectively turn off a timer, enable_timer! turns it on again
disable_timer!(to)
@timeit to "not recorded" sleep(0.1)
enable_timer!(to)
```

The `@notimeit` macro does the same thing for a block and restores the previous state afterwards, so it is the safer form when the disabled region is a call that might itself be timed:

```julia
# Use @notimeit to disable timer and re-enable it afterwards (if it was enabled
# before)
@notimeit to time_test()
```

The third route is to leave the section alone entirely, and use a different label or no instrumentation for it.

These controls matter for a specific reason. Every `@timeit` call records a timestamp, and the package also tracks allocations, so a section you have instrumented is no longer the code you were trying to measure. Being able to switch the whole timer off for a region is how you keep a baseline measurement honest.

## Four ways to instrument the same code

The macro is the common case, but the package offers several ways in.

First, the macro with an explicit label. Second, the macro without a label, where the README explains that timing a function call with no label uses the callee's name as the label, so it is shorthand for `@timeit to "sum" sum(1:100)`:

```julia
# Timing a function call with no label uses the callee's name as the label,
# i.e. this is shorthand for @timeit to "sum" sum(1:100)
@timeit to sum(1:100)
```

Third, annotating a function definition, which the README shows as `@timeit to funcdef(x) = x`, so the label is attached to the function rather than to a call site. Fourth, wrapping an existing function in the `TimerOutput` itself:

```julia
# Or to instrument an existing function:
foo(x) = x + 1
timed_foo = to(foo)
timed_foo(5)
```

For code you did not write, the wrapper is the least invasive option because it leaves the original definition untouched.

There is also `@timeit_all`, which the README describes as additionally timing every statement in a block or function body, that is, line level profiling. It is the tool to reach for when a single labelled section is still too coarse to explain the time you are seeing, and it is more expensive than the other forms because every statement is measured.

## Printing the report and reading the repository

The report is produced by displaying the `TimerOutput`:

```julia
# Print the timings in the default way
show(to)
```

The README notes that printing `to` shows a formatted table with the number of calls, the total time spent in each section, the percentage of the time spent in each section since `to` was created, and averages per call, with the same information available for allocations.

As a package, TimerOutputs is a standard Julia project. The repository root holds `Project.toml`, `src/`, `test/` and an `ext/` directory, which in Julia is where package extensions live, so the package can carry optional integrations without forcing them on every user. There is also a `CHANGELOG.md`, a `LICENSE.md`, a `.pre-commit-config.yaml` for formatting hooks and a `.quality-env/` directory.

Two housekeeping notes. The README's link to the changelog points at version 0.5.30, while the tags are far ahead at v1.2.2, v1.2.1 and v1.2.0 from 2026, so the README text is not tracking the release notes. And the licence is classified as custom by GitHub, with the terms in `LICENSE.md`, so read that file rather than assuming MIT.

## What the report cannot tell you

The tool measures, it does not analyse. A table of times and allocations tells you which section dominates and how much memory it moves, but it says nothing about why. If a section is slow because of a bad algorithm, a lock, an allocation pattern or a cache miss, the report narrows the search and stops there.

Two practical limits are worth naming. First, instrumentation is not free. Every timed section records a timestamp and tracks allocations, so a section called in a hot loop is being measured rather than run, and the resulting number describes the instrumented code. That is why the package offers `disable_timer!` and `@notimeit`, and why the sensible pattern is to instrument selectively, look at the table, then remove the instrumentation.

Second, the report is organised around labels you choose. The accumulation rule, the same label under the same parent, means that a careless label can silently merge unrelated work, and a tree that reflects the code rather than your intent will mislead you.

The alternative approach, for comparison, is to instrument by hand with Julia's own timing primitives and keep your own counters. That gives you full control and no dependency, but you write the aggregation, the hierarchy and the report yourself, and you get no allocation accounting unless you add that too. TimerOutputs exists to remove exactly that work.

## Conclusion

Adopt TimerOutputs.jl when you are profiling a Julia program and want per section time, allocation counts and a nested call structure in one report, and when the extra instrumentation cost is acceptable during development. Do not adopt it for production hot paths, where the allocation and timing overhead of every call is not free, and do not expect it to explain why a section is slow, since it reports rather than analyses. Verify first that the version you add through the Julia package manager is v1.2.2 from 2026-09-15, that your labels are unique per scope so that accumulation matches what you intend, and that the LICENSE.md file in the repository root states terms you can accept, since GitHub classifies this licence as custom.

## FAQ

### How do I use the @timeit macro in TimerOutputs.jl?

Create a TimerOutput, pass it to the macro with a label, and print it at the end. The README shows `const to = TimerOutput()`, a call such as `@timeit to "sleep" sleep(0.02)`, and `show(to)` to print the formatted table of times, allocations and call counts.

### What is the difference between @timeit and @timeit_all?

The README describes @timeit_all as additionally timing every statement in a block or function body, which gives line level timing inside a section. @timeit on its own measures the labelled block as a whole, and @timeit returns the value being evaluated just like the Base @time macro.

### How do I stop TimerOutputs from recording a section?

The package offers disable_timer! and enable_timer! to switch the timer off and on again, and the @notimeit macro to disable it for a block and re-enable it afterwards if it was enabled before. You can also simply not instrument a section.

### Does TimerOutputs.jl track memory allocations?

Yes. The report has a Time half and an Allocations half, and the README says similar information is available for allocations, showing total allocations per section, the percentage of the total and the average per call, with the same bar columns and colouring.

## Sources

- [Issues](https://github.com/KristofferC/TimerOutputs.jl/issues)
- [KristofferC/TimerOutputs.jl on GitHub](https://github.com/KristofferC/TimerOutputs.jl)
- [README](https://github.com/KristofferC/TimerOutputs.jl/blob/master/README.md)
- [Releases](https://github.com/KristofferC/TimerOutputs.jl/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kristofferc-timeroutputs-jl
