Skip to content

Commit 24464d5

Browse files
authored
perf: reduce arena function calls (#43)
1 parent 808f0d8 commit 24464d5

File tree

8 files changed

+245
-256
lines changed

8 files changed

+245
-256
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
uses: codecov/codecov-action@v5
2525
with:
2626
token: ${{ secrets.CODECOV_TOKEN }}
27+
slug: projectwallace/css-parser
2728

2829
check-ts:
2930
name: Check types

src/arena.test.ts

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, test, expect } from 'vitest'
2-
import { CSSDataArena, STYLESHEET, DECLARATION, FLAG_IMPORTANT, FLAG_HAS_ERROR } from './arena'
2+
import { CSSDataArena, STYLESHEET, STYLE_RULE, DECLARATION, FLAG_IMPORTANT, FLAG_HAS_ERROR } from './arena'
33

44
describe('CSSDataArena', () => {
55
describe('initialization', () => {
@@ -20,24 +20,24 @@ describe('CSSDataArena', () => {
2020
test('should create nodes and increment count', () => {
2121
const arena = new CSSDataArena(10)
2222

23-
const node1 = arena.create_node()
23+
const node1 = arena.create_node(STYLESHEET, 0, 0, 1, 1)
2424
expect(node1).toBe(1) // First node index is 1 (0 is reserved for "no node")
2525
expect(arena.get_count()).toBe(2)
2626

27-
const node2 = arena.create_node()
27+
const node2 = arena.create_node(STYLESHEET, 0, 0, 1, 1)
2828
expect(node2).toBe(2)
2929
expect(arena.get_count()).toBe(3)
3030
})
3131

3232
test('should automatically grow when capacity is exceeded', () => {
3333
const arena = new CSSDataArena(3)
3434

35-
arena.create_node() // 1
36-
arena.create_node() // 2
35+
arena.create_node(STYLESHEET, 0, 0, 1, 1) // 1
36+
arena.create_node(STYLESHEET, 0, 0, 1, 1) // 2
3737
expect(arena.get_capacity()).toBe(3)
3838

3939
// This should trigger growth (count is now 3, capacity is 3)
40-
const node3 = arena.create_node() // 3
40+
const node3 = arena.create_node(STYLESHEET, 0, 0, 1, 1) // 3
4141
expect(node3).toBe(3)
4242
expect(arena.get_count()).toBe(4)
4343
// Capacity should be ceil(3 * 1.3) = 4
@@ -47,17 +47,11 @@ describe('CSSDataArena', () => {
4747
test('should preserve existing data when growing', () => {
4848
const arena = new CSSDataArena(2)
4949

50-
const node1 = arena.create_node()
51-
const node2 = arena.create_node()
52-
53-
// Set data on existing nodes
54-
arena.set_type(node1, STYLESHEET)
55-
arena.set_start_offset(node1, 100)
56-
arena.set_type(node2, DECLARATION)
57-
arena.set_start_offset(node2, 200)
50+
const node1 = arena.create_node(STYLESHEET, 100, 0, 1, 1)
51+
const node2 = arena.create_node(DECLARATION, 200, 0, 1, 1)
5852

5953
// Trigger growth
60-
const node3 = arena.create_node()
54+
const node3 = arena.create_node(STYLESHEET, 0, 0, 1, 1)
6155

6256
// Verify old data is preserved
6357
expect(arena.get_type(node1)).toBe(STYLESHEET)
@@ -74,41 +68,37 @@ describe('CSSDataArena', () => {
7468
describe('node reading and writing', () => {
7569
test('should read default values for uninitialized nodes', () => {
7670
const arena = new CSSDataArena(10)
77-
const node = arena.create_node()
71+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
7872

79-
expect(arena.get_type(node)).toBe(0)
73+
expect(arena.get_type(node)).toBe(STYLESHEET)
8074
expect(arena.get_flags(node)).toBe(0)
8175
expect(arena.get_start_offset(node)).toBe(0)
8276
expect(arena.get_length(node)).toBe(0)
8377
})
8478

8579
test('should write and read node type', () => {
8680
const arena = new CSSDataArena(10)
87-
const node = arena.create_node()
81+
const node = arena.create_node(DECLARATION, 0, 0, 1, 1)
8882

8983
arena.set_type(node, STYLESHEET)
9084
expect(arena.get_type(node)).toBe(STYLESHEET)
9185
})
9286

9387
test('should write and read node flags', () => {
9488
const arena = new CSSDataArena(10)
95-
const node = arena.create_node()
89+
const node = arena.create_node(DECLARATION, 0, 0, 1, 1)
9690

9791
arena.set_flags(node, FLAG_IMPORTANT)
9892
expect(arena.get_flags(node)).toBe(FLAG_IMPORTANT)
9993
})
10094

10195
test('should write and read all node properties', () => {
10296
const arena = new CSSDataArena(10)
103-
const node = arena.create_node()
97+
const node = arena.create_node(DECLARATION, 100, 50, 5, 1)
10498

105-
arena.set_type(node, DECLARATION)
10699
arena.set_flags(node, FLAG_IMPORTANT)
107-
arena.set_start_offset(node, 100)
108-
arena.set_length(node, 50)
109-
arena.set_content_start(node, 110)
100+
arena.set_content_start_delta(node, 10)
110101
arena.set_content_length(node, 30)
111-
arena.set_start_line(node, 5)
112102

113103
expect(arena.get_type(node)).toBe(DECLARATION)
114104
expect(arena.get_flags(node)).toBe(FLAG_IMPORTANT)
@@ -121,14 +111,8 @@ describe('CSSDataArena', () => {
121111

122112
test('should handle multiple nodes independently', () => {
123113
const arena = new CSSDataArena(10)
124-
const node1 = arena.create_node()
125-
const node2 = arena.create_node()
126-
127-
arena.set_type(node1, STYLESHEET)
128-
arena.set_start_offset(node1, 0)
129-
130-
arena.set_type(node2, DECLARATION)
131-
arena.set_start_offset(node2, 100)
114+
const node1 = arena.create_node(STYLESHEET, 0, 0, 1, 1)
115+
const node2 = arena.create_node(DECLARATION, 100, 0, 1, 1)
132116

133117
expect(arena.get_type(node1)).toBe(STYLESHEET)
134118
expect(arena.get_start_offset(node1)).toBe(0)
@@ -140,10 +124,10 @@ describe('CSSDataArena', () => {
140124
describe('tree linking', () => {
141125
test('should append first child to parent', () => {
142126
const arena = new CSSDataArena(10)
143-
const parent = arena.create_node()
144-
const child = arena.create_node()
127+
const parent = arena.create_node(STYLESHEET, 0, 0, 1, 1)
128+
const child = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
145129

146-
arena.append_child(parent, child)
130+
arena.append_children(parent, [child])
147131

148132
expect(arena.get_first_child(parent)).toBe(child)
149133
expect(arena.has_children(parent)).toBe(true)
@@ -152,14 +136,12 @@ describe('CSSDataArena', () => {
152136

153137
test('should append multiple children as siblings', () => {
154138
const arena = new CSSDataArena(10)
155-
const parent = arena.create_node()
156-
const child1 = arena.create_node()
157-
const child2 = arena.create_node()
158-
const child3 = arena.create_node()
139+
const parent = arena.create_node(STYLESHEET, 0, 0, 1, 1)
140+
const child1 = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
141+
const child2 = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
142+
const child3 = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
159143

160-
arena.append_child(parent, child1)
161-
arena.append_child(parent, child2)
162-
arena.append_child(parent, child3)
144+
arena.append_children(parent, [child1, child2, child3])
163145

164146
expect(arena.get_first_child(parent)).toBe(child1)
165147
expect(arena.get_next_sibling(child1)).toBe(child2)
@@ -169,18 +151,16 @@ describe('CSSDataArena', () => {
169151

170152
test('should build complex tree structure', () => {
171153
const arena = new CSSDataArena(10)
172-
const root = arena.create_node()
173-
const rule1 = arena.create_node()
174-
const rule2 = arena.create_node()
175-
const decl1 = arena.create_node()
176-
const decl2 = arena.create_node()
154+
const root = arena.create_node(STYLESHEET, 0, 0, 1, 1)
155+
const rule1 = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
156+
const rule2 = arena.create_node(STYLE_RULE, 0, 0, 1, 1)
157+
const decl1 = arena.create_node(DECLARATION, 0, 0, 1, 1)
158+
const decl2 = arena.create_node(DECLARATION, 0, 0, 1, 1)
177159

178160
// Build tree: root -> [rule1, rule2]
179161
// rule1 -> [decl1, decl2]
180-
arena.append_child(root, rule1)
181-
arena.append_child(root, rule2)
182-
arena.append_child(rule1, decl1)
183-
arena.append_child(rule1, decl2)
162+
arena.append_children(root, [rule1, rule2])
163+
arena.append_children(rule1, [decl1, decl2])
184164

185165
// Verify root level
186166
expect(arena.get_first_child(root)).toBe(rule1)
@@ -198,7 +178,7 @@ describe('CSSDataArena', () => {
198178

199179
test('should handle nodes with no children or siblings', () => {
200180
const arena = new CSSDataArena(10)
201-
const node = arena.create_node()
181+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
202182

203183
expect(arena.has_children(node)).toBe(false)
204184
expect(arena.has_next_sibling(node)).toBe(false)
@@ -210,7 +190,7 @@ describe('CSSDataArena', () => {
210190
describe('flag management', () => {
211191
test('should set and check individual flags', () => {
212192
const arena = new CSSDataArena(10)
213-
const node = arena.create_node()
193+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
214194

215195
expect(arena.has_flag(node, FLAG_IMPORTANT)).toBe(false)
216196

@@ -220,7 +200,7 @@ describe('CSSDataArena', () => {
220200

221201
test('should set multiple flags independently', () => {
222202
const arena = new CSSDataArena(10)
223-
const node = arena.create_node()
203+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
224204

225205
arena.set_flag(node, FLAG_IMPORTANT)
226206

@@ -230,7 +210,7 @@ describe('CSSDataArena', () => {
230210

231211
test('should clear individual flags without affecting others', () => {
232212
const arena = new CSSDataArena(10)
233-
const node = arena.create_node()
213+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
234214

235215
arena.set_flag(node, FLAG_IMPORTANT)
236216
arena.set_flag(node, FLAG_HAS_ERROR)
@@ -241,7 +221,7 @@ describe('CSSDataArena', () => {
241221

242222
test('should handle all flag combinations', () => {
243223
const arena = new CSSDataArena(10)
244-
const node = arena.create_node()
224+
const node = arena.create_node(STYLESHEET, 0, 0, 1, 1)
245225

246226
// Set all flags at once using setFlags
247227
const allFlags = FLAG_IMPORTANT | FLAG_HAS_ERROR

src/arena.ts

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,8 @@ export class CSSDataArena {
238238
this.view.setUint16(this.node_offset(node_index) + 8, length, true)
239239
}
240240

241-
// Write content start offset (stored as delta from startOffset)
242-
set_content_start(node_index: number, offset: number): void {
243-
const startOffset = this.get_start_offset(node_index)
244-
const delta = offset - startOffset
241+
// Write content start delta (offset from startOffset)
242+
set_content_start_delta(node_index: number, delta: number): void {
245243
this.view.setUint16(this.node_offset(node_index) + 12, delta, true)
246244
}
247245

@@ -285,10 +283,8 @@ export class CSSDataArena {
285283
this.view.setUint16(this.node_offset(node_index) + 36, column, true)
286284
}
287285

288-
// Write value start offset (stored as delta from startOffset, declaration value / at-rule prelude)
289-
set_value_start(node_index: number, offset: number): void {
290-
const startOffset = this.get_start_offset(node_index)
291-
const delta = offset - startOffset
286+
// Write value start delta (offset from startOffset, declaration value / at-rule prelude)
287+
set_value_start_delta(node_index: number, delta: number): void {
292288
this.view.setUint16(this.node_offset(node_index) + 16, delta, true)
293289
}
294290

@@ -313,35 +309,45 @@ export class CSSDataArena {
313309
this.capacity = new_capacity
314310
}
315311

316-
// Allocate a new node and return its index
317-
// The node is zero-initialized by default (ArrayBuffer guarantees this)
312+
// Allocate and initialize a new node with core properties
318313
// Automatically grows the arena if capacity is exceeded
319-
create_node(): number {
314+
create_node(
315+
type: number,
316+
start_offset: number,
317+
length: number,
318+
start_line: number,
319+
start_column: number
320+
): number {
320321
if (this.count >= this.capacity) {
321322
this.grow()
322323
}
323-
let node_index = this.count
324+
const node_index = this.count
324325
this.count++
326+
327+
const offset = node_index * BYTES_PER_NODE
328+
this.view.setUint8(offset, type) // +0: type
329+
this.view.setUint32(offset + 4, start_offset, true) // +4: startOffset
330+
this.view.setUint16(offset + 8, length, true) // +8: length
331+
this.view.setUint32(offset + 32, start_line, true) // +32: startLine
332+
this.view.setUint16(offset + 36, start_column, true) // +36: startColumn
333+
325334
return node_index
326335
}
327336

328337
// --- Tree Building Helpers ---
329338

330-
// Add a child node to a parent node
331-
// This appends to the end of the child list using the sibling chain
332-
// O(1) operation using lastChild pointer
333-
append_child(parentIndex: number, childIndex: number): void {
334-
let last_child = this.get_last_child(parentIndex)
335-
336-
if (last_child === 0) {
337-
// No children yet, make this the first and last child
338-
this.set_first_child(parentIndex, childIndex)
339-
this.set_last_child(parentIndex, childIndex)
340-
} else {
341-
// Append to the current last child's sibling chain
342-
this.set_next_sibling(last_child, childIndex)
343-
// Update parent's last child pointer
344-
this.set_last_child(parentIndex, childIndex)
339+
// Link multiple child nodes to a parent
340+
// Children are linked as siblings in the order provided
341+
append_children(parent_index: number, children: number[]): void {
342+
if (children.length === 0) return
343+
344+
const offset = this.node_offset(parent_index)
345+
this.view.setUint32(offset + 20, children[0], true) // firstChild
346+
this.view.setUint32(offset + 24, children[children.length - 1], true) // lastChild
347+
348+
// Chain siblings
349+
for (let i = 0; i < children.length - 1; i++) {
350+
this.set_next_sibling(children[i], children[i + 1])
345351
}
346352
}
347353

src/parse-anplusb.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,21 +265,23 @@ export class ANplusBParser {
265265
}
266266

267267
private create_anplusb_node(start: number, a_start: number, a_end: number, b_start: number, b_end: number): number {
268-
const node = this.arena.create_node()
269-
this.arena.set_type(node, NTH_SELECTOR)
270-
this.arena.set_start_offset(node, start)
271-
this.arena.set_length(node, this.lexer.pos - start)
272-
this.arena.set_start_line(node, this.lexer.line)
268+
const node = this.arena.create_node(
269+
NTH_SELECTOR,
270+
start,
271+
this.lexer.pos - start,
272+
this.lexer.line,
273+
1
274+
)
273275

274276
// Store 'a' coefficient in content fields if it exists (length > 0)
275277
if (a_end > a_start) {
276-
this.arena.set_content_start(node, a_start)
278+
this.arena.set_content_start_delta(node, a_start - start)
277279
this.arena.set_content_length(node, a_end - a_start)
278280
}
279281

280282
// Store 'b' coefficient in value fields if it exists (length > 0)
281283
if (b_end > b_start) {
282-
this.arena.set_value_start(node, b_start)
284+
this.arena.set_value_start_delta(node, b_start - start)
283285
this.arena.set_value_length(node, b_end - b_start)
284286
}
285287

0 commit comments

Comments
 (0)