Profiling Mistral

What Is Profiling?

Profiling is a procedure for gathering runtime statistics about certain code snippets like:

  • The maximum run time

  • The minimum run time

  • The average run time

  • The number of runs

Such info is a key to understanding performance bottlenecks residing in a system. Having these metrics, we can focus on places in code that slow down the system most and come up with optimisations to improve them.

A typical code snippet eligible for gathering this kind of information is a function or a method since, most popular engineering techniques encourage developers to decompose code into functions/methods representing well defined parts of program logic. However, any arbitrary piece of code may be a target for measuring.

‘osprofiler’ Project

osprofiler is a project created within the OpenStack ecosystem to do profiling. The paragraphs below explain how Mistral uses ‘osprofiler’ for profiling. The central concept of ‘osprofiler’ is a profile trace. A developer can mark code snippets with profiler traces and ‘osprofiler’ will be tracking them. In general, ‘osprofiler’ allows cross-service profiling, that is, tracking a chain of calls that belong to different RESTful services but related with the same user request. However, this guide doesn’t cover this more complex use case and focus on profiling within just one service, Mistral.

Profiler Traces

The most common way to create a profiler trace in the code is adding a special ‘’@trace

from osprofiler import profiler


class DefaultEngine(base.Engine):
    ...

    @profiler.trace('engine-on-action-complete', hide_args=True)
    def on_action_complete(self, action_ex_id, result, wf_action=False,
                           async_=False):
        with db_api.transaction():
            if wf_action:
                action_ex = db_api.get_workflow_execution(action_ex_id)

                if result is None:
                    result = ml_actions.Result(data=action_ex.output)
            else:
                action_ex = db_api.get_action_execution(action_ex_id)

            action_handler.on_action_complete(action_ex, result)

            return action_ex.get_clone()

In this example, we applied a special decorator to a method that adds a profiling trace. The most important argument of the decorator is a trace name. Its value is ‘engine-on-action-complete’ in our case. The second argument ‘hide_args’ defines whether ‘osprofiler’ needs to pass method argument values down to other layers. More specifically, there’s a notion metrics collector in ‘osprofiler’ that accumulates info about traces in any desirable form, it depends on a particular implementation. This topic though is out of the scope of this document. For our purposes, it’s better to set this argument to True which will not lead to loosing performance on processing additional data (argument values of all method calls).

Another way of adding a profiling trace is the following:

try:
    profiler.start("engine-on-action-complete")

    action_handler.on_action_complete(action_ex, result)
finally:
    profiler.stop()

Here we don’t decorate the entire method, we only want to profile just one line of code. But like in the previous example, we added a profiling trace. The obvious advantage of using the decorator is that it can live in code permanently because it doesn’t pollute it too much and we can use them any time we want to profile the system.

Even simpler and more concise way to achieve the same is use a special context manager from ‘osprofiler’:

with profiler.Trace('engine-on-action-complete'):
    action_handler.on_action_complete(action_ex, result)

Configuring Mistral for Profiling

To start a profiling session, one needs to make the steps below.

Mistral Configuration File

Make these change in the config file:

[DEFAULT]
log_config_append = wf_trace_logging.conf

[profiler]
enabled = True
hmac_keys = secret_word

Defining the ‘log_config_append’ property allows to have all the logging configuration in a separate file. In the example above, it’s called ‘wf_trace_logging.conf’ but it can have a different name, if needed. ‘[profiler]’ group directly refers to the ‘osprofiler’ project and is brought by it. The property ‘enabled’ is self-explaining, but the other one is not. The value of the property ‘hmac_keys’ basically needs to be known by someone who wants to start a profiling session. This value needs to be passed as part of the user request. It will be shown a bit later.

Logging Configuration File

The content of the logging configuration file conforms the documentation for the standard ‘logging’ Python module. Find more details at https://docs.python.org/3/library/logging.config.html#configuration-file-format

This particular example of the logging file configures three different loggers and their corresponding counterparts like handlers. For the purpose of this document though we only need to pay attention how ‘profiler_trace’ logger is configure. Every entity starting with ‘profiler’ is related to profiling configuration. The reason why other loggers are also included here is to show how different loggers can coexist within one configuration file and how they can reuse same entities.

[loggers]
keys=workflow_trace,profiler_trace,root

[handlers]
keys=consoleHandler, wfTraceFileHandler, profilerFileHandler, fileHandler

[formatters]
keys=wfFormatter, profilerFormatter, simpleFormatter, verboseFormatter

[logger_workflow_trace]
level=INFO
handlers=consoleHandler, wfTraceFileHandler
qualname=workflow_trace
propagate=0

[logger_profiler_trace]
level=INFO
handlers=profilerFileHandler
qualname=profiler_trace

[logger_root]
level=DEBUG
handlers=fileHandler

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=verboseFormatter
args=("/tmp/mistral.log",)

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)

[handler_wfTraceFileHandler]
class=FileHandler
level=INFO
formatter=wfFormatter
args=("/tmp/mistral_wf_trace.log",)

[handler_profilerFileHandler]
class=FileHandler
level=INFO
formatter=profilerFormatter
args=("/tmp/mistral_osprofile.log",)

[formatter_verboseFormatter]
format=%(asctime)s %(thread)s %(levelname)s %(module)s [-] %(message)s
datefmt=

[formatter_simpleFormatter]
format=%(asctime)s - %(message)s
datefmt=%y-%m-%d %H:%M:%S

[formatter_wfFormatter]
format=%(asctime)s WF [-] %(message)s
datefmt=

[formatter_profilerFormatter]
format=%(message)s
datefmt=%H:%M:%S

Triggering Profiling Sessions

Once Mistral is configured like explained above, in order to start a profiling session we need to make a user request to Mistral that we want to analyse but adding one property to it. The name of the property is ‘profile’ and it needs to be set to the value of the ‘hmac_keys’ property from the main configuration file.

$ mistral execution-create my_slow_workflow --profile secret_word

Profiling Session Result

When started in a profiling mode like just shown, Mistral will be writing info about the profiling traces into the configured file. In our case it is ‘/tmp/mistral_osprofile.log’.

2020-02-27T08:04:25.789433          f12e75d5-5d59-4cbc-b74d-357f19290dd7 f12e75d5-5d59-4cbc-b74d-357f19290dd7 b9b29981-0916-4635-af18-d6c92f991f46 engine-start-workflow-start
2020-02-27T08:04:25.790232          f12e75d5-5d59-4cbc-b74d-357f19290dd7 b9b29981-0916-4635-af18-d6c92f991f46 3cdd41b5-318a-4926-a38e-63344b6aef7a workflow-handler-start-workflow-start
2020-02-27T08:04:25.812879          f12e75d5-5d59-4cbc-b74d-357f19290dd7 3cdd41b5-318a-4926-a38e-63344b6aef7a 603f1fab-be78-438d-af13-d94ed3b7e416 workflow-start-start
2020-02-27T08:04:25.954502          f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 b1d0a77a-52f5-4415-a6c4-f16b3591a47d workflow-set-state-start
2020-02-27T08:04:25.961298 0.006782 f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 b1d0a77a-52f5-4415-a6c4-f16b3591a47d workflow-set-state-stop
2020-02-27T08:04:25.961769          f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 27b58351-aebe-4e37-9cec-91fdbef5c68b wf-controller-get-controller-start
2020-02-27T08:04:25.962041 0.000267 f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 27b58351-aebe-4e37-9cec-91fdbef5c68b wf-controller-get-controller-stop
2020-02-27T08:04:25.962311          f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 605ebfc2-a2bb-4fe1-8159-fc16f6741f5f workflow-controller-continue-workflow-start
2020-02-27T08:04:26.023134 0.060832 f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 605ebfc2-a2bb-4fe1-8159-fc16f6741f5f workflow-controller-continue-workflow-stop
2020-02-27T08:04:26.023600          f12e75d5-5d59-4cbc-b74d-357f19290dd7 603f1fab-be78-438d-af13-d94ed3b7e416 3a5a384a-9598-4844-a740-981f92e604af dispatcher-dispatch-commands-start
2020-02-27T08:04:26.023918          f12e75d5-5d59-4cbc-b74d-357f19290dd7 3a5a384a-9598-4844-a740-981f92e604af d84a13e4-4763-4321-ab08-8cbd19656f2f task-handler-run-task-start
2020-02-27T08:04:26.024179          f12e75d5-5d59-4cbc-b74d-357f19290dd7 d84a13e4-4763-4321-ab08-8cbd19656f2f 7878e4f8-aaaa-4b9b-b15a-35848b5cdd61 task-handler-build-task-from-command-start
2020-02-27T08:04:26.024422 0.000243 f12e75d5-5d59-4cbc-b74d-357f19290dd7 d84a13e4-4763-4321-ab08-8cbd19656f2f 7878e4f8-aaaa-4b9b-b15a-35848b5cdd61 task-handler-build-task-from-command-stop

So any time Mistral runs code marked as a profiling trace it prints two entries into the file: right before the code snippet starts and right after its completion. Notice also that for the corresponding “-stop” entry (the suffix going after the trace name) Mistral prints an additional number in the second column. This is a duration of the code snippet.

This content of this file itself is probably not so useful (although, it might be for some purpose) but based on it we can build the following report:

Total time | Max time | Avg time | Occurrences | Trace name
-------------------------------------------------------------------------------------------
2948.326     8.612      1.218      2420          engine-on-action-complete
2859.172     8.516      1.181      2420          action-handler-on-action-complete
2812.726     8.482      1.162      2420          task-handler-on-action-complete
2767.836     8.412      1.144      2420          regular-task-on-action-complete
2766.199     8.411      1.143      2420          task-complete
2702.764     8.351      0.460      5878          task-run
2506.531     8.354      0.850      2948          dispatcher-dispatch-commands
2503.398     8.353      0.437      5735          task-handler-run-task
2488.940     8.350      0.434      5735          task-run-new
1669.179     54.737     0.881      1894          default-executor-run-action
1201.582     3.687      0.497      2420          regular-task-get-action-input
1126.351     2.093      0.476      2366          ad-hoc-action-validate-input
1125.129     2.092      0.238      4732          ad-hoc-action-prepare-input
687.619      7.594      0.651      1056          task-handler-refresh-task-state
387.622      3.872      0.300      1291          workflow-handler-check-and-fix-integrity
234.231      4.068      0.392      597           workflow-handler-check-and-complete
224.026      4.042      0.375      597           workflow-check-and-complete
210.184      6.694      1.470      143           task-run-existing
160.118      8.343      0.304      526           workflow-action-schedule
141.398      4.546      0.268      528           workflow-handler-start-workflow
109.641      4.361      0.208      528           workflow-start
78.683       2.004      0.077      1024          direct-wf-controller-get-join-logical-state

...

To generate this report, run:

$ python tools/rank_profiled_methods.py /tmp/mistral_osprofile.log report.txt

And this report is somewhat really useful when it comes to analysing performance bottlenecks. All times are shown in seconds.