Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.7.0-beta.0] - Unreleased

### Added

- Added support for integer parts-per-second timeunit, with `badarg` raised on int64 overflow

## [0.7.0-alpha.1] - 2026-04-06

### Added
Expand Down
2 changes: 1 addition & 1 deletion libs/estdlib/src/erlang.erl
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
-type atom_encoding() :: latin1 | utf8 | unicode.

-type mem_type() :: binary.
-type time_unit() :: second | millisecond | microsecond | nanosecond | native.
-type time_unit() :: second | millisecond | microsecond | nanosecond | native | pos_integer().
-type timestamp() :: {
MegaSecs :: non_neg_integer(), Secs :: non_neg_integer(), MicroSecs :: non_neg_integer
}.
Expand Down
1 change: 1 addition & 0 deletions libs/exavmlib/lib/System.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule System do
| :millisecond
| :microsecond
| :nanosecond
| pos_integer()

@doc """
Returns the current monotonic time in the `:native` time unit.
Expand Down
181 changes: 125 additions & 56 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1841,66 +1841,142 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[])
return term_from_ref_ticks(ref_ticks, &ctx->heap);
}

term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
static bool time_unit_to_parts_per_second(term unit, avm_int64_t *parts_per_second)
{
UNUSED(ctx);

term unit;
if (argc == 0) {
unit = NATIVE_ATOM;
if (unit == SECOND_ATOM) {
*parts_per_second = 1;
} else if (unit == MILLISECOND_ATOM) {
*parts_per_second = 1000;
} else if (unit == MICROSECOND_ATOM) {
*parts_per_second = INT64_C(1000000);
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
*parts_per_second = INT64_C(1000000000);
} else if (term_is_int64(unit)) {
*parts_per_second = term_maybe_unbox_int64(unit);
if (UNLIKELY(*parts_per_second <= 0)) {
return false;
}
} else {
unit = argv[0];
return false;
}

struct timespec ts;
sys_monotonic_time(&ts);
return true;
}

if (unit == SECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ts.tv_sec);
// Convert nanoseconds to parts using: parts = nanoseconds * pps / 1e9
// Splits into high/low to avoid intermediate overflow.
// Caller must ensure 0 <= nanoseconds < 1e9 and pps > 0.
static bool nanoseconds_to_parts_per_second(
avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
{
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
avm_int64_t fractional_high = nanoseconds * quotient;
avm_int64_t remainder_product = nanoseconds * remainder;
avm_int64_t fractional_low = remainder_product / INT64_C(1000000000);

} else if (unit == MILLISECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
if (round_up && (remainder_product % INT64_C(1000000000)) != 0) {
fractional_low += 1;
}

} else if (unit == MICROSECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
return false;
}

} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
*parts = fractional_high + fractional_low;
return true;
}

} else {
RAISE_ERROR(BADARG_ATOM);
// Convert a normalized timespec (0 <= tv_nsec < 1e9) to integer parts.
// Uses floor semantics for negative timestamps with non-zero tv_nsec.
static bool timespec_to_parts_per_second(
const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts)
{
avm_int64_t seconds = (avm_int64_t) ts->tv_sec;
avm_int64_t fractional_part;

if (ts->tv_nsec == 0 || seconds >= 0) {
if (UNLIKELY(
((seconds > 0) && (seconds > INT64_MAX / parts_per_second))
|| ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) {
return false;
}

if (UNLIKELY(!nanoseconds_to_parts_per_second(
(avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) {
return false;
}

avm_int64_t second_part = seconds * parts_per_second;
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
return false;
}

*parts = second_part + fractional_part;
return true;
}

// Preserve floor semantics for normalized negative timespecs such as {-2, 999999999}.
avm_int64_t adjusted_seconds = seconds + 1;
if (UNLIKELY(adjusted_seconds < INT64_MIN / parts_per_second)) {
return false;
}

if (UNLIKELY(!nanoseconds_to_parts_per_second(
INT64_C(1000000000) - (avm_int64_t) ts->tv_nsec, parts_per_second, true,
&fractional_part))) {
return false;
}

avm_int64_t second_part = adjusted_seconds * parts_per_second;
if (UNLIKELY(second_part < INT64_MIN + fractional_part)) {
return false;
}

*parts = second_part - fractional_part;
return true;
}

term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
static term make_time_in_unit(Context *ctx, term unit, void (*time_fun)(struct timespec *))
{
UNUSED(ctx);
avm_int64_t parts_per_second;
if (UNLIKELY(!time_unit_to_parts_per_second(unit, &parts_per_second))) {
RAISE_ERROR(BADARG_ATOM);
}

struct timespec ts;
time_fun(&ts);

avm_int64_t value;
if (UNLIKELY(!timespec_to_parts_per_second(&ts, parts_per_second, &value))) {
RAISE_ERROR(BADARG_ATOM);
}

return make_maybe_boxed_int64(ctx, value);
}

term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
{
term unit;
if (argc == 0) {
unit = NATIVE_ATOM;
} else {
unit = argv[0];
}

struct timespec ts;
sys_time(&ts);

if (unit == SECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ts.tv_sec);

} else if (unit == MILLISECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);

} else if (unit == MICROSECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);

} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
return make_time_in_unit(ctx, unit, sys_monotonic_time);
}

term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
{
term unit;
if (argc == 0) {
unit = NATIVE_ATOM;
} else {
RAISE_ERROR(BADARG_ATOM);
unit = argv[0];
}

return make_time_in_unit(ctx, unit, sys_time);
}

static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
Expand Down Expand Up @@ -2007,33 +2083,26 @@ term nif_erlang_timestamp_0(Context *ctx, int argc, term argv[])

term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term argv[])
{
UNUSED(ctx);
UNUSED(argc);

struct timespec ts;

if (UNLIKELY(!term_is_int64(argv[0]))) {
RAISE_ERROR(BADARG_ATOM);
}
avm_int64_t value = term_maybe_unbox_int64(argv[0]);

if (argv[1] == SECOND_ATOM) {
ts.tv_sec = (time_t) value;
ts.tv_nsec = 0;

} else if (argv[1] == MILLISECOND_ATOM) {
ts.tv_sec = (time_t) (value / 1000);
ts.tv_nsec = (value % 1000) * 1000000;

} else if (argv[1] == MICROSECOND_ATOM) {
ts.tv_sec = (time_t) (value / 1000000);
ts.tv_nsec = (value % 1000000) * 1000;

} else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) {
ts.tv_sec = (time_t) (value / INT64_C(1000000000));
ts.tv_nsec = value % INT64_C(1000000000);

} else {
avm_int64_t parts_per_second;
if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) {
RAISE_ERROR(BADARG_ATOM);
}

// Floor division: round negative fractional seconds toward negative infinity
avm_int64_t quotient = value / parts_per_second;
avm_int64_t remainder = value % parts_per_second;
struct timespec ts = {
.tv_sec = (time_t) (quotient - (remainder < 0)),
.tv_nsec = 0
};

struct tm broken_down_time;
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
}
Expand Down
64 changes: 64 additions & 0 deletions tests/erlang_tests/test_monotonic_time.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ start() ->
true = is_integer(N2 - N1) andalso (N2 - N1) >= 0,

ok = test_native_monotonic_time(),
ok = test_integer_time_unit(),
ok = test_non_power_of_10_integer_time_unit(),
ok = test_bad_integer_time_unit(),

1.

Expand All @@ -46,6 +49,15 @@ test_diff(X) when is_integer(X) andalso X >= 0 ->
test_diff(X) when X < 0 ->
0.

expect(F, Expect) ->
try
F(),
fail
catch
_:E when E == Expect ->
ok
end.

test_native_monotonic_time() ->
Na1 = erlang:monotonic_time(native),
receive
Expand All @@ -54,3 +66,55 @@ test_native_monotonic_time() ->
Na2 = erlang:monotonic_time(native),
true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0,
ok.

test_integer_time_unit() ->
%% integer 1 = parts per second, equivalent to second
S = erlang:monotonic_time(second),
S1 = erlang:monotonic_time(1),
true = abs(S1 - S) =< 1,

%% integer 1000 = parts per second, equivalent to millisecond
Ms = erlang:monotonic_time(millisecond),
Ms1 = erlang:monotonic_time(1000),
true = abs(Ms1 - Ms) =< 1,

%% integer 1000000 = parts per second, equivalent to microsecond
Us = erlang:monotonic_time(microsecond),
Us1 = erlang:monotonic_time(1000000),
true = abs(Us1 - Us) =< 1000,

%% integer 1000000000 = parts per second, equivalent to nanosecond
Ns = erlang:monotonic_time(nanosecond),
Ns1 = erlang:monotonic_time(1000000000),
true = abs(Ns1 - Ns) =< 1000000,

%% verify monotonicity with integer unit
T1 = erlang:monotonic_time(1000),
receive
after 1 -> ok
end,
T2 = erlang:monotonic_time(1000),
true = T2 >= T1,

ok.

test_non_power_of_10_integer_time_unit() ->
ok = test_integer_time_unit_monotonicity(256),
ok = test_integer_time_unit_monotonicity(48000),
ok.

test_bad_integer_time_unit() ->
ok = expect(fun() -> erlang:monotonic_time(0) end, badarg),
ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg),
ok.

test_integer_time_unit_monotonicity(PartsPerSecond) ->
T1 = erlang:monotonic_time(PartsPerSecond),
receive
after 1 -> ok
end,
T2 = erlang:monotonic_time(PartsPerSecond),
true = is_integer(T1),
true = is_integer(T2),
true = T2 >= T1,
ok.
Loading
Loading