moonbit-community / moonbit-community/window

macOS pump_app_events appears to cap FPS even for zero-draw apps

Open
#8 1 comment 0 reactions 0 assignees View on GitHub

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/window version: 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:

  • mgstudio window bridge run_loop
  • window_poll_host_events
  • host_window_poll_events
  • native pump_native_events
  • window::macos::EventLoop::pump_app_events / try_pump_app_events
  • native_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:

  1. enter native_application_run()
  2. stop the app from app_state_on_before_waiting() via stop_app_immediately()
  3. native_application_stop_immediately() posts an application-defined event again via native_post_application_defined_event(true)

Relevant functions in the published source:

  • macos/event_loop.mbt
    • native_application_run()
    • native_application_stop_immediately()
    • EventLoop::pump_app_events()
    • EventLoop::try_pump_app_events()
  • macos/app_state.mbt
    • app_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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.