Snippet Wednesday - ⏱️ Time profiling the parametrization ⏱️

Many VIKTOR developers have encountered issues with slow parametrization, which is deemed problematic when the parameterization call exceeds a duration of 1 second. Sluggish parametrization significantly impacts the application, resulting in a laggy and unresponsive user experience, hampering its overall performance and responsiveness.

As the application grows in complexity with a multitude of parameters and intricate callbacks, pinpointing the specific areas where time is consumed becomes increasingly challenging. The expanding scope and complexity might obscure the source of the time penalty within the codebase, making it difficult to identify the exact segments responsible for the slowdown in execution.

Here profiling can help you out.

Time profiling

Time profiling in programming refers to the practice of measuring and analyzing the execution time of specific sections or the entirety of a program’s code. This technique helps developers identify performance bottlenecks by pinpointing which parts of the code take the most time to run. By utilizing specialized tools or modules, such as timeit or cProfile in Python, time profiling allows for precise measurement of execution times, aiding in the optimization and improvement of code efficiency for better overall performance.

Profiling wrapper

The following decorator can be used to profile a certain function as follows:

@profile_it()
def some_function(input_1, input_2)
    ...
import cProfile
from functools import wraps


def profile_it(output_file: str= None):
    """A time profiler decorator.
    Inspired by and modified the profile decorator of Giampaolo Rodola:
    http://code.activestate.com/recipes/577817-profile-decorator/
    Args:
        output_file: str or None. Default is None
            Path of the output file. If only name of the file is given, it's
            saved in the current directory.
            If it's None, the name of the decorated function is used.
    Returns:
        Profile of the decorated function
    """

    def inner(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            _output_file = output_file or func.__name__ + ".prof"
            pr = cProfile.Profile()
            pr.enable()
            retval = func(*args, **kwargs)
            pr.disable()
            pr.dump_stats(_output_file)
            return retval

        return wrapper

    return inner

Profiling the parametrization

Now the big question is, how do we profile the parametrization. This is done by overwriting the protected method _generate() on the parametrization object.

DISCLAIMER: YOU SHOULD NEVER OVERWRITE A PROTECTED MEMBER IN PRODUCTION CODE, ONLY USE IN DEVELOPMENT AND REMOVE THIS BEFORE PUBLISHING.

class Parametrization(ViktorParametrization):

    @profile_it()
    def _generate(self, *args, **kwargs):
        return super()._generate(*args, **kwargs)

    ... (field definitions)

Interpreting the profiling result

The decorator will save a .prof file on you local file system. This file can be interpreted using a tool like SnakeViz or the pstats module.

Using SnakeViz, it can be seen that, in the above example, 2 callbacks are responsible for the slow parametrization. Both these callback rely on the slow get_file_from_storage function, which is called 6 times!

Usual causes of slow parametrization

Often when an app is suffering from slow parametrization, some callback rely on a slow underleying function. This function can appear not that slow (+/- 300ms), but when multiple callbacks rely on this function or the callback is used in multiple fields, this can add up to multiple seconds of lag.

Profiling creates a great insight where to start optimizing your Parametrization.

9 Likes

Nice work @mweehuizen ! For those interested in an introduction to profiling, you can refer to this snippet by @kvangiessen :

1 Like

Very useful. Thanks!

@mweehuizen Very useful, thank you for sharing this approach with us. I’m yet to put it to the test and see the results!

If I understood correctly, it is fair to assume objects from the viktor.ai library will perform faster than callbacks in most cases?

I’m thinking for example to how we can pass an object e.g. parametrization.And(...) or a function func() to the attribute visible of a Field.