Compare commits

..
Author SHA1 Message Date
Daniel Imms c2f5c424a8 Merge branch 'master' into fix/buffer-copy-and-reflow-wrap 2026-06-06 11:16:13 -07:00
3cdbc77386 fix(addon-search): advance highlight scan by buffer match size (#5994)
* fix(addon-search): advance highlight scan by buffer match size

_highlightAllMatches() stepped to the next find() using term.length
and col + 1 when staying on the same row. Search results expose size
in buffer columns (wide glyphs, wrapped matches), which select() and
decorations already use.

Advance with col + size and wrap row/col when crossing the terminal
width so highlight-all does not skip, duplicate, or stop early on
non-ASCII matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(addon-search): regression for highlight-all wide char advance by size

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* test(addon-search): move highlight-all regression to integration tests

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 11:12:04 -07:00
Daniel ImmsandCursor e9c648f519 fix(BufferReflow): clear stale isWrapped after larger reflow
When widening the terminal unwraps a multi-row soft wrap and removes
trailing buffer lines, the last retained row could still have
isWrapped set, and the following line could still think it continued
a removed row. That broke wrapped range translation.

Clear isWrapped on the last retained row in the group and on the
first line after the group when rows are removed during reflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:37:20 +00:00
Daniel ImmsandCursor 60bae9528d fix(BufferLine): limit copyCellsFrom metadata to copied range
copyCellsFrom copied Uint32 cell data only for the requested column
range but migrated every combined entry with key >= srcCol, which could
attach grapheme metadata to unrelated destination columns during
partial line copies in reflow and wraparound.

Copy combined and extended attributes only per copied cell and clear
stale entries on the destination when the source cell does not use
them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:36:39 +00:00
10 changed files with 96 additions and 201 deletions
+10 -10
View File
@@ -436,9 +436,9 @@ export class ImageStorage implements IDisposable {
for (let col = 0; col < cols; ++col) {
let e: IExtendedAttrsImage;
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
e = line._extendedAttrs?.[col] ?? EMPTY_ATTRS;
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
} else {
const maybeImg = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
continue;
}
@@ -461,7 +461,7 @@ export class ImageStorage implements IDisposable {
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
*/
while (++col < cols) {
const nextE = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break;
}
@@ -515,7 +515,7 @@ export class ImageStorage implements IDisposable {
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[oldCol] ?? EMPTY_ATTRS;
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
@@ -560,7 +560,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
@@ -580,7 +580,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
@@ -611,7 +611,7 @@ export class ImageStorage implements IDisposable {
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs?.[x];
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
@@ -627,13 +627,13 @@ export class ImageStorage implements IDisposable {
return;
}
// found a plain ExtendedAttrs instance, clone it to new entry
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return;
}
}
// fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
}
private _evictOnAlternate(): void {
@@ -652,7 +652,7 @@ export class ImageStorage implements IDisposable {
}
for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs?.[x]?.imageId;
const imgId = line._extendedAttrs[x]?.imageId;
if (imgId) {
const spec = this._images.get(imgId);
if (spec) {
+1 -1
View File
@@ -64,7 +64,7 @@ export interface IExtendedAttrsImage extends IExtendedAttrs {
/* eslint-disable */
export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined} | undefined;
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array;
}
+1 -4
View File
@@ -118,10 +118,7 @@ test.describe('ImageAddon', () => {
}
// bufferline privates
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
strictEqual(await ctx.page.evaluate(`(() => {
const ea = window.term._core.buffer.lines.get(0)._extendedAttrs;
return ea === undefined || (typeof ea === 'object' && ea !== null);
})()`), true);
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
// inputhandler privates
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData');
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
+8 -6
View File
@@ -142,12 +142,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
}
prevResult = result;
results.push(prevResult);
result = this._engine.find(
term,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
const cols = this._terminal.cols;
let nextCol = prevResult.col + prevResult.size;
let nextRow = prevResult.row;
if (nextCol >= cols) {
nextRow += Math.floor(nextCol / cols);
nextCol = nextCol % cols;
}
result = this._engine.find(term, nextRow, nextCol, searchOptions);
}
this._resultTracker.updateResults(results, this._highlightLimit);
@@ -414,6 +414,20 @@ test.describe('Search Tests', () => {
});
test.describe('Regression tests', () => {
test('should advance highlight-all scan by buffer match size for wide characters', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.resize(3, 5);
await ctx.proxy.write('𝄞𝄞𝄞');
strictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
await ctx.proxy.resize(80, 24);
});
test.describe('#2444 wrapped line content not being found', () => {
let fixture: string;
test.beforeAll(async () => {
+15
View File
@@ -495,6 +495,21 @@ describe('BufferLine', function(): void {
assert.deepEqual(columns, [0]);
});
});
describe('copyCellsFrom', () => {
it('should only move combined data within the copied range', () => {
const src = new TestBufferLine(10, NULL_CELL_DATA, false);
src.setCell(1, createCellData(1, 'a', 1));
src.addCodepointToCell(1, '\u0301'.charCodeAt(0), 0);
src.setCell(5, createCellData(1, 'z', 1));
src.addCodepointToCell(5, '\u0301'.charCodeAt(0), 0);
const dest = new TestBufferLine(10, NULL_CELL_DATA, false);
dest.copyCellsFrom(src, 0, 0, 2, false);
assert.property(dest.combined, '1');
assert.notProperty(dest.combined, '5');
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
+19 -67
View File
@@ -71,20 +71,12 @@ export interface IBufferLineStringCache {
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/
/**
* Shared empty sparse map for lines with no combining data.
* Uses a sentinel + copy-on-write so `copyFrom` can detect empty `_combined` with
* reference equality. `_extendedAttrs` uses `undefined` instead (addon-image writes
* the map directly without BufferLine APIs).
*/
const EMPTY_SPARSE_MAP: {[index: number]: string} = Object.create(null);
export class BufferLine implements IBufferLine {
protected _data: Uint32Array;
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
protected _combined: {[index: number]: string} = EMPTY_SPARSE_MAP;
protected _combined: {[index: number]: string} = {};
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} | undefined;
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
public length: number;
@@ -129,7 +121,6 @@ export class BufferLine implements IBufferLine {
this._invalidateStringCache();
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
this._ensureCombinedMap();
this._combined[index] = value[1];
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
@@ -220,7 +211,7 @@ export class BufferLine implements IBufferLine {
cell.combinedData = '';
}
if (cell.bg & BgFlags.HAS_EXTENDED) {
cell.extended = this._extendedAttrs![index]!;
cell.extended = this._extendedAttrs[index]!;
} else {
// Do not mutate cell.extended in place: it may still reference this line's map entry from a
// prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
@@ -235,11 +226,10 @@ export class BufferLine implements IBufferLine {
public setCell(index: number, cell: ICellData): void {
this._invalidateStringCache();
if (cell.content & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[index] = cell.combinedData;
}
if (cell.bg & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[index] = cell.extended;
this._extendedAttrs[index] = cell.extended;
}
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
@@ -254,7 +244,7 @@ export class BufferLine implements IBufferLine {
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
this._invalidateStringCache();
if (attrs.bg & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[index] = attrs.extended;
this._extendedAttrs[index] = attrs.extended;
}
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
@@ -272,14 +262,12 @@ export class BufferLine implements IBufferLine {
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add
this._ensureCombinedMap();
this._combined[index] += stringFromCodePoint(codePoint);
} else {
if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars:
// - move current leading char + new one into combined string
// - set combined flag
this._ensureCombinedMap();
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK;
@@ -422,13 +410,11 @@ export class BufferLine implements IBufferLine {
}
}
// remove any cut off extended attributes
if (this._extendedAttrs) {
const extKeys = Object.keys(this._extendedAttrs);
for (let i = 0; i < extKeys.length; i++) {
const key = parseInt(extKeys[i], 10);
if (key >= cols) {
delete this._extendedAttrs[key];
}
const extKeys = Object.keys(this._extendedAttrs);
for (let i = 0; i < extKeys.length; i++) {
const key = parseInt(extKeys[i], 10);
if (key >= cols) {
delete this._extendedAttrs[key];
}
}
}
@@ -464,8 +450,8 @@ export class BufferLine implements IBufferLine {
}
return;
}
this._combined = EMPTY_SPARSE_MAP;
this._extendedAttrs = undefined;
this._combined = {};
this._extendedAttrs = {};
for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
@@ -622,57 +608,23 @@ export class BufferLine implements IBufferLine {
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[destCol] = src._combined[srcCol];
} else {
delete this._combined[destCol];
}
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[destCol] = src._extendedAttrs![srcCol]!;
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
} else {
delete this._extendedAttrs[destCol];
}
}
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */
private _copySparseMapsFrom(line: BufferLine): void {
const srcCombined = line._combined;
const srcExtended = line._extendedAttrs;
// Fast path: blank / scroll-recycle source (empty `_combined` sentinel, no extended map).
if (srcCombined === EMPTY_SPARSE_MAP && !srcExtended) {
this._combined = EMPTY_SPARSE_MAP;
this._extendedAttrs = undefined;
return;
}
this._copySparseMapsFromDense(line);
}
/** Copy sparse maps by scanning `_data` flags (inlined, no per-cell function calls). */
private _copySparseMapsFromDense(line: BufferLine): void {
this._combined = {};
this._extendedAttrs = {};
const srcData = line._data;
const srcCombined = line._combined;
const srcExtended = line._extendedAttrs;
const len = line.length;
const indices = Constants.CELL_INDICIES;
const combined = this._combined;
const extendedAttrs = this._extendedAttrs;
for (let i = 0; i < len; i++) {
const start = i * indices;
if (srcData[start + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
combined[i] = srcCombined[i];
}
if (srcData[start + Cell.BG] & BgFlags.HAS_EXTENDED) {
extendedAttrs[i] = srcExtended![i];
}
for (let i = 0; i < line.length; i++) {
this._copyCellMapsFrom(line, i, i);
}
}
private _ensureCombinedMap(): void {
if (this._combined === EMPTY_SPARSE_MAP) {
this._combined = {};
}
}
private _ensureExtendedAttrsMap(): {[index: number]: IExtendedAttrs | undefined} {
return this._extendedAttrs ??= {};
}
}
+23
View File
@@ -10,6 +10,7 @@ import { CellData } from './CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants';
import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { IBufferLine } from './Types';
import { createCellData, NULL_CELL_DATA } from '../TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
@@ -122,5 +123,27 @@ describe('BufferReflow', () => {
const lines = createWrappedLines('abcde');
assert.notDeepEqual(reflowLargerGetLinesToRemove(lines, 1, 5, 10, nullCell, false), []);
});
it('should clear isWrapped on the last retained row after unwrap', () => {
const lines = new CircularList<BufferLine>(10);
const set2 = (line: BufferLine, s: string) => {
for (let i = 0; i < s.length; i++) {
line.setCell(i, createCellData(0, s[i], 1));
}
};
const l0 = new BufferLine(TEST_STRING_CACHE, 2);
set2(l0, 'ab');
const l1 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l1, 'cd');
const l2 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l2, 'ef');
lines.push(l0);
lines.push(l1);
lines.push(l2);
reflowLargerGetLinesToRemove(lines, 2, 6, 99, NULL_CELL_DATA, false);
assert.equal(l1.isWrapped, false);
});
});
});
+5
View File
@@ -99,6 +99,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, o
}
if (countToRemove > 0) {
wrappedLines[destLineIndex].isWrapped = false;
const lineAfterGroup = lines.get(y + wrappedLines.length);
if (lineAfterGroup) {
lineAfterGroup.isWrapped = false;
}
toRemove.push(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove);
}
-113
View File
@@ -1,113 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { CellData } from 'common/buffer/CellData';
import { BgFlags, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
const ITERATIONS = 500_000;
const stringCache = new BufferLineStringCache();
const nullCell = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
const yCell = CellData.fromCharData([0, 'y', 1, 'y'.charCodeAt(0)]);
function fillPlainY(line: BufferLine, cols: number): void {
for (let i = 0; i < cols; i++) {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
function setupScrollRecyclePair(cols: number): { recycled: BufferLine, blank: BufferLine } {
const blank = new BufferLine(stringCache, cols, nullCell, false);
const recycled = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(recycled, cols);
return { recycled, blank };
}
/** Source line with combining chars and extended attrs spread across columns. */
function buildSparseSourceLine(cols: number): BufferLine {
const line = new BufferLine(stringCache, cols, nullCell, false);
for (let i = 0; i < cols; i++) {
if (i % 10 === 0) {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(i, cell);
} else if (i % 17 === 0) {
line.setCell(i, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
} else {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
return line;
}
function setupCopyFromSparsePair(cols: number): { dest: BufferLine, src: BufferLine } {
const src = buildSparseSourceLine(cols);
const dest = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(dest, cols);
return { dest, src };
}
perfContext('BufferLine.copyFrom (scroll recycle)', () => {
perfContext('cols=80', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});
perfContext('BufferLine.copyFrom (sparse source: combined + extended attrs)', () => {
perfContext('cols=80', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});