Settings load writes UserDefaults when nothing changed, and pays for it twice over - #8631
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughSettings persistence now supports injected stores and skips semantically redundant writes. CI runs eight logical test batches with selector coverage checks. Network determinism detection recognizes executable process launches. Notification handlers use updated closure syntax. ChangesSettings persistence
CI test sharding
Network determinism detection
Notification handler cleanup
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CIWorker as CI worker
participant Workflow as run_unit_tests
participant Xcodebuild as xcodebuild
participant ShardValidator as shard validation
CIWorker->>Workflow: start physical worker
Workflow->>Xcodebuild: run logical batch 1
Xcodebuild-->>Workflow: return test output and status
Workflow->>Xcodebuild: run logical batch 2
Xcodebuild-->>Workflow: return test output and status
Workflow->>ShardValidator: validate logical selector assignments
ShardValidator-->>CIWorker: report coverage and failures
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning, 1 inconclusive)
✅ Passed checks (21 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
d86002b to
27ffeda
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Greptile SummaryThis PR eliminates spurious
Confidence Score: 5/5Safe to merge. The change is a targeted guard around pre-existing write sites with no new code paths introduced. Both modified functions follow the pattern already established in restoreUserDefaultsBackup in the same file. The existence check before removeObject is a direct Foundation correctness fix, and the sorted-keys byte comparison correctly handles the per-process hash seed instability of Swift dictionary encoding. There are no new write paths, no threading model changes, and the previously-failing test now passes. Files Needing Attention: No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant FS as KeyboardShortcutSettingsFileStore
participant UD as UserDefaults
participant NC as NotificationCenter
participant HC as SystemWideHotkeyController
Note over FS: BEFORE — every settings load / file-watcher reload
FS->>UD: removeObject(legacyKey) [key absent]
UD->>NC: didChangeNotification (spurious)
NC->>HC: observer fires synchronously on main thread
HC->>UD: read ~200 keyboard actions
FS->>UD: set(importedData) [value unchanged]
UD->>NC: didChangeNotification (spurious)
NC->>HC: observer fires again
HC->>UD: read ~200 keyboard actions (second time)
Note over FS: AFTER — this PR
FS->>UD: object(forKey: legacyKey)?
UD-->>FS: nil — skip removeObject
FS->>UD: data(forKey: importedKey)?
UD-->>FS: existing bytes
FS->>FS: "encode(imported, sortedKeys) == existing bytes?"
FS->>FS: yes — skip set()
Note over NC,HC: No notification fired, no hotkey re-registration
Reviews (2): Last reviewed commit: "Compare encoded defaults with sorted key..." | Re-trigger Greptile |
27ffeda to
f7edce3
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Regarding CodeRabbit’s Cmux Swift Package Boundaries check: I’m intentionally keeping this change in the existing |
Provide an origin-owned sibling commit that lets GitHub attach the maintained branch to the fork-backed pull request without a direct push to the contributor fork.
Provide an origin-owned sibling commit so GitHub can attach the review fix to the fork-backed pull request without a direct push to the contributor fork.
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Provide an origin-owned sibling commit so GitHub can attach the main-actor test fix to the fork-backed pull request without a direct push to the contributor fork.
Provide an origin-owned sibling commit so GitHub can attach the current origin branch to the fork-backed pull request without a direct push to the contributor fork.
…ettings-store-noop-defaults-write
…op-defaults-write
…op-defaults-write
manaflow-ai#10662 assigns the namespaced group id (MobileWorkspaceGroupPreview.ID) directly to anchorWorkspaceID (MobileWorkspacePreview.ID), which does not compile for the iOS app. Convert through rawValue, the same idiom the group preview initializer already uses for its empty-group fallback. Carried on this branch only to unblock the tagged iOS build; same patch offered to main separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second iOS compile break from manaflow-ai#10662: groupActionCapabilities(for:) was declared fileprivate in WorkspaceListTableCoordinator.swift but is called from the +Actions extension file, so the iOS app does not compile. Widen to internal. Carried on this branch only to unblock the tagged iOS build; same patch offered to main separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same-scope 'let projectRoot = projectRoot ?? cwdURL' after 'var projectRoot: URL?' is an invalid redeclaration and breaks the build; bind the resolved root under a new name.
TerminalNotificationPolicyInFlightStore's guarded dictionary compactMap fails ElementOfResult inference on the fleet toolchain; annotate it and the sibling guarded closure in TerminalNotificationStore explicitly.
…op-defaults-write # Conflicts: # Sources/TerminalNotificationPolicyInFlightStore.swift # Sources/TerminalNotificationStore.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
What happens
Loading settings writes
UserDefaultseven when nothing changed, and that write costs far more than it looks.KeyboardShortcutSettingsFileStore's construction, and every later file-watcher reload whilecmux.jsonis being edited, unconditionally removes one legacy key and rewrites two others.UserDefaultspostsdidChangeNotificationfor a write that changes nothing, and for removing a key that was never there. An observer registered withqueue: .mainruns synchronously on the posting thread rather than being deferred, andSystemWideHotkeyControllerobserves that notification by re-registering the global-search hotkey — which walks the whole action table twice throughKeyboardShortcutSettings, reading each entry out ofUserDefaultsand decoding it.So each settings load pays for a no-op write: every
UserDefaultsobserver in the app wakes, the managed-settings reapply runs again, and roughly two hundred actions are read and decoded twice.It is also a latent re-entrancy hazard. Those lookups reach back through the static store that may still be inside its own initializer; on the app path the store is a lazy static, so only launch ordering keeps this from re-entering its own initialization.
The fix is to write only when the value actually changes, which is the rule
restoreUserDefaultsBackupin this same file already follows.Why no new test
The coverage already existed and was already failing.
testLegacySettingsShortcutBindingsParseWithoutRuntimeConflictLookupasserts that parsing a legacy shortcuts file performs no runtime conflict lookup, and it has been red because construction triggers exactly that through the notification above.Worth recording, because it is what made this hard to see: the same test also asserts that zero
KeyboardShortcutSettings.didChangeNotificationposts reach the default center during the window, and that assertion passed the whole time. The trigger isUserDefaults.didChangeNotification, a different notification, so the test's own instrumentation could not point at it. The recorded lookups start with a loneglobalSearchand then run through the entire action table, which is the exact shape of one hotkey re-registration.Two Foundation behaviours this depends on were checked by running them, not by reading documentation: an observer registered with
queue: .mainruns synchronously when the post happens on the main thread, andremoveObjectposts even for an absent key.Verification
Run on a macOS builder, identical arms, only this change between them:
eeb4866b17Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Stop no‑op
UserDefaultswrites during settings load and file‑watcher reloads to prevent redundant notifications and hotkey re‑registration. Make persistence deterministic across relaunches and injectUserDefaults/LanguageSettingsStoreend‑to‑end.Bug Fixes
cmux.settingsFile.importedManagedDefaults.v1andcmux.settingsFile.backups.v1; encode with sorted‑key JSON to avoid rewriting equal content; remove legacy keys only if present.UserDefaultsandLanguageSettingsStoreacross the store; pass injected defaults through socket‑policy resolution and fail‑closed mode; watchers compare with injected defaults; choosing “system” clears language overrides in the injected suite; apply app icon using injected defaults.NotificationsPage.onChangedeprecations.CI
Written for commit 73ad557. Summary will update on new commits.
Summary by CodeRabbit
Bug Fixes
Improvements
Note
Medium Risk
Changes sit on the hot settings-load and
UserDefaultsnotification path at startup and during file-watcher reloads, but the behavioral change is narrowly scoped to skip writes when values are unchanged.Overview
Settings file load and reload no longer touch
UserDefaultswhen imported managed defaults and backup blobs are already equivalent, avoiding spuriousUserDefaults.didChangeNotificationposts that synchronously re-register global hotkeys and re-read the full shortcut table.KeyboardShortcutSettingsFileStorenow compares decoded values before persisting, uses sorted-key JSON when a write is needed so legacy non-canonical bytes aren’t rewritten on upgrade, and removes legacy sidebar keys only if they exist. The same injectableUserDefaultspattern is extended through language override application (clearing overrides when returning to system language), socket-policy resolution, and app-icon startup paths, with regressions for no-op persistence and language reset.CI adds a dedicated app-host step for the no-op regression, tighter determinism-guard scanning, and safer parallel app-test sharding; NotificationsPage updates deprecated
.onChangeusage.Reviewed by Cursor Bugbot for commit 73ad557. Bugbot is set up for automated code reviews on this repo. Configure here.