Skip to content

Instantly share code, notes, and snippets.

@rdlabo
Created October 27, 2026 13:09
Show Gist options
  • Select an option

  • Save rdlabo/1a422e90a9cfcd1b17eca9fb42274491 to your computer and use it in GitHub Desktop.

Select an option

Save rdlabo/1a422e90a9cfcd1b17eca9fb42274491 to your computer and use it in GitHub Desktop.
Android scroll-entry PR 7 verification evidence
{
"platform": "Android emulator-5580 / API 36.1",
"method": "Same instrumented APK, early path disabled/enabled by a temporary log-tag gate; 8 swipes with 2 native ads per run. No parallel builds or tests during recording.",
"apkSha256": "d1dcfa680db62fb5611465f037cf529b62052960657bc0246b348d2b3130875f",
"runs": {
"final-baseline": {
"samples": [
{
"gesture": 1,
"path": "bridge-hide",
"hideAfterDragMs": 86,
"drawAfterDragMs": 94
},
{
"gesture": 1,
"path": "bridge-hide",
"hideAfterDragMs": 90,
"drawAfterDragMs": 94
},
{
"gesture": 2,
"path": "bridge-hide",
"hideAfterDragMs": 17,
"drawAfterDragMs": 34
},
{
"gesture": 2,
"path": "bridge-hide",
"hideAfterDragMs": 34,
"drawAfterDragMs": 101
},
{
"gesture": 3,
"path": "bridge-hide",
"hideAfterDragMs": 19,
"drawAfterDragMs": 34
},
{
"gesture": 3,
"path": "bridge-hide",
"hideAfterDragMs": 36,
"drawAfterDragMs": 84
},
{
"gesture": 4,
"path": "bridge-hide",
"hideAfterDragMs": 19,
"drawAfterDragMs": 35
},
{
"gesture": 4,
"path": "bridge-hide",
"hideAfterDragMs": 36,
"drawAfterDragMs": 51
},
{
"gesture": 5,
"path": "bridge-hide",
"hideAfterDragMs": 19,
"drawAfterDragMs": 35
},
{
"gesture": 5,
"path": "bridge-hide",
"hideAfterDragMs": 36,
"drawAfterDragMs": 81
},
{
"gesture": 6,
"path": "bridge-hide",
"hideAfterDragMs": 22,
"drawAfterDragMs": 36
},
{
"gesture": 6,
"path": "bridge-hide",
"hideAfterDragMs": 23,
"drawAfterDragMs": 36
},
{
"gesture": 7,
"path": "bridge-hide",
"hideAfterDragMs": 20,
"drawAfterDragMs": 41
},
{
"gesture": 7,
"path": "bridge-hide",
"hideAfterDragMs": 21,
"drawAfterDragMs": 41
},
{
"gesture": 8,
"path": "bridge-hide",
"hideAfterDragMs": 20,
"drawAfterDragMs": 35
},
{
"gesture": 8,
"path": "bridge-hide",
"hideAfterDragMs": 36,
"drawAfterDragMs": 118
}
],
"hideMedianMs": 22.5,
"hideMaxMs": 90,
"recordedFrameAnalysis": {
"frames": 204,
"eligible": 204,
"minimumPixels": 577,
"blankRuns": []
}
},
"final-enabled": {
"samples": [
{
"gesture": 1,
"path": "early-hide",
"hideAfterDragMs": 3,
"drawAfterDragMs": 9
},
{
"gesture": 1,
"path": "early-hide",
"hideAfterDragMs": 4,
"drawAfterDragMs": 9
},
{
"gesture": 2,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 3
},
{
"gesture": 2,
"path": "early-hide",
"hideAfterDragMs": 2,
"drawAfterDragMs": 3
},
{
"gesture": 3,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
},
{
"gesture": 3,
"path": "early-hide",
"hideAfterDragMs": 2,
"drawAfterDragMs": 2
},
{
"gesture": 4,
"path": "early-hide",
"hideAfterDragMs": 0,
"drawAfterDragMs": 1
},
{
"gesture": 4,
"path": "early-hide",
"hideAfterDragMs": 0,
"drawAfterDragMs": 1
},
{
"gesture": 5,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
},
{
"gesture": 5,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
},
{
"gesture": 6,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
},
{
"gesture": 6,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
},
{
"gesture": 7,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 5
},
{
"gesture": 7,
"path": "early-hide",
"hideAfterDragMs": 4,
"drawAfterDragMs": 5
},
{
"gesture": 8,
"path": "early-hide",
"hideAfterDragMs": 0,
"drawAfterDragMs": 2
},
{
"gesture": 8,
"path": "early-hide",
"hideAfterDragMs": 1,
"drawAfterDragMs": 2
}
],
"hideMedianMs": 1.0,
"hideMaxMs": 4,
"recordedFrameAnalysis": {
"frames": 223,
"eligible": 214,
"minimumPixels": 437,
"blankRuns": []
}
}
},
"limitations": [
"Native View.INVISIBLE / UI draw callback timings, not physical display-frame timing.",
"Single sequential pair with different served test creatives, not a CPU/memory or occurrence-rate benchmark.",
"Empty-frame heuristic cannot detect all overlap.",
"Final clean build removes instrumentation and makes explicit native hide/teardown retire pending pixels immediately; normal entry path is unchanged."
],
"rawEvidenceRoot": "/private/tmp/admob-native-entry-proof",
"cleanAcceptance": {
"apkSha256": "32cbdc9da915ee01470dd2fd17df9629edf23d457a3b3d23cf3d7dd1a07f4a1a",
"normal": {
"eligibleFrames": 200,
"blankCandidates": 0
},
"bothPrepared": {
"frames": 226,
"eligibleFrames": 219,
"blankCandidates": 0,
"initialPngFiles": 2,
"newDecodedImageUrls": 0,
"finalFileProbe": "cache directory absent; not an allocation benchmark"
},
"multipleAds": {
"eligibleFramesPerAd": 65,
"blankCandidates": 0,
"bridgeErrors": 0,
"scenarios": ["mixed capture availability", "reload one ad", "detach one ad", "pause both"]
},
"scrollSpeedBenchmark": false
}
}

Android prepared-image drag entry

A prepainted opaque image can move with Web scrolling before a live native view is hidden by a later bridge request. Android VIEW registrations with placement handoff now observe the first one-pointer drag MOVE at the Window callback, before passing that MOVE to the application/WebView. The observer never consumes or rewrites input.

Entry and return

  • DOWN and sub-slop movement leave native controls interactive. A drag starting inside the native source's screen bounds stays on the existing path.
  • Each registration checks its prepared/drawn token, current native View identity and dimensions, visibility, capture eligibility, and scroll-region bounds and direction/range. Multitouch does not initiate this early path.
  • Eligible registrations cover their native view immediately and notify their own Web session. A separate gesture region supports rounded Ionic hosts that cannot use native compositor scroll tracking. Missing/unready tokens stay native.
  • The Web session adopts the early cover, retains placement ownership while the finger remains down, and restores after scroll settling. Existing generation, token, latest-placement and draw-ACK checks remain in use.
  • Cache invalidation can precede delivery of the cover notification. The idle underlay is retained until native release acknowledges; an unclaimed native cover asks for current placement, and release waits for native drawing. Repeated invalidation cannot prematurely clear those pixels. Native hide/teardown cancels that retention, and an older completion cannot clear a successor's image.

No new PNG or bitmap is generated on the gesture path. The Window observer is shared, with per-registration readiness/geometry metadata. Gesture recognition runs only until the first qualifying MOVE; no new per-frame measurement loop or bridge stream is added. Idle retirement can briefly retain the existing decoded image until the bounded native acknowledgement. CPU/memory parity was not measured.

Verification

An instrumented APK was run twice on Android emulator-5580 (API 36.1), once with only the early native path disabled and once enabled. Each run used eight swipes and two ads, with builds/tests stopped while recording. The served creatives were different; these are timing observations, not a rate/performance comparison.

Drag recognition to native View.INVISIBLE Existing bridge path Early native path
Samples 16 16
Median 22.5 ms 1 ms
Maximum 90 ms 4 ms

The early hide ran before the consuming MOVE callback. These are Android UI-thread state-change timings, not physical display-frame or visible-overlap durations. The instrumented recordings had 204/204 and 214/223 eligible frames respectively, with zero blank candidates in the existing image-content heuristic. That heuristic does not establish zero overlap/flash on every display frame.

The final code removes the measurement logs and temporary feature switch; it also makes explicit native hide/teardown cancel pending image retention immediately. The normal early-hide path is unchanged. Detailed timing samples and APK checksum are in the evidence JSON. Raw recordings and bridge/native traces are under /private/tmp/admob-native-entry-proof in the verification workspace.

Validation:

  • Engine: build/typecheck, lint/format and 88 browser tests passed.
  • Android: release lint; 18 instrumentation tests passed, with one API-23-only test skipped on API 36.1. New checks cover real native click delivery, callback order for multiple registrations, unprepared/uncapturable/released pixels, source hit regions, horizontal/out-of-region gestures and retirement recovery.
  • AdMob linked to this local Engine: 46 Web tests and 84 Android unit tests passed.
  • Clean APK: normal recording had 200/200 eligible frames, with no blank candidates. A second run waiting for both prepared ads had 219/226 eligible frames, also with no blank candidates. This is not a scroll-speed/FPS benchmark.
  • Multiple-ad recording: 65 eligible frames per ad, no blank candidates or bridge errors. Mixed capture availability, one-ad reload/detach and pause were exercised; the unaffected ad retained its projection and pause removed both images.
  • The prepared-ad run started with two cached PNGs and decoded no new image URL during the recorded scroll (its single recorded decode reused a pre-existing URL). Its final file probe returned no cache directory, so file-count sampling alone cannot establish allocation behavior throughout the run.

An earlier concurrent-build test run timed out while evaluating JavaScript during an unrelated navigation fixture. The final full Android run was repeated without parallel builds and passed. lintDebug also flags the pre-existing test Activity's intentional JavaScript enablement; CI's lintRelease passes.

Remaining scope

This is an Android opaque VIEW + placement-handoff optimization. Programmatic scroll, ineligible captures, out-of-region/unsupported directions and gestures owned by the native source keep existing behavior. It is not a universal atomic compositor transition, and no iOS/Liquid Glass capture change is included.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment