Skip to content

Commit fd42b04

Browse files
authored
perf: reorganize arena to save ~~4~~ 8 bytes (20%) (#59)
No measurable perf gains in benchmark, but memory benchmark comparisons against postcss and cstree show 2% (e.g. 88% vs. 90% efficiency) improvement at no perf cost.
1 parent f6349e7 commit fd42b04

File tree

2 files changed

+53
-71
lines changed

2 files changed

+53
-71
lines changed

src/arena.ts

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,49 @@
11
// CSS Data Arena - Single contiguous ArrayBuffer for all AST nodes
22
//
3-
// Each node occupies 40 bytes with the following layout:
3+
// Each node occupies 32 bytes with the following layout:
44
// Offset | Size | Field
55
// -------|------|-------------
66
// 0 | 1 | type
77
// 1 | 1 | flags
8-
// 2 | 2 | (padding)
9-
// 4 | 4 | startOffset
10-
// 8 | 2 | length
11-
// 10 | 2 | (padding)
12-
// 12 | 2 | contentStartDelta (offset from startOffset, property name / at-rule name)
13-
// 14 | 2 | contentLength
14-
// 16 | 2 | valueStartDelta (offset from startOffset, declaration value / at-rule prelude)
15-
// 18 | 2 | valueLength
16-
// 20 | 4 | firstChild
17-
// 24 | 4 | lastChild
18-
// 28 | 4 | nextSibling
19-
// 32 | 4 | startLine
20-
// 36 | 2 | startColumn
21-
// 38 | 2 | (padding)
8+
// 2 | 2 | length
9+
// 4 | 4 | firstChild
10+
// 8 | 4 | nextSibling
11+
// 12 | 4 | startOffset
12+
// 16 | 2 | contentStartDelta (offset from startOffset, property name / at-rule name)
13+
// 18 | 2 | valueStartDelta (offset from startOffset, declaration value / at-rule prelude)
14+
// 20 | 2 | contentLength
15+
// 22 | 2 | valueLength
16+
// 24 | 4 | startLine
17+
// 28 | 2 | startColumn
18+
// 30 | 1 | attr_operator (reusing padding)
19+
// 31 | 1 | attr_flags (reusing padding)
2220
//
2321
// HOW THE ARENA WORKS:
24-
// 1. BYTES_PER_NODE defines the size of each node (40 bytes). The ArrayBuffer size is calculated
25-
// as: capacity × BYTES_PER_NODE. For example, 1024 nodes = 40,960 bytes (~40KB).
26-
// Node indices map to byte offsets via: node_offset = node_index × 40.
22+
// 1. BYTES_PER_NODE defines the size of each node (32 bytes). The ArrayBuffer size is calculated
23+
// as: capacity × BYTES_PER_NODE. For example, 1024 nodes = 32,768 bytes (32KB).
24+
// Node indices map to byte offsets via: node_offset = node_index × 32.
2725
//
2826
// 2. We use a single DataView over the ArrayBuffer to read/write different types at specific offsets.
2927
// - Uint8: 1-byte reads/writes for type, flags (e.g., view.getUint8(offset))
3028
// - Uint16: 2-byte reads/writes for length, deltas, column (e.g., view.getUint16(offset, true))
3129
// - Uint32: 4-byte reads/writes for startOffset, pointers, line (e.g., view.getUint32(offset, true))
3230
// The 'true' parameter specifies little-endian byte order (native on x86/ARM CPUs).
3331
//
34-
// 3. Padding (6 bytes total at offsets 2-3, 10-11, 38-39) ensures memory alignment for performance:
35-
// - Uint32 fields align to 4-byte boundaries (offsets 4, 20, 24, 28, 32)
36-
// - Uint16 fields align to 2-byte boundaries (offsets 8, 10, 12, 14, 16, 18, 36, 38)
32+
// 3. Padding (2 bytes total at offsets 30-31) ensures memory alignment for performance:
33+
// - Uint32 fields align to 4-byte boundaries (offsets 4, 8, 12, 24)
34+
// - Uint16 fields align to 2-byte boundaries (offsets 2, 16, 18, 20, 22, 28)
3735
// Aligned access is faster (single CPU instruction) vs unaligned (multiple memory accesses).
3836
// Modern CPUs penalize unaligned reads/writes, making padding essential for performance.
3937
//
40-
// 4. The padding at offset 2-3 is reused for attribute selector data (attr_operator, attr_flags),
38+
// 4. The padding at offset 30-31 is reused for attribute selector data (attr_operator, attr_flags),
4139
// making efficient use of otherwise wasted bytes. This is a space optimization trick.
4240
//
4341
// 5. Delta offsets (contentStartDelta, valueStartDelta) save memory: instead of storing absolute
44-
// positions as uint32 (4 bytes), we store relative offsets as uint16 (2 bytes). This reduced
45-
// node size from 44→40 bytes (10% smaller), saving memory while maintaining performance.
42+
// positions as uint32 (4 bytes), we store relative offsets as uint16 (2 bytes). Removing unused
43+
// lastChild field saved another 4 bytes. This reduced node size from 44→40→36→32 bytes (27%
44+
// smaller than original), saving memory while maintaining performance.
4645

47-
let BYTES_PER_NODE = 40
46+
let BYTES_PER_NODE = 32
4847

4948
// Node type constants
5049
export const STYLESHEET = 1
@@ -174,71 +173,66 @@ export class CSSDataArena {
174173

175174
// Read start offset in source
176175
get_start_offset(node_index: number): number {
177-
return this.view.getUint32(this.node_offset(node_index) + 4, true)
176+
return this.view.getUint32(this.node_offset(node_index) + 12, true)
178177
}
179178

180179
// Read length in source
181180
get_length(node_index: number): number {
182-
return this.view.getUint16(this.node_offset(node_index) + 8, true)
181+
return this.view.getUint16(this.node_offset(node_index) + 2, true)
183182
}
184183

185184
// Read content start offset (stored as delta from startOffset)
186185
get_content_start(node_index: number): number {
187186
const startOffset = this.get_start_offset(node_index)
188-
const delta = this.view.getUint16(this.node_offset(node_index) + 12, true)
187+
const delta = this.view.getUint16(this.node_offset(node_index) + 16, true)
189188
return startOffset + delta
190189
}
191190

192191
// Read content length
193192
get_content_length(node_index: number): number {
194-
return this.view.getUint16(this.node_offset(node_index) + 14, true)
193+
return this.view.getUint16(this.node_offset(node_index) + 20, true)
195194
}
196195

197196
// Read attribute operator (for NODE_SELECTOR_ATTRIBUTE)
198197
get_attr_operator(node_index: number): number {
199-
return this.view.getUint8(this.node_offset(node_index) + 2)
198+
return this.view.getUint8(this.node_offset(node_index) + 30)
200199
}
201200

202201
// Read attribute flags (for NODE_SELECTOR_ATTRIBUTE)
203202
get_attr_flags(node_index: number): number {
204-
return this.view.getUint8(this.node_offset(node_index) + 3)
203+
return this.view.getUint8(this.node_offset(node_index) + 31)
205204
}
206205

207206
// Read first child index (0 = no children)
208207
get_first_child(node_index: number): number {
209-
return this.view.getUint32(this.node_offset(node_index) + 20, true)
210-
}
211-
212-
// Read last child index (0 = no children)
213-
get_last_child(node_index: number): number {
214-
return this.view.getUint32(this.node_offset(node_index) + 24, true)
208+
return this.view.getUint32(this.node_offset(node_index) + 4, true)
215209
}
216210

217211
// Read next sibling index (0 = no sibling)
218212
get_next_sibling(node_index: number): number {
219-
return this.view.getUint32(this.node_offset(node_index) + 28, true)
213+
return this.view.getUint32(this.node_offset(node_index) + 8, true)
220214
}
221215

222216
// Read start line
223217
get_start_line(node_index: number): number {
224-
return this.view.getUint32(this.node_offset(node_index) + 32, true)
218+
return this.view.getUint32(this.node_offset(node_index) + 24, true)
225219
}
226220

227221
// Read start column
228222
get_start_column(node_index: number): number {
229-
return this.view.getUint16(this.node_offset(node_index) + 36, true)
223+
return this.view.getUint16(this.node_offset(node_index) + 28, true)
230224
}
231225

232226
// Read value start offset (stored as delta from startOffset, declaration value / at-rule prelude)
233227
get_value_start(node_index: number): number {
234228
const startOffset = this.get_start_offset(node_index)
235-
const delta = this.view.getUint16(this.node_offset(node_index) + 16, true)
229+
const delta = this.view.getUint16(this.node_offset(node_index) + 18, true)
236230
return startOffset + delta
237231
}
238232

239233
// Read value length
240234
get_value_length(node_index: number): number {
241-
return this.view.getUint16(this.node_offset(node_index) + 18, true)
235+
return this.view.getUint16(this.node_offset(node_index) + 22, true)
242236
}
243237

244238
// --- Write Methods ---
@@ -255,67 +249,62 @@ export class CSSDataArena {
255249

256250
// Write start offset in source
257251
set_start_offset(node_index: number, offset: number): void {
258-
this.view.setUint32(this.node_offset(node_index) + 4, offset, true)
252+
this.view.setUint32(this.node_offset(node_index) + 12, offset, true)
259253
}
260254

261255
// Write length in source
262256
set_length(node_index: number, length: number): void {
263-
this.view.setUint16(this.node_offset(node_index) + 8, length, true)
257+
this.view.setUint16(this.node_offset(node_index) + 2, length, true)
264258
}
265259

266260
// Write content start delta (offset from startOffset)
267261
set_content_start_delta(node_index: number, delta: number): void {
268-
this.view.setUint16(this.node_offset(node_index) + 12, delta, true)
262+
this.view.setUint16(this.node_offset(node_index) + 16, delta, true)
269263
}
270264

271265
// Write content length
272266
set_content_length(node_index: number, length: number): void {
273-
this.view.setUint16(this.node_offset(node_index) + 14, length, true)
267+
this.view.setUint16(this.node_offset(node_index) + 20, length, true)
274268
}
275269

276270
// Write attribute operator (for NODE_SELECTOR_ATTRIBUTE)
277271
set_attr_operator(node_index: number, operator: number): void {
278-
this.view.setUint8(this.node_offset(node_index) + 2, operator)
272+
this.view.setUint8(this.node_offset(node_index) + 30, operator)
279273
}
280274

281275
// Write attribute flags (for NODE_SELECTOR_ATTRIBUTE)
282276
set_attr_flags(node_index: number, flags: number): void {
283-
this.view.setUint8(this.node_offset(node_index) + 3, flags)
277+
this.view.setUint8(this.node_offset(node_index) + 31, flags)
284278
}
285279

286280
// Write first child index
287281
set_first_child(node_index: number, childIndex: number): void {
288-
this.view.setUint32(this.node_offset(node_index) + 20, childIndex, true)
289-
}
290-
291-
// Write last child index
292-
set_last_child(node_index: number, childIndex: number): void {
293-
this.view.setUint32(this.node_offset(node_index) + 24, childIndex, true)
282+
this.view.setUint32(this.node_offset(node_index) + 4, childIndex, true)
294283
}
295284

296285
// Write next sibling index
297286
set_next_sibling(node_index: number, siblingIndex: number): void {
298-
this.view.setUint32(this.node_offset(node_index) + 28, siblingIndex, true)
287+
this.view.setUint32(this.node_offset(node_index) + 8, siblingIndex, true)
299288
}
300289

301290
// Write start line
302291
set_start_line(node_index: number, line: number): void {
303-
this.view.setUint32(this.node_offset(node_index) + 32, line, true)
292+
this.view.setUint32(this.node_offset(node_index) + 24, line, true)
304293
}
305294

306295
// Write start column
307296
set_start_column(node_index: number, column: number): void {
308-
this.view.setUint16(this.node_offset(node_index) + 36, column, true)
297+
this.view.setUint16(this.node_offset(node_index) + 28, column, true)
309298
}
310299

311300
// Write value start delta (offset from startOffset, declaration value / at-rule prelude)
312301
set_value_start_delta(node_index: number, delta: number): void {
313-
this.view.setUint16(this.node_offset(node_index) + 16, delta, true)
302+
this.view.setUint16(this.node_offset(node_index) + 18, delta, true)
314303
}
315304

316305
// Write value length
317306
set_value_length(node_index: number, length: number): void {
318-
this.view.setUint16(this.node_offset(node_index) + 18, length, true)
307+
this.view.setUint16(this.node_offset(node_index) + 22, length, true)
319308
}
320309

321310
// --- Node Creation ---
@@ -351,10 +340,10 @@ export class CSSDataArena {
351340

352341
const offset = node_index * BYTES_PER_NODE
353342
this.view.setUint8(offset, type) // +0: type
354-
this.view.setUint32(offset + 4, start_offset, true) // +4: startOffset
355-
this.view.setUint16(offset + 8, length, true) // +8: length
356-
this.view.setUint32(offset + 32, start_line, true) // +32: startLine
357-
this.view.setUint16(offset + 36, start_column, true) // +36: startColumn
343+
this.view.setUint16(offset + 2, length, true) // +2: length
344+
this.view.setUint32(offset + 12, start_offset, true) // +12: startOffset
345+
this.view.setUint32(offset + 24, start_line, true) // +24: startLine
346+
this.view.setUint16(offset + 28, start_column, true) // +28: startColumn
358347

359348
return node_index
360349
}
@@ -367,8 +356,7 @@ export class CSSDataArena {
367356
if (children.length === 0) return
368357

369358
const offset = this.node_offset(parent_index)
370-
this.view.setUint32(offset + 20, children[0], true) // firstChild
371-
this.view.setUint32(offset + 24, children[children.length - 1], true) // lastChild
359+
this.view.setUint32(offset + 4, children[0], true) // firstChild
372360

373361
// Chain siblings
374362
for (let i = 0; i < children.length - 1; i++) {

src/parse-selector.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export class SelectorParser {
125125

126126
// Set the complex selector chain as children
127127
this.arena.set_first_child(selector_wrapper, complex_selector)
128-
this.arena.set_last_child(selector_wrapper, last_component)
129128

130129
selectors.push(selector_wrapper)
131130
}
@@ -748,7 +747,6 @@ export class SelectorParser {
748747
let child = this.parse_nth_expression(content_start, content_end)
749748
if (child !== null) {
750749
this.arena.set_first_child(node, child)
751-
this.arena.set_last_child(node, child)
752750
}
753751
} else if (str_equals('lang', func_name_substr)) {
754752
// Parse as :lang() - comma-separated language identifiers
@@ -771,7 +769,6 @@ export class SelectorParser {
771769
// Add as child if parsed successfully
772770
if (child_selector !== null) {
773771
this.arena.set_first_child(node, child_selector)
774-
this.arena.set_last_child(node, child_selector)
775772
}
776773
}
777774
}
@@ -847,7 +844,6 @@ export class SelectorParser {
847844
this.arena.set_first_child(parent_node, first_child)
848845
}
849846
if (last_child !== null) {
850-
this.arena.set_last_child(parent_node, last_child)
851847
}
852848

853849
// Restore lexer state
@@ -897,11 +893,9 @@ export class SelectorParser {
897893
// Link An+B and selector list
898894
if (anplusb_node !== null && selector_list !== null) {
899895
this.arena.set_first_child(of_node, anplusb_node)
900-
this.arena.set_last_child(of_node, selector_list)
901896
this.arena.set_next_sibling(anplusb_node, selector_list)
902897
} else if (anplusb_node !== null) {
903898
this.arena.set_first_child(of_node, anplusb_node)
904-
this.arena.set_last_child(of_node, anplusb_node)
905899
}
906900

907901
return of_node

0 commit comments

Comments
 (0)