Skip to content

Commit 48e0afc

Browse files
authored
perf: increase parse speed ~10% by increasing initial arena size (#80)
Closes #75
1 parent 686643a commit 48e0afc

File tree

4 files changed

+104
-30
lines changed

4 files changed

+104
-30
lines changed

src/arena.test.ts

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { describe, test, expect } from 'vitest'
2+
import { readFileSync } from 'fs'
23
import { CSSDataArena, STYLESHEET, STYLE_RULE, DECLARATION, FLAG_IMPORTANT, FLAG_HAS_ERROR } from './arena'
4+
import { parse } from './parse'
35

46
describe('CSSDataArena', () => {
57
describe('initialization', () => {
@@ -237,34 +239,99 @@ describe('CSSDataArena', () => {
237239
})
238240
})
239241

240-
describe('capacity planning', () => {
241-
test('should have minimum capacity for empty files', () => {
242-
const capacity = CSSDataArena.capacity_for_source(0)
243-
expect(capacity).toBe(16)
242+
describe('growth tracking', () => {
243+
test('should initialize growth count to zero', () => {
244+
const arena = new CSSDataArena()
245+
expect(arena.get_growth_count()).toBe(0)
246+
})
247+
248+
test('should track single growth event', () => {
249+
const arena = new CSSDataArena(2)
250+
expect(arena.get_growth_count()).toBe(0)
251+
252+
// Create nodes to trigger growth
253+
arena.create_node(STYLESHEET, 0, 0, 1, 1) // count = 2
254+
expect(arena.get_growth_count()).toBe(0)
255+
256+
arena.create_node(STYLESHEET, 0, 0, 1, 1) // count = 3, triggers growth
257+
expect(arena.get_growth_count()).toBe(1)
258+
})
259+
260+
test('should track multiple growth events', () => {
261+
const arena = new CSSDataArena(2)
262+
expect(arena.get_growth_count()).toBe(0)
263+
264+
// First growth: 2 -> 3
265+
arena.create_node(STYLESHEET, 0, 0, 1, 1)
266+
arena.create_node(STYLESHEET, 0, 0, 1, 1)
267+
expect(arena.get_growth_count()).toBe(1)
268+
expect(arena.get_capacity()).toBe(3) // ceil(2 * 1.3) = 3
269+
270+
// Second growth: 3 -> 4
271+
arena.create_node(STYLESHEET, 0, 0, 1, 1)
272+
expect(arena.get_growth_count()).toBe(2)
273+
expect(arena.get_capacity()).toBe(4) // ceil(3 * 1.3) = 4
274+
275+
// Third growth: 4 -> 6
276+
arena.create_node(STYLESHEET, 0, 0, 1, 1)
277+
expect(arena.get_growth_count()).toBe(3)
278+
expect(arena.get_capacity()).toBe(6) // ceil(4 * 1.3) = 6
244279
})
245280

246-
test('should calculate capacity for small CSS files', () => {
247-
// 1KB CSS = 60 nodes * 1.15 buffer = 69 nodes
248-
const capacity = CSSDataArena.capacity_for_source(1024)
249-
expect(capacity).toBe(69)
281+
test('should not increment growth count when capacity is sufficient', () => {
282+
const arena = new CSSDataArena(100)
283+
284+
// Create many nodes without exceeding capacity
285+
for (let i = 0; i < 50; i++) {
286+
arena.create_node(STYLESHEET, 0, 0, 1, 1)
287+
}
288+
289+
expect(arena.get_growth_count()).toBe(0)
290+
expect(arena.get_count()).toBe(51) // 50 created + 1 initial
250291
})
292+
})
251293

252-
test('should calculate capacity for medium CSS files', () => {
253-
// 100KB CSS = 6000 nodes * 1.15 buffer = 6900 nodes
254-
const capacity = CSSDataArena.capacity_for_source(100 * 1024)
255-
expect(capacity).toBe(6900)
294+
describe('real-world CSS frameworks', () => {
295+
test('should not grow for Bootstrap CSS', () => {
296+
const css = readFileSync('node_modules/bootstrap/dist/css/bootstrap.css', 'utf-8')
297+
const result = parse(css)
298+
299+
expect(result.__get_arena().get_growth_count()).toBe(0)
300+
const utilization = (result.__get_arena().get_count() / result.__get_arena().get_capacity()) * 100
301+
expect(utilization).toBeLessThan(85)
302+
expect(utilization).toBeGreaterThan(30)
256303
})
257304

258-
test('should calculate capacity for large CSS files', () => {
259-
// 10MB = 10240 KB CSS = 614400 nodes * 1.15 buffer = 706560 nodes
260-
const capacity = CSSDataArena.capacity_for_source(10 * 1024 * 1024)
261-
expect(capacity).toBe(706560)
305+
test('should not grow for Bootstrap minified CSS', () => {
306+
const css = readFileSync('node_modules/bootstrap/dist/css/bootstrap.min.css', 'utf-8')
307+
const result = parse(css)
308+
const arena = result.__get_arena()
309+
310+
expect(arena.get_growth_count()).toBe(0)
311+
const utilization = (arena.get_count() / arena.get_capacity()) * 100
312+
expect(utilization).toBeLessThan(85)
313+
expect(utilization).toBeGreaterThan(30)
262314
})
263315

264-
test('should round up for partial KBs', () => {
265-
// 1.5KB = ceil(1.5 * 60) * 1.15 = 90 * 1.15 = 104 (rounded up)
266-
const capacity = CSSDataArena.capacity_for_source(1536)
267-
expect(capacity).toBe(104)
316+
test('should not grow for Tailwind CSS', () => {
317+
const css = readFileSync('node_modules/tailwindcss/dist/tailwind.css', 'utf-8')
318+
const result = parse(css)
319+
const arena = result.__get_arena()
320+
321+
expect(arena.get_growth_count()).toBe(0)
322+
const utilization = (arena.get_count() / arena.get_capacity()) * 100
323+
expect(utilization).toBeLessThan(85)
324+
expect(utilization).toBeGreaterThan(30)
325+
})
326+
327+
test('should not grow for Tailwind minified CSS', () => {
328+
const css = readFileSync('node_modules/tailwindcss/dist/tailwind.min.css', 'utf-8')
329+
const result = parse(css)
330+
331+
expect(result.__get_arena().get_growth_count()).toBe(0)
332+
const utilization = (result.__get_arena().get_count() / result.__get_arena().get_capacity()) * 100
333+
expect(utilization).toBeLessThan(85)
334+
expect(utilization).toBeGreaterThan(30)
268335
})
269336
})
270337
})

src/arena.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,22 @@ export class CSSDataArena {
118118
private view: DataView
119119
private capacity: number // Number of nodes that can fit
120120
private count: number // Number of nodes currently allocated
121+
private growth_count: number // Number of times the arena has grown
121122

122123
// Growth multiplier when capacity is exceeded
123124
private static readonly GROWTH_FACTOR = 1.3
124125

125126
// Estimated nodes per KB of CSS (based on real-world data)
126-
private static readonly NODES_PER_KB = 60
127+
private static readonly NODES_PER_KB = 270
127128

128129
// Buffer to avoid frequent growth (15%)
129-
private static readonly CAPACITY_BUFFER = 1.15
130+
private static readonly CAPACITY_BUFFER = 1.2
130131

131132
constructor(initial_capacity: number = 1024) {
132133
this.capacity = initial_capacity
133134
// Start count at 1 since 0 is reserved for "no node"
134135
this.count = 1
136+
this.growth_count = 0
135137
this.buffer = new ArrayBuffer(initial_capacity * BYTES_PER_NODE)
136138
this.view = new DataView(this.buffer)
137139
}
@@ -156,6 +158,11 @@ export class CSSDataArena {
156158
return this.capacity
157159
}
158160

161+
// Get the number of times the arena has grown
162+
get_growth_count(): number {
163+
return this.growth_count
164+
}
165+
159166
// Calculate byte offset for a node
160167
private node_offset(node_index: number): number {
161168
return node_index * BYTES_PER_NODE
@@ -311,6 +318,7 @@ export class CSSDataArena {
311318

312319
// Grow the arena by 1.3x when capacity is exceeded
313320
private grow(): void {
321+
this.growth_count++
314322
let new_capacity = Math.ceil(this.capacity * CSSDataArena.GROWTH_FACTOR)
315323
let new_buffer = new ArrayBuffer(new_capacity * BYTES_PER_NODE)
316324

@@ -325,13 +333,7 @@ export class CSSDataArena {
325333

326334
// Allocate and initialize a new node with core properties
327335
// Automatically grows the arena if capacity is exceeded
328-
create_node(
329-
type: number,
330-
start_offset: number,
331-
length: number,
332-
start_line: number,
333-
start_column: number
334-
): number {
336+
create_node(type: number, start_offset: number, length: number, start_line: number, start_column: number): number {
335337
if (this.count >= this.capacity) {
336338
this.grow()
337339
}

src/css-node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ export class CSSNode {
187187
this.index = index
188188
}
189189

190+
/** Get the arena (for internal/advanced use only) */
191+
__get_arena(): CSSDataArena {
192+
return this.arena
193+
}
194+
190195
/** Get node type as number (for performance) */
191196
get type(): CSSNodeType {
192197
return this.arena.get_type(this.index) as CSSNodeType

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"resolveJsonModule": true,
1010
"isolatedModules": true,
1111
"noEmit": true,
12-
"types": ["vitest/globals"]
12+
"types": ["vitest/globals", "node"]
1313
},
1414
"include": ["src/**/*"],
1515
"exclude": ["node_modules", "dist", "benchmark"]

0 commit comments

Comments
 (0)