How to Measure Functions Performance Using Decorators
Decorators in Python are a powerful tool that allows to adjust or enhance the behavior of functions without modifying them.
This structural pattern can be especially useful in performance measurements, where we can gauge and log the execution time of functions. Timing decorators can be helpful:
In code optimization, especially in complex applications, it’s crucial to know which functions are consuming the most time. Timing decorators help pinpoint these performance bottlenecks.
To compare performance and choose the most efficient algorithm or approach for a task.
In services with Service Level Agreements (SLAs), timing decorators can ensure that certain operations complete within the required time limits.
In resource-limited environments, like embedded systems or IoT devices, timing decorators help to run functions in acceptable time and memory frames.
In web applications, particularly those with RESTful APIs, timing decorators can measure the response times of endpoints, helping to ensure that they meet performance standards and provide a good user experience.
Using Timing Decorator
We measure the execution time of functions in seconds with perf_counter function of time module by putting timestamps.
Use wraps function from functools module to get the correct name of decorated function and its docstring in output.
This can be useful when we work with several functions - to differentiate them.
By default decorators hide the function they are decorating which means that decorator renames decorated functions by its own name
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Running function: '{func.__name__}'")
print(f'Docstring of running function: {func.__doc__}')
start = time.perf_counter() # we put a timestamp before function execution
result = func(*args, **kwargs)
end = time.perf_counter() # and put a timestamp after function execution
elapsed = end - start # calculate the final amount of time spent on functions implementation
print(f'Time elapsed: {elapsed:.4f} sec')
print(f'-' * 33)
return result
return wrapper
@timing_decorator
def slow_function(data_size):
"""Iterates a for loop, simulating a slow process"""
for _ in range(data_size):
pass
@timing_decorator
def artificial_delay_function(seconds):
"""Introduces an artificial delay for the specified number of seconds"""
time.sleep(seconds)
pass
slow_function(200000)
artificial_delay_function(3)
# Output:
Running function: 'slow_function'
Docstring of running function: Iterates a for loop, simulating a slow process
Time elapsed: 0.0028 sec
---------------------------------
Running function: 'artificial_delay_function'
Docstring of running function: Introduces an artificial delay for the specified number of seconds
Time elapsed: 3.0031 sec
---------------------------------
Timing Decorator + Execution Logging
- In this example we added the logging of the date and time when function is executed using datetime.today method.
import time
from functools import wraps
from datetime import datetime
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Running function: '{func.__name__}'")
print(f'Docstring of running function: {func.__doc__}')
start = time.perf_counter() # we put a timestamp before function execution
result = func(*args, **kwargs)
end = time.perf_counter() # and put a timestamp after function execution
elapsed = end - start # calculate the final amount of time spent on functions implementation
print(f'Time elapsed: {elapsed:.4f} sec')
print(f'Run on: {datetime.today().strftime("%Y-%m-%d %H:%M:%S")}') # logging the date and time of function execution
print(f'-' * 33)
return result
return wrapper
@timing_decorator
def slow_function(data_size):
"""Iterates a for loop, simulating a slow process"""
for _ in range(data_size):
pass
@timing_decorator
def artificial_delay_function(seconds):
"""Introduces an artificial delay for the specified number of seconds"""
time.sleep(seconds)
pass
slow_function(200000)
artificial_delay_function(3)
# Output:
Running function: 'slow_function'
Docstring of running function: Iterates a for loop, simulating a slow process
Time elapsed: 0.0076 sec
Run on: 2024-06-24 18:28:18
---------------------------------
Running function: 'artificial_delay_function'
Docstring of running function: Introduces an artificial delay for the specified number of seconds
Time elapsed: 3.0031 sec
Run on: 2024-06-24 18:28:21
---------------------------------
Timing Decorator + Execution Logging + Memory Usage
Here we will also add memory usage measures of every function using tracemalloc module.
Is a debug tool to trace memory blocks allocated by Python.
Is used to compute the differences between two snapshots to detect memory leaks.
import time
from functools import wraps
import tracemalloc
from datetime import datetime
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
tracemalloc.start() # start tracking memory allocations
print(f"Running function: '{func.__name__}'")
print(f'Docstring of running function: {func.__doc__}')
start = time.perf_counter() # we put a timestamp before function execution
result = func(*args, **kwargs)
current, peak = tracemalloc.get_traced_memory()
end = time.perf_counter() # and put a timestamp after function execution
elapsed = end - start # calculate the final amount of time spent on functions implementation
print(f'Time elapsed: {elapsed:.4f} sec')
print(f'Memory usage: {current / 10**6:.4f} MB \n'
f'Peak memory usage: {peak / 10**6:.4f} MB')
print(f'Run on: {datetime.today().strftime("%Y-%m-%d %H:%M:%S")}')
print(f'-' * 33)
tracemalloc.stop() # stop tracking memory allocations
return result
return wrapper
@timing_decorator
def slow_function(data_size):
"""Iterates a for loop, simulating a slow process"""
for _ in range(data_size):
pass
@timing_decorator
def artificial_delay_function(seconds):
"""Introduces an artificial delay for the specified number of seconds"""
time.sleep(seconds)
pass
slow_function(200000)
artificial_delay_function(3)
# Output:
Running function: 'slow_function'
Docstring of running function: Iterates a for loop, simulating a slow process
Time elapsed: 0.1446 sec
Memory usage: 0.0062 MB
Peak memory usage: 0.0064 MB
Run on: 2024-06-24 18:50:35
---------------------------------
Running function: 'artificial_delay_function'
Docstring of running function: Introduces an artificial delay for the specified number of seconds
Time elapsed: 3.0032 sec
Memory usage: 0.0160 MB
Peak memory usage: 0.0850 MB
Run on: 2024-06-24 18:50:38
---------------------------------