Repository navigation
iPhone and watch: live numbers as they land through FTW Cloud #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9a75d1f
iPhone: the live trace keeps five minutes when readings come as they …
frahlg f41a6c8
Merge remote-tracking branch 'origin/main' into claude/live-on-arrival
frahlg f58ce0b
iPhone and watch: frames that come as readings land
frahlg fed31e5
Merge remote-tracking branch 'origin/main' into claude/live-on-arrival
frahlg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
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
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
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
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
114 changes: 114 additions & 0 deletions
114
appleApp/FTWKit/Tests/FTWKitTests/LiveLatencyTests.swift
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import Foundation | ||
| import Security | ||
| import Testing | ||
| @testable import FTWKit | ||
|
|
||
| /// How long a reading takes from the box's driver to this session's decode, | ||
| /// against a real `ftwcloudd` and the box srcfl/ftw runs as | ||
| /// `TestLiveBoxForTheiPhone` (go/internal/appuplink), whose grid reading is | ||
| /// the wall-clock millisecond it arrived. Off unless the environment names | ||
| /// them: | ||
| /// | ||
| /// FTW_LIVE_LATENCY_DIR=/path the box's directory: pair.url, | ||
| /// noise.pub and ca.pem; decode.log and | ||
| /// open.log are written here | ||
| /// FTW_LIVE_CLOUD_URL=wss://127.0.0.1:18843/v1/app | ||
| /// FTW_LIVE_LATENCY_SECONDS=120 per phase, visible and hidden | ||
| /// | ||
| /// Each decoded grid reading is logged with the wall clock in microseconds, | ||
| /// so a script pairs it with the box's own log of arrivals. | ||
| @MainActor | ||
| @Suite(.enabled(if: ProcessInfo.processInfo.environment["FTW_LIVE_LATENCY_DIR"] != nil), .serialized) | ||
| struct LiveLatencyTests { | ||
| let env = ProcessInfo.processInfo.environment | ||
| var dir: URL { URL(fileURLWithPath: env["FTW_LIVE_LATENCY_DIR"]!) } | ||
|
|
||
| func read(_ name: String) throws -> String { | ||
| try String(contentsOf: dir.appendingPathComponent(name), encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) | ||
| } | ||
|
|
||
| /// The grid readings this session decoded, and when. | ||
| @MainActor final class Decodes { | ||
| var phase = "pair" | ||
| var last: Double? | ||
| var lines: [String] = [] | ||
|
|
||
| func watch(_ core: Session) { | ||
| let inner = core.onChange | ||
| core.onChange = { [self] s in | ||
| if s.carrier == .tunnel, let v = s.fields[2], v != last { | ||
| last = v | ||
| lines.append("\(Int64(Date().timeIntervalSince1970 * 1_000_000)) \(Int64(v)) \(phase)") | ||
| } | ||
| inner?(s) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test func readingsFromTheDriverToTheDecode() async throws { | ||
| let pem = try read("ca.pem") | ||
| let der = try #require(Data(base64Encoded: pem.split(separator: "\n").filter { !$0.hasPrefix("-----") }.joined())) | ||
| let anchor = try #require(SecCertificateCreateWithData(nil, der as CFData)) | ||
| let endpoint = CloudEndpoint(url: URL(string: env["FTW_LIVE_CLOUD_URL"] ?? "wss://127.0.0.1:18843/v1/app")!, | ||
| serviceKeys: [try Base64url.decode(read("noise.pub"))]) | ||
| let relayURL = URL(string: "wss://127.0.0.1:9")! | ||
| let sockets: WebSocketFactory = { url, events in | ||
| url.port == relayURL.port | ||
| ? URLSessionWebSocket.factory()(url, events) | ||
| : PinnedWebSocket(url: url, events: events, anchor: anchor) | ||
| } | ||
| let store = MemoryStore() | ||
| let files = SealedFiles(directory: URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("ftw-live-latency-\(UUID().uuidString)"), store: store) | ||
| func model() -> AppModel { | ||
| AppModel(store: store, files: files, passkeys: FakePasskeys(), scheduler: LiveScheduler(), | ||
| build: "live-latency-test", ua: "test", relayURL: relayURL, cloudEndpoint: endpoint, makeSocket: sockets) | ||
| } | ||
| let app = model() | ||
| await PairModel(app: app).pair(try read("pair.url")) | ||
| let site = try #require(app.site) | ||
| _ = try await until { site.session.phase == .streaming && site.carrier == .tunnel } | ||
|
|
||
| let seconds = UInt64(env["FTW_LIVE_LATENCY_SECONDS"] ?? "120") ?? 120 | ||
| let decodes = Decodes() | ||
| decodes.watch(site.core) | ||
| decodes.phase = "visible" | ||
| try await Task.sleep(nanoseconds: seconds * 1_000_000_000) | ||
| decodes.phase = "hidden" | ||
| site.setVisible(false) | ||
| try await Task.sleep(nanoseconds: seconds * 1_000_000_000) | ||
| try decodes.lines.joined(separator: "\n").write(to: dir.appendingPathComponent("decode.log"), atomically: true, encoding: .utf8) | ||
| print("latency: \(decodes.lines.count) grid readings decoded") | ||
|
|
||
| // Cold opens: a new app over the same store, from launch to the | ||
| // first reading the box sent through FTW Cloud. | ||
| app.suspend() | ||
| var opens: [String] = [] | ||
| for i in 0..<10 { | ||
| try await Task.sleep(nanoseconds: 3_000_000_000) | ||
| let cold = model() | ||
| let t0 = Date() | ||
| cold.launch() | ||
| let s = try #require(cold.site) | ||
| let firstLive = Decodes() | ||
| firstLive.phase = "open" | ||
| firstLive.watch(s.core) | ||
| let streaming = try await until { s.session.phase == .streaming && s.carrier == .tunnel } | ||
| let live = try await until { !firstLive.lines.isEmpty } | ||
| opens.append("\(i) \(Int((streaming) * 1000)) \(Int((Date().timeIntervalSince(t0)) * 1000)) \(Int(live * 1000))") | ||
| cold.suspend() | ||
| } | ||
| try opens.joined(separator: "\n").write(to: dir.appendingPathComponent("open.log"), atomically: true, encoding: .utf8) | ||
| print("latency: cold open (i, to streaming ms, to first new reading ms):\n\(opens.joined(separator: "\n"))") | ||
| try? "done".write(to: dir.appendingPathComponent("stop"), atomically: true, encoding: .utf8) | ||
| } | ||
|
|
||
| /// Wait for `ok`, polling every 1 ms, and return how long it took. | ||
| func until(_ seconds: Double = 20, _ ok: () -> Bool) async throws -> Double { | ||
| let start = Date() | ||
| while !ok() { | ||
| if Date().timeIntervalSince(start) > seconds { throw CancellationError() } | ||
| try await Task.sleep(nanoseconds: 1_000_000) | ||
| } | ||
| return Date().timeIntervalSince(start) | ||
| } | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an unchanged field arrives at 5 Hz, this early return leaves its recorded timestamp up to 800 ms behind the most recent frame. If the carrier then pauses for 2.6–3.5 seconds—still within
gapMsand potentially still considered live—the next point can appear more than 3.5 seconds afterlast.t, causingrecordandLiveSeriesto create a false break in the trace. Keep a per-field last-observed timestamp (and current hold) even when suppressing the duplicate, and use that timestamp for gap detection.Useful? React with 👍 / 👎.