Skip to content

Commit

Permalink
feat: pass keyword arguments separately in memoized calls (#95)
Browse files Browse the repository at this point in the history
Related to Safe-DS/DSL#1087

### Summary of Changes

Memoized calls now accept positional and keyword arguments separately.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot committed Apr 23, 2024
1 parent e9660f7 commit 0f63b0c
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 151 deletions.
40 changes: 24 additions & 16 deletions src/safeds_runner/server/_memoization_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ def remove_worst_element(self, capacity_to_free: int) -> None:

def memoized_function_call(
self,
function_name: str,
function_callable: Callable,
parameters: list[Any],
hidden_parameters: list[Any],
fully_qualified_function_name: str,
callable_: Callable,
positional_arguments: list[Any],
keyword_arguments: dict[str, Any],
hidden_arguments: list[Any],
) -> Any:
"""
Handle a memoized function call.
Expand All @@ -127,14 +128,16 @@ def memoized_function_call(
Parameters
----------
function_name:
fully_qualified_function_name:
Fully qualified function name
function_callable:
callable_:
Function that is called and memoized if the result was not found in the memoization map
parameters:
List of parameters passed to the function
hidden_parameters:
List of hidden parameters for the function. This is used for memoizing some impure functions.
positional_arguments:
List of arguments passed to the function
keyword_arguments:
Dictionary of keyword arguments passed to the function
hidden_arguments:
List of hidden arguments for the function. This is used for memoizing some impure functions.
Returns
-------
Expand All @@ -145,28 +148,33 @@ def memoized_function_call(

# Lookup memoized value
lookup_time_start = time.perf_counter_ns()
key = _create_memoization_key(function_name, parameters, hidden_parameters)
key = _create_memoization_key(
fully_qualified_function_name,
positional_arguments,
keyword_arguments,
hidden_arguments,
)
try:
memoized_value = self._lookup_value(key)
# Pickling may raise AttributeError, hashing may raise TypeError
except (AttributeError, TypeError) as exception:
# Fallback to executing the call to continue working, but inform user about this failure
logging.exception(
"Could not lookup value for function %s. Falling back to calling the function",
function_name,
fully_qualified_function_name,
exc_info=exception,
)
return function_callable(*parameters)
return callable_(*positional_arguments, **keyword_arguments)
lookup_time = time.perf_counter_ns() - lookup_time_start

# Hit
if memoized_value is not None:
self._update_stats_on_hit(function_name, access_timestamp, lookup_time)
self._update_stats_on_hit(fully_qualified_function_name, access_timestamp, lookup_time)
return memoized_value

# Miss
computation_time_start = time.perf_counter_ns()
computed_value = function_callable(*parameters)
computed_value = callable_(*positional_arguments, **keyword_arguments)
computation_time = time.perf_counter_ns() - computation_time_start
memory_size = _get_size_of_value(computed_value)

Expand All @@ -176,7 +184,7 @@ def memoized_function_call(
self._map_values[key] = memoizable_value

self._update_stats_on_miss(
function_name,
fully_qualified_function_name,
access_timestamp,
lookup_time,
computation_time,
Expand Down
22 changes: 13 additions & 9 deletions src/safeds_runner/server/_memoization_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,28 +374,32 @@ def _get_size_of_value(value: Any) -> int:


def _create_memoization_key(
function_name: str,
parameters: list[Any],
hidden_parameters: list[Any],
fully_qualified_function_name: str,
positional_arguments: list[Any],
keyword_arguments: dict[str, Any],
hidden_arguments: list[Any],
) -> MemoizationKey:
"""
Convert values provided to a memoized function call to a memoization key.
Parameters
----------
function_name:
fully_qualified_function_name:
Fully qualified function name
parameters:
List of parameters passed to the function
hidden_parameters:
List of parameters not passed to the function
positional_arguments:
List of arguments passed to the function
keyword_arguments:
Dictionary of keyword arguments passed to the function
hidden_arguments:
List of arguments not passed to the function
Returns
-------
key:
A memoization key, which contains the lists converted to tuples
"""
return function_name, _make_hashable(parameters), _make_hashable(hidden_parameters)
arguments = [*positional_arguments, *keyword_arguments.values()]
return fully_qualified_function_name, _make_hashable(arguments), _make_hashable(hidden_arguments)


def _wrap_value_to_shared_memory(
Expand Down
68 changes: 42 additions & 26 deletions src/safeds_runner/server/_pipeline_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,11 @@ def save_placeholder(placeholder_name: str, value: Any) -> None:


def memoized_static_call(
function_name: str,
function_callable: typing.Callable,
parameters: list[Any],
hidden_parameters: list[Any],
fully_qualified_function_name: str,
callable_: typing.Callable,
positional_arguments: list[Any],
keyword_arguments: dict[str, Any],
hidden_arguments: list[Any],
) -> Any:
"""
Call a function that can be memoized and save the result.
Expand All @@ -360,14 +361,16 @@ def memoized_static_call(
Parameters
----------
function_name:
fully_qualified_function_name:
Fully qualified function name
function_callable:
callable_:
Function that is called and memoized if the result was not found in the memoization map
parameters:
List of parameters for the function
hidden_parameters:
List of hidden parameters for the function. This is used for memoizing some impure functions.
positional_arguments:
List of positions arguments for the function
keyword_arguments:
Dictionary of keyword arguments for the function
hidden_arguments:
List of hidden arguments for the function. This is used for memoizing some impure functions.
Returns
-------
Expand All @@ -376,15 +379,23 @@ def memoized_static_call(
"""
if current_pipeline is None:
return None # pragma: no cover

memoization_map = current_pipeline.get_memoization_map()
return memoization_map.memoized_function_call(function_name, function_callable, parameters, hidden_parameters)
return memoization_map.memoized_function_call(
fully_qualified_function_name,
callable_,
positional_arguments,
keyword_arguments,
hidden_arguments,
)


def memoized_dynamic_call(
receiver: Any,
function_name: str,
function_callable: typing.Callable | None,
parameters: list[Any],
hidden_parameters: list[Any],
positional_arguments: list[Any],
keyword_arguments: dict[str, Any],
hidden_arguments: list[Any],
) -> Any:
"""
Dynamically call a function that can be memoized and save the result.
Expand All @@ -395,13 +406,15 @@ def memoized_dynamic_call(
Parameters
----------
receiver : Any
Instance the function should be called on
function_name:
Simple function name
function_callable:
Function that is called and memoized if the result was not found in the memoization map or none, if the function handle should be in the provided instance
parameters:
List of parameters for the function, the first parameter should be the instance the function should be called on (receiver)
hidden_parameters:
positional_arguments:
List of positions arguments for the function
keyword_arguments:
Dictionary of keyword arguments for the function
hidden_arguments:
List of hidden parameters for the function. This is used for memoizing some impure functions.
Returns
Expand All @@ -411,18 +424,21 @@ def memoized_dynamic_call(
"""
if current_pipeline is None:
return None # pragma: no cover

fully_qualified_function_name = (
parameters[0].__class__.__module__ + "." + parameters[0].__class__.__qualname__ + "." + function_name
receiver.__class__.__module__ + "." + receiver.__class__.__qualname__ + "." + function_name
)

member = getattr(receiver, function_name)
callable_ = member.__func__

memoization_map = current_pipeline.get_memoization_map()
if function_callable is None:
function_target_bound = getattr(parameters[0], function_name)
function_callable = function_target_bound.__func__
return memoization_map.memoized_function_call(
fully_qualified_function_name,
function_callable,
parameters,
hidden_parameters,
callable_,
[receiver, *positional_arguments],
keyword_arguments,
hidden_arguments,
)


Expand Down

0 comments on commit 0f63b0c

Please sign in to comment.