# Python Bindings

The timemory python interface is generated via the PyBind11 library. The combination of these two template-based
libraries provides a feature-rich interface which combines the flexibility of python with the performance of C++.

## Description

The python interface provides several pre-configured scoped components in bundles and profiler sub-packages which can be used as decorators or context-managers. The timemory settings be controlled either directly via the `settings` sub-package or by using environment variables. The `component` subpackage contains the individual components that can be used for custom instrumentation. Further, the `hardware_counters` sub-package provides API interface to accessing hardware counters (requires PAPI and/or CUDA support). The `mpi` subpackage provides bindings to timemory's MPI support. The generic data `plotting` (such as plotting instrumentation graphs) and `roofline` plotting are available in subsequent sub-packages.

## Contents

```bash
$ python -c "import timemory; help(timemory)"

Help on package timemory:

NAME
    timemory

PACKAGE CONTENTS
    api (package)
    bundle (package)
    common
    component (package)
    ert (package)
    hardware_counters (package)
    libpytimemory
    line_profiler (package)
    mpi (package)
    mpi_support (package)
    notebook (package)
    options
    plotting (package)
    profiler (package)
    region (package)
    roofline (package)
    settings (package)
    signals
    test (package)
    trace (package)
    units
    util (package)

SUBMODULES
    scope

CLASSES
    pybind11_builtins.pybind11_object(builtins.object)
        timemory.libpytimemory.auto_timer
        timemory.libpytimemory.component_bundle
        timemory.libpytimemory.manager
        timemory.libpytimemory.rss_usage
        timemory.libpytimemory.settings
        timemory.libpytimemory.timer

    class auto_timer(...)
    class component_bundle(...)
    class manager(...)
    class rss_usage(...)
    class settings(...)
    class timer(...)

FUNCTIONS
    FILE = file(back=2, only_basename=True, use_dirname=False, noquotes=True)
        Returns the file name

    FUNC = func(back=2)
        Returns the function name

    LINE = line(back=1)
        Returns the line number

    disable(...)
        disable() -> None

        Disable timemory

    disable_signal_detection(...)
        disable_signal_detection() -> None

        Enable signal detection

    enable(...)
        enable() -> None

        Enable timemory

    enable_signal_detection(...)
        enable_signal_detection(signal_list: list = []) -> None

        Enable signal detection

    enabled(...)
        enabled() -> bool

        Return if timemory is enabled or disabled

    finalize(...)
        finalize() -> None

        Finalize timemory (generate output) -- important to call if using MPI

    has_mpi_support(...)
        has_mpi_support() -> bool

        Return if the timemory library has MPI support

    init = initialize(...)
        initialize(argv: list = [], prefix: str = 'timemory-', suffix: str = '-output') -> None

        Initialize timemory

    initialize(...)
        initialize(argv: list = [], prefix: str = 'timemory-', suffix: str = '-output') -> None

        Initialize timemory

    is_enabled(...)
        is_enabled() -> bool

        Return if timemory is enabled or disabled

    report(...)
        report(filename: str = '') -> None

        Print the data

    set_rusage_children(...)
        set_rusage_children() -> None

        Set the rusage to record child processes

    set_rusage_self(...)
        set_rusage_self() -> None

        Set the rusage to record child processes

    timemory_finalize(...)
        timemory_finalize() -> None

        Finalize timemory (generate output) -- important to call if using MPI

    timemory_init(...)
        timemory_init(argv: list = [], prefix: str = 'timemory-', suffix: str = '-output') -> None

        Initialize timemory

    toggle(...)
        toggle(on: bool = True) -> None

        Enable/disable timemory

DATA
    __all__ = ['version_info', 'build_info', 'version', 'libpytimemory', '...
    __copyright__ = 'Copyright 2020, The Regents of the University of Cali...
    __email__ = 'jrmadsen@lbl.gov'
    __license__ = 'MIT'
    __maintainer__ = 'Jonathan Madsen'
    __status__ = 'Development'
    __warningregistry__ = {'version': 0, ("the imp module is deprecated in...
    build_info = {'build_type': 'RelWithDebInfo', 'compiler': '/opt/local/...
    version = '3.2.0'
    version_info = (3, 2, 0)

VERSION
    3.2.0

AUTHOR
    Jonathan Madsen

CREDITS
    ['Jonathan Madsen']

FILE
    /.../timemory/__init__.py
```

## Initialization and Finalization

- `timemory.init(...)` is called when the package is imported
  - Although it is maybe potentially usually useful to initialize with the filename
- It is highly recommended to call `timemory.finalize()` explicitly before the application terminates

```python
import timemory

# ... use timemory components, decorators, bundles, etc. ...

if __name__ == "__main__":
    # optional
    timemory.init([__file__])

    # ... etc. ...

    timemory.finalize()
```

## Settings

Timemory settings can be directly modified in Python or may be configured via enviroment variables.

### Direct Modification

```python
import timemory

# set verbose output to 1
timemory.settings.verbose = 1
# disable timemory debug prints
timemory.settings.debug = False
# set output data format output to json
timemory.settings.json_output = True
# disable mpi_thread mode
timemory.settings.mpi_thread  = False
# enable timemory dart output
timemory.settings.dart_output = True
timemory.settings.dart_count = 1
# disable timemory banner
timemory.settings.banner = False
```

### Environment Variables

```python
import os

# enable timemory flat profile mode
os.environ["TIMEMORY_FLAT_PROFILE"] = "ON"
# enable timemory timeline profile mode
os.environ["TIMEMORY_TIMELINE_PROFILE"] = "ON"
# parse the environment variable updates within timemory
timemory.settings.parse()
```

## Decorators

```python
import timemory
import timemory.util.marker as marker, auto_timer

@marker(["trip_count", "peak_rss"])
def fibonacci_instrumented(n):
    return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)

@auto_timer()
def fibonacci_timed(n):
    return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)
```

## Context Managers

```python
import timemory
from timemory.util import marker

def main():
    with marker(["wall_clock", "peak_rss"], flat=False, timeline=False):
        ans = fibonacci(n=3)
```

## Function Profiler

### Profiler Example

```python
#!/usr/bin/env python

import numpy as np
import timemory

from timemory.profiler import Profiler
from timemory.profiler import Config as ProfilerConfig

def eval_func(arr, tol):
    """Dummy tolerance-checking function"""
    max = np.max(arr)
    avg = np.mean(arr)
    return True if avg < tol and max < tol else False

@Profiler(["wall_clock", "cpu_clock"])
def profile_func(arr, tol):
    """Dummy function for profiling"""
    while not eval_func(arr, tol):
        arr = arr - np.power(arr, 3)

if __name__ == "__main__":

    ProfilerConfig.only_filenames = [__file__, "_methods.py"]
    profile_func(np.random.rand(100, 100), 1.0e-2)

    timemory.finalize()
```

### Profiler Output

```console
$ python ./doc-profiler.py

[cpu]|0> Outputting 'timemory-doc-profiler-output/cpu.flamegraph.json'...
[cpu]|0> Outputting 'timemory-doc-profiler-output/cpu.tree.json'...
[cpu]|0> Outputting 'timemory-doc-profiler-output/cpu.json'...
[cpu]|0> Outputting 'timemory-doc-profiler-output/cpu.txt'...

|------------------------------------------------------------------------------------------------------------------------------------------|
|                                            TOTAL CPU TIME SPENT IN BOTH USER- AND KERNEL-MODE                                            |
|------------------------------------------------------------------------------------------------------------------------------------------|
|                     LABEL                      | COUNT  | DEPTH  | METRIC | UNITS  |  SUM   | MEAN   |  MIN   |  MAX   | STDDEV | % SELF |
|------------------------------------------------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| >>> profile_func/doc-profiler.py:15            |      1 |      0 | cpu    | sec    |  1.610 |  1.610 |  1.610 |  1.610 |  0.000 |   57.1 |
| >>> |_eval_func/doc-profiler.py:9              |   4994 |      1 | cpu    | sec    |  0.690 |  0.000 |  0.000 |  0.000 |  0.001 |   50.7 |
| >>>   |__mean/_methods.py:134                  |   4994 |      2 | cpu    | sec    |  0.340 |  0.000 |  0.000 |  0.000 |  0.001 |   55.9 |
| >>>     |__count_reduce_items/_methods.py:50   |   4994 |      3 | cpu    | sec    |  0.040 |  0.000 |  0.000 |  0.000 |  0.000 |   75.0 |
| >>>       |__count_reduce_items/_methods.py:50 |   4994 |      4 | cpu    | sec    |  0.010 |  0.000 |  0.000 |  0.000 |  0.000 |  100.0 |
| >>>     |__mean/_methods.py:134                |  24970 |      3 | cpu    | sec    |  0.110 |  0.000 |  0.000 |  0.000 |  0.000 |  100.0 |
|------------------------------------------------------------------------------------------------------------------------------------------|

[wall]|0> Outputting 'timemory-doc-profiler-output/wall.flamegraph.json'...
[wall]|0> Outputting 'timemory-doc-profiler-output/wall.tree.json'...
[wall]|0> Outputting 'timemory-doc-profiler-output/wall.json'...
[wall]|0> Outputting 'timemory-doc-profiler-output/wall.txt'...

|------------------------------------------------------------------------------------------------------------------------------------------|
|                                                 REAL-CLOCK TIMER (I.E. WALL-CLOCK TIMER)                                                 |
|------------------------------------------------------------------------------------------------------------------------------------------|
|                     LABEL                      | COUNT  | DEPTH  | METRIC | UNITS  |  SUM   | MEAN   |  MIN   |  MAX   | STDDEV | % SELF |
|------------------------------------------------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| >>> profile_func/doc-profiler.py:15            |      1 |      0 | wall   | sec    |  1.614 |  1.614 |  1.614 |  1.614 |  0.000 |   56.9 |
| >>> |_eval_func/doc-profiler.py:9              |   4994 |      1 | wall   | sec    |  0.697 |  0.000 |  0.000 |  0.000 |  0.000 |   39.0 |
| >>>   |__mean/_methods.py:134                  |   4994 |      2 | wall   | sec    |  0.425 |  0.000 |  0.000 |  0.000 |  0.000 |   51.5 |
| >>>     |__count_reduce_items/_methods.py:50   |   4994 |      3 | wall   | sec    |  0.084 |  0.000 |  0.000 |  0.000 |  0.000 |   76.2 |
| >>>       |__count_reduce_items/_methods.py:50 |   4994 |      4 | wall   | sec    |  0.020 |  0.000 |  0.000 |  0.000 |  0.000 |  100.0 |
| >>>     |__mean/_methods.py:134                |  24970 |      3 | wall   | sec    |  0.122 |  0.000 |  0.000 |  0.000 |  0.000 |  100.0 |
|------------------------------------------------------------------------------------------------------------------------------------------|
```

## Tracing Profiler

### Tracer Example

```python
#!/usr/bin/env python

import numpy as np
import timemory

from timemory.trace import Tracer
from timemory.trace import Config as TracerConfig

def eval_func(arr, tol):
    """Dummy tolerance-checking function"""
    max = np.max(arr)
    avg = np.mean(arr)
    return True if avg < tol and max < tol else False

@Tracer(["wall_clock", "cpu_clock"])
def trace_func(arr, tol):
    """Dummy function for tracing"""
    while not eval_func(arr, tol):
        arr = arr - np.power(arr, 3)

if __name__ == "__main__":

    TracerConfig.only_filenames = [__file__, "_methods.py"]
    trace_func(np.random.rand(100, 100), 1.0e-2)

    timemory.finalize()
```

### Tracer Output

```console
$ python ./doc-tracer.py

[cpu]|0> Outputting 'timemory-doc-tracer-output/cpu.flamegraph.json'...
[cpu]|0> Outputting 'timemory-doc-tracer-output/cpu.tree.json'...
[cpu]|0> Outputting 'timemory-doc-tracer-output/cpu.json'...
[cpu]|0> Outputting 'timemory-doc-tracer-output/cpu.txt'...

# ... report for cpu-clock ...

[wall]|0> Outputting 'timemory-doc-tracer-output/wall.flamegraph.json'...
[wall]|0> Outputting 'timemory-doc-tracer-output/wall.json'...
[wall]|0> Outputting 'timemory-doc-tracer-output/wall.tree.json'...
[wall]|0> Outputting 'timemory-doc-tracer-output/wall.txt'...

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                                                                                    REAL-CLOCK TIMER (I.E. WALL-CLOCK TIMER)                                                                                    |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                                                        LABEL                                                         | COUNT  | DEPTH  | METRIC | UNITS  |  SUM   | MEAN   |  MIN   |  MAX   | STDDEV | % SELF |
|----------------------------------------------------------------------------------------------------------------------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| >>> @Tracer(["wall_clock", "cpu_clock"])                                               [trace_func/doc-tracer.py:15] |        |        |        |        |        |        |        |        |        |        |
| >>> def trace_func(arr, tol):                                                          [trace_func/doc-tracer.py:16] |      1 |      0 | wall   | sec    |  2.044 |  2.044 |  2.044 |  2.044 |  0.000 |  100.0 |
| >>>     while not eval_func(arr, tol):                                                 [trace_func/doc-tracer.py:18] |   4994 |      0 | wall   | sec    |  0.027 |  0.000 |  0.027 |  0.027 |  0.000 |  100.0 |
| >>>         arr = arr - np.power(arr, 3)                                               [trace_func/doc-tracer.py:19] |   4993 |      0 | wall   | sec    |  0.894 |  0.000 |  0.894 |  0.894 |  0.000 |  100.0 |
| >>> def eval_func(arr, tol):                                                           [trace_func/doc-tracer.py:09] |   4994 |      0 | wall   | sec    |  1.080 |  0.000 |  1.080 |  1.080 |  0.000 |  100.0 |
| >>>     max = np.max(arr)                                                              [trace_func/doc-tracer.py:11] |   4994 |      0 | wall   | sec    |  0.023 |  0.000 |  0.023 |  0.023 |  0.000 |  100.0 |
| >>>     avg = np.mean(arr)                                                             [trace_func/doc-tracer.py:12] |   4994 |      0 | wall   | sec    |  0.027 |  0.000 |  0.027 |  0.027 |  0.000 |  100.0 |
| >>>     return True if avg < tol and max < tol else False                              [trace_func/doc-tracer.py:13] |   4994 |      0 | wall   | sec    |  0.022 |  0.000 |  0.022 |  0.022 |  0.000 |  100.0 |
| >>> def _mean(a, axis=None, dtype=None, out=None, keepdims=False):                           [_mean/_methods.py:134] |   4994 |      0 | wall   | sec    |  0.799 |  0.000 |  0.799 |  0.799 |  0.000 |  100.0 |
| >>>     arr = asanyarray(a)                                                                  [_mean/_methods.py:135] |   4994 |      0 | wall   | sec    |  0.023 |  0.000 |  0.023 |  0.023 |  0.000 |  100.0 |
| >>>     is_float16_result = False                                                            [_mean/_methods.py:137] |   4994 |      0 | wall   | sec    |  0.022 |  0.000 |  0.022 |  0.022 |  0.000 |  100.0 |
| >>>     rcount = _count_reduce_items(arr, axis)                                              [_mean/_methods.py:138] |   4994 |      0 | wall   | sec    |  0.022 |  0.000 |  0.022 |  0.022 |  0.000 |  100.0 |
| >>>     if rcount == 0:                                                                      [_mean/_methods.py:140] |   4994 |      0 | wall   | sec    |  0.021 |  0.000 |  0.021 |  0.021 |  0.000 |  100.0 |
| >>>         warnings.warn("Mean of empty slice.", RuntimeWarning, stacklevel=2)              [_mean/_methods.py:141] |        |        |        |        |        |        |        |        |        |        |
| >>>     if dtype is None:                                                                    [_mean/_methods.py:144] |   4994 |      0 | wall   | sec    |  0.021 |  0.000 |  0.021 |  0.021 |  0.000 |  100.0 |
| >>>         if issubclass(arr.dtype.type, (nt.integer, nt.bool_)):                           [_mean/_methods.py:145] |   4994 |      0 | wall   | sec    |  0.025 |  0.000 |  0.025 |  0.025 |  0.000 |  100.0 |
| >>>             dtype = mu.dtype('f8')                                                       [_mean/_methods.py:146] |        |        |        |        |        |        |        |        |        |        |
| >>>         elif issubclass(arr.dtype.type, nt.float16):                                     [_mean/_methods.py:147] |   4994 |      0 | wall   | sec    |  0.023 |  0.000 |  0.023 |  0.023 |  0.000 |  100.0 |
| >>>             dtype = mu.dtype('f4')                                                       [_mean/_methods.py:148] |        |        |        |        |        |        |        |        |        |        |
| >>>             is_float16_result = True                                                     [_mean/_methods.py:149] |        |        |        |        |        |        |        |        |        |        |
| >>>     ret = umr_sum(arr, axis, dtype, out, keepdims)                                       [_mean/_methods.py:151] |   4994 |      0 | wall   | sec    |  0.056 |  0.000 |  0.056 |  0.056 |  0.000 |  100.0 |
| >>>     if isinstance(ret, mu.ndarray):                                                      [_mean/_methods.py:152] |   4994 |      0 | wall   | sec    |  0.026 |  0.000 |  0.026 |  0.026 |  0.000 |  100.0 |
| >>>         ret = um.true_divide(                                                            [_mean/_methods.py:153] |        |        |        |        |        |        |        |        |        |        |
| >>>                 ret, rcount, out=ret, casting='unsafe', subok=False)                     [_mean/_methods.py:154] |        |        |        |        |        |        |        |        |        |        |
| >>>         if is_float16_result and out is None:                                            [_mean/_methods.py:155] |        |        |        |        |        |        |        |        |        |        |
| >>>             ret = arr.dtype.type(ret)                                                    [_mean/_methods.py:156] |        |        |        |        |        |        |        |        |        |        |
| >>>     elif hasattr(ret, 'dtype'):                                                          [_mean/_methods.py:157] |   4994 |      0 | wall   | sec    |  0.023 |  0.000 |  0.023 |  0.023 |  0.000 |  100.0 |
| >>>         if is_float16_result:                                                            [_mean/_methods.py:158] |   4994 |      0 | wall   | sec    |  0.021 |  0.000 |  0.021 |  0.021 |  0.000 |  100.0 |
| >>>             ret = arr.dtype.type(ret / rcount)                                           [_mean/_methods.py:159] |        |        |        |        |        |        |        |        |        |        |
| >>>         else:                                                                            [_mean/_methods.py:160] |        |        |        |        |        |        |        |        |        |        |
| >>>             ret = ret.dtype.type(ret / rcount)                                           [_mean/_methods.py:161] |   4994 |      0 | wall   | sec    |  0.028 |  0.000 |  0.028 |  0.028 |  0.000 |  100.0 |
| >>>     else:                                                                                [_mean/_methods.py:162] |        |        |        |        |        |        |        |        |        |        |
| >>>         ret = ret / rcount                                                               [_mean/_methods.py:163] |        |        |        |        |        |        |        |        |        |        |
| >>>     return ret                                                                           [_mean/_methods.py:165] |   4994 |      0 | wall   | sec    |  0.021 |  0.000 |  0.021 |  0.021 |  0.000 |  100.0 |
| >>> def _count_reduce_items(arr, axis):                                                      [_mean/_methods.py:050] |   4994 |      0 | wall   | sec    |  0.304 |  0.000 |  0.304 |  0.304 |  0.000 |  100.0 |
| >>>     if axis is None:                                                                     [_mean/_methods.py:051] |   4994 |      0 | wall   | sec    |  0.021 |  0.000 |  0.021 |  0.021 |  0.000 |  100.0 |
| >>>         axis = tuple(range(arr.ndim))                                                    [_mean/_methods.py:052] |   4994 |      0 | wall   | sec    |  0.027 |  0.000 |  0.027 |  0.027 |  0.000 |  100.0 |
| >>>     if not isinstance(axis, tuple):                                                      [_mean/_methods.py:053] |   4994 |      0 | wall   | sec    |  0.022 |  0.000 |  0.022 |  0.022 |  0.000 |  100.0 |
| >>>         axis = (axis,)                                                                   [_mean/_methods.py:054] |        |        |        |        |        |        |        |        |        |        |
| >>>     items = 1                                                                            [_mean/_methods.py:055] |   4994 |      0 | wall   | sec    |  0.020 |  0.000 |  0.020 |  0.020 |  0.000 |  100.0 |
| >>>     for ax in axis:                                                                      [_mean/_methods.py:056] |  14982 |      0 | wall   | sec    |  0.060 |  0.000 |  0.060 |  0.060 |  0.000 |  100.0 |
| >>>         items *= arr.shape[ax]                                                           [_mean/_methods.py:057] |   9988 |      0 | wall   | sec    |  0.042 |  0.000 |  0.042 |  0.042 |  0.000 |  100.0 |
| >>>     return items                                                                         [_mean/_methods.py:058] |   4994 |      0 | wall   | sec    |  0.020 |  0.000 |  0.020 |  0.020 |  0.000 |  100.0 |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
```

## Library Bindings

This is a sample of the python bindings to the timemory library interface available to C, C++, and Fortran.
Using these libraries calls in python code which calls C/C++/Fortran code also instrumented with
timemory will enable the same set of performance analysis components to span all the languages.

```python
import timemory

# initialize
timemory.trace.init("wall_clock", False, "tracing")

# insert a trace
timemory.trace.push("consume_time")
ans = work_milliseconds(1000)
timemory.trace.pop("consume_time")

# insert another trace point
timemory.trace.push("sleeping")
ans = sleep_milliseconds(1000)
timemory.trace.pop("sleeping")

# insert a region
timemory.region.push("work_region")
for i in range(10):
    ans = work_milliseconds(1000)
timemory.region.pop("work_region")

# finalize
timemory.trace.finalize()
```

## Individual Components

Each individual component is available as a stand-alone Python class.
The intention of making these individual components available is such that
tools can use these types to create custom tools. Thus, unless `push()` and `pop()`
are called on these objects, instances of these classes
will not store any data in the timemory call-graph (when applicable).

```python
import time
import timemory
from  timemory.component import WallClock

# instantiate wall clock component
wc = WallClock("wall")
# start the clock
wc.start()
# sleep
time.sleep(2)
# stop the clock
wc.stop()
# get data
result = wc.get()
#finalize timemory
timemory.finalize()
```

## Storage

Timemory allows direct access to the aforementioned call-graph storage.
The call-graph storage is the accumulated data for components which occur through
push and pop operations. These push and pop operations are implicitly
performed via decorators, context-managers, the function profiler, and the
trace profiler and may be explicitly performed when using individual components.
Internally, timemory stores call-graph entries in a minimal representation
and then packs the results together with more information when the data is requested.
Thus, one should not expect manipulation of the data provided by these routines to
be propagated to timemory internally.

Call-graph storage is available in two different layouts. The first represents
the call-graph results for each process as a one-dimensional array where the hierarchy
is represented through indentation of the string identifiers, a depth field, and
an array of hash values. The second represents the call-graph entries as a nested
data structure where each entry has a value and list of children.

### Storage Example

```python
#!/usr/bin/env python

import time
import timemory
from timemory.util import marker
from timemory.component import WallClock
from timemory.storage import WallClockStorage

@marker(["wall_clock"])
def foo():
    """Generate some data for timemory"""
    time.sleep(1)

    wc = WallClock("bar")
    for i in range(0, 10):
        # push every even iteration
        if i % 2 == 0:
            wc.push()
        wc.start()
        time.sleep(0.1 * (i + 1))
        wc.stop()
        # pop every odd iteration
        if i % 2 == 1:
            wc.pop()


def print_result():
    """
    Print the call-graph storage via the flat layout
    """

    print("\n#{}#\n# Storage Result".format("-" * 40))

    indent = "  "
    for itr in WallClockStorage.get():
        print("#{}#".format("-" * 40))
        print("{}{:20} : {}".format(indent, "Thread id", itr.tid()))
        print("{}{:20} : {}".format(indent, "Process id", itr.pid()))
        print("{}{:20} : {}".format(indent, "Depth", itr.depth()))
        print("{}{:20} : {}".format(indent, "Hash", itr.hash()))
        print("{}{:20} : {}".format(indent, "Rolling hash", itr.rolling_hash()))
        print("{}{:20} : {}".format(indent, "Prefix", itr.prefix()))
        print("{}{:20} : {}".format(indent, "Hierarchy", itr.hierarchy()))
        print("{}{:20} : {}".format(indent, "Data object", itr.data()))
        print("{}{:20} : {}".format(indent, "Statistics", itr.stats()))


def print_tree(data=None, depth=0):
    """
    Print the call-graph storage via the nested layout
    """

    if data is None:
        print("\n#{}#\n# Storage Tree".format("-" * 40))
        data = WallClockStorage.get_tree()

    def print_value(itr, indent):
        print("{}{:20} : {}".format(indent, "Thread id", itr.tid()))
        print("{}{:20} : {}".format(indent, "Process id", itr.pid()))
        print("{}{:20} : {}".format(indent, "Depth", itr.depth()))
        print("{}{:20} : {}".format(indent, "Hash", itr.hash()))
        print("{}{:20} : {}".format(indent, "Inclusive data", itr.inclusive().data()))
        print("{}{:20} : {}".format(indent, "Inclusive stat", itr.inclusive().stats()))
        print("{}{:20} : {}".format(indent, "Exclusive data", itr.exclusive().data()))
        print("{}{:20} : {}".format(indent, "Exclusive stat", itr.exclusive().stats()))

    indent = "  " * depth
    for itr in data:
        print("{}#{}#".format(indent, "-" * 40))
        print_value(itr.value(), indent)
        print_tree(itr.children(), depth + 1)


if __name__ == "__main__":
    # disable automatic output
    timemory.settings.auto_output = False

    foo()
    print_result()
    print_tree()
```

### Storage Output

```console

#----------------------------------------#
# Storage Result
#----------------------------------------#
  Thread id            : 0
  Process id           : 4385
  Depth                : 0
  Hash                 : 9631199822919835227
  Rolling hash         : 9631199822919835227
  Prefix               : >>> foo
  Hierarchy            : [9631199822919835227]
  Data object          :    6.534 sec wall
  Statistics           : [sum: 6.53361] [min: 6.53361] [max: 6.53361] [sqr: 42.6881] [count: 1]
#----------------------------------------#
  Thread id            : 0
  Process id           : 4385
  Depth                : 1
  Hash                 : 11474628671133349553
  Rolling hash         : 2659084420343633164
  Prefix               : >>> |_bar
  Hierarchy            : [9631199822919835227, 11474628671133349553]
  Data object          :    5.531 sec wall
  Statistics           : [sum: 5.53115] [min: 0.307581] [max: 0.307581] [sqr: 7.71154] [count: 5]

#----------------------------------------#
# Storage Tree
#----------------------------------------#
Thread id            : {0}
Process id           : {4385}
Depth                : -1
Hash                 : 0
Prefix               : unknown-hash=0
Inclusive data       :    0.000 sec wall
Inclusive stat       : [sum: 0] [min: 0] [max: 0] [sqr: 0] [count: 0]
Exclusive data       :   -6.534 sec wall
Exclusive stat       : [sum: 0] [min: 0] [max: 0] [sqr: 0] [count: 0]
  #----------------------------------------#
  Thread id            : {0}
  Process id           : {4385}
  Depth                : 0
  Hash                 : 9631199822919835227
  Prefix               : foo
  Inclusive data       :    6.534 sec wall
  Inclusive stat       : [sum: 6.53361] [min: 6.53361] [max: 6.53361] [sqr: 42.6881] [count: 1]
  Exclusive data       :    1.002 sec wall
  Exclusive stat       : [sum: 1.00246] [min: 6.53361] [max: 6.53361] [sqr: 34.9765] [count: 1]
    #----------------------------------------#
    Thread id            : {0}
    Process id           : {4385}
    Depth                : 1
    Hash                 : 11474628671133349553
    Prefix               : bar
    Inclusive data       :    5.531 sec wall
    Inclusive stat       : [sum: 5.53115] [min: 0.307581] [max: 0.307581] [sqr: 7.71154] [count: 5]
    Exclusive data       :    5.531 sec wall
    Exclusive stat       : [sum: 5.53115] [min: 0.307581] [max: 0.307581] [sqr: 7.71154] [count: 5]
```

Note the first entry of storage tree has a negative depth and hash of zero. Nodes such of these
are "dummy" nodes which timemory keeps internally as bookmarks for root nodes and thread-forks
(parent call-graph location when a child thread was initialized or returned to "sea-level").
These entries can be discarded and a member function `is_dummy()` exists to help identify these nodes, e.g.:

```python
    for itr in data:
        if itr.value().is_dummy():
            print_tree(itr.children(), depth)
        else:
            print("{}#{}#".format(indent, "-" * 40))
            print_value(itr.value(), indent)
            print_tree(itr.children(), depth + 1)
```

Future versions of timemory may eliminate these reporting these nodes.
