rbtree-ruby 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28594e663eac8519467b09c0756a032b2ef887d69df9e9335d1d79c36744a134
4
- data.tar.gz: 492d6896dd4a0644ecc8ad721214afe2827daf3acbde38dce938e6da54cac87f
3
+ metadata.gz: 327862d13e55ed6406023e1d10676e4b5e94541c59400d9205380af08822e4bf
4
+ data.tar.gz: 465e1a514390286bf0165db4dfa9f25ada4cd3d1410b0115b11eba15b03dcf33
5
5
  SHA512:
6
- metadata.gz: 3aeeca5b0d3de1a8826fac660dc6b201c046cb193328ba75bcd20f679f682fbf65479179fcddebe63920522eaa9ede28a9f8a784c76387e86530105a7378c8cd
7
- data.tar.gz: 1e960a2132c1c3bc8988e233ebfb249f121900ac6eabbbcce0aa86186ceb24caa3224d4e7ebb8ad2acda5b927976ec0d5a0437ec20ad5fe2162eda841d15f909
6
+ metadata.gz: 1f354523b8fb78b746cf256949887b89184ce1dcd190403a7024949cded391de24ce6f932720a58ae24592b4b453f3901b1896c9904f546aa03fe4b1524698c2
7
+ data.tar.gz: e1219a20ea771f3fe1ca2fc160d737c787a1a0e81cb5389b63f320dcd1d2ac9d2cc71d9df2b9698d73afc98069f76d39285ca8106691f0025f1d5a5fa8b6bf91
data/CHANGELOG.md CHANGED
@@ -5,9 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2] - 2026-01-15
9
+
10
+ ### Added
11
+ - **Custom Node Allocator**: `RBTree#initialize` now accepts a `node_allocator:` keyword argument, allowing customization of the node pooling strategy.
12
+ - **AutoShrinkNodePool**: Introduced `RBTree::AutoShrinkNodePool` as the default allocator. It automatically releases memory back to the GC when the pool size exceeds the fluctuation range of recent active node counts, preventing memory bloat in long-running processes with variable loads.
13
+ - **Explicit Overwrite**: `overwrite: true` is now the explicit default for `initialize` and `insert`.
14
+
15
+ ## [0.3.1] - 2026-01-15
16
+
17
+ ### Added
18
+ - **`to_h`**: Added `to_h` method to `RBTree` and `MultiRBTree` for efficient conversion to Hash.
19
+ - **`merge!`**: Added `merge!` method to `RBTree` and `MultiRBTree`.
20
+ - `RBTree#merge!`: Supports `overwrite` option (default: `true`). Raises `ArgumentError` if merging a `MultiRBTree` (incompatible value structures).
21
+ - `MultiRBTree#merge!`: Always appends values from the source.
22
+
8
23
  ## [0.3.0] - 2026-01-15
9
24
 
10
- ### Changed
11
25
  ### API Changes (Method Renaming & Deprecation)
12
26
  > [!WARNING]
13
27
  > The public API has been refactored to improve consistency. The following methods have been renamed.
@@ -184,6 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
184
198
  - ASCII diagrams for tree rotation operations
185
199
  - MIT License (Copyright © 2026 Masahito Suzuki)
186
200
 
201
+ [0.3.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.2
202
+ [0.3.1]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.1
187
203
  [0.3.0]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.0
188
204
  [0.2.3]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.3
189
205
  [0.2.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.2
data/README.ja.md CHANGED
@@ -174,6 +174,25 @@ tree.lt(3).first # => [1, "one"] (遅延評価、配列は作
174
174
  tree.gt(0).lazy.take(2).to_a # => [[1, "one"], [2, "two"]] (最初の2件のみ計算)
175
175
  ```
176
176
 
177
+ ### 変換と結合
178
+
179
+ 標準のRubyオブジェクトへの変換や、他のコレクションとの結合も簡単です:
180
+
181
+ ```ruby
182
+ tree = RBTree.new({1 => 'one', 2 => 'two'})
183
+
184
+ # 配列への変換(Enumerable経由)
185
+ tree.to_a # => [[1, "one"], [2, "two"]]
186
+
187
+ # ハッシュへの変換
188
+ tree.to_h # => {1 => "one", 2 => "two"}
189
+
190
+ # 他のツリー、ハッシュ、またはEnumerableの結合
191
+ other = {3 => 'three'}
192
+ tree.merge!(other)
193
+ tree.size # => 3
194
+ ```
195
+
177
196
  ### MultiRBTree 値配列アクセス
178
197
 
179
198
  複数の値を持つキーで、どの値にアクセスするか選択:
@@ -217,12 +236,6 @@ tree.max(last: true) # => [2, "b"] (最大キーの最後の値)
217
236
 
218
237
  全要素のイテレーションはO(n)時間。
219
238
 
220
- ### メモリ効率
221
-
222
- RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用:
223
- - 頻繁な挿入・削除時のGC負荷を大幅に削減
224
- - 100,000回の循環操作ベンチマークで**GC時間0.0秒**を達成
225
-
226
239
  ### RBTree vs Hash vs Array
227
240
 
228
241
  順序付き操作と空間的操作において、RBTreeは単に速いだけでなく、全く異なるクラスの性能を発揮。**50万件**でのベンチマーク:
@@ -235,6 +248,23 @@ RBTreeは内部的な**メモリプール**を使用してノードオブジェ
235
248
  | **ソート済みイテレーション** | **O(n)** | O(n log n) | **無料** | 常にソート済み vs 明示的な`sort` |
236
249
  | **キー検索** | **O(1)** | O(1) | **同等** | **ハイブリッドハッシュインデックスにより、Hashと同等のO(1)検索速度を実現** |
237
250
 
251
+ ### メモリ効率とカスタムアロケータ
252
+
253
+ RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用します。
254
+ - 頻繁な挿入・削除時のGC負荷を大幅に削減します。
255
+ - **自動縮小**: デフォルトの `AutoShrinkNodePool` は、現在の使用量に対してプールが大きくなりすぎた場合、自動的に未使用ノードをRubyのGCに解放します。これにより、負荷が変動する長時間実行アプリケーションでのメモリ肥大化を防ぎます。
256
+ - **カスタマイズ**: プールの動作をカスタマイズしたり、独自のアロケータを指定することができます:
257
+
258
+ ```ruby
259
+ # 自動縮小パラメータのカスタマイズ
260
+ pool = RBTree::AutoShrinkNodePool.new(
261
+ history_size: 60, # 60秒の履歴
262
+ buffer_factor: 1.5, # 変動幅の50%をバッファとして保持
263
+ reserve_ratio: 0.2 # 常に最大値の20%を予備として保持
264
+ )
265
+ tree = RBTree.new(node_allocator: pool)
266
+ ```
267
+
238
268
  ### RBTreeを使うべき場面
239
269
 
240
270
  ✅ **RBTreeが適している場合:**
data/README.md CHANGED
@@ -174,6 +174,25 @@ tree.lt(3).first # => [1, "one"] (lazy, no array created)
174
174
  tree.gt(0).lazy.take(2).to_a # => [[1, "one"], [2, "two"]] (only computes first 2)
175
175
  ```
176
176
 
177
+ ### Conversion and Merging
178
+
179
+ Seamlessly convert to standard Ruby objects or merge other collections:
180
+
181
+ ```ruby
182
+ tree = RBTree.new({1 => 'one', 2 => 'two'})
183
+
184
+ # Convert to Array (via Enumerable)
185
+ tree.to_a # => [[1, "one"], [2, "two"]]
186
+
187
+ # Convert to Hash
188
+ tree.to_h # => {1 => "one", 2 => "two"}
189
+
190
+ # Merge another tree, hash, or enumerable
191
+ other = {3 => 'three'}
192
+ tree.merge!(other)
193
+ tree.size # => 3
194
+ ```
195
+
177
196
  ### MultiRBTree Value Array Access
178
197
 
179
198
  For keys with multiple values, choose which value to access:
@@ -217,12 +236,6 @@ All major operations run in **O(log n)** time:
217
236
 
218
237
  Iteration over all elements takes O(n) time.
219
238
 
220
- ### Memory Efficiency
221
-
222
- RBTree uses an internal **Memory Pool** to recycle node objects.
223
- - Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions (e.g., in high-throughput queues).
224
- - In benchmarks with 100,000 cyclic operations, **GC time was 0.0s** compared to significant pauses without pooling.
225
-
226
239
  ### RBTree vs Hash vs Array (Overwhelming Power)
227
240
 
228
241
  For ordered and spatial operations, RBTree is not just faster—it is in a completely different class. The following benchmarks were conducted with **500,000 items**:
@@ -235,6 +248,23 @@ For ordered and spatial operations, RBTree is not just faster—it is in a compl
235
248
  | **Sorted Iteration** | **O(n)** | O(n log n) | **FREE** | Always sorted vs explicit `sort` |
236
249
  | **Key Lookup** | **O(1)** | O(1) | **Equal** | **Hybrid Hash Index provides O(1) access like standard Hash** |
237
250
 
251
+ ### Memory Efficiency & Custom Allocators
252
+
253
+ RBTree uses an internal **Memory Pool** to recycle node objects.
254
+ - Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions.
255
+ - **Auto-Shrinking**: The default `AutoShrinkNodePool` automatically releases unused nodes back to Ruby's GC when the pool gets too large relative to current usage, preventing memory leaks in long-running applications with fluctuating workloads.
256
+ - **Customization**: You can customize the pool behavior or provide your own allocator:
257
+
258
+ ```ruby
259
+ # Customize auto-shrink parameters
260
+ pool = RBTree::AutoShrinkNodePool.new(
261
+ history_size: 60, # 1 minute history
262
+ buffer_factor: 1.5, # Keep 50% buffer above fluctuation
263
+ reserve_ratio: 0.2 # Always keep 20% reserve
264
+ )
265
+ tree = RBTree.new(node_allocator: pool)
266
+ ```
267
+
238
268
  ### When to Use RBTree
239
269
 
240
270
  ✅ **Use RBTree when you need:**
@@ -2,5 +2,5 @@
2
2
 
3
3
  class RBTree
4
4
  # The version of the rbtree-ruby gem
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.2"
6
6
  end
data/lib/rbtree.rb CHANGED
@@ -77,6 +77,7 @@ class RBTree
77
77
  #
78
78
  # @param args [Hash, Array, nil] optional initial data
79
79
  # @param overwrite [Boolean] whether to overwrite existing keys (default: true)
80
+ # @param node_allocator [NodeAllocator] allocator instance to use (default: AutoShrinkNodePool.new)
80
81
  # @yieldreturn [Object] optional initial data
81
82
  # - If a Hash is provided, each key-value pair is inserted into the tree
82
83
  # - If an Array is provided, it should contain [key, value] pairs
@@ -91,7 +92,7 @@ class RBTree
91
92
  # tree = RBTree.new([[1, 'one'], [2, 'two']])
92
93
  # @example Create with overwrite: false
93
94
  # tree = RBTree.new([[1, 'one'], [1, 'uno']], overwrite: false)
94
- def initialize(*args, overwrite: true, &block)
95
+ def initialize(*args, overwrite: true, node_allocator: AutoShrinkNodePool.new, &block)
95
96
  @nil_node = Node.new
96
97
  @nil_node.color = Node::BLACK
97
98
  @nil_node.left = @nil_node
@@ -99,14 +100,21 @@ class RBTree
99
100
  @root = @nil_node
100
101
  @min_node = @nil_node
101
102
  @hash_index = {} # Hash index for O(1) key lookup
102
- @node_pool = [] # Memory pool for recycling nodes
103
+ @node_allocator = node_allocator
103
104
  @key_count = 0
104
105
 
106
+ @overwrite = overwrite
107
+
105
108
  if args.size > 0 || block_given?
106
109
  insert(*args, overwrite: overwrite, &block)
107
110
  end
108
111
  end
109
112
 
113
+ # Returns a Hash containing all key-value pairs from the tree.
114
+ #
115
+ # @return [Hash] a new Hash with the tree's contents
116
+ def to_h = @hash_index.transform_values(&:value)
117
+
110
118
  # Checks if the tree is empty.
111
119
  #
112
120
  # @return [Boolean] true if the tree contains no elements, false otherwise
@@ -298,7 +306,7 @@ class RBTree
298
306
  # tree.insert({1 => 'one', 2 => 'two'})
299
307
  # @example Bulk insert from Array
300
308
  # tree.insert([[1, 'one'], [2, 'two']])
301
- def insert(*args, overwrite: true, &block)
309
+ def insert(*args, overwrite: @overwrite, &block)
302
310
  if args.size == 2
303
311
  key, value = args
304
312
  insert_entry(key, value, overwrite: overwrite)
@@ -335,6 +343,19 @@ class RBTree
335
343
  end
336
344
  end
337
345
  alias :[]= :insert
346
+
347
+ # Merges the contents of another tree, hash, or enumerable into this tree.
348
+ #
349
+ # @param other [RBTree, Hash, Enumerable] the source to merge from
350
+ # @param overwrite [Boolean] whether to overwrite existing keys (default: true)
351
+ # @return [RBTree] self
352
+ def merge!(other, overwrite: true)
353
+ if defined?(MultiRBTree) && other.is_a?(MultiRBTree)
354
+ raise ArgumentError, "Cannot merge MultiRBTree into RBTree"
355
+ end
356
+ insert(other, overwrite: overwrite)
357
+ self
358
+ end
338
359
 
339
360
  # Deletes the key-value pair with the specified key.
340
361
  #
@@ -1308,30 +1329,17 @@ class RBTree
1308
1329
  # Allocates a new node or recycles one from the pool.
1309
1330
  # @return [Node]
1310
1331
  def allocate_node(key, value, color, left, right, parent)
1311
- node = @node_pool.pop
1312
- if node
1313
- node.key = key
1314
- node.value = value
1315
- node.color = color
1316
- node.left = left
1317
- node.right = right
1318
- node.parent = parent
1319
- node
1320
- else
1321
- node = Node.new(key, value, color, left, right, parent)
1322
- end
1332
+ node = @node_allocator.allocate(key, value, color, left, right, parent)
1323
1333
  @key_count += 1
1324
1334
  node
1325
1335
  end
1326
1336
 
1337
+ # Releases a node back to the pool.
1338
+ # @param node [Node] the node to release
1327
1339
  # Releases a node back to the pool.
1328
1340
  # @param node [Node] the node to release
1329
1341
  def release_node(node)
1330
- node.left = nil
1331
- node.right = nil
1332
- node.parent = nil
1333
- node.value = nil # Help GC
1334
- @node_pool << node
1342
+ @node_allocator.release(node)
1335
1343
  @key_count -= 1
1336
1344
  end
1337
1345
 
@@ -1548,61 +1556,15 @@ class MultiRBTree < RBTree
1548
1556
  # tree.succ(4) # => [5, "five"]
1549
1557
  def succ(key, last: false) = (pair = super(key)) && [pair[0], pair[1].send(last ? :last : :first)]
1550
1558
 
1551
- # Inserts a value for the given key.
1559
+ # Merges the contents of another tree, hash, or enumerable into this tree.
1552
1560
  #
1553
- # If the key already exists, the value is appended to its list.
1554
- # If the key doesn't exist, a new node is created.
1561
+ # Appends values from the other source to the existing values for each key.
1555
1562
  #
1556
- # @param key [Object] the key (must implement <=>)
1557
- # @param value [Object] the value to insert
1558
- # @param overwrite [Boolean] ignored for MultiRBTree which always appends
1559
- # @return [Boolean] always returns true
1560
- # @example
1561
- # tree = MultiRBTree.new
1562
- # tree.insert(1, 'first')
1563
- # tree.insert(1, 'second') # adds another value for key 1
1564
- def insert_entry(key, value, **)
1565
- if (node = @hash_index[key])
1566
- node.value << value
1567
- @value_count += 1
1568
- return true
1569
- end
1570
- y = @nil_node
1571
- x = @root
1572
- while x != @nil_node
1573
- y = x
1574
- cmp = key <=> x.key
1575
- if cmp == 0
1576
- x.value << value
1577
- @value_count += 1
1578
- return true
1579
- elsif cmp < 0
1580
- x = x.left
1581
- else
1582
- x = x.right
1583
- end
1584
- end
1585
- z = allocate_node(key, [value], Node::RED, @nil_node, @nil_node, @nil_node)
1586
- z.parent = y
1587
- if y == @nil_node
1588
- @root = z
1589
- elsif (key <=> y.key) < 0
1590
- y.left = z
1591
- else
1592
- y.right = z
1593
- end
1594
- z.left = @nil_node
1595
- z.right = @nil_node
1596
- z.color = Node::RED
1597
- insert_fixup(z)
1598
- @value_count += 1
1599
-
1600
- if @min_node == @nil_node || (key <=> @min_node.key) < 0
1601
- @min_node = z
1602
- end
1603
-
1604
- @hash_index[key] = z # Add to hash index
1605
- true
1563
+ # @param other [RBTree, Hash, Enumerable] the source to merge from
1564
+ # @return [MultiRBTree] self
1565
+ def merge!(other)
1566
+ insert(other)
1567
+ self
1606
1568
  end
1607
1569
 
1608
1570
  # Deletes a single value for the specified key.
@@ -1692,6 +1654,63 @@ class MultiRBTree < RBTree
1692
1654
  # @!visibility private
1693
1655
  private
1694
1656
 
1657
+ # Inserts a value for the given key.
1658
+ #
1659
+ # If the key already exists, the value is appended to its list.
1660
+ # If the key doesn't exist, a new node is created.
1661
+ #
1662
+ # @param key [Object] the key (must implement <=>)
1663
+ # @param value [Object] the value to insert
1664
+ # @param overwrite [Boolean] ignored for MultiRBTree which always appends
1665
+ # @return [Boolean] always returns true
1666
+ # @example
1667
+ # tree = MultiRBTree.new
1668
+ # tree.insert(1, 'first')
1669
+ # tree.insert(1, 'second') # adds another value for key 1
1670
+ def insert_entry(key, value, **)
1671
+ if (node = @hash_index[key])
1672
+ node.value << value
1673
+ @value_count += 1
1674
+ return true
1675
+ end
1676
+ y = @nil_node
1677
+ x = @root
1678
+ while x != @nil_node
1679
+ y = x
1680
+ cmp = key <=> x.key
1681
+ if cmp == 0
1682
+ x.value << value
1683
+ @value_count += 1
1684
+ return true
1685
+ elsif cmp < 0
1686
+ x = x.left
1687
+ else
1688
+ x = x.right
1689
+ end
1690
+ end
1691
+ z = allocate_node(key, [value], Node::RED, @nil_node, @nil_node, @nil_node)
1692
+ z.parent = y
1693
+ if y == @nil_node
1694
+ @root = z
1695
+ elsif (key <=> y.key) < 0
1696
+ y.left = z
1697
+ else
1698
+ y.right = z
1699
+ end
1700
+ z.left = @nil_node
1701
+ z.right = @nil_node
1702
+ z.color = Node::RED
1703
+ insert_fixup(z)
1704
+ @value_count += 1
1705
+
1706
+ if @min_node == @nil_node || (key <=> @min_node.key) < 0
1707
+ @min_node = z
1708
+ end
1709
+
1710
+ @hash_index[key] = z # Add to hash index
1711
+ true
1712
+ end
1713
+
1695
1714
  # Traverses the tree in ascending order, yielding each key-value pair.
1696
1715
  #
1697
1716
  # @param range [Range] the range of keys to traverse
@@ -1761,3 +1780,203 @@ class RBTree::Node
1761
1780
  # @return [Array(Object, Object)] the key-value pair
1762
1781
  def pair = [key, value]
1763
1782
  end
1783
+
1784
+ # Allocator for RBTree nodes.
1785
+ #
1786
+ # @api private
1787
+ class RBTree::NodeAllocator
1788
+ # Allocates a new node.
1789
+ #
1790
+ # @param key [Object] the key
1791
+ # @param value [Object] the value
1792
+ # @param color [Boolean] the color (true=red, false=black)
1793
+ # @param left [Node] the left child
1794
+ # @param right [Node] the right child
1795
+ # @param parent [Node] the parent node
1796
+ def allocate(key, value, color, left, right, parent) = RBTree::Node.new(key, value, color, left, right, parent)
1797
+
1798
+ # Releases a node.
1799
+ #
1800
+ # @param node [Node] the node to release
1801
+ def release(node) = nil
1802
+ end
1803
+
1804
+ # Internal node pool for RBTree.
1805
+ #
1806
+ # Manages recycling of Node objects to reduce object allocation overhead.
1807
+ #
1808
+ # @api private
1809
+ class RBTree::NodePool < RBTree::NodeAllocator
1810
+ def initialize
1811
+ @pool = []
1812
+ end
1813
+
1814
+ # Allocates a new node or recycles one from the pool.
1815
+ #
1816
+ # @param key [Object] the key
1817
+ # @param value [Object] the value
1818
+ # @param color [Boolean] the color (true=red, false=black)
1819
+ # @param left [Node] the left child
1820
+ # @param right [Node] the right child
1821
+ # @param parent [Node] the parent node
1822
+ def allocate(key, value, color, left, right, parent)
1823
+ node = @pool.pop
1824
+ if node
1825
+ node.key = key
1826
+ node.value = value
1827
+ node.color = color
1828
+ node.left = left
1829
+ node.right = right
1830
+ node.parent = parent
1831
+ node
1832
+ else
1833
+ super
1834
+ end
1835
+ end
1836
+
1837
+ # Releases a node back to the pool.
1838
+ #
1839
+ # @param node [Node] the node to release
1840
+ def release(node)
1841
+ node.left = node.right = node.parent = node.value = node.key = nil
1842
+ @pool << node
1843
+ end
1844
+ end
1845
+
1846
+ # Internal node pool for RBTree.
1847
+ #
1848
+ # Manages recycling of Node objects to reduce object allocation overhead.
1849
+ # Includes an auto-shrink mechanism to release memory back to GC when
1850
+ # the pool size exceeds the fluctuation range of recent active node count.
1851
+ #
1852
+ # This class can be used to customize the node allocation strategy by passing
1853
+ # an instance to {RBTree#initialize}.
1854
+ class RBTree::AutoShrinkNodePool < RBTree::NodePool
1855
+ # Initializes a new AutoShrinkNodePool.
1856
+ #
1857
+ # @param max_maintenance_interval [Integer] maximum interval between maintenance checks (default: 1000)
1858
+ # @param target_check_interval [Float] target interval in seconds for maintenance checks (default: 1.0)
1859
+ # @param history_size [Integer] duration in seconds to keep history for fluctuation analysis (default: 120)
1860
+ # @param buffer_factor [Float] buffer factor to apply to observed fluctuation (default: 1.25)
1861
+ # @param reserve_ratio [Float] minimum reserve capacity as a ratio of max active nodes (default: 0.1)
1862
+ def initialize(
1863
+ max_maintenance_interval: 1000,
1864
+ target_check_interval: 1.0,
1865
+ history_size: 120,
1866
+ buffer_factor: 1.25,
1867
+ reserve_ratio: 0.1)
1868
+ @pool = []
1869
+
1870
+ @max_maintenance_interval = max_maintenance_interval
1871
+ @target_check_interval = target_check_interval
1872
+ @history_limit = history_size
1873
+ @buffer_ratio = buffer_factor
1874
+ @reserve_ratio = reserve_ratio
1875
+
1876
+ @maintenance_count = 0
1877
+ @check_interval = 1000
1878
+ @check_count = 0
1879
+ @avg_release_rate = nil
1880
+ @last_check_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1881
+
1882
+ @active_nodes = 0
1883
+ @global_max_active = 0
1884
+ @global_min_active = 0
1885
+ @max_active_in_interval = 0
1886
+ @min_active_in_interval = 0
1887
+ @history = []
1888
+ @current_target_capacity = Float::INFINITY
1889
+ end
1890
+
1891
+ # Allocates a new node or recycles one from the pool.
1892
+ #
1893
+ # @param key [Object] the key
1894
+ # @param value [Object] the value
1895
+ # @param color [Boolean] the color (true=red, false=black)
1896
+ # @param left [Node] the left child
1897
+ # @param right [Node] the right child
1898
+ # @param parent [Node] the parent node
1899
+ def allocate(key, value, color, left, right, parent)
1900
+ @active_nodes += 1
1901
+ @max_active_in_interval = @active_nodes if @active_nodes > @max_active_in_interval
1902
+ super
1903
+ end
1904
+
1905
+ # Releases a node back to the pool.
1906
+ #
1907
+ # Checks auto-shrink logic to decide whether to keep the node or let it be GC'd.
1908
+ #
1909
+ # @param node [Node] the node to release
1910
+ def release(node)
1911
+ @active_nodes -= 1
1912
+ @min_active_in_interval = @active_nodes if @active_nodes < @min_active_in_interval
1913
+
1914
+ @check_count += 1
1915
+
1916
+ perform_maintenance if @check_count >= @check_interval
1917
+
1918
+ super if @pool.size < @current_target_capacity
1919
+ end
1920
+
1921
+ private
1922
+
1923
+ def perform_maintenance
1924
+ @maintenance_count += 1
1925
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1926
+ elapsed = now - @last_check_time
1927
+ return if elapsed <= 0
1928
+
1929
+ current_rate = @check_count / elapsed
1930
+
1931
+ if @avg_release_rate.nil?
1932
+ @avg_release_rate = current_rate
1933
+ else
1934
+ @avg_release_rate = (@avg_release_rate * 3 + current_rate) / 4
1935
+ end
1936
+
1937
+ @check_interval = [[(@avg_release_rate * @target_check_interval).to_i, 1].max, @max_maintenance_interval].min
1938
+
1939
+ expired_min = false
1940
+ expired_max = false
1941
+ needs_recalc = false
1942
+
1943
+ cutoff_time = now - @history_limit
1944
+ while !@history.empty? && @history.first[0] < cutoff_time
1945
+ if !expired_min && @history.first[1] == @global_min_active
1946
+ expired_min = true
1947
+ needs_recalc = true
1948
+ end
1949
+ if !expired_max && @history.first[2] == @global_max_active
1950
+ expired_max = true
1951
+ needs_recalc = true
1952
+ end
1953
+ @history.shift
1954
+ end
1955
+
1956
+ @history << [now, @min_active_in_interval, @max_active_in_interval]
1957
+
1958
+ if @min_active_in_interval < @global_min_active
1959
+ @global_min_active = @min_active_in_interval
1960
+ expired_min = false
1961
+ needs_recalc = true
1962
+ end
1963
+ if @max_active_in_interval > @global_max_active
1964
+ @global_max_active = @max_active_in_interval
1965
+ expired_max = false
1966
+ needs_recalc = true
1967
+ end
1968
+
1969
+ @global_min_active = @history.map { |_, min, _| min }.min if expired_min
1970
+ @global_max_active = @history.map { |_, _, max| max }.max if expired_max
1971
+ if needs_recalc
1972
+ fluctuation = @global_max_active - @global_min_active
1973
+ reserve = (@reserve_ratio * @global_max_active).to_i
1974
+ @current_target_capacity = [(fluctuation * @buffer_ratio).to_i, reserve].max
1975
+ end
1976
+
1977
+ @check_count = 0
1978
+ @last_check_time = now
1979
+ @max_active_in_interval = @active_nodes
1980
+ @min_active_in_interval = @active_nodes
1981
+ end
1982
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbtree-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masahito Suzuki
@@ -25,7 +25,6 @@ files:
25
25
  - README.ja.md
26
26
  - README.md
27
27
  - Rakefile
28
- - lib/rbtree.old.rb
29
28
  - lib/rbtree.rb
30
29
  - lib/rbtree/version.rb
31
30
  - rbtree-ruby.gemspec