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
12 changes: 11 additions & 1 deletion .github/workflows/swift-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,17 @@ jobs:

- name: Premium performance budget
shell: bash
run: scripts/ops/performance-budget.rb --check-home-recent-captures --allow-missing-parakeet-model --max-app-mb 220 --max-resources-mb 80
# The app-build step above runs build.sh, whose launch smoke writes
# build/launch-ui-smoke.json with launchToInteractiveMs. Score it here so
# a launch-to-interactive regression fails CI (PRD WS4.2). The real UX
# number is the warm Apple-Silicon product target (<400ms, measured
# ~285ms). This CI gate is a COARSE catastrophic-regression guard only:
# the GitHub runner's single cold-launch sample is very noisy (observed
# 1420ms and 2095ms on identical commits, ~48% swing), so 3000ms is set
# to never flake while still failing on a gross blow-up (e.g. a
# synchronous model/network load added to the launch path). A future
# tightening is min-of-N sampling in the smoke to sharpen the signal.
run: scripts/ops/performance-budget.rb --check-home-recent-captures --allow-missing-parakeet-model --max-app-mb 220 --max-resources-mb 80 --launch-ui-smoke build/launch-ui-smoke.json --max-launch-interactive-ms 3000

# ---------------------------------------------------------------------------
# Required-status umbrella. Branch protection requires the "build-and-test"
Expand Down
25 changes: 25 additions & 0 deletions Sources/TranscriptedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ class TranscriptedAppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegat
statusItemExists: statusItem != nil,
popoverConfigured: popover != nil,
onboardingCompleted: true,
launchToInteractiveMs: Self.processStartToNowMilliseconds(),
menuVisibilityOverride: smokeMenuVisibility
)
let reportURL = URL(fileURLWithPath: reportPath, isDirectory: false)
Expand All @@ -908,6 +909,30 @@ class TranscriptedAppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegat
}
}

/// Wall-clock milliseconds from the kernel's process-start time to now.
/// Measured from `kinfo_proc.p_starttime` so it captures the true
/// launch-to-interactive latency including pre-`main` dyld/runtime setup,
/// not just the time since the app delegate began running. Returns nil if
/// the syscall fails or the clock reads backwards. CI-smoke only.
private static func processStartToNowMilliseconds() -> Double? {
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var info = kinfo_proc()
var size = MemoryLayout<kinfo_proc>.stride
let status = mib.withUnsafeMutableBufferPointer { buffer in
sysctl(buffer.baseAddress, UInt32(buffer.count), &info, &size, nil, 0)
}
guard status == 0 else { return nil }

var now = timeval()
guard gettimeofday(&now, nil) == 0 else { return nil }

let start = info.kp_proc.p_starttime
let startMs = Double(start.tv_sec) * 1000.0 + Double(start.tv_usec) / 1000.0
let nowMs = Double(now.tv_sec) * 1000.0 + Double(now.tv_usec) / 1000.0
let elapsed = nowMs - startMs
return elapsed >= 0 ? elapsed : nil
}

private func scheduleLaunchUISmokeTerminationIfRequested(environment: [String: String]) {
guard environment["TRANSCRIPTED_LAUNCH_UI_SMOKE_TERMINATE_AFTER_REPORT"] == "1" else { return }

Expand Down
9 changes: 8 additions & 1 deletion Sources/UI/MenuBar/MenuBarPanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ struct MenuBarLaunchUISmokeReport: Codable, Equatable {
let popoverConfigured: Bool
let onboardingCompleted: Bool
let content: MenuBarContentSmokeSnapshot
/// Wall-clock from kernel process-start to the moment the menu-bar UI is
/// interactive (status item + popover configured). Powers the CI
/// launch-to-interactive perf budget (PRD WS4.2). Optional so older report
/// consumers and hand-built fixtures stay decodable.
var launchToInteractiveMs: Double?
}

@MainActor
Expand Down Expand Up @@ -158,6 +163,7 @@ final class MenuBarPanelController: NSViewController {
statusItemExists: Bool,
popoverConfigured: Bool,
onboardingCompleted: Bool,
launchToInteractiveMs: Double? = nil,
menuVisibilityOverride: [MenuBarOptionalItem: Bool]
) -> MenuBarLaunchUISmokeReport {
loadViewIfNeeded()
Expand All @@ -184,7 +190,8 @@ final class MenuBarPanelController: NSViewController {
),
primaryActions: [:],
utilityActions: [:]
)
),
launchToInteractiveMs: launchToInteractiveMs
)
}

Expand Down
14 changes: 10 additions & 4 deletions Sources/UI/Overlay/OverlayRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,16 @@ final class OverlayRootView: NSView {
// MARK: - State Updates

// @_optimize(none): the -O SimplifyCFG pass crashes on this function's
// parameter/closure shape under swiftlang-6.3.2.1.108 (SIL/BorrowUtils.swift:542,
// "cannot get borrow introducers for unknown guaranteed value"). This is a
// compiler bug, not a correctness issue — disable optimization here until upstream
// fixes it. Reproduced in CI at github.com/r3dbars/transcripted actions run 28685559441.
// parameter/closure shape (SIL/BorrowUtils.swift:542, "cannot get borrow
// introducers for unknown guaranteed value"). This is a compiler bug, not a
// correctness issue — disable optimization here until upstream fixes it.
// First seen under swiftlang-6.3.2.1.108 (CI actions run 28685559441).
// Re-verified STILL CRASHING under swiftlang-6.3.3.1.3 (Swift 6.3.3) on
// 2026-07-07: removing this attribute reproduces the identical SimplifyCFG
// fault. Keep it. Perf cost is nil: `updateForState` runs only on discrete
// overlay state transitions (see FloatingOverlayController.pushStateToViews),
// never per audio frame — the hot meter path calls headerView.updateWaveformLevel
// directly, so this function is not on any hot path.
@_optimize(none)
func updateForState(
_ state: FloatingOverlayController.OverlayState,
Expand Down
7 changes: 7 additions & 0 deletions scripts/entrypoints/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,13 @@ PERFORMANCE_BUDGET_ARGS=(--app "$APP_BUNDLE")
if [ "$BUNDLE_PARAKEET_MODELS" = "0" ]; then
PERFORMANCE_BUDGET_ARGS+=(--allow-missing-parakeet-model --max-app-mb 220 --max-resources-mb 80)
fi
# The launch smoke (above) writes the interactive-readiness report; score its
# launch-to-interactive latency when it ran. Skipped smoke → no report → skip
# the budget rather than fail on a file that was intentionally not produced.
LAUNCH_UI_SMOKE_REPORT="$REPO_ROOT/$BUILD_DIR/launch-ui-smoke.json"
if [ "${TRANSCRIPTED_SKIP_LAUNCH_SMOKE:-0}" != "1" ] && [ -s "$LAUNCH_UI_SMOKE_REPORT" ]; then
PERFORMANCE_BUDGET_ARGS+=(--launch-ui-smoke "$LAUNCH_UI_SMOKE_REPORT" --max-launch-interactive-ms 3000)
fi
scripts/ops/performance-budget.rb "${PERFORMANCE_BUDGET_ARGS[@]}"

echo "Build complete!"
Expand Down
38 changes: 38 additions & 0 deletions scripts/ops/performance-budget.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
MAX_MEETING_P95_RTF = 0.05
MAX_HOME_RECENT_CAPTURE_AVERAGE_LOAD_MS = 750.0
MAX_HOME_RECENT_CAPTURE_CANCEL_MS = 100.0
# Launch-to-interactive ceiling (kernel process-start -> menu-bar UI ready),
# read from the launch UI smoke report. PRD WS4.2 product target is <400ms warm
# on Apple Silicon; this regression ceiling carries headroom for slower/cold CI
# runners while still catching a multi-x blow-up. Callers pass an explicit
# --max-launch-interactive-ms tuned to the surface (dev build vs CI runner).
MAX_LAUNCH_TO_INTERACTIVE_MS = 1_500.0
MIN_MEETING_DURATION_SECONDS = 30.0
MIN_TRANSCRIPTION_SAMPLES = 10
MIN_STARTUP_SAMPLES = 3
Expand Down Expand Up @@ -60,6 +66,8 @@
check_home_recent_captures: false,
max_home_recent_capture_average_load_ms: MAX_HOME_RECENT_CAPTURE_AVERAGE_LOAD_MS,
max_home_recent_capture_cancel_ms: MAX_HOME_RECENT_CAPTURE_CANCEL_MS,
launch_ui_smoke_path: nil,
max_launch_to_interactive_ms: MAX_LAUNCH_TO_INTERACTIVE_MS,
require_launch_model_ready_samples: 0,
require_dictation_fast_start_samples: 0,
require_dictation_stop_latency_samples: 0,
Expand Down Expand Up @@ -90,6 +98,8 @@
parser.on("--check-home-recent-captures", "Run deterministic Home recent-captures loader budget") { options[:check_home_recent_captures] = true }
parser.on("--max-home-recent-capture-average-load-ms MS", Float, "Home recent-captures average load budget") { |ms| options[:max_home_recent_capture_average_load_ms] = ms }
parser.on("--max-home-recent-capture-cancel-ms MS", Float, "Home recent-captures cancellation acknowledgement budget") { |ms| options[:max_home_recent_capture_cancel_ms] = ms }
parser.on("--launch-ui-smoke PATH", "Launch UI smoke report JSON for the launch-to-interactive budget") { |path| options[:launch_ui_smoke_path] = path }
parser.on("--max-launch-interactive-ms MS", Float, "Launch-to-interactive budget in ms (kernel process-start to menu-bar UI ready)") { |ms| options[:max_launch_to_interactive_ms] = ms }
parser.on("--require-launch-model-ready-samples N", Integer, "Require at least N launch-to-model-ready samples in --events logs") { |count| options[:require_launch_model_ready_samples] = count }
parser.on("--require-dictation-fast-start-samples N", Integer, "Require at least N fast-start samples in --events logs") { |count| options[:require_dictation_fast_start_samples] = count }
parser.on("--require-dictation-stop-latency-samples N", Integer, "Require at least N stop-latency samples in --events logs") { |count| options[:require_dictation_stop_latency_samples] = count }
Expand Down Expand Up @@ -477,6 +487,30 @@ def run_home_recent_capture_benchmark(max_average_load_ms:, max_cancel_ms:)
end
end

launch_interactive_summary = nil
if options[:launch_ui_smoke_path]
smoke_path = Pathname.new(options[:launch_ui_smoke_path]).expand_path
if !smoke_path.file?
errors << "Missing launch UI smoke report: #{smoke_path}"
else
begin
report = JSON.parse(File.read(smoke_path))
value = float_or_nil(report["launchToInteractiveMs"])
budget = options[:max_launch_to_interactive_ms]
if value.nil?
errors << "Launch UI smoke report #{smoke_path} is missing launchToInteractiveMs"
else
launch_interactive_summary = { value_ms: value, budget_ms: budget }
if value > budget
errors << "Launch to interactive is #{format("%.1fms", value)}, above #{format("%.1fms", budget)}"
end
end
rescue JSON::ParserError => error
errors << "Launch UI smoke report #{smoke_path} is not valid JSON: #{error.message}"
end
end
end

fail_budget!(errors)

puts "Performance budget OK"
Expand Down Expand Up @@ -555,3 +589,7 @@ def run_home_recent_capture_benchmark(max_average_load_ms:, max_cancel_ms:)
puts "Home recent-captures benchmark:"
puts home_recent_capture_summary[:stdout].strip
end
if launch_interactive_summary
puts "Launch to interactive: #{format("%.1fms", launch_interactive_summary[:value_ms])} " \
"(budget #{format("%.1fms", launch_interactive_summary[:budget_ms])})"
end
Loading