Skip to content

perf(stream): 加速进入串流的用户感知体验#27

Merged
qiin2333 merged 3 commits into
masterfrom
perf/stream-entry-speedup
May 14, 2026
Merged

perf(stream): 加速进入串流的用户感知体验#27
qiin2333 merged 3 commits into
masterfrom
perf/stream-entry-speedup

Conversation

@qiin2333
Copy link
Copy Markdown
Contributor

@qiin2333 qiin2333 commented May 13, 2026

概述

合并 6 项优化让点击应用→看到画面更快。冷启动预计省 0.5–1.5 秒,热启动省 0.3–0.8 秒。

改动清单

# 优化 文件 预期收益
1 ServerInfo 缓存复用 (TTL 30s) ComputerManager / StreamingSession / AppListPageV2 50–300 ms
2 AI LLM 探测延后到串流就绪后 StreamPage 50–300 ms 启动期带宽
3 AppListPage 预热 native + settings AppListPageV2 100–500 ms (首次)
4 fetchServerInfo / initializeNative 并行 StreamingSession 50–200 ms
5 drStart 提前进入 CONNECTED StreamingSession / StreamViewModel 200–500 ms 视觉感知
6 Settings 预热(随 #3 AppListPageV2 20–80 ms

关键实现

  • ComputerManager 新增 cacheServerInfo / getCachedServerInfo / invalidateServerInfoCache,5s 轮询路径与列表刷新均写入缓存
  • StreamingSession.fetchServerInfo 命中即跳过 HTTPS round-trip
  • startStreaming 把网络请求与本地 NAPI 初始化用 Promise.all 并行
  • 新增 setDecoderStartedCallback → ViewModel 在 drStart 触发即切 CONNECTED
  • AI 探测移到 probeAiAvailableDeferred,launchStream 完成后 2s 异步触发

验证

  • BUILD SUCCESSFUL (hvigorw assembleHap --mode module -p product=default)
  • 真机点击应用→画面计时对比

注意

未提交 submodule 指针变更(aubio / moonlight-common-c)。

Summary by CodeRabbit

发布说明

性能优化

  • 启动时预热流媒体资源,加速初始连接
  • 缓存服务器信息以减少网络请求
  • 并行执行初始化任务,提高响应速度
  • 优化连接状态更新,加快用户反馈

Review Change Stack

合并 6 项优化让点击应用→看到画面更快:

1. ServerInfo 缓存复用 (TTL 30s)
   ComputerManager 新增 cacheServerInfo/getCachedServerInfo。
   轮询路径 (updateComputerFromPoll) 与 AppListPage 刷新均写入缓存;
   StreamingSession.fetchServerInfo 命中即跳过一次 HTTPS round-trip
   (典型局域网省 50-300ms)。

2. AI LLM 探测延后到串流就绪后
   StreamPage.aboutToAppear 不再立即调用 nvHttp.checkAiAvailable(),
   避免与 serverinfo / launchApp 抢带宽和 TLS 资源。改成 launchStream
   返回后通过 probeAiAvailableDeferred 异步触发,用户感知延迟 < 2s。

3. AppListPage 预热启动链路资源
   aboutToAppear 调用 StreamingSession.getDecoderCapabilities() 触发
   .so 加载 + NAPI 注册(首次冷启动省 100-500ms),同时预读三组
   设置项让 startStreaming 时走内存缓存。

4. startStreaming 并行化
   将 fetchServerInfo (网络) 与 initializeNative (本地) 改为
   Promise.all 并行执行,省下两者中较短的耗时。

5. drStart 提前进入 CONNECTED
   StreamingSession 新增 setDecoderStartedCallback;StreamViewModel
   在 drStart 触发时立即把 connectionState 切到 CONNECTED,让 loading
   蒙层比原来等 session.start() resolve 更早消失。

6. Settings 预热(随 #3 一起完成)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Warning

Rate limit exceeded

@qiin2333 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 15 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe2a56ba-77b2-49d2-83bd-53947b67bbb0

📥 Commits

Reviewing files that changed from the base of the PR and between 59aa23f and 720f248.

📒 Files selected for processing (4)
  • entry/src/main/ets/pages/StreamPage.ets
  • entry/src/main/ets/service/streaming/StreamingSession.ets
  • entry/src/main/ets/service/usbdriver/UsbDriverService.ets
  • entry/src/main/ets/viewmodel/StreamViewModel.ets
📝 Walkthrough

工作流

此 PR 优化了 Moonlight 流启动流程。通过在 ComputerManager 中添加 TTL 缓存机制、在 StreamingSession 中实现解码器启动回调和并行化异步操作,以及在 StreamViewModel 中提早更新 UI 连接状态,减少网络往返并加快用户可见的响应。同时推迟 StreamPage 的 AI 检查,避免阻塞流启动。

变更

流启动性能和 UI 响应性优化

层级 / 文件 总结
ComputerManager ServerInfo 缓存基础设施
entry/src/main/ets/service/ComputerManager.ets
添加 SERVER_INFO_CACHE_TTL_MS 常量和 CachedServerInfo 结构,在 ComputerManager 实现 serverInfoCache 映射,提供 cacheServerInfo()getCachedServerInfo()invalidateServerInfoCache() 方法进行 TTL 基础缓存管理,在 updateComputerFromPoll 中自动缓存轮询获得的 ServerInfo。
StreamingSession 解码器回调与并行启动
entry/src/main/ets/service/streaming/StreamingSession.ets
添加 decoderStartedCallback 字段和 setDecoderStartedCallback() 接口,将 start() 内的 fetchServerInfo()initializeNative() 改为 Promise.all 并行执行,fetchServerInfo() 优先查询 ComputerManager 缓存,命中时提前返回,否则获取后写回缓存,drStart 原生事件触发时调用注册的回调。
StreamViewModel 解码器启动时提早更新连接状态
entry/src/main/ets/viewmodel/StreamViewModel.ets
startStreaming 中通过 setDecoderStartedCallback 注册回调,当解码器启动且当前未连接时,立即更新 connectionProgress 为 1.0、connectionStageText 为"已连接"、connectionState 为 CONNECTED,加快用户可见的连接 UI 反馈。
StreamPage AI 可用性检查推迟到流启动后
entry/src/main/ets/pages/StreamPage.ets
缓存 NvHttp 实例到 aiKeyNvHttp 字段,移除 aboutToAppear 中的同步 AI 检查,新增 probeAiAvailableDeferred() 方法在 2 秒延迟后异步检查,在 startStreaming()launchStream() 成功后触发该异步探测,避免阻塞流启动。
AppListPageV2 资源预热与服务器信息缓存
entry/src/main/ets/pages/AppListPageV2.ets
导入 SettingsServiceStreamingSession,在 aboutToAppear 调用 preheatStreamResources() 查询解码器能力和预加载设置,在 refreshApps() 后通过 computerManager.cacheServerInfo() 缓存获得的 ServerInfo,为后续流启动流程提供预热数据。

代码审查工作量估计

🎯 3 (中等) | ⏱️ ~20 分钟

🚥 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 标题准确概括了PR的核心目标:通过多项优化加速进入串流的用户感知体验,与变更内容(缓存、延后AI探测、并行初始化等)完全对应。
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/stream-entry-speedup

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
entry/src/main/ets/service/ComputerManager.ets (3)

587-591: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

删除电脑时应清理 ServerInfo 缓存

removeComputer 清理了 computersofflineCount,但未清理 serverInfoCache。虽然缓存 30 秒后会过期,但为了一致性和避免微小的内存泄漏,建议同步删除。

🔧 建议的修复
 async removeComputer(uuid: string): Promise<void> {
   this.computers.delete(uuid);
   this.offlineCount.delete(uuid);
+  this.serverInfoCache.delete(uuid);
   await this.saveComputers();
 }
🤖 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 `@entry/src/main/ets/service/ComputerManager.ets` around lines 587 - 591,
removeComputer currently deletes entries from computers and offlineCount and
calls saveComputers but does not remove the corresponding entry from
serverInfoCache; update the async removeComputer(uuid: string) method to also
call serverInfoCache.delete(uuid) (or equivalent cache removal) when removing a
computer so the serverInfoCache, computers, and offlineCount are kept consistent
and avoid lingering cache entries.

1092-1104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

解除配对后应使 ServerInfo 缓存失效

unpairComputer 更改了配对状态,但未调用 invalidateServerInfoCache(uuid)。缓存中的 ServerInfo 可能仍包含 paired=true,导致后续"进入串流"时读取到过期状态。

根据 invalidateServerInfoCache 方法注释(第 418-422 行),配对/解配后应主动失效缓存。

🔧 建议的修复
 async unpairComputer(uuid: string): Promise<void> {
   const computer = this.computers.get(uuid);
   if (!computer) {
     throw new Error('电脑不存在');
   }

   const nvHttp = NvHttp.fromComputer(computer, this.context || undefined);
   await nvHttp.unpair();
   
   computer.pairState = PairState.NOT_PAIRED;
   computer.serverCert = '';
+  this.invalidateServerInfoCache(uuid);
   await this.saveComputers();
 }
🤖 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 `@entry/src/main/ets/service/ComputerManager.ets` around lines 1092 - 1104,
unpairComputer currently updates computer.pairState, clears serverCert and calls
saveComputers but does not invalidate the cached ServerInfo, so callers may
still see paired=true; update unpairComputer (the method) to call
invalidateServerInfoCache(uuid) after setting PairState.NOT_PAIRED (and
before/after saveComputers as you prefer) to ensure the cached ServerInfo for
that uuid is cleared; reference invalidateServerInfoCache and
PairState.NOT_PAIRED when making the change.

1044-1087: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

配对成功后应使 ServerInfo 缓存失效

pairComputer 成功配对后更新了 pairStateserverCert,但未调用 invalidateServerInfoCache(uuid)。缓存中的旧 ServerInfo 可能包含 paired=false,导致状态不一致。

建议在配对成功后(第 1066 行之后)失效缓存,确保后续请求获取最新状态。

🔧 建议的修复
       computer.pairState = PairState.PAIRED;
       if (result.serverCert) {
         const buf = buffer.from(result.serverCert);
         computer.serverCert = buf.toString('base64');
       }
       computer.pairName = result.pairName || '';
+      this.invalidateServerInfoCache(uuid);
       await this.saveComputers();
       return pin;
🤖 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 `@entry/src/main/ets/service/ComputerManager.ets` around lines 1044 - 1087,
After a successful pairing in pairComputer, the ServerInfo cache isn't
invalidated so stale data (e.g., paired=false) may be returned; modify
pairComputer so that after setting computer.pairState = PairState.PAIRED,
updating computer.serverCert and computer.pairName and awaiting
this.saveComputers(), you call invalidateServerInfoCache(uuid) to evict the
cached ServerInfo for that computer (ensure you reference the same uuid
parameter and the existing invalidateServerInfoCache function).
🤖 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 `@entry/src/main/ets/pages/StreamPage.ets`:
- Around line 457-477: probeAiAvailableDeferred() starts a setTimeout but never
clears it, causing callbacks (and Toasts/requests) after the page is destroyed;
add a class-level timer id (e.g., aiProbeTimerId: number | null) and assign the
return of setTimeout in probeAiAvailableDeferred(), then clear it in
aboutToDisappear() via clearTimeout(this.aiProbeTimerId) and null it;
additionally, guard the async callback by checking the page-is-mounted flag or
that aiKeyNvHttp still exists before using it to avoid post-destroy
actions—apply the same pattern to the other deferred probes referenced around
the other occurrences (lines ~688-701 and ~1328-1330) such as any other
probeXDeferred functions.

In `@entry/src/main/ets/service/streaming/StreamingSession.ets`:
- Around line 1017-1020: The drStart handler currently calls the external
decoderStartedCallback directly which can throw and bubble into native callback
flow; wrap the invocation of decoderStartedCallback inside a try/catch within
drStart (the arrow function named drStart) so any thrown error is caught, log
the error via existing logger/console (include context like
"decoderStartedCallback error"), and do not rethrow so native callback chain
remains stable.

In `@entry/src/main/ets/viewmodel/StreamViewModel.ets`:
- Around line 360-366: The callback passed to
streamingSession.setDecoderStartedCallback is too permissive and can flip
ERROR/DISCONNECTED back to CONNECTED; change the callback to first capture the
session instance (e.g., const session = this.streamingSession) and then only set
connectionState = StreamConnectionState.CONNECTED when this.connectionState ===
StreamConnectionState.CONNECTING and this.streamingSession === session, updating
connectionProgress and connectionStageText only under those conditions; use
streamingSession.setDecoderStartedCallback(...) and the captured session
reference to ensure the callback belongs to the current session before mutating
connectionState.

---

Outside diff comments:
In `@entry/src/main/ets/service/ComputerManager.ets`:
- Around line 587-591: removeComputer currently deletes entries from computers
and offlineCount and calls saveComputers but does not remove the corresponding
entry from serverInfoCache; update the async removeComputer(uuid: string) method
to also call serverInfoCache.delete(uuid) (or equivalent cache removal) when
removing a computer so the serverInfoCache, computers, and offlineCount are kept
consistent and avoid lingering cache entries.
- Around line 1092-1104: unpairComputer currently updates computer.pairState,
clears serverCert and calls saveComputers but does not invalidate the cached
ServerInfo, so callers may still see paired=true; update unpairComputer (the
method) to call invalidateServerInfoCache(uuid) after setting
PairState.NOT_PAIRED (and before/after saveComputers as you prefer) to ensure
the cached ServerInfo for that uuid is cleared; reference
invalidateServerInfoCache and PairState.NOT_PAIRED when making the change.
- Around line 1044-1087: After a successful pairing in pairComputer, the
ServerInfo cache isn't invalidated so stale data (e.g., paired=false) may be
returned; modify pairComputer so that after setting computer.pairState =
PairState.PAIRED, updating computer.serverCert and computer.pairName and
awaiting this.saveComputers(), you call invalidateServerInfoCache(uuid) to evict
the cached ServerInfo for that computer (ensure you reference the same uuid
parameter and the existing invalidateServerInfoCache function).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d357090f-330e-44c7-abfb-a3e78010afc4

📥 Commits

Reviewing files that changed from the base of the PR and between 8177459 and 59aa23f.

📒 Files selected for processing (5)
  • entry/src/main/ets/pages/AppListPageV2.ets
  • entry/src/main/ets/pages/StreamPage.ets
  • entry/src/main/ets/service/ComputerManager.ets
  • entry/src/main/ets/service/streaming/StreamingSession.ets
  • entry/src/main/ets/viewmodel/StreamViewModel.ets

Comment thread entry/src/main/ets/pages/StreamPage.ets
Comment thread entry/src/main/ets/service/streaming/StreamingSession.ets
Comment thread entry/src/main/ets/viewmodel/StreamViewModel.ets
qiin2333 added 2 commits May 13, 2026 23:29
1. launchApp 成功后立即 invalidateServerInfoCache(computerId)
   缓存里的 currentGame 在进入串流后必然变化,30s TTL 内复用
   会导致下次 fetchServerInfo 命中缓存时拿到陈旧值,可能误触发
   resumeApp 路径或漏 quitApp。

2. UsbDriverService.listKnownUsbGamepads 显式判 undefined
   模拟器/无 USB 权限场景下 usbManager.getDevices() 返回 undefined,
   原代码进入 for 循环触发 TypeError,每 5 秒一次噪声。
- StreamPage: AI 探测 setTimeout 在 aboutToDisappear 中清理,避免页面销毁后弹 Toast
- StreamingSession: drStart 回调用 try/catch 隔离,外部异常不再冒泡到 native 回调链
- StreamViewModel: drStart 提前置 CONNECTED 加 session 实例校验 + 仅 CONNECTING 时生效,
  防止旧会话延迟回调把 ERROR/DISCONNECTED 改回 CONNECTED
@qiin2333 qiin2333 merged commit 8ee0a04 into master May 14, 2026
2 checks passed
@qiin2333 qiin2333 deleted the perf/stream-entry-speedup branch May 14, 2026 02:41
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