mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
perf(mobile): defer unopened terminal document allocation (#34743)
* perf(mobile): defer terminal documents until first selection * test(mobile): address review findings on pane-gating test * test(mobile): use T[] array type in terminal document mount test --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import type { ParsedFileLinkLocation } from '../../../src/shared/file-link-location'
|
||||
import { TerminalWebView } from '../terminal/TerminalWebView'
|
||||
@@ -57,6 +57,10 @@ export function TerminalPaneView({
|
||||
onTextScaleChange,
|
||||
onCellBoxChange
|
||||
}: TerminalPaneViewProps) {
|
||||
const [visited, setVisited] = useState(active)
|
||||
if (active && !visited) {
|
||||
setVisited(true)
|
||||
}
|
||||
const setRef = useCallback(
|
||||
(ref: TerminalWebViewHandle | null) => {
|
||||
onRef(handle, ref)
|
||||
@@ -64,6 +68,10 @@ export function TerminalPaneView({
|
||||
[handle, onRef]
|
||||
)
|
||||
|
||||
if (!active && !visited) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
// Why: inactive terminal WebViews stay mounted to preserve xterm state,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { terminalDocumentDouble } from './document/document-terminal-double.test-support'
|
||||
import type { TerminalDocumentTerminal } from './document/document-terminal-shape'
|
||||
import type { TerminalWebViewCommand } from './terminal-webview-messages'
|
||||
import type { TerminalWebViewHandle } from './terminal-webview-contract'
|
||||
import { TerminalPaneView } from '../session/TerminalPaneView'
|
||||
import { TERMINAL_DOCUMENT_MARKUP } from './terminal-webview-html'
|
||||
import terminalNerdFontUrl from '../../assets/fonts/SymbolsNerdFontMono-Regular.woff2'
|
||||
|
||||
@@ -38,6 +40,7 @@ vi.mock('react-native', () => ({
|
||||
AppState: { addEventListener: () => ({ remove() {} }) }
|
||||
}))
|
||||
vi.mock('lucide-react-native', () => ({ RefreshCw: 'Icon' }))
|
||||
vi.mock('./TerminalWebView', () => import('./TerminalWebView.web'))
|
||||
|
||||
const { createTerminalDocument } = await import('./document/create-terminal-document')
|
||||
const { mountTerminalWebDocument } = await import('./terminal-web-document-mount')
|
||||
@@ -146,6 +149,114 @@ describe('a stopped document takes its engines with it', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal panes start when first selected', () => {
|
||||
it('starts only the selected document among several terminal tabs', () => {
|
||||
vi.useFakeTimers()
|
||||
const count = 3
|
||||
const refs = new Map<string, TerminalWebViewHandle>()
|
||||
const ready: string[] = []
|
||||
const engines: ReturnType<typeof terminalDocumentDouble>[] = []
|
||||
const replayed: string[] = []
|
||||
const addListener = vi.spyOn(document, 'addEventListener')
|
||||
const removeListener = vi.spyOn(document, 'removeEventListener')
|
||||
gridTerminal = () => {
|
||||
const engine = terminalDocumentDouble()
|
||||
engine.terminal.write = (data, callback) => {
|
||||
replayed.push(data)
|
||||
callback?.()
|
||||
}
|
||||
engines.push(engine)
|
||||
return engine.terminal
|
||||
}
|
||||
const noop = () => {}
|
||||
const onRef = (handle: string, ref: TerminalWebViewHandle | null) => {
|
||||
if (ref) {
|
||||
refs.set(handle, ref)
|
||||
} else {
|
||||
refs.delete(handle)
|
||||
}
|
||||
}
|
||||
const onWebReady = (handle: string) => {
|
||||
ready.push(handle)
|
||||
const ref = refs.get(handle)
|
||||
expect(ref).toBeDefined()
|
||||
void ref?.init({ ...INIT, initialData: `snapshot:${handle}`, frame: null })
|
||||
}
|
||||
const panes = (selected: number) =>
|
||||
Array.from({ length: count }, (_, index) =>
|
||||
createElement(TerminalPaneView, {
|
||||
key: index,
|
||||
handle: `terminal-${index}`,
|
||||
active: index === selected,
|
||||
keyboardLift: 0,
|
||||
textScale: 1,
|
||||
onRef,
|
||||
onWebReady,
|
||||
onTextScaleChange: noop,
|
||||
onCellBoxChange: noop,
|
||||
onSelectionMode: noop,
|
||||
onSelectionCopy: noop,
|
||||
onSelectionEvicted: noop,
|
||||
onClipboardWrite: noop,
|
||||
onModesChanged: noop,
|
||||
onKeyboardAvoidanceMetrics: noop,
|
||||
onHaptic: noop,
|
||||
onTerminalInput: noop,
|
||||
onTerminalQueryReply: noop,
|
||||
onTerminalTap: noop,
|
||||
onFileTap: noop,
|
||||
onOpenUrl: noop
|
||||
})
|
||||
)
|
||||
const documentListeners = () => addListener.mock.calls.length - removeListener.mock.calls.length
|
||||
try {
|
||||
act(() => {
|
||||
renderer = create(createElement('div', null, panes(0)), {
|
||||
createNodeMock: () => plantHost()
|
||||
})
|
||||
})
|
||||
expect(startedBuilds).toEqual([true])
|
||||
expect(ready).toEqual(['terminal-0'])
|
||||
expect(refs.size).toBe(1)
|
||||
expect(document.querySelectorAll(`.${HOST_CLASS}`)).toHaveLength(1)
|
||||
const perDocument = documentListeners()
|
||||
expect(perDocument).toBeGreaterThan(0)
|
||||
const firstHost = document.querySelector(`.${HOST_CLASS}`)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(32)
|
||||
})
|
||||
expect(replayed).toEqual(['\x1b[0msnapshot:terminal-0'])
|
||||
|
||||
act(() => renderer?.update(createElement('div', null, panes(1))))
|
||||
expect(startedBuilds).toEqual([true, true])
|
||||
expect(ready).toEqual(['terminal-0', 'terminal-1'])
|
||||
expect(refs.size).toBe(2)
|
||||
expect(documentListeners()).toBe(2 * perDocument)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(32)
|
||||
})
|
||||
expect(replayed).toEqual(['\x1b[0msnapshot:terminal-0', '\x1b[0msnapshot:terminal-1'])
|
||||
|
||||
act(() => renderer?.update(createElement('div', null, panes(0))))
|
||||
expect(document.querySelector(`.${HOST_CLASS}`)).toBe(firstHost)
|
||||
expect(startedBuilds).toEqual([true, true])
|
||||
expect(ready).toEqual(['terminal-0', 'terminal-1'])
|
||||
expect(engines.map((engine) => engine.disposals())).toEqual([0, 0])
|
||||
|
||||
act(() => renderer?.unmount())
|
||||
renderer = null
|
||||
expect(refs.size).toBe(0)
|
||||
expect(document.querySelectorAll(`.${HOST_CLASS}`)).toHaveLength(0)
|
||||
expect(documentListeners()).toBe(0)
|
||||
expect(engines.map((engine) => engine.disposals())).toEqual([1, 1])
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
addListener.mockRestore()
|
||||
removeListener.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('the page keeps its own capture buffer', () => {
|
||||
it('quotes the lines this document captured, and a second mount has none of them', () => {
|
||||
// Per-mount reporters append before quoting the buffer; embedded modules have no pre-engine head script.
|
||||
|
||||
Reference in New Issue
Block a user