mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
perf(mobile): avoid materializing input characters on backspace (#20220)
Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { build } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <baseline-ref>:mobile/src/terminal/terminal-live-text-commit.ts | node config/scripts/mobile-backspace-benchmark.mjs
|
||||
const target = resolve('mobile/src/terminal/terminal-live-text-commit.ts')
|
||||
async function load(source) {
|
||||
const result = await build({
|
||||
stdin: { contents: source, loader: 'ts', resolveDir: dirname(target) },
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: 'node',
|
||||
format: 'esm'
|
||||
})
|
||||
return (
|
||||
await import(
|
||||
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
).getTerminalLiveAccessoryLocalEditText
|
||||
}
|
||||
const baseline = readFileSync(0, 'utf8')
|
||||
assert.ok(
|
||||
baseline.includes('function getTerminalLiveAccessoryLocalEditText'),
|
||||
'Pipe baseline source into stdin'
|
||||
)
|
||||
const implementations = {
|
||||
before: await load(baseline),
|
||||
after: await load(readFileSync(target, 'utf8'))
|
||||
}
|
||||
const tokens = [
|
||||
'',
|
||||
'a',
|
||||
'\u0000',
|
||||
'\r',
|
||||
'\n',
|
||||
'한',
|
||||
'\u0301',
|
||||
'\u200d',
|
||||
'🙂',
|
||||
'\ud800',
|
||||
'\udbff',
|
||||
'\udc00',
|
||||
'\udfff'
|
||||
]
|
||||
let cases = 0
|
||||
for (const first of tokens) {
|
||||
for (const second of tokens) {
|
||||
for (const third of tokens) {
|
||||
for (const localEdit of ['backspace', 'delete']) {
|
||||
const input = { fieldText: first + second + third, localEdit }
|
||||
assert.equal(
|
||||
implementations.after(input),
|
||||
implementations.before(input),
|
||||
JSON.stringify(input)
|
||||
)
|
||||
cases += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = []
|
||||
for (const inputBytes of [32, 4096, 65_536, 262_144]) {
|
||||
for (const glyph of ['a', '🙂']) {
|
||||
const fieldText = glyph.repeat(inputBytes / Buffer.byteLength(glyph))
|
||||
const input = { localEdit: 'backspace', fieldText }
|
||||
const expected = implementations.before(input)
|
||||
assert.equal(implementations.after(input), expected)
|
||||
const iterations = Math.max(10, Math.floor(1_000_000 / inputBytes))
|
||||
for (let warmup = 0; warmup < 100; warmup += 1) {
|
||||
implementations.before(input)
|
||||
implementations.after(input)
|
||||
}
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
let actual
|
||||
const started = performance.now()
|
||||
for (let repeat = 0; repeat < iterations; repeat += 1) {
|
||||
actual = implementations[arm](input)
|
||||
}
|
||||
samples[arm].push(performance.now() - started)
|
||||
assert.equal(actual, expected)
|
||||
}
|
||||
}
|
||||
const means = Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
)
|
||||
results.push({
|
||||
inputBytes,
|
||||
glyph,
|
||||
iterations,
|
||||
meanMicrosecondsPerCall: means,
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialCases: cases, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
getTerminalLiveAccessoryBytesDecision,
|
||||
getTerminalLiveAccessoryLocalEditText,
|
||||
@@ -101,6 +101,50 @@ describe('terminal live accessory bytes decision', () => {
|
||||
})
|
||||
|
||||
describe('terminal live accessory local edit text', () => {
|
||||
it.each([
|
||||
['', ''],
|
||||
['a', ''],
|
||||
['a🙂', 'a'],
|
||||
['🙂a', '🙂'],
|
||||
['🙂🙂', '🙂'],
|
||||
['a\ud800', 'a'],
|
||||
['a\udc00', 'a'],
|
||||
['\ud800\ud800', '\ud800'],
|
||||
['\udc00\ud800', '\udc00'],
|
||||
['e\u0301', 'e'],
|
||||
['👩💻', '👩'],
|
||||
['\r\n', '\r']
|
||||
])('preserves code-point deletion for %j', (fieldText, expected) => {
|
||||
expect(getTerminalLiveAccessoryLocalEditText({ localEdit: 'backspace', fieldText })).toBe(
|
||||
expected
|
||||
)
|
||||
expect(getTerminalLiveAccessoryLocalEditText({ localEdit: 'delete', fieldText })).toBe(
|
||||
fieldText
|
||||
)
|
||||
})
|
||||
|
||||
it('does not iterate the input prefix to delete the final code point', () => {
|
||||
const fieldText = `${'a'.repeat(100_000)}🙂`
|
||||
const originalIterator = String.prototype[Symbol.iterator]
|
||||
let visits = 0
|
||||
const iterator = vi
|
||||
.spyOn(String.prototype, Symbol.iterator)
|
||||
.mockImplementation(function* (this: string) {
|
||||
for (const codePoint of originalIterator.call(this)) {
|
||||
visits += 1
|
||||
yield codePoint
|
||||
}
|
||||
})
|
||||
let result: string
|
||||
try {
|
||||
result = getTerminalLiveAccessoryLocalEditText({ localEdit: 'backspace', fieldText })
|
||||
} finally {
|
||||
iterator.mockRestore()
|
||||
}
|
||||
expect(result).toBe('a'.repeat(100_000))
|
||||
expect(visits).toBeLessThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('Given backspace Then drops the last code point of the field text', () => {
|
||||
expect(
|
||||
getTerminalLiveAccessoryLocalEditText({ localEdit: 'backspace', fieldText: '한글' })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getTerminalLiveSpecialKeyBytes } from './terminal-live-input'
|
||||
import { readUtf8CodePointAt } from '../../../src/shared/utf8-byte-limits'
|
||||
|
||||
export type TerminalLiveSpecialKeyDecision =
|
||||
| { readonly kind: 'ignore' }
|
||||
@@ -79,5 +80,7 @@ export function getTerminalLiveAccessoryLocalEditText({
|
||||
return fieldText
|
||||
}
|
||||
|
||||
return Array.from(fieldText).slice(0, -1).join('')
|
||||
const end = fieldText.length
|
||||
const width = end > 1 && readUtf8CodePointAt(fieldText, end - 2) > 0xffff ? 2 : 1
|
||||
return fieldText.slice(0, -width)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user