Skip to content

perf(android): move state persistence off UI thread (AN-1) + test hardening#602

Merged
sunnylqm merged 5 commits into
masterfrom
review/audit-fixes-p0-p2
Jul 4, 2026
Merged

perf(android): move state persistence off UI thread (AN-1) + test hardening#602
sunnylqm merged 5 commits into
masterfrom
review/audit-fixes-p0-p2

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

背景

代码审计(见 CODE_AUDIT.md)路线图的收尾项。P0/P1 的绝大部分已在 7d959ad 落地(已在 master)。本 PR 包含两个后续提交:

perf(android): 状态持久化移出 UI 线程(AN-1)

  • switchVersion / markSuccess / setUuid / setLocalHashInfo 本质只是 SharedPreferences 的同步 commit(),之前 dispatch 到 UI 线程纯粹为串行化。markSuccess 每次冷启动必经,其阻塞磁盘写在主线程是低端机卡顿/ANR 来源。
  • 新增 StateSerialRunner(专用单线程 executor),保留串行化 + commit() 持久性,把磁盘 I/O 移出 UI 线程。
  • reloadUpdate / restartApp 仍走 UiThreadRunner(reload 是真 UI 操作)。

improvement 2: 测试加固

  • C++ patch_core 测试跑完全部用例并汇总统计(原来首个失败即 return);test-patch-core.sh 增加 SANITIZE=1 开关跑 ASan+UBSan。
  • 新增 JS 回归测试:JS-3(apkStatus 失败复位可重试)、JS-8(并发下载去重),均验证过移除修复后测试确实失败。

验证

  • 本地:bun lint 通过、73 个 JS 单测全绿、16 个 C++ patch_core 测试默认与 sanitizer 双模式全通过。
  • AN-1 的功能正确性依赖本 PR 的 Android e2eExample/e2etest/e2e/local-merge.test.ts,覆盖新架构 + RN0.77 旧架构双 job)回归;沙盒环境无法跑模拟器/Gradle,故开此 PR 让 CI 验证。

已知盲区(记录,不在本 PR 处理)

串行 e2e 测不到并发竞态窗口(switchVersionLater 后台线程 vs reloadUpdate→switchVersion UI 线程对同一 SP 的交叉写)——该窗口与改动前风险等价,AN-1 未扩大它。详见 CODE_AUDIT.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved update handling so repeated update requests for the same package are deduplicated and complete reliably.
    • Fixed APK download retries so a failed download no longer blocks future install attempts.
  • Performance / Reliability
    • Reduced the chance of UI slowdowns by moving update state persistence work to a background thread while keeping operations in order.
  • Tests
    • Expanded coverage with Android-specific mocks, added regression tests for update deduplication and APK retry behavior.
    • Enhanced patch-core test reliability and added optional sanitizer builds for native tests.

sunnylqm and others added 2 commits July 4, 2026 19:50
switchVersion/markSuccess/setUuid/setLocalHashInfo only read/modify
SharedPreferences via a synchronous commit(); they were dispatched to the
UI thread purely to serialize them. markSuccess runs on every cold start,
so its blocking disk write on the main thread caused jank/ANR on low-end
devices.

Add StateSerialRunner (a dedicated single-thread executor) that preserves
the same serialization + commit() persistence guarantees while keeping the
disk I/O off the UI thread. reloadUpdate/restartApp remain on UiThreadRunner
since reload is a genuine UI operation.

Also adds CODE_AUDIT.md documenting the full P0-P2 audit and remediation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8f88be7-11ab-4ae1-9a3d-efec84ce1dae

📥 Commits

Reviewing files that changed from the base of the PR and between 0763c29 and 0d6ec3e.

📒 Files selected for processing (2)
  • android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
  • src/__tests__/client.test.ts

📝 Walkthrough

Walkthrough

Adds an Android StateSerialRunner for serialized background state writes, switches several UpdateModuleImpl methods to use it, updates the patch-core C++ test runner and build script, and extends client tests with Android mocks plus concurrency and retry coverage.

Changes

Android background state serialization

Layer / File(s) Summary
StateSerialRunner implementation
android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
New helper runs Operation tasks on a single-thread Executor, preserving order and rejecting the Promise or logging on failure.
UpdateModuleImpl wiring to StateSerialRunner
android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
setNeedUpdate, markSuccess, setUuid, and setLocalHashInfo (both promise and non-promise overloads) now use StateSerialRunner.run instead of UiThreadRunner.run.

C++ patch-core test infrastructure

Layer / File(s) Summary
Test runner failure aggregation
cpp/patch_core/tests/patch_core_test.cpp
main() keeps running after test exceptions, counts failures, prints a pass/fail summary, and returns exit code based on total failures.
Sanitizer build flags in test script
scripts/test-patch-core.sh
Adds optional SANITIZE=1 mode that sets sanitizer flags and passes them into the native compile and link commands.

Client download/install concurrency tests

Layer / File(s) Summary
Android APK mock helper
src/__tests__/client.test.ts
Adds setupAndroidApkMocks and restores __DEV__ after each test while keeping Android-specific mocks in place.
Concurrent download and retry tests
src/__tests__/client.test.ts
Adds regression tests for concurrent downloadUpdate deduplication and downloadAndInstallApk retry after failure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main Android performance change and the added test hardening.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review/audit-fixes-p0-p2

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/__tests__/client.test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: .eslintrc.js » @react-native/eslint-config#overrides[4]:
Environment key "jest/globals" is unknown

at /node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2079:23
at Array.forEach (<anonymous>)
at ConfigValidator.validateEnvironment (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2073:34)
at ConfigValidator.validateConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2223:18)
at CascadingConfigArrayFactory._finalizeConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3985:23)
at CascadingConfigArrayFactory.getConfigArrayForFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3791:21)
at FileEnumerator._iterateFilesWithFile (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:368:43)
at FileEnumerator._iterateFiles (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:349:25)
at FileEnumerator.iterateFiles (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:299:59)
at iterateFiles.next (<anonymous>)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java (2)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider naming the executor's thread for easier debugging.

Executors.newSingleThreadExecutor() produces an unnamed thread (e.g., pool-1-thread-1), which makes it harder to identify in thread dumps/ANR traces when diagnosing persistence-related issues.

♻️ Optional: use a named ThreadFactory
-    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
+    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(
+        r -> new Thread(r, "StateSerialRunner")
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java`
at line 31, The executor in StateSerialRunner currently uses
Executors.newSingleThreadExecutor() with a default unnamed thread, making
debugging harder. Update the EXECUTOR initialization to use a ThreadFactory that
assigns a clear, stable thread name for this runner, and keep the change
localized to the StateSerialRunner class so thread dumps and ANR traces are
easier to identify.

25-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a unit test for the ordering/error-handling contract.

This class introduces new threading semantics (serialized execution, promise-reject-vs-log branching) that other code now depends on. A small JVM-level test (submit N operations, assert execution order; submit a throwing operation with promise=null and assert Log.e path, etc.) would guard against regressions, complementing the test hardening already done in this PR for the C++ side.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java`
around lines 25 - 56, Add a JVM-level test for StateSerialRunner to lock in its
new contract: verify that run() executes submitted Operation instances in
submission order on the single-thread EXECUTOR, and verify the error-handling
branch by running a throwing Operation with a null Promise so the Log.e path is
exercised. Also cover the promise-reject path by passing a Promise and
confirming promise.reject is invoked from the catch block, using
StateSerialRunner.run, Operation.run, and the UpdateContext.TAG logging branch
as the key entry points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/client.test.ts`:
- Around line 100-155: Reset the Android test setup after each case in
client.test.ts: setupAndroidApkMocks mutates globalThis.__DEV__ and installs
mock.module overrides for react-native, ../core, ../permissions, and ../i18n
without cleanup, so later tests can inherit the Android/DEV-off state. Add
teardown in the test suite (for example via afterEach with mock.restore or
equivalent) to restore the mocked modules and reset any globals changed by
setupAndroidApkMocks.

---

Nitpick comments:
In `@android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java`:
- Line 31: The executor in StateSerialRunner currently uses
Executors.newSingleThreadExecutor() with a default unnamed thread, making
debugging harder. Update the EXECUTOR initialization to use a ThreadFactory that
assigns a clear, stable thread name for this runner, and keep the change
localized to the StateSerialRunner class so thread dumps and ANR traces are
easier to identify.
- Around line 25-56: Add a JVM-level test for StateSerialRunner to lock in its
new contract: verify that run() executes submitted Operation instances in
submission order on the single-thread EXECUTOR, and verify the error-handling
branch by running a throwing Operation with a null Promise so the Log.e path is
exercised. Also cover the promise-reject path by passing a Promise and
confirming promise.reject is invoked from the catch block, using
StateSerialRunner.run, Operation.run, and the UpdateContext.TAG logging branch
as the key entry points.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bee95f0-1c99-481e-acee-b8372caee6b4

📥 Commits

Reviewing files that changed from the base of the PR and between 7d959ad and 0763c29.

📒 Files selected for processing (5)
  • android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
  • cpp/patch_core/tests/patch_core_test.cpp
  • scripts/test-patch-core.sh
  • src/__tests__/client.test.ts

Comment thread src/__tests__/client.test.ts
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • src/__tests__/client.test.ts

Commit: c56ab4d8375d9073acf998cb0566b13f2ea125cc

The changes have been pushed to the review/audit-fixes-p0-p2 branch.

Time taken: 2m 18s

coderabbitai Bot and others added 2 commits July 4, 2026 14:52
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Executors.newSingleThreadExecutor() produces an unnamed thread
(pool-1-thread-1), which is hard to spot in thread dumps / ANR traces. Give it
a named ThreadFactory ("pushy-state-serial") so persistence-related issues are
easier to diagnose.

Addresses CodeRabbit review on PR #602.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunnylqm sunnylqm merged commit 33b9d29 into master Jul 4, 2026
5 of 7 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.

1 participant