Skip to content

fix: stop the app freezing during backfills, add real crash/jank visibility#85

Merged
abdulsaheel merged 2 commits into
mainfrom
perf-fixes
Jul 16, 2026
Merged

fix: stop the app freezing during backfills, add real crash/jank visibility#85
abdulsaheel merged 2 commits into
mainfrom
perf-fixes

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces a comprehensive telemetry and observability upgrade throughout the app, with a focus on improved error reporting, app state context, and performance tracing. It adds a new TelemetryService API with breadcrumbs, context, non-fatal error reporting, and custom trace support, and wires it into key flows such as navigation, derivation, and UI jank detection. Several main-isolate performance bottlenecks are also addressed by offloading heavy JSON encoding/decoding to isolates. Additionally, minor state management improvements and bug fixes are included.

Telemetry and Observability Improvements:

  • Introduced a new TelemetryService API providing methods for logging breadcrumbs, setting persistent context, recording non-fatal errors, and wrapping code in Firebase Performance traces. Also adds a frame jank watchdog to report UI freezes as non-fatal Crashlytics events. (lib/telemetry/telemetry_service.dart) [1] [2]
  • Wired TelemetryService into the app's navigation via a new TelemetryNavigatorObserver (wired into MaterialApp.navigatorObservers), and into top-level route changes in _Gate, so every screen transition is logged and visible in Crashlytics context. (lib/app.dart, lib/telemetry/telemetry_service.dart) [1] [2] [3] [4]
  • The derivation pipeline now logs context and breadcrumbs for each derive pass, and reports caught errors as non-fatal Crashlytics events. It also annotates Crashlytics with the current derive mode and activity status. (lib/state/app_state.dart) [1] [2] [3]
  • Readiness-absent diagnostics from the derivation engine are now logged with detailed context and throttled to once per calendar day, improving the ability to debug why readiness is missing. (lib/compute/derivation_engine.dart, lib/compute/onehz_pipeline.dart) [1] [2] [3]

Performance and Main-Isolate Responsiveness:

  • Offloaded heavy JSON encoding/decoding work in cross-day derivation to isolates, addressing a production jank issue where the main isolate would freeze for several seconds during large derivation passes. (lib/compute/derivation_engine.dart) [1] [2] [3]

Minor Improvements and Bug Fixes:

  • Switched a context.watch to context.select in the live session screen to avoid unnecessary widget rebuilds. (lib/ui/activity/live_session_screen.dart)

These changes significantly improve the app's ability to surface, diagnose, and resolve issues in production by providing much richer telemetry and by reducing the risk of main-isolate freezes during heavy computation.

Summary by CodeRabbit

  • New Features

    • Improved app navigation tracking for more accurate screen activity visibility.
    • Added diagnostics for missing readiness data to support clearer health insights.
    • Added monitoring for slow frames and non-fatal app errors.
  • Bug Fixes

    • Prevented invalid map camera calculations during early screen layout.
    • Reduced unnecessary screen refreshes, improving responsiveness across activity, profile, wellness, and workout views.
    • Improved background data processing efficiency.

…bility

Two separate problems, found by actually wiring the app up to Firebase.

The freeze/hang: derivation_engine.dart's cross-day rollup reads up to 90
days of stored results, decodes each one, rebuilds a cross-day record for
it, and re-encodes the result — none of that ran off the main thread. On
an older phone this blocked the UI for several seconds during a heavy
derive pass. Moved the decode/transform/encode work into Isolate.run (the
two helper functions it uses were already static, so this was a clean
move) and did the same for the crossday bundle's own jsonEncode, which
was happening back on the main isolate after the isolate call returned.

The rebuild storm: several screens (Today, Steps, Spot Check, Profile's
device sheet, breathing, the status banner, coach cards) used a blanket
context.watch<AppState>() and rebuilt on all 67 of AppState's
notifyListeners() calls, including three separate per-second timers.
Switched these to context.select() scoped to the fields each screen
actually reads. Reverted the same change on the main Profile screen after
it broke the telemetry toggle — that screen touches too many fields
across too many helper methods to track by hand safely, and it isn't one
of the screens background timers actually hit, so watch() stays there.

Also fixed a live-session map crash: fitCamera() could get called before
the map had a real rendered size, and flutter_map's zoom math divides by
that size — a zero-size viewport can produce a NaN/Infinite zoom that
doesn't throw until the tile layer tries to use it a frame later. Added
a guard on the viewport size before calling fitCamera.

Added actual observability: Crashlytics breadcrumbs on navigation and
derive passes, custom keys for screen/derive state, a frame-timing
watchdog that reports slow frames as non-fatal issues (this is what
caught the hang above), and a readiness diagnostic that records which
of HRV/RHR/resp/temp were missing when the readiness score comes back
empty.

flutter analyze: clean. flutter test: 457/457.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3282ef69-61e1-492d-b9de-df077ece7031

📥 Commits

Reviewing files that changed from the base of the PR and between ac57620 and fac5117.

📒 Files selected for processing (16)
  • lib/app.dart
  • lib/compute/derivation_engine.dart
  • lib/theme/theme_switcher.dart
  • lib/ui/activity/live_session_screen.dart
  • lib/ui/ai/ai_breakdown_screen.dart
  • lib/ui/ai/ai_settings_screen.dart
  • lib/ui/coach/ai_coach_screen.dart
  • lib/ui/journal/journal_compose_screen.dart
  • lib/ui/journal/journal_screen.dart
  • lib/ui/onboarding/welcome_screen.dart
  • lib/ui/profile/notification_relay_section.dart
  • lib/ui/profile/profile_screen.dart
  • lib/ui/screens/trend_screen.dart
  • lib/ui/sleep/sleep_detail_screen.dart
  • lib/ui/today/today_screen.dart
  • lib/ui/workouts/workouts_screen.dart
🚧 Files skipped from review as they are similar to previous changes (4)
  • lib/ui/today/today_screen.dart
  • lib/app.dart
  • lib/ui/activity/live_session_screen.dart
  • lib/ui/profile/profile_screen.dart

📝 Walkthrough

Walkthrough

The PR adds consent-gated telemetry for navigation, jank, derivation timing, and failures; surfaces readiness-absence diagnostics; moves cross-day JSON processing into isolates; names routes; and narrows selected Flutter AppState rebuild subscriptions.

Changes

Observability and performance updates

Layer / File(s) Summary
Telemetry service and app instrumentation
lib/telemetry/telemetry_service.dart, lib/app.dart, lib/main.dart
TelemetryService now supports breadcrumbs, context keys, non-fatal errors, traces, jank monitoring, and Navigator observation. App startup and the app shell install and use these capabilities.
Named route metadata and navigation wiring
lib/theme/theme_switcher.dart, lib/app.dart, lib/ui/**
themedRoute forwards optional names through RouteSettings, and navigation sites provide explicit or runtime-derived route names.
Derivation diagnostics and isolate processing
lib/compute/onehz_pipeline.dart, lib/compute/derivation_engine.dart
Readiness absence data is added to derived bundles and throttled telemetry logging is added for today. Cross-day JSON encoding and artifact construction run inside isolates.
Derivation lifecycle reporting
lib/state/app_state.dart
Derivation passes record mode, active state, breadcrumbs, traces, and non-fatal failures, with active state cleared in finally.
UI state subscription and layout guards
lib/ui/insights/coach_cards.dart, lib/ui/profile/*, lib/ui/screens/*, lib/ui/spotcheck/*, lib/ui/stress/*, lib/ui/today/*, lib/ui/widgets/*, lib/ui/activity/live_session_screen.dart
Selected widgets subscribe only to the AppState fields they render, while live map camera fitting skips zero-sized viewports.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppShell
  participant MaterialApp
  participant Navigator
  participant TelemetryNavigatorObserver
  participant TelemetryService
  participant Crashlytics
  AppShell->>TelemetryService: install jank watchdog
  AppShell->>MaterialApp: register navigator observer
  Navigator->>TelemetryNavigatorObserver: navigation lifecycle event
  TelemetryNavigatorObserver->>TelemetryService: set screen context and breadcrumb
  TelemetryService->>Crashlytics: record consent-gated telemetry
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main changes: reducing backfill freezes and improving crash/jank telemetry visibility.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf-fixes

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/compute/derivation_engine.dart (1)

1368-1386: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nested context map will render as an unqueryable string in Firebase Analytics.

record(kind: 'event', ..., context: (absentDiag as Map).cast<String, dynamic>()) passes a map whose values (hrv, rhr, resp, temp) are themselves maps. TelemetryService.record()'s Analytics branch only special-cases num/String and otherwise falls back to v.toString(), so each of those keys will show up in Firebase Analytics as a raw {value: true, baseline_n: 5}-style string instead of filterable fields. Breadcrumb/Crashlytics logging is unaffected (plain string), but the Analytics event loses most of its diagnostic value.

Consider flattening before calling record(), e.g. 'hrv_value': ..., 'hrv_baseline_n': ... per input, so each field is independently filterable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/compute/derivation_engine.dart` around lines 1368 - 1386, Flatten
absentDiag’s nested per-input diagnostic maps before passing context to
TelemetryService.record in the readiness-absent block, using independently named
fields such as each input’s value and baseline count. Preserve the existing
breadcrumb message, event metadata, and once-per-day throttling while ensuring
context contains only Analytics-supported scalar values.
lib/telemetry/telemetry_service.dart (1)

364-394: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

current_screen will often just report the route type, not the screen.

_nameOf falls back to route.runtimeType.toString() when settings.name is unset. Several screens in this codebase push with plain MaterialPageRoute(builder: ...)/themedRoute(...) (no RouteSettings.name), so current_screen on a crash/ANR report will read something generic like MaterialPageRoute<dynamic> for most navigations instead of the actual destination — undercutting the diagnostic value this observer is meant to add.

Consider passing RouteSettings(name: ...) at the more common push sites (or wrapping themedRoute/similar helpers to auto-derive a name) so current_screen is actually screen-specific.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/telemetry/telemetry_service.dart` around lines 364 - 394, Ensure routes
passed through TelemetryNavigatorObserver carry screen-specific
RouteSettings.name values instead of relying on the generic runtime-type
fallback. Update the common MaterialPageRoute/themedRoute navigation helpers or
their call sites to provide meaningful destination names, while preserving
existing named-route behavior in _nameOf and the observer callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/compute/derivation_engine.dart`:
- Around line 1368-1386: Flatten absentDiag’s nested per-input diagnostic maps
before passing context to TelemetryService.record in the readiness-absent block,
using independently named fields such as each input’s value and baseline count.
Preserve the existing breadcrumb message, event metadata, and once-per-day
throttling while ensuring context contains only Analytics-supported scalar
values.

In `@lib/telemetry/telemetry_service.dart`:
- Around line 364-394: Ensure routes passed through TelemetryNavigatorObserver
carry screen-specific RouteSettings.name values instead of relying on the
generic runtime-type fallback. Update the common MaterialPageRoute/themedRoute
navigation helpers or their call sites to provide meaningful destination names,
while preserving existing named-route behavior in _nameOf and the observer
callbacks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d094b783-42c3-4b45-83f2-8339a565940d

📥 Commits

Reviewing files that changed from the base of the PR and between ad7fc30 and ac57620.

📒 Files selected for processing (15)
  • lib/app.dart
  • lib/compute/derivation_engine.dart
  • lib/compute/onehz_pipeline.dart
  • lib/main.dart
  • lib/state/app_state.dart
  • lib/telemetry/telemetry_service.dart
  • lib/ui/activity/live_session_screen.dart
  • lib/ui/insights/coach_cards.dart
  • lib/ui/profile/advanced_data_screen.dart
  • lib/ui/profile/profile_screen.dart
  • lib/ui/screens/screens.dart
  • lib/ui/spotcheck/spot_check_screen.dart
  • lib/ui/stress/calm_breathing_screen.dart
  • lib/ui/today/today_screen.dart
  • lib/ui/widgets/status_banner.dart

Two things it flagged, both real:

The readiness-absent diagnostic was passing a nested map straight into
TelemetryService.record(), which only keeps num/String context values
as-is and stringifies everything else. So each of HRV/RHR/resp/temp
would've shown up in Firebase Analytics as one unqueryable
"{value: true, baseline_n: 5}"-style string instead of separate
filterable fields. Flattened it into hrv_value/hrv_baseline_n/etc.

current_screen (the custom key TelemetryNavigatorObserver sets on push)
falls back to the route's runtime type when there's no RouteSettings
name, and nothing in this codebase was naming its routes — so in
practice every crash/ANR report would just say something like
"MaterialPageRoute<void>" instead of which screen it actually happened
on. Added an optional name parameter to themedRoute() and passed a real
name at all 24 push sites across the app. Today screen's shared _push()
helper (used by 15 of those) now reads the destination widget's runtime
type once before pushing, so its many callers didn't need touching
individually.

flutter analyze: clean. flutter test: 457/457.
@OpenStrap OpenStrap deleted a comment from coderabbitai Bot Jul 16, 2026
@OpenStrap OpenStrap deleted a comment from coderabbitai Bot Jul 16, 2026
@abdulsaheel
abdulsaheel merged commit 7e42f3e into main Jul 16, 2026
1 check passed
@abdulsaheel
abdulsaheel deleted the perf-fixes branch July 16, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant