Follow-up to #304, which is closed because its symptom — Release builds deadlocking at launch — is fixed by #308. That fix removed the trigger (-force_load), not the defect, and it cost a feature.
What doesn't work
The MediaPipe graph paths are non-functional in Release builds. Without -force_load, plain archive linking dead-strips the calculator registrations, so the holistic landmarker — and therefore fingerspelling (Plan CK) — cannot run in a shipping build. FstRegisterer symbols in the app binary went 152 → 6 with the flag removed, which is exactly why it launches now and equally why the graph can't be constructed.
This loses nothing that previously worked: no Release build had ever launched on device, so these paths have never functioned in a shipping build. But it is a live gap between what the app advertises and what a TestFlight/App Store build can do.
CK P3's device smoke is blocked by the same thing. It needs -force_load in a device build to exercise the real landmarker — precisely the configuration that deadlocks.
Root cause: two abseil runtimes in one process
| binary |
absl symbols |
fst:: symbols |
libMediaPipeTasksCommon_device_graph.a (410 MB) |
~47,859 |
867 registrars |
MediaPipeTasksCommon.framework |
2,046 (incl. 26 absl::Mutex::lock) |
0 |
libsherpa-onnx.a |
0 |
0 |
Force-loading pulls all 867 OpenFst FstRegisterer static initialisers into the app binary alongside the archive's abseil, while the embedded framework carries its own. A registrar can then bind to a different abseil instance than the one whose state was initialised, and the registry's absl::Mutex never resolves — the main thread sleeps inside dyld4::Loader::findAndRunAllInitializers until iOS kills it at the 20-second launch watchdog (0x8BADF00D). Full traces and crash-log analysis in #304.
⚠️ sherpa-onnx is not involved. An earlier comment on #304 guessed it supplied a second OpenFst copy; measurement showed zero fst::/absl symbols. Don't start there.
Most tractable route
Replace blanket -force_load with targeted -u <symbol> forcing — name only the calculator registration symbols the holistic landmarker graph actually needs, so the linker pulls those objects instead of all 867 registrars. Steps:
- Determine the calculator list for MediaPipe's holistic landmarker graph (from the graph definition, or observed from a Debug-with-
force_load device run).
- Map each to its registration symbol via
nm over libMediaPipeTasksCommon_device_graph.a.
- Iterate: device build → install → launch and probe past 20s. The failure mode is a watchdog kill, not a build error, so a wrong symbol set looks like a hang rather than a compile failure — budget a full round trip per attempt.
- Validate the graph actually runs afterwards (CK P3 device smoke), since a launching app proves nothing about whether the calculators registered.
Rejected alternatives:
- Dedupe upstream — not possible; both abseil copies are inside Google's prebuilt artefacts.
- Defer the initialisers — not possible; statically-linked archive initialisers run at dyld time by definition. Deferral would require the archive to live in a
dlopened dylib, but the embedded framework references its symbols at launch.
Definition of done
- Release device build launches (survives >20s, no
0x8BADF00D)
- Holistic landmarker constructs and fingerspelling decodes on hardware in a Release build
project.base.yml carries the reasoning, so the next person doesn't reintroduce blanket -force_load
Follow-up to #304, which is closed because its symptom — Release builds deadlocking at launch — is fixed by #308. That fix removed the trigger (
-force_load), not the defect, and it cost a feature.What doesn't work
The MediaPipe graph paths are non-functional in Release builds. Without
-force_load, plain archive linking dead-strips the calculator registrations, so the holistic landmarker — and therefore fingerspelling (Plan CK) — cannot run in a shipping build.FstRegisterersymbols in the app binary went 152 → 6 with the flag removed, which is exactly why it launches now and equally why the graph can't be constructed.This loses nothing that previously worked: no Release build had ever launched on device, so these paths have never functioned in a shipping build. But it is a live gap between what the app advertises and what a TestFlight/App Store build can do.
CK P3's device smoke is blocked by the same thing. It needs
-force_loadin a device build to exercise the real landmarker — precisely the configuration that deadlocks.Root cause: two abseil runtimes in one process
fst::symbolslibMediaPipeTasksCommon_device_graph.a(410 MB)MediaPipeTasksCommon.frameworkabsl::Mutex::lock)libsherpa-onnx.aForce-loading pulls all 867 OpenFst
FstRegistererstatic initialisers into the app binary alongside the archive's abseil, while the embedded framework carries its own. A registrar can then bind to a different abseil instance than the one whose state was initialised, and the registry'sabsl::Mutexnever resolves — the main thread sleeps insidedyld4::Loader::findAndRunAllInitializersuntil iOS kills it at the 20-second launch watchdog (0x8BADF00D). Full traces and crash-log analysis in #304.fst::/abslsymbols. Don't start there.Most tractable route
Replace blanket
-force_loadwith targeted-u <symbol>forcing — name only the calculator registration symbols the holistic landmarker graph actually needs, so the linker pulls those objects instead of all 867 registrars. Steps:force_loaddevice run).nmoverlibMediaPipeTasksCommon_device_graph.a.Rejected alternatives:
dlopened dylib, but the embedded framework references its symbols at launch.Definition of done
0x8BADF00D)project.base.ymlcarries the reasoning, so the next person doesn't reintroduce blanket-force_load