mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
perf(i18n): an xterm row record is decided by its rows container, once
xterm's DOM renderer rewrites its rows (`.xterm-rows > div`) on every frame a pane changes, and with a grid of tiles that is every record the i18n observer gets (about 4,600 a second for six printing tiles, all of them row rewrites). Each paid one closest() over the whole skip selector list (the per-record skip of21beacf7, which stays). Every row of one terminal shares its parent, so that parent's own shouldSkip() verdict is now kept once it says skip: same verdict, one closest() per terminal instead of one per record. A rows container outside any skipped surface keeps the full check, so nothing that was translated stops being translated. Measured (6 printing tiles, 30 s profiles, n=3 interleaved A/B, load 5.8 to 9.5, equivalent class-check variant): observer 391 to 445 ms -> 47 to 52 ms per 30 s in English, 473 -> 58 ms in zh-CN; main-thread script time -0.35 s per 30 s (-14%). Frame share unchanged within noise. Live, this code (6 printing tiles, 30 s profiles, interleaved against the file at00440c02, load 4.4 to 6.6): observer 417 to 430 ms -> 56 to 58 ms per 30 s; script time 2.63 to 2.79 s -> 2.24 s in the undisturbed window. The other window of this code was disturbed by CPU contention on the box (every rendering cost 3 to 4 times higher, xterm's own included, 351 frames in 30 s) and is not counted; its observer time was 56 ms all the same. Test: 60 row rewrites in a terminal cost one closest(), the rows stay untranslated, and a stray `.xterm-rows` outside any skip surface is still translated (the verdict closest() gives); mutation-checked both ways. Scope: PR 2 (i18n, on top of21beacf7). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -1142,6 +1142,23 @@
|
|||||||
return !element || Boolean(element.closest(SKIP_SELECTOR));
|
return !element || Boolean(element.closest(SKIP_SELECTOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xterm's DOM renderer rewrites its rows (`.xterm-rows > div`) on every frame
|
||||||
|
// a pane changes: thousands of mutation records a second with a grid of tiles,
|
||||||
|
// each paying a closest() over the whole skip list. All rows of one terminal
|
||||||
|
// share that parent, so its own shouldSkip() verdict is kept once it says
|
||||||
|
// skip; a skip verdict cannot lapse, since xterm keeps `.xterm-rows` inside
|
||||||
|
// its `.xterm`. A rows container that is not skipped is never kept: its rows
|
||||||
|
// go through the full check below like any other node.
|
||||||
|
const skippedRows = new WeakSet();
|
||||||
|
function isSkippedRow(node) {
|
||||||
|
const rows = node.parentNode;
|
||||||
|
if (!rows?.classList?.contains('xterm-rows')) return false;
|
||||||
|
if (skippedRows.has(rows)) return true;
|
||||||
|
if (!shouldSkip(rows)) return false;
|
||||||
|
skippedRows.add(rows);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldSkipText(node) {
|
function shouldSkipText(node) {
|
||||||
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
||||||
return shouldSkip(node) || Boolean(element?.closest(USER_TEXT_SELECTOR));
|
return shouldSkip(node) || Boolean(element?.closest(USER_TEXT_SELECTOR));
|
||||||
@@ -1243,7 +1260,7 @@
|
|||||||
// check per record instead of one per text node and attribute matters
|
// check per record instead of one per text node and attribute matters
|
||||||
// for xterm's DOM renderer, which replaces rows every frame (the split
|
// for xterm's DOM renderer, which replaces rows every frame (the split
|
||||||
// pane, every tile of the grid).
|
// pane, every tile of the grid).
|
||||||
if (shouldSkip(mutation.target)) continue;
|
if (isSkippedRow(mutation.target) || 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);
|
||||||
|
|||||||
@@ -132,6 +132,59 @@ describe('custom display name and browser localization', () => {
|
|||||||
dom.window.close();
|
dom.window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('decides an xterm row by its rows container, with the verdict closest() gives', async () => {
|
||||||
|
// A terminal (rows inside .xterm) and a stray `.xterm-rows` outside any
|
||||||
|
// skipped surface, which the full check translates.
|
||||||
|
const dom = makeDom(
|
||||||
|
'<div class="xterm"><div class="xterm-screen"><div class="xterm-rows" id="rows"></div></div></div>' +
|
||||||
|
'<div class="xterm-rows" id="stray"></div>'
|
||||||
|
);
|
||||||
|
const { window } = dom;
|
||||||
|
const api = window.CodemanI18n;
|
||||||
|
api.start();
|
||||||
|
api.configure({ language: 'zh-CN' });
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
const rowsOf = (id: string) =>
|
||||||
|
Array.from({ length: 20 }, () => {
|
||||||
|
const row = window.document.createElement('div');
|
||||||
|
window.document.getElementById(id)!.appendChild(row);
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
const rows = rowsOf('rows');
|
||||||
|
const stray = rowsOf('stray');
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
let closest = 0;
|
||||||
|
const original = window.Element.prototype.closest;
|
||||||
|
window.Element.prototype.closest = function (this: Element, selector: string) {
|
||||||
|
closest++;
|
||||||
|
return original.call(this, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
// What xterm's DOM renderer does every frame: replace each row's children.
|
||||||
|
const frames = (list: Element[]) => {
|
||||||
|
for (let frame = 0; frame < 3; frame++) {
|
||||||
|
for (const row of list) {
|
||||||
|
const span = window.document.createElement('span');
|
||||||
|
span.textContent = 'Settings saved';
|
||||||
|
row.replaceChildren(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
frames(rows);
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
// 60 row records, ONE closest(): the container's own verdict, then kept.
|
||||||
|
expect(closest).toBe(1);
|
||||||
|
expect(rows.every((row) => row.textContent === 'Settings saved')).toBe(true);
|
||||||
|
|
||||||
|
// A rows container outside any skipped surface keeps the full check, and
|
||||||
|
// with it the verdict closest() gives: its rows are translated.
|
||||||
|
frames(stray);
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||||
|
window.Element.prototype.closest = original;
|
||||||
|
expect(stray.every((row) => row.textContent === '设置已保存')).toBe(true);
|
||||||
|
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