Flowlight 0.8.1 - #5
Merged
Merged
Conversation
Two fixes to the same symptom: macOS listing Flowlight under "Using Significant Energy" while it sat idle in the menu bar. Measured 6.3% of a core at idle, 37% averaged over three and a half hours. Occlusion, not view lifecycle. The live chart series and the per-app list were gated on uiVisible, which was driven by SwiftUI's onAppear and onDisappear. Those track the view's lifecycle, which says only that a window exists — it stays "appeared" while the window is minimised, fully covered by another app, or on a Space nobody is looking at. In all three the chart was rebuilt every second for an audience of nobody. It now follows NSWindow.occlusionState, which is the state that actually answers "can anyone see this". The counter became a set. Occlusion notifications are not guaranteed to pair up: a window closed while already occluded, or one missed teardown, leaks a count — and a leaked count pins visibility to true for the rest of the session, silently restoring the cost the gate exists to remove. Keying on window identity makes every update idempotent. WindowVisibility is its own type so the state machine is testable without constructing a TrafficMonitor, whose init opens the real traffic database. SMAppService out of a @State default. SettingsView read "launch at login" in a property's default value. That expression re-runs every time the view struct is constructed, and Settings { } is rebuilt whenever the App body is invalidated — which TrafficMonitor did every second through its @published properties. SMAppService.status is a synchronous XPC round trip to smd, so this put blocking IPC on the main thread at 1 Hz for the life of the process, whether or not Settings was ever opened. A sample caught the whole chain: AppBodyAccessor.updateBody → Settings.init(content:) → SettingsView.init → -[SMAppService status] → _xpc_pipe_routine. It is now read in .task, once, when the pane appears. 470 tests pass, including five new ones covering the drift cases that a counter could not survive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The version bump landed without regenerating `docs/`, so the branch carries a site that still says 0.8.0 — the version string, the structured data and the download button, in all ten languages — while `project.yml` says 0.8.1. That is what the `site` check has been failing on since the branch was pushed. It is the safe direction of the two (the site behind the release rather than ahead of it), but it is the reason `docs/` and `site/` are checked against each other at all: they travel in one commit, because `build_site.py` reads `MARKETING_VERSION`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two fixes to one symptom: macOS listing Flowlight under Using Significant Energy while it sat idle in the menu bar. Measured 6.3% of a core at idle, 37% averaged over three and a half hours.
Occlusion, not view lifecycle
The live chart series and the per-app list were already gated on
uiVisible— but that was driven by SwiftUI'sonAppear/onDisappear, which only say a window exists. It stays "appeared" while the window is minimised, fully covered by another app, or on a Space nobody is looking at. In all three, the chart was rebuilt every second for an audience of nobody.It now follows
NSWindow.occlusionState, which is the state that actually answers "can anyone see this".The counter became a set
Occlusion notifications are not guaranteed to pair up. A window closed while already occluded, or one missed teardown, leaks a count — and a leaked count pins visibility to
truefor the rest of the session, silently restoring the cost the gate exists to remove. Keying on window identity makes every update idempotent.WindowVisibilityis its own type so the state machine is testable without constructing aTrafficMonitor, whoseinitopens the real traffic database.SMAppService out of a
@StatedefaultSettingsViewread "launch at login" in a property's default value:That expression re-runs every time the view struct is constructed, and
Settings { }is rebuilt whenever the App body is invalidated — whichTrafficMonitordid every second through its@Publishedproperties.SMAppService.statusis a synchronous XPC round trip tosmd, so this put blocking IPC on the main thread at 1 Hz for the life of the process, whether or not Settings was ever opened.A sample caught the whole chain:
Now read in
.task, once, when the pane appears.Tests
470 pass, including five new ones covering the drift cases a counter could not survive: repeated notifications for one window, hiding a window that was never visible, and two windows closing in either order.