| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """Add measurement-only hooks to disposable git-archive copies, never a checkout.
- Usage: python3 instrument-ios.py /tmp/bot-latency-acceptance [baseline|optimized]
- """
- from pathlib import Path
- import sys
- root = Path(sys.argv[1] if len(sys.argv) > 1 else '/tmp/bot-latency-acceptance')
- modes = [sys.argv[2]] if len(sys.argv) > 2 else ['baseline', 'optimized']
- helper = r'''
- @MainActor enum LatencyProbe {
- private static var opened = false
- static func mark(_ name: String, _ detail: String = "") {
- guard let run = ProcessInfo.processInfo.environment["LATENCY_RUN"] else { return }
- let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("latency-\(run).jsonl")
- if !opened { FileManager.default.createFile(atPath: url.path, contents: Data()); opened = true }
- let row: [String: Any] = ["time": Date().timeIntervalSince1970, "name": name, "detail": detail]
- guard var data = try? JSONSerialization.data(withJSONObject: row) else { return }; data.append(10)
- if let handle = try? FileHandle(forWritingTo: url) { try? handle.seekToEnd(); try? handle.write(contentsOf: data); try? handle.close() }
- }
- }
- #if DEBUG
- @MainActor private struct LatencyFixtureView: View {
- @State private var client: HermesRESTClient?
- @State private var error = "连接隔离测试服务"
- var body: some View {
- Group {
- if let client { WorkspaceChatView(client: client, accountID: nil, accountName: "隔离测试", onOpenDrawer: {}) }
- else { Text(error) }
- }.task {
- do {
- let config = try ServerConfiguration(urlString: ProcessInfo.processInfo.environment["LATENCY_URL"]!)
- let network = HermesNetworkSession.isolated()
- var request = URLRequest(url: config.baseURL.appendingPathComponent("auth/password-login"))
- request.httpMethod = "POST"; request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.httpBody = Data(#"{"username":"fixture","password":"fixture-pass","provider":"basic"}"#.utf8)
- let (_, response) = try await network.session.data(for: request)
- guard (response as? HTTPURLResponse)?.statusCode == 200 else { error = "夹具登录失败"; return }
- let rest = HermesRESTClient(session: network.session, cookieStorage: network.cookieStorage)
- await rest.configure(server: config, authenticationTransport: .cookie); client = rest
- } catch { self.error = error.localizedDescription }
- }
- }
- }
- #endif
- '''
- ui_test = r'''
- extension LaunchSmokeTests {
- func testLatencyCurrent() async throws {
- continueAfterFailure = false
- let product = "MODE-ios", nonce = String(UUID().uuidString.prefix(8))
- for (kind, count) in [("short", 6), ("long", 1)] {
- let group = "\(product)-\(kind)"
- _ = try await URLSession.shared.data(from: URL(string: "http://127.0.0.1:19359/scenario?delay=160&run=\(group)-setup")!)
- let app = XCUIApplication()
- app.launchEnvironment["LATENCY_URL"] = "http://127.0.0.1:PORT"
- app.launchEnvironment["LATENCY_RUN"] = group
- app.launchArguments = ["-AppleLanguages", "(zh-Hans)", "-AppleLocale", "zh_CN"]
- app.launch()
- let chat = app.buttons.matching(NSPredicate(format: "label CONTAINS %@", "LatencyBot")).firstMatch
- XCTAssertTrue(chat.waitForExistence(timeout: 20)); chat.tap()
- let input = app.descendants(matching: .any)["workspace.message-input"].firstMatch
- XCTAssertTrue(input.waitForExistence(timeout: 10))
- for n in 1...count {
- let run = "\(group)-\(n)", marker = "\(kind)-\(group)-\(n)-\(nonce)"
- _ = try await URLSession.shared.data(from: URL(string: "http://127.0.0.1:19359/scenario?delay=160&run=\(run)")!)
- input.tap(); input.typeText("[latency:\(marker)]"); app.buttons["workspace.send"].tap()
- let reply = app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "END-\(marker)")).firstMatch
- XCTAssertTrue(reply.waitForExistence(timeout: 30))
- if n == count { let a = XCTAttachment(screenshot: app.screenshot()); a.name = run; a.lifetime = .keepAlways; add(a) }
- try await Task.sleep(for: .milliseconds(700))
- }
- app.terminate()
- }
- }
- }
- '''
- for mode in modes:
- assert mode in ('baseline', 'optimized')
- repo = root / f'ios-{mode}'
- assert not (repo / '.git').exists(), 'Use a disposable source copy, not a git checkout'
- app = repo / 'YaoYaoAI/App/YaoYaoAIApp.swift'
- text = app.read_text()
- assert 'enum LatencyProbe' not in text, 'Copy is already instrumented'
- needle = 'if let scenario = UITestScenario.current {'
- assert needle in text
- app.write_text(text.replace(needle, 'if ProcessInfo.processInfo.environment["LATENCY_URL"] != nil { LatencyFixtureView() } else '+needle, 1) + helper)
- model = repo / 'YaoYaoAI/Features/WorkspaceChat/WorkspaceChatModel.swift'
- text = model.read_text(); start = text.index(' func send() async {')
- part = text[start:].replace(' busy = true', ' LatencyProbe.mark("send_start")\n busy = true', 1)
- part = part.replace(' defer { busy = false }', ' defer { busy = false; LatencyProbe.mark("send_ready") }', 1)
- lines = part.splitlines(); index = next(i for i, line in enumerate(lines) if 'client.post("/api/app/conversations/\\(c.id)/messages"' in line)
- lines.insert(index + 1, ' LatencyProbe.mark("post_ack")')
- text = text[:start] + '\n'.join(lines) + '\n'
- lines = text.splitlines(); index = next(i for i, line in enumerate(lines) if line.startswith(' private func present('))
- lines.insert(index, ' @ObservationIgnored private var latencyCompletedIDs: Set<String> = []')
- lines.insert(index + 2, ' if let m = detail.messages.last, m.role == "assistant", m.status == "complete", latencyCompletedIDs.insert(m.id).inserted { LatencyProbe.mark("completed_text", m.content) }')
- model.write_text('\n'.join(lines) + '\n')
- view = repo / 'YaoYaoAI/Features/WorkspaceChat/WorkspaceTranscriptView.swift'
- text = view.read_text(); needle = '.onChange(of: messages.last?.content.count) { _, _ in'
- assert needle in text
- view.write_text(text.replace(needle, needle + '\n LatencyProbe.mark("view_text", "\\(messages.last?.role ?? "")|\\(messages.last?.status ?? "")|\\(messages.last?.content.count ?? 0)")', 1))
- tests = repo / 'YaoYaoAIUITests/LaunchSmokeTests.swift'
- tests.write_text(tests.read_text() + ui_test.replace('MODE', mode).replace('PORT', '19371' if mode == 'baseline' else '19372'))
|