Hone logo
Hone
Problems

Python Code Profiling Framework

This challenge asks you to build a simple yet effective profiling framework in Python. The goal is to create a tool that can measure the execution time of specific functions or code blocks, helping developers identify performance bottlenecks in their applications. A good profiling framework can significantly improve the efficiency and speed of Python programs.

Problem Description

Your task is to design and implement a Python class or set of functions that can be used to profile code execution. The framework should allow users to:

  1. Decorate functions: Apply a decorator to a function to automatically start and stop timing when the function is called.
  2. Profile arbitrary code blocks: Allow users to manually start and stop timing for any section of code within a script.
  3. Store and retrieve profiling data: Maintain a record of executed functions/blocks, their total execution time, number of calls, and average execution time.
  4. Report profiling results: Provide a clear and concise way to display the collected profiling data, sorted by execution time or number of calls.

Key Requirements:

  • Decorator: Implement a decorator (e.g., @profile_this) that wraps functions.
  • Manual Profiling: Provide a mechanism (e.g., start_timer(), stop_timer()) for profiling specific code segments.
  • Data Storage: Use a suitable data structure (e.g., a dictionary) to store profiling results for each profiled item (function name or custom label). For each item, store:
    • total_time: Sum of all execution times.
    • num_calls: Number of times the item was executed.
    • average_time: total_time / num_calls.
  • Reporting: Implement a function (e.g., print_profile_report()) that displays the stored data in a human-readable format. The report should be sortable by total time (descending) and number of calls (descending).
  • Clear Identification: Each profiled item should be uniquely identifiable, either by its function name (for decorators) or a custom label provided by the user (for manual profiling).

Expected Behavior:

When a decorated function is called, its execution time should be measured and recorded. When manual timers are used, the code between start_timer() and stop_timer() should have its duration measured and recorded under a specified label. The report should clearly show which functions/blocks took the longest, how many times they were called, and their average execution time.

Edge Cases:

  • What happens if stop_timer() is called without a preceding start_timer()?
  • What happens if a decorated function raises an exception? The timing should still be recorded, and the exception should be re-raised.
  • Handling of nested function calls within profiled blocks.
  • What if multiple profilers are active simultaneously (e.g., a decorated function calling another decorated function, or a manually timed block containing a decorated function)? The framework should correctly attribute time to each level.

Examples

Example 1: Decorator Usage

import time

# Assume the profiling framework has been initialized and decorated functions are registered

@profile_this("slow_function")
def slow_function(n):
    time.sleep(n)
    return n * 2

@profile_this("fast_function")
def fast_function(x):
    return x + 1

slow_function(0.1)
fast_function(5)
slow_function(0.05)

# Assume print_profile_report() is called here

Expected Output (Conceptual - actual times will vary):

--- Profiling Report ---
Sorted by Total Time (descending):
---------------------------------
Label           | Calls | Total Time (s) | Average Time (s)
---------------------------------
slow_function   | 2     | 0.150          | 0.075
fast_function   | 1     | 0.001          | 0.001
---------------------------------

Sorted by Calls (descending):
---------------------------------
Label           | Calls | Total Time (s) | Average Time (s)
---------------------------------
slow_function   | 2     | 0.150          | 0.075
fast_function   | 1     | 0.001          | 0.001
---------------------------------

Example 2: Manual Profiling Usage

import time

# Assume the profiling framework has been initialized

def process_data():
    start_timer("data_processing_segment")
    time.sleep(0.2)
    # Some complex operations
    for i in range(10000):
        _ = i * i
    time.sleep(0.1)
    stop_timer("data_processing_segment")

    start_timer("network_request")
    time.sleep(0.08)
    stop_timer("network_request")

process_data()
process_data()

# Assume print_profile_report() is called here

Expected Output (Conceptual - actual times will vary):

--- Profiling Report ---
Sorted by Total Time (descending):
---------------------------------
Label               | Calls | Total Time (s) | Average Time (s)
---------------------------------
data_processing_segment | 2     | 0.600          | 0.300
network_request     | 2     | 0.160          | 0.080
---------------------------------

Sorted by Calls (descending):
---------------------------------
Label               | Calls | Total Time (s) | Average Time (s)
---------------------------------
data_processing_segment | 2     | 0.600          | 0.300
network_request     | 2     | 0.160          | 0.080
---------------------------------

Example 3: Nested Profiling

import time

@profile_this("outer_task")
def outer_task():
    time.sleep(0.05)
    inner_task()
    time.sleep(0.03)

@profile_this("inner_task")
def inner_task():
    time.sleep(0.02)

outer_task()
outer_task()

# Assume print_profile_report() is called here

Expected Output (Conceptual - actual times will vary):

--- Profiling Report ---
Sorted by Total Time (descending):
---------------------------------
Label      | Calls | Total Time (s) | Average Time (s)
---------------------------------
outer_task | 2     | 0.200          | 0.100
inner_task | 2     | 0.040          | 0.020
---------------------------------

Sorted by Calls (descending):
---------------------------------
Label      | Calls | Total Time (s) | Average Time (s)
---------------------------------
outer_task | 2     | 0.200          | 0.100
inner_task | 2     | 0.040          | 0.020
---------------------------------

Constraints

  • The profiling framework must be implemented purely in Python. No external libraries are allowed for the core profiling logic (though time module is naturally used for timing).
  • The time.time() function should be used for measuring elapsed time.
  • The report should display floating-point numbers for time with at least 3 decimal places of precision.
  • The framework should not significantly impact the performance of the code being profiled (e.g., overhead should be minimal).
  • The profiling data should be cleared automatically or have a function to reset it if needed for multiple profiling runs within a single script execution.

Notes

  • Consider how to handle concurrent timing if your framework were to be used in multithreaded or multiprocessing environments (though for this challenge, focus on a single-threaded execution model).
  • Think about how you will store the start_time for manual profiling. A stack or a dictionary might be useful.
  • For the decorator, you'll need to use functools.wraps to preserve the original function's metadata.
  • The reporting function should handle the case where no profiling data has been collected.
  • Consider naming conventions for your profiling functions and classes to make them intuitive.
Loading editor...
python