aboutsummaryrefslogtreecommitdiff
path: root/website/src/content
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-08-03 23:17:37 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-08-03 23:17:37 +0300
commit35be08cd953e057927c66dc2ad255df459810446 (patch)
tree5a23dbed0d99a20ff25030a397d8e61ca5299cd0 /website/src/content
parent3642e67c7b9c1d8608d72f288e80b410c602e55c (diff)
docs: refine cpu optimization blog post
Diffstat (limited to 'website/src/content')
-rw-r--r--website/src/content/blog/ten-agents-three-clients-95-percent-less-cpu.md52
1 files changed, 23 insertions, 29 deletions
diff --git a/website/src/content/blog/ten-agents-three-clients-95-percent-less-cpu.md b/website/src/content/blog/ten-agents-three-clients-95-percent-less-cpu.md
index e21bf451..635e787e 100644
--- a/website/src/content/blog/ten-agents-three-clients-95-percent-less-cpu.md
+++ b/website/src/content/blog/ten-agents-three-clients-95-percent-less-cpu.md
@@ -1,30 +1,26 @@
---
title: Ten agents, three clients, 95% less CPU
-description: Herdr's sidebar stopped animating, hidden panes stopped drawing, and mouse movement stopped counting as a change. The saving compounds with panes and clients, so the busiest sessions gained the most.
+description: Herdr stopped animating its sidebar, drawing hidden panes, and repainting unchanged frames during mouse movement. The busiest sessions gained the most.
date: 2026-08-03
ogImage: /assets/og-blog-frames-v1.png
draft: false
---
-Herdr did not get faster.
+Across workloads dominated by unnecessary rendering, total CPU for the Herdr server and its attached clients fell by 89 to 95 percent. The renderer still runs at the same speed. Herdr now asks it to draw fewer frames.
-That distinction is worth keeping, because the honest version is more interesting. No renderer in Herdr became ten times quicker. Herdr stopped asking the renderer to draw things that nobody was going to look at. In the workloads where that work dominated, total CPU across the server and every attached client fell by 89 to 95 percent.
+The saving grows with the session. With one agent and one client, total CPU fell 91 percent. With ten agents producing background output and three clients, it fell 95 percent. Every frame the server skips is also one less frame to serialize, transmit, and apply in each client.
-The saving also compounds, which is the part worth knowing. One agent with one client attached improved 91 percent. Ten agents with background output and three clients attached improved 95, because a frame that is never produced is also never serialized, never transmitted, and never applied by any of them. The busier the session, the more there is to not do.
-
-Three changes got us there, and none of them were found in a profiler. The first started as a complaint, and the rule that came out of it was that motion should mean something changed.
+Three changes got us there, and none came from a profiler. The first started as a complaint and gave us a rule. Motion should mean something changed.
## The spinner was a complaint before it was a cost
Herdr's job is to tell you which agent needs you. You run five, ten, twenty of them, and the sidebar is how you know where to look.
-So every working agent got a spinner. It seemed obvious at the time: motion means alive, and an agent that is thinking should look like it is thinking.
-
-Run twenty agents and you find out what that actually builds. Twenty things moving at once, all saying the same thing, none of them saying it is the one that needs you. The workspace list already carried state as coloured dots, and the sidebar was the noisy half of the same information. People told us. We agreed.
+So every working agent got a spinner. It seemed obvious at the time. Motion means alive, and an agent that is thinking should look like it is thinking.
-The first fix was a performance fix. Instead of rebuilding pane contents for a spinner tick, the server reused the client's retained frame, redrew only the status rectangle, patched it, and sent the smaller result. It worked. It made an animation that should not exist about as cheap as an animation can be.
+Run twenty agents and you get twenty things moving at once, all saying the same thing, none saying it is the one that needs you. The workspace list already carried state as coloured dots. The sidebar was the noisy half of the same information. People told us. We agreed.
-It was still twenty things moving. So we removed them.
+The first fix isolated spinner redraws to the status rectangle. Instead of rebuilding pane contents for every tick, the server reused the client's retained frame, patched the smaller result, and sent it. We had made the spinner about as cheap as possible. Then we removed it anyway. It was a design decision first; the CPU saving was a side effect.
<figure class="pf">
<div class="pf-2">
@@ -48,19 +44,17 @@ It was still twenty things moving. So we removed them.
<figcaption class="pf-cap">Both panels carry the same information. Only one of them lets you find the blocked agent without reading. The left panel is animating in your browser right now, which is the entire argument.</figcaption>
</figure>
-Working state is now a static coloured mark. Entering the state still renders immediately, so a change catches your eye the moment it happens. Staying in it costs nothing, and looks like nothing.
+Working state is now a static coloured mark. Entering the state still renders immediately, so a change catches your eye when it happens. Remaining in the state schedules no animation.
-The performance consequence was larger than the design one. Animating that sidebar meant waking on a timer, rendering the status surface, comparing the frame, preparing it, serializing it, sending it to every attached client, and having each client receive and apply it. Roughly eight times a second, for as long as any agent was working, whether or not one other thing on screen had changed.
+That design change saved more CPU than we expected. The spinner woke the server on a timer, rendered the status surface, compared and prepared the frame, serialized it, sent it to every attached client, and made each client apply it. This happened roughly eight times a second for as long as any agent was working, even when the rest of the screen stayed unchanged.
Removing the spinner removed all of it: the timer, the animation deadlines, the scans asking whether any workspace contained a working pane, the animation-only render causes, the frame patching. Master produces zero scheduled animation frames. One agent working with nothing else on screen went from 1.467 percent CPU to 0.133 on Linux, and from 3.280 to 0.265 on macOS.
-We removed spinners because they were noise, not because they were inefficient.
-
## The same rule, applied to output you cannot see
-Once motion means a change, the question becomes which things are actually changes.
+The same rule applies to output from panes nobody can see.
-When a pane produced output, the PTY reader set a shared flag meaning something happened. It coalesced wakeups efficiently and threw away the one fact that mattered, which was which pane. So when the server picked that flag up it could not tell whether the pane was even on screen. A background agent writing to a tab you were not looking at went into rendering anyway, and with several clients attached, produced frames for all of them.
+When a pane produced output, the PTY reader set a shared flag to say something had happened. That coalesced wakeups efficiently but discarded the source pane. By the time the server picked up the flag, it could not tell whether the pane was on screen. A background agent writing to a tab nobody was viewing still produced frames for every attached client.
<figure class="pf">
<div class="pf-2">
@@ -92,21 +86,21 @@ When a pane produced output, the PTY reader set a shared flag meaning something
<figcaption class="pf-cap">Render requests now carry their source. Writes from one pane coalesce into a single entry, while simultaneous writes from different panes each keep their identity.</figcaption>
</figure>
-Skipping loses nothing. The bytes have already been parsed, so terminal state, scrollback, cursor, OSC metadata and agent detection all stay current. Switching to that tab renders from that state normally. Where the mapping is unknown, the pane is treated as visible rather than suppressed, because a wrong skip costs you a stale screen and a wrong render costs some CPU. Those are not the same kind of mistake.
+Skipping the render does not drop output. The bytes have already been parsed, so terminal state, scrollback, cursor, OSC metadata and agent detection all stay current. Switching to that tab renders from the current state. Where the mapping is unknown, Herdr treats the pane as visible, because a wrong skip leaves a stale screen while a wrong render only costs some CPU.
-## And to input that changes nothing
+## Mouse movement should not redraw an unchanged frame
Every mouse event reported that the view had changed. Pointer movement over a pane is forwarded to the application inside it, but Herdr's own frame is usually identical before and after. At 60 motion events per second, that meant roughly 60 server renders and 60 client frames per second, to draw the same picture sixty times.
-Herdr now knows which of its own modes respond to hover, which is the global menu, context menus and the navigator. Outside those, movement alone repaints nothing, while the events themselves still route through to the pane exactly as before.
+Herdr now tracks which of its own modes respond to hover: the global menu, context menus and the navigator. Outside those modes, movement alone repaints nothing. The events still reach the pane exactly as before.
The benchmark counted both ends. In every measured round, on both builds, 1,680 motion packets were sent and all 1,680 arrived in the pane. Version 0.7.5 emitted around 56 to 60 frames per second doing it. Master emitted none. Linux CPU fell from 9.450 percent to 0.667.
## Why three changes became ninety percent
-A render avoided at the server is not one saving. It is the frame preparation, the serialization, the transmission and the frame handling in every attached client, none of which happen.
+Skipping one server render also skips frame preparation, serialization, transmission, and frame handling in every attached client.
-That is why the numbers grow with clients rather than shrinking. Ten silent working panes with one client improved 78 percent. The same panes with three clients improved 91.
+More clients multiply the saving. Ten silent working panes with one client improved 78 percent. The same panes with three clients improved 91 percent.
<figure class="pf">
<div class="pf-panel">
@@ -139,7 +133,7 @@ That is why the numbers grow with clients rather than shrinking. Ten silent work
</div>
</div>
</div>
- <figcaption class="pf-cap">The last row is the one to look at. macOS followed the same shape, from 22.958 percent to 1.673 on the hidden output case and from 14.857 to 13.498 on the last one.</figcaption>
+ <figcaption class="pf-cap">macOS followed the same shape: 22.958 percent to 1.673 in the hidden-output case, and 14.857 to 13.498 with forty-nine hidden writers at 60Hz.</figcaption>
</figure>
On 0.7.5, adding hidden background output to a session cost 8.7 CPU points on Linux and 12.7 on macOS. On master the same activity costs 0.2 and 0.6, which is roughly the price of reading and parsing the bytes and nothing else.
@@ -148,16 +142,16 @@ On 0.7.5, adding hidden background output to a session cost 8.7 CPU points on Li
Fifty panes, forty-nine of them writing at 60Hz, is about 2,940 terminal updates per second. Every one of them still has to be read from the PTY and parsed into terminal state, because that is what makes the pane correct when you switch to it. Suppressing frames cannot suppress terminal emulation.
-That case improved 23.6 percent on Linux and 9.1 on macOS. Visible output at 60Hz, a pane you are actually watching, improved 33.9 percent on Linux and 2.7 on macOS, which is to say it did not regress. Idle sat at the measurement floor on both builds and deserves no percentage claim at all.
+That case improved 23.6 percent on Linux and 9.1 on macOS. Visible output at 60Hz, in a pane you are watching, improved 33.9 percent on Linux and 2.7 on macOS. Idle sat at the measurement floor on both builds and deserves no percentage claim at all.
-None of those are the headline. All of them are the point. The gains are large exactly where the work was unnecessary and small exactly where it was not.
+The optimization targets unnecessary rendering. Output-heavy cases remain dominated by PTY reads and terminal parsing.
## How this was measured
-Official 0.7.5 release binaries against hashed master builds, on separate Linux and macOS machines, giving 54 isolated observations per operating system. Every run used a fresh named session, real attached clients in fixed 86 by 47 terminals, a five second warmup and twenty one second samples. The figures are total CPU for the Herdr server plus every attached client.
+We compared official 0.7.5 release binaries with hashed master builds on separate Linux and macOS machines, giving 54 isolated observations per operating system. Every run used a fresh named session, real attached clients in fixed 86 by 47 terminals, a five-second warmup and twenty-one-second samples. The figures show total CPU for the Herdr server plus every attached client.
-Every headline result also has a behavioural counter behind it rather than only a CPU total: scheduled frames per second for the sidebar, hidden source skips for background output, exact packet delivery counts for mouse motion. A CPU number on its own can be explained by a dozen things. A CPU number with a matching mechanism counter is harder to argue with.
+We paired every headline CPU result with a behavioural counter: scheduled frames per second for the sidebar, hidden-source skips for background output, and exact packet delivery counts for mouse motion. A CPU number on its own can be explained by a dozen things. A CPU number with a matching mechanism counter is harder to argue with.
-One limit worth stating plainly. This compares a shipped release against master, so intervening changes and build environments are part of what is being compared. The profiler evidence matches the mechanisms closely, but not every CPU point can be assigned to one commit.
+This compares a shipped release against master, so intervening changes and build environments are part of the comparison. The profiler evidence matches the mechanisms closely, but not every CPU point can be assigned to one commit.
-None of this started in a profiler. It started with people saying the sidebar was too busy, and with bug reports and performance reports from people running far stranger sessions than we test. Keep them coming. Thank you to everyone helping make Herdr better.
+The work started with people saying the sidebar was too busy, along with bug reports and performance reports from people running far stranger sessions than we test. Keep them coming, and thank you to everyone helping make Herdr better.