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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for configuring pins and width for sdmmc on ESP32
- Added support for map comprehensions
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
- Added support for integer parts-per-second time unit, with `badarg` raised on int64 overflow

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
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
228 changes: 174 additions & 54 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1851,70 +1851,185 @@ 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) {
// AtomVM exposes the Erlang `native` time unit as nanoseconds on all platforms.
*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.
static bool nanoseconds_to_parts_per_second(
avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
{
if (UNLIKELY(
nanoseconds < 0 || nanoseconds >= INT64_C(1000000000) || parts_per_second <= 0)) {
return false;
}

} else if (unit == MILLISECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
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 == MICROSECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
if (round_up && (remainder_product % INT64_C(1000000000)) != 0) {
fractional_low += 1;
}

} 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);
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
return false;
}

} else {
RAISE_ERROR(BADARG_ATOM);
*parts = fractional_high + fractional_low;
return true;
}

// 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)
{
if (UNLIKELY(
parts_per_second <= 0 || ts->tv_nsec < 0 || ts->tv_nsec >= INT64_C(1000000000))) {
return false;
}

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);
return make_time_in_unit(ctx, unit, sys_monotonic_time);
}

if (unit == SECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ts.tv_sec);
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
{
term unit;
if (argc == 0) {
unit = NATIVE_ATOM;
} else {
unit = argv[0];
}

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

} else if (unit == MICROSECOND_ATOM) {
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
static bool int64_to_time_t_checked(avm_int64_t seconds, time_t *out)
{
if (((time_t) -1) > (time_t) 0) {
if (seconds < 0) {
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);
time_t time_seconds = (time_t) (uint64_t) seconds;
if ((uint64_t) time_seconds != (uint64_t) seconds) {
return false;
}
*out = time_seconds;
return true;
}

} else {
RAISE_ERROR(BADARG_ATOM);
time_t time_seconds = (time_t) seconds;
if ((avm_int64_t) time_seconds != seconds) {
return false;
}
*out = time_seconds;
return true;
}

static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
{
avm_int64_t year = (avm_int64_t) broken_down_time->tm_year + 1900;
// Avoid passing a negative value to term_from_int11(), which left-shifts its signed argument.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this comment.

if (UNLIKELY(year < 0 || year > INT16_MAX)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rather check [AVM_INT_MIN, AVM_INT_MAX] range.

RAISE_ERROR(BADARG_ATOM);
}

// 4 = size of date/time tuple, 3 size of date time tuple
if (UNLIKELY(memory_ensure_free_opt(ctx, 3 + 4 + 4, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
Expand All @@ -1923,7 +2038,7 @@ static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
term time_tuple = term_alloc_tuple(3, &ctx->heap);
term date_time_tuple = term_alloc_tuple(2, &ctx->heap);

term_put_tuple_element(date_tuple, 0, term_from_int11(1900 + broken_down_time->tm_year));
term_put_tuple_element(date_tuple, 0, term_from_int11((int16_t) year));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use term_from_int()

term_put_tuple_element(date_tuple, 1, term_from_int11(broken_down_time->tm_mon + 1));
term_put_tuple_element(date_tuple, 2, term_from_int11(broken_down_time->tm_mday));

Expand All @@ -1946,7 +2061,12 @@ term nif_erlang_universaltime_0(Context *ctx, int argc, term argv[])
sys_time(&ts);

struct tm broken_down_time;
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
struct tm *universal_time = gmtime_r(&ts.tv_sec, &broken_down_time);
if (UNLIKELY(universal_time == NULL)) {
RAISE_ERROR(BADARG_ATOM);
}

return build_datetime_from_tm(ctx, universal_time);
}

// setenv leaks the prior "TZ=value" string on overwrite (unbounded on
Expand Down Expand Up @@ -2095,35 +2215,35 @@ 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;
avm_int64_t parts_per_second;
if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) {
RAISE_ERROR(BADARG_ATOM);
}

} 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);
// 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;
avm_int64_t seconds = quotient - (remainder < 0);

} else {
time_t time_seconds;
if (UNLIKELY(!int64_to_time_t_checked(seconds, &time_seconds))) {
RAISE_ERROR(BADARG_ATOM);
}

struct tm broken_down_time;
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
struct tm *universal_time = gmtime_r(&time_seconds, &broken_down_time);
if (UNLIKELY(universal_time == NULL)) {
RAISE_ERROR(BADARG_ATOM);
}

return build_datetime_from_tm(ctx, universal_time);
}

static term nif_os_getenv_1(Context *ctx, int argc, term argv[])
Expand Down
59 changes: 59 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,50 @@ test_native_monotonic_time() ->
Na2 = erlang:monotonic_time(native),
true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0,
ok.

test_integer_time_unit() ->
S = erlang:monotonic_time(second),
S1 = erlang:monotonic_time(1),
true = abs(S1 - S) =< 1,

Ms = erlang:monotonic_time(millisecond),
Ms1 = erlang:monotonic_time(1000),
true = abs(Ms1 - Ms) =< 1,

Us = erlang:monotonic_time(microsecond),
Us1 = erlang:monotonic_time(1000000),
true = abs(Us1 - Us) =< 1000,

Ns = erlang:monotonic_time(nanosecond),
Ns1 = erlang:monotonic_time(1000000000),
true = abs(Ns1 - Ns) =< 1000000,

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