Deep startup profiling for OneKey mobile — per-module JS factory timings, HBC I/O/parse breakdown, per-segment timings. Gated behind the single env var `ONEKEY_STARTUP_PROFILE=1`. Zero overhead when disabled (default). Use when diagnosing cold-start regressions, sizing main.bundle parse cost, finding slow modules, measuring segment load, or auditing what a 1.7s `require('./App')` actually contains. Triggers on: startup profile, 启动性能分析, module timing, HBC parse time, segment timing, require cost, StartupProfile, ONEKEY_STARTUP_PROFILE, __d patcher, inline-requires.
77
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Single env-var flag ONEKEY_STARTUP_PROFILE=1 turns on three layers of
measurement at once. When OFF (the default), every measurement path is a
single boolean check — zero observable overhead in production.
| Layer | Log tag | Content | Source |
|---|---|---|---|
| 1. JS modules | [StartupProfile.js] | Top 200 modules by self-time (total minus time spent in nested requires) + moduleId → path | apps/mobile/src/startupProfile/index.ts |
| 2. HBC files | [StartupProfile.hbc] | Main bundle I/O ms + size in bytes; parse+eval is derivable from existing ios.main_entry.evaluated / RN timing | iOS AppDelegate.swift, Android MainApplication.java |
| 3. Segments | [StartupProfile.seg] | Per-.seg.hbc total duration + id + key + relative path | apps/mobile/src/splitBundle/installProdBundleLoader.ts |
Export before any of the build commands:
export ONEKEY_STARTUP_PROFILE=1
# JS bundle: Metro plugin picks it up and prepends
# globalThis.__ONEKEY_STARTUP_PROFILE__ = true
# globalThis.__ONEKEY_MODULE_ID_TO_PATH__ = {...}
# to the main entry bundle.
yarn app:android # or `yarn app:ios` / EAS build command
# iOS native: read from Info.plist key or Xcode scheme env
# Android native: read from BuildConfig.ONEKEY_STARTUP_PROFILE (auto-set by
# build.gradle from the same env var)For EAS builds, set the env var in eas.json for the build profile you want to profile:
{
"build": {
"profile-startup": {
"extends": "base",
"env": {
"ONEKEY_STARTUP_PROFILE": "1"
}
}
}
}Then eas build --profile profile-startup --platform android.
Xcode → Product → Scheme → Edit Scheme → Run → Arguments → Environment Variables:
ONEKEY_STARTUP_PROFILE = 1This is picked up by ProcessInfo.processInfo.environment at runtime — works only for debug iOS running from Xcode.
In Info.plist add (or let an xcconfig generate at build time):
<key>ONEKEY_STARTUP_PROFILE</key>
<true/>Or an xcconfig that reads the env var:
ONEKEY_STARTUP_PROFILE = YESShip a profile-enabled TestFlight build to inspect real-device release timings.
Cold-start sequence (Android, ~2.5s to TTI):
[StartupTiming] android.app.on_create.start: +0ms from launch (anchor)
...existing native instrumentation...
[StartupProfile.hbc] android.index.android.bundle: io=35ms size=18900000B (prewarm, at +180ms from launch)
[StartupTiming] main_host.did_start: +318ms from launch (android)
...
[StartupTiming] MMKV contextAtom snapshot pre-read: 10 keys (+121ms)
[StartupProfile.seg] 22ms id=3288 key=seg:nm.@formatjs path=segments/nm._formatjs.seg.hbc
[StartupProfile.seg] 12ms id=3311 key=seg:shared.locale.json.zh_CN.json path=...
[StartupProfile.seg] 230ms id=3021 key=seg:kit.provider.Container.KeylessWalletContainer.KeylessWalletContainer path=...
...
[StartupTiming] main entry evaluated (+1780ms)
[StartupProfile.js] summary: tracked=187 modules timed (>=1ms), sum_self=1524ms, sum_inclusive=3847ms, flushing top 200 by self-time
[StartupProfile.js] self=142ms total=189ms packages/kit/src/provider/Container/PrimeLoginContainer/PrimeLoginContainer.tsx
[StartupProfile.js] self=87ms total=312ms packages/kit/src/states/jotai/contexts/tokenList/atoms.ts
[StartupProfile.js] self=64ms total=64ms node_modules/@walletconnect/core/dist/...
...etc up to 200 modules[StartupProfile.js] — module-levelself = time inside this module's own factory body excluding any nested require() calls. This is the actionable signal — a module with self=100ms actually costs 100ms of its own code.total = inclusive time — includes time spent in everything this module requires. Useful for identifying "roots" of slow subtrees (e.g. ./App might have self=5ms total=1600ms).__r (require), NOT __d (define). Reason: by the time our JS code runs, __d has already registered all factories with the unwrapped version — too late to wrap. __r is what runs the factory lazily, so wrapping it captures every first-time module load regardless of patch timing.[StartupProfile.hbc] — main bundle file I/Oio is pure OS-level read time (open → EOF).parse+exec = <native ios.main_entry.evaluated> - io.Application.onCreate.[StartupProfile.seg] — segment load@onekeyfe/react-native-split-bundle-loader; for now the aggregate is enough to identify the slowest segments.Grep the startup log after enabling:
grep 'StartupProfile' app-latest.log | head -5Expected: at least one [StartupProfile.js], [StartupProfile.hbc], [StartupProfile.seg] line. If only some show up:
| Missing | Likely cause | Fix |
|---|---|---|
[StartupProfile.js] | Metro prologue not injected → globalThis.__ONEKEY_STARTUP_PROFILE__ is undefined | Confirm ONEKEY_STARTUP_PROFILE=1 was set in the env when Metro built the bundle. Check apps/mobile/plugins/index.js buildStartupProfilePrologue() |
[StartupProfile.hbc] | Native flag not read: Android BuildConfig.ONEKEY_STARTUP_PROFILE is false, or iOS Info.plist key missing | Android: ensure env var was set at ./gradlew assembleRelease time (not just at Metro time). iOS: check Info.plist or xcconfig |
[StartupProfile.seg] | The JS-side flag was never set (same as first row) — segment logs reuse the JS flag | Same fix as first row |
Each layer's code path when ONEKEY_STARTUP_PROFILE is not set:
isStartupProfileEnabled() reads globalThis.__ONEKEY_STARTUP_PROFILE__ (undefined → false), function returns, no patching, no allocation.isStartupProfileEnabled() checks ProcessInfo env and Info.plist, returns false, skips the Data(contentsOf:) probe.if (BuildConfig.ONEKEY_STARTUP_PROFILE) — dead-code eliminated at compile time when false.if ((globalThis as any).__ONEKEY_STARTUP_PROFILE__ === true) — one identity check per segment load, sub-µs.Production release bundle overhead when flag is off: ≈ 0. No removal needed, instrumentation can live in the main branch permanently.
| File | Purpose |
|---|---|
apps/mobile/src/startupProfile/index.ts | JS __r patcher + flush |
apps/mobile/index.ts | Two-line hook to install & flush |
apps/mobile/plugins/index.js | Metro prologue injection (__ONEKEY_STARTUP_PROFILE__ + moduleId→path map) |
apps/mobile/src/splitBundle/installProdBundleLoader.ts | Per-segment log |
apps/mobile/ios/AppDelegate.swift | iOS HBC I/O probe + flag reader |
apps/mobile/android/app/build.gradle | Expose env var → BuildConfig.ONEKEY_STARTUP_PROFILE |
apps/mobile/android/app/src/main/java/.../MainApplication.java | Android HBC I/O probe |
LOG=path/to/app-latest.log
# JS modules sorted by self-time (what you really want to know)
grep '\[StartupProfile\.js\] self=' "$LOG" | head -30
# Total JS cost of top 20 modules
grep '\[StartupProfile\.js\] self=' "$LOG" \
| head -20 | awk '{sum+=$3} END {print sum}' \
| sed 's/self=//'
# Segment slowest 10
grep '\[StartupProfile\.seg\]' "$LOG" | sort -k2 -n -r | head -10
# HBC I/O vs (ios.main_entry.evaluated - io)
grep 'StartupProfile\.hbc\|ios.main_entry.evaluated\|android.index.android.bundle' "$LOG"inline-requires transform moves top-level const X = require(...) to the first use-site. This is why we patch __r rather than __d. But module IDs are the same — patching is safe.sum_inclusive will be much larger than sum_self because every nested require is counted in each parent. Don't add them naively to reason about total time.@onekeyfe/react-native-split-bundle-loader. Not done yet. The combined number is usually enough for triage.d71e6b7
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.