mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
In an embedded tab the keyboard edits a mirror in the main process and the
shim replays each edit into the page. A differential harness
(tools/ime-test/shim-parity.mjs) now runs 57 interactions both natively in
Chromium and through the shim; these differed and are fixed:
- A rich editor's own re-rendering (Brainstorm's search chips) was never
reported back, so the next keystroke was diffed against stale text and
mangled the field ("since:2026-10-01:2026-10-01 x"). After each edit, and
on any later DOM change to a focused contenteditable, the shim hands the
host the field as it really is.
- Typing and Backspace never fired `beforeinput`, so input masks didn't
refuse characters and chip editors couldn't delete a chip. Each edit is
now announced cancelably first, with the caret where Chrome has it.
- `maxlength` was ignored (the shim writes through the value setter).
- Enter in a normal contenteditable inserted "\n", which renders as a
space; line breaks are now <br>, and the offset mapping counts them.
- Paste and cut from the host toolbar or a hardware keyboard reached the
page as typing, so its paste handler never ran. They now go to the page
as paste/cut (clipboard event, then insertFromPaste/deleteByCut).
- The keyboard always showed Go: Enter in a form's first field submitted
the form (Brainstorm's profile name raised a signing prompt). As in
Chrome, a single-line field with another after it in its form now gets
Next (moving focus), search fields get Search, the last field Go.
- Typing into a field the page had removed went nowhere with the keyboard
left up; the host now hears a blur.
Performance: the new offset mapping first made a 1,000-line
contenteditable quadratic (118 ms/keystroke); it is now one walk without a
JS node filter, shared across each edit's reads: 1.8 ms vs 2.9 ms on main
(0.6 vs 0.9 ms for 1,000 paragraphs). Caret moves no longer resend the
whole field text, and page writes that don't change the value are not
reported. tools/ime-test/shim-perf.mjs measures this.
Device-checked on the emulator with tools/ime-test/device.html in an
embedded tab and on Brainstorm (form Next/Go, chips, line breaks,
maxlength, toolbar copy/paste/cut, chat Shift+Enter/Enter, tap to move the
caret, tab switch, keyboard dismiss and reopen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
3.4 KiB
JavaScript
53 lines
3.4 KiB
JavaScript
// Per-keystroke cost of the embedded IME relay's page side: replays 200 host `ime.set` keystrokes into the REAL
|
|
// shim on long fields and reports the time each takes to apply (p50/p95/max, ms). A long contenteditable is the
|
|
// case to watch: every keystroke maps offsets through a walk of the field.
|
|
//
|
|
// cd tools/ime-test && npm i playwright-core && node shim-perf.mjs (SHIM=<path> to compare another shim)
|
|
import { chromium } from 'playwright-core'
|
|
import { readFileSync } from 'node:fs'
|
|
const SHIM = process.env.SHIM ?? '../../commonsUI/src/commonMain/composeResources/files/napplet/shim.js'
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH })
|
|
const cases = {
|
|
'input, short': ['<input id="f">', 0],
|
|
'textarea 20k chars': ['<textarea id="f"></textarea>', 20000],
|
|
'contenteditable 1000 lines': ['<div id="f" contenteditable="true"></div>', -1000],
|
|
'contenteditable 1000 paragraphs': ['<div id="f" contenteditable="true"></div>', -1001],
|
|
'page rewrites value on input': ['<input id="f"><script>var f=document.getElementById("f");f.addEventListener("input",function(){f.value=f.value})</script>', 0],
|
|
}
|
|
for (const [name, [html, fill]] of Object.entries(cases)) {
|
|
const ctx = await browser.newContext()
|
|
await ctx.addInitScript(() => { window.__sent = []; window.__nappletDirectBridge = true; window.__nappletImeProxy = true; window.__nappletBridge = { postMessage(s) { window.__sent.push(s) }, set onmessage(fn) { window.__imeIn = fn }, get onmessage() { return window.__imeIn } } })
|
|
await ctx.addInitScript({ content: readFileSync(SHIM, 'utf8') })
|
|
await ctx.route('https://ime.test/**', (r) => r.fulfill({ contentType: 'text/html', body: `<body>${html}</body>` }))
|
|
const p = await ctx.newPage(); await p.goto('https://ime.test/')
|
|
const r = await p.evaluate(async (fill) => {
|
|
const f = document.getElementById('f')
|
|
if (fill > 0) { f.value = 'lorem ipsum dolor sit amet '.repeat(fill / 27 + 1).slice(0, fill) }
|
|
if (fill === -1000) f.innerHTML = Array.from({ length: 1000 }, (_, i) => 'line number ' + i).join('<br>')
|
|
if (fill === -1001) f.innerHTML = Array.from({ length: 1000 }, (_, i) => '<div>line number ' + i + '</div>').join('')
|
|
f.focus()
|
|
if (!f.isContentEditable) f.setSelectionRange(f.value.length, f.value.length)
|
|
else { const s = getSelection(); s.selectAllChildren(f); s.collapseToEnd() }
|
|
await new Promise((r) => setTimeout(r, 50))
|
|
const focusMsg = window.__sent.map((x) => JSON.parse(x)).filter((m) => m.type === 'ime.focus').pop()
|
|
window.__sent.length = 0
|
|
const handle = window.__nappletImeHandle
|
|
// host text as the shim reported it on focus
|
|
let text = (f.isContentEditable ? null : f.value)
|
|
if (text === null) { const st = window.__sent; text = null }
|
|
const times = []
|
|
let host = f.isContentEditable ? null : f.value
|
|
for (let i = 0; i < 200; i++) {
|
|
if (host === null) host = focusMsg.text
|
|
host = host + 'x'
|
|
const msg = { type: 'ime.set', text: host, selStart: host.length, selEnd: host.length, composingStart: -1, composingEnd: -1 }
|
|
const t0 = performance.now(); handle(msg); await Promise.resolve(); times.push(performance.now() - t0)
|
|
}
|
|
times.sort((a, b) => a - b)
|
|
return { p50: times[100].toFixed(3), p95: times[190].toFixed(3), max: times[199].toFixed(3), reports: window.__sent.filter((s) => s.includes('ime.state')).length }
|
|
}, fill)
|
|
console.log(name.padEnd(32), JSON.stringify(r))
|
|
await ctx.close()
|
|
}
|
|
await browser.close()
|