Add .fig test fixtures (LFS) and fix O(n²) import bottleneck

- 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
This commit is contained in:
Danila Poyarkov 2026-03-01 09:55:07 +03:00
parent 88bfacdc9e
commit f24f6e0dc6
5 changed files with 48 additions and 87 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
tests/fixtures/*.fig filter=lfs diff=lfs merge=lfs -text

View file

@ -374,6 +374,7 @@ export function importNodeChanges(
const changeMap = new Map<string, NodeChange>()
const parentMap = new Map<string, string>()
const childrenMap = new Map<string, string[]>()
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<string>()

View file

@ -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 {

3
tests/fixtures/material3.fig vendored Normal file
View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75c99e40a4a0e62db6e8f1127ef586369cc6737bd8c9f6369191ce7431f43519
size 57312586

3
tests/fixtures/nuxtui.fig vendored Normal file
View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:add13fd8037ea2d1c3df1e86a224425b0c11dfa093f102a60b9e122ac8ea4361
size 85671218