Examples
========

The Python OTF2 bindings have mainly three use-cases:

- Reading an existing trace
- Writing  a new trace
- Rewriting a trace, which means to read a trace and write a trace, which depends on the input trace

This page gives a walk through of all these three cases.

Read an existing trace
----------------------

In order to read a trace with the Python interface, the first step is to import the necesarry
classes.

.. code-block:: python

    import otf2
    from otf2.events import *

In a :py:obj:`with` statement the trace file is opened with the :py:obj:`otf2.reader.open()` method.

.. code-block:: python

    def read_trace(trace_name="TestTrace/traces.otf2"):
        with otf2.reader.open(trace_name) as trace:

When this call succeeds, the trace is opened and all definitions are read. Definitions are stored
in the :py:obj:`definitions` member of the :py:obj:`otf2.reader.Reader`. So it is possible to
process specific types of definitions at once, e.g.:

.. code-block:: python

            print("Read {} string definitions".format(len(trace.definitions.strings)))

            for string in trace.definitions.strings:
                print("String definition with value '{}' in trace.".format(string))
    #

Reading events is as easy as reading definitions. The member :py:obj:`events` of the
:py:obj:`otf2.reader.Reader` is an iterable over the events of the trace. With an
:py:obj:`isinstance` check on an event, specific events can be handled only. All possible
events can be looked up in the module :py:mod:`otf2.events`.

.. code-block:: python

            for location, event in trace.events:
                if isinstance(event, Enter):
                    print("Encountered enter event into '{event.region.name}' on location {location} at {event.time}".format(location, event))
                elif isinstance(event, Leave):
                    print("Encountered leave event for '{event.region.name}' on location {location} at {event.time}".format(location, event))
                else:
                    print("Encountered event on location {} at {}".format(location, event.time))
    #

And finally the usual Python magic to create an executable script from the code.

.. code-block:: python

    if __name__ == "__main__":
        read_trace()


Write a new trace
-----------------

The second important use-case is the creation of a new trace from scratch.
Again, importing the required classes is the first thing.

.. code-block:: python

    import otf2
    from otf2.enums import Type

In order to write a new trace, some sort of time measurement or timestamp generation, called timer,
is needed. This example uses the built-in Python module :py:mod:`time`.

.. note::

    In OTF2 all timestamps are 64-Bit integers. As :py:func:`time.time()` returns a float, a
    conversion is needed.

.. code-block:: python

    import time

    TIMER_GRANULARITY = 1000000


    def t():
        return int(round(time.time() * TIMER_GRANULARITY))


A new trace is opened with a call to :py:func:`otf2.write.open`.

.. note::

    The :py:obj:`archive_name` is not the trace anchor file, but the name of the root folder for
    the whole trace. After the trace is written, this folder will contain the anchor file, the
    global definition file, and all local event and defintion files.

.. note::

    The :py:class:`otf2.writer.Writer` needs to know the resolution of the used timer, that is the
    number of ticks per second.

.. code-block:: python

    def create_trace(archive_name="NewTrace"):
        with otf2.writer.open(archive_name, timer_resolution=TIMER_GRANULARITY) as trace:

New definitions can be created using the member :py:obj:`definitions` of the
:py:obj:`otf2.writer.Writer`. The hazzle of the id management is hidden from the user.
Instead references to other objects are stored. For details see :ref:`def-handling`.

.. code-block:: python

            root_node = trace.definitions.system_tree_node("root node")
            system_tree_node = trace.definitions.system_tree_node("myHost", parent=root_node)
            trace.definitions.system_tree_node_property(system_tree_node, "color", value="black")
            trace.definitions.system_tree_node_property(system_tree_node, "rack #", value=42)

            location_group = trace.definitions.location_group("Master Process", system_tree_parent=system_tree_node)
    #

After having defined a few definitions, getting an event writer for a location is the next step.

.. code-block:: python

            writer = trace.event_writer("Main Thread", group=location_group)
    #

This writer can be used to write events.

.. note::

    Events need a timestamp as first argument.

.. code-block:: python

            function = trace.definitions.region("My Function")

            writer.enter(t(), function)
            writer.leave(t(), function)
    #

Using attributes, adding more information to an event is also possible.

.. code-block:: python

            attr = trace.definitions.attribute(name="StringTest", description="A test attribute", type=Type.STRING)

            writer.enter(t(), function, attributes={attr: "Hello"})
            writer.leave(t(), function, attributes={attr: "World"})
    #

There is also an short-hand to create simple metrics.

.. code-block:: python

            coffee_metric = trace.definitions.metric("Time since last coffee", unit="min")
            writer.metric(t(), coffee_metric, 72.0)
    #

And finally the usual Python magic to create an executable script from the code.

.. code-block:: python

    if __name__ == "__main__":
        create_trace()

Rewrite an existing trace
-------------------------

Last but not least, the third important use-case is the rewriting of an existing trace.
The important point in this case, is the possibility to manipulate a trace to all wishes.
Again, importing the required classes is the first thing.

.. code-block:: python

    import otf2
    from otf2.enums import Type

Then the existing trace is opened with an :py:class:`otf2.reader.Reader` object.

.. code-block:: python

    def rewrite_trace(old_anchor_path="old_trace/traces.otf2", new_archive_path="new_trace"):
        with otf2.reader.open(old_anchor_path) as trace_reader:

And the new trace is opened with an :py:class:`otf2.writer.Writer` instance.

.. note::

    The important bit here, is that the :py:obj:`definitions` member of the input trace is passed
    as argument to the :py:func:`otf2.writer.open()` call.

.. code-block:: python

            with otf2.writer.open(new_archive_path,
                                  definitions=trace_reader.definitions) as write_trace:
    #

Like in the reading case, the :py:obj:`events` property of the input trace is used, to read all
events.

.. code-block:: python

                for location, event in trace_reader.events:
    #

These events can be manipulated by any means, e.g., the following code normalizes all
timestamps of the trace, such that the first event occures at time point 0.

.. code-block:: python

                    event.time -= trace_reader.definitions.clock_properties.global_offset
    #

Then the manipulated event needs to be written into the new trace. Therefore an event writer is
needed.

.. code-block:: python

                    writer = write_trace.event_writer_from_location(location)
                    writer(event)
    #


And finally the usual Python magic to create an executable script from the code.

.. code-block:: python

    if __name__ == "__main__":
        rewrite_trace()
