fix(event): wait(), jobwait() timeouts #42599

Problem:
- wait() never times out if each check returns in under 1 ms (e.g. the
  condition closes a timer), because each pass truncates the elapsed time
  to whole milliseconds.
- jobwait() with a timeout returns -1 for all jobs after the first one
  that exits.
- CursorHold drops sub-millisecond waits, so frequent events postpone it.
- wait() leaks the condition result if it is a String, List, etc.

Solution:
- Compute the remaining time from a fixed start time.
- Track the CursorHold wait time in nanoseconds.
- Free the condition result.
This commit is contained in:
Justin M. Keyes authored and GitHub committed 2026-10-30 10:14:26 -04:00
1 parent bf4b0ea764
commit 7fa72edcd4
5 files changed
+29 -19

No files matched your search

+9 -8
View File
@@ -2408,7 +2408,8 @@ static void f_wait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
ui_flush();
LOOP_PROCESS_EVENTS_UNTIL(&main_loop, main_loop.events, timeout,
eval_expr_typval(&expr, false, &argv, 0, &exprval) != OK
(tv_clear(&exprval),
eval_expr_typval(&expr, false, &argv, 0, &exprval) != OK)
|| tv_get_number_chk(&exprval, &error)
|| called_emsg > called_emsg_before || error || got_int);
@@ -2421,6 +2422,7 @@ static void f_wait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
} else if (tv_get_number_chk(&exprval, &error)) {
rettv->vval.v_number = 0;
}
tv_clear(&exprval);
// Stop dummy timer
time_watcher_stop(tw);
@@ -3579,12 +3581,13 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
i++;
});
int remaining = -1;
uint64_t before = 0;
int timeout = -1;
uint64_t start = 0;
if (argvars[1].v_type == VAR_NUMBER && argvars[1].vval.v_number >= 0) {
remaining = (int)argvars[1].vval.v_number;
before = os_hrtime();
timeout = (int)argvars[1].vval.v_number;
start = os_hrtime();
}
int remaining = timeout;
// Only mark the UI as busy when jobwait() blocks
const bool busy = remaining != 0;
@@ -3606,9 +3609,7 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
break; // Interrupted (CTRL-C) or timeout, skip remaining jobs.
}
if (remaining > 0) {
uint64_t now = os_hrtime();
remaining = MIN(0, remaining - (int)((now - before) / 1000000));
before = now;
remaining = MAX(0, timeout - (int)((os_hrtime() - start) / 1000000));
}
}
+4 -5
View File
@@ -25,16 +25,15 @@
// Poll for events until a condition or timeout
#define LOOP_PROCESS_EVENTS_UNTIL(loop, multiqueue, timeout, condition) \
do { \
int64_t remaining = timeout; \
uint64_t before = (remaining > 0) ? os_hrtime() : 0; \
const int64_t timeout_ms = (timeout); \
const uint64_t start = (timeout_ms > 0) ? os_hrtime() : 0; \
int64_t remaining = timeout_ms; \
while (!(condition)) { \
LOOP_PROCESS_EVENTS(loop, multiqueue, remaining); \
if (remaining == 0) { \
break; \
} else if (remaining > 0) { \
uint64_t now = os_hrtime(); \
remaining -= (int64_t)((now - before) / 1000000); \
before = now; \
remaining = timeout_ms - (int64_t)((os_hrtime() - start) / 1000000); \
if (remaining <= 0) { \
break; \
} \
+4 -4
View File
@@ -43,7 +43,7 @@ static char *input_read_pos = input_buffer;
static char *input_write_pos = input_buffer;
static bool blocking = false;
static int cursorhold_time = 0; ///< time waiting for CursorHold event
static uint64_t cursorhold_time = 0; ///< Time (ns) spent waiting for CursorHold event.
static int cursorhold_tb_change_cnt = 0; ///< tb_change_cnt when waiting started
#include "os/input.c.generated.h"
@@ -120,7 +120,7 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e
}
} else {
uint64_t wait_start = os_hrtime();
cursorhold_time = MIN(cursorhold_time, (int)p_ut);
cursorhold_time = MIN(cursorhold_time, (uint64_t)p_ut * 1000000);
// When an autocomplete is pending, wake at the sooner of
// 'autocompletedelay' and 'updatetime' so the delay does not postpone
// CursorHold. Once CursorHold has fired, only the delay is left.
@@ -133,7 +133,7 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e
&& (get_real_state() & MODE_INSERT) != 0;
// Measure the delay from when it was armed (the keystroke), so a
// CursorHold returning in between does not push the popup back.
int64_t wait_time = p_ut - cursorhold_time;
int64_t wait_time = p_ut - (int64_t)(cursorhold_time / 1000000);
if (delay_pending) {
int64_t delay_left = p_acl - ins_compl_autocomplete_elapsed();
wait_time = MIN(MAX(delay_left, 0), wait_time);
@@ -168,7 +168,7 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e
inbuf_poll(-1, events);
}
} else {
cursorhold_time += (int)((os_hrtime() - wait_start) / 1000000);
cursorhold_time += os_hrtime() - wait_start;
}
}
+7 -2
View File
@@ -1022,15 +1022,20 @@ describe('jobs', function()
it('can pass 0 to check if a job exists', function()
source([[
call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [
let g:jobs = has('win32') ? [
\ jobstart('Start-Sleep -Milliseconds 50; exit 4'),
\ jobstart('Start-Sleep -Milliseconds 300; exit 5'),
\ ] : [
\ jobstart('sleep 0.05; exit 4'),
\ jobstart('sleep 0.3; exit 5'),
\ ], 0))
\ ]
call rpcnotify(g:channel, 'wait', jobwait(g:jobs, 0))
]])
eq({ 'notification', 'wait', { { -1, -1 } } }, next_msg())
-- With a timeout, keeps waiting for the second job after the first one exits.
command([[call rpcnotify(g:channel, 'wait', jobwait(g:jobs, 10000))]])
eq({ 'notification', 'wait', { { 4, 5 } } }, next_msg())
end)
end)
+5
View File
@@ -36,6 +36,11 @@ describe('wait()', function()
it('returns -1 on timeout', function()
eq(-1, eval('wait(0, 0)'))
eq(-1, eval('wait(50, 0)'))
-- Each check closes a timer, so every poll returns in under 1 ms. #42538
command('let g:Cond = { -> timer_stop(timer_start(10000, { -> 0 })) }')
feed(':call rpcnotify(g:channel, "wait", wait(100, g:Cond))<CR>')
eq({ 'notification', 'wait', { -1 } }, next_msg(5000))
end)
it('returns -2 when interrupted', function()