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
18 changes: 18 additions & 0 deletions ios/ExportOptions/AppStoreConnect.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>2U62X3RF3R</string>
<key>signingStyle</key>
<string>automatic</string>
<key>destination</key>
<string>upload</string>
<key>uploadSymbols</key>
<true/>
<key>stripSwiftSymbols</key>
<true/>
</dict>
</plist>
3 changes: 2 additions & 1 deletion lib/compute/background_derivation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ void derivationDispatcher() {
} else if (task == kHeavyDeriveTaskName) {
debugPrint('[bg-derive] triggered by WorkManager');
final profile = await _loadProfile();
final engine = DerivationEngine(log: (m) => debugPrint('[bg-derive] $m'));
final engine = DerivationEngine(
log: (m) => debugPrint('[bg-derive] $m'), background: true);
await engine.run(profile, heavy: true);
// Baseline-dirty rescan on the scheduled tick: refresh baseline-dependent
// scalars on recent finalized days when the rolling baseline has moved.
Expand Down
30 changes: 19 additions & 11 deletions lib/compute/derivation_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import '../notify/notification_event.dart';
import '../notify/tap_router.dart' show kRouteWorkoutSuggestion;
import '../telemetry/telemetry_service.dart';
import 'crossday_pipeline.dart';
import 'derive_pacing.dart';
import 'derive_prepare.dart';
import 'onehz_pipeline.dart';
import 'profile.dart';
Expand Down Expand Up @@ -655,9 +656,17 @@ Future<void> runWithConcurrency<T>(
}

class DerivationEngine {
DerivationEngine({this.log});
DerivationEngine({this.log, this.background = false});
final void Function(String)? log;

/// True when this engine was constructed inside a headless/background entry
/// (iOS BGProcessingTask / BGAppRefreshTask, Android WorkManager, the
/// post-drain background sync pass). The OS throttles CPU hard in those
/// contexts, which changes two tuning decisions — see [_deriveConcurrency]
/// and [_perDayTimeout]. Set at construction, not per-run, so a long-lived
/// foreground engine can never inherit background tuning by accident.
final bool background;

bool _running = false;
bool get running => _running;
final Map<String, dynamic> _diag = {
Expand Down Expand Up @@ -1493,9 +1502,13 @@ class DerivationEngine {
'v$kAlgoVersion|na';
}

/// Foreground vs background pacing — lane count and per-day wall-clock
/// budget. See [DerivePacing] for why the background numbers differ.
DerivePacing get _pacing => DerivePacing(background: background);

/// Max wall-clock for ONE day's off-isolate compute. On timeout the day is
/// skipped so the sweep always makes progress.
static const Duration _perDayTimeout = Duration(seconds: 90);
Duration get _perDayTimeout => _pacing.perDayTimeout;

/// Throttle for the readiness-absent diagnostic log — one per calendar day
/// so repeated light-pass re-derives of today don't spam the outbox.
Expand All @@ -1510,17 +1523,12 @@ class DerivationEngine {
/// substrate loads + compute-isolate all finishing before the next day even
/// started), which wastes every core beyond the one doing the current day's
/// work. Running several days' isolate work genuinely concurrently gets
/// real wall-clock speedup from the device's other cores. Capped
/// conservatively — this is a phone doing background/foreground compute,
/// not a server batch job — rather than using every available core.
static const int _maxDeriveConcurrency = 3;

/// real wall-clock speedup from the device's other cores — in the FOREGROUND.
/// A headless background slot has no spare cores to soak up, so it takes one
/// lane; [DerivePacing] owns that decision and explains it.
int get _deriveConcurrency {
try {
return math.max(
1,
math.min(_maxDeriveConcurrency, Platform.numberOfProcessors),
);
return _pacing.concurrency(Platform.numberOfProcessors);
} catch (_) {
return 1; // Platform unavailable on this target — sequential fallback
}
Expand Down
56 changes: 56 additions & 0 deletions lib/compute/derive_pacing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// derive_pacing.dart — how hard to push per-day derivation, and how long to
// wait for it, depending on whether we are in the foreground or in a headless
// OS-granted background slot.
//
// WHY THIS EXISTS (production, Crashlytics 0.9.20 / iOS 27):
// `_runDayBlocksCancellable` was reporting `day_blocks_failed` —
// "TimeoutException: day-blocks computation timed out after 0:01:30" — from
// inside `IosBgTask._run`, i.e. only ever in BACKGROUND. Two foreground
// assumptions were being applied to a context that breaks both:
//
// 1. CONCURRENCY. Running 3 day-lanes concurrently is a win when there are
// spare cores. A background task does not get spare cores — it gets a
// throttled slice of CPU. Three lanes therefore do not go faster; they
// divide one budget three ways and make each day ~3x slower in wall-clock.
// Paired with a wall-clock timeout, that converts "3 days derived" into
// "3 days timed out". Serial lanes also cap peak memory at one day's
// substrate instead of three.
//
// 2. TIMEOUT. The 90 s guard exists to survive a HUNG day, but it is measured
// in wall clock, and wall clock stops tracking work once the OS throttles
// us. A day that computes in 20 s foreground can legitimately need several
// times that in a BGProcessingTask on a busy or thermally-limited device.
//
// Kept pure and separate so the tuning is unit-testable without a database, an
// isolate, or a real background slot.

/// Pacing decisions for one derivation run.
class DerivePacing {
const DerivePacing({required this.background});

/// True for headless entries: iOS BGProcessingTask / BGAppRefreshTask,
/// Android WorkManager, and the derive pass that follows a background drain.
final bool background;

/// Upper bound on foreground day-lanes. Deliberately conservative — this is
/// a phone doing work alongside the UI, not a server batch job.
static const int maxForegroundConcurrency = 3;

static const Duration foregroundPerDayTimeout = Duration(seconds: 90);
static const Duration backgroundPerDayTimeout = Duration(minutes: 4);

/// Worker-pool size. [cores] is the device's processor count; pass whatever
/// `Platform.numberOfProcessors` reported (callers that cannot read it should
/// pass 1 and get the sequential fallback).
int concurrency(int cores) {
if (background) return 1;
if (cores < 1) return 1;
return cores < maxForegroundConcurrency ? cores : maxForegroundConcurrency;
}

/// Max wall-clock for ONE day's off-isolate compute. On timeout the day is
/// skipped so the sweep always makes progress; the headline result still
/// persists (partial) and stays un-finalized for a later retry.
Duration get perDayTimeout =>
background ? backgroundPerDayTimeout : foregroundPerDayTimeout;
}
Loading
Loading