Perf/reduce main thread load#29
Open
iamkishann wants to merge 2 commits into
Open
Conversation
…nt publishes - ActivitySessionModel: lower workout timer from 60 Hz to 1 Hz; each tick updates 4+ @published properties, so 60 Hz was driving 60 SwiftUI re-renders per second on the main thread for the duration of any activity session. 1 Hz is sufficient precision for a stopwatch display; timer tolerance raised from 2 ms to 50 ms to let the system coalesce timer firings. - HomeDashboardView: compute landingSnapshots (→ healthStore.landingSnapshots) once per body pass instead of 9 times. Previously every call to landingSnapshot(for:), homeSnapshot(for:), scoreSnapshots, and scorePickerSnapshots each triggered a separate full evaluation of the expensive healthStore.landingSnapshots(…) call. - GooseAppModel: add equality guards on applyHeartRateTimelineSnapshot so heartRateHourlyRanges (updated at 1 Hz by the pipeline) only fires objectWillChange when the data actually changes, preventing unnecessary full-view re-renders of all GooseAppModel @EnvironmentObject observers. - GooseAppModel: equality guard on activityDetectionStatus (set on every movement packet) and movementPacketValidationStatus to suppress redundant objectWillChange publications when the string value is unchanged. - GooseMessageStore: replace O(n) insert-at-0 with array concatenation to avoid unnecessary buffer-copy churn on every 500 ms message flush. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ishes The 60 Hz timer is intentional and needed for accurate HR-zone accounting and motion-intensity tracking for athletes. The problem was that every tick also wrote to @published properties, driving a SwiftUI re-render on every sample. Fix: accumulate all metrics (elapsed, zoneDurations, averageHeartRate, maxHeartRate) into private backing stores at full 60 Hz, and flush to the @published properties at 4 Hz (every 250 ms). SwiftUI coalesces the four property assignments inside flushToUI into a single re-render because they happen synchronously in the same runloop turn. State-transition calls (pause, end) always call flushToUI immediately so the UI reflects the final sample before the session stops. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tigercraft4
referenced
this pull request
in tigercraft4/goose
Jun 9, 2026
Open
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.
ActivitySessionModel.swift — Keep 60 Hz sampling intact, but decouple it from SwiftUI publishes. Each tick now writes to private backing stores (_elapsed, _zoneDurations, etc.) and only flushes to the @published properties at 4 Hz (250 ms). pause() and end() always flush immediately so the UI reflects the final state. Result: 60 re-renders/sec → 4 re-renders/sec during an active session.
HomeDashboardView.swift — healthStore.landingSnapshots(…) was being called 9× per body render (once per landingSnapshot(for:), scoreSnapshots, scorePickerSnapshots call site). Now computed once at the top of body and threaded through as a parameter.
GooseAppModel+Lifecycle.swift — applyHeartRateTimelineSnapshot fires every 1 second and was unconditionally writing to heartRateHourlyRanges, triggering objectWillChange on GooseAppModel (and a full re-render of every @EnvironmentObject observer) even when the data hadn't changed. Added equality guard; same for heartRateStorageStatus.
GooseAppModel+PacketPublishing.swift — activityDetectionStatus (set on every movement packet) and movementPacketValidationStatus (set during validation runs) now skip the @published assignment when the new value equals the current one, suppressing unnecessary broadcasts.
GooseMessageStore.swift — Replaced messages.insert(contentsOf: pendingMessages.reversed(), at: 0) with array concatenation to avoid a buffer-copy churn on every 500 ms flush.