Skip to content

fix(image): reuse memory-cached renditions on mount (OK-63873) - #126

Merged
huhuanming merged 7 commits into
mainfrom
fix/image-mount-memory-probe-ok-63873
Sep 22, 2026
Merged

huhuanming merged 7 commits into
mainfrom
fix/image-mount-memory-probe-ok-63873

Conversation

@weatherstar

@weatherstar weatherstar commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Problem

On iOS, a freshly mounted OneKeyImage could paint a skeleton for 3–5 frames even when the image was already decoded in memory. The common cachePolicy arrived as nil, and a pre-layout request could derive a different thumbnail key from the later render or preload, so the synchronous probe missed and entered the asynchronous image pipeline.

A second case remained after exact-key probing: the same raw image is requested at several view sizes. SDWebImage and Glide include decode size in their resource identity, so an already decoded larger or smaller rendition was ignored while the exact rendition loaded. The result was an avoidable blank/skeleton frame during account switches and native-list reuse.

The package also had no authoritative document for lifecycle, cache identity, event ownership, safety limits, or accepted iOS/Android differences, and the repository had no always-on native-module development rule requiring such a spec.

Fix

Exact and cross-size memory reuse

  • Treat an unset cache policy as the existing memory-disk default during the iOS probe.
  • Derive pre-layout decode candidates from resizeWidth/resizeHeight, including the render contentFit and preload-compatible cover key.
  • Select the TOS rendition from the hinted long edge in render and preload so URL and decode identity can match.
  • Add resizeHeight to the Nitro view contract and reset it on reuse.
  • Add a bounded iOS/Android memory-variant registry keyed by raw URL plus canonical header digest. It retains at most 1,024 families and 8 learned variants per family.
  • Probe exact optimized/raw keys first, then the smallest compatible larger variant and the largest compatible smaller variant. Cross-size candidates allow at most 1.1 aspect-ratio drift.
  • Keep a compatible static preview visible while the exact asynchronous request replaces it. Animated images, disk, and none policies do not participate.
  • SDImageCache and Glide remain authoritative. Registry entries are hints and are removed after a memory miss; memory/all cache clearing also clears the hints.
  • Android uses Glide's onlyRetrieveFromCache(true) and accepts only an immediate memory callback. A square target never reuses a circle-transformed bitmap; a circular target may reuse an untransformed bitmap because the host clips it.

There is no fixed three-size cache family: TOS has five layout tiers with standard/high-density renditions, while the registry records up to eight variants actually used by a source family.

Observable behavior

  • An approximate preview emits no load/display/error callbacks. The exact request still owns onLoad or onError, followed by onLoadEnd.
  • An iOS exact synchronous memory hit may finish the request immediately with cacheType: memory. Android can display the exact entry immediately while the normal Glide request remains terminal.
  • Existing cache-policy, fallback, optimized-to-raw retry, cancellation, recycling, and safety behavior remains in force.

Specs and development requirements

  • Add native-views/react-native-image/docs/SPEC.md as the authoritative Native Image contract: scope, definitions, API/defaults, lifecycle and event order, cache identity, cross-size selection, TOS tiers, safety limits, platform differences, conformance map, and acceptance matrix.
  • Add docs/NATIVE_MODULE_DEVELOPMENT.md requiring repository-template initialization, a package docs/SPEC.md, defined boundaries/defaults/lifecycle/platform differences/limits, and a post-change code-versus-spec audit.
  • Add root AGENTS.md as the always-loaded instruction entry, with CLAUDE.md importing the same rules, so detailed requirements stay in one document.
  • Point the root and Native Image READMEs to the new requirements/spec and use the checked-in yarn create:module / yarn create:view generators.

Verification

  • Native Image Jest: 21/21 tests pass.
  • Android focused unit tests for memory-variant ordering/compatibility and request signatures pass; Kotlin compilation also passes in the example Gradle project.
  • Android full unit run executed 82 tests; 10 existing Blockie/decode tests fail during JVM initialization because android.text.TextUtils.isEmpty is not mocked. The memory-variant and signature tests are not among those failures.
  • Markdown Prettier check, git diff --check, and relative-link validation pass.
  • Earlier app-monorepo iOS simulator recordings for the exact-key path show the header, rows, and icons landing in the same frame instead of icons remaining grey for roughly 50–70 ms. The cross-size preview implementation is complete on iOS and Android. Dedicated rendered acceptance on both platforms remains additional validation evidence, not missing implementation.

@sidmorizon

Copy link
Copy Markdown
Contributor

@sidmorizon

Copy link
Copy Markdown
Contributor

View Devin Review ↗️

@sidmorizon

Copy link
Copy Markdown
Contributor

Local Codex CLI review

Open the review session in Slack

The review runs in an isolated temporary worktree. It may make bounded reproduction changes there, but never modifies the original project checkout or running systems, and continues in this session when the PR receives a substantive commit.

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex review

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T00:02:05.320319Z 5d1db65 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sidmorizon

Copy link
Copy Markdown
Contributor

Claude review session: https://claude.ai/code/session_01Hjx1BHahasA5T9Nz3RXYZx

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 5d1db65c95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread native-views/react-native-image/ios/OneKeyImage.swift Outdated
Comment thread CHANGELOG.md Outdated
@weatherstar
weatherstar force-pushed the fix/image-mount-memory-probe-ok-63873 branch 2 times, most recently from 1a9cfdd to e4ca102 Compare September 22, 2026 00:26
@weatherstar
weatherstar changed the base branch from main to codex/react-native-image-round September 22, 2026 00:26
@weatherstar

Copy link
Copy Markdown
Contributor Author

Addressed the three review findings (branch force-pushed, now based on #125 so the 3.0.157 release carries the round prop that app-monorepo release/v6.6.0 already depends on):

P1 — probe key vs. preload / laid-out request

  • probeViewSize is gone. OneKeyImageDecodeSizing.probeThumbnailPixelSizes(bounds:resizeWidth:resizeHeight:scale:contentFit:) returns the candidate decode-thumbnail keys, most specific first: laid out → exactly the render request key (bounds + contentFit); before layout → logicalViewSize(resizeWidth, resizeHeight) under this view's contentFit and under .cover (the preload key), deduped. The memory probe tries each candidate.
  • New resizeHeight view prop (nitro spec + iOS + Android + JS wrapper), so a non-square image can pass the same hints its preload used.
  • Fallback for the "pre-layout hit, laid-out miss" case: startLoad keeps the displayed image (shouldPreserveDisplayedImage) and hands it to sd_internalSetImage as placeholderImage; showLoading no longer blanks it or starts the skeleton in that case.

P2 — tests

  • Replaced the self-comparing test with cross-path assertions: laid-out probe == render key for every contentFit; square hint == preload key == render key (120x120); README shape 48×64 @3 under .contain == [render 144x192, preload 192x192] (would have failed before); no hint → []; plus shouldPreserveDisplayedImage. Still not executed locally (the example workspace has no test action for the pod scheme); swiftc -parse clean.

P1 — version

@weatherstar
weatherstar changed the base branch from codex/react-native-image-round to main September 22, 2026 00:28
@weatherstar

Copy link
Copy Markdown
Contributor Author

Base is back on main. The branch intentionally carries the 7 commits of #125 (round images, iOS NativeList disposal, 3.0.156) underneath the two commits of this fix: app-monorepo release/v6.6.0 already depends on 3.0.154's round prop, so a 3.0.157 cut from plain main would regress it. Merging this PR therefore lands #125 as well; if #125 merges first, this rebases to just the two commits on top.

Comment thread native-views/react-native-image/ios/OneKeyImage.swift
Comment thread native-views/react-native-image/ios/OneKeyImageRequestContext.swift
Comment thread native-views/react-native-image/ios/OneKeyImage.swift
@weatherstar
weatherstar force-pushed the fix/image-mount-memory-probe-ok-63873 branch from e4ca102 to fa29148 Compare September 22, 2026 00:43
Comment thread native-views/react-native-image/ios/OneKeyImage.swift Outdated
@weatherstar
weatherstar force-pushed the fix/image-mount-memory-probe-ok-63873 branch 2 times, most recently from 2c42bcc to d7b3aaf Compare September 22, 2026 01:24
Comment thread native-views/react-native-image/src/index.tsx
…-63873)

On iOS a freshly mounted OneKeyImage probes the memory cache from
identityDidChange, before its first layout. The SDWebImage key carries the
decode thumbnail size derived from the host view bounds, which are still
empty at that point, so the probe never matched the entry the prewarm (or
the previous mount) stored and every new token row painted an icon
skeleton for at least one frame even when the logo was already in memory.

Derive the probe size from the resizeWidth hint while the bounds are empty
(a square, exactly what the preload uses for a width-only source), so the
mount frame can show the cached image; a laid-out view keeps using its real
bounds.
@huhuanming
huhuanming force-pushed the fix/image-mount-memory-probe-ok-63873 branch from d7b3aaf to 3abd450 Compare September 22, 2026 07:14
react-native-image and react-native-native-list were the only two Nitro
packages without a package .gitignore, so their nitrogen/generated trees
were committed. Every prop change then dragged a dozen generated C++,
Kotlin, Swift and ObjC++ files into the diff — 117 files in total, all of
them reproducible from the .nitro.ts spec.

Ignore nitrogen/ in both packages, matching the other 23 Nitro packages
and the scaffolding template, and drop the tracked output. react-native-
image already ran `yarn nitrogen` in its release script; native-list did
not, and its prepare step copies nitrogen into lib, so add the codegen
step there too. Both packages keep `nitrogen` in their npm `files`, so
the published tarballs are unchanged.

Regenerating on top of the deleted tree reproduces the committed files
byte for byte apart from trailing whitespace an editor had stripped.

The image fix and this cleanup are recorded under Unreleased; no version
bump, so the next release picks them up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@huhuanming
huhuanming force-pushed the fix/image-mount-memory-probe-ok-63873 branch from 3abd450 to 1f208d9 Compare September 22, 2026 07:28
huhuanming and others added 2 commits September 22, 2026 16:53
`round` clips the native view to an oval on Android (`addOval`) and to a
`min(width, height) / 2` corner radius on iOS (`updateRoundMask`), but
the RN wrapper that hosts a custom `placeholder` or `fallback` only ever
clipped to the caller's own `borderRadius`. With `round` and no
`borderRadius` the wrapper had no radius at all, so `overflow: hidden`
clipped to a rectangle and the overlays were painted square over a round
image — against the contract the README states, that image, placeholder
and fallback share the same clipping.

Give the overlay container and the drawn border overlay a percentage
radius when `round` is set. It says "circle" without measuring the view,
it is exactly Android's oval, and it matches iOS for the square icons
`round` exists for. All the per-corner radii are set too, so a caller's
`borderTopLeftRadius` cannot survive the round clip and leave one square
corner.

Tests assert the container clip overrides a caller's per-corner radius
and that the border overlay follows; both fail without the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread native-views/react-native-image/ios/OneKeyImage.swift Outdated
@sidmorizon

Copy link
Copy Markdown
Contributor

Cursor review failed.

Remote error: cursor_not_found: HTTP 404 Not Found: AI Model Not Found: Model name is not valid: "grok-4.7"

@cursor

cursor Bot commented Sep 22, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@huhuanming huhuanming changed the title fix(image): show memory-cached images on the mount frame instead of an icon skeleton (OK-63873) fix(image): reuse memory-cached renditions on mount (OK-63873) Sep 22, 2026
@sidmorizon

Copy link
Copy Markdown
Contributor

Cursor review failed.

Remote error: cursor_dispatch_reconciliation_required: cursor_dispatch_reconciliation_required

@cursor

cursor Bot commented Sep 22, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@huhuanming
huhuanming merged commit 52849b8 into main Sep 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants