Part 15: Got the time?
Using an open source tracing library, called opencensus-python, can help to break down the performance of the different parts of the simulation. Getting started is pretty easy to do and this post will describe that.
This series of posts is dedicated to building tools that support a simulation of simplified D&D gameplay. To implement the simulation I’ll be using git, Python, PostgreSQL, Elastic, and Docker. The idea behind these blog posts is to document my progress through this project and interesting bits I find along the way. If you’d like to start from the beginning, have a look at part 1. It talks about where this idea came from, and what I’d like to accomplish. The github repository for this project holds the latest version of the files mentioned in these posts. To follow along, part 6 describes how to set up the environment.
As each part of the simulation gets added in, it’s helpful to see how much time is being spent in each. Here are a couple of tools to visualize that.
Opencensus-python is a library that can be used to export application metrics and distributed traces. Spans are defined in the code and then the data is exported to a backend data store and presentation layer. “A Span represents a single operation within a trace. Spans can be nested to form a trace tree. Often, a trace contains a root span that describes the end-to-end latency and, optionally, one or more sub-spans for its sub-operations.”
Exported traces are consumed by Jaeger, which is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and troubleshooting microservices-based distributed systems. There is an “all-in-one” docker image that can be easily implemented.
Picking up on the game of Tag that was implemented in part 14, I’d like to see how long an encounter lasts and what it was doing all that time. To do that, a new docker container definition giving new traces a place to be stored is added to the docker-compose.yml file.
jaeger: image: jaegertracing/all-in-one:latest container_name: jaeger ports: - "5775:5775/udp" - "6831:6831/udp" - "6832:6832/udp" - "5778:5778" - "16686:16686" - "14268:14268" - "9411:9411"
Once that container is started up, changing the Python code to feed it traces becomes pretty easy. Trace_it.py shows how to make a simple class that defines a trace
from opencensus.trace import samplers from opencensus.trace.tracer import Tracer from opencensus.ext.jaeger.trace_exporter import JaegerExporter class Trace_it(object): def __init__(self, trace_name ): sampler = samplers.AlwaysOnSampler() je = JaegerExporter( service_name=trace_name, host_name="localhost", agent_port=6831, endpoint="/api/traces") self.tracer = Tracer(sampler=sampler, exporter=je)
The Trace_it class, when given a trace_name, will start a trace that samples all executions and sends the traces to the port where the Jaeger container is listening.
Here’s a look at the gen_encounter.py script from the last post with the trace and spans defined.
from InvokePSQL import InvokePSQL from PlayerCharacter import PlayerCharacter from Foe import Foe from Encounter import Encounter from Trace_it import Trace_it t = Trace_it("encounter") with t.tracer.span(name='root'): db = InvokePSQL() Heroes = [] Opponents = [] debug_ind = 0 with t.tracer.span(name='heroes_setup'): with t.tracer.span(name='hero_1_setup'): Heroes.append(PlayerCharacter(db, debug_ind=debug_ind)) with t.tracer.span(name='hero_2_setup'): Heroes.append(PlayerCharacter(db, debug_ind=debug_ind)) with t.tracer.span(name='opponents_setup'): with t.tracer.span(name='opponent_1_setup'): Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind)) with t.tracer.span(name='opponent_2_setup'): Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind)) with t.tracer.span(name='opponent_3_setup'): Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind)) print(f"For the Heroes:") for Hero in Heroes: print(f" {Hero.get_name()}") print(f"Against:") for Opponent in Opponents: print(f" {Opponent.get_name()}") e1 = Encounter(Heroes, Opponents, debug_ind=0, tracer=t.tracer) print(f"The winning party was: {e1.winning_list_name} in {e1.round} rounds.") print(f"The surviving {e1.winning_list_name} members:") for i in range(len(e1.winning_list)): if e1.winning_list[i].alive: print(f'{e1.winning_list[i].get_name()}')
Defining a simple span consists of giving a label for the span and then indenting the code that will be timed for that span. For a simple example, the Encounter class has also been updated, adding span definitions for some of the major methods.
$ python gen_encounter.py For the Heroes: Firenze Tordek Balderk Against: Skeleton Skeleton Skeleton The winning party was: Heroes in 4 rounds. The surviving Heroes members: Firenze Tordek Balderk
Running the script returns the same type of output as earlier executions, but browsing to http://localhost:16686/search and clicking on “Find Traces” button at the bottom of the left frame will show traces that have been gathered since starting the docker container.
Clicking on the “57 Spans” label (your number may vary) will switch to the trace timeline.
This view will show how long each span of the encounter took. As more encounters are run at once and their complexity increases, it will be interesting to see how the execution times break down.
With some tracing in place, the next step is to up the complexity of the game and see what happens. Next time, the melee and ranged weapon attacks will make their way into this mix. Until then, go Play!