From f24f6e0dc698c5b844b23296f04fd431109df52f Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 1 Mar 2026 09:55:07 +0300 Subject: [PATCH] =?UTF-8?q?Add=20.fig=20test=20fixtures=20(LFS)=20and=20fi?= =?UTF-8?q?x=20O(n=C2=B2)=20import=20bottleneck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Material 3 Design Kit and Nuxt UI v4 community files as test fixtures - Track tests/fixtures/*.fig with Git LFS - Fix O(n²) getChildren() in fig-import: build children index upfront - material3.fig (87K nodes): 37s → 535ms (69x faster) - nuxtui.fig (314K nodes): minutes → 2.3s - Optimize kiwi ByteBuffer: inline readVarUint, use TextDecoder for strings --- .gitattributes | 1 + packages/core/src/kiwi/fig-import.ts | 21 +++-- packages/core/src/kiwi/kiwi-schema/bb.ts | 107 ++++++----------------- tests/fixtures/material3.fig | 3 + tests/fixtures/nuxtui.fig | 3 + 5 files changed, 48 insertions(+), 87 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/fixtures/material3.fig create mode 100644 tests/fixtures/nuxtui.fig diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6dae82e6d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +tests/fixtures/*.fig filter=lfs diff=lfs merge=lfs -text diff --git a/packages/core/src/kiwi/fig-import.ts b/packages/core/src/kiwi/fig-import.ts index cda4506a7..263b61d4a 100644 --- a/packages/core/src/kiwi/fig-import.ts +++ b/packages/core/src/kiwi/fig-import.ts @@ -374,6 +374,7 @@ export function importNodeChanges( const changeMap = new Map() const parentMap = new Map() + const childrenMap = new Map() for (const nc of nodeChanges) { if (!nc.guid) continue @@ -382,21 +383,27 @@ export function importNodeChanges( changeMap.set(id, nc) if (nc.parentIndex?.guid) { - parentMap.set(id, guidToString(nc.parentIndex.guid)) + const pid = guidToString(nc.parentIndex.guid) + parentMap.set(id, pid) + let siblings = childrenMap.get(pid) + if (!siblings) { + siblings = [] + childrenMap.set(pid, siblings) + } + siblings.push(id) } } - function getChildren(ncId: string): string[] { - const children: string[] = [] - for (const [childId, pid] of parentMap) { - if (pid === ncId) children.push(childId) - } + for (const [, children] of childrenMap) { children.sort((a, b) => { const aPos = changeMap.get(a)?.parentIndex?.position ?? '' const bPos = changeMap.get(b)?.parentIndex?.position ?? '' return aPos.localeCompare(bPos) }) - return children + } + + function getChildren(ncId: string): string[] { + return childrenMap.get(ncId) ?? [] } const created = new Set() diff --git a/packages/core/src/kiwi/kiwi-schema/bb.ts b/packages/core/src/kiwi/kiwi-schema/bb.ts index 84f703c84..b6facbe20 100644 --- a/packages/core/src/kiwi/kiwi-schema/bb.ts +++ b/packages/core/src/kiwi/kiwi-schema/bb.ts @@ -1,5 +1,6 @@ let int32 = new Int32Array(1) let float32 = new Float32Array(int32.buffer) +const textDecoder = new TextDecoder() export class ByteBuffer { private _data: Uint8Array @@ -20,64 +21,46 @@ export class ByteBuffer { } readByte(): number { - if (this._index + 1 > this._data.length) { - throw new Error('Index out of bounds') - } return this._data[this._index++] } readByteArray(): Uint8Array { - let length = this.readVarUint() - let start = this._index - let end = start + length - if (end > this._data.length) { - throw new Error('Read array out of bounds') - } - this._index = end - // Copy into a new array instead of just creating another view. - let result = new Uint8Array(length) - result.set(this._data.subarray(start, end)) - return result + const length = this.readVarUint() + const start = this._index + this._index = start + length + return this._data.slice(start, start + length) } readVarFloat(): number { - let index = this._index - let data = this._data - let length = data.length - - // Optimization: use a single byte to store zero - if (index + 1 > length) { - throw new Error('Index out of bounds') - } - let first = data[index] + const index = this._index + const data = this._data + const first = data[index] if (first === 0) { this._index = index + 1 return 0 } - // Endian-independent 32-bit read - if (index + 4 > length) { - throw new Error('Index out of bounds') - } let bits = first | (data[index + 1] << 8) | (data[index + 2] << 16) | (data[index + 3] << 24) this._index = index + 4 - - // Move the exponent back into place bits = (bits << 23) | (bits >>> 9) - - // Reinterpret as a floating-point number int32[0] = bits return float32[0] } readVarUint(): number { - let value = 0 - let shift = 0 - do { - var byte = this.readByte() - value |= (byte & 127) << shift - shift += 7 - } while (byte & 128 && shift < 35) + const data = this._data + let i = this._index + let b = data[i++] + let value = b & 127 + if (b < 128) { this._index = i; return value } + b = data[i++]; value |= (b & 127) << 7 + if (b < 128) { this._index = i; return value } + b = data[i++]; value |= (b & 127) << 14 + if (b < 128) { this._index = i; return value } + b = data[i++]; value |= (b & 127) << 21 + if (b < 128) { this._index = i; return value } + b = data[i++]; value |= (b & 127) << 28 + this._index = i return value >>> 0 } @@ -108,48 +91,12 @@ export class ByteBuffer { } readString(): string { - let result = '' - - while (true) { - let codePoint - - // Decode UTF-8 - let a = this.readByte() - if (a < 0xc0) { - codePoint = a - } else { - let b = this.readByte() - if (a < 0xe0) { - codePoint = ((a & 0x1f) << 6) | (b & 0x3f) - } else { - let c = this.readByte() - if (a < 0xf0) { - codePoint = ((a & 0x0f) << 12) | ((b & 0x3f) << 6) | (c & 0x3f) - } else { - let d = this.readByte() - codePoint = ((a & 0x07) << 18) | ((b & 0x3f) << 12) | ((c & 0x3f) << 6) | (d & 0x3f) - } - } - } - - // Strings are null-terminated - if (codePoint === 0) { - break - } - - // Encode UTF-16 - if (codePoint < 0x10000) { - result += String.fromCharCode(codePoint) - } else { - codePoint -= 0x10000 - result += String.fromCharCode( - (codePoint >> 10) + 0xd800, - (codePoint & ((1 << 10) - 1)) + 0xdc00 - ) - } - } - - return result + const data = this._data + const start = this._index + let i = start + while (data[i] !== 0) i++ + this._index = i + 1 + return textDecoder.decode(data.subarray(start, i)) } private _growBy(amount: number): void { diff --git a/tests/fixtures/material3.fig b/tests/fixtures/material3.fig new file mode 100644 index 000000000..e16fff601 --- /dev/null +++ b/tests/fixtures/material3.fig @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75c99e40a4a0e62db6e8f1127ef586369cc6737bd8c9f6369191ce7431f43519 +size 57312586 diff --git a/tests/fixtures/nuxtui.fig b/tests/fixtures/nuxtui.fig new file mode 100644 index 000000000..a43207585 --- /dev/null +++ b/tests/fixtures/nuxtui.fig @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:add13fd8037ea2d1c3df1e86a224425b0c11dfa093f102a60b9e122ac8ea4361 +size 85671218