From 16474a76722f5cea4371e50c630c0972a028d9cc Mon Sep 17 00:00:00 2001 From: Franciszek Kubis Date: Mon, 25 Aug 2025 14:44:29 +0200 Subject: [PATCH] Implemented erlang:bump_reductions nif. --- libs/estdlib/src/erlang.erl | 18 +++++++- src/libAtomVM/context.c | 21 +++++++++ src/libAtomVM/context.h | 2 + src/libAtomVM/defaultatoms.def | 1 + src/libAtomVM/nifs.c | 15 +++++++ src/libAtomVM/nifs.gperf | 1 + src/libAtomVM/opcodesswitch.h | 48 ++++++++++---------- tests/erlang_tests/CMakeLists.txt | 5 +++ tests/erlang_tests/test_bump_reductions.erl | 49 +++++++++++++++++++++ tests/test-structs.c | 5 +++ tests/test.c | 2 + 11 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 tests/erlang_tests/test_bump_reductions.erl diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index fde10c642..b495a1464 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -131,7 +131,8 @@ dist_ctrl_get_data/1, dist_ctrl_put_data/2, unique_integer/0, - unique_integer/1 + unique_integer/1, + bump_reductions/1 ]). -export_type([ @@ -272,7 +273,8 @@ send_after(Time, Dest, Msg) -> (Pid :: pid(), message_queue_len) -> {message_queue_len, non_neg_integer()}; (Pid :: pid(), memory) -> {memory, non_neg_integer()}; (Pid :: pid(), links) -> {links, [pid()]}; - (Pid :: pid(), monitored_by) -> {monitored_by, [pid() | resource() | port()]}. + (Pid :: pid(), monitored_by) -> {monitored_by, [pid() | resource() | port()]}; + (Pid :: pid(), reductions) -> {reductions, [pos_integer()]}. process_info(_Pid, _Key) -> erlang:nif_error(undefined). @@ -1529,3 +1531,15 @@ unique_integer(_Options) -> -spec nif_error(Reason :: any()) -> no_return(). nif_error(_Reason) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Reductions an integer representing a value of which the reduction counter +%% will be incremented by. +%% @returns true +%% @doc Increments the reduction counter for the calling process, a context switch is +%% forced when the counter reaches the maximum number of reductions for a process. +%% @end +%%----------------------------------------------------------------------------- +-spec bump_reductions(pos_integer()) -> true. +bump_reductions(Reductions) when is_integer(Reductions), Reductions > 1 -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 3a73ac055..89f2ba326 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -28,6 +28,7 @@ #include "erl_nif.h" #include "erl_nif_priv.h" #include "globalcontext.h" +#include "limits.h" #include "list.h" #include "mailbox.h" #include "smp.h" @@ -89,6 +90,8 @@ Context *context_new(GlobalContext *glb) ctx->native_handler = NULL; + ctx->reductions = 0; + ctx->saved_module = NULL; ctx->saved_ip = NULL; ctx->restore_trap_handler = NULL; @@ -471,6 +474,7 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a case MESSAGE_QUEUE_LEN_ATOM: case REGISTERED_NAME_ATOM: case MEMORY_ATOM: + case REDUCTIONS_ATOM: ret_size = TUPLE_SIZE(2); break; case LINKS_ATOM: { @@ -626,6 +630,23 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a break; } + case REDUCTIONS_ATOM: { + term_put_tuple_element(ret, 0, REDUCTIONS_ATOM); + if (UNLIKELY((uint64_t) LLONG_MAX < ctx->reductions)) { + *out = BADARG_ATOM; + return false; + } + int64_t reductions = (int64_t) ctx->reductions; + size_t reductions_size = term_boxed_integer_size(reductions); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, reductions_size, 1, &ret, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + *out = OUT_OF_MEMORY_ATOM; + return false; + } + term reductions_term = term_make_maybe_boxed_int64(reductions, &ctx->heap); + term_put_tuple_element(ret, 1, reductions_term); + break; + } + default: UNREACHABLE(); } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index 496e60daa..4cd2a77a4 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -125,6 +125,8 @@ struct Context // Ports support native_handler_f native_handler; + uint64_t reductions; + unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index bff6e87fa..c70ae6817 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -50,6 +50,7 @@ X(PORT_COUNT_ATOM, "\xA", "port_count") X(ATOM_COUNT_ATOM, "\xA", "atom_count") X(SYSTEM_ARCHITECTURE_ATOM, "\x13", "system_architecture") X(WORDSIZE_ATOM, "\x8", "wordsize") +X(REDUCTIONS_ATOM, "\xA", "reductions") X(DECIMALS_ATOM, "\x8", "decimals") X(SCIENTIFIC_ATOM, "\xA", "scientific") diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 819418dfa..a179c618c 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -202,6 +202,7 @@ static term nif_unicode_characters_to_list(Context *ctx, int argc, term argv[]); static term nif_unicode_characters_to_binary(Context *ctx, int argc, term argv[]); static term nif_erlang_lists_subtract(Context *ctx, int argc, term argv[]); static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]); +static term nif_erlang_bump_reductions_1(Context *ctx, int argc, term argv[]); #define DECLARE_MATH_NIF_FUN(moniker) \ static term nif_math_##moniker(Context *ctx, int argc, term argv[]); @@ -875,6 +876,11 @@ static const struct Nif zlib_compress_nif = }; +static const struct Nif erlang_bump_reductions_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_bump_reductions_1 +}; + #define DEFINE_MATH_NIF(moniker) \ static const struct Nif math_##moniker##_nif = \ { \ @@ -5920,6 +5926,15 @@ static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]) } #endif +static term nif_erlang_bump_reductions_1(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + VALIDATE_VALUE(argv[0], term_is_integer); + int64_t reductions_to_bump = term_to_int(argv[0]) - 1; + ctx->reductions += reductions_to_bump; + return TRUE_ATOM; +} + // // MAINTENANCE NOTE: Exception handling for fp operations using math // error handling is designed to be thread-safe, as errors are specified diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index bd8d30284..62d923b12 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -211,3 +211,4 @@ math:sqrt/1, &math_sqrt_nif math:tan/1, &math_tan_nif math:tanh/1, &math_tanh_nif zlib:compress/1, &zlib_compress_nif +erlang:bump_reductions/1, &erlang_bump_reductions_nif diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 69d55514e..981066b2e 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -1767,7 +1767,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) Module *prev_mod; term *x_regs; const uint8_t *pc; - int remaining_reductions; + uint64_t max_reductions; Context *ctx = scheduler_run(glb); @@ -1780,7 +1780,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) code = mod->code->code; x_regs = ctx->x; pc = (ctx->saved_ip); - remaining_reductions = DEFAULT_REDUCTIONS_AMOUNT; + max_reductions = ctx->reductions + DEFAULT_REDUCTIONS_AMOUNT; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" @@ -1864,8 +1864,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) #ifdef IMPL_EXECUTE_LOOP ctx->cp = module_address(mod->module_index, pc - code); - remaining_reductions--; - if (LIKELY(remaining_reductions)) { + ++ctx->reductions; + if (LIKELY(ctx->reductions < max_reductions)) { TRACE_CALL(ctx, mod, "call", label, arity); JUMP_TO_ADDRESS(mod->labels[label]); } else { @@ -1895,8 +1895,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) DEBUG_DUMP_STACK(ctx); - remaining_reductions--; - if (LIKELY(remaining_reductions)) { + ++ctx->reductions; + if (LIKELY(ctx->reductions < max_reductions)) { TRACE_CALL(ctx, mod, "call_last", label, arity); JUMP_TO_ADDRESS(mod->labels[label]); } else { @@ -1917,8 +1917,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) USED_BY_TRACE(label); #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (LIKELY(remaining_reductions)) { + ++ctx->reductions; + if (LIKELY(ctx->reductions < max_reductions)) { TRACE_CALL(ctx, mod, "call_only", label, arity); JUMP_TO_ADDRESS(mod->labels[label]); } else { @@ -1933,8 +1933,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) // save pc in case of error const uint8_t *orig_pc = pc - 1; - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, orig_pc); } #endif @@ -2041,8 +2041,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) // save pc in case of error const uint8_t *orig_pc = pc - 1; - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, orig_pc); } #endif @@ -3240,8 +3240,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) USED_BY_TRACE(label); #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (LIKELY(remaining_reductions)) { + ++ctx->reductions; + if (LIKELY(ctx->reductions < max_reductions)) { JUMP_TO_ADDRESS(mod->labels[label]); } else { SCHEDULE_NEXT(mod, mod->labels[label]); @@ -3477,8 +3477,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) case OP_CALL_FUN: { #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, pc - 1); } #endif @@ -3533,8 +3533,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) case OP_CALL_EXT_ONLY: { #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, pc - 1); } #endif @@ -5233,8 +5233,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) // save pc in case of error const uint8_t *orig_pc = pc - 1; - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, orig_pc); } #endif @@ -5286,8 +5286,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) case OP_APPLY_LAST: { #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, pc - 1); } #endif @@ -6793,8 +6793,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) case OP_CALL_FUN2: { #ifdef IMPL_EXECUTE_LOOP - remaining_reductions--; - if (UNLIKELY(!remaining_reductions)) { + ++ctx->reductions; + if (UNLIKELY(ctx->reductions >= max_reductions)) { SCHEDULE_NEXT(mod, pc - 1); } #endif diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 8aeb8b9fd..c215ba7a9 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -546,6 +546,8 @@ compile_erlang(test_raw_raise) compile_erlang(test_ets) compile_erlang(test_node) +compile_erlang(test_bump_reductions) + compile_erlang(test_op_bs_start_match) compile_assembler(test_op_bs_start_match_asm) compile_erlang(test_op_bs_create_bin) @@ -1068,6 +1070,9 @@ add_custom_target(erlang_test_modules DEPENDS test_raw_raise.beam test_ets.beam + + test_bump_reductions.beam + test_node.beam test_op_bs_start_match.beam diff --git a/tests/erlang_tests/test_bump_reductions.erl b/tests/erlang_tests/test_bump_reductions.erl new file mode 100644 index 000000000..16b87652b --- /dev/null +++ b/tests/erlang_tests/test_bump_reductions.erl @@ -0,0 +1,49 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Franciszek Kubis +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_bump_reductions). + +-export([start/0, process_test/0]). + +start() -> + {reductions, 1} = erlang:process_info(self(), reductions), + erlang:bump_reductions(500), + {reductions, 502} = erlang:process_info(self(), reductions), + Pid = erlang:spawn_opt(fun() -> process_test() end, [link]), + Pid ! {ready, self()}, + receive + {r1, {reductions, Reductions}} -> + 2 = Reductions + end, + receive + {r2, {reductions, Reductions2}} -> + 1029 = Reductions2 + end, + erlang:bump_reductions(2000), + {reductions, 2505} = erlang:process_info(self(), reductions), + 0. + +process_test() -> + receive + {ready, Pid} -> + Pid ! {r1, erlang:process_info(self(), reductions)}, + erlang:bump_reductions(1025), + Pid ! {r2, erlang:process_info(self(), reductions)} + end. diff --git a/tests/test-structs.c b/tests/test-structs.c index c244e8028..88eecfadd 100644 --- a/tests/test-structs.c +++ b/tests/test-structs.c @@ -159,6 +159,8 @@ static const char *const bounded_free_atom = "\xC" "bounded_free"; static const char *const minimum_atom = "\x7" "minimum"; static const char *const fibonacci_atom = "\x9" "fibonacci"; +static const char *const reductions_atom = "\xA" "reductions"; + void test_valueshashtable() { struct ValuesHashTable *htable = valueshashtable_new(); @@ -429,6 +431,9 @@ atom_index_t insert_atoms_into_atom_table(struct AtomTable *table) assert(r == AtomTableEnsureAtomOk); r = atom_table_ensure_atom(table, (const uint8_t *) fibonacci_atom + 1, fibonacci_atom[0], AtomTableNoOpts, &global_atom_index); + assert(r == AtomTableEnsureAtomOk); + r = atom_table_ensure_atom(table, (const uint8_t *) reductions_atom + 1, reductions_atom[0], AtomTableNoOpts, &global_atom_index); + return decimals_index; } diff --git a/tests/test.c b/tests/test.c index f322316c3..54265b842 100644 --- a/tests/test.c +++ b/tests/test.c @@ -598,6 +598,8 @@ struct Test tests[] = { TEST_CASE(test_ets), TEST_CASE(test_node), + TEST_CASE(test_bump_reductions), + // TEST CRASHES HERE: TEST_CASE(memlimit), { NULL, 0, false, false }