Conversation
Agent-Logs-Url: https://github.com/debugtheworldbot/keyStats/sessions/2462d95c-e8b9-423e-8931-2248c29db376 Co-authored-by: debugtheworldbot <62830430+debugtheworldbot@users.noreply.github.com>
…peak tracking Agent-Logs-Url: https://github.com/debugtheworldbot/keyStats/sessions/2462d95c-e8b9-423e-8931-2248c29db376 Co-authored-by: debugtheworldbot <62830430+debugtheworldbot@users.noreply.github.com>
…ateLock Agent-Logs-Url: https://github.com/debugtheworldbot/keyStats/sessions/2462d95c-e8b9-423e-8931-2248c29db376 Co-authored-by: debugtheworldbot <62830430+debugtheworldbot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds per-day peak input-rate metrics (peak KPS/CPS over a 1-second sliding window) and displays them in the stats UI on macOS and Windows.
Changes:
- Extended the daily stats model with
peakKPS/peakCPS(backward-compatible decoding). - Implemented 1-second sliding-window peak tracking for keys and clicks.
- Surfaced peak KPS/CPS in the macOS popover and Windows stats popup UI.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| KeyStats/StatsManager.swift | Adds timestamp-window tracking and stores peak KPS/CPS in current daily stats. |
| KeyStats/StatsModels.swift | Extends DailyStats Codable model with peakKPS/peakCPS and defaults. |
| KeyStats/StatsPopoverViewController.swift | Displays peak KPS/CPS in the macOS stats popover. |
| KeyStats/en.lproj/Localizable.strings | Adds English localized labels for peak KPS/CPS. |
| KeyStats/zh-Hans.lproj/Localizable.strings | Adds Simplified Chinese localized labels for peak KPS/CPS. |
| KeyStats.Windows/KeyStats/Services/StatsManager.cs | Adds Windows-side peak tracking and clears windows on day rollover. |
| KeyStats.Windows/KeyStats/Models/DailyStats.cs | Adds JSON fields for peak KPS/CPS on Windows daily stats. |
| KeyStats.Windows/KeyStats/ViewModels/StatsPopupViewModel.cs | Exposes formatted PeakKPS/PeakCPS bindings for the popup UI. |
| KeyStats.Windows/KeyStats/Views/StatsPopupWindow.xaml | Adds two stat cards for Peak KPS/CPS. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // KPS/CPS peak tracking (1-second sliding window) | ||
| private readonly Queue<DateTime> _recentKeyTimestamps = new(); | ||
| private readonly Queue<DateTime> _recentClickTimestamps = new(); | ||
|
|
There was a problem hiding this comment.
PeakKPS/PeakCPS are now tracked and serialized on DailyStats, but the Windows import/normalize pipeline still drops them: NormalizeDailyStats(...) and MergeDailyStats(...) don’t copy/merge the new fields, so peaks will reset to 0 after load/import and won’t merge as described. Update normalization to carry forward sanitized PeakKPS/PeakCPS and update merge to use max(existing, incoming) for both fields.
| let now = Date() | ||
| let cutoff = now.addingTimeInterval(-1.0) | ||
| peakRateLock.lock() | ||
| recentKeyTimestamps.append(now) | ||
| recentKeyTimestamps = recentKeyTimestamps.filter { $0 > cutoff } | ||
| let currentKPS = Double(recentKeyTimestamps.count) | ||
| peakRateLock.unlock() |
There was a problem hiding this comment.
This peak-rate path allocates a new array on every keypress (filter + reassignment). Since it runs per input event, consider using a queue/deque (or an array with a moving start index + occasional compaction) to purge old timestamps in-place to reduce allocations/CPU; apply the same approach to the CPS tracker too.
| var peakKPS: Double | ||
| /// 今日峰值 CPS(每秒点击数) | ||
| var peakCPS: Double | ||
|
|
There was a problem hiding this comment.
DailyStats gained new Codable fields (peakKPS/peakCPS) but the StatsModels test suite doesn’t currently cover their defaults/backward-compatible decoding. Add unit tests that (1) decode legacy JSON without these keys and assert both are 0, and (2) round-trip encode/decode preserves non-zero peaks.
| enum CodingKeys: String, CodingKey { | |
| case date | |
| case keyPresses | |
| case keyPressCounts | |
| case leftClicks | |
| case rightClicks | |
| case sideBackClicks | |
| case sideForwardClicks | |
| case mouseDistance | |
| case scrollDistance | |
| case appStats | |
| case peakKPS | |
| case peakCPS | |
| } | |
| init(from decoder: Decoder) throws { | |
| let container = try decoder.container(keyedBy: CodingKeys.self) | |
| self.date = try container.decode(Date.self, forKey: .date) | |
| self.keyPresses = try container.decode(Int.self, forKey: .keyPresses) | |
| self.keyPressCounts = try container.decode([String: Int].self, forKey: .keyPressCounts) | |
| self.leftClicks = try container.decode(Int.self, forKey: .leftClicks) | |
| self.rightClicks = try container.decode(Int.self, forKey: .rightClicks) | |
| self.sideBackClicks = try container.decode(Int.self, forKey: .sideBackClicks) | |
| self.sideForwardClicks = try container.decode(Int.self, forKey: .sideForwardClicks) | |
| self.mouseDistance = try container.decode(Double.self, forKey: .mouseDistance) | |
| self.scrollDistance = try container.decode(Double.self, forKey: .scrollDistance) | |
| self.appStats = try container.decode([String: AppStats].self, forKey: .appStats) | |
| self.peakKPS = try container.decodeIfPresent(Double.self, forKey: .peakKPS) ?? 0 | |
| self.peakCPS = try container.decodeIfPresent(Double.self, forKey: .peakCPS) ?? 0 | |
| } | |
| func encode(to encoder: Encoder) throws { | |
| var container = encoder.container(keyedBy: CodingKeys.self) | |
| try container.encode(date, forKey: .date) | |
| try container.encode(keyPresses, forKey: .keyPresses) | |
| try container.encode(keyPressCounts, forKey: .keyPressCounts) | |
| try container.encode(leftClicks, forKey: .leftClicks) | |
| try container.encode(rightClicks, forKey: .rightClicks) | |
| try container.encode(sideBackClicks, forKey: .sideBackClicks) | |
| try container.encode(sideForwardClicks, forKey: .sideForwardClicks) | |
| try container.encode(mouseDistance, forKey: .mouseDistance) | |
| try container.encode(scrollDistance, forKey: .scrollDistance) | |
| try container.encode(appStats, forKey: .appStats) | |
| try container.encode(peakKPS, forKey: .peakKPS) | |
| try container.encode(peakCPS, forKey: .peakCPS) | |
| } |
- Add getCurrentKPS()/getCurrentCPS() methods to StatsManager - Add ⚡ KPS badge button next to key press count in stats popover - Badge auto-refreshes every 0.3s while popover is visible - Click badge to show KPS/CPS detail popover with current & peak values - New KPSDetailView (SwiftUI) showing real-time KPS, CPS and peaks - All values displayed as integers - Add localization strings (en/zh-Hans)
Peak KPS/CPS data is now accessible via the ⚡ badge detail popover, so the dedicated row in the main popover is redundant.
- Replace bold centered title + separator with semibold left-aligned 'KeyStats' title (matching section title style) - Move ⚡ KPS badge from key press row to title row (right of 'KeyStats') - Remove keyPressRow wrapper, use keyPressView directly in stats stack
Badge now displays two lines:
- Line 1: ⚡{peakKPS} (keyboard peak keys per second)
- Line 2: ⚡{peakCPS} (mouse peak clicks per second)
… on right
Layout:
⚡️ {peakKPS}
{peakCPS}
Single large lightning icon vertically centered on the left,
two lines of numbers (KPS/CPS) stacked on the right.
Replaced NSButton with custom KPSBadgeView using NSStackView.
- Rounded corners (6pt) with subtle controlBackgroundColor - Inner padding for better visual appearance
Anchor popover to the bottom edge of the badge view.
- Dynamic cornerRadius (height/2) for capsule/pill appearance - Bold weight for KPS/CPS numbers - Tighter icon spacing and balanced padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Peak data on left (large), current data on right (small) - Descriptive titles: "今日峰值按键速度(KPS)" / "当前KPS" etc. - Widened popover to 260pt to fit longer labels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…iency - Add unified statsStateLock protecting currentStats + sliding window state - Make peak update and day reset atomic within same critical section - Ensure scheduleSave and midnight timer are created on main thread - Replace Array+filter with head-pointer queue for O(1) prune - Switch from Date() to ProcessInfo.systemUptime (monotonic clock) - Unify peakKPS/peakCPS type from Double to Int with legacy decode compat - Add currentRatesSnapshot() for atomic UI reads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…snapshots saveStats() now takes a single atomic snapshot of both currentStats and history under statsStateLock before encoding. Export and import paths also snapshot history under the same lock to prevent inconsistent reads. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tracks the highest keys-per-second (KPS) and clicks-per-second (CPS) recorded within any 1-second sliding window during the current day, and surfaces these peaks in the stats UI on both macOS and Windows.
Data model
peakKPS: Double/peakCPS: DoubletoDailyStatson both platforms, backward-compatible (decoded viadecodeIfPresent/ JSON optional)maxof both sides rather than summingTracking logic (1-second sliding window)
Each key press / click enqueues a timestamp, purges entries older than 1 s, and updates the stored peak if the current window count exceeds it. Queues are cleared atomically with
CurrentStatson day rollover.macOS – dedicated
peakRateLockprotects the timestamp queues;currentStats.peakKPS/CPSis updated outside the lock, consistent with the rest of the codebase.Windows – timestamp queues and peak update share
_lockalongsideCurrentStats, so the queue clear inSynchronizeCurrentDayis inherently atomic with the stats reset:UI
peakKPSView/peakCPSViewrow appended to the stats stack inStatsPopoverViewController峰值 KPS/峰值 CPS) added below the distance row inStatsPopupWindow.xaml, bound toPeakKPS/PeakCPSonStatsPopupViewModelenandzh-Hans