mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Speed up Mobile Checks by overlapping typechecks and tests (#28127)
Overlap mobile typechecks with tests and avoid repeated filesystem probes and unused device substitutes in recording replays. Keep state isolated between loaders, give exhaustive snapshot cases an explicit execution deadline, and retire obsolete locale keys that blocked repository validation.
This commit is contained in:
@@ -135,10 +135,10 @@ jobs:
|
||||
# drifted. This fails when a test file that checks today stops checking, when a test leaves
|
||||
# the program, and on @ts-nocheck; the baseline may only shrink.
|
||||
- name: Typecheck tests (ratchet)
|
||||
id: test-types
|
||||
background: true
|
||||
run: node scripts/check-tests-typecheck-ratchet.mjs
|
||||
|
||||
- wait: production-types
|
||||
|
||||
# This includes the bridged replay of the whole recording corpus, which used to be a second
|
||||
# step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point:
|
||||
# it fails when a divergence class grows, when a divergence lands in no class at all, or when
|
||||
@@ -168,6 +168,9 @@ jobs:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
run: node ../config/scripts/run-mobile-ci-tests.mjs
|
||||
|
||||
# All checks read the completed install; compilers need not hold up test workers.
|
||||
- wait: [production-types, test-types]
|
||||
|
||||
# Reports, never gates: the goldens are content-addressed JSON, so their raw diff is hashes.
|
||||
# This decodes which recorded behaviour the pull request moves into the job summary, and runs
|
||||
# after a red Test too, when a reviewer most wants it.
|
||||
|
||||
@@ -79,3 +79,29 @@ it('skips only tests, after successful detection, and retains all other mobile g
|
||||
expect(steps.find((step) => step.name === name).if).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('overlaps both typechecks with tests and collects their failures before the summary', () => {
|
||||
const workflow = parse(
|
||||
readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')
|
||||
)
|
||||
const steps = workflow.jobs.verify.steps
|
||||
const install = steps.findIndex((step) => step.name === 'Install dependencies')
|
||||
const test = steps.findIndex((step) => step.name === 'Test')
|
||||
const summary = steps.findIndex((step) => step.name === 'Summarize RPC recording changes')
|
||||
const waits = steps.filter((step) => step.wait !== undefined)
|
||||
expect(waits).toHaveLength(1)
|
||||
const [wait] = waits
|
||||
expect(wait.wait).toEqual(['production-types', 'test-types'])
|
||||
expect(wait.if).toBeUndefined()
|
||||
expect(steps.indexOf(wait)).toBeGreaterThan(test)
|
||||
expect(steps.indexOf(wait)).toBeLessThan(summary)
|
||||
for (const id of wait.wait) {
|
||||
const step = steps.find((candidate) => candidate.id === id)
|
||||
expect(step.background).toBe(true)
|
||||
expect(step['continue-on-error']).toBeUndefined()
|
||||
expect(step.if).toBeUndefined()
|
||||
expect(steps.indexOf(step)).toBeGreaterThan(install)
|
||||
expect(steps.indexOf(step)).toBeLessThan(test)
|
||||
expect(step.run).toMatch(/^node /)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -221,11 +221,16 @@ describe.each(['after-end', 'during-turn'] as const)(
|
||||
])
|
||||
})
|
||||
|
||||
it.each([1, 2, 3])('from every subscribe point, %i row(s) per frame', (rowsPerFrame) => {
|
||||
expect(compareFrames(journal, range(2, journal.rows.length), rowsPerFrame).differing).toEqual(
|
||||
[]
|
||||
)
|
||||
})
|
||||
// Exhaustive frame enumeration shares CI CPUs with the typechecks.
|
||||
it.each([1, 2, 3])(
|
||||
'from every subscribe point, %i row(s) per frame',
|
||||
(rowsPerFrame) => {
|
||||
expect(
|
||||
compareFrames(journal, range(2, journal.rows.length), rowsPerFrame).differing
|
||||
).toEqual([])
|
||||
},
|
||||
10_000
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'
|
||||
import type * as FsModule from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { nativeMountingSubstitutes } from './native-mounting-substitutes'
|
||||
import type * as NativeSubstitutesModule from './native-mounting-substitutes'
|
||||
import { operationModuleLoader } from './operation-module-loader'
|
||||
|
||||
vi.mock('node:fs', async (original) => {
|
||||
const fs = await original<typeof FsModule>()
|
||||
return { ...fs, existsSync: vi.fn(fs.existsSync) }
|
||||
})
|
||||
|
||||
vi.mock('./native-mounting-substitutes', async (original) => {
|
||||
const module = await original<typeof NativeSubstitutesModule>()
|
||||
return { ...module, nativeMountingSubstitutes: vi.fn(module.nativeMountingSubstitutes) }
|
||||
})
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
function sourceTree(files: Record<string, string>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'rpc-loader-work-budget-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'mobile', 'src'), { recursive: true })
|
||||
for (const [name, source] of Object.entries(files)) {
|
||||
writeFileSync(join(root, 'mobile', 'src', name), source)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('resolves a relative import once instead of probing disk on every exported member read', () => {
|
||||
const root = sourceTree({
|
||||
'value.ts': 'export const value = 42',
|
||||
'operation.ts': `import { value } from './value'; export const read = () => value`
|
||||
})
|
||||
const operation = operationModuleLoader(root).load<{ read: () => number }>(
|
||||
'mobile/src/operation.ts'
|
||||
)
|
||||
expect(operation.read()).toBe(42)
|
||||
vi.mocked(existsSync).mockClear()
|
||||
for (let index = 0; index < 1_000; index++) {
|
||||
expect(operation.read()).toBe(42)
|
||||
}
|
||||
expect(existsSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('builds device substitutes only for a used loader and keeps their state per loader', async () => {
|
||||
const root = sourceTree({
|
||||
'operation.ts': `
|
||||
import * as clipboard from 'expo-clipboard'
|
||||
export const read = () => clipboard.getStringAsync()
|
||||
export const write = () => clipboard.setStringAsync('first recording')
|
||||
`
|
||||
})
|
||||
const loaders = Array.from({ length: 64 }, () => operationModuleLoader(root))
|
||||
expect(nativeMountingSubstitutes).not.toHaveBeenCalled()
|
||||
type ClipboardOperation = { read: () => Promise<string>; write: () => Promise<void> }
|
||||
const first = loaders[0].load<ClipboardOperation>('mobile/src/operation.ts')
|
||||
await first.write()
|
||||
expect(await first.read()).toBe('first recording')
|
||||
expect(nativeMountingSubstitutes).toHaveBeenCalledTimes(1)
|
||||
const second = loaders[1].load<ClipboardOperation>('mobile/src/operation.ts')
|
||||
expect(await second.read()).toBe('')
|
||||
expect(await first.read()).toBe('first recording')
|
||||
expect(nativeMountingSubstitutes).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
@@ -32,7 +32,7 @@ export function operationModuleLoader(
|
||||
declared: ReadonlyMap<string, unknown> = new Map()
|
||||
) {
|
||||
const cache = new Map<string, OperationModule>()
|
||||
const natives = new Map([...nativeMountingSubstitutes(), ...declared])
|
||||
let natives: Map<string, unknown> | undefined
|
||||
const sharedModulePath = resolve(root, SHARED_MODULE)
|
||||
let mutationCount = 0
|
||||
function pathFor(base: string): string {
|
||||
@@ -45,13 +45,12 @@ export function operationModuleLoader(
|
||||
return file
|
||||
}
|
||||
function imported(base: string, name: string): unknown {
|
||||
// Most registered adapters are never mounted in this recording.
|
||||
natives ??= new Map([...nativeMountingSubstitutes(), ...declared])
|
||||
const native = natives.get(name)
|
||||
if (native !== undefined) {
|
||||
return native
|
||||
}
|
||||
if (name.startsWith('.') && pathFor(resolve(dirname(base), name)) === sharedModulePath) {
|
||||
return deliveryAmbiguity
|
||||
}
|
||||
if (!name.startsWith('.')) {
|
||||
return new Proxy(
|
||||
{},
|
||||
@@ -69,10 +68,11 @@ export function operationModuleLoader(
|
||||
}
|
||||
)
|
||||
}
|
||||
return new Proxy(
|
||||
{},
|
||||
{ get: (_target, key) => load(pathFor(resolve(dirname(base), name)))[String(key)] }
|
||||
)
|
||||
const file = pathFor(resolve(dirname(base), name))
|
||||
if (file === sharedModulePath) {
|
||||
return deliveryAmbiguity
|
||||
}
|
||||
return new Proxy({}, { get: (_target, key) => load(file)[String(key)] })
|
||||
}
|
||||
function barrel(file: string, source: string): OperationModule {
|
||||
const parsed = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
|
||||
|
||||
Reference in New Issue
Block a user