Skip to content

Commit

Permalink
perf(json-crdt-extensions): ⚡️ do not pre-compute chunk slices
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Nov 15, 2024
1 parent b155ae3 commit 7bf4f93
Show file tree
Hide file tree
Showing 7 changed files with 41 additions and 49 deletions.
4 changes: 2 additions & 2 deletions src/json-crdt-extensions/peritext/block/Block.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {printTree} from 'tree-dump/lib/printTree';
import {CONST, updateJson, updateNum} from '../../../json-hash';
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import type {OverlayPoint} from '../overlay/OverlayPoint';
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
import {Inline} from './Inline';
import {formatType} from '../slice/util';
import type {OverlayPoint} from '../overlay/OverlayPoint';
import type {Path} from '@jsonjoy.com/json-pointer';
import type {Printable} from 'tree-dump';
import type {Peritext} from '../Peritext';
Expand Down Expand Up @@ -100,7 +100,7 @@ export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
const iterator = this.tuples0();
return () => {
const pair = iterator();
return pair && Inline.create(txt, pair[0], pair[1]);
return pair && new Inline(txt, pair[0], pair[1]);
};
}

Expand Down
50 changes: 21 additions & 29 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {printTree} from 'tree-dump/lib/printTree';
import type {OverlayPoint} from '../overlay/OverlayPoint';
import {stringify} from '../../../json-text/stringify';
import {SliceBehavior, CommonSliceType} from '../slice/constants';
import {Range} from '../rga/Range';
Expand All @@ -8,17 +7,12 @@ import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import {Cursor} from '../editor/Cursor';
import {hashId} from '../../../json-crdt/hash';
import {formatType} from '../slice/util';
import type {AbstractRga} from '../../../json-crdt/nodes/rga';
import type {OverlayPoint} from '../overlay/OverlayPoint';
import type {Printable} from 'tree-dump/lib/types';
import type {PathStep} from '@jsonjoy.com/json-pointer';
import type {Peritext} from '../Peritext';
import type {Slice} from '../slice/types';

/**
* @todo Make sure these inline attributes can handle the cursor which ends
* with attaching to the start of the next character.
*/

/** The attribute started before this inline and ends after this inline. */
export class InlineAttrPassing {
constructor(public slice: Slice) {}
Expand Down Expand Up @@ -68,29 +62,12 @@ export type InlineAttrs = Record<string | number, InlineAttrStack>;
* full text content of the inline.
*/
export class Inline extends Range implements Printable {
public static create(txt: Peritext, start: OverlayPoint, end: OverlayPoint) {
const texts: ChunkSlice[] = [];
txt.overlay.chunkSlices0(undefined, start, end, (chunk, off, len) => {
if (txt.overlay.isMarker(chunk.id)) return;
texts.push(new ChunkSlice(chunk, off, len));
});
return new Inline(txt.str, start, end, texts);
}

constructor(
rga: AbstractRga<string>,
public readonly txt: Peritext,
public start: OverlayPoint,
public end: OverlayPoint,

/**
* @todo PERF: for performance reasons, we should consider not passing in
* this array. Maybe pass in just the initial chunk and the offset. However,
* maybe even that is not necessary, as the `.start` point should have
* its chunk cached, or will have it cached after the first access.
*/
public readonly texts: ChunkSlice[],
) {
super(rga, start, end);
super(txt.str, start, end);
}

/**
Expand All @@ -107,7 +84,7 @@ export class Inline extends Range implements Printable {
* @returns The position of the inline within the text.
*/
public pos(): number {
const chunkSlice = this.texts[0];
const chunkSlice = this.texts(1)[0];
if (!chunkSlice) return -1;
const chunk = chunkSlice.chunk;
const pos = this.rga.pos(chunk);
Expand Down Expand Up @@ -240,6 +217,20 @@ export class Inline extends Range implements Printable {
return;
}

public texts(limit: number = 1e6): ChunkSlice[] {
const texts: ChunkSlice[] = [];
const txt = this.txt;
const overlay = txt.overlay;
let cnt = 0;
overlay.chunkSlices0(this.start.chunk(), this.start, this.end, (chunk, off, len): boolean | void => {
if (overlay.isMarker(chunk.id)) return;
cnt++;
texts.push(new ChunkSlice(chunk, off, len));
if (cnt === limit) return true;
});
return texts;
}

public text(): string {
const str = super.text();
return this.start instanceof MarkerOverlayPoint ? str.slice(1) : str;
Expand All @@ -261,6 +252,7 @@ export class Inline extends Range implements Printable {
const header = `Inline ${range} ${text}`;
const attr = this.attr();
const attrKeys = Object.keys(attr);
const texts = this.texts();
return (
header +
printTree(tab, [
Expand All @@ -282,13 +274,13 @@ export class Inline extends Range implements Printable {
);
}),
),
!this.texts.length
!texts.length
? null
: (tab) =>
'texts' +
printTree(
tab,
this.texts.map((text) => (tab) => text.toString(tab)),
this.texts().map((text) => (tab) => text.toString(tab)),
),
])
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ describe('tuples', () => {
const tuples1 = [...block1.tuples()];
const tuples2 = [...block2.tuples()];
expect(tuples1.length).toBe(3);
const text1 = tuples1.map(([p1, p2]) => Inline.create(peritext, p1, p2).text()).join('');
const text2 = tuples2.map(([p1, p2]) => Inline.create(peritext, p1, p2).text()).join('');
const text1 = tuples1.map(([p1, p2]) => new Inline(peritext, p1, p2).text()).join('');
const text2 = tuples2.map(([p1, p2]) => new Inline(peritext, p1, p2).text()).join('');
expect(text1).toBe('hello ');
expect(text2).toBe('world');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const runKeyTests = (setup: () => Kit) => {
peritext.editor.cursor.setAt(i, j);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
if (keys.has(inline.key())) {
const inline2 = keys.get(inline.key())!;
// tslint:disable-next-line:no-console
Expand Down
12 changes: 6 additions & 6 deletions src/json-crdt-extensions/peritext/block/__tests__/Inline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.cursor.setAt(i, j);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const str = inline.text();
expect(str).toBe(
peritext
Expand All @@ -43,7 +43,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.cursor.setAt(i, j);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const pos = inline.pos();
expect(pos).toBe(i);
}
Expand All @@ -61,7 +61,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.saved.insStack('em', 1);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const attr = inline.attr();
expect(attr.bold[0].slice.data()).toEqual(1);
expect(attr.bold[1].slice.data()).toEqual(2);
Expand All @@ -77,7 +77,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.saved.insOverwrite('em', 1);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const attr = inline.attr();
expect(attr.bold[0].slice.data()).toEqual(2);
expect(attr.em[0].slice.data()).toEqual(1);
Expand All @@ -92,7 +92,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.saved.insOverwrite('em');
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const attr = inline.attr();
expect(attr.bold).toBe(undefined);
expect(attr.em[0]).toBeInstanceOf(InlineAttrContained);
Expand All @@ -107,7 +107,7 @@ const runStrTests = (setup: () => Kit) => {
peritext.editor.saved.insStack(['bold', 'normal'], 2);
overlay.refresh();
const [start, end] = [...overlay.points()];
const inline = Inline.create(peritext, start, end);
const inline = new Inline(peritext, start, end);
const attr = inline.attr();
expect(attr['bold,very'][0].slice.data()).toEqual(1);
expect(attr['bold,normal'][0].slice.data()).toEqual(2);
Expand Down
7 changes: 3 additions & 4 deletions src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,11 @@ export class Overlay<T = string> implements Printable, Stateful {
}

/** @todo Rename to `chunks()`. */
/** @todo Rewrite this as `UndefIterator`. */
public chunkSlices0(
chunk: Chunk<T> | undefined,
p1: Point<T>,
p2: Point<T>,
callback: (chunk: Chunk<T>, off: number, len: number) => void,
callback: (chunk: Chunk<T>, off: number, len: number) => boolean | void,
): Chunk<T> | undefined {
const rga = this.txt.str;
const strId = rga.id;
Expand All @@ -179,7 +178,7 @@ export class Overlay<T = string> implements Printable, Stateful {
const time1 = id1.time;
const sid2 = id2.sid;
const time2 = id2.time;
return rga.range0(undefined, id1, id2, (chunk: Chunk<T>, off: number, len: number) => {
return rga.range0(undefined, id1, id2, (chunk: Chunk<T>, off: number, len: number): boolean | void => {
if (checkFirstAnchor) {
checkFirstAnchor = false;
const chunkId = chunk.id;
Expand All @@ -196,7 +195,7 @@ export class Overlay<T = string> implements Printable, Stateful {
len -= 1;
}
}
callback(chunk, off, len);
if (callback(chunk, off, len)) return true;
}) as Chunk<T>;
}

Expand Down
11 changes: 6 additions & 5 deletions src/json-crdt/nodes/rga/AbstractRga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,15 @@ export abstract class AbstractRga<T> {
* to avoid a lookup.
* @param from ID of the first element in the range.
* @param to ID of the last element in the range.
* @param callback Function to call for each chunk slice in the range.
* @param callback Function to call for each chunk slice in the range. If it
* returns truthy value, the iteration will stop.
* @returns Reference to the last chunk in the range.
*/
public range0(
startChunk: Chunk<T> | undefined,
from: ITimestampStruct,
to: ITimestampStruct,
callback: (chunk: Chunk<T>, off: number, len: number) => void,
callback: (chunk: Chunk<T>, off: number, len: number) => boolean | void,
): Chunk<T> | undefined {
let chunk: Chunk<T> | undefined = startChunk ? startChunk : this.findById(from);
if (startChunk) while (chunk && !containsId(chunk.id, chunk.span, from)) chunk = next(chunk);
Expand All @@ -422,7 +423,7 @@ export abstract class AbstractRga<T> {
return chunk;
}
const len = chunk.span - off;
callback(chunk, off, len);
if (callback(chunk, off, len)) return chunk;
} else {
if (containsId(chunk.id, chunk.span, to)) return;
}
Expand All @@ -431,10 +432,10 @@ export abstract class AbstractRga<T> {
const toContainedInChunk = containsId(chunk.id, chunk.span, to);
// TODO: fast path for chunk.del
if (toContainedInChunk) {
if (!chunk.del) callback(chunk, 0, to.time - chunk.id.time + 1);
if (!chunk.del) if (callback(chunk, 0, to.time - chunk.id.time + 1)) return chunk;
return chunk;
}
if (!chunk.del) callback(chunk, 0, chunk.span);
if (!chunk.del) if (callback(chunk, 0, chunk.span)) return chunk;
chunk = next(chunk);
}
return chunk;
Expand Down

0 comments on commit 7bf4f93

Please sign in to comment.