diff --git a/platforms/unix/vm-display-X11/sqUnixX11.c b/platforms/unix/vm-display-X11/sqUnixX11.c index 9267fc1e60..5760098d1c 100644 --- a/platforms/unix/vm-display-X11/sqUnixX11.c +++ b/platforms/unix/vm-display-X11/sqUnixX11.c @@ -4858,10 +4858,38 @@ display_ioBeep(void) } +#define MAX_IDLE_USECS 500000 /* 500ms cap when no Delays pending */ + static sqInt display_ioRelinquishProcessorForMicroseconds(sqInt microSeconds) { - aioSleepForUsecs(handleEvents() ? 0 : microSeconds); + extern usqLong getNextWakeupUsecs(void); + extern volatile int mainThreadIsIdle; + + if (handleEvents()) + return 0; + + usqLong nextWakeupUsecs = getNextWakeupUsecs(); + usqLong utcNow = ioUTCMicroseconds(); + long realTimeToWait; + + if (nextWakeupUsecs != 0 && nextWakeupUsecs <= utcNow) + return 0; /* Delay already overdue, don't sleep */ + + if (nextWakeupUsecs != 0) { + realTimeToWait = nextWakeupUsecs - utcNow; + if (realTimeToWait > MAX_IDLE_USECS) + realTimeToWait = MAX_IDLE_USECS; + } else { + realTimeToWait = MAX_IDLE_USECS; + } + + mainThreadIsIdle = 1; + __sync_synchronize(); + aioSleepForUsecs(realTimeToWait); + __sync_synchronize(); + mainThreadIsIdle = 0; + return 0; } diff --git a/platforms/unix/vm-display-null/sqUnixDisplayNull.c b/platforms/unix/vm-display-null/sqUnixDisplayNull.c index 010a3966c1..49d26f6a1f 100644 --- a/platforms/unix/vm-display-null/sqUnixDisplayNull.c +++ b/platforms/unix/vm-display-null/sqUnixDisplayNull.c @@ -20,9 +20,34 @@ static sqInt display_ioFormPrint(sqInt b, sqInt w, sqInt h, sqInt d, double hS, static sqInt display_ioBeep(void) { return 0; } +#define MAX_IDLE_USECS 500000 /* 500ms cap when no Delays pending */ + static sqInt display_ioRelinquishProcessorForMicroseconds(sqInt microSeconds) { - aioSleepForUsecs(microSeconds); + extern usqLong getNextWakeupUsecs(void); + extern volatile int mainThreadIsIdle; + + usqLong nextWakeupUsecs = getNextWakeupUsecs(); + usqLong utcNow = ioUTCMicroseconds(); + long realTimeToWait; + + if (nextWakeupUsecs != 0 && nextWakeupUsecs <= utcNow) + return 0; /* Delay already overdue, don't sleep */ + + if (nextWakeupUsecs != 0) { + realTimeToWait = nextWakeupUsecs - utcNow; + if (realTimeToWait > MAX_IDLE_USECS) + realTimeToWait = MAX_IDLE_USECS; + } else { + realTimeToWait = MAX_IDLE_USECS; + } + + mainThreadIsIdle = 1; + __sync_synchronize(); + aioSleepForUsecs(realTimeToWait); + __sync_synchronize(); + mainThreadIsIdle = 0; + return 0; } diff --git a/platforms/unix/vm/aio.c b/platforms/unix/vm/aio.c index 6c5e3f8dee..447c5a68fe 100644 --- a/platforms/unix/vm/aio.c +++ b/platforms/unix/vm/aio.c @@ -65,6 +65,11 @@ # include # elif HAVE_EPOLL # include +# elif defined(__APPLE__) +/* macOS Cocoa build: HAVE_CONFIG_H is set but no config.h exists, + * so HAVE_KQUEUE is never defined. Detect kqueue via __APPLE__. */ +# include +# define HAVE_KQUEUE 1 # elif HAVE_SELECT # include # endif @@ -97,12 +102,22 @@ # include # include # include -# include # include # include +# if defined(__APPLE__) +# include +# define USE_KQUEUE 1 +# else +# include +# endif #endif /* !HAVE_CONFIG_H */ +/* Unify kqueue detection: USE_KQUEUE defined either way */ +#if !defined(USE_KQUEUE) && defined(HAVE_KQUEUE) && HAVE_KQUEUE +# define USE_KQUEUE 1 +#endif + /* function to inform the VM about idle time */ extern void addIdleUsecs(long idleUsecs); @@ -137,7 +152,32 @@ const static int epollFlagsForAIOFlags[] = { EPOLLIN | EPOLLOUT | EPOLLPRI // AIO_RWX }; -#else // HAVE_CONFIG_H && HAVE_EPOLL +#elif defined(USE_KQUEUE) +/* kqueue-based I/O for macOS (JMM-619 Phase 4) */ +int kqFd = -1; /* non-static: accessed by sqUnixHeartbeat.c */ + +struct kqEventData { + int fd; + int aioMask; + aioHandler readHandler; + aioHandler writeHandler; + aioHandler exceptionHandler; + void *clientData; +}; + +static struct kqEventData **kqEventsByFd = NULL; +static size_t kqEventsCount = 0; + +/* Heartbeat timer folded into kqueue (JMM-619 Phase 4). + * When the main thread is idle, we register EVFILT_TIMER in kqueue + * so kevent() delivers heartbeat events directly, eliminating the + * need for the heartbeat thread to wake up. + */ +#define KQ_HEARTBEAT_IDENT 0xBEA7 /* magic ident for timer */ +int kqHeartbeatActive = 0; /* non-static: accessed by sqUnixHeartbeat.c */ +extern void heartbeat(void); /* from sqUnixHeartbeat.c */ + +#else // select() fallback # define _DO_FLAG_TYPE() do { _DO(AIO_R, rd) _DO(AIO_W, wr) _DO(AIO_X, ex) } while (0) @@ -153,7 +193,7 @@ static fd_set rdMask; /* handle read */ static fd_set wrMask; /* handle write */ static fd_set exMask; /* handle exception */ static fd_set xdMask; /* external descriptor */ -#endif // HAVE_CONFIG_H && HAVE_EPOLL +#endif // epoll / kqueue / select static void undefinedHandler(int fd, void *clientData, int flags) @@ -255,6 +295,18 @@ aioInit(void) epollInit(); if (pthread_atfork(NULL, NULL, epollInit)) perror("pthread_atfork"); +#elif defined(USE_KQUEUE) + if (kqFd >= 0) + close(kqFd); + kqFd = kqueue(); + if (kqFd == -1) { + perror("kqueue failed"); + exit(1); + } + /* set close-on-exec */ + fcntl(kqFd, F_SETFD, FD_CLOEXEC); + kqEventsCount = 0; + kqEventsByFd = NULL; #else FD_ZERO(&fdMask); FD_ZERO(&rdMask); @@ -313,6 +365,14 @@ aioFini(void) } } } +#elif defined(USE_KQUEUE) + for (size_t i = 0; i < kqEventsCount; ++i) { + struct kqEventData *data = kqEventsByFd[i]; + if (data && !(data->aioMask & AIO_EXT)) { + aioDisable(i); + close(i); + } + } #else int fd; @@ -440,7 +500,61 @@ aioPoll(long microSeconds) } while(microSeconds > 0); return 0; -#else // HAVE_CONFIG_H && HAVE_EPOLL +#elif defined(USE_KQUEUE) + + DO_TICK(SHOULD_TICK()); + + if (kqEventsCount == 0 && microSeconds == 0) + return 0; + + do { + const usqLong start = ioUTCMicroseconds(); + struct kevent events[128]; + struct timespec ts; + ts.tv_sec = microSeconds / 1000000; + ts.tv_nsec = (microSeconds % 1000000) * 1000; + const int nev = kevent(kqFd, NULL, 0, events, 128, + microSeconds >= 0 ? &ts : NULL); + if (nev == -1) { + if (errno != EINTR) { + perror("kevent"); + return 0; + } + } else if (nev > 0) { + for (int i = 0; i < nev; ++i) { + aioHandler handler; + struct kqEventData *data = (struct kqEventData *)events[i].udata; + if (!data) continue; + /* Heartbeat timer event */ + if (events[i].filter == EVFILT_TIMER + && events[i].ident == KQ_HEARTBEAT_IDENT) { + heartbeat(); + continue; + } + if (events[i].filter == EVFILT_READ) { + if ((handler = data->readHandler)) + handler(data->fd, data->clientData, AIO_R); + } + if (events[i].filter == EVFILT_WRITE) { + if ((handler = data->writeHandler)) + handler(data->fd, data->clientData, AIO_W); + } + /* EV_ERROR or EOF treated as exception */ + if (events[i].flags & (EV_EOF | EV_ERROR)) { + if ((handler = data->exceptionHandler)) + handler(data->fd, data->clientData, AIO_X); + } + } + return 1; + } else { /* nev == 0: timeout */ + if (microSeconds > 0) addIdleUsecs(microSeconds); + return 0; + } + microSeconds -= max(ioUTCMicroseconds() - start, 1); + } while (microSeconds > 0); + return 0; + +#else // select() fallback int fd; fd_set rd, wr, ex; @@ -648,7 +762,37 @@ aioEnable(int fd, void *data, int flags) */ makeFileDescriptorNonBlockingAndSetupSigio(fd); } -#else // HAVE_CONFIG_H && HAVE_EPOLL +#elif defined(USE_KQUEUE) + if (fd >= (int)kqEventsCount) { + size_t newCount = kqEventsCount ? kqEventsCount * 2 : 64; + if ((size_t)fd >= newCount) newCount = (size_t)fd + 1; + struct kqEventData **newArr = (struct kqEventData **)realloc( + kqEventsByFd, newCount * sizeof(*kqEventsByFd)); + if (!newArr) { perror("aioEnable realloc"); return; } + memset(newArr + kqEventsCount, 0, + (newCount - kqEventsCount) * sizeof(*newArr)); + kqEventsByFd = newArr; + kqEventsCount = newCount; + } + if (kqEventsByFd[fd]) { + FPRINTF((stderr, "aioEnable: descriptor %d already enabled\n", fd)); + return; + } + struct kqEventData *kqd = (struct kqEventData *)calloc(1, sizeof(*kqd)); + if (!kqd) { perror("aioEnable calloc"); return; } + kqd->fd = fd; + kqd->aioMask = flags & AIO_EXT; + kqd->readHandler = undefinedHandler; + kqd->writeHandler = undefinedHandler; + kqd->exceptionHandler = undefinedHandler; + kqd->clientData = data; + kqEventsByFd[fd] = kqd; + if (flags & AIO_EXT) { + FPRINTF((stderr, "aioEnable(%d): external\n", fd)); + } else { + makeFileDescriptorNonBlockingAndSetupSigio(fd); + } +#else // select() fallback if (fd >= FD_SETSIZE) { FPRINTF((stderr, "aioEnable(%d): fd too large\n", fd)); return; @@ -679,7 +823,7 @@ aioEnable(int fd, void *data, int flags) FD_CLR(fd, &xdMask); makeFileDescriptorNonBlockingAndSetupSigio(fd); } -#endif // HAVE_CONFIG_H && HAVE_EPOLL +#endif // epoll / kqueue / select } #if defined(AIO_DEBUG) @@ -751,6 +895,34 @@ aioHandle(int fd, aioHandler handlerFn, int mask) else { FPRINTF((stderr, "aioHandle(%d, %p, %d): epoll_ctl(%d, %d, %d, %p) succeeded\n", fd, handlerFn, mask, epollFd, epoll_operation, fd, event)); } +#elif defined(USE_KQUEUE) + if (fd >= (int)kqEventsCount || !kqEventsByFd[fd]) { + FPRINTF((stderr, "aioHandle(%d): NOT ENABLED\n", fd)); + return; + } + { + struct kqEventData *data = kqEventsByFd[fd]; + struct kevent changes[2]; + int nchanges = 0; + + if (mask & AIO_R) data->readHandler = handlerFn; + if (mask & AIO_W) data->writeHandler = handlerFn; + if (mask & AIO_X) data->exceptionHandler = handlerFn; + + if (mask & AIO_R) { + EV_SET(&changes[nchanges], fd, EVFILT_READ, + EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, data); + nchanges++; + } + if (mask & AIO_W) { + EV_SET(&changes[nchanges], fd, EVFILT_WRITE, + EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, data); + nchanges++; + } + data->aioMask |= mask; + if (nchanges > 0 && kevent(kqFd, changes, nchanges, NULL, 0, NULL) == -1) + perror("aioHandle kevent"); + } #else # undef _DO # define _DO(FLAG, TYPE) \ @@ -790,6 +962,31 @@ aioSuspend(int fd, int mask) else { FPRINTF((stderr, "aioSuspend(%d, %d): NOTHING TO SUSPEND\n", fd, mask)); } +#elif defined(USE_KQUEUE) + if (fd >= (int)kqEventsCount || !kqEventsByFd[fd]) { + FPRINTF((stderr, "aioSuspend(%d): NOT ENABLED\n", fd)); + return; + } + { + struct kqEventData *data = kqEventsByFd[fd]; + struct kevent changes[2]; + int nchanges = 0; + + if ((mask & AIO_R) && (data->aioMask & AIO_R)) { + EV_SET(&changes[nchanges], fd, EVFILT_READ, + EV_DELETE, 0, 0, NULL); + nchanges++; + } + if ((mask & AIO_W) && (data->aioMask & AIO_W)) { + EV_SET(&changes[nchanges], fd, EVFILT_WRITE, + EV_DELETE, 0, 0, NULL); + nchanges++; + } + data->aioMask &= ~mask; + if (nchanges > 0 && kevent(kqFd, changes, nchanges, NULL, 0, NULL) == -1) { + if (errno != ENOENT) perror("aioSuspend kevent"); + } + } #else #undef _DO #define _DO(FLAG, TYPE) \ @@ -827,6 +1024,28 @@ aioDisable(int fd) free(event->data.ptr); free(event); epollEventsByFileDescriptor[fd] = NULL; +#elif defined(USE_KQUEUE) + if (fd >= (int)kqEventsCount || !kqEventsByFd[fd]) { + FPRINTF((stderr, "aioDisable(%d): NOT ENABLED\n", fd)); + return; + } + { + struct kqEventData *data = kqEventsByFd[fd]; + struct kevent changes[2]; + int nchanges = 0; + if (data->aioMask & AIO_R) { + EV_SET(&changes[nchanges], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + nchanges++; + } + if (data->aioMask & AIO_W) { + EV_SET(&changes[nchanges], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + nchanges++; + } + if (nchanges > 0) + kevent(kqFd, changes, nchanges, NULL, 0, NULL); /* ignore ENOENT */ + free(data); + kqEventsByFd[fd] = NULL; + } #else aioSuspend(fd, AIO_RWX); FD_CLR(fd, &xdMask); diff --git a/platforms/unix/vm/sqUnixHeartbeat.c b/platforms/unix/vm/sqUnixHeartbeat.c index 6e75c0db06..120830ed76 100644 --- a/platforms/unix/vm/sqUnixHeartbeat.c +++ b/platforms/unix/vm/sqUnixHeartbeat.c @@ -50,6 +50,20 @@ static sqLong vmGMTOffset = 0; static usqLong frequencyMeasureStart = 0; static unsigned long heartbeats; +/* Phase 4: Adaptive heartbeat for idle CPU optimization (JMM-619). + * + * When the main VM thread is idle (sleeping in ioRelinquishProcessorForMicroseconds), + * the heartbeat thread backs off to a longer interval since nothing needs + * preempting. When the main thread is running Smalltalk code, the heartbeat + * runs at full speed for accurate clocks and timely preemption. + */ +volatile int mainThreadIsIdle = 0; + +#if !defined(IDLE_BEAT_MS) +# define IDLE_BEAT_MS 50 +#endif +static struct timespec idleBeatperiod = { 0, IDLE_BEAT_MS * 1000 * 1000 }; + #define microToMilliseconds(usecs) ((((usecs) - utcStartMicroseconds) \ / MicrosecondsPerMillisecond) \ & MillisecondClockMask) @@ -239,6 +253,49 @@ ioUTCSeconds(void) { return get64(utcMicrosecondClock) / MicrosecondsPerSecond; sqInt ioUTCSecondsNow(void) { return currentUTCMicroseconds() / MicrosecondsPerSecond; } +#if defined(USE_KQUEUE) || defined(__APPLE__) +/* Phase 4: Condition variable to suspend heartbeat thread during idle. + * When kqueue handles heartbeat timing, the thread sleeps here. + */ +#include +static pthread_mutex_t hbMutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t hbCond = PTHREAD_COND_INITIALIZER; + +/* Register/remove heartbeat timer in kqueue. + * Called from ioRelinquishProcessorForMicroseconds. */ +extern int kqFd; +extern int kqHeartbeatActive; +/* forward ref — defined later with other heartbeat state */ +#if !defined(DEFAULT_BEAT_MS) +# define DEFAULT_BEAT_MS 2 +#endif +static int beatMilliseconds; /* initialized below */ +#define KQ_HEARTBEAT_IDENT 0xBEA7 + +static void +kqRegisterHeartbeatTimer(void) +{ + if (kqFd < 0 || kqHeartbeatActive) return; + struct kevent ev; + EV_SET(&ev, KQ_HEARTBEAT_IDENT, EVFILT_TIMER, + EV_ADD | EV_ENABLE, NOTE_USECONDS, + (intptr_t)(beatMilliseconds * 1000), NULL); + if (kevent(kqFd, &ev, 1, NULL, 0, NULL) == 0) + kqHeartbeatActive = 1; +} + +static void +kqRemoveHeartbeatTimer(void) +{ + if (kqFd < 0 || !kqHeartbeatActive) return; + struct kevent ev; + EV_SET(&ev, KQ_HEARTBEAT_IDENT, EVFILT_TIMER, + EV_DELETE, 0, 0, NULL); + kevent(kqFd, &ev, 1, NULL, 0, NULL); + kqHeartbeatActive = 0; +} +#endif /* USE_KQUEUE || __APPLE__ */ + /* * On Mac OS X use the following. * On Unix use dpy->ioRelinquishProcessorForMicroseconds @@ -266,7 +323,21 @@ ioRelinquishProcessorForMicroseconds(sqInt microSeconds) realTimeToWait = microSeconds; } + /* Phase 4: Set idle flag and register kqueue heartbeat timer. + * The flag stays set between consecutive idle calls — only + * cleared on return so the heartbeat thread can resume for + * any active Smalltalk code that runs between calls. + * The kqueue timer stays registered while idle to handle + * heartbeat duties from within kevent(). */ +#if defined(USE_KQUEUE) || defined(__APPLE__) + if (!kqHeartbeatActive) + kqRegisterHeartbeatTimer(); +#endif + mainThreadIsIdle = 1; + sqLowLevelMFence(); aioSleepForUsecs(realTimeToWait); + sqLowLevelMFence(); + mainThreadIsIdle = 0; return 0; } @@ -280,7 +351,8 @@ ioInitTime(void) utcStartMicroseconds = utcMicrosecondClock; } -static void +/* Phase 4: heartbeat() is non-static so aio.c kqueue timer can call it */ +void heartbeat() { int saved_errno = errno; @@ -359,7 +431,26 @@ beatStateMachine(void *careLess) beatState = active; while (beatState != condemned) { # define MINSLEEPNS 2000 /* don't bother sleeping for short times */ - struct timespec naptime = beatperiod; + struct timespec naptime; + + /* Phase 4: When main thread is idle and kqueue handles + * heartbeat timing, this thread can sleep much longer. + * Use 500ms nanosleep — still wakes to check state but + * dramatically fewer wakeups than 2ms. */ +#if defined(USE_KQUEUE) || defined(__APPLE__) + if (mainThreadIsIdle && kqHeartbeatActive) { + struct timespec longNap = { 0, 500000000L }; /* 500ms */ + nanosleep(&longNap, NULL); + heartbeat(); /* keep clocks updated */ + continue; + } +#endif + /* Adaptive: use longer interval when main thread is idle + * (fallback for non-kqueue or when kqueue timer not active). */ + if (mainThreadIsIdle) + naptime = idleBeatperiod; + else + naptime = beatperiod; while (nanosleep(&naptime, &naptime) == -1 && naptime.tv_sec >= 0 /* oversleeps can return tv_sec < 0 */