Skip to content

Performance [P2]: Make entity method signature caching effective #186

Description

What is the issue?

The entity executor's method-signature cache is recreated for every operation in an entity batch, so it does not avoid repeated reflection.

What is the impact?

Entity-heavy workloads repeatedly execute inspect.signature() for the same entity operation. This is unnecessary CPU overhead, especially for batches containing repeated operations or high-throughput small entity requests.

Details about the issue including code reference

Relevant code:

results: list[pb.OperationResult] = []
for operation in req.operations:
start_time = datetime.now(timezone.utc)
executor = _EntityExecutor(self._registry, self._logger, self._data_converter)

class _EntityExecutor:
def __init__(self, registry: _Registry, logger: logging.Logger,
data_converter: DataConverter):
self._registry = registry
self._logger = logger
self._data_converter = data_converter
self._entity_method_cache: dict[tuple[type, str], bool] = {}
def execute(
self,
orchestration_id: str,
entity_id: EntityInstanceId,
operation: str,
state: StateShim,
encoded_input: str | None,
) -> str | None:
"""Executes an entity function and returns the serialized result, if any."""
self._logger.debug(
f"{orchestration_id}: Executing entity '{entity_id}'..."
)
fn = self._registry.get_entity(entity_id.entity)
if not fn:
raise EntityNotRegisteredError(
f"Entity function named '{entity_id.entity}' was not registered!"
)
input_type = type_discovery.entity_input_type(fn, operation, self._data_converter) if encoded_input else None
entity_input = self._data_converter.deserialize(encoded_input, input_type)
ctx = EntityContext(orchestration_id, operation, state, entity_id, self._data_converter)
if isinstance(fn, type) and issubclass(fn, DurableEntity):
entity_instance = fn()
if not hasattr(entity_instance, operation):
raise AttributeError(f"Entity '{entity_id}' does not have operation '{operation}'")
method = getattr(entity_instance, operation)
if not callable(method):
raise TypeError(f"Entity operation '{operation}' is not callable")
# Execute the entity method
entity_instance._initialize_entity_context(ctx) # pyright: ignore[reportPrivateUsage]
cache_key = (type(entity_instance), operation)
has_required_param = self._entity_method_cache.get(cache_key)
if has_required_param is None:
sig = inspect.signature(method)
has_required_param = any(
p.default == inspect.Parameter.empty
for p in sig.parameters.values()
if p.kind not in (inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD)
)
self._entity_method_cache[cache_key] = has_required_param

_execute_entity_batch() creates _EntityExecutor inside its operation loop. _EntityExecutor initializes _entity_method_cache and uses it to cache the required-parameter check, but the cache is cold for every operation because the executor is new each time.

A potential or proposed solution

Keep executor lifetime independent from entity instance lifetime, but move the signature result to a bounded per-worker cache or create one executor for the full batch. The cache should safely support concurrent worker threads and preserve the current no-argument versus input-argument behavior for class-based entities.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performance / optimizationUsed for issues or PRs purely focused on performance and optimizations, not bugs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions