moonbit-community / moonbit-community/window
macOS pump_app_events appears to cap FPS even for zero-draw apps
Nobody has claimed this yet.
- Dominant language
- MoonBit
- Stars
- 2
- Forks
- 2
- PR merge metrics
- No merged PRs in 30d
Description
Summary
On macOS, simple apps built on top of Milky2018/window appear to spend a large amount of time in the AppKit event-loop pump path, to the point that even a zero-draw app runs far below expected frame rate.
This does not look like an application-logic or renderer-workload problem.
Environment
Milky2018/windowversion:0.2.11- Platform: macOS 26.3.1
- Architecture: arm64 (Apple Silicon)
- Consumer project:
mgstudio-engine
Repro evidence from a consumer project
I measured this from mgstudio-engine, but the hotspot is in Milky2018/window's macOS loop.
1. Zero-draw baseline is already very slow
A zero-draw example (examples/window/clear_color) was built and then run directly from the built executable.
Command:
cd mgstudio-engine
MGSTUDIO_WINDOW_FRAME_LIMIT=180 ./_build/native/debug/build/examples/window/clear_color/clear_color.exe
Observed:
- renderer stats show
mesh2d=0,mesh3d=0 - wall-clock time for 180 ticks: about 7.17s
- that is roughly 25 FPS
Release is still slow:
cd mgstudio-engine
MGSTUDIO_WINDOW_FRAME_LIMIT=180 ./_build/native/release/build/examples/window/clear_color/clear_color.exe
Observed:
- still
mesh2d=0,mesh3d=0 - wall-clock time for 180 ticks: about 6.29s
- that is roughly 28.6 FPS
So this is not just debug -O0 overhead.
2. A small 2D example is only slightly heavier
For comparison, examples/2d/2d_shapes in the same consumer project only draws 22 mesh items per frame.
Command:
cd mgstudio-engine
MGSTUDIO_WINDOW_FRAME_LIMIT=180 ./_build/native/debug/build/examples/2d/2d_shapes/2d_shapes.exe
Observed:
- about 5.22s for 180 ticks
- roughly 34 FPS
- renderer stats correspond to only 22 mesh draws per frame
This suggests the main cost is fixed per-frame overhead, not example complexity.
Sampling results
I sampled the zero-draw clear_color.exe process on macOS.
The dominant stack is:
mgstudiowindow bridgerun_loopwindow_poll_host_eventshost_window_poll_events- native
pump_native_events window::macos::EventLoop::pump_app_events / try_pump_app_eventsnative_application_run-[NSApplication run]
A large fraction of samples then flow through the stop/wake path around:
app_state_on_before_waiting()stop_app_immediately()native_application_stop_immediately()native_post_application_defined_event(true)
I also saw noticeable time in helper work under that path (UTF-8 / buffer / event-posting related work), which looks consistent with per-tick event-post churn.
Suspected root cause
The current macOS pump strategy appears to do this on each engine tick:
- enter
native_application_run() - stop the app from
app_state_on_before_waiting()viastop_app_immediately() native_application_stop_immediately()posts an application-defined event again vianative_post_application_defined_event(true)
Relevant functions in the published source:
macos/event_loop.mbtnative_application_run()native_application_stop_immediately()EventLoop::pump_app_events()EventLoop::try_pump_app_events()
macos/app_state.mbtapp_state_on_before_waiting()
From the consumer side, the bridge enters this path from its per-frame window polling loop.
Why I think this is a window-layer issue
Because the zero-draw app is already slow.
If the problem were in the 2D renderer, draw batching, shader compilation, or example logic, clear_color should be near-trivial. Instead, it is still around 25-29 FPS.
Question
Is this enter-run / stop / post-event cycle on every tick intended on macOS?
If not, could the macOS event loop be redesigned toward a cheaper non-blocking pump strategy? Right now it appears to cap the effective frame rate of very small apps well below what should be expected.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read macos/event_loop.mbt, especially native_application_run(), native_application_stop_immediately(), and EventLoop::pump_app_events()/try_pump_app_events(), then inspect macos/app_state.mbt's app_state_on_before_waiting(). Reproduce the zero-draw clear_color timing and sample the process; done means the macOS pump behavior is understood and the fixed per-frame event-loop overhead is addressed or confirmed intentional.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos
- Domain
- desktop, operating-systems, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100