diff --git a/.ci/bindcov.sh b/.ci/bindcov.sh index 02d948c7..da718f8f 100755 --- a/.ci/bindcov.sh +++ b/.ci/bindcov.sh @@ -3,6 +3,10 @@ unbound_functions=0 skipped=() +# waiting on these being documented +# https://github.com/libuv/libuv/issues/4007 +skipped+=(uv_os_get_passwd2 uv_os_get_group uv_os_free_group) + # false positives skipped+=(uv_thread_create uv_thread_create_ex) diff --git a/.ci/pthread_create.supp b/.ci/pthread_create.supp deleted file mode 100644 index 0160dbb0..00000000 --- a/.ci/pthread_create.supp +++ /dev/null @@ -1,9 +0,0 @@ -{ - pthread_create unknown leak - Memcheck:Leak - match-leak-kinds: possible - fun:calloc - fun:allocate_dtv - fun:_dl_allocate_tls - fun:allocate_stack -} \ No newline at end of file diff --git a/.ci/valgrind_mem.supp b/.ci/valgrind_mem.supp new file mode 100644 index 00000000..fae922e1 --- /dev/null +++ b/.ci/valgrind_mem.supp @@ -0,0 +1,71 @@ +{ + luv_push_stats_table + Memcheck:Cond + fun:luv_push_stats_table +} +{ + tostringbuff + Memcheck:Value8 + fun:_itoa_word + fun:__vfprintf_internal + fun:__vsnprintf_internal + fun:snprintf + fun:tostringbuff +} +{ + tostringbuff Conditional jump or move depends on uninitialised value(s) + Memcheck:Cond + fun:_itoa_word + fun:__vfprintf_internal + fun:__vsnprintf_internal + fun:snprintf + fun:tostringbuff +} +{ + addnum2buff Conditional jump or move depends on uninitialised value(s) + Memcheck:Cond + fun:__vfprintf_internal + fun:__vsnprintf_internal + fun:snprintf + fun:tostringbuff + fun:addnum2buff + fun:luaO_pushvfstring + fun:lua_pushfstring + fun:luaL_tolstring + fun:luaB_tostring + fun:precallC + fun:luaD_precall + fun:luaV_execute +} +{ + luv_fs_read + Memcheck:Cond + fun:malloc + fun:luv_fs_read +} +{ + luv_fulfill_req + Memcheck:Cond + fun:luaV_execute + fun:ccall + fun:luaD_callnoyield + fun:f_call + fun:luaD_rawrunprotected + fun:luaD_pcall + fun:lua_pcallk + fun:luv_fulfill_req +} +{ + Conditional jump or move depends on uninitialised value(s) + Memcheck:Cond + fun:luaV_execute + fun:ccall + fun:luaD_callnoyield + fun:f_call + fun:luaD_rawrunprotected + fun:luaD_pcall + fun:lua_pcallk + fun:luv_cfpcall + fun:luv_fulfill_req + fun:luv_fs_cb +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6118528..521f63e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: - name: Build run: make - name: Test - run: valgrind --suppressions=.ci/pthread_create.supp --error-exitcode=1 --leak-check=full --child-silent-after-fork=yes --keep-debuginfo=yes ./build/lua -e "collectgarbage('setpause', 0); collectgarbage('setstepmul', 10000000000000)" tests/run.lua + run: valgrind --suppressions=.ci/valgrind_mem.supp --error-exitcode=1 --leak-check=full --child-silent-after-fork=yes --keep-debuginfo=yes --track-origins=yes ./build/lua -e "collectgarbage('setpause', 0); collectgarbage('setstepmul', 10000000000000)" tests/run.lua process-cleanup-test: runs-on: ubuntu-latest @@ -144,7 +144,7 @@ jobs: - name: Build with Luarocks (alternate rockspec) run: | mkdir ${{github.workspace}}/build/lib - cp ${{github.workspace}}/build/deps/libuv/libuv_a.a ${{github.workspace}}/build/lib/libuv.a + cp ${{github.workspace}}/build/deps/libuv/libuv.a ${{github.workspace}}/build/lib/libuv.a cp -a ${{github.workspace}}/deps/libuv/include ${{github.workspace}}/build luarocks make rockspecs/$(ls rockspecs) LIBUV_DIR=${{github.workspace}}/build LUA_COMPAT53_INCDIR=${{github.workspace}}/deps/lua-compat-5.3/c-api test $PWD = `lua -e "print(require'luv'.cwd())"` diff --git a/appveyor.yml b/appveyor.yml index e3f141a3..3ef72191 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -64,7 +64,7 @@ for: - luarocks remove luv # Test the alternate rockspec - mkdir build\lib - - cp build.luarocks\deps\libuv\Release\uv_a.lib build\lib\uv.lib + - cp build.luarocks\deps\libuv\Release\libuv.lib build\lib\uv.lib - cp -a deps\libuv\include build - ps: luarocks make rockspecs\$(ls rockspecs) LIBUV_DIR=build LUA_COMPAT53_INCDIR=deps/lua-compat-5.3/c-api CFLAGS="/nologo /MT /O2" - ps: if("$(Get-Location)" -eq $(lua -e "print(require'luv'.cwd())")) { "LuaRocks test OK" } else { "LuaRocks test failed"; exit 1 } diff --git a/deps/libuv b/deps/libuv index 0c1fa696..96e05543 160000 --- a/deps/libuv +++ b/deps/libuv @@ -1 +1 @@ -Subproject commit 0c1fa696aa502eb749c2c4735005f41ba00a27b8 +Subproject commit 96e05543f53b19d9642b4b0dd73b86ad3cea313e diff --git a/deps/lua b/deps/lua index 5d708c3f..64431851 160000 --- a/deps/lua +++ b/deps/lua @@ -1 +1 @@ -Subproject commit 5d708c3f9cae12820e415d4f89c9eacbe2ab964b +Subproject commit 6443185167c77adcc8552a3fee7edab7895db1a9 diff --git a/deps/lua-compat-5.3 b/deps/lua-compat-5.3 index e245d3a1..8f8e4c6a 160000 --- a/deps/lua-compat-5.3 +++ b/deps/lua-compat-5.3 @@ -1 +1 @@ -Subproject commit e245d3a18957e43ef902a59a72c8902e2e4435b9 +Subproject commit 8f8e4c6adb43e107f5902e784ef207dc3c8ca06b diff --git a/deps/luajit b/deps/luajit index 03080b79..224129a8 160000 --- a/deps/luajit +++ b/deps/luajit @@ -1 +1 @@ -Subproject commit 03080b795aa3496ed62d4a0697c9f4767e7ca7e5 +Subproject commit 224129a8e64bfa219d35cd03055bf03952f167f6 diff --git a/docs.md b/docs.md index 05ae93a4..f66b0ae0 100644 --- a/docs.md +++ b/docs.md @@ -3286,6 +3286,60 @@ equivalent to the `__eq` metamethod. **Returns:** `boolean` +### `uv.thread_setaffinity(thread, affinity, [get_old_affinity])` + +> method form `thread:setaffinity(affinity, [get_old_affinity])` + +**Parameters:** +- `thread`: `luv_thread_t userdata` +- `affinity`: `table` + - `[1, 2, 3, ..., n]` : `boolean` +- `get_old_affinity`: `boolean` + +Sets the specified thread's affinity setting. `affinity` must be an array-like +table where each of the keys correspond to a CPU number and the values are +booleans that represent whether the `thread` should be eligible to run on that +CPU. The length of the `affinity` table must be greater than or equal to +`uv.cpumask_size()`. If `get_old_affinity` is `true`, the previous affinity +settings for the `thread` will be returned. Otherwise, `true` is returned after +a successful call. + +**Note:** Thread affinity setting is not atomic on Windows. Unsupported on macOS. + +**Returns:** `table` or `boolean` or `fail` +- `[1, 2, 3, ..., n]` : `boolean` + +### `uv.thread_getaffinity(thread, [mask_size])` + +> method form `thread:getaffinity([mask_size])` + +**Parameters:** +- `thread`: `luv_thread_t userdata` +- `mask_size`: `integer` + +Gets the specified thread's affinity setting. + +If `mask_size` is provided, it must be greater than or equal to +`uv.cpumask_size()`. If the `mask_size` parameter is omitted, then the return +of `uv.cpumask_size()` will be used. Returns an array-like table where each of +the keys correspond to a CPU number and the values are booleans that represent +whether the `thread` is eligible to run on that CPU. + +**Note:** Thread affinity getting is not atomic on Windows. Unsupported on macOS. + +**Returns:** `table` or `fail` +- `[1, 2, 3, ..., n]` : `boolean` + +### `uv.thread_getcpu()` + +Gets the CPU number on which the calling thread is running. + +**Note:** The first CPU will be returned as the number 1, not 0. This allows for +the number to correspond with the table keys used in `uv.thread_getaffinity` and +`uv.thread_setaffinity`. + +**Returns:** `integer` or `fail` + ### `uv.thread_self()` Returns the handle for the thread in which this is called. @@ -3373,6 +3427,15 @@ greater than the total system memory. **Returns:** `number` +### `uv.get_available_memory()` + +Gets the amount of free memory that is still available to the process (in +bytes). This differs from `uv.get_free_memory()` in that it takes into account +any limits imposed by the OS. If there is no such constraint, or the constraint +is unknown, the amount returned will be identical to `uv.get_free_memory()`. + +**Returns:** `number` + ### `uv.resident_set_memory()` Returns the resident set size (RSS) for the current process. @@ -3433,6 +3496,13 @@ CPU found. - `idle` : `number` - `irq` : `number` +### `uv.cpumask_size()` + +Returns the maximum size of the mask used for process/thread affinities, or +`ENOTSUP` if affinities are not supported on the current platform. + +**Returns:** `integer` or `fail` + ### `uv.getpid()` **Deprecated:** Please use `uv.os_getpid()` instead. @@ -3484,6 +3554,24 @@ time between intervals. **Returns:** `number` +### `uv.clock_gettime(clock_id)` + +**Parameters:** +- `clock_id`: `string` + +Obtain the current system time from a high-resolution real-time or monotonic +clock source. `clock_id` can be the string `"monotonic"` or `"realtime"`. + +The real-time clock counts from the UNIX epoch (1970-01-01) and is subject +to time adjustments; it can jump back in time. + +The monotonic clock counts from an arbitrary point in the past and never +jumps back in time. + +**Returns:** `table` or `fail` +- `sec`: `integer` +- `nsec`: `integer` + ### `uv.uptime()` Returns the current system uptime in seconds. @@ -3738,6 +3826,22 @@ time until calling `loop_configure` with `"metrics_idle_time"`. **Returns:** `number` +### `uv.metrics_info()` + +Get the metrics table from current set of event loop metrics. + +**Returns:** `table` + +The table contains event loop metrics. It is recommended to retrieve these +metrics in a uv_prepare_cb in order to make sure there are no inconsistencies +with the metrics counters. + +- `loop_count` : `integer` +- `events` : `integer` +- `events_waiting` : `integer` + +**Note**: New in libuv version 1.45.0. + --- [luv]: https://github.com/luvit/luv diff --git a/rockspecs/luv-scm-0.rockspec b/rockspecs/luv-scm-0.rockspec index dcf612a0..6527b5aa 100644 --- a/rockspecs/luv-scm-0.rockspec +++ b/rockspecs/luv-scm-0.rockspec @@ -79,6 +79,9 @@ build = { 'userenv'; 'ws2_32'; 'advapi32'; + 'Dbghelp'; + "Ole32"; + "Shell32"; }; }; }; diff --git a/src/fs.c b/src/fs.c index 917f13f1..8ef36145 100644 --- a/src/fs.c +++ b/src/fs.c @@ -508,6 +508,7 @@ static int luv_fs_read(lua_State* L) { // -1 offset means "the current file offset is used and updated" int64_t offset = -1; int ref; + char* data; // both offset and callback are optional if (luv_is_callable(L, 3) && lua_isnoneornil(L, 4)) { ref = luv_check_continuation(L, 3); @@ -516,7 +517,7 @@ static int luv_fs_read(lua_State* L) { offset = luaL_optinteger(L, 3, offset); ref = luv_check_continuation(L, 4); } - char* data = (char*)malloc(len); + data = (char*)malloc(len); if (!data) { luaL_unref(L, LUA_REGISTRYINDEX, ref); return luaL_error(L, "Failure to allocate buffer"); diff --git a/src/luv.c b/src/luv.c index 1bbce26d..ad596faf 100644 --- a/src/luv.c +++ b/src/luv.c @@ -372,12 +372,22 @@ static const luaL_Reg luv_functions[] = { {"random", luv_random}, #endif {"sleep", luv_sleep}, +#if LUV_UV_VERSION_GEQ(1, 45, 0) + {"cpumask_size", luv_cpumask_size}, + {"get_available_memory", luv_get_available_memory}, + {"clock_gettime", luv_clock_gettime}, +#endif // thread.c {"new_thread", luv_new_thread}, {"thread_equal", luv_thread_equal}, {"thread_self", luv_thread_self}, {"thread_join", luv_thread_join}, +#if LUV_UV_VERSION_GEQ(1, 45, 0) + {"thread_getaffinity", luv_thread_getaffinity}, + {"thread_setaffinity", luv_thread_setaffinity}, + {"thread_getcpu", luv_thread_getcpu}, +#endif // work.c {"new_work", luv_new_work}, @@ -392,6 +402,9 @@ static const luaL_Reg luv_functions[] = { #if LUV_UV_VERSION_GEQ(1, 39, 0) {"metrics_idle_time", luv_metrics_idle_time}, #endif +#if LUV_UV_VERSION_GEQ(1, 45, 0) + {"metrics_info", luv_metrics_info}, +#endif {NULL, NULL} }; diff --git a/src/metrics.c b/src/metrics.c index bbe59411..48253746 100644 --- a/src/metrics.c +++ b/src/metrics.c @@ -25,3 +25,27 @@ static int luv_metrics_idle_time(lua_State* L) { return 1; } #endif + +#if LUV_UV_VERSION_GEQ(1, 45, 0) +static int luv_metrics_info(lua_State *L) { + uv_metrics_t metrics; + int ret = uv_metrics_info(luv_loop(L), &metrics); + if (ret < 0) return luv_error(L, ret); + + lua_newtable(L); + + lua_pushliteral(L, "loop_count"); + lua_pushinteger(L, metrics.loop_count); + lua_rawset(L, -3); + + lua_pushliteral(L, "events"); + lua_pushinteger(L, metrics.events); + lua_rawset(L, -3); + + lua_pushliteral(L, "events_waiting"); + lua_pushinteger(L, metrics.events_waiting); + lua_rawset(L, -3); + + return 1; +} +#endif diff --git a/src/misc.c b/src/misc.c index 26be960f..548e029f 100644 --- a/src/misc.c +++ b/src/misc.c @@ -750,3 +750,35 @@ static int luv_random(lua_State* L) { } } #endif + +#if LUV_UV_VERSION_GEQ(1, 45, 0) +static int luv_cpumask_size(lua_State* L) { + int ret = uv_cpumask_size(); + if (ret < 0) return luv_error(L, ret); + lua_pushinteger(L, ret); + return 1; +} + +static int luv_get_available_memory(lua_State* L) { + lua_pushnumber(L, uv_get_available_memory()); + return 1; +} + +// These are the same order as uv_membership which also starts at 0 +static const char *const luv_clock_id_opts[] = { + "monotonic", "realtime", NULL +}; + +static int luv_clock_gettime(lua_State* L) { + uv_clock_id clock_id = (uv_clock_id)luaL_checkoption(L, 1, NULL, luv_clock_id_opts); + uv_timespec64_t timespec; + int ret = uv_clock_gettime(clock_id, ×pec); + if (ret < 0) return luv_error(L, ret); + lua_createtable(L, 0, 2); + lua_pushinteger(L, timespec.tv_sec); + lua_setfield(L, -2, "sec"); + lua_pushinteger(L, timespec.tv_nsec); + lua_setfield(L, -2, "nsec"); + return 1; +} +#endif diff --git a/src/thread.c b/src/thread.c index 5e780102..2d138fef 100644 --- a/src/thread.c +++ b/src/thread.c @@ -382,6 +382,84 @@ static int luv_new_thread(lua_State* L) { return 1; } +#if LUV_UV_VERSION_GEQ(1, 45, 0) +static int luv_thread_getaffinity(lua_State* L) { + luv_thread_t* tid = luv_check_thread(L, 1); + int default_mask_size = uv_cpumask_size(); + if (default_mask_size < 0) { + return luv_error(L, default_mask_size); + } + int mask_size = luaL_optinteger(L, 2, default_mask_size); + if (mask_size < default_mask_size) { + return luaL_argerror(L, 2, lua_pushfstring(L, "cpumask size must be >= %d (from cpumask_size()), got %d", default_mask_size, mask_size)); + } + char* cpumask = malloc(mask_size); + int ret = uv_thread_getaffinity(&tid->handle, cpumask, mask_size); + if (ret < 0) { + free(cpumask); + return luv_error(L, ret); + } + lua_newtable(L); + for (int i = 0; i < mask_size; i++) { + lua_pushboolean(L, cpumask[i]); + lua_rawseti(L, -2, i + 1); + } + free(cpumask); + return 1; +} + +static int luv_thread_setaffinity(lua_State* L) { + luv_thread_t* tid = luv_check_thread(L, 1); + luaL_checktype(L, 2, LUA_TTABLE); + int get_old_mask = lua_toboolean(L, 3); + int min_mask_size = uv_cpumask_size(); + if (min_mask_size < 0) { + return luv_error(L, min_mask_size); + } + int mask_size = lua_rawlen(L, 2); + if (mask_size < min_mask_size) { + return luaL_argerror(L, 2, lua_pushfstring(L, "cpumask size must be >= %d (from cpumask_size()), got %d", min_mask_size, mask_size)); + } + char* cpumask = malloc(mask_size); + for (int i = 0; i < mask_size; i++) { + lua_rawgeti(L, 2, i+1); + int val = lua_toboolean(L, -1); + cpumask[i] = val; + lua_pop(L, 1); + } + char* oldmask = get_old_mask ? malloc(mask_size) : NULL; + int ret = uv_thread_setaffinity(&tid->handle, cpumask, oldmask, mask_size); + // Done with cpumask at this point + free(cpumask); + if (ret < 0) { + if (get_old_mask) free(oldmask); + return luv_error(L, ret); + } + if (get_old_mask) { + lua_newtable(L); + for (int i = 0; i < mask_size; i++) { + lua_pushboolean(L, oldmask[i]); + lua_rawseti(L, -2, i + 1); + } + free(oldmask); + } + else { + lua_pushboolean(L, 1); + } + return 1; +} + +static int luv_thread_getcpu(lua_State* L) { + int ret = uv_thread_getcpu(); + if (ret < 0) return luv_error(L, ret); + // Make the returned value start at 1 to match how getaffinity/setaffinity + // masks are implemented (they use array-like tables so the first + // CPU is index 1). + lua_pushinteger(L, ret + 1); + return 1; +} +#endif + static int luv_thread_join(lua_State* L) { luv_thread_t* tid = luv_check_thread(L, 1); int ret = uv_thread_join(&tid->handle); @@ -414,6 +492,10 @@ static int luv_thread_equal(lua_State* L) { static const luaL_Reg luv_thread_methods[] = { {"equal", luv_thread_equal}, {"join", luv_thread_join}, +#if LUV_UV_VERSION_GEQ(1, 45, 0) + {"getaffinity", luv_thread_getaffinity}, + {"setaffinity", luv_thread_setaffinity}, +#endif {NULL, NULL} }; diff --git a/tests/test-metrics.lua b/tests/test-metrics.lua new file mode 100644 index 00000000..3991837d --- /dev/null +++ b/tests/test-metrics.lua @@ -0,0 +1,31 @@ + +return require('lib/tap')(function (test) + + test("idle time", function (print, p, expect, uv) + local NS_TO_MS = 1000000 + local timeout = 1000 + local timer = uv.new_timer() + local counter = 0 + timer:start(timeout, 0, function() + counter = counter + 1 + local t = uv.hrtime() + + -- Spin for 500 ms to spin loop time out of the delta check. + while uv.hrtime() - t < 600 * NS_TO_MS do end + timer:close() + + local metrics = uv.metrics_info() + p(metrics) + assert(metrics.loop_count > 0) + assert(metrics.events >= 0) + assert(metrics.events_waiting >= 0) + end) + + local metrics = assert(uv.metrics_info()) + p(metrics) + assert(metrics.loop_count >= 0) + assert(metrics.events >= 0) + assert(metrics.events_waiting >= 0) + end, "1.45.0") + +end) diff --git a/tests/test-misc.lua b/tests/test-misc.lua index fcacfc5b..c1811d5b 100644 --- a/tests/test-misc.lua +++ b/tests/test-misc.lua @@ -23,9 +23,15 @@ return require('lib/tap')(function (test) local constrained = nil if uv.get_constrained_memory then constrained = uv.get_constrained_memory() + assert(constrained >= 0) + end + local available = nil + if uv.get_available_memory then + available = uv.get_available_memory() + assert(available >= 0, available) end local free = uv.get_free_memory() - p{rss=rss,total=total,free=free, constrained=constrained} + p{rss=rss,total=total,free=free,available=available,constrained=constrained} assert(rss < total) end) @@ -182,4 +188,29 @@ return require('lib/tap')(function (test) end end) + test("uv.cpumask_size", function(print, p, expect, uv) + -- The result can vary per-platform and is only supported on some platforms, + -- so just test that the function exists and behaves coherently. + local size, err = uv.cpumask_size() + p(size, err) + if err then + assert(not size) + else + assert(size >= 0) + end + end, "1.45.0") + + test("uv.clock_gettime", function(print, p, expect, uv) + for _, clock_id in ipairs({"monotonic", "realtime"}) do + local timespec, err = uv.clock_gettime("monotonic") + p(clock_id, timespec, err) + if err then + assert(not timespec) + else + assert(timespec.sec >= 0) + assert(timespec.nsec >= 0) + end + end + end, "1.45.0") + end) diff --git a/tests/test-thread.lua b/tests/test-thread.lua index 5491906f..32b77e72 100644 --- a/tests/test-thread.lua +++ b/tests/test-thread.lua @@ -108,4 +108,47 @@ return require('lib/tap')(function (test) collectgarbage('collect') collectgarbage('collect') end) + + test("thread_getcpu", function(print, p, expect, uv) + local cpu, err = uv.thread_getcpu() + if not cpu then + print(err, "skipping") + return + end + -- starts at 1 to match the tables used by getaffinity/setaffinity + assert(cpu >= 1) + end, "1.45.0") + + test("getaffinity, setaffinity", function(print, p, expect, uv) + local mask_size, err = uv.cpumask_size() + if not mask_size then + print(err, "skipping") + return + end + uv.new_thread(function(cpumask_size) + local _uv = require('luv') + local thread = _uv.thread_self() + local affinity = assert(thread:getaffinity()) + assert(#affinity == cpumask_size) + + -- set every cpu's affinity to false except the current cpu + local cur_cpu = _uv.thread_getcpu() + local affinity_to_set = {} + for i=1,cpumask_size do + affinity_to_set[i] = i == cur_cpu + end + local prev_affinity = assert(thread:setaffinity(affinity_to_set, true)) + -- the returned affinity should match the original affinity + assert(#prev_affinity == #affinity) + for i=1,#affinity do + assert(prev_affinity[i] == affinity[i]) + end + + local new_affinity = thread:getaffinity() + assert(#new_affinity == #affinity_to_set) + for i=1,#new_affinity do + assert(new_affinity[i] == affinity_to_set[i]) + end + end, mask_size):join() + end, "1.45.0") end)