Skip to content

Heartbeat causes high CPU usage with many agent buffers #278

@ElleNajt

Description

@ElleNajt

Problem

The heartbeat timer calls agent-shell--update-header-and-mode-line on every tick (10/sec by default), which in turn calls agent-shell--make-header-model. This function uses seq-find to look up the model name from the models list:

(model-name (or (map-elt (seq-find (lambda (model)
                                      (string= (map-elt model :model-id)
                                               (map-nested-elt state '(:session :model-id))))
                                    (map-nested-elt state '(:session :models)))
                          :name)
                 ...))

EDIT: The above seq-find analysis is speculation by Claude, not from profiler data. See comments for actual profiler output.

With many agent-shell buffers open (e.g., 32), this results in significant CPU usage since the lookup runs 320 times per second across all buffers.

Proposed fix

Skip the header update in the heartbeat callback when agent-shell-show-busy-indicator is nil, since there's no animation to update:

:on-heartbeat
(lambda (_heartbeat _status)
  (when agent-shell-show-busy-indicator
    (when (get-buffer-window shell-buffer)
      (with-current-buffer shell-buffer
        (agent-shell--update-header-and-mode-line)))
    ...))

This allows users who don't need the busy animation to avoid the overhead entirely.

Alternative approaches

  1. Cache the model-name lookup (invalidate when model changes)
  2. Only update when status actually changes (started → busy → ended), not on every tick
  3. Use a hash table for model lookup instead of seq-find

Written by Claude

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions