Skip to content

perf: Cache frame context lookups to reduce ic() overhead in loops - #247

Closed
AhmadAL-Quraan wants to merge 1 commit into
gruns:masterfrom
AhmadAL-Quraan:cache-getcontext
Closed

AhmadAL-Quraan wants to merge 1 commit into
gruns:masterfrom
AhmadAL-Quraan:cache-getcontext

Conversation

@AhmadAL-Quraan

@AhmadAL-Quraan AhmadAL-Quraan commented Aug 14, 2026 •

Copy link
Copy Markdown

PR Description

Description

Resolves #246 && #36

Repeated calls to ic() (such as inside tight loops or frequently called functions) spend significant CPU time re-resolving frame context via inspect.getsourcefile() and realpath() / basename().

This PR introduces an LRU cache (@functools.lru_cache(maxsize=8192)) for frame context lookups (_cached_frame_context). For static call sites where the code object, line number, and path settings remain identical, subsequent calls retrieve the context in $O(1)$ time rather than repeating disk/path inspections.


Changes

  • Extracted frame metadata resolution into _cached_frame_context(code, lineNumber, contextAbsPath).
  • Applied @functools.lru_cache(maxsize=8192) to memoize context lookups based on frame code object and line number.
  • Updated IceCreamDebugger._getContext() to use the cached helper.

Performance Verification

Benchmark Results (20,000 iterations via test.py)

  • Before: 1.1946s (16,741.34 calls/sec)
  • After: 0.7468s (26,781.97 calls/sec)
  • Result: ~37.5% reduction in total execution time (~60% increase in throughput)
#test.py
import time
from icecream import ic

# Discard output to strictly measure execution overhead
ic.configureOutput(outputFunction=lambda s: None)

def run_workload(n=20000):
    start = time.perf_counter()
    for i in range(n):
        ic(i * 2)
    elapsed = time.perf_counter() - start
    print(f"Time for {n} iterations: {elapsed:.4f} seconds ({n/elapsed:.2f} calls/sec)")

if __name__ == '__main__':
    run_workload()

Profiling with py-spy over a workload executing ic() 8,000 times shows a substantial reduction in frame inspection overhead:

  • Before: _formatContext / inspect occupied a large portion of execution time across the call stack.
  • After: Context resolution overhead is almost completely eliminated, allowing the runtime to spend time strictly on AST node analysis and argument formatting.

before:
before-flame3
after:
after-flame3


Checklist

  • Tested with existing test suite
  • Verified backward compatibility with contextAbsPath and line number resolution

BTW

I tried making the tests provided with the repo:

There were 2 errors already being there before me making the changes, so hope this won't affect :)

image

@Jakeroid

Copy link
Copy Markdown
Collaborator

@AhmadAL-Quraan

Thank you for your work! I have a couple of questions.

  1. I think replacing inspect.getframeinfo(callFrame) contributed significantly to the performance improvement. Maybe it would perform even faster without caching. What do you think about having caching disabled by default, with an option to enable it?
  2. Regarding the cache itself, what do you think about adding the file name as an argument to ensure that the cache does not resolve the same code from different files?

@AhmadAL-Quraan AhmadAL-Quraan closed this by deleting the head repository Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ic() spends ~78% of its time in source re-inspection (_getContext) - flame graph + numbers for #36

2 participants