Skip to content

fix(settings): normalize icon refresh rate onto a 1–30 fps grid - #929

Merged
diazdesandi merged 3 commits into
thaw-app:developmentfrom
CamilleGuillory:fix/icon-refresh-rate-grid
Aug 11, 2026
Merged

fix(settings): normalize icon refresh rate onto a 1–30 fps grid#929
diazdesandi merged 3 commits into
thaw-app:developmentfrom
CamilleGuillory:fix/icon-refresh-rate-grid

Conversation

@CamilleGuillory

@CamilleGuillory CamilleGuillory commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Normalize icon refresh intervals so the slider, URI handler, live SCK capture floor, and Defaults all agree on one discrete grid: Off, or 1/n seconds for integer n in 1…30.

Scope: Icon refresh rate path only (settings + image-cache sleep/floor + URI/docs/tests).

Linked issue (required)

Closes: N/A

PR Type

  • Bug fix
  • Documentation
  • Enhancement
  • Test addition or update

Area

  • menubar
  • layout
  • settings

Does this PR introduce a breaking change?

  • Yes — thaw:// values above 1 second now clamp to 1 second, and positive values snap to the documented 1–30 fps grid.
  • No

What is the new behavior?

  • Slider range is derived from MenuBarItemImageCache.maxIconRefreshRate (30 fps); stored intervals snap onto the grid in AdvancedSettings.didSet and the normalized value is persisted.
  • URI iconRefreshInterval clamps to 0…1 s, then snaps onto the same grid before writing Defaults (so persistence does not depend on an observer).
  • Live refresh sleep is floored to minIconRefreshInterval so a sub-ms value cannot spin the main actor.
  • Default remains 0.25 s (~4 fps). Users may opt into 30 fps; the slider warns that higher values use more CPU. SkyLight offscreen capture stays at 1/s.

Addresses review on #928/#929: DCO sign-off, URI-side normalization, normalized Defaults persistence, explicit breaking-change disclosure. The 30 fps SCK ceiling is intentional; the existing UI warning exposes its CPU tradeoff.

PR Checklist

  • I've built and run the app locally and verified that it works as expected.
  • I've run swiftformat . to keep the code style consistent.
  • I've run the smallest relevant test commands (list 1–2 below), e.g. xcodebuild test … or swift test --package-path MenuBarModel.
  • I've added tests for new behavior (if applicable).
  • I've documented new public APIs / non-obvious helpers.
  • I've updated documentation as needed.
  • This PR targets the development branch.
  • No dependencies or lockfiles changed.

Test commands run:

  • Blocked locally: Xcode license not accepted on this machine (sudo xcodebuild -license). CI runs the suite.
  • New/updated: IconRefreshIntervalNormalizationTests, URI grid persistence, Defaults-load persistence, profile-apply persistence, and related model expectations.

Follow-up work

  • Monitor CPU at the optional 30 fps setting. The default remains ~4 fps and the slider already warns that higher rates use more CPU.

Summary by CodeRabbit

  • Improvements

    • Icon refresh rates now use supported frame-rate steps from 1–30 FPS.
    • Refreshing is capped at 30 FPS for smoother, more predictable menu bar updates.
    • Very small or invalid intervals are safely normalized, preventing excessive refresh activity.
    • The refresh-rate control now reflects the supported maximum dynamically.
  • Documentation

    • Updated settings guidance to describe the 0–1 second range, frame-rate snapping, and default interval.

Align the slider, URI handler, and live SCK capture floor so stored
intervals only ever hold Off or 1/n seconds for n in 1…30. Snap URI
writes before Defaults persistence, clamp live-loop sleep, and document
the discrete grid. Default remains 0.25 s (~4 fps); SkyLight offscreen
stays at 1/s.

Signed-off-by: Camille Guillory <camille.guillory@gmail.com>
@github-actions github-actions Bot added docs Improvements or additions to documentation enhancement Iteration, improvement, or optimization of an existing feature fix PR that fixes a defect (issue reports use bug) layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity profiles Profiles and layout snapshots settings Settings UI not covered by a more specific area test Test additions or updates labels Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Labels: docs, enhancement, test, breaking-change, fix, menubar, layout, settings, profiles

All convention checks passed. ✅

cc @CamilleGuillory

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR normalizes icon refresh intervals to reciprocal FPS values from 1–30 FPS. It updates runtime throttling, settings assignment, slider limits, URI handling, tests, and URI documentation.

Changes

Icon refresh interval normalization

Layer / File(s) Summary
Refresh limits and settings normalization
Thaw/MenuBar/MenuBarItems/MenuBarItemImageCache.swift, Thaw/Settings/Models/AdvancedSettings.swift, Thaw/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift
The refresh loop uses a 30 FPS minimum interval. AdvancedSettings normalizes assigned values to 0 or reciprocal FPS values. The slider uses the configured maximum rate.
URI validation and persistence
Thaw/Utilities/SettingsURIHandler.swift, ThawTests/Settings/URI/SettingsURIHandlerApplyTests.swift, docs/URI_SCHEMES.md
URI values are clamped to 0–1 seconds, snapped to the supported FPS grid, and persisted as normalized values. Tests and documentation describe the updated behavior.
Normalization and profile validation
ThawTests/Settings/Models/IconRefreshIntervalNormalizationTests.swift, ThawTests/Settings/Models/AdvancedSettingsTests.swift, ThawTests/Settings/Models/ProfileDecodingDefaultsTests.swift
Tests cover edge cases, idempotence, FPS-grid round trips, stored values, external changes, and profile decoding and application.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsURIHandler
  participant AdvancedSettings
  participant MenuBarItemImageCache
  participant SettingsPersistence
  SettingsURIHandler->>AdvancedSettings: normalize iconRefreshInterval
  AdvancedSettings->>SettingsPersistence: persist normalized value
  AdvancedSettings->>MenuBarItemImageCache: provide supported interval
  MenuBarItemImageCache->>MenuBarItemImageCache: clamp refresh-loop sleep
Loading

Possibly related PRs

  • thaw-app/Thaw#399: Extends the same URI-driven iconRefreshInterval handling.
  • thaw-app/Thaw#861: Modifies the same icon refresh and settings code.
  • thaw-app/Thaw#928: Modifies the same refresh-rate constants, normalization logic, UI binding, URI handling, tests, and documentation.

Suggested labels: performance

Suggested reviewers: diazdesandi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description follows the template, explains the normalized behavior, identifies the breaking change, documents tests and limitations, and includes the required Closes line.
Title check ✅ Passed The title clearly and concisely summarizes the main change: normalizing icon refresh rates to a 1–30 FPS grid.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Thaw/Settings/Models/AdvancedSettings.swift`:
- Around line 119-125: Update the iconRefreshInterval didSet logic around
normalizedIconRefreshInterval so normalization does not return before
Defaults.set executes. Persist the normalized value whenever it changes, while
retaining the oldValue guard, and add assertions covering Defaults loading and
profile application to verify the normalized interval is stored.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b178c7d4-a1d8-4bb7-a61b-e96ed3c9dbf0

📥 Commits

Reviewing files that changed from the base of the PR and between 2344eb3 and 42350c7.

📒 Files selected for processing (9)
  • Thaw/MenuBar/MenuBarItems/MenuBarItemImageCache.swift
  • Thaw/Settings/Models/AdvancedSettings.swift
  • Thaw/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift
  • Thaw/Utilities/SettingsURIHandler.swift
  • ThawTests/Settings/Models/AdvancedSettingsTests.swift
  • ThawTests/Settings/Models/IconRefreshIntervalNormalizationTests.swift
  • ThawTests/Settings/Models/ProfileDecodingDefaultsTests.swift
  • ThawTests/Settings/URI/SettingsURIHandlerApplyTests.swift
  • docs/URI_SCHEMES.md

Comment thread Thaw/Settings/Models/AdvancedSettings.swift

@diazdesandi diazdesandi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Bug: the snapping path never writes to Defaults (AdvancedSettings.swift:112).
if iconRefreshInterval != normalized {
    iconRefreshInterval = normalized
    return          // nothing after this runs
}

Swift does not re-enter a property observer when you assign to the property from inside its own didSet. The assignment only replaces the stored value. That return is not handing off to a nested didSet; it drops the write.

I checked this with a standalone repro of the same shape (swiftc, both -Onone and -O): assigning 3.5 leaves an in-memory 1.0 and zero Defaults.set calls.

So any off-grid write updates memory and leaves Defaults stale. That hits profile apply (Profile.swift:157) and the external-change path (AdvancedSettings.swift:346). Apply a profile carrying 2.5, relaunch, and you get whatever was in Defaults before. The URI path is safe only because it writes Defaults itself first.

Delete the return and fall through with the already-normalized value:

didSet {
    let normalized = Self.normalizedIconRefreshInterval(iconRefreshInterval)
    if iconRefreshInterval != normalized {
        iconRefreshInterval = normalized
    }
    guard oldValue != iconRefreshInterval else { return }
    Defaults.set(iconRefreshInterval, forKey: .iconRefreshInterval)
}

Then 3.5 persists as 1.0, 0.26 persists as 0.25, and 0.25 writes nothing.

The tests miss this because they only assert the in-memory value (ProfileDecodingDefaultsTests.swift:327, AdvancedSettingsTests.swift:114). Adding Defaults.double(forKey: .iconRefreshInterval) == 1.0 to the profile-apply test would have caught it.

  1. Design: this reverts the capture rate limit from 8962b4be. MenuBarItemImageCache.swift:252 goes from 250 ms to 1/30 s. That constant landed on July 27 in "perf(image-cache): rate-limit the on-screen capture path," and the comment being deleted says why: 30 composite captures per second is enough to pin a core.

Making the slider ceiling and the engine floor agree is the right goal. Raising the engine to 30 fps is one way; capping the slider at 4 fps is the other, and that one keeps the guard. Your own follow-up note says you may need to put the floor back. If 30 fps raises that risk, move the slider instead of the engine. If 30 fps really is wanted, that seems worth a word with whoever wrote 8962b4be, not a quiet revert inside a normalization change.

  1. Label: the breaking-change box says No, but the URI range narrowed. SettingsURIHandler.swift:115 goes from (0, 5) to (0, 1). An existing thaw://…?iconRefreshInterval=2.5 used to store 2.5 and now stores 1.0. Small blast radius; just tick the box.

What's good: the normalization function is well tested, and the 1 to 30 round trip is the right shape of test. Snapping in the URI handler before the Defaults write, instead of relying on a live observer, is the correct instinct. The Task.sleep fix stands on its own: Int(interval * 1000) truncating to a zero-length sleep was a real spin risk. Docs also quietly fix the stale default of 0.1.

Ensure off-grid values are written after normalization, including Defaults
load and profile application paths.

Signed-off-by: Camille Guillory <camille.guillory@gmail.com>
@CamilleGuillory

Copy link
Copy Markdown
Contributor Author

Addressed review:

  1. Fixed normalized-value persistence in ac8244dd; added Defaults assertions for load and profile apply.
  2. Keeping the requested 30 fps ceiling intentionally. Default remains ~4 fps, SkyLight stays 1/s, and the slider warns that higher rates use more CPU.
  3. Marked the URI range/grid change as breaking in the PR description.
  4. Synced the branch with current development.

@github-actions github-actions Bot added the breaking-change PR introduces a breaking change label Aug 11, 2026
@CamilleGuillory

Copy link
Copy Markdown
Contributor Author

@diazdesandi The requested persistence fix and tests are now in ac8244dd; the PR description marks the URI change as breaking. We are intentionally keeping 30 fps because the slider already warns about CPU usage. CI is green and the branch is current with development. Please re-review when convenient.

@diazdesandi diazdesandi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great, thanks

@diazdesandi
diazdesandi merged commit eb08bcf into thaw-app:development Aug 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change PR introduces a breaking change docs Improvements or additions to documentation enhancement Iteration, improvement, or optimization of an existing feature fix PR that fixes a defect (issue reports use bug) layout Saved layouts, LayoutBar, reorder, spacing menubar Hide/show, sections, control items, backends, capacity profiles Profiles and layout snapshots settings Settings UI not covered by a more specific area test Test additions or updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants