Skip to content

Commit 4bc7491

Browse files
committed
Bump attribution CHUNK_SIZE from 50 to 500
1 parent 573ce8c commit 4bc7491

2 files changed

Lines changed: 18 additions & 17 deletions

File tree

hub-client/src/services/attribution.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,10 @@ describe('updateAttributionMap — incremental update', () => {
468468

469469
describe('buildAttributionMap — chunked processing', () => {
470470
it('processes large history in chunks via requestIdleCallback', async () => {
471-
// Create 120 history entries, each inserting one character
471+
// Create CHUNK_SIZE+20 history entries to force more than one chunk.
472+
const N = CHUNK_SIZE + 20;
472473
const entries: MockHistoryEntry[] = [];
473-
for (let i = 0; i < 120; i++) {
474+
for (let i = 0; i < N; i++) {
474475
entries.push({
475476
heads: [`h${i}`],
476477
actor: `actor${i % 2}`,
@@ -485,11 +486,9 @@ describe('buildAttributionMap — chunked processing', () => {
485486
const result = await buildAttributionMap(handle as any, 'text');
486487

487488
expect(result).not.toBeNull();
488-
expect(result!.entries).toHaveLength(120);
489-
// requestIdleCallback should have been called for chunking
490-
// 120 entries / CHUNK_SIZE chunks, plus potentially the initial call
489+
expect(result!.entries).toHaveLength(N);
491490
expect(mockRequestIdleCallback.mock.calls.length).toBeGreaterThanOrEqual(
492-
Math.ceil(120 / CHUNK_SIZE)
491+
Math.ceil(N / CHUNK_SIZE),
493492
);
494493
});
495494

@@ -507,15 +506,14 @@ describe('buildAttributionMap — chunked processing', () => {
507506
mockDiff.mockImplementation(createMockDiff(entries));
508507

509508
const controller = new AbortController();
510-
controller.abort(); // Abort before calling
509+
controller.abort();
511510

512511
const result = await buildAttributionMap(handle as any, 'text', controller.signal);
513512

514513
expect(result).toBeNull();
515514
});
516515

517516
it('returns null when signal is aborted between chunks', async () => {
518-
// Create enough entries to span multiple chunks
519517
const entries: MockHistoryEntry[] = [];
520518
for (let i = 0; i < CHUNK_SIZE + 10; i++) {
521519
entries.push({
@@ -531,12 +529,10 @@ describe('buildAttributionMap — chunked processing', () => {
531529

532530
const controller = new AbortController();
533531

534-
// Abort after the first chunk's requestIdleCallback fires
535532
let callCount = 0;
536533
mockRequestIdleCallback.mockImplementation((cb: IdleRequestCallback) => {
537534
callCount++;
538535
if (callCount === 1) {
539-
// Let first chunk complete, then abort
540536
cb({ didTimeout: false, timeRemaining: () => 50 } as IdleDeadline);
541537
controller.abort();
542538
} else {

hub-client/src/services/attribution.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,15 @@ export class HistoryCompactedError extends Error {
6666
}
6767
}
6868

69-
/** Number of history entries processed per idle callback chunk. */
70-
export const CHUNK_SIZE = 50;
69+
/**
70+
* History entries processed between idle-callback yields. Tuning knob:
71+
* larger values reduce the number of rIC round trips (faster
72+
* time-to-attribution) but make each slice's CPU block bigger (more frame
73+
* jank risk). 500 gives ~2.5 ms of CPU per slice at bench-measured
74+
* ~5 µs/entry, comfortably under one 60 Hz frame even when real Automerge
75+
* diffs push per-entry cost an order of magnitude higher.
76+
*/
77+
export const CHUNK_SIZE = 500;
7178

7279
// ---------------------------------------------------------------------------
7380
// Internal: patch application
@@ -186,14 +193,14 @@ export async function buildAttributionMap(
186193
let prevHeads: unknown = null;
187194
let lastHeads: unknown[] = [];
188195

189-
// Process history in chunks, yielding to the event loop between each
196+
// Process history in fixed-count chunks, yielding between each. Chunk
197+
// size is the frame-impact vs. time-to-attribution tuning knob — see
198+
// CHUNK_SIZE docs.
190199
for (let chunkStart = 0; chunkStart < history.length; chunkStart += CHUNK_SIZE) {
191-
// Yield before each chunk to avoid blocking the main thread
192200
await waitForIdle();
193201
if (signal?.aborted) return null;
194202

195203
const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, history.length);
196-
197204
for (let i = chunkStart; i < chunkEnd; i++) {
198205
const currHeads = history[i];
199206
const changeHash = extractChangeHash(currHeads);
@@ -202,10 +209,8 @@ export async function buildAttributionMap(
202209
const time = meta?.time ?? 0;
203210
const attribution: CharAttribution = { actor, time };
204211

205-
// Get patches from diff
206212
const decodedCurr = decodeHeads(currHeads as Parameters<typeof decodeHeads>[0]);
207213
let patches: unknown[];
208-
209214
if (prevHeads === null) {
210215
patches = diff(
211216
viewable.doc() as Parameters<typeof diff>[0],

0 commit comments

Comments
 (0)