Skip to content
Jacob Morris edited this page Jan 18, 2020 · 19 revisions

Welcome to the ExecTiming wiki!

ExecTiming is a package for Python the provides an easy way to measure the execution time of pieces of code. Several cool features include decorators, best-fit-curve determination, graphing features, and the ability to redirect the output to any file-like object.

Installation

pip install exectiming
# scipy, numpy, and scikit-learn are required for best-fit-curve determination
# matplotlib is needed for plotting

pip install --no-deps exectiming # can be used if the best-fit-curve and plotting parts will not be used

Time a Function

from exectiming.exectiming import StaticTimer

@StaticTimer.decorate()
def factorial(n):
    if n <= 1:
        return 1
    else:
        return n * factorial.__wrapped__(n-1)

factorial(50)
# 0.02175 ms - factorial ... [runs=  1, iterations=  1]

.decorate() can be used to wrap any function, even a recursive one, such that the function will be timed whenever it is called. .__wrapped__(n-1) is used so that the decorator is by-passed on the recursive call, which is necessary to avoid unpredictable behavior.

StaticTimer is intended for quick tests where the output is immediately displayed or returned and doesn't need to be retained. Other important methods for StaticTimer are .start(), .elapsed(), and .time_it().

Logging

from exectiming.exectiming import Timer
from time import sleep

timer = Timer(split=True)

timer.start()
sleep(1.0)
timer.log()

timer.output()
# Split:
#     1000.43591 ms - Log ... [runs=  1, iterations=  1]

Timer() retains measured times and a method like .output() must be called to get any output. .start() and .elapsed() can be used to measure the execution time of statements that aren't in a function. Timer() organizes data into splits which contain runs. Splits are used to separate the measured times of different sections of code.

Plotting

from exectiming.exectiming import Timer

timer = Timer()
@timer.decorate(runs=100, iterations_per_run=5, log_arguments=True, call_callable_args=True)
def binary_search(sorted_array, element):
    lower, upper = 0, len(sorted_array)
    middle = upper // 2

    while middle >= lower and middle != upper:
        if element == sorted_array[middle]:
            return middle
        elif element > sorted_array[middle]:
            lower = middle + 1  # lower must be beyond middle because the middle wasn't right
        else:
            upper = middle - 1  # upper must be lower than the middle because the middle wasn't right

        middle = (upper + lower) // 2

    return None  # couldn't find it

binary_search(lambda: [i for i in range(randint(0, 10000))], lambda: randint(0, 10000))
timer.plot(plot_curve=True, curve=timer.best_fit_curve(exclude={1}, transformers=len), key=0,
           transformer=len, time_unit=timer.US, x_label="List Length", equation_rounding=4,
           title="Binary Search - Random Size, Random Element")

Imgur Image

Above, a function named binary_search is wrapped with a decorator that executes the function for 500 runs, each with 5 iterations. The time for each run is the sum of the execution time of the 5 iterations. iterations_per_run can be important in situations like this where the runtime is very quick and so repeating the call a couple of times can help make sure the measured time is more accurate.

binary_search is called with two anonymous functions but requires a list and integer according to the function definition. However, the decorator has call_callable_args=True, meaning binary_search is actually passed the return values of calls to the anonymous functions. The same return value is used for all the iterations, but each run will result in a new set of parameters. The option to replace parameters with the return value of a call to them allows greater testing through randomized data.

After binary_search executes, the best-fit-curve is determined. All arguments must be integers and many of the best-fit-curve options require there to be only one independent variable. This is accomplished by transforming the first parameter, the list, to be its length, which is done with transformers=len. The second parameter, at index 1 is excluded with exclude={1}.

Finally, the measured times and best-fit-curve are plotted using matplotlib. Again, there can only be one independent variable for plotting and it must be an integer, so .plot() is passed key=0 and transformer=len to use the positional argument at index 0 and to transform it into an integer using len. The best-fit-curve data is passed in as well.