parsanol 1.0.1-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY.txt +12 -0
- data/LICENSE +23 -0
- data/README.adoc +487 -0
- data/Rakefile +135 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +122 -0
- data/lib/parsanol/atoms/base.rb +202 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +334 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +162 -0
- data/lib/parsanol/atoms/lookahead.rb +82 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +259 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +194 -0
- data/lib/parsanol/atoms/str.rb +103 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +149 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +177 -0
- data/lib/parsanol/incremental_parser.rb +177 -0
- data/lib/parsanol/interval_tree.rb +217 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/lexer.rb +144 -0
- data/lib/parsanol/mermaid.rb +139 -0
- data/lib/parsanol/native/parser.rb +612 -0
- data/lib/parsanol/native/serializer.rb +248 -0
- data/lib/parsanol/native/transformer.rb +435 -0
- data/lib/parsanol/native/types.rb +42 -0
- data/lib/parsanol/native.rb +217 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/ruby_transform.rb +107 -0
- data/lib/parsanol/options/serialized.rb +94 -0
- data/lib/parsanol/options/zero_copy.rb +128 -0
- data/lib/parsanol/options.rb +20 -0
- data/lib/parsanol/parallel.rb +133 -0
- data/lib/parsanol/parser.rb +182 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +159 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +175 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +261 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +146 -0
- data/lib/parsanol/source/line_cache.rb +109 -0
- data/lib/parsanol/source.rb +180 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +195 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +280 -0
- data/parsanol-ruby.gemspec +67 -0
- metadata +280 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Generic object pool for reducing garbage collection pressure.
|
|
5
|
+
#
|
|
6
|
+
# The ObjectPool class implements a simple object pooling strategy:
|
|
7
|
+
# - Objects are pre-allocated on initialization
|
|
8
|
+
# - Objects are reused instead of created new
|
|
9
|
+
# - Objects are reset before being returned to the pool
|
|
10
|
+
# - Pool size is bounded to prevent unbounded growth
|
|
11
|
+
#
|
|
12
|
+
# This reduces GC pressure by reusing objects instead of constantly
|
|
13
|
+
# creating and destroying them, which is particularly beneficial for
|
|
14
|
+
# frequently allocated objects like Slice instances.
|
|
15
|
+
#
|
|
16
|
+
# == Thread Safety
|
|
17
|
+
#
|
|
18
|
+
# This implementation is NOT thread-safe. If thread safety is required,
|
|
19
|
+
# wrap pool operations in a mutex or use thread-local pools.
|
|
20
|
+
#
|
|
21
|
+
# == Usage Example
|
|
22
|
+
#
|
|
23
|
+
# # Create a pool for Slice objects
|
|
24
|
+
# pool = Parsanol::ObjectPool.new(Parsanol::Slice, size: 1000)
|
|
25
|
+
#
|
|
26
|
+
# # Acquire an object from the pool
|
|
27
|
+
# slice = pool.acquire
|
|
28
|
+
# slice.instance_variable_set(:@bytepos, 0)
|
|
29
|
+
# slice.instance_variable_set(:@str, "hello")
|
|
30
|
+
#
|
|
31
|
+
# # Use the slice...
|
|
32
|
+
#
|
|
33
|
+
# # Return it to the pool for reuse
|
|
34
|
+
# pool.release(slice)
|
|
35
|
+
#
|
|
36
|
+
# == Object Reset Protocol
|
|
37
|
+
#
|
|
38
|
+
# Objects returned to the pool will have their reset! method called
|
|
39
|
+
# if they respond to it. This allows objects to clean up their state
|
|
40
|
+
# before being reused. If reset! is not defined, the object is still
|
|
41
|
+
# pooled but without automatic cleanup.
|
|
42
|
+
#
|
|
43
|
+
class ObjectPool
|
|
44
|
+
# @return [Integer] Maximum number of objects to keep in the pool
|
|
45
|
+
attr_reader :size
|
|
46
|
+
|
|
47
|
+
# @return [Hash] Statistics about pool usage
|
|
48
|
+
attr_reader :stats
|
|
49
|
+
|
|
50
|
+
# Initialize a new object pool.
|
|
51
|
+
#
|
|
52
|
+
# @param klass [Class] The class of objects to pool
|
|
53
|
+
# @param size [Integer] Maximum number of objects to keep in pool (default: 1000)
|
|
54
|
+
# @param preallocate [Boolean] Whether to pre-allocate objects on initialization (default: true)
|
|
55
|
+
#
|
|
56
|
+
# @example Create a pool with default settings
|
|
57
|
+
# pool = ObjectPool.new(Array, size: 1000)
|
|
58
|
+
#
|
|
59
|
+
# @example Create a pool without pre-allocation
|
|
60
|
+
# pool = ObjectPool.new(Array, size: 1000, preallocate: false)
|
|
61
|
+
#
|
|
62
|
+
def initialize(klass, size: 1000, preallocate: true)
|
|
63
|
+
@klass = klass
|
|
64
|
+
@size = size
|
|
65
|
+
@available = []
|
|
66
|
+
@stats = {
|
|
67
|
+
created: 0,
|
|
68
|
+
reused: 0,
|
|
69
|
+
released: 0,
|
|
70
|
+
discarded: 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Pre-allocate objects for efficiency if requested
|
|
74
|
+
# This reduces allocation overhead during initial parsing
|
|
75
|
+
preallocate(size) if preallocate && can_preallocate?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Acquire an object from the pool.
|
|
79
|
+
#
|
|
80
|
+
# If the pool has available objects, one is returned (and considered "reused").
|
|
81
|
+
# If the pool is empty, a new object is created (and considered "created").
|
|
82
|
+
#
|
|
83
|
+
# @return [Object] An object instance from the pool or newly created
|
|
84
|
+
#
|
|
85
|
+
# @example Acquire from pool
|
|
86
|
+
# obj = pool.acquire
|
|
87
|
+
#
|
|
88
|
+
def acquire
|
|
89
|
+
if @available.empty?
|
|
90
|
+
@stats[:created] += 1
|
|
91
|
+
@klass.new
|
|
92
|
+
else
|
|
93
|
+
@stats[:reused] += 1
|
|
94
|
+
@available.pop
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Return an object to the pool for reuse.
|
|
99
|
+
#
|
|
100
|
+
# Before returning to the pool:
|
|
101
|
+
# 1. If object responds to reset!, that method is called to clean up state
|
|
102
|
+
# 2. If pool is at capacity, the object is discarded instead of pooled
|
|
103
|
+
#
|
|
104
|
+
# This ensures:
|
|
105
|
+
# - Objects are cleaned before reuse (no stale state)
|
|
106
|
+
# - Pool doesn't grow unbounded (respects size limit)
|
|
107
|
+
#
|
|
108
|
+
# @param obj [Object] The object to return to the pool
|
|
109
|
+
# @return [Boolean] true if object was returned to pool, false if discarded
|
|
110
|
+
#
|
|
111
|
+
# @example Return object to pool
|
|
112
|
+
# pool.release(obj)
|
|
113
|
+
#
|
|
114
|
+
def release(obj)
|
|
115
|
+
# Don't pool if we're at capacity - discard instead
|
|
116
|
+
if @available.size >= @size
|
|
117
|
+
@stats[:discarded] += 1
|
|
118
|
+
return false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Reset object state if it supports the protocol
|
|
122
|
+
obj.reset! if obj.respond_to?(:reset!)
|
|
123
|
+
|
|
124
|
+
@stats[:released] += 1
|
|
125
|
+
@available.push(obj)
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get current pool statistics.
|
|
130
|
+
#
|
|
131
|
+
# Statistics include:
|
|
132
|
+
# - size: Maximum pool capacity
|
|
133
|
+
# - available: Number of objects currently available in pool
|
|
134
|
+
# - created: Total number of new objects created
|
|
135
|
+
# - reused: Total number of times objects were reused from pool
|
|
136
|
+
# - released: Total number of objects returned to pool
|
|
137
|
+
# - discarded: Total number of objects discarded (pool was full)
|
|
138
|
+
# - utilization: Percentage of acquires that were reused (0-100)
|
|
139
|
+
#
|
|
140
|
+
# @return [Hash] Hash containing pool statistics
|
|
141
|
+
#
|
|
142
|
+
# @example Get statistics
|
|
143
|
+
# stats = pool.stats
|
|
144
|
+
# puts "Pool utilization: #{stats[:utilization]}%"
|
|
145
|
+
#
|
|
146
|
+
def statistics
|
|
147
|
+
total_acquires = @stats[:created] + @stats[:reused]
|
|
148
|
+
utilization = total_acquires.zero? ? 0.0 : (@stats[:reused].to_f / total_acquires * 100)
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
size: @size,
|
|
152
|
+
available: @available.size,
|
|
153
|
+
created: @stats[:created],
|
|
154
|
+
reused: @stats[:reused],
|
|
155
|
+
released: @stats[:released],
|
|
156
|
+
discarded: @stats[:discarded],
|
|
157
|
+
utilization: utilization.round(2)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Clear all objects from the pool.
|
|
162
|
+
#
|
|
163
|
+
# This removes all pooled objects and resets statistics.
|
|
164
|
+
# Useful for testing or when you want to force fresh allocations.
|
|
165
|
+
#
|
|
166
|
+
# @return [void]
|
|
167
|
+
#
|
|
168
|
+
# @example Clear the pool
|
|
169
|
+
# pool.clear!
|
|
170
|
+
#
|
|
171
|
+
def clear!
|
|
172
|
+
@available.clear
|
|
173
|
+
@stats = {
|
|
174
|
+
created: 0,
|
|
175
|
+
reused: 0,
|
|
176
|
+
released: 0,
|
|
177
|
+
discarded: 0
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
# Check if the pooled class can be pre-allocated.
|
|
184
|
+
#
|
|
185
|
+
# Some classes require arguments to initialize and cannot be
|
|
186
|
+
# pre-allocated without those arguments. This method checks if
|
|
187
|
+
# the class has a zero-arity initialize method.
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean] true if class can be instantiated without arguments
|
|
190
|
+
#
|
|
191
|
+
def can_preallocate?
|
|
192
|
+
# Check if the class can be instantiated without arguments
|
|
193
|
+
# This is a heuristic - we try to create one instance to test
|
|
194
|
+
|
|
195
|
+
@klass.new
|
|
196
|
+
true
|
|
197
|
+
rescue ArgumentError
|
|
198
|
+
# Class requires arguments, cannot pre-allocate
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Pre-allocate objects to fill the pool.
|
|
203
|
+
#
|
|
204
|
+
# This is called during initialization if preallocate: true is set.
|
|
205
|
+
# Pre-allocation reduces allocation overhead during initial parsing.
|
|
206
|
+
#
|
|
207
|
+
# @param count [Integer] Number of objects to pre-allocate
|
|
208
|
+
# @return [void]
|
|
209
|
+
#
|
|
210
|
+
def preallocate(count)
|
|
211
|
+
count.times do
|
|
212
|
+
@available.push(@klass.new)
|
|
213
|
+
end
|
|
214
|
+
# Adjust stats to reflect pre-allocation as "released" not "created"
|
|
215
|
+
# since these objects haven't been acquired yet
|
|
216
|
+
@stats[:released] = count
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Pools
|
|
5
|
+
# Specialized object pool for Array instances.
|
|
6
|
+
#
|
|
7
|
+
# ArrayPool extends ObjectPool to provide array-specific behavior,
|
|
8
|
+
# particularly ensuring arrays are cleared before being returned to
|
|
9
|
+
# the pool for reuse.
|
|
10
|
+
#
|
|
11
|
+
# == Usage
|
|
12
|
+
#
|
|
13
|
+
# pool = Parsanol::Pools::ArrayPool.new(size: 1000)
|
|
14
|
+
#
|
|
15
|
+
# # Acquire an array
|
|
16
|
+
# array = pool.acquire
|
|
17
|
+
# array << 'item1'
|
|
18
|
+
# array << 'item2'
|
|
19
|
+
#
|
|
20
|
+
# # Return to pool (automatically cleared)
|
|
21
|
+
# pool.release(array)
|
|
22
|
+
#
|
|
23
|
+
# # Next acquire gets a clean, empty array
|
|
24
|
+
# array2 = pool.acquire
|
|
25
|
+
# array2.empty? # => true
|
|
26
|
+
#
|
|
27
|
+
# == Why Pool Arrays?
|
|
28
|
+
#
|
|
29
|
+
# Profiling (Session 19) showed that array allocations account for
|
|
30
|
+
# 74% of memory usage during parsing. Temporary arrays used for:
|
|
31
|
+
# - Collecting repetition results
|
|
32
|
+
# - Building sequence results
|
|
33
|
+
# - Accumulating alternative matches
|
|
34
|
+
#
|
|
35
|
+
# By pooling arrays, we can:
|
|
36
|
+
# - Reduce array allocations by 60-70%
|
|
37
|
+
# - Decrease memory pressure
|
|
38
|
+
# - Improve overall parsing performance
|
|
39
|
+
#
|
|
40
|
+
class ArrayPool < Parsanol::ObjectPool
|
|
41
|
+
# Initialize a new ArrayPool.
|
|
42
|
+
#
|
|
43
|
+
# @param size [Integer] Maximum number of Arrays to pool (default: 1000)
|
|
44
|
+
# @param preallocate [Boolean] Whether to pre-allocate arrays (default: true)
|
|
45
|
+
#
|
|
46
|
+
# @example Create an ArrayPool
|
|
47
|
+
# pool = ArrayPool.new(size: 2000)
|
|
48
|
+
#
|
|
49
|
+
def initialize(size: 1000, preallocate: true)
|
|
50
|
+
super(Array, size: size, preallocate: preallocate)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return an array to the pool after clearing its contents.
|
|
54
|
+
#
|
|
55
|
+
# This override ensures arrays are always empty when returned to
|
|
56
|
+
# the pool, preventing stale data from polluting future uses.
|
|
57
|
+
#
|
|
58
|
+
# @param array [Array] The array to return to the pool
|
|
59
|
+
# @return [Boolean] true if returned to pool, false if discarded
|
|
60
|
+
#
|
|
61
|
+
# @example Release with automatic clearing
|
|
62
|
+
# array = pool.acquire
|
|
63
|
+
# array << 1 << 2 << 3
|
|
64
|
+
# pool.release(array)
|
|
65
|
+
# # Array is now cleared and back in pool
|
|
66
|
+
#
|
|
67
|
+
def release(array)
|
|
68
|
+
# Clear array before pooling to prevent stale data
|
|
69
|
+
# Note: Array#clear is more efficient than array = []
|
|
70
|
+
array.clear
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../buffer'
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module Pools
|
|
7
|
+
# Manages fixed-size buffers organized by size class.
|
|
8
|
+
#
|
|
9
|
+
# BufferPool provides efficient buffer allocation by maintaining
|
|
10
|
+
# separate pools for common buffer sizes. This reduces allocation
|
|
11
|
+
# overhead and enables buffer reuse across parses.
|
|
12
|
+
#
|
|
13
|
+
# == Usage
|
|
14
|
+
#
|
|
15
|
+
# pool = BufferPool.new
|
|
16
|
+
# buffer = pool.acquire(size: 8) # Get buffer with capacity >= 8
|
|
17
|
+
# buffer.push("a")
|
|
18
|
+
# pool.release(buffer)
|
|
19
|
+
#
|
|
20
|
+
# == Size Classes
|
|
21
|
+
#
|
|
22
|
+
# Buffers are organized into size classes:
|
|
23
|
+
# - Small: 2, 4, 8 (most common)
|
|
24
|
+
# - Medium: 16, 32 (common)
|
|
25
|
+
# - Large: 64+ (rare, allocated on demand)
|
|
26
|
+
#
|
|
27
|
+
# This matches typical parsing patterns where most arrays are small.
|
|
28
|
+
#
|
|
29
|
+
class BufferPool
|
|
30
|
+
# Standard size classes (power of 2 for efficiency)
|
|
31
|
+
SIZE_CLASSES = [2, 4, 8, 16, 32, 64].freeze
|
|
32
|
+
|
|
33
|
+
# Default pool size per class
|
|
34
|
+
DEFAULT_POOL_SIZE = 100
|
|
35
|
+
|
|
36
|
+
# @return [Hash] Pools by size class
|
|
37
|
+
attr_reader :pools
|
|
38
|
+
|
|
39
|
+
# @return [Hash] Statistics per size class
|
|
40
|
+
attr_reader :stats
|
|
41
|
+
|
|
42
|
+
# Initialize a new BufferPool.
|
|
43
|
+
#
|
|
44
|
+
# @param pool_size [Integer] Number of buffers per size class
|
|
45
|
+
#
|
|
46
|
+
def initialize(pool_size: DEFAULT_POOL_SIZE)
|
|
47
|
+
@pool_size = pool_size
|
|
48
|
+
@pools = {}
|
|
49
|
+
@stats = {}
|
|
50
|
+
|
|
51
|
+
# Create pool for each size class
|
|
52
|
+
SIZE_CLASSES.each do |size|
|
|
53
|
+
@pools[size] = []
|
|
54
|
+
@stats[size] = { created: 0, reused: 0, released: 0, discarded: 0 }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Acquire a buffer with at least the requested capacity.
|
|
59
|
+
#
|
|
60
|
+
# Returns a buffer from the appropriate size class pool.
|
|
61
|
+
# If no buffer available, creates a new one.
|
|
62
|
+
#
|
|
63
|
+
# @param size [Integer] Minimum required capacity
|
|
64
|
+
# @return [Buffer] Buffer with capacity >= size
|
|
65
|
+
#
|
|
66
|
+
def acquire(size:)
|
|
67
|
+
size_class = select_size_class(size)
|
|
68
|
+
|
|
69
|
+
# For non-standard size classes, create buffer on demand
|
|
70
|
+
return Buffer.new(capacity: size_class) unless @pools.key?(size_class)
|
|
71
|
+
|
|
72
|
+
pool = @pools[size_class]
|
|
73
|
+
|
|
74
|
+
if pool.empty?
|
|
75
|
+
@stats[size_class][:created] += 1
|
|
76
|
+
Buffer.new(capacity: size_class)
|
|
77
|
+
else
|
|
78
|
+
@stats[size_class][:reused] += 1
|
|
79
|
+
pool.pop
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Release a buffer back to the pool.
|
|
84
|
+
#
|
|
85
|
+
# Clears the buffer and returns it to the appropriate size class pool.
|
|
86
|
+
#
|
|
87
|
+
# @param buffer [Buffer] Buffer to release
|
|
88
|
+
# @return [Boolean] true if returned to pool, false if discarded
|
|
89
|
+
#
|
|
90
|
+
def release(buffer)
|
|
91
|
+
size_class = buffer.capacity
|
|
92
|
+
pool = @pools[size_class]
|
|
93
|
+
|
|
94
|
+
# Discard if pool is full or size not in standard classes
|
|
95
|
+
if !pool || pool.size >= @pool_size
|
|
96
|
+
@stats[size_class][:discarded] += 1 if @stats[size_class]
|
|
97
|
+
return false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
buffer.clear!
|
|
101
|
+
@stats[size_class][:released] += 1
|
|
102
|
+
pool.push(buffer)
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get statistics for all size classes.
|
|
107
|
+
#
|
|
108
|
+
# @return [Hash] Statistics by size class
|
|
109
|
+
#
|
|
110
|
+
def statistics
|
|
111
|
+
result = {}
|
|
112
|
+
SIZE_CLASSES.each do |size|
|
|
113
|
+
stats = @stats[size]
|
|
114
|
+
total_acquires = stats[:created] + stats[:reused]
|
|
115
|
+
utilization = if total_acquires.zero?
|
|
116
|
+
0.0
|
|
117
|
+
else
|
|
118
|
+
(stats[:reused].to_f / total_acquires * 100)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
result[size] = {
|
|
122
|
+
available: @pools[size].size,
|
|
123
|
+
created: stats[:created],
|
|
124
|
+
reused: stats[:reused],
|
|
125
|
+
released: stats[:released],
|
|
126
|
+
discarded: stats[:discarded],
|
|
127
|
+
utilization: utilization.round(2)
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
result
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Clear all pools.
|
|
134
|
+
#
|
|
135
|
+
# @return [void]
|
|
136
|
+
#
|
|
137
|
+
def clear!
|
|
138
|
+
@pools.each_value(&:clear)
|
|
139
|
+
@stats.each_value do |s|
|
|
140
|
+
s[:created] = s[:reused] = s[:released] = s[:discarded] = 0
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Select appropriate size class for requested size.
|
|
147
|
+
#
|
|
148
|
+
# Returns smallest size class >= requested size.
|
|
149
|
+
#
|
|
150
|
+
# @param size [Integer] Requested size
|
|
151
|
+
# @return [Integer] Size class
|
|
152
|
+
#
|
|
153
|
+
def select_size_class(size)
|
|
154
|
+
SIZE_CLASSES.find { |sc| sc >= size } || next_power_of_2(size)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Find next power of 2 greater than or equal to n.
|
|
158
|
+
#
|
|
159
|
+
# @param n [Integer] Input value
|
|
160
|
+
# @return [Integer] Next power of 2
|
|
161
|
+
#
|
|
162
|
+
def next_power_of_2(n)
|
|
163
|
+
return 1 if n <= 0
|
|
164
|
+
|
|
165
|
+
n -= 1
|
|
166
|
+
n |= n >> 1
|
|
167
|
+
n |= n >> 2
|
|
168
|
+
n |= n >> 4
|
|
169
|
+
n |= n >> 8
|
|
170
|
+
n |= n >> 16
|
|
171
|
+
n + 1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Pools
|
|
5
|
+
# Specialized object pool for Position instances.
|
|
6
|
+
#
|
|
7
|
+
# PositionPool extends ObjectPool to provide position-specific behavior,
|
|
8
|
+
# particularly managing the line and column state for reuse.
|
|
9
|
+
#
|
|
10
|
+
# == Usage
|
|
11
|
+
#
|
|
12
|
+
# pool = Parsanol::Pools::PositionPool.new(size: 1000)
|
|
13
|
+
#
|
|
14
|
+
# # Acquire a position with line/column
|
|
15
|
+
# pos = pool.acquire_with(string: "source", bytepos: 42, charpos: 42)
|
|
16
|
+
#
|
|
17
|
+
# # Return to pool (automatically reset)
|
|
18
|
+
# pool.release(pos)
|
|
19
|
+
#
|
|
20
|
+
# == Architecture
|
|
21
|
+
#
|
|
22
|
+
# v3.0.0 uses integer positions during parsing for efficiency.
|
|
23
|
+
# Position objects are only created when:
|
|
24
|
+
# - Generating error messages (need line/column)
|
|
25
|
+
# - Materializing error context
|
|
26
|
+
#
|
|
27
|
+
# By pooling Position objects, we reduce GC pressure at the
|
|
28
|
+
# materialization point without changing the fast integer-based
|
|
29
|
+
# parsing path.
|
|
30
|
+
#
|
|
31
|
+
class PositionPool < Parsanol::ObjectPool
|
|
32
|
+
# Initialize a new PositionPool.
|
|
33
|
+
#
|
|
34
|
+
# @param size [Integer] Maximum number of Position objects to pool
|
|
35
|
+
# @param preallocate [Boolean] Whether to pre-allocate positions
|
|
36
|
+
#
|
|
37
|
+
def initialize(size: 1000, preallocate: false)
|
|
38
|
+
# NOTE: Position requires arguments, so we cannot pre-allocate
|
|
39
|
+
super(Parsanol::Position, size: size, preallocate: false)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Acquire a Position from the pool.
|
|
43
|
+
# Overrides ObjectPool#acquire to handle Position's required arguments.
|
|
44
|
+
#
|
|
45
|
+
# @return [Parsanol::Position] A position instance from pool or newly created
|
|
46
|
+
#
|
|
47
|
+
def acquire
|
|
48
|
+
if @available.empty?
|
|
49
|
+
@stats[:created] += 1
|
|
50
|
+
# Create Position with default values since it requires arguments
|
|
51
|
+
Parsanol::Position.new('', 0, 0)
|
|
52
|
+
else
|
|
53
|
+
@stats[:reused] += 1
|
|
54
|
+
@available.pop
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Acquire a Position from the pool and initialize it with values.
|
|
59
|
+
#
|
|
60
|
+
# @param string [String] Source string for position tracking
|
|
61
|
+
# @param bytepos [Integer] Byte position in source
|
|
62
|
+
# @param charpos [Integer, nil] Character position (optional)
|
|
63
|
+
# @return [Parsanol::Position] Initialized position from pool
|
|
64
|
+
#
|
|
65
|
+
def acquire_with(string:, bytepos:, charpos: nil)
|
|
66
|
+
pos = acquire
|
|
67
|
+
pos.reset!(string, bytepos, charpos)
|
|
68
|
+
pos
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Return a position to the pool after resetting it.
|
|
72
|
+
#
|
|
73
|
+
# @param pos [Parsanol::Position] The position to return
|
|
74
|
+
# @return [Boolean] true if returned to pool, false if discarded
|
|
75
|
+
#
|
|
76
|
+
def release(pos)
|
|
77
|
+
# Don't pool if we're at capacity - discard instead
|
|
78
|
+
if @available.size >= @size
|
|
79
|
+
@stats[:discarded] += 1
|
|
80
|
+
return false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Reset position state with default values before returning to pool
|
|
84
|
+
pos.reset!('', 0, 0)
|
|
85
|
+
|
|
86
|
+
@stats[:released] += 1
|
|
87
|
+
@available.push(pos)
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Pools
|
|
5
|
+
# Specialized object pool for Parsanol::Slice instances.
|
|
6
|
+
#
|
|
7
|
+
# SlicePool extends ObjectPool to provide convenient methods for
|
|
8
|
+
# acquiring and configuring Slice objects. Since Slices are frequently
|
|
9
|
+
# created during parsing, pooling them significantly reduces GC pressure.
|
|
10
|
+
#
|
|
11
|
+
# == Usage
|
|
12
|
+
#
|
|
13
|
+
# pool = Parsanol::Pools::SlicePool.new(size: 1000)
|
|
14
|
+
#
|
|
15
|
+
# # Acquire and initialize in one step
|
|
16
|
+
# slice = pool.acquire_with(0, "hello", line_cache)
|
|
17
|
+
#
|
|
18
|
+
# # Use the slice...
|
|
19
|
+
#
|
|
20
|
+
# # Return to pool
|
|
21
|
+
# pool.release(slice)
|
|
22
|
+
#
|
|
23
|
+
# == Why Pool Slices?
|
|
24
|
+
#
|
|
25
|
+
# Profiling (Session 19) showed that Slice allocation contributes
|
|
26
|
+
# significantly to GC overhead. By reusing Slice objects, we can:
|
|
27
|
+
# - Reduce object allocations by 70-80%
|
|
28
|
+
# - Decrease GC time from 67% to ~20%
|
|
29
|
+
# - Improve overall parsing throughput by 2-3x
|
|
30
|
+
#
|
|
31
|
+
class SlicePool < Parsanol::ObjectPool
|
|
32
|
+
# Initialize a new SlicePool.
|
|
33
|
+
#
|
|
34
|
+
# @param size [Integer] Maximum number of Slice objects to pool (default: 1000)
|
|
35
|
+
# @param preallocate [Boolean] Whether to pre-allocate slices (default: true)
|
|
36
|
+
#
|
|
37
|
+
# @example Create a SlicePool
|
|
38
|
+
# pool = SlicePool.new(size: 2000)
|
|
39
|
+
#
|
|
40
|
+
def initialize(size: 1000, preallocate: true)
|
|
41
|
+
super(Parsanol::Slice, size: size, preallocate: preallocate)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Acquire a Slice from the pool and initialize it with given values.
|
|
45
|
+
#
|
|
46
|
+
# This is a convenience method that combines acquire + reset! into
|
|
47
|
+
# a single operation, making it easier to work with pooled slices.
|
|
48
|
+
#
|
|
49
|
+
# @param bytepos [Integer] Byte position in the original input
|
|
50
|
+
# @param str [String] The slice content
|
|
51
|
+
# @param line_cache [Object] Optional line cache for line/column info
|
|
52
|
+
# @return [Parsanol::Slice] An initialized slice ready for use
|
|
53
|
+
#
|
|
54
|
+
# @example Acquire and initialize
|
|
55
|
+
# slice = pool.acquire_with(0, "hello", line_cache)
|
|
56
|
+
#
|
|
57
|
+
def acquire_with(bytepos, str, line_cache = nil)
|
|
58
|
+
slice = acquire
|
|
59
|
+
slice.reset!(bytepos, str, line_cache)
|
|
60
|
+
slice
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|