feat(native-export): scaffold local Expo module + native decode/encode bench probe (P0)

Local Kotlin module modules/recipescam-export (autolinked via expo ./modules)
exposes decodeEncodeAsync — BitmapFactory decode -> JPEG q95 encode -> file,
all on Dispatchers.IO off the JS/main threads. EXPO_PUBLIC_BENCH=1 builds run
src/dev/nativeBenchProbe.ts: 3 timed decode+encode passes on the bundled
wallframe artwork plus a JS-timer gap check proving the JS thread never
blocks during native rendering. Legacy Skia engine untouched (reference).
This commit is contained in:
2026-09-09 12:39:35 +07:00
parent f588849ac5
commit a8932f52ed
8 changed files with 157 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system/legacy';
import RecipescamExport from '../../modules/recipescam-export';
const RUNS = 3;
/**
* P0 spike harness (temporary — removed at P7). Runs native decode+encode on
* the bundled wallframe artwork (3117x4000, representative of a 12MP export)
* and logs per-stage ms plus a JS-timer gap check proving the JS thread is
* not blocked while native work runs. Gated by EXPO_PUBLIC_BENCH=1.
*/
export async function nativeBenchProbe(): Promise<void> {
console.log('[BENCH] start');
try {
const asset = Asset.fromModule(require('../../wallframe.png'));
await asset.downloadAsync();
if (!asset.localUri) {
console.error('[BENCH] wallframe asset has no localUri');
return;
}
const src = asset.localUri.replace(/^file:\/\//, '');
console.log('[BENCH] src=' + src);
for (let i = 0; i < RUNS; i++) {
const dst = `${FileSystem.cacheDirectory}bench-dst-${Date.now()}-${i}.jpg`;
const gaps: number[] = [];
let last = Date.now();
// 5ms JS timer while native work runs: a blocked JS thread would stretch gaps.
const iv = setInterval(() => {
const now = Date.now();
gaps.push(now - last);
last = now;
}, 5);
const t0 = Date.now();
const timings = await RecipescamExport.decodeEncodeAsync(src, dst, 95);
const jsElapsed = Date.now() - t0;
clearInterval(iv);
const sorted = [...gaps].sort((a, b) => b - a);
console.log(
'[BENCH] run=' + i +
' jsElapsed=' + jsElapsed + 'ms' +
' decodeMs=' + timings.decodeMs +
' encodeWriteMs=' + timings.encodeWriteMs +
' totalMs=' + timings.totalMs +
' jsGapMax=' + (sorted[0] ?? 0) + 'ms' +
' jsGapP95=' + (sorted[Math.floor(sorted.length * 0.05)] ?? 0) + 'ms' +
' dst=' + dst
);
}
console.log('[BENCH] done');
} catch (e) {
console.error('[BENCH] failed', e);
}
}