Skip to content

perf(component): Add LRU caching for class discovery methods#106

Open
khresth wants to merge 2 commits intooracle:mainfrom
khresth:main
Open

perf(component): Add LRU caching for class discovery methods#106
khresth wants to merge 2 commits intooracle:mainfrom
khresth:main

Conversation

@khresth
Copy link

@khresth khresth commented Feb 17, 2026

Summary

Optimize Component class discovery by adding LRU caching to frequently-used methods.

Problem

Component.get_class_from_name() and Component._get_all_subclasses() in pyagentspec/src/pyagentspec/component.py perform BFS traversal of the class hierarchy on every call. During serialization/deserialization of complex flows, these methods are called repeatedly with the same arguments, causing unnecessary CPU overhead.

Solution

Add @functools.lru_cache to both methods:

  • _get_class_from_name_cached(maxsize=256) - caches class name lookups
  • _get_all_subclasses_cached(maxsize=128) - caches subclass enumeration

Cache invalidation is handled automatically by clearing both caches in Component.init_subclass() when new Component subclasses are dynamically created (e.g., in tests or custom components).

Testing

  • All 474+ existing tests pass
  • Cache invalidation verified with dynamic subclass creation tests (e.g., test_inheritance_is_correctly_applied)

Performance Impact

Repeated calls to these methods now return cached results instead of traversing the class hierarchy each time, providing measurable speedup during:

  • Component deserialization
  • JSON schema generation
  • Serialization context operations

@khresth khresth requested a review from a team February 17, 2026 13:06
@oracle-contributor-agreement
Copy link

Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
The following contributors of this PR have not signed the OCA:

To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application.

When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated.

If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public.

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. label Feb 17, 2026
@dhilloulinoracle
Copy link
Contributor

Thank you @khresth for your contribution!

Do you have any improvement numbers maybe?

@khresth
Copy link
Author

khresth commented Feb 17, 2026

I created a benchmark to measure the caching benefit. Here are the results:

get_class_from_name()

With caching: ~0.0002ms per lookup
10,000 cache hits vs 1 miss (99.99% hit rate)
Previously required BFS traversal of entire class hierarchy on every call
_get_all_subclasses()

With caching: ~0.0003ms per call
1,000 cache hits vs 1 miss (99.9% hit rate)
Previously traversed all Component subclasses via BFS each invocation
Real-world scenario (simulated deserialization)

3,000 component lookups: 0.0012 seconds total
Combined 99.9%+ cache hit rate across both methods

The repeated class lookups that previously required traversing the Component hierarchy (O(n) where n = number of classes) now complete in O(1) time via dictionary lookup. This improves deserialization performance for complex flows with many nested components.

@lfaucon
Copy link
Member

lfaucon commented Feb 18, 2026

Hi @khresth ,

Thanks a lot for your optimization suggestion. This is very appreciated.

I see that your benchmark script does not actually evaluates deserialization, so I tried evaluating it using the commands below. I find that using the code from your branch does not seem to impact the total runtime compared to main. Can you try to run the command below and tell me if you see similar numbers from your side? You can use other example configuration files available in our test suite.

The look-ups that you optimized seem to only take a few microseconds, so they are unlikely to improve the deserialization that takes several milliseconds. Did you try to profile any of the (i) Component deserialization (ii) JSON schema generation or (iii) Serialization context operations to find which methods actually significantly impact performance?

Switched to branch 'khresth/main'
~/github/agent-spec/pyagentspec khresth/main ······························································································································································ Py agent-spec 15:08:09
❯ python3 -m timeit -s "from pyagentspec.serialization import AgentSpecDeserializer" "AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read())" 
50 loops, best of 5: 9.26 msec per loop
~/github/agent-spec/pyagentspec khresth/main 
❯ gco main 
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
~/github/agent-spec/pyagentspec main ······································································································································································ Py agent-spec 15:08:35
❯ python3 -m timeit -s "from pyagentspec.serialization import AgentSpecDeserializer" "AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read())" 
50 loops, best of 5: 9.09 msec per loop

I also don't see an improvement when deserializing the same configuration three times in a row:

Switched to branch 'khresth/main'
~/github/agent-spec/pyagentspec khresth/main ······························································································································································ Py agent-spec 15:19:07
❯ python3 -m timeit -s "from pyagentspec.serialization import AgentSpecDeserializer" "AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read()); AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read()); AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read())"
10 loops, best of 5: 27.4 msec per loop
~/github/agent-spec/pyagentspec khresth/main ······························································································································································ Py agent-spec 15:19:11
❯ gco main 
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
~/github/agent-spec/pyagentspec main ······································································································································································ Py agent-spec 15:19:16
❯ python3 -m timeit -s "from pyagentspec.serialization import AgentSpecDeserializer" "AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read()); AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read()); AgentSpecDeserializer().from_yaml(open('./tests/agentspec_configs/flow_with_multiple_levels_of_references.yaml').read())"
10 loops, best of 5: 27.3 msec per loop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Required At least one contributor does not have an approved Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants