Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ native, and smooth enough to impress.
changed, and compute the points once per change.
- **Every chart takes a finger.** The point under it is marked, and the
line under the chart says what was there. Two fingers give the span.
- **Charts draw on the engine.** Geometry is a value built once per data
change (`FTWShared/Charts`, tested). A chart is one Canvas
(`ChartSurface`) that never sees the finger and a small `ScrubOverlay`
that alone redraws under it, reading a `ScrubModel`. About one point
per 6 pt of width, and every stroke joins round (`ChartStrokeTests`).
Widgets draw the same shapes as still paths.
- **Haptics are sparse.** `.selection` per slot while scrubbing, at most
about 25 a second. `.impact(weight: .light)` at now, the edges of the
cheapest window, the day's high and low, the car's goal and the ends of
Expand Down
2 changes: 2 additions & 0 deletions appleApp/FTW-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<array>
<string>fetch</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
Expand Down
52 changes: 52 additions & 0 deletions appleApp/FTW/UI/FrameGapProbe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#if DEBUG
import os
import QuartzCore
import SwiftUI

/// A debug-only measure of late frames, for where Instruments' Animation
/// Hitches cannot run (the simulator says it is not supported there).
/// With `-FTWFrameLog YES` it wakes at the display's rate
/// and logs, once a second, how many milliseconds the frames came late:
/// each gap longer than 1.5 frames counts for its time past one frame. It
/// sees the main thread's frames, not the render server's.
struct FrameGapProbe: View {
static var enabled: Bool { UserDefaults.standard.bool(forKey: "FTWFrameLog") }

var body: some View {
TimelineView(.animation) { context in
Color.clear.onChange(of: context.date) { FrameGaps.shared.frame(CACurrentMediaTime()) }
}
.frame(width: 1, height: 1)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}

@MainActor
final class FrameGaps {
static let shared = FrameGaps()
private static let log = Logger(subsystem: "energy.ftw.app", category: "frames")
private var last: CFTimeInterval?
private var windowStart: CFTimeInterval?
private var gaps: [Double] = []

func frame(_ t: CFTimeInterval) {
defer { last = t }
guard let last else {
windowStart = t
return
}
gaps.append(t - last)
guard let start = windowStart, t - start >= 1 else { return }
// The median gap is the display's own frame; a gap past 1.5 of
// them counts for the time beyond one frame.
let frame = gaps.sorted()[gaps.count / 2]
let late = gaps.filter { $0 > frame * 1.5 }.reduce(0) { $0 + ($1 - frame) } * 1000
let seconds = t - start
let draws = ChartDraws.take()
Self.log.info("frames \(self.gaps.count) in \(seconds, format: .fixed(precision: 2)) s, late \(late / seconds, format: .fixed(precision: 2)) ms/s, worst gap \((self.gaps.max() ?? 0) * 1000, format: .fixed(precision: 1)) ms, frame \(frame * 1000, format: .fixed(precision: 1)) ms, surface draws \(draws.surface), slot changes \(draws.changes), overlay draws \(draws.overlay), overlay draw \(draws.overlayMs, format: .fixed(precision: 2)) ms, worst \(draws.worstMs, format: .fixed(precision: 2)) ms")
windowStart = t
gaps.removeAll(keepingCapacity: true)
}
}
#endif
Loading
Loading