P0 native decode/encode benchmark passes on Xiaomi (12MP decode+encode ~210-270ms, off-main-thread)

This commit is contained in:
2026-09-09 15:10:30 +07:00
parent a8932f52ed
commit dfcf0f3a28
5 changed files with 157 additions and 40 deletions
+15 -39
View File
@@ -1,5 +1,3 @@
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system/legacy';
import RecipescamExport from '../../modules/recipescam-export';
const RUNS = 3;
@@ -7,50 +5,28 @@ 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.
* and logs per-stage ms proving the JS thread is not blocked while native work
* runs. Gated by EXPO_PUBLIC_BENCH=1.
*
* Release-build visibility is via native Log.i (RecipesCamExport tag) because
* Hermes release builds strip console.* entirely. JS-side dispatch gaps appear
* as the logcat deltas between consecutive decode "start" lines.
*/
export async function nativeBenchProbe(): Promise<void> {
console.log('[BENCH] start');
const m = RecipescamExport as any;
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);
const pong = await m.ping();
const src: string = await m.materializeBenchAsset('wallframe');
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
const dst = src.replace(/\.png$/, `-out-${Date.now()}-${i}.jpg`);
const timings = await m.decodeEncodeAsync(src, dst, 95);
console.error(
'[BENCH] run=' + i + ' jsRoundTripMs=' + (Date.now() - t0) +
' decodeMs=' + timings.decodeMs + ' encodeWriteMs=' + timings.encodeWriteMs +
' totalMs=' + timings.totalMs + ' pong=' + pong
);
}
console.log('[BENCH] done');
} catch (e) {
console.error('[BENCH] failed', e);
}