mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.
This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.
174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.
Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
- useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
returns undefined there, so every Tamagui Dialog kills a prerender — the
exact crash hanzo.app spent a day on.
- data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.
Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.
Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.
Co-Authored-By: Hanzo Dev <[email protected]>
305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
import { setupPage } from './test-utils'
|
|
|
|
// layout poll interval is ~233ms (14 frames)
|
|
const POLL_WAIT = 500
|
|
// CI environments are slower - give more time for layouts to settle
|
|
const INITIAL_SETTLE_TIME = process.env.CI ? 3000 : 1500
|
|
|
|
// gating thresholds - fail if exceeded
|
|
const MAX_IO_DELAY_MS = 100 // fail if IO callback takes > 100ms
|
|
const MAX_WARNINGS_PER_TEST = 3 // fail if too many warnings
|
|
|
|
async function collectConsoleWarnings(
|
|
page: Page
|
|
): Promise<{ count: number; avgDelay: number; maxDelay: number }> {
|
|
const result = await page.evaluate(() => {
|
|
const warnings = (window as any).__ioDelayWarnings || []
|
|
if (warnings.length === 0) return { count: 0, avgDelay: 0, maxDelay: 0 }
|
|
const sum = warnings.reduce((a: number, b: number) => a + b, 0)
|
|
const max = Math.max(...warnings)
|
|
return {
|
|
count: warnings.length,
|
|
avgDelay: Math.round(sum / warnings.length),
|
|
maxDelay: max,
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
async function getStats(
|
|
page: Page
|
|
): Promise<{ total: number; lastBatch: number; lastTime: number; max: number }> {
|
|
const total = await page.getByTestId('stat-total').innerText()
|
|
const lastBatch = await page.getByTestId('stat-last-batch').innerText()
|
|
const lastTime = await page.getByTestId('stat-last-time').innerText()
|
|
const max = await page.getByTestId('stat-max').innerText()
|
|
|
|
return {
|
|
total: parseInt(total.replace('total: ', ''), 10) || 0,
|
|
lastBatch: parseInt(lastBatch.replace('lastBatch: ', ''), 10) || 0,
|
|
lastTime: parseInt(lastTime.replace('lastTime: ', ''), 10) || 0,
|
|
max: parseInt(max.replace('max: ', '').replace('ms', ''), 10) || 0,
|
|
}
|
|
}
|
|
|
|
test.describe('onLayout stress test', () => {
|
|
// stress tests are flaky in CI - give them extra retries
|
|
test.describe.configure({ retries: 3 })
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// set up console warning tracking
|
|
// BUG FIX: console.warn('[onLayout-io-delay]', delay + 'ms', ...) puts delay in args[1]
|
|
await page.addInitScript(() => {
|
|
;(window as any).__ioDelayWarnings = []
|
|
const originalWarn = console.warn
|
|
console.warn = (...args: any[]) => {
|
|
const prefix = args[0]
|
|
const delayArg = args[1] // delay is in second argument
|
|
if (typeof prefix === 'string' && prefix.includes('[onLayout-io-delay]')) {
|
|
if (typeof delayArg === 'string') {
|
|
const match = delayArg.match(/(\d+)ms/)
|
|
if (match) {
|
|
;(window as any).__ioDelayWarnings.push(parseInt(match[1], 10))
|
|
}
|
|
}
|
|
}
|
|
originalWarn.apply(console, args)
|
|
}
|
|
})
|
|
|
|
await setupPage(page, { name: 'OnLayoutStressCase', type: 'useCase' })
|
|
})
|
|
|
|
test('initial render should not exceed threshold', async ({ page }) => {
|
|
// wait for initial layout callbacks to settle
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== INITIAL RENDER BENCHMARK ===')
|
|
console.log(`total callbacks: ${stats.total}`)
|
|
console.log(`last batch size: ${stats.lastBatch}`)
|
|
console.log(`max batch time: ${stats.max}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
if (warnings.count > 0) {
|
|
console.log(`avg IO delay: ${warnings.avgDelay}ms`)
|
|
console.log(`max IO delay: ${warnings.maxDelay}ms`)
|
|
}
|
|
|
|
// gating assertions
|
|
expect(stats.total, 'should have onLayout callbacks').toBeGreaterThan(50)
|
|
expect(warnings.maxDelay, `IO delay should be < ${MAX_IO_DELAY_MS}ms`).toBeLessThan(
|
|
MAX_IO_DELAY_MS
|
|
)
|
|
})
|
|
|
|
test('resize should trigger batch updates efficiently', async ({ page }) => {
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
|
|
// scroll sibling section into view — onLayout only fires for visible elements
|
|
await page.getByTestId('section-siblings').scrollIntoViewIfNeeded()
|
|
await page.waitForTimeout(POLL_WAIT)
|
|
|
|
// reset stats after initial render
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(200)
|
|
|
|
// trigger width resize
|
|
await page.getByTestId('btn-resize-width').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== WIDTH RESIZE BENCHMARK ===')
|
|
console.log(`callbacks after resize: ${stats.total}`)
|
|
console.log(`last batch size: ${stats.lastBatch}`)
|
|
console.log(`last batch time: ${stats.lastTime}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
|
|
// gating assertions
|
|
expect(stats.total, 'resize should trigger callbacks').toBeGreaterThan(0)
|
|
expect(
|
|
warnings.count,
|
|
`should have fewer than ${MAX_WARNINGS_PER_TEST} warnings`
|
|
).toBeLessThan(MAX_WARNINGS_PER_TEST)
|
|
})
|
|
|
|
test('grid resize should trigger efficient updates', async ({ page }) => {
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
|
|
await page.getByTestId('btn-resize-grid').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== GRID RESIZE BENCHMARK ===')
|
|
console.log(`callbacks: ${stats.total}`)
|
|
console.log(`batch time: ${stats.lastTime}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
|
|
// gating assertions - 40 grid items should trigger ~40 callbacks
|
|
expect(stats.total, 'grid resize should trigger callbacks').toBeGreaterThan(20)
|
|
expect(warnings.maxDelay, `IO delay should be < ${MAX_IO_DELAY_MS}ms`).toBeLessThan(
|
|
MAX_IO_DELAY_MS
|
|
)
|
|
})
|
|
|
|
test('list expand should trigger efficient updates', async ({ page }) => {
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
|
|
await page.getByTestId('btn-toggle-expand').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== LIST EXPAND BENCHMARK ===')
|
|
console.log(`callbacks: ${stats.total}`)
|
|
console.log(`batch time: ${stats.lastTime}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
|
|
// gating assertions - 20 list items should trigger ~20 callbacks
|
|
expect(stats.total, 'list expand should trigger callbacks').toBeGreaterThan(10)
|
|
expect(warnings.maxDelay, `IO delay should be < ${MAX_IO_DELAY_MS}ms`).toBeLessThan(
|
|
MAX_IO_DELAY_MS
|
|
)
|
|
})
|
|
|
|
test('container resize affects all children', async ({ page }) => {
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
|
|
await page.getByTestId('btn-resize-container').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== CONTAINER RESIZE BENCHMARK ===')
|
|
console.log(`callbacks: ${stats.total}`)
|
|
console.log(`batch time: ${stats.lastTime}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
|
|
// gating assertion
|
|
expect(warnings.maxDelay, `IO delay should be < ${MAX_IO_DELAY_MS}ms`).toBeLessThan(
|
|
MAX_IO_DELAY_MS
|
|
)
|
|
})
|
|
|
|
test('rapid successive resizes', async ({ page }) => {
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
|
|
// rapid fire multiple resize operations
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.getByTestId('btn-resize-width').click()
|
|
await page.waitForTimeout(50)
|
|
await page.getByTestId('btn-resize-grid').click()
|
|
await page.waitForTimeout(50)
|
|
}
|
|
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
|
|
const stats = await getStats(page)
|
|
const warnings = await collectConsoleWarnings(page)
|
|
|
|
console.log('=== RAPID RESIZE BENCHMARK ===')
|
|
console.log(`total callbacks: ${stats.total}`)
|
|
console.log(`max batch time: ${stats.max}ms`)
|
|
console.log(`IO delay warnings: ${warnings.count}`)
|
|
if (warnings.count > 0) {
|
|
console.log(`avg IO delay: ${warnings.avgDelay}ms`)
|
|
console.log(`max IO delay: ${warnings.maxDelay}ms`)
|
|
}
|
|
|
|
// gating assertion - rapid resize is stressful but shouldn't explode
|
|
expect(
|
|
warnings.maxDelay,
|
|
`IO delay should be < ${MAX_IO_DELAY_MS}ms even under stress`
|
|
).toBeLessThan(MAX_IO_DELAY_MS)
|
|
})
|
|
|
|
test('full benchmark suite', async ({ page }) => {
|
|
const results: Record<string, any> = {}
|
|
|
|
// initial render
|
|
await page.waitForTimeout(INITIAL_SETTLE_TIME)
|
|
results.initial = {
|
|
...(await getStats(page)),
|
|
warnings: await collectConsoleWarnings(page),
|
|
}
|
|
|
|
// width resize
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
await page.getByTestId('btn-resize-width').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
results.widthResize = {
|
|
...(await getStats(page)),
|
|
warnings: await collectConsoleWarnings(page),
|
|
}
|
|
|
|
// grid resize
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
await page.getByTestId('btn-resize-grid').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
results.gridResize = {
|
|
...(await getStats(page)),
|
|
warnings: await collectConsoleWarnings(page),
|
|
}
|
|
|
|
// expand list
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
await page.getByTestId('btn-toggle-expand').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
results.listExpand = {
|
|
...(await getStats(page)),
|
|
warnings: await collectConsoleWarnings(page),
|
|
}
|
|
|
|
// container resize
|
|
await page.getByTestId('btn-reset-stats').click()
|
|
await page.waitForTimeout(100)
|
|
await page.getByTestId('btn-resize-container').click()
|
|
await page.waitForTimeout(POLL_WAIT * 3)
|
|
results.containerResize = {
|
|
...(await getStats(page)),
|
|
warnings: await collectConsoleWarnings(page),
|
|
}
|
|
|
|
console.log('\n========== FULL BENCHMARK RESULTS ==========')
|
|
console.log(JSON.stringify(results, null, 2))
|
|
console.log('=============================================\n')
|
|
|
|
// calculate aggregate metrics
|
|
const allMaxDelays = Object.values(results).map((r: any) => r.warnings?.maxDelay || 0)
|
|
const overallMaxDelay = Math.max(...allMaxDelays)
|
|
const totalWarnings = Object.values(results).reduce(
|
|
(sum: number, r: any) => sum + (r.warnings?.count || 0),
|
|
0
|
|
)
|
|
|
|
console.log(
|
|
`AGGREGATE: ${totalWarnings} total warnings, ${overallMaxDelay}ms max IO delay`
|
|
)
|
|
|
|
// gating assertions for the full suite
|
|
expect(
|
|
overallMaxDelay,
|
|
`max IO delay across all tests should be < ${MAX_IO_DELAY_MS}ms`
|
|
).toBeLessThan(MAX_IO_DELAY_MS)
|
|
})
|
|
})
|