Table of Contents

cs20ps23as10: Profiling via Decoration

Goals


Prerequisites

This assignment requires familiarity with the lecture materials presented in class through week 10.


Assignment

You shall define a function named profile in a module named call_profiler. Calls to functions decorated with call_profiler.profile will be profiled in a manner similar to that of cProfile, i.e. the total number of calls to decorated functions during an interpreter session will be recorded, as will the cumulative amount of time spent executing each function. Profiling information shall be supplied via four other functions in the call_profile module, as demonstrated below.

Starter Code

call_profiler.py
"""
Provides decorator function "profile" that counts calls and cumulative execution time for all
decorated functions for the duration of an interpreter session.
"""
 
__author__ = 'A student in CS 20P, someone@jeff.cis.cabrillo.edu'
 
import time
 
 
def profile(function):
  """
  Decorates a function so that the number of calls and cumulative execution time of all calls can
  be reported by call_count() and cumulative_time(), respectively. Execution time is measured by
  calling time.perf_counter() before and after a call to the decorated function.
  """
  pass  # TODO
 
 
def call_count(function):
  """
  Returns the number of times a given function has been called during this interpreter session,
  assuming the function has been decorated by profile().
  """
  pass  # TODO
 
 
def call_counts():
  """
  Returns a dictionary mapping functions decorated by profile() to the number of times they have
  been called during this interpreter session.
  """
  pass  # TODO
 
 
def cumulative_time(function):
  """
  Returns the cumulative amount of time (in seconds) that have been spent executing calls to a given
  function during this interpreter session, assuming the function has been decorated by profile().
  """
  pass  # TODO
 
 
def cumulative_times():
  """
  Returns a dictionary mapping functions decorated by profile() to the cumulative amount of time (in
  seconds) that have been spent executing calls to a given function during this interpreter session.
  """
  pass  # TODO

Demo

Here is a module with a few functions from previous lectures, now decorated with call_profiler.profile:

call_profiler_demo.py
"""
Various functions for experimenting with the call_profiler module.
"""
 
__author__ = 'Jeffrey Bergamini for CS 20P, jeffrey.bergamini@cabrillo.edu'
 
import call_profiler
 
 
@call_profiler.profile
def dna(sequence) -> dict[str, int]:
  """
  Counts the bases in a DNA string.
 
  :param sequence: A DNA string (expected to contain A/C/G/T characters).
  :return: A dict[str, int] with keys in bases 'ACGT', and associated base counts.
  """
  return {base: sequence.count(base) for base in 'ACGT'}
 
 
@call_profiler.profile
def euler_004():
  """ Single statement with walrus. """
  return max(a * b
             for a in range(100, 1000)
             for b in range(100, 1000)
             if (prod_str := str(a * b)) == prod_str[::-1])
 
 
@call_profiler.profile
def euler_009():
  """ Generator expression passed to next()—we know there can be only one solution. """
  return next(a * b * c
              for a in range(1, 334)
              for b in range(a + 1, 1000 - a)
              if a**2 + b**2 == (c := 1000 - a - b)**2)

And here is a REPL session demonstrating the profiling behavior:

>>> import call_profiler_demo
>>> import call_profiler
>>> call_profiler.call_counts()
{<function dna at 0x1028d1da0>: 0, <function euler_004 at 0x1028d22a0>: 0, <function euler_009 at 0x1028d23e0>: 0}
>>> call_profiler.call_count(call_profiler_demo.euler_004)
0
>>> call_profiler.cumulative_times()
{<function dna at 0x1028d1da0>: 0.0, <function euler_004 at 0x1028d22a0>: 0.0, <function euler_009 at 0x1028d23e0>: 0.0}
>>> call_profiler.cumulative_time(call_profiler_demo.euler_004)
0.0
>>> call_profiler_demo.euler_004()
906609
>>> call_profiler_demo.euler_004()
906609
>>> call_profiler_demo.euler_009()
31875000
>>> call_profiler_demo.dna('GATTACA')
{'A': 3, 'C': 1, 'G': 1, 'T': 2}
>>> call_profiler_demo.dna(open('/srv/datasets/chromosome4').read())
{'A': 3030352, 'C': 2102095, 'G': 2092163, 'T': 3009609}
>>> call_profiler.call_counts()
{<function dna at 0x1028d1da0>: 2, <function euler_004 at 0x1028d22a0>: 2, <function euler_009 at 0x1028d23e0>: 1}
>>> call_profiler.call_count(call_profiler_demo.euler_004)
2
>>> call_profiler.cumulative_times()
{<function dna at 0x1028d1da0>: 0.09427983299246989, <function euler_004 at 0x1028d22a0>: 0.20388466698932461, <function euler_009 at 0x1028d23e0>: 0.02965154201956466}
>>> call_profiler.cumulative_time(call_profiler_demo.euler_004)
0.20388466698932461

Lines of Code!

Just for fun, as submissions are received, this table will be updated and ranked with regard to the number of lines in a fully functional solution, as measured by SLOCCount. Fewer lines isn't always better, but it's interesting to see by how much different approaches vary.

RankSLOC (lines)User


Submission

Submit call_profiler.py via turnin.

Feedback Robot

This project has a feedback robot that will run some tests on your submission and provide you with a feedback report via email within roughly one minute.

Please read the feedback carefully.

Due Date and Point Value

Due at 23:59:59 on the date listed on the syllabus.

Assignment 10 is worth 60 points.

Possible point values per category:
---------------------------------------
call_counts()                        15
call_count()                         15
cumulative_times()                   15
cumulative_time()                    15

Possible deductions:
  Style and practices            10–20%
Possible extra credit:
  Submission via Git                 5%
---------------------------------------