mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
perf(mobile): warm WebKit's processes on iOS before the first page opens (#39851)
* perf(mobile): warm WebKit's processes on iOS before the first page opens Builds a blank, non-persistent WKWebView a second after Home has a paired host, so the first page open starts against WebContent, GPU and Network processes already running. A release before the delayed start now makes the start a no-op. Based on #32163 by Jinwoo-H. Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com> * fix(mobile): guard warmUp on older binaries and release the warm view on page failure Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com> --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
This commit is contained in:
co-authored by
Jinwoo-H
m4air
parent
346dcae485
commit
537d764f58
@@ -603,6 +603,7 @@ final class OrcaMobileWebShellView: ExpoView, WKNavigationDelegate, WKUIDelegate
|
||||
|
||||
/// history.replaceState changes the path before finish; finished() uses commit state instead of URL equality.
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
MobileWebShellWarmUp.release()
|
||||
emit(loadState.finished())
|
||||
}
|
||||
|
||||
@@ -611,15 +612,18 @@ final class OrcaMobileWebShellView: ExpoView, WKNavigationDelegate, WKUIDelegate
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error
|
||||
) {
|
||||
MobileWebShellWarmUp.release()
|
||||
reportNavigationFailure(error)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
MobileWebShellWarmUp.release()
|
||||
reportNavigationFailure(error)
|
||||
}
|
||||
|
||||
/// Report renderer death; memory pressure/provider updates look alike, so the caller owns retry policy.
|
||||
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
MobileWebShellWarmUp.release()
|
||||
clearBridgeTarget()
|
||||
// Jetsam takes WebContent while Orca is backgrounded; the report can arrive as the app resumes, before it is active.
|
||||
let background = window == nil || UIApplication.shared.applicationState != .active
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
/// The first page view in a process waits for WebKit to launch its helper processes. A blank view
|
||||
/// loaded while Home is idle pays that instead. It is held until the first page has loaded, so that
|
||||
/// page starts against processes already up, or until the app backgrounds, memory runs low or a
|
||||
/// minute passes. It has its own non-persistent store and no bridge or scripts, so it shares no
|
||||
/// state with a page.
|
||||
enum MobileWebShellWarmUp {
|
||||
private static var view: WKWebView?
|
||||
private static var started = false
|
||||
private static var observers: [NSObjectProtocol] = []
|
||||
|
||||
static func start() {
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
// A launch into the background may never show a page, so it gets no web process.
|
||||
guard !started, UIApplication.shared.applicationState == .active else { return }
|
||||
started = true
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.websiteDataStore = .nonPersistent()
|
||||
let view = WKWebView(frame: .zero, configuration: configuration)
|
||||
view.loadHTMLString("", baseURL: nil)
|
||||
self.view = view
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { release() }
|
||||
observers = [
|
||||
UIApplication.didReceiveMemoryWarningNotification,
|
||||
UIApplication.didEnterBackgroundNotification
|
||||
].map { name in
|
||||
NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { _ in release() }
|
||||
}
|
||||
}
|
||||
|
||||
static func release() {
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
// A page that finished before the delayed start already has its processes, so start must no-op.
|
||||
started = true
|
||||
view = nil
|
||||
observers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
observers = []
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,14 @@ public class OrcaMobileWebShellModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("OrcaMobileWebShell")
|
||||
|
||||
// Home calls this once it has a paired host, so a user with nothing to open pays nothing.
|
||||
Function("warmUp") {
|
||||
// A second's delay keeps the warm-up off the launch's first frames.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
MobileWebShellWarmUp.start()
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("presentPdf") { (url: URL) in
|
||||
guard let presenter = self.appContext?.utilities?.currentViewController() else {
|
||||
throw PdfPresenterUnavailableException()
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core'
|
||||
import {
|
||||
requireNativeModule,
|
||||
requireOptionalNativeModule,
|
||||
requireNativeViewManager
|
||||
} from 'expo-modules-core'
|
||||
import type { ComponentType, RefAttributes } from 'react'
|
||||
import type { NativeSyntheticEvent, ViewProps } from 'react-native'
|
||||
import { Platform, type NativeSyntheticEvent, type ViewProps } from 'react-native'
|
||||
import type { MobileWebShellLoadStatePayload } from './load-state'
|
||||
|
||||
/** One raw JSON envelope, exactly as the page posted it. Parsing is the caller's. */
|
||||
@@ -66,6 +70,18 @@ export function saveImageToPhotos(uri: string, mime: string): Promise<void> {
|
||||
return requireNativeModule<ImageModule>('OrcaMobileWebShell').saveImageToPhotos(uri, mime)
|
||||
}
|
||||
|
||||
// iOS only: starts WebKit's helper processes ahead of the first page. Later calls are no-ops.
|
||||
export function warmUpWebShell(): void {
|
||||
if (Platform.OS !== 'ios') {
|
||||
return
|
||||
}
|
||||
// A binary built before warmUp existed has the module without the function.
|
||||
const shell = requireOptionalNativeModule<{ warmUp?: () => void }>('OrcaMobileWebShell')
|
||||
if (typeof shell?.warmUp === 'function') {
|
||||
shell.warmUp()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
MOBILE_WEB_SHELL_FAILURE_REASONS,
|
||||
parseMobileWebShellLoadState,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { useRouter, useFocusEffect } from 'expo-router'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { warmUpWebShell } from '../../modules/orca-mobile-web-shell/src'
|
||||
import { loadHomeSnapshot, saveHomeSnapshot } from '../cache/home-snapshot-cache'
|
||||
import type { AccountsSnapshot } from '../components/AccountUsage'
|
||||
import { getCachedWorktrees, setCachedWorktrees } from '../cache/worktree-cache'
|
||||
@@ -93,6 +94,13 @@ export function useMobileHomeData() {
|
||||
}
|
||||
}, [worktreeInfo])
|
||||
|
||||
const hasPairedHost = hosts.length > 0
|
||||
useEffect(() => {
|
||||
if (hasPairedHost) {
|
||||
warmUpWebShell()
|
||||
}
|
||||
}, [hasPairedHost])
|
||||
|
||||
useEffect(() => {
|
||||
allClientsRef.current = connections.allClients
|
||||
}, [connections.allClients])
|
||||
|
||||
Reference in New Issue
Block a user