Cold start on the target TV box reaches its first frame 21 ms after `dart_main`, so nothing Dart-side gates the splash. Everything below is on the path to the first *useful* frame, which is a strictly serial chain and where the viewer actually waits. - `monoTheme` is a pure function of two bools that builds a full `ColorScheme`, an applied-and-copied 15-style `TextTheme`, ~14 sub-themes and then clones the whole `ThemeData` again. It was rebuilt five times per cold start, two of them before `runApp`, and twice more per app-shell rebuild. It is memoized now, keyed by palette plus `TargetPlatform` -- the platform matters because `ThemeData()` derives tap target size, visual density and typography from `defaultTargetPlatform`, so a palette-only key would be wrong under a debug or test platform override. - `initializeDateFormatting` ignores its locale argument and builds CLDR symbols and patterns for all 121 locales synchronously. It blocked the gate ahead of the database open for data that only content screens use. - `DownloadStorageService.initialize` ended in a `path_provider` round trip plus mkdir at the tail of the gate, contradicting the comment above it that already explained offline artwork is not a launch requirement. - `recoverInterruptedDownloads()` and `TrackerCoordinator.initialize()` ran from `initState` of the widget whose first build produces the first app frame, and the RSS watchdog installed a periodic timer there whose first useful sample is 15 s away regardless. - `CredentialVault` decrypted every token with pure-Dart AES-GCM on the main isolate, uncached, on every registry read and on every Drift re-emit -- and the binder writes tokens during the startup sweep, so writes re-triggered reads. Decryption is memoized by ciphertext, with `invalidateCache()` wired into the preference-store repair path so a repaired install cannot serve stale plaintext. - `reloadFromStorage` now coalesces in-flight callers. The two serial awaits around the legacy migration are deliberately not merged; only genuinely concurrent callers share a snapshot. - `_sameConnections` ran two `jsonEncode` calls per connection on every Drift emit purely to compare, allocating two maps and two strings each time. - The splash rendered one `CircularProgressIndicator` per pending server on top of the aggregate one, so N+1 tickers scheduled a frame every vsync for the whole of `awaitBindingSettle` -- competing with the startup work they were reporting on. Measured on device in a settled dexopt state: time to `main_screen` 1495 -> 1400 ms, `credentials_loaded` 1238 -> 1128 ms, `database_ready` 501 -> 443 ms. First frame is unchanged at ~18 ms, as expected.
79 lines
3.7 KiB
Dart
79 lines
3.7 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/theme/mono_theme.dart';
|
|
|
|
/// `monoTheme` builds a full `ColorScheme`, an applied+copyWith'd Typography
|
|
/// `TextTheme`, ~14 sub-theme objects, and then clones the whole `ThemeData`
|
|
/// again via `copyWith(extensions:)`. It used to run five times per cold start —
|
|
/// two of them before `runApp`, on the critical path to first frame — and twice
|
|
/// more on every app-shell rebuild. It is memoized now, so these tests pin the
|
|
/// two properties that make that safe: stable identity per palette, and a cache
|
|
/// key that includes every input the builder actually reads.
|
|
void main() {
|
|
test('repeated calls return the identical instance per palette', () {
|
|
expect(identical(monoTheme(dark: false), monoTheme(dark: false)), isTrue);
|
|
expect(identical(monoTheme(dark: true), monoTheme(dark: true)), isTrue);
|
|
expect(identical(monoTheme(dark: true, oled: true), monoTheme(dark: true, oled: true)), isTrue);
|
|
});
|
|
|
|
test('the three palettes are distinct instances', () {
|
|
final light = monoTheme(dark: false);
|
|
final dark = monoTheme(dark: true);
|
|
final oled = monoTheme(dark: true, oled: true);
|
|
|
|
expect(identical(light, dark), isFalse);
|
|
expect(identical(dark, oled), isFalse);
|
|
expect(light.brightness, Brightness.light);
|
|
expect(dark.brightness, Brightness.dark);
|
|
// OLED is the pure-black variant, which is the whole reason it exists.
|
|
expect(oled.scaffoldBackgroundColor, const Color(0xFF000000));
|
|
expect(dark.scaffoldBackgroundColor, isNot(const Color(0xFF000000)));
|
|
});
|
|
|
|
test('oled normalizes dark, so it cannot produce a light OLED theme', () {
|
|
// ThemeProvider.darkThemeFor only ever asks for oled alongside dark, but the
|
|
// signature allows dark:false — collapsing it keeps the cache at three
|
|
// palette states instead of four, and a light OLED theme is meaningless.
|
|
expect(identical(monoTheme(dark: false, oled: true), monoTheme(dark: true, oled: true)), isTrue);
|
|
expect(monoTheme(dark: false, oled: true).brightness, Brightness.dark);
|
|
});
|
|
|
|
test('palette colours are unchanged by memoization', () {
|
|
// Guards against a cache key that accidentally collapses two palettes.
|
|
final light = monoTheme(dark: false);
|
|
final dark = monoTheme(dark: true);
|
|
|
|
expect(light.colorScheme.brightness, Brightness.light);
|
|
expect(dark.colorScheme.brightness, Brightness.dark);
|
|
expect(light.scaffoldBackgroundColor, isNot(dark.scaffoldBackgroundColor));
|
|
expect(light.colorScheme.onSurface, isNot(dark.colorScheme.onSurface));
|
|
});
|
|
|
|
group('target platform is part of the cache key', () {
|
|
tearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
test('each platform gets a theme carrying that platform', () {
|
|
// ThemeData() derives platform defaults (tap target size, visual density,
|
|
// typography) from defaultTargetPlatform. Keying only on the palette would
|
|
// hand an Android-derived theme to a test or debug run overriding to iOS.
|
|
for (final platform in [TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.macOS]) {
|
|
debugDefaultTargetPlatformOverride = platform;
|
|
expect(monoTheme(dark: true).platform, platform);
|
|
}
|
|
});
|
|
|
|
test('two platforms do not share an instance, and returning restores identity', () {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
|
final android = monoTheme(dark: true);
|
|
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
final ios = monoTheme(dark: true);
|
|
expect(identical(android, ios), isFalse);
|
|
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
|
expect(identical(monoTheme(dark: true), android), isTrue);
|
|
});
|
|
});
|
|
}
|