aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 18:12:23 +0100
committereunhhu <[email protected]>2026-09-29 18:12:23 +0100
commitc919b5f81ad6cad55935fa7a22bfda9539dd584f (patch)
tree35a59078aad71e90b5d9f1f23f9f80355ed9325b
parent434e8a9930d954494a462690739ea4ead21ff4e8 (diff)
downloadvega-c919b5f81ad6cad55935fa7a22bfda9539dd584f.tar.gz
vega-c919b5f81ad6cad55935fa7a22bfda9539dd584f.zip
Wait for timers on the monotonic clock; events still see the wall clock
The terminal, the pixel surfaces and the web page reckoned when a timer was due on the wall clock: set back, a timer waited until the clock came again to the tick it had waited for (Pure's review 004). vega_clock is the monotonic clock; timers are due on it, and a tick sets the time its event sees from the wall clock (vega_now), as before. check.py's clock_set_back runs a timer of 20 ms in the terminal with the wall clock set back an hour 0.2 s in (Pure's build/clock-back.so, an LD_PRELOAD of clock_gettime): 40 ticks in 0.8 s, where Vega before this stops at 9. make check passes it --clock-back.
-rw-r--r--Makefile5
-rw-r--r--surfaces/main.c10
-rw-r--r--surfaces/screen.c20
-rw-r--r--surfaces/vega.h14
-rw-r--r--surfaces/web.c6
-rw-r--r--tests/check.py43
6 files changed, 76 insertions, 22 deletions
diff --git a/Makefile b/Makefile
index 3ddf7e6..f416d1e 100644
--- a/Makefile
+++ b/Makefile
@@ -99,5 +99,6 @@ check-text: $(BUILD)/vega-symbols $(BUILD)/test-text $(BUILD)/test-layout check-
check: $(BUILD)/vega $(BUILD)/vega-surface.so check-text
$(MAKE) -C $(AUP) build/aup
- $(MAKE) -C $(PURE) build/pure-serve
- python3 tests/check.py --vega $(BUILD)/vega --symbols $(BUILD)/vega-symbols --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve
+ $(MAKE) -C $(PURE) build/pure-serve build/clock-back.so
+ python3 tests/check.py --vega $(BUILD)/vega --symbols $(BUILD)/vega-symbols --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve \
+ --clock-back $(PURE)/build/clock-back.so
diff --git a/surfaces/main.c b/surfaces/main.c
index 6c7e368..70958e1 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -483,7 +483,7 @@ static int interactive(void) {
VegaGrid grid = {0};
drawn = &grid;
VegaTimers timers;
- vega_timers_start(&timers, app.module, vega_now());
+ vega_timers_start(&timers, app.module, vega_clock());
for (;;) {
if (resized) {
struct winsize size;
@@ -494,7 +494,7 @@ static int interactive(void) {
}
/* Tick what is due, draw, then wait for input, a frame or the next tick. */
bool ticked = false;
- long due = vega_timers_run(&timers, &app, vega_now(), &ticked);
+ long due = vega_timers_run(&timers, &app, vega_clock(), &ticked);
char line[VEGA_TEXT + 4];
vega_draw(&grid, &app, &screen, line, shown_entry(line), typing);
paint_terminal(&grid);
@@ -505,7 +505,7 @@ static int interactive(void) {
app.now = vega_now();
if (ready == 0) {
if (screen.ticks) pure_app_frame(&app);
- if (module_changed()) vega_timers_start(&timers, app.module, app.now);
+ if (module_changed()) vega_timers_start(&timers, app.module, vega_clock());
continue;
}
if (ready < 0) continue; /* a signal, such as a resize */
@@ -647,7 +647,7 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
signal(SIGTERM, on_stop);
}
VegaTimers timers;
- vega_timers_start(&timers, app.module, vega_now());
+ vega_timers_start(&timers, app.module, vega_clock());
char path[512] = "";
unsigned long differing = 0;
bool open_input = true, printed = false;
@@ -664,7 +664,7 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
bool dirty = true;
while (!stopping && !(node && node->stopping(node->host))) {
bool ticked = false;
- long due = node ? -1 : vega_timers_run(&timers, &app, vega_now(), &ticked);
+ long due = node ? -1 : vega_timers_run(&timers, &app, vega_clock(), &ticked);
PureSnapshot *snapshot = node ? node->read(node->host, &app) : NULL;
dirty = dirty || ticked || snapshot || access_acted;
access_acted = false;
diff --git a/surfaces/screen.c b/surfaces/screen.c
index fdecaf4..f079bd5 100644
--- a/surfaces/screen.c
+++ b/surfaces/screen.c
@@ -11,23 +11,29 @@ double vega_now(void) {
return floor((double)t.tv_sec * 1000 + (double)t.tv_nsec / 1e6);
}
-void vega_timers_start(VegaTimers *timers, const PureModule *m, double now) {
+double vega_clock(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return floor((double)t.tv_sec * 1000 + (double)t.tv_nsec / 1e6);
+}
+
+void vega_timers_start(VegaTimers *timers, const PureModule *m, double clock) {
for (uint32_t i = 0; i < m->inputs; ++i)
- timers->next[i] = m->input[i].kind == PURE_KIND_TIMER ? now + m->input[i].period : 0;
+ timers->next[i] = m->input[i].kind == PURE_KIND_TIMER ? clock + m->input[i].period : 0;
}
-long vega_timers_run(VegaTimers *timers, PureApp *app, double now, bool *ticked) {
+long vega_timers_run(VegaTimers *timers, PureApp *app, double clock, bool *ticked) {
const PureModule *m = app->module;
double soonest = -1;
for (uint32_t i = 0; i < m->inputs; ++i) {
if (m->input[i].kind != PURE_KIND_TIMER) continue;
- if (timers->next[i] <= now) {
- app->now = now;
+ if (timers->next[i] <= clock) {
+ app->now = vega_now();
pure_app_tick(app, i); /* a failed tick changes nothing, as any failed event */
*ticked = true;
- timers->next[i] = now + m->input[i].period;
+ timers->next[i] = clock + m->input[i].period;
}
- if (soonest < 0 || timers->next[i] - now < soonest) soonest = timers->next[i] - now;
+ if (soonest < 0 || timers->next[i] - clock < soonest) soonest = timers->next[i] - clock;
}
return soonest < 0 ? -1 : (long)ceil(soonest);
}
diff --git a/surfaces/vega.h b/surfaces/vega.h
index ff880c5..0ab87dd 100644
--- a/surfaces/vega.h
+++ b/surfaces/vega.h
@@ -43,12 +43,16 @@ void vega_screen(VegaScreen *, const PureModule *);
/* The wall-clock time events see on a live surface: milliseconds since 1970
* (UTC). Scripts set their own (time MS). */
double vega_now(void);
-/* When each timer input ticks next. run ticks every one due at now (a tick
- * sees now) and gives the milliseconds until the next, or -1 without timers;
- * ticked says whether any did. Missed ticks are not made up. */
+/* A clock that only goes on (milliseconds, from boot): when timers are due,
+ * which setting the wall clock back must not hold up. */
+double vega_clock(void);
+/* When each timer input ticks next, on vega_clock. run ticks every one due
+ * at clock (a tick sees the wall clock, vega_now) and gives the
+ * milliseconds until the next, or -1 without timers; ticked says whether
+ * any did. Missed ticks are not made up. */
typedef struct { double next[PURE_MODULE_INPUTS]; } VegaTimers;
-void vega_timers_start(VegaTimers *, const PureModule *, double now);
-long vega_timers_run(VegaTimers *, PureApp *, double now, bool *ticked);
+void vega_timers_start(VegaTimers *, const PureModule *, double clock);
+long vega_timers_run(VegaTimers *, PureApp *, double clock, bool *ticked);
/* What a module seems to mean but does not meet: a list label that is not
* text, a _focus view without its table, a column a list never shows, a
* second text input. Each finding names the view or input it is about;
diff --git a/surfaces/web.c b/surfaces/web.c
index 7103c8f..a4759c4 100644
--- a/surfaces/web.c
+++ b/surfaces/web.c
@@ -418,11 +418,11 @@ int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsi
static Client clients[CLIENTS];
for (unsigned i = 0; i < CLIENTS; ++i) clients[i].fd = -1;
VegaTimers timers;
- vega_timers_start(&timers, app->module, vega_now());
+ vega_timers_start(&timers, app->module, vega_clock());
while (!stopping) {
/* Tick what is due; every page hears of it this round, without waiting. */
bool ticked = false;
- long due = vega_timers_run(&timers, app, vega_now(), &ticked);
+ long due = vega_timers_run(&timers, app, vega_clock(), &ticked);
struct pollfd fds[CLIENTS + 1];
unsigned slot[CLIENTS + 1], count = 1;
fds[0] = (struct pollfd){listener, POLLIN, 0};
@@ -441,7 +441,7 @@ int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsi
if (ready == 0 && screen->ticks) changed = pure_app_frame(app) == PURE_OK;
if (reload && reload()) { /* a new module: every page draws it */
changed = true;
- vega_timers_start(&timers, app->module, app->now);
+ vega_timers_start(&timers, app->module, vega_clock());
}
if (fds[0].revents & POLLIN)
for (int fd; (fd = accept4(listener, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC)) >= 0;) {
diff --git a/tests/check.py b/tests/check.py
index af8ce62..efe5027 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -690,6 +690,46 @@ def timing(vega, module):
return len(out), heard
+BEATS_SPEC = """state beats = 0
+input beat = every 20 ms
+on beat { beats = beats + 1 }
+view count = beats
+"""
+
+
+def clock_set_back(vega, aup, work, clock_back):
+ """The terminal's timers wait on the monotonic clock: with the wall clock
+ set back an hour 0.2 s in (Pure's build/clock-back.so, LD_PRELOAD), a
+ timer of 20 ms goes on ticking (review 004: it stopped until the wall
+ clock came back to the tick it had waited for). The ticks are the
+ app's, in its store."""
+ import pty, select, time
+ source, module, store = work / 'beats.aup', work / 'beats.pmod', work / 'beats-store'
+ source.write_text(BEATS_SPEC)
+ built = run(aup, 'build', source, '-o', module)
+ assert built.returncode == 0, built.stderr
+ pid, fd = pty.fork()
+ if pid == 0:
+ os.environ['LD_PRELOAD'] = str(clock_back)
+ os.execv(str(vega), [str(vega), 'tui', str(module), '--store', str(store)])
+ end = time.time() + 0.8
+ while time.time() < end:
+ if select.select([fd], [], [], 0.05)[0]:
+ try:
+ os.read(fd, 65536)
+ except OSError:
+ break
+ os.write(fd, b'\x03')
+ time.sleep(0.2)
+ _, status = os.waitpid(pid, 0)
+ os.close(fd)
+ assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, status
+ shown_lines = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines()
+ beats = next(int(line.split(':')[1]) for line in shown_lines if line.startswith('count'))
+ assert beats >= 20, shown_lines
+ return beats
+
+
def hosts(vega, serve, module, work):
"""examples/board.aup, one app and one store for people and programs: the
CLI adds and moves cards, pure-serve serves the same store as a JSON API
@@ -1653,6 +1693,7 @@ def main():
parser.add_argument('--vega', type=Path, required=True)
parser.add_argument('--aup', type=Path, required=True)
parser.add_argument('--serve', type=Path, required=True)
+ parser.add_argument('--clock-back', type=Path, help="Pure's build/clock-back.so: the wall clock set back")
parser.add_argument('--symbols', type=Path)
parser.add_argument('--seeds', type=int, default=8)
parser.add_argument('--rounds', type=int, default=3)
@@ -1672,6 +1713,8 @@ def main():
report['tui_frames'] = tui(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30), (16, 6)])
report['table_lines'] = scores(vega, modules['scores'])
report['timed_lines'], report['ticks_heard'] = timing(vega, modules['stopwatch'])
+ if args.clock_back:
+ report['beats_with_clock_set_back'] = clock_set_back(vega, aup, work, args.clock_back.resolve())
report['board_rows_across_hosts'] = hosts(vega, args.serve.resolve(), modules['board'], work)
report['hot_swap_lines'] = hot_swap(vega, aup, work)
report['split_list_lines'] = split(vega, modules['today'], work, range(args.seeds))