// macOS/AppKit F05 demo — loop wakeup & external fds
// (../../../features/f05-loop-wakeup.md). A real event loop multiplexes more than
// window events; this demo measures cross-thread wakeup latency into `[NSApp run]`
// via three different mechanisms and probes whether an arbitrary file descriptor can
// join the AppKit run loop.
//
// A single worker thread (core.thread on darwin) drives all three for ~30 s:
// mech=postevent every 100 ms it builds an NSEventTypeApplicationDefined
// event carrying a monotonic send-timestamp in data1 and
// posts it with [NSApp postEvent:atStart:NO]. The main loop
// sees it in an overridden -[NSApplication sendEvent:] and
// logs `wakeup latency_us=… mech=postevent`.
// mech=cfrunloopsource every 100 ms it pushes a send-timestamp onto an SPSC ring
// and signals a version-0 CFRunLoopSourceRef (+ CFRunLoopWakeUp).
// The source's perform callback runs on the main run loop,
// drains the ring, and logs `wakeup … mech=cfrunloopsource`.
// fd_tick every ~143 ms (7 Hz) it writes an 8-byte send-timestamp into
// a pipe; the read end is integrated via a CFFileDescriptorRef
// run-loop source. Its callout drains the pipe and logs
// `fd_tick t=… latency_us=…`.
//
// The finding the fd path proves: AppKit's run loop accepts a raw fd ONLY through a
// CoreFoundation (CFFileDescriptor) or libdispatch (dispatch_source_t) adapter — never
// directly, the way poll()/epoll do. We implement the CFFileDescriptor route and
// document the dispatch_source alternative in ../../f05-loop-wakeup.md.
//
// At exit, per-mechanism latency stats (min/median/p99/max) are reported, then a clean
// bounded exit via [NSApp stop:] + a synthetic event post (the scaffold idiom).
//
// Modes (environment variables):
// WSI_AUTO_EXIT=1 bounded run for WSI_DURATION_S seconds (default 30), then stats
// and exit 0. This is the choreographed, fully agent-verifiable run.
// WSI_DURATION_S=N override the run duration (seconds).
//
// Headless-safe: prints `SKIP:` and exits 0 when there is no window server
// (CGMainDisplayID() == 0), per the research-docs guidelines.
module (module) appapp;
import (package) corecore.(module) core.attributeThis module contains UDA's (User Defined Attributes) either used in
the runtime or special UDA's recognized by compiler.
Attribute Name, Linkage, Description
gnuAbiTag, C++,
Declares an ABI tag on a C++ symbol.
mustuse,,
Ensures that values of a struct or union type are not discarded.
optional, Objective-C,
Makes an Objective-C interface method optional.
selector, Objective-C,
Attaches an Objective-C selector to a method.
standalone,,
Marks a shared module constructor as not depending on any
other module constructor being run first.
weak,,
Specifies that a global symbol should be emitted with weak linkage.
Source
core/attribute.d
attribute : selector;
import (package) corecore.(module) core.atomicThe atomic module provides basic support for lock-free
concurrent programming.
Use the -preview=nosharedaccess compiler flag to detect
unsafe individual read or write operations on shared data.
Source
core/atomic.d
Examples
int y = 2;
shared int x = y; // OK
//x++; // read modify write error
x.atomicOp!"+="(1); // OK
//y = x; // read error with preview flag
y = x.atomicLoad(); // OK
assert(y == 3);
//x = 5; // write error with preview flag
x.atomicStore(5); // OK
assert(x.atomicLoad() == 5);
atomic : (alias template) app.atomicLoad = core.atomic.atomicLoad(MemoryOrder ms = MemoryOrder.seq, T)(auto ref return scope const T val) if (!is(T == shared(U), U) && !is(T == shared(inout(U)), U) && !is(T == shared(const(U)), U))Loads 'val' from memory and returns it. The memory barrier specified
by 'ms' is applied to the operation, which is fully sequenced by
default. Valid memory orders are MemoryOrder.raw, MemoryOrder.acq,
and MemoryOrder.seq.
atomicLoad, (alias template) app.atomicStore = core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))Writes 'newval' into 'val'. The memory barrier specified by 'ms' is
applied to the operation, which is fully sequenced by default.
Valid memory orders are MemoryOrder.raw, MemoryOrder.rel, and
MemoryOrder.seq.
atomicStore, (alias template) app.atomicFence = core.atomic.atomicFence(MemoryOrder order = MemoryOrder.seq)()Inserts a full load/store memory fence (on platforms that need it). This ensures
that all loads and stores before a call to this function are executed before any
loads and stores after the call.
atomicFence;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdioD header file for C99 <stdio.h>
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html, stdio.h
Source
core/stdc/stdio.d
stdio : (alias) app.printf = int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv, (alias) app.atoi = int core.stdc.stdlib.atoi(scope const(char*) nptr) nothrow @nogcatoi;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.fcntlD header file for POSIX.
fcntl : (alias) app.fcntl = int core.sys.posix.fcntl.fcntl(int, int, ...) nothrow @nogcfcntl, (alias constant) app.F_GETFL = int core.sys.posix.fcntl.F_GETFL = 3F_GETFL, (alias constant) app.F_SETFL = int core.sys.posix.fcntl.F_SETFL = 4F_SETFL, (alias constant) app.O_NONBLOCK = int core.sys.posix.fcntl.O_NONBLOCK = 2048O_NONBLOCK;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) app.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe, (alias) app.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite, (alias) app.read = long core.sys.posix.unistd.read(int, void*, ulong) nothrow @nogcread, (alias) app.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose;
import (package) corecore.(module) core.threadThe thread module provides support for thread creation and management.
Source
core/thread/package.d
thread : (class) core.thread.osthread.ThreadThis class encapsulates all threading functionality for the D
programming language. As thread manipulation is a required facility
for garbage collection, all user threads should derive from this
class, and instances of this class should never be explicitly deleted.
A new thread may be created using either derivation or composition, as
in the following example.
Thread;
import (package) corecore.(module) core.timeModule containing core time functionality, such as Duration (which
represents a duration of time) or MonoTime (which represents a
timestamp of the system's monotonic clock).
Various functions take a string (or strings) to represent a unit of time
(e.g. convert!("days", "hours")(numDays)). The valid strings to use
with such functions are "years", "months", "weeks", "days", "hours",
"minutes", "seconds", "msecs" (milliseconds), "usecs" (microseconds),
"hnsecs" (hecto-nanoseconds - i.e. 100 ns) or some subset thereof. There
are a few functions that also allow "nsecs", but very little actually
has precision greater than hnsecs.
Symbol Description Types Duration Represents a duration of time of weeks or less (kept internally as hnsecs). (e.g. 22 days or 700 seconds). TickDuration DEPRECATED Represents a duration of time in system clock ticks, using the highest precision that the system provides. MonoTime Represents a monotonic timestamp in system clock ticks, using the highest precision that the system provides. Functions convert Generic way of converting between two time units. dur Allows constructing a Duration from the given time units with the given length. weeks days hours
minutes seconds msecs
usecs hnsecs nsecs |
Convenience aliases for dur. |
| abs | Returns the absolute value of a duration. |
From Duration
From TickDuration
From units
To Duration tickDuration.to, std,conv!Duration() dur!"msecs"(5) or 5.msecs()
| To TickDuration |
duration.to, std,conv!TickDuration() |
|
TickDuration.from!"msecs"(msecs) |
| To units |
duration.total!"days" |
tickDuration.msecs |
convert!("days", "msecs")(msecs) |
Source
core/time.d
time : (struct) core.time.MonoTimeImpl!(ClockType.normal)MonoTime, msecs, seconds;
import instrument : instr, instrInit, instrNowUs;
import objc.autorelease : autoreleasepool_push, autoreleasepool_pop; // objective-d
import objc.rt : SEL;
// --- CoreFoundation C API (comes with the Cocoa umbrella framework) -------------
alias (alias) app.CFRunLoopRef = void*CFRunLoopRef = void*;
alias (alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef = void*;
alias (alias) app.CFFileDescriptorRef = void*CFFileDescriptorRef = void*;
alias (alias) app.CFStringRef = void*CFStringRef = void*;
alias (alias) app.CFAllocatorRef = void*CFAllocatorRef = void*;
alias (alias) app.CFIndex = longCFIndex = long;
alias (alias) app.CFOptionFlags = ulongCFOptionFlags = ulong;
alias (alias) app.CFFileDescriptorNativeDescriptor = intCFFileDescriptorNativeDescriptor = int;
// A version-0 run-loop source: `info` is passed to every callback; `perform` is the
// only one we use. All function-pointer slots are extern(C) callbacks or null.
extern (C) struct (struct) app.CFRunLoopSourceContextCFRunLoopSourceContext
{
(alias) app.CFIndex = longCFIndex (field) long app.CFRunLoopSourceContext.version_version_;
void* (field) void* app.CFRunLoopSourceContext.infoinfo;
void* (field) void* app.CFRunLoopSourceContext.retainretain;
void* (field) void* app.CFRunLoopSourceContext.releaserelease;
void* (field) void* app.CFRunLoopSourceContext.copyDescriptioncopyDescription;
void* (field) void* app.CFRunLoopSourceContext.equalequal;
void* (field) void* app.CFRunLoopSourceContext.hashhash;
void* (field) void* app.CFRunLoopSourceContext.scheduleschedule;
void* (field) void* app.CFRunLoopSourceContext.cancelcancel;
void* (field) void* app.CFRunLoopSourceContext.performperform; // void (*)(void* info)
}
extern (C) struct (struct) app.CFFileDescriptorContextCFFileDescriptorContext
{
(alias) app.CFIndex = longCFIndex (field) long app.CFFileDescriptorContext.version_version_;
void* (field) void* app.CFFileDescriptorContext.infoinfo;
void* (field) void* app.CFFileDescriptorContext.retainretain;
void* (field) void* app.CFFileDescriptorContext.releaserelease;
void* (field) void* app.CFFileDescriptorContext.copyDescriptioncopyDescription;
}
extern (C) nothrow @nogc
{
(alias) app.CFRunLoopRef = void*CFRunLoopRef void* app.CFRunLoopGetCurrent() nothrow @nogcCFRunLoopGetCurrent();
void void app.CFRunLoopAddSource(void* rl, void* source, void* mode) nothrow @nogcCFRunLoopAddSource((alias) app.CFRunLoopRef = void*CFRunLoopRef (parameter) void* rlrl, (alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef (parameter) void* sourcesource, (alias) app.CFStringRef = void*CFStringRef (parameter) void* modemode);
void void app.CFRunLoopSourceSignal(void* source) nothrow @nogcCFRunLoopSourceSignal((alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef (parameter) void* sourcesource);
void void app.CFRunLoopWakeUp(void* rl) nothrow @nogcCFRunLoopWakeUp((alias) app.CFRunLoopRef = void*CFRunLoopRef (parameter) void* rlrl);
(alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef void* app.CFRunLoopSourceCreate(void* allocator, long order, app.CFRunLoopSourceContext* context) nothrow @nogcCFRunLoopSourceCreate((alias) app.CFAllocatorRef = void*CFAllocatorRef (parameter) void* allocatorallocator, (alias) app.CFIndex = longCFIndex (parameter) long orderorder,
(struct) app.CFRunLoopSourceContextCFRunLoopSourceContext* (parameter) app.CFRunLoopSourceContext* contextcontext);
(alias) app.CFFileDescriptorRef = void*CFFileDescriptorRef void* app.CFFileDescriptorCreate(void* allocator, int fd, bool closeOnInvalidate, void* callout, app.CFFileDescriptorContext* context) nothrow @nogcCFFileDescriptorCreate((alias) app.CFAllocatorRef = void*CFAllocatorRef (parameter) void* allocatorallocator,
(alias) app.CFFileDescriptorNativeDescriptor = intCFFileDescriptorNativeDescriptor (parameter) int fdfd, bool (parameter) bool closeOnInvalidatecloseOnInvalidate, void* (parameter) void* calloutcallout,
(struct) app.CFFileDescriptorContextCFFileDescriptorContext* (parameter) app.CFFileDescriptorContext* contextcontext);
void void app.CFFileDescriptorEnableCallBacks(void* f, ulong types) nothrow @nogcCFFileDescriptorEnableCallBacks((alias) app.CFFileDescriptorRef = void*CFFileDescriptorRef (parameter) void* ff, (alias) app.CFOptionFlags = ulongCFOptionFlags (parameter) ulong typestypes);
(alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef void* app.CFFileDescriptorCreateRunLoopSource(void* allocator, void* f, long order) nothrow @nogcCFFileDescriptorCreateRunLoopSource((alias) app.CFAllocatorRef = void*CFAllocatorRef (parameter) void* allocatorallocator,
(alias) app.CFFileDescriptorRef = void*CFFileDescriptorRef (parameter) void* ff, (alias) app.CFIndex = longCFIndex (parameter) long orderorder);
extern __gshared (alias) app.CFStringRef = void*CFStringRef (__gshared global) void* app.kCFRunLoopCommonModeskCFRunLoopCommonModes;
uint uint app.CGMainDisplayID() nothrow @nogcCGMainDisplayID();
}
enum (alias) app.CFOptionFlags = ulongCFOptionFlags (constant) ulong app.kCFFileDescriptorReadCallBack = 1LUkCFFileDescriptorReadCallBack = 1UL << 0;
// --- Cocoa geometry (CGFloat == double on 64-bit) -------------------------------
extern (C) struct (struct) app.NSPointNSPoint
{
double (field) double app.NSPoint.xx, (field) double app.NSPoint.yy;
}
extern (C) struct (struct) app.NSSizeNSSize
{
double (field) double app.NSSize.widthwidth, (field) double app.NSSize.heightheight;
}
extern (C) struct (struct) app.NSRectNSRect
{
(struct) app.NSPointNSPoint (field) app.NSPoint app.NSRect.originorigin;
(struct) app.NSSizeNSSize (field) app.NSSize app.NSRect.sizesize;
}
// --- AppKit class declarations --------------------------------------------------
extern (Objective-C):
extern class (class) app.NSObjectNSObject
{
}
extern class (class) app.NSStringNSString : (class) app.NSObjectNSObject
{
static (class) app.NSStringNSString app.NSString app.NSString.alloc()alloc() @selector("alloc");
(class) app.NSStringNSString app.NSString app.NSString.initWithUTF8String(const(char)* s)initWithUTF8String(const(char)* (parameter) const(char)* ss) @selector("initWithUTF8String:");
}
extern class (class) app.NSResponderNSResponder : (class) app.NSObjectNSObject
{
}
extern class (class) app.NSEventNSEvent : (class) app.NSObjectNSObject
{
static (class) app.NSEventNSEvent app.NSEvent app.NSEvent.otherEventWithType(long type, app.NSPoint location, ulong modifierFlags, double timestamp, long windowNumber, app.NSObject context, short subtype, long data1, long data2)otherEventWithType(long (parameter) long typetype, (struct) app.NSPointNSPoint (parameter) app.NSPoint locationlocation, ulong (parameter) ulong modifierFlagsmodifierFlags,
double (parameter) double timestamptimestamp, long (parameter) long windowNumberwindowNumber, (class) app.NSObjectNSObject (parameter) app.NSObject contextcontext, short (parameter) short subtypesubtype,
long (parameter) long data1data1, long (parameter) long data2data2)
@selector("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:");
long long app.NSEvent.type()type() @selector("type");
short short app.NSEvent.subtype()subtype() @selector("subtype");
long long app.NSEvent.data1()data1() @selector("data1");
}
// NSApplication declares sendEvent: so WakeApp can override it. sharedApplication is
// declared ONLY on the WakeApp leaf (returning WakeApp) to dodge the covariant-return
// clash on a base declaration — calling +sharedApplication on the subclass makes NSApp
// an instance of it, so our sendEvent: override is installed.
extern class (class) app.NSApplicationNSApplication : (class) app.NSResponderNSResponder
{
void void app.NSApplication.setActivationPolicy(long policy)setActivationPolicy(long (parameter) long policypolicy) @selector("setActivationPolicy:");
void void app.NSApplication.activateIgnoringOtherApps(bool flag)activateIgnoringOtherApps(bool (parameter) bool flagflag) @selector("activateIgnoringOtherApps:");
void void app.NSApplication.run()run() @selector("run");
void void app.NSApplication.stop(app.NSObject sender)stop((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("stop:");
void void app.NSApplication.sendEvent(app.NSEvent event)sendEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent) @selector("sendEvent:");
void void app.NSApplication.postEvent(app.NSEvent event, bool atStart)postEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent, bool (parameter) bool atStartatStart) @selector("postEvent:atStart:");
}
extern class (class) app.NSViewNSView : (class) app.NSResponderNSResponder
{
(struct) app.NSRectNSRect app.NSRect app.NSView.bounds()bounds() @selector("bounds");
}
extern class (class) app.NSWindowNSWindow : (class) app.NSResponderNSResponder
{
static (class) app.NSWindowNSWindow app.NSWindow app.NSWindow.alloc()alloc() @selector("alloc");
(class) app.NSWindowNSWindow app.NSWindow app.NSWindow.initWithContentRect(app.NSRect contentRect, ulong styleMask, ulong backing, bool defer)initWithContentRect((struct) app.NSRectNSRect (parameter) app.NSRect contentRectcontentRect, ulong (parameter) ulong styleMaskstyleMask,
ulong (parameter) ulong backingbacking, bool (parameter) bool deferdefer) @selector("initWithContentRect:styleMask:backing:defer:");
void void app.NSWindow.setTitle(app.NSString title)setTitle((class) app.NSStringNSString (parameter) app.NSString titletitle) @selector("setTitle:");
void void app.NSWindow.setContentView(app.NSView view)setContentView((class) app.NSViewNSView (parameter) app.NSView viewview) @selector("setContentView:");
void void app.NSWindow.makeKeyAndOrderFront(app.NSObject sender)makeKeyAndOrderFront((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("makeKeyAndOrderFront:");
}
// The NSApplication subclass: its sendEvent: override is where mech=postevent and the
// bounded-exit DONE signal are observed (the run loop calls -[NSApp sendEvent:] for
// every dequeued event, including NSEventTypeApplicationDefined).
class (class) app.WakeAppWakeApp : (class) app.NSApplicationNSApplication
{
static (class) app.WakeAppWakeApp app.WakeApp app.WakeApp.sharedApplication()sharedApplication() @selector("sharedApplication");
override void void app.WakeApp.sendEvent(app.NSEvent event)sendEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent) @selector("sendEvent:")
{
if (event.event.typetype() == NSEventTypeApplicationDefined)
{
immutable _error_ stst = event.event.subtypesubtype();
if (st == subtypeWake)
{
onWake(MechId.MechId.posteventpostevent, cast(ulong) event.event.data1data1());
return; // consume — not a real UI event
}
if (st == subtypeDone)
{
onDone();
return;
}
// subtypeStop (0): the synthetic unblock event — fall through to super.
}
super.super.sendEventsendEvent(event);
}
}
// A minimal solid-color content view so the demo is a real windowed AppKit app.
class (class) app.FillViewFillView : (class) app.NSViewNSView
{
static (class) app.FillViewFillView app.FillView app.FillView.alloc()alloc() @selector("alloc");
(class) app.FillViewFillView app.FillView app.FillView.initWithFrame(app.NSRect frame)initWithFrame((struct) app.NSRectNSRect (parameter) app.NSRect frameframe) @selector("initWithFrame:");
void void app.FillView.drawRect(app.NSRect dirtyRect)drawRect((struct) app.NSRectNSRect (parameter) app.NSRect dirtyRectdirtyRect) @selector("drawRect:")
{
// The loop, not the pixels, is the subject here; nothing to draw.
}
}
extern (D): // back to D linkage
enum (constant) int app.NSApplicationActivationPolicyRegular = 0NSApplicationActivationPolicyRegular = 0;
enum : ulong
{
(enum value) app.NSWindowStyleMaskTitled = 1LUNSWindowStyleMaskTitled = 1,
(enum value) app.NSWindowStyleMaskClosable = 2LUNSWindowStyleMaskClosable = 2,
(enum value) app.NSWindowStyleMaskResizable = 8LUNSWindowStyleMaskResizable = 8,
}
enum ulong (constant) ulong app.NSBackingStoreBuffered = 2LUNSBackingStoreBuffered = 2;
enum long (constant) long app.NSEventTypeApplicationDefined = 15LNSEventTypeApplicationDefined = 15;
// applicationDefined subtypes we distinguish in sendEvent:
enum short (constant) short app.subtypeStop = cast(short)0subtypeStop = 0; // synthetic post that unblocks [NSApp run] after stop:
enum short (constant) short app.subtypeWake = cast(short)87subtypeWake = 0x57; // 'W' — a mech=postevent wakeup
enum short (constant) short app.subtypeDone = cast(short)68subtypeDone = 0x44; // 'D' — worker signalling end-of-run
// --- Demo state -----------------------------------------------------------------
enum (enum) app.MechIdMechId : int
{
(enum value) app.MechId.postevent = 0postevent = 0,
(enum value) app.MechId.cfrunloopsource = 1cfrunloopsource = 1,
}
immutable (alias) object.string = stringstring[2] (immutable global) immutable(string[2]) app.mechNamesmechNames = ["postevent", "cfrunloopsource"];
__gshared (class) app.WakeAppWakeApp (__gshared global) app.WakeApp app.g_appg_app;
__gshared (class) app.NSWindowNSWindow (__gshared global) app.NSWindow app.g_wing_win;
__gshared (alias) app.CFRunLoopRef = void*CFRunLoopRef (__gshared global) void* app.g_mainRLg_mainRL;
__gshared (alias) app.CFRunLoopSourceRef = void*CFRunLoopSourceRef (__gshared global) void* app.g_cfSourceg_cfSource;
__gshared int (__gshared global) int app.g_pipeReadg_pipeRead = -1, (__gshared global) int app.g_pipeWriteg_pipeWrite = -1;
__gshared (class) core.thread.osthread.ThreadThis class encapsulates all threading functionality for the D
programming language. As thread manipulation is a required facility
for garbage collection, all user threads should derive from this
class, and instances of this class should never be explicitly deleted.
A new thread may be created using either derivation or composition, as
in the following example.
Thread (__gshared global) core.thread.osthread.Thread app.g_workerg_worker;
shared bool (shared global) shared(bool) app.g_stopWorkerg_stopWorker;
__gshared int (__gshared global) int app.g_durationSg_durationS = 30;
// Latency samples (main thread only appends — all callbacks run on the main loop).
enum (constant) int app.sampleCap = 8192sampleCap = 8192;
__gshared double[(constant) int app.sampleCap = 8192sampleCap][2] (constant) int app.sampleCap = 8192g_lat; // [mech][i], microseconds
__gshared (alias) object.size_t = ulongsize_t[2] (__gshared global) ulong[2] app.g_latNg_latN;
__gshared double[(constant) int app.sampleCap = 8192sampleCap] (__gshared global) double[8192] app.g_fdLatg_fdLat;
__gshared (alias) object.size_t = ulongsize_t (__gshared global) ulong app.g_fdNg_fdN;
__gshared uint (__gshared global) uint app.g_wakeCountg_wakeCount, (__gshared global) uint app.g_fdCountg_fdCount;
// SPSC ring carrying send-timestamps for the coalescing-prone CFRunLoopSource path:
// one signal may collapse several pushes into a single perform, so we drain the ring.
enum (constant) int app.ringCap = 4096ringCap = 4096; // power of two
__gshared ulong[(constant) int app.ringCap = 4096ringCap] (__gshared global) ulong[4096] app.g_ringg_ring;
shared (alias) object.size_t = ulongsize_t (shared global) shared(ulong) app.g_ringHeadg_ringHead; // producer (worker)
shared (alias) object.size_t = ulongsize_t (shared global) shared(ulong) app.g_ringTailg_ringTail; // consumer (main perform)
void void app.ringPush(ulong v) nothrow @nogcringPush(ulong (parameter) ulong vv) nothrow @nogc
{
immutable _error_ hh = atomicLoad(g_ringHead);
g_ring[h & (ringCap - 1)] = v;
atomicFence();
atomicStore(g_ringHead, h + 1);
}
void void app.onWake(app.MechId mech, ulong sendUs)onWake((enum) app.MechIdMechId (parameter) app.MechId mechmech, ulong (parameter) ulong sendUssendUs)
{
immutable _error_ nownow = instrNowUs();
immutable _error_ latlat = now > sendUs ? cast(double)(now - sendUs) : 0.0;
if (g_latN[mech] < sampleCap)
g_lat[mech][g_latN[mech]++] = lat;
++g_wakeCount;
instr("wakeup", "latency_us=%.1f mech=%.*s n=%u", lat,
cast(int) mechNames[mech].length, mechNames[mech].ptr, g_wakeCount);
}
void void app.onDone()onDone()
{
instr("step", "name=worker_done wakes=%u fd_ticks=%u", g_wakeCount, g_fdCount);
g_app.g_app.stopstop(null);
// stop: only takes effect after the next event dispatch; post a synthetic
// application-defined event so [NSApp run] wakes up and returns (scaffold finding).
(unresolved type) NSEventNSEvent _error_ evev = NSEvent.NSEvent.otherEventWithTypeotherEventWithType(NSEventTypeApplicationDefined,
NSPoint(0, 0), 0, 0, 0, null, subtypeStop, 0, 0);
g_app.g_app.postEventpostEvent(ev, true);
}
// CFRunLoopSource perform callback (main run loop): drain every queued send-timestamp.
extern (C) void void app.performCFSource(void* info) nothrowperformCFSource(void* (parameter) void* infoinfo) nothrow
{
(unresolved type) size_tsize_t _error_ tailtail = atomicLoad(g_ringTail);
immutable _error_ headhead = atomicLoad(g_ringHead);
while (tail != head)
{
immutable _error_ sendUssendUs = g_ring[tail & (ringCap - 1)];
++tail;
immutable _error_ nownow = instrNowUs();
immutable _error_ latlat = now > sendUs ? cast(double)(now - sendUs) : 0.0;
if (g_latN[MechId.MechId.cfrunloopsourcecfrunloopsource] < sampleCap)
g_lat[MechId.MechId.cfrunloopsourcecfrunloopsource][g_latN[MechId.MechId.cfrunloopsourcecfrunloopsource]++] = lat;
++g_wakeCount;
instr("wakeup", "latency_us=%.1f mech=cfrunloopsource n=%u", lat, g_wakeCount);
}
atomicStore(g_ringTail, tail);
}
// CFFileDescriptor callout (main run loop): drain all 8-byte timestamps from the pipe.
// CFFileDescriptor disables its callbacks after each fire, so we must re-enable them.
extern (C) void void app.fdCallout(void* f, ulong types, void* info) nothrowfdCallout((alias) app.CFFileDescriptorRef = void*CFFileDescriptorRef (parameter) void* ff, (alias) app.CFOptionFlags = ulongCFOptionFlags (parameter) ulong typestypes, void* (parameter) void* infoinfo) nothrow
{
ubyte[8 * 64] _error_ bufbuf;
for (;;)
{
immutable _error_ nn = read(g_pipeRead, buf.ptr, buf.length);
if (n <= 0)
break;
(unresolved type) size_tsize_t _error_ offoff = 0;
while (off + 8 <= cast((unresolved type) size_tsize_t) n)
{
ulong _error_ sendUssendUs = *cast(ulong*)(buf.ptr + off);
off += 8;
immutable _error_ nownow = instrNowUs();
immutable _error_ latlat = now > sendUs ? cast(double)(now - sendUs) : 0.0;
if (g_fdN < sampleCap)
g_fdLat[g_fdN++] = lat;
++g_fdCount;
instr("fd_tick", "t=%u latency_us=%.1f", g_fdCount, lat);
}
if (cast((unresolved type) size_tsize_t) n < buf.length)
break;
}
CFFileDescriptorEnableCallBacks(f, kCFFileDescriptorReadCallBack);
}
// The worker thread: posts mech A + mech B at 10 Hz and the fd at ~7 Hz for the run
// duration, then signals DONE. All wakeup primitives below are documented thread-safe:
// -[NSApp postEvent:atStart:], CFRunLoopSourceSignal, CFRunLoopWakeUp, and write().
void void app.workerMain()workerMain()
{
auto _error_ poolpool = autoreleasepool_push(); // Cocoa from a secondary thread needs a pool
scope (exit)
autoreleasepool_pop(pool);
immutable _error_ startstart = MonoTime.currTime;
immutable _error_ deadlinedeadline = start + g_durationS.seconds;
(unresolved type) MonoTimeMonoTime _error_ nextABnextAB = start;
(unresolved type) MonoTimeMonoTime _error_ nextFdnextFd = start;
immutable _error_ abPeriodabPeriod = 100.msecs; // 10 Hz
immutable _error_ fdPeriodfdPeriod = 143.msecs; // ~7 Hz
while (MonoTime.currTime < deadline && !atomicLoad(g_stopWorker))
{
immutable _error_ nownow = MonoTime.currTime;
if (now >= nextAB)
{
immutable _error_ sendUssendUs = instrNowUs();
// mech A: post an applicationDefined event carrying the send time in data1.
(unresolved type) NSEventNSEvent _error_ evev = NSEvent.NSEvent.otherEventWithTypeotherEventWithType(NSEventTypeApplicationDefined,
NSPoint(0, 0), 0, 0, 0, null, subtypeWake, cast(long) sendUs, 0);
g_app.g_app.postEventpostEvent(ev, false);
// mech B: push the send time and signal the run-loop source.
ringPush(instrNowUs());
CFRunLoopSourceSignal(g_cfSource);
CFRunLoopWakeUp(g_mainRL);
nextAB += abPeriod;
}
if (now >= nextFd)
{
ulong _error_ sendUssendUs = instrNowUs();
write(g_pipeWrite, &sendUs, sendUs.sizeof);
nextFd += fdPeriod;
}
Thread.sleep(2.msecs);
}
// Signal end-of-run to the main loop (also delivered through sendEvent:).
(unresolved type) NSEventNSEvent _error_ donedone = NSEvent.NSEvent.otherEventWithTypeotherEventWithType(NSEventTypeApplicationDefined,
NSPoint(0, 0), 0, 0, 0, null, subtypeDone, 0, 0);
g_app.g_app.postEventpostEvent(done, false);
}
void void app.reportStats(const(char)* mech, double[] s)reportStats(const(char)* (parameter) const(char)* mechmech, double[] (parameter) double[] ss)
{
if (s.length == 0)
{
instr("stats", "mech=%s n=0", mech);
return;
}
import std.algorithm.sorting : sort;
sort(s);
immutable _error_ mnmn = s[0];
immutable _error_ mxmx = s[$ - 1];
immutable _error_ medmed = s[s.length / 2];
(unresolved type) size_tsize_t _error_ i99i99 = cast((unresolved type) size_tsize_t)(s.length * 99 / 100);
if (i99 >= s.length)
i99 = s.length - 1;
instr("stats", "mech=%s n=%zu min_us=%.1f med_us=%.1f p99_us=%.1f max_us=%.1f",
mech, s.length, mn, med, s[i99], mx);
}
int int D main()main()
{
immutable _error_ autoExitautoExit = getenv("WSI_AUTO_EXIT") !is null;
if (auto d = getenv("WSI_DURATION_S"))
{
immutable _error_ vv = atoi(d);
if (v > 0)
g_durationS = v;
}
instrInit("APPKIT_F05");
instr("init_start", "auto_exit=%d duration_s=%d", autoExit ? 1 : 0, g_durationS);
instr("step", "name=CGMainDisplayID");
if (CGMainDisplayID() == 0)
{
printf("SKIP: no macOS window server (CGMainDisplayID == 0)\n");
return 0;
}
auto _error_ poolpool = autoreleasepool_push();
scope (exit)
autoreleasepool_pop(pool);
// Shared app — instantiated as a WakeApp so our sendEvent: override is live.
instr("step", "name=NSApplication_sharedApplication class=WakeApp");
g_app = WakeApp.WakeApp.sharedApplicationsharedApplication();
g_app.g_app.setActivationPolicysetActivationPolicy(NSApplicationActivationPolicyRegular);
immutable _error_ contentRectcontentRect = NSRect(NSPoint(120, 120), NSSize(360, 240));
g_win = NSWindow.NSWindow.allocalloc().NSWindow.alloc().initWithContentRectinitWithContentRect(contentRect,
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable,
NSBackingStoreBuffered, false);
g_win.g_win.setTitlesetTitle(NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("wsi-f05-loop-wakeup"));
(unresolved type) FillViewFillView _error_ viewview = FillView.FillView.allocalloc().FillView.alloc().initWithFrameinitWithFrame(contentRect);
g_win.g_win.setContentViewsetContentView(view);
g_win.g_win.makeKeyAndOrderFrontmakeKeyAndOrderFront(null);
g_app.g_app.activateIgnoringOtherAppsactivateIgnoringOtherApps(true);
instr("window_created");
// We are on the main thread; [NSApp run] will pump THIS run loop. Add both the
// CFRunLoopSource (mech B) and the CFFileDescriptor source (fd) to it.
g_mainRL = CFRunLoopGetCurrent();
instr("step", "name=CFRunLoopSourceCreate version=0");
(unresolved type) CFRunLoopSourceContextCFRunLoopSourceContext _error_ ctxctx;
ctx.ctx.version_version_ = 0;
ctx.ctx.performperform = cast(void*)&performCFSource;
g_cfSource = CFRunLoopSourceCreate(null, 0, &ctx);
CFRunLoopAddSource(g_mainRL, g_cfSource, kCFRunLoopCommonModes);
// Arbitrary fd: a pipe, read end made nonblocking and wrapped in a
// CFFileDescriptor run-loop source. This is the ONLY way a raw fd joins AppKit's
// loop — see ../../f05-loop-wakeup.md for the dispatch_source_t alternative.
int[2] _error_ fdsfds;
if (pipe(fds) != 0)
{
printf("SKIP: pipe() failed\n");
return 0;
}
g_pipeRead = fds[0];
g_pipeWrite = fds[1];
fcntl(g_pipeRead, F_SETFL, fcntl(g_pipeRead, F_GETFL, 0) | O_NONBLOCK);
instr("step", "name=CFFileDescriptorCreate fd=%d", g_pipeRead);
(unresolved type) CFFileDescriptorContextCFFileDescriptorContext _error_ fctxfctx;
(unresolved type) CFFileDescriptorRefCFFileDescriptorRef _error_ cffdcffd = CFFileDescriptorCreate(null, g_pipeRead, true,
cast(void*)&fdCallout, &fctx);
CFFileDescriptorEnableCallBacks(cffd, kCFFileDescriptorReadCallBack);
(unresolved type) CFRunLoopSourceRefCFRunLoopSourceRef _error_ fdSourcefdSource = CFFileDescriptorCreateRunLoopSource(null, cffd, 0);
CFRunLoopAddSource(g_mainRL, fdSource, kCFRunLoopCommonModes);
// Launch the worker and cede the main thread to AppKit.
instr("step", "name=worker_start period_ms=100 fd_period_ms=143 duration_s=%d",
g_durationS);
g_worker = new (unresolved type) ThreadThread(&workerMain);
g_worker.g_worker.startstart();
instr("step", "name=NSApp_run");
g_app.g_app.runrun();
atomicStore(g_stopWorker, true);
g_worker.join();
reportStats("postevent", g_lat[MechId.MechId.posteventpostevent][0 .. g_latN[MechId.postevent]]);
reportStats("cfrunloopsource",
g_lat[MechId.MechId.cfrunloopsourcecfrunloopsource][0 .. g_latN[MechId.cfrunloopsource]]);
reportStats("fd", g_fdLat[0 .. g_fdN]);
instr("loop_exit", "wakes=%u fd_ticks=%u", g_wakeCount, g_fdCount);
close(g_pipeRead);
close(g_pipeWrite);
return 0;
}