Skip to content

Claude Code Desktop: WindowServer at ~47% CPU while a response streams (deep CoreAnimation layer tree re-walked at 120 Hz) #94003

Description

@tbarkai

Summary

While the Claude desktop app generates a response, macOS WindowServer runs at about 47% of one CPU core. The app window is otherwise cheap: with the app idle and the same window visible, WindowServer sits at 3-6%.

A stack sample of WindowServer shows the time goes into re-walking the Claude window's CoreAnimation layer tree, which nests about 50 levels deep, once per display refresh (120 Hz on this machine).

Environment

  • Claude desktop 1.52386.3
  • macOS 26.6.2 (25G83)
  • MacBook Pro, Apple M3 Pro, 14-core GPU
  • Built-in Liquid Retina XDR display, 1512x982 points @ 3024x1964, 120 Hz

Reproduce

  1. Open the Claude desktop app with one window visible and nothing else on the space.
  2. Record WindowServer CPU every 2 seconds.
  3. Send a prompt that produces a long reply.
  4. Compare the CPU while the reply streams against the CPU when the app is idle.

Measurement

One continuous 2-minute trace. Nothing else changed. No other app window was on screen.

   2s   48.5%   previous reply still streaming
   6s   34.0%
   8s    8.0%   streaming stops
  12s    3.0%
  ...
  54s    3.0%   idle
  58s    3.0%
  60s    5.0%
  62s   37.0%   new reply starts streaming
  64s   50.0%
  73s   50.5%
  87s   48.0%
 101s   48.0%
 105s   27.5%   reply ends
 111s    2.5%
 121s    3.0%   idle

Idle: 3-6%. Streaming: 41-51%.

Stack sample

sudo sample WindowServer 10. All other WindowServer threads are idle; the render-server thread is blocked in mach_msg for 6440 of 6472 samples. All the time is on the main thread:

CGXRunOneServicesPass
  post_port_data
    non_coalesced_timer_handler
      run_timer_pass
        update_display_callback(void*, double)              2545
          CGXUpdateDisplay                                  2544
            WS::Updater::UpdateDisplays                     2050
              WS::Updater::prepare_coreanimation            1893
                ca_prepare_begin_window_update(CGXWindow*)  1657
                  WSSessionConnectionPerformWithPluginOwner 1654
                    CARenderUpdateAddContext2               1487
                      CA::Render::Update::add_context        802
                        CA::Render::Updater::prepare_layer0    \
                        CA::Render::Updater::prepare_sublayer0 / ~50 levels of recursion
                        CA::Render::Updater::prepare_portal

One window accounts for this. The ca_prepare_begin_window_update branch with 1657 samples is the Claude window. Every other ca_prepare_begin_window_update branch in the sample has 20 samples or fewer.

Attribution

  • Hiding the Claude window (Command+H) during streaming takes WindowServer from 47% to 1.5%. Restoring it returns to 47%.
  • Hiding every other application on the space changes nothing. Nine applications were eliminated one group at a time.
  • A second Electron application was on screen for one round and contributed nothing measurable. This is specific to the Claude window.
  • A shorter conversation does not help. A fresh session with a two-message transcript measured 51.2%.
  • A smaller window costs less, consistent with fewer layers in the tree.

Ruled out

Scaled display resolution (tested at native 1512x982 @ 3024x1964), video playback in another app, HID event rate (2.3 events/s), accessibility zoom and color filters (all off), EDR (headroom 1.0, inactive), Stage Manager (off), external displays (none), GPU saturation (10% device utilization), WindowServer log errors (none in 20 minutes).

Expected

A streaming text animation should not require WindowServer to re-walk a 50-level layer tree at 120 Hz. Flattening the animated region into fewer composited layers, or confining the animation to its own layer, should remove most of this cost.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions