From e22fcb801572bcfaeadbc027c7403b2c5cab36d3 Mon Sep 17 00:00:00 2001 From: Rizky Date: Sun, 11 Feb 2024 20:56:50 +0700 Subject: [PATCH] wip fix --- bun.lockb | Bin 269448 -> 269080 bytes pkgs/core/package.json | 3 +- pkgs/core/utils/diff-internal.ts | 416 +++++++++++++++++++++++++++++++ pkgs/core/utils/diff.test.ts | 56 ++++- pkgs/core/utils/diff.ts | 66 +++-- 5 files changed, 512 insertions(+), 29 deletions(-) create mode 100644 pkgs/core/utils/diff-internal.ts diff --git a/bun.lockb b/bun.lockb index 94bce9610e00aea081d2ae5360bbdd3d95f0e13a..a8393042b0b19567c27a1520351cb2f2b61f124d 100755 GIT binary patch delta 5395 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 delta 5386 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 bnL!3y6~te)njiH(B{05e8d1q3&AtBv7}i6o diff --git a/pkgs/core/package.json b/pkgs/core/package.json index a92ec684..0a2835be 100644 --- a/pkgs/core/package.json +++ b/pkgs/core/package.json @@ -18,7 +18,6 @@ "radash": "^11.0.0", "radix3": "^1.1.0", "typescript": "^5.2.2", - "unzipper": "^0.10.14", - "fast-myers-diff": "^3.2.0" + "unzipper": "^0.10.14" } } \ No newline at end of file diff --git a/pkgs/core/utils/diff-internal.ts b/pkgs/core/utils/diff-internal.ts new file mode 100644 index 00000000..e6a57e21 --- /dev/null +++ b/pkgs/core/utils/diff-internal.ts @@ -0,0 +1,416 @@ +export type GenericIndexable = { [key: number]: T; readonly length: number }; +type TypedArray = + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint16Array + | Uint32Array + | Float32Array + | Float64Array; +export type Indexable = string | T[] | TypedArray | GenericIndexable; + +export interface Sliceable extends GenericIndexable { + slice(start: number, end?: number): this; +} + +type Vec4 = [number, number, number, number]; +type Vec3 = [number, number, number]; + +type Comparator = (i: number, j: number) => boolean; + +type DiffState = { + i: number; + N: number; + j: number; + M: number; + Z: number; + b: TypedArray; + eq: (x: number, y: number) => boolean; + should_break?: () => boolean; + pxs: number; + pxe: number; + pys: number; + pye: number; + oxs: number; + oxe: number; + oys: number; + oye: number; + stack_top: number; + stack_base: number[]; +}; + +// Find the list of differences between 2 lists by +// recursive subdivision, requring O(min(N,M)) space +// and O(min(N,M)*D) worst-case execution time where +// D is the number of differences. +function diff_internal(state: DiffState, c: number): number { + const { b, eq, stack_base } = state; + let { i, N, j, M, Z, stack_top } = state; + for (;;) { + switch (c) { + case 0: { + Z_block: while (N > 0 && M > 0) { + b.fill(0, 0, 2 * Z); + + const W = N - M; + const L = N + M; + const parity = L & 1; + const offsetx = i + N - 1; + const offsety = j + M - 1; + const hmax = (L + parity) / 2; + let z: number; + h_loop: for (let h = 0; h <= hmax; h++) { + if (state.should_break && state.should_break()) { + return 0; + } + const kmin = 2 * Math.max(0, h - M) - h; + const kmax = h - 2 * Math.max(0, h - N); + + // Forward pass + for (let k = kmin; k <= kmax; k += 2) { + const gkm = b[k - 1 - Z * Math.floor((k - 1) / Z)]; + const gkp = b[k + 1 - Z * Math.floor((k + 1) / Z)]; + const u = k === -h || (k !== h && gkm < gkp) ? gkp : gkm + 1; + const v = u - k; + let x = u; + let y = v; + while (x < N && y < M && eq(i + x, j + y)) x++, y++; + b[k - Z * Math.floor(k / Z)] = x; + if ( + parity === 1 && + (z = W - k) >= 1 - h && + z < h && + x + b[Z + z - Z * Math.floor(z / Z)] >= N + ) { + if (h > 1 || x !== u) { + stack_base[stack_top++] = i + x; + stack_base[stack_top++] = N - x; + stack_base[stack_top++] = j + y; + stack_base[stack_top++] = M - y; + N = u; + M = v; + Z = 2 * (Math.min(N, M) + 1); + continue Z_block; + } else break h_loop; + } + } + + // Reverse pass + for (let k = kmin; k <= kmax; k += 2) { + const pkm = b[Z + k - 1 - Z * Math.floor((k - 1) / Z)]; + const pkp = b[Z + k + 1 - Z * Math.floor((k + 1) / Z)]; + const u = k === -h || (k !== h && pkm < pkp) ? pkp : pkm + 1; + const v = u - k; + let x = u; + let y = v; + while (x < N && y < M && eq(offsetx - x, offsety - y)) x++, y++; + b[Z + k - Z * Math.floor(k / Z)] = x; + if ( + parity === 0 && + (z = W - k) >= -h && + z <= h && + x + b[z - Z * Math.floor(z / Z)] >= N + ) { + if (h > 0 || x !== u) { + stack_base[stack_top++] = i + N - u; + stack_base[stack_top++] = u; + stack_base[stack_top++] = j + M - v; + stack_base[stack_top++] = v; + N = N - x; + M = M - y; + Z = 2 * (Math.min(N, M) + 1); + continue Z_block; + } else break h_loop; + } + } + } + + if (N === M) continue; + if (M > N) { + i += N; + j += N; + M -= N; + N = 0; + } else { + i += M; + j += M; + N -= M; + M = 0; + } + + // We already know either N or M is zero, so we can + // skip the extra check at the top of the loop. + break; + } + + // yield delete_start, delete_end, insert_start, insert_end + // At this point, at least one of N & M is zero, or we + // wouldn't have gotten out of the preceding loop yet. + if (N + M !== 0) { + if (state.pxe === i || state.pye === j) { + // it is a contiguous difference extend the existing one + state.pxe = i + N; + state.pye = j + M; + } else { + const sx = state.pxs; + state.oxs = state.pxs; + state.oxe = state.pxe; + state.oys = state.pys; + state.oye = state.pye; + + // Defer this one until we can check the next one + state.pxs = i; + state.pxe = i + N; + state.pys = j; + state.pye = j + M; + + if (sx >= 0) { + state.i = i; + state.N = N; + state.j = j; + state.M = M; + state.Z = Z; + state.stack_top = stack_top; + return 1; + } + } + } + } + case 1: { + if (stack_top === 0) return 2; + + M = stack_base[--stack_top]; + j = stack_base[--stack_top]; + N = stack_base[--stack_top]; + i = stack_base[--stack_top]; + Z = 2 * (Math.min(N, M) + 1); + c = 0; + } + } + } +} + +class DiffGen implements IterableIterator { + private c = 0; + private result: IteratorResult = { + value: null as any, + done: false, + }; + + constructor(private state: DiffState) {} + + [Symbol.iterator]() { + return this; + } + + next() { + const { state, result } = this; + + if (state.should_break && state.should_break()) { + result.done = true; + result.value = undefined; + return result; + } + + if (this.c > 1) { + result.done = true; + result.value = undefined; + return result; + } + const c = diff_internal(state, this.c); + if (state.should_break && state.should_break()) { + result.done = true; + result.value = undefined; + return result; + } + + this.c = c; + if (c === 1) { + result.value = [state.oxs, state.oxe, state.oys, state.oye]; + return result; + } + if (state.pxs >= 0) { + result.value = [state.pxs, state.pxe, state.pys, state.pye]; + return result; + } + result.done = true; + result.value = undefined; + return result; + } +} + +export function diff_core( + i: number, + N: number, + j: number, + M: number, + eq: Comparator, + should_break?: () => boolean +): IterableIterator { + const Z = (Math.min(N, M) + 1) * 2; + const L = N + M; + const b = new (L < 256 ? Uint8Array : L < 65536 ? Uint16Array : Uint32Array)( + 2 * Z + ); + + return new DiffGen({ + i, + N, + j, + M, + Z, + b, + eq, + should_break, + pxs: -1, + pxe: -1, + pys: -1, + pye: -1, + oxs: -1, + oxe: -1, + oys: -1, + oye: -1, + stack_top: 0, + stack_base: [], + }); +} + +export function diff>( + xs: T, + ys: T, + eq?: Comparator, + should_break?: () => boolean +): IterableIterator { + let [i, N, M] = [0, xs.length, ys.length]; + + if (typeof eq === "function") { + // eliminate common prefix + while (i < N && i < M && eq(i, i)) i++; + + // check for equality + if (i === N && i === M) return [][Symbol.iterator](); + + // eliminate common suffix + while (eq(--N, --M) && N > i && M > i); + } else { + // eliminate common prefix + while (i < N && i < M && xs[i] === ys[i]) i++; + + // check for equality + if (i === N && i === M) return [][Symbol.iterator](); + + // eliminate common suffix + while (xs[--N] === ys[--M] && N > i && M > i); + + eq = (i, j) => xs[i] === ys[j]; + } + + return diff_core(i, N + 1 - i, i, M + 1 - i, eq, should_break); +} + +class LCSGen implements IterableIterator { + private i = 0; + private j = 0; + + constructor( + private diff: IterableIterator, + private N: number + ) {} + + [Symbol.iterator]() { + return this; + } + + next() { + // Convert diffs into the dual similar-aligned representation. + // In each iteration, i and j will be aligned at the beginning + // of a shared section. This section is yielded, and i and j + // are re-aligned at the end of the succeeding unique sections. + const rec = this.diff.next(); + if (rec.done) { + const { i, j, N } = this; + if (i < N) { + rec.done = false as any; + rec.value = [i, j, N - i] as any; + this.i = N; + } + return rec as IteratorResult; + } + const v = rec.value; + const sx = v[0]; + const ex = v[1]; + const ey = v[3]; + const { i, j } = this; + if (i !== sx) { + v.length--; // re-use the vec4 as a vec3 to avoid allocation + v[0] = i; + v[1] = j; + v[2] = sx - i; + } + + this.i = ex; + this.j = ey; + + return rec as unknown as IteratorResult; + } +} + +export function lcs>( + xs: T, + ys: T, + eq?: Comparator +): IterableIterator { + return new LCSGen(diff(xs, ys, eq), xs.length); +} + +export function* calcPatch>( + xs: S, + ys: S, + eq?: Comparator, + should_break?: () => boolean +): Generator<[number, number, S]> { + // Taking subarrays is cheaper than slicing for TypedArrays. + const slice = ArrayBuffer.isView(xs) + ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice) + : xs.slice; + + for (const v of diff(xs, ys, eq, should_break)) { + v[2] = slice.call(ys, v[2], v[3]) as any; + yield v as any; + } +} + +export function* applyPatch>( + xs: S, + patch: Iterable<[number, number, S]> +): Generator { + let i = 0; // Taking subarrays is cheaper than slicing for TypedArrays. + const slice = ArrayBuffer.isView(xs) + ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice) + : xs.slice; + for (const [dels, dele, ins] of patch) { + if (i < dels) yield slice.call(xs, i, dels); + if (ins.length > 0) yield ins; + i = dele; + } + if (i < xs.length) yield slice.call(xs, i); +} + +export function* calcSlices>( + xs: S, + ys: S, + eq?: Comparator +): Generator<[-1 | 0 | 1, S]> { + let i = 0; // Taking subarrays is cheaper than slicing for TypedArrays. + const slice = ArrayBuffer.isView(xs) + ? (Uint8Array.prototype.subarray as unknown as typeof xs.slice) + : xs.slice; + for (const [dels, dele, inss, inse] of diff(xs, ys, eq)) { + if (i < dels) yield [0, slice.call(xs, i, dels)]; + if (dels < dele) yield [-1, slice.call(xs, dels, dele)]; + if (inss < inse) yield [1, slice.call(ys, inss, inse)]; + i = dele; + } + if (i < xs.length) yield [0, xs.slice(i)]; +} diff --git a/pkgs/core/utils/diff.test.ts b/pkgs/core/utils/diff.test.ts index a785f570..d2949a8d 100644 --- a/pkgs/core/utils/diff.test.ts +++ b/pkgs/core/utils/diff.test.ts @@ -1,16 +1,56 @@ -import { describe, expect, test } from "bun:test"; -import { Diff } from "./diff"; +import { describe } from "bun:test"; +import { readAsync } from "fs-jetpack"; +import { Diff, gunzipAsync } from "./diff"; + +import { Packr } from "msgpackr"; + +const MAX_HISTORY = 10; + +const packr = new Packr({}); describe("simple diff", async () => { - const server = await Diff.server("ini ionadi a"); - const patch = server.getPatch("new"); + const server = await Diff.server(await readAsync("tsconfig.json")); + const patch = await server.getPatch("new"); - const client = await Diff.client(patch); + console.log("init", hmn(patch.length)); + const client = await Diff.client(patch); + console.log("\npatch1"); await server.update("rako12"); + const patch1 = await server.getPatch(client.ts); + await client.applyPatch(patch1); + console.log( + hmn(patch1.length), + `|| client: ${hmn((await client.data).length)}` + ); - const newPatch = server.getPatch(client.ts); + console.log("\npatch2"); + const bin = await readAsync("data/prod/main.js", "utf8"); + await server.update(bin); + const patch2 = await server.getPatch(client.ts); + await client.applyPatch(patch2); + console.log( + hmn(patch2.length), + `|| client: ${hmn((await client.data).length)}` + ); - await client.applyPatch(newPatch); - expect(await client.data).toBe("rako12"); + console.log("\npatch3"); + const ubin = "mantappu" + bin?.substring(0,100000); + await server.update(ubin); + const patch3 = await server.getPatch(client.ts); + await client.applyPatch(patch3); + console.log( + hmn(patch3.length), + `|| client: ${hmn((await client.data).length)}` + ); }); + +function hmn(bytes: number): string { + const sizes = ["bytes", "KB", "MB", "GB", "TB"]; + if (bytes === 0) return "0 bytes"; + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const size = i === 0 ? bytes : (bytes / Math.pow(1024, i)).toFixed(2); + + return `${size} ${sizes[i]}`; +} diff --git a/pkgs/core/utils/diff.ts b/pkgs/core/utils/diff.ts index 274597c3..013fb7b5 100644 --- a/pkgs/core/utils/diff.ts +++ b/pkgs/core/utils/diff.ts @@ -1,12 +1,13 @@ -import { applyPatch, calcPatch } from "fast-myers-diff"; +import { applyPatch, calcPatch } from "./diff-internal"; import { Packr } from "msgpackr"; import { gunzip, gzip } from "zlib"; -const MAX_HISTORY = 10; +const MAX_HISTORY = 10; // max history item +const DIFF_TIMEOUT = 50; // in ms const packr = new Packr({}); -type PATCH_RESULT = +type PATCH_RESULT = | { mode: "new"; ts: string; @@ -22,7 +23,7 @@ export class Diff { ts = ""; mode: "client" | "server" = "server"; - private _data: number[] = []; + _data: number[] = []; private _history = {} as Record; constructor() {} @@ -58,20 +59,51 @@ export class Diff { } } - getPatch(ts: "new" | string): PATCH_RESULT { - if (ts !== "new") { - const old_data = this._history[ts]; + getPatch(ts: "new" | string) { + return new Promise((done) => { + if (ts !== "new") { + const old_data = this._history[ts]; - if (old_data) { - const result_diff = [...calcPatch(old_data, this._data)]; - return { diff: result_diff, mode: "patch", ts: this.ts }; + if (old_data) { + const now = performance.now(); + const result_diff = [ + ...calcPatch( + old_data, + this._data, + (key1, key2) => { + return old_data[key1] === this._data[key2]; + }, + () => { + return performance.now() - now > DIFF_TIMEOUT; + } + ), + ]; + + if (performance.now() - now <= DIFF_TIMEOUT) { + done( + new Uint8Array( + packr.pack({ diff: result_diff, mode: "patch", ts: this.ts }) + ) + ); + return; + } + } } - } - return { data: this._data, mode: "new", ts: this.ts }; + done( + new Uint8Array( + packr.pack({ data: this._data, mode: "new", ts: this.ts }) + ) + ); + }); } - async applyPatch(patch: PATCH_RESULT) { + async applyPatch(_patch: Uint8Array) { + const patch = packr.unpack(_patch) as PATCH_RESULT; + console.log( + patch.mode, + patch.mode === "new" ? patch.data.length : patch.diff.length + ); if (patch.mode === "new") { this.ts = patch.ts; if (patch.data) this._data = patch.data; @@ -97,14 +129,10 @@ export class Diff { return diff; } - static async client(patch: PATCH_RESULT) { + static async client(patch: Uint8Array) { const diff = new Diff(); diff.mode = "client"; - - if (patch.mode === "new") { - diff.ts = patch.ts; - if (patch.data) diff._data = patch.data; - } + diff.applyPatch(patch); return diff; } }