75 lines
3.5 KiB
JavaScript
75 lines
3.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { performance } from 'node:perf_hooks';
|
|
|
|
const source = readFileSync('static/js/authorship.js', 'utf8');
|
|
const { lineAuthors, parseAuthorship } = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64'));
|
|
|
|
// Reference implementation from 0.2.89; include it to detect attribution,
|
|
// color and tie-breaking regressions independently of the optimized code.
|
|
function referenceLineAuthors(content, model) {
|
|
const starts = [0];
|
|
for (let i = 0; i < content.length; i++) if (content.charCodeAt(i) === 10) starts.push(i + 1);
|
|
return starts.map((start, index) => {
|
|
const end = index + 1 < starts.length ? starts[index + 1] : content.length;
|
|
const totals = new Map();
|
|
const representatives = new Map();
|
|
for (const span of model?.spans || []) {
|
|
const overlap = Math.max(0, Math.min(end, span.end) - Math.max(start, span.start));
|
|
if (!overlap) continue;
|
|
const identity = String(span.owner || '').split('\u001f', 1)[0];
|
|
totals.set(identity, (totals.get(identity) || 0) + overlap);
|
|
representatives.set(identity, span.owner);
|
|
}
|
|
return [...totals.entries()]
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([identity]) => representatives.get(identity));
|
|
});
|
|
}
|
|
|
|
let seed = 42;
|
|
function random(n) { seed = (seed * 1664525 + 1013904223) >>> 0; return seed % n; }
|
|
const owners = ['A\u001f#ff0000', 'B', 'A\u001f#00ff00', 'C', 'D', ''];
|
|
for (let sample = 0; sample < 400; sample++) {
|
|
const content = Array.from({ length: 1 + random(65) }, (_, i) => {
|
|
const base = ['text', '', '🙂 éół', ' x', '......'][random(5)];
|
|
return base + (i ? '\n' : '');
|
|
}).join('');
|
|
const spans = [];
|
|
for (let i = 0; i < 4 + random(100); i++) {
|
|
const start = random(content.length + 1);
|
|
spans.push({ start, end: start + random(content.length + 4), owner: owners[random(owners.length)] });
|
|
}
|
|
const canonical = parseAuthorship(content, { version: 2, spans });
|
|
assert.deepEqual(lineAuthors(content, canonical), referenceLineAuthors(content, canonical), `canonical parity sample ${sample}`);
|
|
assert.deepEqual(lineAuthors(content, { spans }), referenceLineAuthors(content, { spans }), `noncanonical parity sample ${sample}`);
|
|
}
|
|
for (const content of ['', 'a', '\n', '🙂\n\nB\n', 'line1\nline2\n']) {
|
|
for (const model of [{ spans: [] }, { spans: [{ start: 0, end: 999, owner: 'one' }] }]) {
|
|
assert.deepEqual(lineAuthors(content, model), referenceLineAuthors(content, model));
|
|
}
|
|
}
|
|
|
|
const n = 6000;
|
|
const lines = [], spans = [];
|
|
let offset = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
const line = `row ${String(i).padStart(5, '0')} lorem ipsum\n`;
|
|
lines.push(line);
|
|
spans.push({ start: offset, end: offset + line.length, owner: `user${i % 37}` });
|
|
offset += line.length;
|
|
}
|
|
const text = lines.join('');
|
|
const model = { version: 2, spans };
|
|
assert.deepEqual(lineAuthors(text, model), referenceLineAuthors(text, model));
|
|
function medianDuration(fn) {
|
|
const ms = [];
|
|
for (let i = 0; i < 7; i++) { const start = performance.now(); fn(); ms.push(performance.now() - start); }
|
|
ms.sort((a, b) => a - b);
|
|
return ms[3];
|
|
}
|
|
medianDuration(() => lineAuthors(text, model));
|
|
const oldMs = medianDuration(() => referenceLineAuthors(text, model));
|
|
const newMs = medianDuration(() => lineAuthors(text, model));
|
|
console.log(`PASS: 400 randomized canonical + noncanonical parity cases; 6000 lines, 6000 spans: reference ${oldMs.toFixed(2)} ms; optimized ${newMs.toFixed(2)} ms; ${(oldMs / newMs).toFixed(1)}x`);
|