fix(desktop): make App Startup Timing measure a startup again - #12271
Open
Git-on-my-level wants to merge 1 commit into
Open
fix(desktop): make App Startup Timing measure a startup again#12271Git-on-my-level wants to merge 1 commit into
Git-on-my-level wants to merge 1 commit into
Conversation
Git-on-my-level
force-pushed
the
telemetry/app-startup-timing
branch
2 times, most recently
from
August 27, 2026 02:10
f4920da to
ec8f908
Compare
This was referenced Aug 27, 2026
Three separate defects, all of which the 2026-08 macOS churn cohort
analysis saw as one ("11 samples across 7 users; had_unclean_shutdown
true in 10/11; TTI of 11-131ms is implausible for a cold start").
1. It does not reach PostHog at all. `trackStartupTiming` writes only a
Sentry breadcrumb, so the event stopped existing when that landed.
PostHog, 2026-08-18 -> 2026-08-25, macOS: every 0.12.x build emits 0
samples against 4,380 `App Launched` on 0.12.187 alone; the only
samples in the whole window come from 0.11.x stragglers. The cohort's
11 samples are not a sampling defect, they are the last of a retired
event.
2. `had_unclean_shutdown` was true on nearly every sample because the
read raced the write. `.omi_running` is created at the end of
`performInitialization()`, and seventeen storage actors open the
database lazily, so whichever one wins arrives before
`ViewModelContainer.loadAllData()` reads the flag - which then sees
*this* session's flag and calls the launch a crash.
3. `time_to_interactive_ms` measured `loadAllData()`, which starts long
after `main()`. That is what produced 11-131ms.
## What changed
- `AnalyticsManager.trackStartupTiming` emits `App Startup Timing` to
PostHog again, once per process (`loadAllData` re-runs on owner
switch, and that is not a launch). The Sentry breadcrumb stays so the
numbers remain attached to a same-session crash report.
- `RewindDatabase` latches the unclean-shutdown verdict at the first
observation in the process, and `performInitialization()` latches it
from `previousCrashed` before writing this session's flag. The verdict
is now a property of the process, not of call order. `close()` clears
the latch so a retarget re-observes for the user it opens.
- `AppStartupTiming.processStartDate()` reads
`kinfo_proc.kp_proc.p_starttime` - the kernel's exec stamp, before
dyld and before `main`. `time_to_interactive_ms` is measured from
there, and is omitted rather than faked when the lookup fails.
- The old measurement is still reported, under the name it deserves:
`data_load_ms`.
**`time_to_interactive_ms` keeps its name and changes meaning.** It now
includes process start. Existing values are not comparable, but there
are only 11 of them and they were wrong.
## Proof
- `RewindDatabaseLifecycleTests.testUncleanShutdownVerdictSurvivesTheDatabaseOpeningFirst`
opens the database first, asserts this session's flag exists, then
asserts the verdict is false. Removing the latch makes it fail
(verified).
- `RewindDatabaseLifecycleTests.testPreviousSessionCrashIsStillReportedAfterTheDatabaseOpens`
proves the latch does not swallow a real crash.
- `AppStartupTimingTests` pins that the kernel stamp is read and precedes
any `Date()` our own code can take, that conversion floors at zero
across a wall-clock adjustment, and that a failed lookup yields no
measurement rather than a fabricated one.
14 tests pass locally.
Failure-Class: FC-alert-never-provably-fired
Git-on-my-level
force-pushed
the
telemetry/app-startup-timing
branch
from
August 27, 2026 03:56
ec8f908 to
284fc20
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Defect
The 2026-08 macOS churn cohort analysis (
omi-knowledge-base/projects/macos-churn-analysis/evidence/2026-08-26-macos-churn-cohort-analysis.md) records under "Methodology guardrails":and under "Engineering leads": "no usable macOS startup-performance signal exists."
That is three separate defects, and the first one changes how the other two should be read.
1. The event does not reach PostHog at all
AnalyticsManager.trackStartupTimingwrites only a Sentry breadcrumb:PostHog,
$os = macOS, 2026-08-18 → 2026-08-25:App Startup TimingsamplesApp LaunchedEvery 0.12.x build emits zero. The cohort's 11 samples are not a sampling defect — they are the last stragglers of an event that was retired from product analytics. So "the event rarely fires" is not the finding; "the event was deliberately moved to Sentry and nobody noticed the fleet metric died" is.
2.
had_unclean_shutdownwas a race, not a crash rate.omi_runningis created at the end ofRewindDatabase.performInitialization(). Seventeen storage actors callRewindDatabase.shared.initialize()lazily, and any of them can beatViewModelContainer.loadAllData()to it — the code even documents this ("TierManager triggers init early"). Once the database is open,hadUncleanShutdown()reads this session's flag and reports every clean launch as a crash.3. TTI measured the wrong interval
timeToInteractivestarted atCFAbsoluteTimeGetCurrent()on the first line ofloadAllData(), which runs long aftermain(). 11–131ms is a correct measurement of the wrong thing.Fix
AnalyticsManager.trackStartupTimingemitsApp Startup Timingto PostHog again, once per process (loadAllDatare-runs on an owner switch, and that is not a launch). The Sentry breadcrumb stays, so the numbers remain attached to a same-session crash report — Sentry answers "what did this crash look like", PostHog answers "is startup getting slower across the fleet", and only the second one was lost.RewindDatabaselatches the unclean-shutdown verdict at the first observation in the process, andperformInitialization()latches it frompreviousCrashedbefore writing this session's flag. The verdict becomes a property of the process rather than of call order.close()clears the latch so a user retarget re-observes for the user it opens.AppStartupTiming.processStartDate()readskinfo_proc.kp_proc.p_starttime— the kernel's exec stamp, before dyld and beforemain.time_to_interactive_msis measured from there, and is omitted rather than faked when the lookup fails.data_load_ms.Event shape change
time_to_interactive_mskeeps its name and changes meaning. Historical values are not comparable — but there are 11 of them and they were wrong. Anything reading this property (theomi-posthog-macosskill documents it) should treat 0.12.x-and-later as a new series.This adds a per-launch PostHog event, which moves the legacy all-events DAU insight (already labelled
[Legacy] DAU — all events (instrumentation-sensitive)).Core Product DAU v1 — clean by platformis an explicit allowlist and is unaffected.Proof
xcrun swift test --filter "AppStartupTimingTests|RewindDatabaseLifecycleTests"— 14 tests pass locally.testUncleanShutdownVerdictSurvivesTheDatabaseOpeningFirst— opens the database first, asserts this session's flag exists (so the test cannot pass vacuously), then asserts the verdict is false. Mutation-checked: deleting the latch read makes this test fail with the exact production symptom.testPreviousSessionCrashIsStillReportedAfterTheDatabaseOpens— a stale flag from a previous session still reports unclean, so the latch cannot swallow a real crash.AppStartupTimingTests— the kernel stamp is read and precedes anyDate()our own code can take; conversion floors at zero across a wall-clock adjustment; a failed lookup yields no measurement rather than a fabricated one.Local
make preflightpasses. Full Swift contract CI (~42 min) runs after push; not claimed green here.Invariants
RewindDatabase.swiftis in the desktop session-truth glob). This PR adds a latched boolean about the previous process's shutdown; it does not touch session death,invalidateSession,signOut(), Firebase credentials, or owner identity.close()/configure()semantics are unchanged apart from clearing that boolean.Failure-Class: FC-alert-never-provably-fired
Line-Count-Exception: desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift | 3481 -> 3508 | +27 lines: one optional
Boolfield, a latch read inhadUncleanShutdown(), a latch write inperformInitialization(), a clear inclose(), and the comments that explain the race. Splitting a 3.5k-line database class is the right cleanup but is not this telemetry fix's job, and folding it in would bury a 4-line behavior change in a move diff.First commit is unrelated housekeeping needed to get any desktop PR past the local pre-push gate on Xcode 26.x (also in #12267 and #12269): a swift-format drift already on
mainand one implicitselfcapture that Swift 6.2 rejects and Xcode 16.4 accepts. It drops out on rebase once any of the three lands.