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.
import otf2
from otf2.events import *
In a with
statement the trace file is opened with the otf2.reader.open()
method.
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 definitions
member of the otf2.reader.Reader
. So it is possible to
process specific types of definitions at once, e.g.:
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 events
of the
otf2.reader.Reader
is an iterable over the events of the trace. With an
isinstance
check on an event, specific events can be handled only. All possible
events can be looked up in the module otf2.events
.
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.
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.
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 time
.
Note
In OTF2 all timestamps are 64-Bit integers. As time.time()
returns a float, a
conversion is needed.
import time
TIMER_GRANULARITY = 1000000
def t():
return int(round(time.time() * TIMER_GRANULARITY))
A new trace is opened with a call to otf2.write.open()
.
Note
The 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 otf2.writer.Writer
needs to know the resolution of the used timer, that is the
number of ticks per second.
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 definitions
of the
otf2.writer.Writer
. The hazzle of the id management is hidden from the user.
Instead references to other objects are stored. For details see Creating definitions.
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("Initial Process", system_tree_parent=system_tree_node)
#
After having defined a few definitions, getting an event writer for a location is the next step.
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.
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.
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.
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.
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.
import otf2
from otf2.enums import Type
Then the existing trace is opened with an otf2.reader.Reader
object.
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 otf2.writer.Writer
instance.
Note
The important bit here, is that the definitions
member of the input trace is passed
as argument to the otf2.writer.open()
call.
with otf2.writer.open(new_archive_path,
definitions=trace_reader.definitions) as write_trace:
#
Like in the reading case, the events
property of the input trace is used, to read all
events.
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.
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.
writer = write_trace.event_writer_from_location(location)
writer(event)
#
And finally the usual Python magic to create an executable script from the code.
if __name__ == "__main__":
rewrite_trace()