Skip to content

fix(fgs): migrate background engine to sibling-isolate spawn#1192

Draft
SERDUN wants to merge 9 commits into
developfrom
fix/fgs-flutter-engine-group-v2
Draft

fix(fgs): migrate background engine to sibling-isolate spawn#1192
SERDUN wants to merge 9 commits into
developfrom
fix/fgs-flutter-engine-group-v2

Conversation

@SERDUN

@SERDUN SERDUN commented Apr 21, 2026

Copy link
Copy Markdown
Member

Re-opening #1191 as draft for further investigation and testing.

Original fix: spawn background engine as sibling isolate from main engine to avoid root-isolate conflict in AOT mode.

SERDUN added 9 commits April 21, 2026 20:00
…rtup failure (WT-1373)

Replaces standalone FlutterEngine + executeDartCallback with
FlutterEngineGroup.makeEngine to fix a silent Dart isolate startup
failure on Samsung Android 13 (and similar devices).

Root cause: when the main FlutterEngine is already running in the same
process, calling executeDartCallback on an independent FlutterEngine
silently fails to start the Dart isolate at the JNI level. No exception
is thrown, but signalingServiceCallbackDispatcher never executes — confirmed
by a debugPrint canary that never appears in logcat across 8+ restart cycles.

FlutterEngineGroup.makeEngine uses FlutterJNI.spawn() for all engines after
the first, which creates a proper child isolate sharing the process-wide Dart
VM. This avoids the root-isolate conflict that causes the silent failure.

The custom createEngine override preserves automaticallyRegisterPlugins=false
so audio and hardware plugins cannot block the Dart VM startup on Android 16+
REMOTE_MESSAGING services (regression guard for the fix in commit 455f170).

The engineGroup singleton is double-checked-locked and process-scoped so the
shared Dart VM is initialised at most once per process lifetime.
FlutterEngineGroup.createAndRunEngine calls executeDartEntrypoint which
requires the 3-arg DartEntrypoint(bundle, libraryUri, functionName) form.
Without the library URI the AOT linker cannot resolve the entry point by
name alone and logs "Could not resolve main entrypoint function".

Passes callbackInformation.callbackLibraryPath as the second argument so
the runtime can locate signalingServiceCallbackDispatcher in the snapshot.
…ate conflict

When the main FlutterEngine is running, FlutterEngineGroup.createAndRunEngine
for the first engine in a fresh group falls back to executeDartEntrypoint.
In AOT mode this calls Dart_LookupLibrary("package:...") on a newly created
isolate group where package URIs are not registered, resulting in:
  "Could not resolve main entrypoint function"

The spawn path (FlutterJNI.spawn) creates a sibling isolate inside the
existing Dart VM where the library table is already populated — lookup succeeds.

WebtritSignalingServicePlugin now stores binding.flutterEngine in a static
field. FlutterEngineHelper reads it and:
  - if main engine is active → calls FlutterEngine.spawn() via reflection
    (spawn() is package-private, not accessible outside io.flutter.embedding.engine)
  - if no main engine (push cold-start) → FlutterEngineGroup fallback
    (no existing root isolate, so executeDartEntrypoint works correctly)
… into FlutterEngineHelper

Removes the implicit static dependency on WebtritSignalingServicePlugin.mainFlutterEngine
from FlutterEngineHelper. The coupling was hidden — FlutterEngineHelper silently read a
field on an unrelated plugin class with no visible dependency in its API.

- Introduce FlutterEngineHolder (single-responsibility object) to hold a reference to
  whichever running FlutterEngine most recently attached WebtritSignalingServicePlugin
- Rename the field to runningEngine to reflect the actual semantics: any engine with an
  active Dart VM qualifies (main UI engine, push-handler engine, etc.), not just "main"
- FlutterEngineHelper constructor now receives mainEngineProvider: () -> FlutterEngine?
  making the dependency explicit and the class independently testable
- SignalingForegroundService passes { FlutterEngineHolder.runningEngine } at construction
… recovery

Startup watchdog: 30s → 10s
With the spawn-from-main-engine path the Dart isolate starts in 1–3s;
10s is sufficient margin for loaded devices while cutting worst-case
user-visible recovery time roughly in half.

Hub no-port timeout: 15s → 8s
Hub port appears after the background isolate registers it (~2–3s).
8s gives enough margin while allowing faster detection when the FGS
fails silently (e.g. during a cold push-start with no running engine).
…ation

- Wrap spawnFromEngine() in try-catch(ReflectiveOperationException): if the
  Flutter embedding changes spawn()'s signature, fall back to FlutterEngineGroup
  instead of leaving backgroundEngine null and entering a WorkManager restart loop
- Use 'as? FlutterEngine ?: throw IllegalStateException' instead of hard cast so
  a null return from Method.invoke() produces a clear error rather than a
  misleading NPE
- Pin spawn() reflection to flutter_embedding 3.32.4 with a re-verify comment
- Clarify PlatformViewsController() comment: instance is intentionally headless
- Document FlutterEngineHolder thread-safety assumption (main-thread reads/writes)
- Document engineGroup singleton lifetime and why reuse across restarts is correct
- Broaden inner catch from ReflectiveOperationException to Exception so
  that IllegalStateException from spawnFromEngine (spawn() returned null)
  also triggers the FlutterEngineGroup fallback instead of bypassing it
- Assign backgroundEngine after attachToService() succeeds rather than
  inside .also{} so a throw from attachToService cannot leave the engine
  running but unreferenced (resource leak)
- Add comment to engineGroup singleton noting that bad internal state
  requires a process restart to recover
@SERDUN SERDUN changed the title fix(fgs): fix Dart isolate startup failure (v2) fix(fgs): migrate background engine to sibling-isolate spawn Apr 21, 2026
@SERDUN SERDUN added the enhancement New feature or request label Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant