crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18
parent ddb3e953d67455b295bd929595e742c18f25524b
Author: Michael Camilleri <[email protected]>
Date:   Sat,  2 May 2026 17:25:51 +0900

Remove performance logging

At the risk of tempting the universe, this commit preemptively
removes the performance logging. A debugging device is now available
that makes this less necessary.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/CrossmateApp.swift | 2--
MCrossmate/Models/PlayerPreferences.swift | 16----------------
MCrossmate/Models/PlayerSession.swift | 86-------------------------------------------------------------------------------
MCrossmate/Services/AppServices.swift | 39+--------------------------------------
MCrossmate/Sync/MoveBuffer.swift | 37-------------------------------------
MCrossmate/Views/CellView.swift | 28----------------------------
MCrossmate/Views/DiagnosticsView.swift | 93-------------------------------------------------------------------------------
MCrossmate/Views/GridView.swift | 66+-----------------------------------------------------------------
MCrossmate/Views/PuzzleView.swift | 60+++++++-----------------------------------------------------
MCrossmate/Views/SettingsView.swift | 5-----
10 files changed, 9 insertions(+), 423 deletions(-)

diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift @@ -21,7 +21,6 @@ struct CrossmateApp: App { .environment(\.managedObjectContext, services.persistence.viewContext) .environment(services.driveMonitor) .environment(services.syncMonitor) - .environment(services.performanceMonitor) .environment(\.syncEngine, services.syncEngine) .environment(services.nytAuth) .environment(\.nytPuzzleFetcher, services.nytFetcher) @@ -314,7 +313,6 @@ private struct PuzzleDisplayView: View { do { let (game, mutator) = try store.loadGame(id: gameID) let newSession = PlayerSession(game: game, mutator: mutator) - newSession.performanceMonitor = services.performanceMonitor if mutator.isShared && preferences.isICloudSyncEnabled { Task { await AppDelegate.requestNotificationAuthorizationIfNeeded() } roster = services.makePlayerRoster(for: gameID, preferences: preferences) diff --git a/Crossmate/Models/PlayerPreferences.swift b/Crossmate/Models/PlayerPreferences.swift @@ -18,8 +18,6 @@ final class PlayerPreferences { static let colorID = "playerColorID" static let name = "playerName" static let isICloudSyncEnabled = "isICloudSyncEnabled" - static let isClueBarAnimationEnabled = "isClueBarAnimationEnabled" - static let isViewBodyLoggingEnabled = "isViewBodyLoggingEnabled" } private let local: UserDefaults @@ -40,18 +38,6 @@ final class PlayerPreferences { didSet { local.set(isICloudSyncEnabled, forKey: Keys.isICloudSyncEnabled) } } - /// Debugging switch for testing whether clue text transitions are - /// contributing to input jank. Stored locally only. - var isClueBarAnimationEnabled: Bool { - didSet { local.set(isClueBarAnimationEnabled, forKey: Keys.isClueBarAnimationEnabled) } - } - - /// Debugging switch for the relatively noisy SwiftUI body-count probes. - /// The core input/render duration probes remain active. - var isViewBodyLoggingEnabled: Bool { - didSet { local.set(isViewBodyLoggingEnabled, forKey: Keys.isViewBodyLoggingEnabled) } - } - var color: PlayerColor { get { PlayerColor.color(for: colorID) } set { colorID = newValue.id } @@ -70,8 +56,6 @@ final class PlayerPreferences { ?? local.string(forKey: Keys.name) ?? "Player" self.isICloudSyncEnabled = local.object(forKey: Keys.isICloudSyncEnabled) as? Bool ?? true - self.isClueBarAnimationEnabled = local.object(forKey: Keys.isClueBarAnimationEnabled) as? Bool ?? true - self.isViewBodyLoggingEnabled = local.object(forKey: Keys.isViewBodyLoggingEnabled) as? Bool ?? true cloud.synchronize() NotificationCenter.default.addObserver( forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift @@ -34,11 +34,6 @@ final class PlayerSession { @ObservationIgnored var onCompletionStateChanged: ((Game.CompletionState) -> Void)? - @ObservationIgnored - var performanceMonitor: PerformanceMonitor? - - var renderProbeID: Int? - /// Rebus mode lets the player type a multi-character value into a single /// cell (e.g. "STAR" or "♥"). While active, keyboard input accumulates in /// `rebusBuffer` rather than going straight to `Game.squares`; on commit @@ -213,34 +208,19 @@ final class PlayerSession { // MARK: - Input func enter(_ letter: String) { - let start = ContinuousClock.now let inputRow = selectedRow let inputCol = selectedCol let cell = puzzle.cells[inputRow][inputCol] guard !cell.isBlock else { return } - let expectedEntry = letter.uppercased() - startRenderProbe( - "render.enter", - row: inputRow, - col: inputCol, - expectedEntry: expectedEntry, - detail: "letter=\(expectedEntry)" - ) mutator.setLetter(letter, atRow: inputRow, atCol: inputCol, pencil: isPencilMode) let completionState = game.completionState publishTerminalCompletionState(completionState) if completionState != .solved { advance() } - recordInputDuration( - "input.enter", - start: start, - detail: inputDetail(row: inputRow, col: inputCol, extra: "letter=\(letter)") - ) } func deleteBackward() { - let start = ContinuousClock.now // If the cursor is on an empty cell or a revealed (locked) cell, // retreat first — revealed cells can't be cleared in place, so delete // tunnels past them to the previous editable cell. `clearLetter` @@ -251,20 +231,7 @@ final class PlayerSession { if currentEmpty || currentMark.isRevealed { retreat() } - let inputRow = selectedRow - let inputCol = selectedCol - startRenderProbe( - "render.delete", - row: inputRow, - col: inputCol, - expectedEntry: "" - ) mutator.clearLetter(atRow: selectedRow, atCol: selectedCol) - recordInputDuration( - "input.delete", - start: start, - detail: inputDetail(row: inputRow, col: inputCol) - ) } // MARK: - Rebus @@ -286,31 +253,17 @@ final class PlayerSession { } func commitRebus() { - let start = ContinuousClock.now let value = rebusBuffer let inputRow = selectedRow let inputCol = selectedCol isRebusActive = false rebusBuffer = "" - let expectedEntry = value.uppercased() - startRenderProbe( - "render.rebusCommit", - row: inputRow, - col: inputCol, - expectedEntry: expectedEntry, - detail: "length=\(expectedEntry.count)" - ) mutator.setLetter(value, atRow: inputRow, atCol: inputCol, pencil: isPencilMode) let completionState = game.completionState publishTerminalCompletionState(completionState) if completionState != .solved { advance() } - recordInputDuration( - "input.rebusCommit", - start: start, - detail: inputDetail(row: inputRow, col: inputCol, extra: "length=\(value.count)") - ) } // MARK: - Word geometry @@ -370,45 +323,6 @@ final class PlayerSession { onCompletionStateChanged?(state) } - private func recordInputDuration( - _ name: String, - start: ContinuousClock.Instant, - detail: String = "" - ) { - let duration = start.duration(to: .now) - let milliseconds = Double(duration.components.seconds) * 1000 - + Double(duration.components.attoseconds) / 1_000_000_000_000_000 - performanceMonitor?.record( - name, - durationMS: milliseconds, - detail: detail, - thresholdMS: 6 - ) - } - - private func startRenderProbe( - _ name: String, - row: Int, - col: Int, - expectedEntry: String, - detail: String = "" - ) { - guard let performanceMonitor else { return } - renderProbeID = performanceMonitor.beginRenderProbe( - name: name, - position: GridPosition(row: row, col: col), - expectedEntry: expectedEntry, - detail: detail - ) - } - - private func inputDetail(row: Int, col: Int, extra: String = "") -> String { - let probePrefix = renderProbeID.flatMap { performanceMonitor?.renderProbeDetailPrefix(for: $0) } - ?? "row=\(row) col=\(col)" - guard !extra.isEmpty else { return probePrefix } - return "\(probePrefix) \(extra)" - } - private func advance() { let (dr, dc) = step(for: direction) let r = selectedRow + dr diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -8,7 +8,6 @@ final class AppServices { let store: GameStore let syncEngine: SyncEngine let syncMonitor: SyncMonitor - let performanceMonitor: PerformanceMonitor let nytAuth: NYTAuthService let driveMonitor: DriveMonitor let nytFetcher: NYTPuzzleFetcher @@ -39,8 +38,6 @@ final class AppServices { let syncEngine = SyncEngine(container: self.ckContainer, persistence: persistence) self.syncEngine = syncEngine self.syncMonitor = SyncMonitor() - let performanceMonitor = PerformanceMonitor() - self.performanceMonitor = performanceMonitor self.nytAuth = NYTAuthService() self.driveMonitor = DriveMonitor() self.nytFetcher = NYTPuzzleFetcher { NYTAuthService.currentCookie() } @@ -60,46 +57,17 @@ final class AppServices { guard isEnabled else { return } await syncEngine.enqueueMoves(moves) }, - performanceSink: { [performanceMonitor] name, durationMS, detail, thresholdMS in - await performanceMonitor.record( - name, - durationMS: durationMS, - detail: detail, - thresholdMS: thresholdMS - ) - }, - afterFlush: { [performanceMonitor] gameIDs in + afterFlush: { gameIDs in let isEnabled = await MainActor.run { preferences.isICloudSyncEnabled } guard isEnabled else { return } - let snapshotStart = ContinuousClock.now let result = await store.createSnapshotsIfNeeded(for: gameIDs) - await performanceMonitor.record( - "moveBuffer.snapshotCheck", - durationMS: Self.milliseconds(from: snapshotStart.duration(to: .now)), - detail: "games=\(gameIDs.count) snapshots=\(result.snapshotNames.count) prunedMoves=\(result.prunedMoveNames.count)", - thresholdMS: 8 - ) - let snapshotEnqueueStart = ContinuousClock.now for name in result.snapshotNames { await syncEngine.enqueueSnapshot(ckRecordName: name) } - await performanceMonitor.record( - "moveBuffer.snapshotEnqueue", - durationMS: Self.milliseconds(from: snapshotEnqueueStart.duration(to: .now)), - detail: "snapshots=\(result.snapshotNames.count)", - thresholdMS: 4 - ) - let deleteEnqueueStart = ContinuousClock.now await syncEngine.enqueueDeleteRecords(result.prunedMoveNames) - await performanceMonitor.record( - "moveBuffer.deleteEnqueue", - durationMS: Self.milliseconds(from: deleteEnqueueStart.duration(to: .now)), - detail: "moves=\(result.prunedMoveNames.count)", - thresholdMS: 4 - ) }, sessionPingSink: { [preferences] gameID, authorID in guard await MainActor.run(body: { preferences.isICloudSyncEnabled }) else { return } @@ -414,11 +382,6 @@ final class AppServices { syncMonitor.updateSnapshot(snapshot) } - private static func milliseconds(from duration: Duration) -> Double { - Double(duration.components.seconds) * 1000 - + Double(duration.components.attoseconds) / 1_000_000_000_000_000 - } - /// Builds the `GameStore.onGameDeleted` callback. Extracted so tests can /// drive the exact same closure that production wires up — keeps the /// colour-cleanup branch from drifting silently. diff --git a/Crossmate/Sync/MoveBuffer.swift b/Crossmate/Sync/MoveBuffer.swift @@ -32,7 +32,6 @@ actor MoveBuffer { private let sessionPingStaleInterval: TimeInterval private let persistence: PersistenceController private let sink: @Sendable ([Move]) async -> Void - private let performanceSink: (@Sendable (String, Double, String, Double) async -> Void)? private let afterFlush: (@Sendable (Set<UUID>) async -> Void)? private let sessionPingSink: (@Sendable (UUID, String) async -> Void)? @@ -60,7 +59,6 @@ actor MoveBuffer { sessionPingStaleInterval: TimeInterval = 30 * 60, persistence: PersistenceController, sink: @escaping @Sendable ([Move]) async -> Void, - performanceSink: (@Sendable (String, Double, String, Double) async -> Void)? = nil, afterFlush: (@Sendable (Set<UUID>) async -> Void)? = nil, sessionPingSink: (@Sendable (UUID, String) async -> Void)? = nil ) { @@ -68,7 +66,6 @@ actor MoveBuffer { self.sessionPingStaleInterval = sessionPingStaleInterval self.persistence = persistence self.sink = sink - self.performanceSink = performanceSink self.afterFlush = afterFlush self.sessionPingSink = sessionPingSink } @@ -165,57 +162,23 @@ actor MoveBuffer { order.removeAll(keepingCapacity: true) lastCell = nil - let persistStart = ContinuousClock.now let persistedMoves = persistAndAssignLamports( snapshot: snapshot, order: snapshotOrder ) pendingSinkMoves.append(contentsOf: persistedMoves) - await recordPerformance( - "moveBuffer.persist", - start: persistStart, - detail: "moves=\(persistedMoves.count) pendingCloudKit=\(pendingSinkMoves.count)", - thresholdMS: 4 - ) } guard runAfterFlush, !pendingSinkMoves.isEmpty else { return } let moves = pendingSinkMoves pendingSinkMoves.removeAll(keepingCapacity: true) - let sinkStart = ContinuousClock.now await sink(moves) - await recordPerformance( - "moveBuffer.cloudKitEnqueue", - start: sinkStart, - detail: "moves=\(moves.count)", - thresholdMS: 4 - ) if let afterFlush { - let afterFlushStart = ContinuousClock.now await afterFlush(Set(moves.map { $0.gameID })) - await recordPerformance( - "moveBuffer.afterFlush", - start: afterFlushStart, - detail: "games=\(Set(moves.map { $0.gameID }).count)", - thresholdMS: 8 - ) } } - private func recordPerformance( - _ name: String, - start: ContinuousClock.Instant, - detail: String, - thresholdMS: Double - ) async { - guard let performanceSink else { return } - let duration = start.duration(to: .now) - let milliseconds = Double(duration.components.seconds) * 1000 - + Double(duration.components.attoseconds) / 1_000_000_000_000_000 - await performanceSink(name, milliseconds, detail, thresholdMS) - } - /// Allocates lamports from each game's `lamportHighWater`, writes /// `MoveEntity` rows, and bumps the high-water — all inside a single /// background-context transaction so a crash can't leave the high-water diff --git a/Crossmate/Views/CellView.swift b/Crossmate/Views/CellView.swift @@ -10,10 +10,8 @@ struct CellView: View, Equatable { let specialKind: Puzzle.Special? var remoteWordTint: Color? = nil var remoteOutline: Color? = nil - var diagnosticPosition: GridPosition? @Environment(PlayerPreferences.self) private var preferences - @Environment(PerformanceMonitor.self) private var performanceMonitor private var playerColor: PlayerColor { preferences.color } nonisolated static func == (lhs: CellView, rhs: CellView) -> Bool { @@ -26,17 +24,9 @@ struct CellView: View, Equatable { && lhs.specialKind == rhs.specialKind && lhs.remoteWordTint == rhs.remoteWordTint && lhs.remoteOutline == rhs.remoteOutline - && lhs.diagnosticPosition == rhs.diagnosticPosition } var body: some View { - let _ = preferences.isViewBodyLoggingEnabled - ? performanceMonitor.markViewBodyDeferred( - "cell", - key: "active", - detail: diagnosticDetail() - ) - : () ZStack(alignment: .topLeading) { background if !cell.isBlock { @@ -75,24 +65,6 @@ struct CellView: View, Equatable { .contentShape(Rectangle()) } - private func diagnosticDetail() -> String { - var parts: [String] = [] - if let diagnosticPosition { - parts.append("lastRow=\(diagnosticPosition.row)") - parts.append("lastCol=\(diagnosticPosition.col)") - } - if isSelected { - parts.append("selected=1") - } - if isHighlighted { - parts.append("highlighted=1") - } - if !entry.isEmpty { - parts.append("entry=1") - } - return parts.joined(separator: " ") - } - /// Foreground style for the main entry letter. Pencil entries use the /// hierarchical `.secondary` style so they render lighter and respect /// dark mode; everything else — including revealed and checkedWrong diff --git a/Crossmate/Views/DiagnosticsView.swift b/Crossmate/Views/DiagnosticsView.swift @@ -273,96 +273,3 @@ struct ShareDiagnosticsView: View { return lines.joined(separator: "\n") } } - -struct PerformanceDiagnosticsView: View { - @Environment(PerformanceMonitor.self) private var performanceMonitor - - private static let timestampFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .none - formatter.timeStyle = .medium - return formatter - }() - - var body: some View { - List { - Section("Summary") { - row("Recorded Slow Events", String(performanceMonitor.entries.count)) - row("Measured Events", String(performanceMonitor.totalEvents)) - row("Suppressed Fast Events", String(performanceMonitor.suppressedEvents)) - } - - Section("Actions") { - Button("Clear Log", role: .destructive) { - performanceMonitor.clear() - } - } - - Section("Recent Slow Events") { - if performanceMonitor.entries.isEmpty { - Text("No slow events captured yet.") - .foregroundStyle(.secondary) - } else { - ForEach(performanceMonitor.entries.reversed()) { entry in - VStack(alignment: .leading, spacing: 4) { - Text( - "\(Self.timestampFormatter.string(from: entry.timestamp)) \(entry.name) \(formatMS(entry.durationMS))" - ) - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - - if !entry.detail.isEmpty { - Text(entry.detail) - .font(.caption.monospaced()) - .textSelection(.enabled) - } - } - .padding(.vertical, 2) - } - } - } - } - .navigationTitle("Performance Diagnostics") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Copy") { - UIPasteboard.general.string = diagnosticDump - } - } - } - } - - @ViewBuilder - private func row(_ title: String, _ value: String) -> some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - Text(value) - .font(.body.monospaced()) - .textSelection(.enabled) - } - .padding(.vertical, 2) - } - - private func formatMS(_ value: Double) -> String { - value == 0 ? "note" : String(format: "%.2f ms", value) - } - - private var diagnosticDump: String { - var lines: [String] = [] - lines.append("Recorded Slow Events: \(performanceMonitor.entries.count)") - lines.append("Measured Events: \(performanceMonitor.totalEvents)") - lines.append("Suppressed Fast Events: \(performanceMonitor.suppressedEvents)") - lines.append("") - lines.append("Recent Slow Events:") - for entry in performanceMonitor.entries { - let detail = entry.detail.isEmpty ? "" : " \(entry.detail)" - lines.append( - "\(Self.timestampFormatter.string(from: entry.timestamp)) \(entry.name) \(formatMS(entry.durationMS))\(detail)" - ) - } - return lines.joined(separator: "\n") - } -} diff --git a/Crossmate/Views/GridView.swift b/Crossmate/Views/GridView.swift @@ -3,64 +3,26 @@ import SwiftUI struct GridView: View { @Bindable var session: PlayerSession var roster: PlayerRoster? = nil - @Environment(PlayerPreferences.self) private var preferences - @Environment(PerformanceMonitor.self) private var performanceMonitor private let spacing: CGFloat = 1 var body: some View { - let bodyDetail = diagnosticDetail() - let shouldLogViewBodies = preferences.isViewBodyLoggingEnabled - let _ = shouldLogViewBodies - ? performanceMonitor.markViewBodyDeferred( - "grid", - key: session.renderProbeID.map { "probe=\($0)" } ?? "idle", - detail: bodyDetail - ) - : () let width = session.puzzle.width let height = session.puzzle.height // Index remote selections by cell so each CellView only receives the // value it needs. Multiple peers landing on the same cell collapse // to the most recent. - let overlayStart = ContinuousClock.now let (outlineByCell, tintByCell) = remoteOverlays() - let _ = shouldLogViewBodies - ? performanceMonitor.recordDeferred( - "grid.remoteOverlays", - start: overlayStart, - detail: bodyDetail, - thresholdMS: 2 - ) - : () - let relatedStart = ContinuousClock.now let relatedCells = session.puzzle.relatedCells( atRow: session.selectedRow, col: session.selectedCol, direction: session.direction ) - let _ = shouldLogViewBodies - ? performanceMonitor.recordDeferred( - "grid.relatedCells", - start: relatedStart, - detail: "\(bodyDetail) count=\(relatedCells.count)", - thresholdMS: 2 - ) - : () - let wordStart = ContinuousClock.now let currentWordCells = Set(session.puzzle.wordCells( atRow: session.selectedRow, col: session.selectedCol, direction: session.direction ).map { GridPosition(row: $0.row, col: $0.col) }) - let _ = shouldLogViewBodies - ? performanceMonitor.recordDeferred( - "grid.currentWord", - start: wordStart, - detail: "\(bodyDetail) count=\(currentWordCells.count)", - thresholdMS: 2 - ) - : () PuzzleGridLayout(columns: width, rows: height, spacing: spacing) { ForEach(0..<(width * height), id: \.self) { index in let r = index / width @@ -75,31 +37,17 @@ struct GridView: View { isRelatedToFocus: relatedCells.contains(pos), specialKind: session.puzzle.specialKind, remoteWordTint: tintByCell[pos], - remoteOutline: outlineByCell[pos], - diagnosticPosition: pos + remoteOutline: outlineByCell[pos] ) .equatable() .onTapGesture { session.select(row: r, col: c) } - .onChange(of: entryProbeValue(id: session.renderProbeID, entry: session.game.squares[r][c].entry)) { _, value in - guard let id = value.id else { return } - performanceMonitor.completeRenderProbe(id: id, position: pos, entry: value.entry) - } } } .background(Color.black) } - private struct EntryProbeValue: Equatable { - let id: Int? - let entry: String - } - - private func entryProbeValue(id: Int?, entry: String) -> EntryProbeValue { - EntryProbeValue(id: id, entry: entry) - } - /// Builds the focused-cell outline map and the word-tint map from each /// peer's selection. Conflicts (two peers on the same cell or word) are /// resolved by keeping the most recent `updatedAt`. @@ -127,18 +75,6 @@ struct GridView: View { return (outline.mapValues { $0.1 }, tint.mapValues { $0.1 }) } - private func diagnosticDetail() -> String { - var parts: [String] = [] - if let probeID = session.renderProbeID { - parts.append("probe=\(probeID)") - } - parts.append("selected=\(session.selectedRow),\(session.selectedCol)") - parts.append("direction=\(session.direction)") - if let roster { - parts.append("remoteSelections=\(roster.remoteSelections.count)") - } - return parts.joined(separator: " ") - } } // MARK: - Layout diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -371,17 +371,7 @@ private struct ClueBarContent: View { var onNext: (() -> Void)? var onClueTap: (() -> Void)? - @Environment(PlayerPreferences.self) private var preferences - @Environment(PerformanceMonitor.self) private var performanceMonitor - var body: some View { - let _ = preferences.isViewBodyLoggingEnabled - ? performanceMonitor.markViewBodyDeferred( - "clueBarContent", - key: currentKey.map { "\($0.direction)-\($0.number)" } ?? "none", - detail: currentKey.map { "direction=\($0.direction) number=\($0.number)" } ?? "none" - ) - : () HStack(alignment: .clueCenter, spacing: 12) { ClueBarIcon(systemName: "chevron.left", action: onPrevious) @@ -410,16 +400,12 @@ private struct ClueBarContent: View { @ViewBuilder private var clueTextView: some View { - if preferences.isClueBarAnimationEnabled { - baseClueText - .id(currentKey) - .transition(.asymmetric( - insertion: .move(edge: slideEdge), - removal: .move(edge: slideEdge == .trailing ? .leading : .trailing) - )) - } else { - baseClueText - } + baseClueText + .id(currentKey) + .transition(.asymmetric( + insertion: .move(edge: slideEdge), + removal: .move(edge: slideEdge == .trailing ? .leading : .trailing) + )) } private var baseClueText: some View { @@ -456,32 +442,13 @@ private struct ClueBarIcon: View { private struct ClueBar: View { @Bindable var session: PlayerSession @Environment(PlayerPreferences.self) private var preferences - @Environment(PerformanceMonitor.self) private var performanceMonitor @State private var slideEdge: Edge = .trailing @State private var isShowingClueList = false private var playerColor: PlayerColor { preferences.color } var body: some View { - let bodyDetail = diagnosticDetail() - let shouldLogViewBodies = preferences.isViewBodyLoggingEnabled - let _ = shouldLogViewBodies - ? performanceMonitor.markViewBodyDeferred( - "clueBar", - key: session.renderProbeID.map { "probe=\($0)" } ?? "idle", - detail: bodyDetail - ) - : () - let clueStart = ContinuousClock.now let clue = session.currentClue() - let _ = shouldLogViewBodies - ? performanceMonitor.recordDeferred( - "clueBar.currentClue", - start: clueStart, - detail: bodyDetail, - thresholdMS: 1 - ) - : () let currentKey = clue.map { ClueKey(direction: session.direction, number: $0.number) } ClueBarContent( @@ -502,10 +469,7 @@ private struct ClueBar: View { } ) .background(playerColor.highlightFill) - .animation( - preferences.isClueBarAnimationEnabled ? .smooth(duration: 0.22) : nil, - value: currentKey - ) + .animation(.smooth(duration: 0.22), value: currentKey) .sheet(isPresented: $isShowingClueList) { ClueList(session: session) .presentationDetents([.medium, .large]) @@ -513,16 +477,6 @@ private struct ClueBar: View { } } - private func diagnosticDetail() -> String { - var parts: [String] = [] - if let probeID = session.renderProbeID { - parts.append("probe=\(probeID)") - } - parts.append("selected=\(session.selectedRow),\(session.selectedCol)") - parts.append("direction=\(session.direction)") - return parts.joined(separator: " ") - } - private func label(for clue: Puzzle.Clue?) -> String { let direction = session.direction == .across ? "Across" : "Down" if let clue { diff --git a/Crossmate/Views/SettingsView.swift b/Crossmate/Views/SettingsView.swift @@ -23,8 +23,6 @@ struct SettingsView: View { Section("Debugging") { Toggle("Enable iCloud Sync", isOn: $preferences.isICloudSyncEnabled) - Toggle("Animate Clue Bar", isOn: $preferences.isClueBarAnimationEnabled) - Toggle("Log View Body Counts", isOn: $preferences.isViewBodyLoggingEnabled) NavigationLink("iCloud Diagnostics") { DiagnosticsView() @@ -32,9 +30,6 @@ struct SettingsView: View { NavigationLink("Share Diagnostics") { ShareDiagnosticsView() } - NavigationLink("Performance Diagnostics") { - PerformanceDiagnosticsView() - } Button("Reset Database", role: .destructive) { showResetConfirmation = true