Skip to content

perf improvements: Replace Thread.sleep polling with NSCondition signaling - #5

Open
AtAFork wants to merge 1 commit into
jnsahaj:mainfrom
AtAFork:refactor/event-driven-tracking
Open

AtAFork wants to merge 1 commit into
jnsahaj:mainfrom
AtAFork:refactor/event-driven-tracking

Conversation

@AtAFork

@AtAFork AtAFork commented Mar 27, 2026

Copy link
Copy Markdown

The previous polling approach wasted CPU spinning in sleep loops regardless of whether new frames were available.

Switch from busy-wait loops (Thread.sleep 33ms) to condition-variable signaling so the calibration loop, camera startup check, and main tracking loop all block until a new frame actually arrives.

  • Add FaceSample value type for thread-safe snapshot delivery
  • FaceTracker: NSLock → NSCondition, reuse VNSequenceRequestHandler and VNDetectFaceRectanglesRequest across frames, wrap Vision processing in autoreleasepool
  • CameraCapture: use beginConfiguration/commitConfiguration, set VGA preset to reduce processing overhead, add autoreleaseFrequency
  • main.swift: build monitorNamesByID dictionary for O(1) lookups, flatten nested control flow in tracking loop

Summary by CodeRabbit

  • Bug Fixes

    • Improved camera initialization with enhanced device validation and automatic resolution selection (VGA 640x480 preferred)
    • Strengthened error handling during camera configuration with proper session cleanup on failure
  • Improvements

    • Enhanced face tracking responsiveness with event-based sampling strategy
    • Optimized monitor detection and name lookup performance

Switch from busy-wait loops (Thread.sleep 33ms) to condition-variable
signaling so the calibration loop, camera startup check, and main
tracking loop all block until a new frame actually arrives.

- Add FaceSample value type for thread-safe snapshot delivery
- FaceTracker: NSLock → NSCondition, reuse VNSequenceRequestHandler
  and VNDetectFaceRectanglesRequest across frames, wrap Vision
  processing in autoreleasepool
- CameraCapture: use beginConfiguration/commitConfiguration, set VGA
  preset to reduce processing overhead, add autoreleaseFrequency
- main.swift: build monitorNamesByID dictionary for O(1) lookups,
  flatten nested control flow in tracking loop
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes introduce an event-driven sampling mechanism through a new FaceSample data structure and waitForNextSample() method in FaceTracker, replacing polling-based frame sampling in Calibration and main loops. CameraCapture is enhanced with better error handling, session configuration wrapping, and conditional preset selection. Monitor name lookups are optimized via a dictionary-based approach.

Changes

Cohort / File(s) Summary
State Management & Synchronization
Sources/FaceTracker.swift
Introduced FaceSample struct and refactored internal state from separate NSLock-guarded fields to a single NSCondition-based latestSampleState. Added snapshot() and waitForNextSample(after:timeout:) methods for event-driven sample delivery. Replaced direct field access with atomic updateSample(yaw:pitch:) and improved face detection handling with persistent VNSequenceRequestHandler.
Camera Infrastructure
Sources/CameraCapture.swift
Enhanced dispatch queue configuration with autoreleaseFrequency: .workItem, strengthened camera index validation, and wrapped preset/input/output configuration within session.beginConfiguration()/session.commitConfiguration() blocks. Added conditional preset selection (.vga640x480 if supported, else .medium) and improved error handling with proper configuration cleanup.
Sampling Loop Integration
Sources/Calibration.swift, Sources/main.swift
Replaced polling loops with event-driven waitForNextSample(after:timeout:) calls, eliminating fixed sleep intervals. Optimized monitor name lookup by introducing monitorNamesByID dictionary and refactored debug logging conditions and target name resolution logic.

Sequence Diagram

sequenceDiagram
    participant App as main.swift
    participant Camera as CameraCapture
    participant Tracker as FaceTracker
    participant Vision as AVFoundation/Vision

    App->>Camera: start(cameraIndex:)
    Camera->>Camera: beginConfiguration()
    Camera->>Vision: configure preset, input, output
    Vision-->>Camera: success
    Camera->>Camera: commitConfiguration()
    
    loop Frame Processing
        Vision->>Tracker: processFrame(pixelBuffer)
        Tracker->>Tracker: detectFaceRectangles()
        Tracker->>Tracker: extract yaw, pitch
        Tracker->>Tracker: updateSample(yaw:pitch:)
        Tracker->>Tracker: broadcast condition
        
        App->>Tracker: waitForNextSample(after:timeout:)
        Tracker-->>Tracker: frameCount advanced?
        alt Sample available
            Tracker-->>App: return FaceSample
            App->>App: process gaze & update display
        else Timeout
            Tracker-->>App: return nil
            App->>App: skip iteration
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Frame by frame, no more the sleep,
Events wake where samples deep,
Condition guards our state so bright,
FaceTracker dances with Vision's sight,
Camera configured just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main performance improvement: replacing polling-based synchronization (Thread.sleep) with event-driven signaling (NSCondition), which is the primary objective of the changeset across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
Sources/main.swift (1)

160-169: Consider adding a consecutive timeout counter for graceful degradation.

Currently, if the camera stops delivering frames, waitForNextSample will repeatedly timeout and continue, causing the loop to spin on 0.25s intervals indefinitely. While not a busy-wait, this could be improved by tracking consecutive timeouts and either logging a warning or exiting gracefully after sustained frame loss.

💡 Example enhancement
 var lastFrameCount = faceTracker.frameCount
+var consecutiveTimeouts = 0
+let maxConsecutiveTimeouts = 20  // ~5 seconds at 0.25s timeout

 while running {
     guard let sample = faceTracker.waitForNextSample(after: lastFrameCount, timeout: 0.25) else {
+        consecutiveTimeouts += 1
+        if consecutiveTimeouts >= maxConsecutiveTimeouts {
+            CLI.warning("Camera stopped delivering frames")
+            break
+        }
         continue
     }
+    consecutiveTimeouts = 0

     lastFrameCount = sample.frameCount
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/main.swift` around lines 160 - 169, The loop that calls
faceTracker.waitForNextSample(after: lastFrameCount, timeout: 0.25) should track
consecutive timeouts so the app can degrade gracefully: add a local counter
(e.g., consecutiveTimeouts) inside the while running loop scope, increment it
when waitForNextSample returns nil, reset it to zero when a sample is received
(before updating lastFrameCount), and when the counter exceeds a small threshold
(e.g., maxConsecutiveTimeouts constant) either log a warning via your logger and
continue with backoff or break/exit the loop to stop processing; update
references to lastFrameCount, running, faceTracker, and yaw as needed so the
counter logic executes around the existing guard checks.
Sources/FaceTracker.swift (1)

90-99: Consider using defer for lock safety in publishCurrentState() and updateSample().

Both methods use manual lock()/unlock() patterns. While currently correct, using defer { condition.unlock() } immediately after lock() (as done in snapshot() and waitForNextSample()) provides better safety against future modifications that might add early returns or throwing code.

♻️ Suggested refactor for consistency
 private func publishCurrentState() {
     condition.lock()
+    defer { condition.unlock() }
     latestSampleState = FaceSample(
         yaw: smoothedYaw,
         pitch: smoothedPitch,
         frameCount: latestSampleState.frameCount + 1
     )
     condition.broadcast()
-    condition.unlock()
 }

 private func updateSample(yaw: Double, pitch: Double?) {
     condition.lock()
+    defer { condition.unlock() }
     
     if let previousYaw = smoothedYaw {
         smoothedYaw = previousYaw + smoothing * (yaw - previousYaw)
     } else {
         smoothedYaw = yaw
     }
     
     if let pitch {
         if let previousPitch = smoothedPitch {
             smoothedPitch = previousPitch + smoothing * (pitch - previousPitch)
         } else {
             smoothedPitch = pitch
         }
     }
     
     latestSampleState = FaceSample(
         yaw: smoothedYaw,
         pitch: smoothedPitch,
         frameCount: latestSampleState.frameCount + 1
     )
     condition.broadcast()
-    condition.unlock()
 }

Also applies to: 101-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/FaceTracker.swift` around lines 90 - 99, The manual lock()/unlock()
usage in publishCurrentState() (and similarly in updateSample()) should be made
safer by calling condition.lock() and immediately adding defer {
condition.unlock() } so the unlock always runs even if future changes add early
returns or throws; update the bodies of publishCurrentState() and updateSample()
to remove explicit condition.unlock() calls and rely on the defer-based unlock
while keeping the same updates to latestSampleState and condition.broadcast().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/FaceTracker.swift`:
- Around line 90-99: The manual lock()/unlock() usage in publishCurrentState()
(and similarly in updateSample()) should be made safer by calling
condition.lock() and immediately adding defer { condition.unlock() } so the
unlock always runs even if future changes add early returns or throws; update
the bodies of publishCurrentState() and updateSample() to remove explicit
condition.unlock() calls and rely on the defer-based unlock while keeping the
same updates to latestSampleState and condition.broadcast().

In `@Sources/main.swift`:
- Around line 160-169: The loop that calls faceTracker.waitForNextSample(after:
lastFrameCount, timeout: 0.25) should track consecutive timeouts so the app can
degrade gracefully: add a local counter (e.g., consecutiveTimeouts) inside the
while running loop scope, increment it when waitForNextSample returns nil, reset
it to zero when a sample is received (before updating lastFrameCount), and when
the counter exceeds a small threshold (e.g., maxConsecutiveTimeouts constant)
either log a warning via your logger and continue with backoff or break/exit the
loop to stop processing; update references to lastFrameCount, running,
faceTracker, and yaw as needed so the counter logic executes around the existing
guard checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a749afe4-3fc6-4445-9d40-c5e2b095846b

📥 Commits

Reviewing files that changed from the base of the PR and between f7eeb13 and 1f52a4b.

📒 Files selected for processing (4)
  • Sources/Calibration.swift
  • Sources/CameraCapture.swift
  • Sources/FaceTracker.swift
  • Sources/main.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant