Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
497 changes: 497 additions & 0 deletions c_src/py_event_loop.c

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions c_src/py_event_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,62 @@ ERL_NIF_TERM nif_process_ready_tasks(ErlNifEnv *env, int argc,
ERL_NIF_TERM nif_event_loop_set_py_loop(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/* ============================================================================
* Module Import Caching
* ============================================================================ */

/**
* @brief Import and cache a module in the event loop's interpreter
*
* Pre-imports the module and caches it for faster subsequent calls.
* The __main__ module is never cached (returns error).
*
* NIF: loop_import_module(LoopRef, Module) -> ok | {error, Reason}
*/
ERL_NIF_TERM nif_loop_import_module(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/**
* @brief Import a module and cache a specific function
*
* Pre-imports the module and caches the function reference.
* The __main__ module is never cached (returns error).
*
* NIF: loop_import_function(LoopRef, Module, Func) -> ok | {error, Reason}
*/
ERL_NIF_TERM nif_loop_import_function(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/**
* @brief Flush the import cache for an event loop's interpreter
*
* Clears the module/function cache for all namespaces in this loop.
*
* NIF: loop_flush_import_cache(LoopRef) -> ok
*/
ERL_NIF_TERM nif_loop_flush_import_cache(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/**
* @brief Get import cache statistics for the calling process's namespace
*
* Returns a map with count of cached entries.
*
* NIF: loop_import_stats(LoopRef) -> {ok, #{count => N}} | {error, Reason}
*/
ERL_NIF_TERM nif_loop_import_stats(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/**
* @brief List all cached imports in the calling process's namespace
*
* Returns a list of binary strings with cached module and function names.
*
* NIF: loop_import_list(LoopRef) -> {ok, [binary()]} | {error, Reason}
*/
ERL_NIF_TERM nif_loop_import_list(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

/* ============================================================================
* Internal Helper Functions
* ============================================================================ */
Expand Down
6 changes: 6 additions & 0 deletions c_src/py_nif.c
Original file line number Diff line number Diff line change
Expand Up @@ -6359,7 +6359,7 @@
if (write(w->cmd_pipe[1], &header, sizeof(header)) == sizeof(header)) {
/* Wait for response */
owngil_header_t resp;
read(w->result_pipe[0], &resp, sizeof(resp));

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Documentation

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Lint

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.12 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.13 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Free-threaded Python 3.13t

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.13

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.12

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Documentation

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Lint

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.13 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.12 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Free-threaded Python 3.13t

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.13

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6362 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.12

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
}

pthread_mutex_unlock(&w->dispatch_mutex);
Expand Down Expand Up @@ -6764,6 +6764,12 @@
/* Per-process namespace NIFs */
{"event_loop_exec", 2, nif_event_loop_exec, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"event_loop_eval", 2, nif_event_loop_eval, ERL_NIF_DIRTY_JOB_IO_BOUND},
/* Module import caching NIFs */
{"loop_import_module", 2, nif_loop_import_module, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"loop_import_function", 3, nif_loop_import_function, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"loop_flush_import_cache", 1, nif_loop_flush_import_cache, 0},
{"loop_import_stats", 1, nif_loop_import_stats, 0},
{"loop_import_list", 1, nif_loop_import_list, 0},
{"add_reader", 3, nif_add_reader, 0},
{"remove_reader", 2, nif_remove_reader, 0},
{"add_writer", 3, nif_add_writer, 0},
Expand Down
93 changes: 93 additions & 0 deletions src/py.erl
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
stream/4,
stream_eval/1,
stream_eval/2,
%% Module import caching
import/1,
import/2,
flush_imports/0,
import_stats/0,
import_list/0,
version/0,
memory_stats/0,
gc/0,
Expand Down Expand Up @@ -327,6 +333,93 @@ exec(Ctx, Code) when is_pid(Ctx) ->
EnvRef = get_local_env(Ctx),
py_context:exec(Ctx, Code, EnvRef).

%%% ============================================================================
%%% Module Import Caching
%%% ============================================================================

%% @doc Import and cache a module in the current interpreter.
%%
%% The module is imported in the interpreter handling this process (via affinity).
%% The `__main__' module is never cached in the interpreter cache.
%%
%% This is useful for pre-warming imports before making calls, ensuring the
%% first call doesn't pay the import penalty.
%%
%% Example:
%% ```
%% ok = py:import(json),
%% {ok, Result} = py:call(json, dumps, [Data]). %% Uses cached module
%% '''
%%
%% @param Module Python module name
%% @returns ok | {error, Reason}
-spec import(py_module()) -> ok | {error, term()}.
import(Module) ->
py_event_loop_pool:import(Module).

%% @doc Import and cache a module function in the current interpreter.
%%
%% Pre-imports the module and caches the function reference for faster
%% subsequent calls. The `__main__' module is never cached.
%%
%% Example:
%% ```
%% ok = py:import(json, dumps),
%% {ok, Result} = py:call(json, dumps, [Data]). %% Uses cached function
%% '''
%%
%% @param Module Python module name
%% @param Func Function name to cache
%% @returns ok | {error, Reason}
-spec import(py_module(), py_func()) -> ok | {error, term()}.
import(Module, Func) ->
py_event_loop_pool:import(Module, Func).

%% @doc Flush import caches across all interpreters.
%%
%% Clears the module/function cache in all interpreters. Use this after
%% modifying Python modules on disk to force re-import.
%%
%% @returns ok
-spec flush_imports() -> ok.
flush_imports() ->
py_event_loop_pool:flush_imports().

%% @doc Get import cache statistics for the current interpreter.
%%
%% Returns a map with cache metrics for the interpreter handling this process.
%%
%% Example:
%% ```
%% {ok, #{count => 5}} = py:import_stats().
%% '''
%%
%% @returns {ok, Stats} where Stats is a map with cache metrics
-spec import_stats() -> {ok, map()} | {error, term()}.
import_stats() ->
py_event_loop_pool:import_stats().

%% @doc List all cached imports in the current interpreter.
%%
%% Returns a map of modules to their cached functions.
%% Module names are binary keys, function lists are the values.
%% An empty list means only the module is cached (no specific functions).
%%
%% Example:
%% ```
%% ok = py:import(json),
%% ok = py:import(json, dumps),
%% ok = py:import(json, loads),
%% ok = py:import(math),
%% {ok, #{<<"json">> => [<<"dumps">>, <<"loads">>],
%% <<"math">> => []}} = py:import_list().
%% '''
%%
%% @returns {ok, #{Module => [Func]}} map of modules to functions
-spec import_list() -> {ok, #{binary() => [binary()]}} | {error, term()}.
import_list() ->
py_event_loop_pool:import_list().

%%% ============================================================================
%%% Asynchronous API
%%% ============================================================================
Expand Down
97 changes: 96 additions & 1 deletion src/py_event_loop_pool.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@
await/1, await/2,
%% Per-process namespace API
exec/1, exec/2,
eval/1, eval/2
eval/1, eval/2,
%% Module import caching
import/1, import/2,
flush_imports/0,
import_stats/0,
import_list/0,
get_all_loops/0
]).

%% Legacy API
Expand Down Expand Up @@ -332,6 +338,95 @@ eval(Expr) ->
eval(LoopRef, Expr) ->
py_nif:event_loop_eval(LoopRef, Expr).

%%% ============================================================================
%%% Module Import Caching
%%% ============================================================================

%% @doc Import and cache a module in the current interpreter.
%%
%% The module is imported in the interpreter assigned to this process (via
%% PID hash affinity). The `__main__' module is never cached.
%%
%% Example:
%% <pre>
%% ok = py_event_loop_pool:import(json),
%% Ref = py_event_loop_pool:create_task(json, dumps, [[1,2,3]])
%% </pre>
-spec import(Module :: atom() | binary()) -> ok | {error, term()}.
import(Module) ->
case get_loop() of
{ok, LoopRef} ->
ModuleBin = py_util:to_binary(Module),
py_nif:loop_import_module(LoopRef, ModuleBin);
{error, not_available} ->
{error, event_loop_not_available}
end.

%% @doc Import a module and cache a specific function.
%%
%% Pre-imports the module and caches the function reference for faster
%% subsequent calls. The `__main__' module is never cached.
-spec import(Module :: atom() | binary(), Func :: atom() | binary()) -> ok | {error, term()}.
import(Module, Func) ->
case get_loop() of
{ok, LoopRef} ->
ModuleBin = py_util:to_binary(Module),
FuncBin = py_util:to_binary(Func),
py_nif:loop_import_function(LoopRef, ModuleBin, FuncBin);
{error, not_available} ->
{error, event_loop_not_available}
end.

%% @doc Flush import caches across all event loop interpreters.
%%
%% Clears the module/function cache in all interpreters. Use this after
%% modifying Python modules on disk to force re-import.
-spec flush_imports() -> ok.
flush_imports() ->
case get_all_loops() of
{ok, Loops} ->
[py_nif:loop_flush_import_cache(LoopRef) || {LoopRef, _} <- Loops],
ok;
{error, _} ->
ok
end.

%% @doc Get import cache statistics for the current interpreter.
%%
%% Returns a map with cache metrics.
-spec import_stats() -> {ok, map()} | {error, term()}.
import_stats() ->
case get_loop() of
{ok, LoopRef} ->
py_nif:loop_import_stats(LoopRef);
{error, not_available} ->
{error, event_loop_not_available}
end.

%% @doc List all cached imports in the current interpreter.
%%
%% Returns a list of cached module and function names.
-spec import_list() -> {ok, [binary()]} | {error, term()}.
import_list() ->
case get_loop() of
{ok, LoopRef} ->
py_nif:loop_import_list(LoopRef);
{error, not_available} ->
{error, event_loop_not_available}
end.

%% @doc Get all event loop references in the pool.
%%
%% Returns a list of {LoopRef, WorkerPid} tuples for all loops in the pool.
-spec get_all_loops() -> {ok, [{reference(), pid()}]} | {error, not_available}.
get_all_loops() ->
case pool_size() of
0 -> {error, not_available};
N ->
Loops = persistent_term:get(?PT_LOOPS),
{ok, [element(Idx, Loops) || Idx <- lists:seq(1, N)]}
end.

%%% ============================================================================
%%% Legacy API
%%% ============================================================================
Expand Down
67 changes: 67 additions & 0 deletions src/py_nif.erl
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
%% Per-process namespace NIFs
event_loop_exec/2,
event_loop_eval/2,
%% Module import caching NIFs
loop_import_module/2,
loop_import_function/3,
loop_flush_import_cache/1,
loop_import_stats/1,
loop_import_list/1,
add_reader/3,
remove_reader/2,
add_writer/3,
Expand Down Expand Up @@ -846,6 +852,67 @@ event_loop_exec(_LoopRef, _Code) ->
event_loop_eval(_LoopRef, _Expr) ->
?NIF_STUB.

%%% ============================================================================
%%% Module Import Caching
%%% ============================================================================

%% @doc Import and cache a module in the event loop's interpreter.
%%
%% Pre-imports the module and caches it for faster subsequent calls.
%% The `__main__' module is never cached (returns error).
%%
%% @param LoopRef Event loop reference
%% @param Module Module name as binary
%% @returns ok | {error, Reason}
-spec loop_import_module(reference(), binary()) -> ok | {error, term()}.
loop_import_module(_LoopRef, _Module) ->
?NIF_STUB.

%% @doc Import a module and cache a specific function.
%%
%% Pre-imports the module and caches the function reference for faster
%% subsequent calls. The `__main__' module is never cached (returns error).
%%
%% @param LoopRef Event loop reference
%% @param Module Module name as binary
%% @param Func Function name as binary
%% @returns ok | {error, Reason}
-spec loop_import_function(reference(), binary(), binary()) -> ok | {error, term()}.
loop_import_function(_LoopRef, _Module, _Func) ->
?NIF_STUB.

%% @doc Flush the import cache for an event loop's interpreter.
%%
%% Clears the module/function cache. Use this after modifying Python
%% modules on disk to force re-import.
%%
%% @param LoopRef Event loop reference
%% @returns ok
-spec loop_flush_import_cache(reference()) -> ok.
loop_flush_import_cache(_LoopRef) ->
?NIF_STUB.

%% @doc Get import cache statistics for an event loop's interpreter.
%%
%% Returns a map with cache metrics for the calling process's namespace.
%%
%% @param LoopRef Event loop reference
%% @returns {ok, Stats} where Stats is a map with count
-spec loop_import_stats(reference()) -> {ok, map()} | {error, term()}.
loop_import_stats(_LoopRef) ->
?NIF_STUB.

%% @doc List all cached imports in an event loop's interpreter.
%%
%% Returns a map of modules to their cached functions for the calling
%% process's namespace.
%%
%% @param LoopRef Event loop reference
%% @returns {ok, #{Module => [Func]}} map of modules to functions
-spec loop_import_list(reference()) -> {ok, #{binary() => [binary()]}} | {error, term()}.
loop_import_list(_LoopRef) ->
?NIF_STUB.

%% @doc Register a file descriptor for read monitoring.
%% Uses enif_select to register with the Erlang scheduler.
-spec add_reader(reference(), integer(), non_neg_integer()) ->
Expand Down
Loading
Loading