mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
perf(i18n): skip a whole mutation inside a skipped surface, not each node
xterm's DOM renderer replaces terminal rows every frame (the split pane's Pane B, every tile of the grid), and the translator's MutationObserver walked each added row and ran closest(SKIP_SELECTOR) for every text node and element in it, only to find each one inside .xterm and skip it. A CPU profile of six printing tiles put about 2.2 s of 40 s there (closest, translateNode, tree walks). Now one shouldSkip(mutation.target) per record decides it: every node a record adds or edits sits under that target, so both translators would return on their own closest() check anyway, and the output is identical. Measured in headless Chromium, six tiles printing 20 lines/s each, three interleaved 40 s pairs at the same machine load: frames over 20 ms fell from 9.1/11.5/11.8% to 7.3/8.1/8.0%; nine tiles 9.5% to 7.2%. Pinned in i18n-branding.test: a burst of terminal rows causes no tree walk, terminal text stays untranslated, application DOM beside it still translates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1128,6 +1128,13 @@
|
|||||||
observer = new MutationObserver((mutations) => {
|
observer = new MutationObserver((mutations) => {
|
||||||
if (applying) return;
|
if (applying) return;
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
|
// A change inside a skipped surface cannot need translating: every
|
||||||
|
// node it adds or edits sits under the same skip ancestor, so both
|
||||||
|
// translators would return on their own closest() check anyway. One
|
||||||
|
// check per record instead of one per text node and attribute matters
|
||||||
|
// for xterm's DOM renderer, which replaces rows every frame (the split
|
||||||
|
// pane, every tile of the grid).
|
||||||
|
if (shouldSkip(mutation.target)) continue;
|
||||||
if (mutation.type === 'characterData') translateNode(mutation.target);
|
if (mutation.type === 'characterData') translateNode(mutation.target);
|
||||||
if (mutation.type === 'attributes') translateAttributes(mutation.target);
|
if (mutation.type === 'attributes') translateAttributes(mutation.target);
|
||||||
for (const added of mutation.addedNodes) translateNode(added);
|
for (const added of mutation.addedNodes) translateNode(added);
|
||||||
|
|||||||
@@ -91,6 +91,47 @@ describe('custom display name and browser localization', () => {
|
|||||||
dom.window.close();
|
dom.window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not walk what a terminal renders: one check per mutation inside a skipped surface', async () => {
|
||||||
|
const dom = makeDom('<div class="xterm"><div class="xterm-rows" id="rows"></div></div><div id="app"></div>');
|
||||||
|
const { window } = dom;
|
||||||
|
const api = window.CodemanI18n;
|
||||||
|
api.start();
|
||||||
|
api.configure({ language: 'zh-CN' });
|
||||||
|
// i18n.js also starts itself on DOMContentLoaded (one walk of the body).
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
let walks = 0;
|
||||||
|
const createTreeWalker = window.document.createTreeWalker.bind(window.document);
|
||||||
|
window.document.createTreeWalker = (...args: Parameters<Document['createTreeWalker']>) => {
|
||||||
|
walks++;
|
||||||
|
return createTreeWalker(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// What xterm's DOM renderer does every frame: replace the rows.
|
||||||
|
const rows = window.document.getElementById('rows')!;
|
||||||
|
for (let frame = 0; frame < 3; frame++) {
|
||||||
|
const fresh = Array.from({ length: 20 }, () => {
|
||||||
|
const row = window.document.createElement('div');
|
||||||
|
row.innerHTML = '<span>Settings saved</span><span title="Run">Run</span>';
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
rows.replaceChildren(...fresh);
|
||||||
|
}
|
||||||
|
rows.firstElementChild!.firstChild!.firstChild!.nodeValue = 'Settings saved';
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
expect(walks).toBe(0);
|
||||||
|
expect(rows.firstElementChild?.textContent).toBe('Settings savedRun');
|
||||||
|
expect(rows.querySelector('[title]')?.getAttribute('title')).toBe('Run');
|
||||||
|
|
||||||
|
// Application DOM next to it is still translated.
|
||||||
|
const button = window.document.createElement('button');
|
||||||
|
button.textContent = 'Settings saved';
|
||||||
|
window.document.getElementById('app')!.appendChild(button);
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
expect(button.textContent).toBe('设置已保存');
|
||||||
|
expect(walks).toBe(1);
|
||||||
|
dom.window.close();
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps a quoted group name apart from the fixed "Move to" entries in zh-CN', () => {
|
it('keeps a quoted group name apart from the fixed "Move to" entries in zh-CN', () => {
|
||||||
const dom = makeDom('');
|
const dom = makeDom('');
|
||||||
const api = dom.window.CodemanI18n;
|
const api = dom.window.CodemanI18n;
|
||||||
|
|||||||
Reference in New Issue
Block a user