rbtree-ruby 0.3.1 → 0.3.3

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: df8aaee3fedf309a0079082fa38c10e3748bc68873131800e13f30dee1202273
4
- data.tar.gz: 5e6dd162b0ebb1b50929d9127c4ac57598c7391ecf63a7d1dca7e42107d4ca3a
3
+ metadata.gz: 2145d91a96cc64b7e65c65460716b678301f74d6decdffa2dbd077a914273c21
4
+ data.tar.gz: 66503c542ce07672a493830afbfcb66ebfea14d8563e67b1b2dd1d5ecd1d9c48
5
5
  SHA512:
6
- metadata.gz: c1df7b56cad01d4a0725668f415510a3e65fd6409a0271143504ec1223d64a7e395a93a1dbd43881ceb75e5a36ea9328f60e86e95cdfcde653ab075fab81cb7b
7
- data.tar.gz: cecad1db1c9fba3eb4c91b20493dc849cfbf70f8d2e1fede1c09e8b22d59d925e724e26788e06627a02b620de42c655afef4a8be51d31fdeaff5fa5158a2d192
6
+ metadata.gz: 7befca8277d78b75242a443c2ec155bded73f899222b32330c6416e6ffeaad8ec6bca01e11ce23c6047208871cf71cfb8c6a742732b4ae0b531f54e51a8e424e
7
+ data.tar.gz: eb77127897b535277717575afe27c47cd087ba15685f59b0df67516c55d393a8d7412cf08d667cea5494978746967c098294bb73e8e9771c2aee363762afcca9
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.3] - 2026-01-23
9
+
10
+ ### Optimized
11
+ - **find_successor_node**: Optimized performance for scans starting from the beginning (or before `min_key`) when the `safe: true` option is used. Added a fast-path (short-circuit) when the given key is smaller than `min_key`.
12
+
13
+ ### Fixed
14
+ - **MultiRBTree Aliases**: Fixed `delete`, `get`, and `[]` aliases in `MultiRBTree`. They now correctly point to the overridden methods in the derived class, ensuring correct value handling and size tracking.
15
+ - **enum_for Syntax**: Syntactic corrections for `enum_for` to ensure robust keyword argument handling in enumerators.
16
+
17
+ ## [0.3.2] - 2026-01-15
18
+
19
+ ### Added
20
+ - **Custom Node Allocator**: `RBTree#initialize` now accepts a `node_allocator:` keyword argument, allowing customization of the node pooling strategy.
21
+ - **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.
22
+ - **Explicit Overwrite**: `overwrite: true` is now the explicit default for `initialize` and `insert`.
23
+
8
24
  ## [0.3.1] - 2026-01-15
9
25
 
10
26
  ### Added
@@ -191,6 +207,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
191
207
  - ASCII diagrams for tree rotation operations
192
208
  - MIT License (Copyright © 2026 Masahito Suzuki)
193
209
 
210
+ [0.3.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.2
211
+ [0.3.1]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.1
194
212
  [0.3.0]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.0
195
213
  [0.2.3]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.3
196
214
  [0.2.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.2
data/README.ja.md CHANGED
@@ -6,11 +6,14 @@ Red-Black Tree(赤黒木)データ構造のピュアRuby実装です。挿
6
6
 
7
7
  ## 特徴
8
8
 
9
- - **自己平衡二分探索木**: 赤黒木の性質により最適なパフォーマンスを維持
10
- - **順序付き操作**: 効率的な範囲クエリ、最小/最大値の取得、ソート済みイテレーション
11
- - **複数値サポート**: `MultiRBTree`クラスで同一キーに複数の値を格納可能
12
- - **ピュアRuby**: C拡張不要、あらゆるRuby実装で動作
13
- - **充実したドキュメント**: 使用例付きの包括的なRDocドキュメント
9
+ - **自己平衡二分探索木**: 赤黒木の性質により最適なパフォーマンスを維持。挿入・削除・検索がすべてO(log n)。
10
+ - **順序付き操作**: ソート済みイテレーション、範囲クエリ(lt, gt, between)、最小/最大値取得が高速に実行可能。
11
+ - **複数値サポート**: `MultiRBTree`クラスで同一キーに複数の値を格納。値は挿入順に保持され、最初または最後の値を個別にアクセス可能。
12
+ - **ピュアRuby**: C拡張不要。MRI, JRuby, TruffleRubyなどあらゆるRuby実装で動作。
13
+ - **ハイブリッドインデックス**: 内部ハッシュインデックスにより、キー検索と存在確認がO(1)の超高速アクセスを実現。
14
+ - **メモリ効率**: ノードプールによるオブジェクト再利用と自動縮小機能でGC負荷を大幅に削減。長時間実行アプリにも適応。
15
+ - **最近傍検索**: 数値キーに対して、最も近いキーペアをO(log n)で効率的に探索。
16
+ - **安全なイテレーション**: `safe: true`オプションにより、イテレーション中に他の操作(削除・挿入)を安全に実行可能。
14
17
 
15
18
  ## インストール
16
19
 
@@ -236,12 +239,6 @@ tree.max(last: true) # => [2, "b"] (最大キーの最後の値)
236
239
 
237
240
  全要素のイテレーションはO(n)時間。
238
241
 
239
- ### メモリ効率
240
-
241
- RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用:
242
- - 頻繁な挿入・削除時のGC負荷を大幅に削減
243
- - 100,000回の循環操作ベンチマークで**GC時間0.0秒**を達成
244
-
245
242
  ### RBTree vs Hash vs Array
246
243
 
247
244
  順序付き操作と空間的操作において、RBTreeは単に速いだけでなく、全く異なるクラスの性能を発揮。**50万件**でのベンチマーク:
@@ -254,6 +251,23 @@ RBTreeは内部的な**メモリプール**を使用してノードオブジェ
254
251
  | **ソート済みイテレーション** | **O(n)** | O(n log n) | **無料** | 常にソート済み vs 明示的な`sort` |
255
252
  | **キー検索** | **O(1)** | O(1) | **同等** | **ハイブリッドハッシュインデックスにより、Hashと同等のO(1)検索速度を実現** |
256
253
 
254
+ ### メモリ効率とカスタムアロケータ
255
+
256
+ RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用します。
257
+ - 頻繁な挿入・削除時のGC負荷を大幅に削減します。
258
+ - **自動縮小**: デフォルトの `AutoShrinkNodePool` は、現在の使用量に対してプールが大きくなりすぎた場合、自動的に未使用ノードをRubyのGCに解放します。これにより、負荷が変動する長時間実行アプリケーションでのメモリ肥大化を防ぎます。
259
+ - **カスタマイズ**: プールの動作をカスタマイズしたり、独自のアロケータを指定することができます:
260
+
261
+ ```ruby
262
+ # 自動縮小パラメータのカスタマイズ
263
+ pool = RBTree::AutoShrinkNodePool.new(
264
+ history_size: 60, # 60秒の履歴
265
+ buffer_factor: 1.5, # 変動幅の50%をバッファとして保持
266
+ reserve_ratio: 0.2 # 常に最大値の20%を予備として保持
267
+ )
268
+ tree = RBTree.new(node_allocator: pool)
269
+ ```
270
+
257
271
  ### RBTreeを使うべき場面
258
272
 
259
273
  ✅ **RBTreeが適している場合:**
data/README.md CHANGED
@@ -6,11 +6,14 @@ A pure Ruby implementation of the Red-Black Tree data structure, providing effic
6
6
 
7
7
  ## Features
8
8
 
9
- - **Self-Balancing Binary Search Tree**: Maintains optimal performance through red-black tree properties
10
- - **Ordered Operations**: Efficient range queries, min/max retrieval, and sorted iteration
11
- - **Multi-Value Support**: `MultiRBTree` class for storing multiple values per key
12
- - **Pure Ruby**: No C extensions required, works on any Ruby implementation
13
- - **Well-Documented**: Comprehensive RDoc documentation with examples
9
+ - **Self-Balancing Binary Search Tree**: Maintains optimal performance through red-black tree properties. All insertions, deletions, and lookups run in O(log n).
10
+ - **Ordered Operations**: Efficient sorted iteration, range queries (`lt`, `gt`, `between`), min/max retrieval.
11
+ - **Multi-Value Support**: `MultiRBTree` class stores multiple values per key, with access to first or last value individually.
12
+ - **Pure Ruby**: No C extensions required. Works on MRI, JRuby, TruffleRuby, and all Ruby implementations.
13
+ - **Hybrid Indexing**: Internal hash index enables O(1) key lookup and membership checks — matching standard Hash performance.
14
+ - **Memory Efficiency**: Node recycling with auto-shrinking pool (`AutoShrinkNodePool`) drastically reduces GC pressure in long-running apps.
15
+ - **Nearest Key Search**: Finds the closest numeric key in O(log n) time — ideal for spatial or temporal queries.
16
+ - **Safe Iteration**: Use `safe: true` to safely modify the tree (insert/delete) during iteration.
14
17
 
15
18
  ## Installation
16
19
 
@@ -236,12 +239,6 @@ All major operations run in **O(log n)** time:
236
239
 
237
240
  Iteration over all elements takes O(n) time.
238
241
 
239
- ### Memory Efficiency
240
-
241
- RBTree uses an internal **Memory Pool** to recycle node objects.
242
- - Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions (e.g., in high-throughput queues).
243
- - In benchmarks with 100,000 cyclic operations, **GC time was 0.0s** compared to significant pauses without pooling.
244
-
245
242
  ### RBTree vs Hash vs Array (Overwhelming Power)
246
243
 
247
244
  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**:
@@ -254,6 +251,23 @@ For ordered and spatial operations, RBTree is not just faster—it is in a compl
254
251
  | **Sorted Iteration** | **O(n)** | O(n log n) | **FREE** | Always sorted vs explicit `sort` |
255
252
  | **Key Lookup** | **O(1)** | O(1) | **Equal** | **Hybrid Hash Index provides O(1) access like standard Hash** |
256
253
 
254
+ ### Memory Efficiency & Custom Allocators
255
+
256
+ RBTree uses an internal **Memory Pool** to recycle node objects.
257
+ - Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions.
258
+ - **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.
259
+ - **Customization**: You can customize the pool behavior or provide your own allocator:
260
+
261
+ ```ruby
262
+ # Customize auto-shrink parameters
263
+ pool = RBTree::AutoShrinkNodePool.new(
264
+ history_size: 60, # 1 minute history
265
+ buffer_factor: 1.5, # Keep 50% buffer above fluctuation
266
+ reserve_ratio: 0.2 # Always keep 20% reserve
267
+ )
268
+ tree = RBTree.new(node_allocator: pool)
269
+ ```
270
+
257
271
  ### When to Use RBTree
258
272
 
259
273
  ✅ **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.1"
5
+ VERSION = "0.3.3"
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,9 +100,11 @@ 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
@@ -303,7 +306,7 @@ class RBTree
303
306
  # tree.insert({1 => 'one', 2 => 'two'})
304
307
  # @example Bulk insert from Array
305
308
  # tree.insert([[1, 'one'], [2, 'two']])
306
- def insert(*args, overwrite: true, &block)
309
+ def insert(*args, overwrite: @overwrite, &block)
307
310
  if args.size == 2
308
311
  key, value = args
309
312
  insert_entry(key, value, overwrite: overwrite)
@@ -429,7 +432,7 @@ class RBTree
429
432
  # tree.delete(k) if k.even?
430
433
  # end
431
434
  def keys(reverse: false, safe: false, &block)
432
- return enum_for(:keys, reverse: reverse, safe: safe) { @key_count } unless block_given?
435
+ return enum_for(__method__, reverse: reverse, safe: safe) { @key_count } unless block_given?
433
436
  each(reverse: reverse, safe: safe) { |key, _| yield key }
434
437
  self
435
438
  end
@@ -456,7 +459,7 @@ class RBTree
456
459
  # tree.delete(k) if k.even?
457
460
  # end
458
461
  def each(reverse: false, safe: false, &block)
459
- return enum_for(:each, reverse: reverse, safe: safe) { size } unless block_given?
462
+ return enum_for(__method__, reverse: reverse, safe: safe) { size } unless block_given?
460
463
  if reverse
461
464
  traverse_all_desc(@root, safe: safe, &block)
462
465
  else
@@ -485,7 +488,7 @@ class RBTree
485
488
  # @return [Enumerator, RBTree] an Enumerator if no block is given, self otherwise
486
489
  # @see #each
487
490
  def reverse_each(safe: false, &block)
488
- return enum_for(:reverse_each, safe: safe) { size } unless block_given?
491
+ return enum_for(__method__, safe: safe) { size } unless block_given?
489
492
  each(reverse: true, safe: safe, &block)
490
493
  end
491
494
 
@@ -502,7 +505,7 @@ class RBTree
502
505
  # tree.lt(3, reverse: true).first # => [2, "two"]
503
506
  # tree.lt(3, safe: true) { |k, _| tree.delete(k) if k.even? } # safe to delete
504
507
  def lt(key, reverse: false, safe: false, &block)
505
- return enum_for(:lt, key, reverse: reverse, safe: safe) unless block_given?
508
+ return enum_for(__method__, key, reverse: reverse, safe: safe) unless block_given?
506
509
  if reverse
507
510
  traverse_lt_desc(@root, key, safe: safe, &block)
508
511
  else
@@ -523,7 +526,7 @@ class RBTree
523
526
  # tree.lte(3).to_a # => [[1, "one"], [2, "two"], [3, "three"]]
524
527
  # tree.lte(3, reverse: true).first # => [3, "three"]
525
528
  def lte(key, reverse: false, safe: false, &block)
526
- return enum_for(:lte, key, reverse: reverse, safe: safe) unless block_given?
529
+ return enum_for(__method__, key, reverse: reverse, safe: safe) unless block_given?
527
530
  if reverse
528
531
  traverse_lte_desc(@root, key, safe: safe, &block)
529
532
  else
@@ -544,7 +547,7 @@ class RBTree
544
547
  # tree.gt(2).to_a # => [[3, "three"], [4, "four"]]
545
548
  # tree.gt(2, reverse: true).first # => [4, "four"]
546
549
  def gt(key, reverse: false, safe: false, &block)
547
- return enum_for(:gt, key, reverse: reverse, safe: safe) unless block_given?
550
+ return enum_for(__method__, key, reverse: reverse, safe: safe) unless block_given?
548
551
  if reverse
549
552
  traverse_gt_desc(@root, key, safe: safe, &block)
550
553
  else
@@ -565,7 +568,7 @@ class RBTree
565
568
  # tree.gte(2).to_a # => [[2, "two"], [3, "three"], [4, "four"]]
566
569
  # tree.gte(2, reverse: true).first # => [4, "four"]
567
570
  def gte(key, reverse: false, safe: false, &block)
568
- return enum_for(:gte, key, reverse: reverse, safe: safe) unless block_given?
571
+ return enum_for(__method__, key, reverse: reverse, safe: safe) unless block_given?
569
572
  if reverse
570
573
  traverse_gte_desc(@root, key, safe: safe, &block)
571
574
  else
@@ -589,7 +592,7 @@ class RBTree
589
592
  # tree.between(2, 4).to_a # => [[2, "two"], [3, "three"], [4, "four"]]
590
593
  # tree.between(2, 4, reverse: true).first # => [4, "four"]
591
594
  def between(min, max, include_min: true, include_max: true, reverse: false, safe: false, &block)
592
- return enum_for(:between, min, max, include_min: include_min, include_max: include_max, reverse: reverse, safe: safe) unless block_given?
595
+ return enum_for(__method__, min, max, include_min: include_min, include_max: include_max, reverse: reverse, safe: safe) unless block_given?
593
596
  if reverse
594
597
  traverse_between_desc(@root, min, max, include_min, include_max, safe: safe, &block)
595
598
  else
@@ -1191,6 +1194,8 @@ class RBTree
1191
1194
  # @param key [Object] the reference key
1192
1195
  # @return [Node] the successor node, or @nil_node if none exists
1193
1196
  def find_successor_node(key)
1197
+ # If key is smaller than min_key, return min_node
1198
+ return @min_node if min_key && key < min_key
1194
1199
  # Check if key exists using O(1) hash lookup
1195
1200
  if (node = @hash_index[key])
1196
1201
  # Key exists: find successor in subtree or ancestors
@@ -1326,30 +1331,17 @@ class RBTree
1326
1331
  # Allocates a new node or recycles one from the pool.
1327
1332
  # @return [Node]
1328
1333
  def allocate_node(key, value, color, left, right, parent)
1329
- node = @node_pool.pop
1330
- if node
1331
- node.key = key
1332
- node.value = value
1333
- node.color = color
1334
- node.left = left
1335
- node.right = right
1336
- node.parent = parent
1337
- node
1338
- else
1339
- node = Node.new(key, value, color, left, right, parent)
1340
- end
1334
+ node = @node_allocator.allocate(key, value, color, left, right, parent)
1341
1335
  @key_count += 1
1342
1336
  node
1343
1337
  end
1344
1338
 
1339
+ # Releases a node back to the pool.
1340
+ # @param node [Node] the node to release
1345
1341
  # Releases a node back to the pool.
1346
1342
  # @param node [Node] the node to release
1347
1343
  def release_node(node)
1348
- node.left = nil
1349
- node.right = nil
1350
- node.parent = nil
1351
- node.value = nil # Help GC
1352
- @node_pool << node
1344
+ @node_allocator.release(node)
1353
1345
  @key_count -= 1
1354
1346
  end
1355
1347
 
@@ -1506,6 +1498,8 @@ class MultiRBTree < RBTree
1506
1498
  # tree.get(1) # => "first"
1507
1499
  # tree.get(1, last: true) # => "second"
1508
1500
  def value(key, last: false) = @hash_index[key]&.value&.send(last ? :last : :first)
1501
+ alias :get :value
1502
+ alias :[] :value
1509
1503
 
1510
1504
  # Retrieves the first value associated with the given key.
1511
1505
  #
@@ -1531,7 +1525,7 @@ class MultiRBTree < RBTree
1531
1525
  # tree.insert(1, 'second')
1532
1526
  # tree.values(1).to_a # => ["first", "second"]
1533
1527
  def values(key, reverse: false)
1534
- return enum_for(:values, key) { value_count(key) } unless block_given?
1528
+ return enum_for(__method__, key) { value_count(key) } unless block_given?
1535
1529
  @hash_index[key]&.value&.send(reverse ? :reverse_each : :each) { |v| yield v }
1536
1530
  end
1537
1531
  alias :get_all :values
@@ -1632,6 +1626,7 @@ class MultiRBTree < RBTree
1632
1626
  delete_indexed_node(z.key)
1633
1627
  value
1634
1628
  end
1629
+ alias :delete :delete_key
1635
1630
 
1636
1631
  # Removes and returns the first key-value pair.
1637
1632
  #
@@ -1790,3 +1785,203 @@ class RBTree::Node
1790
1785
  # @return [Array(Object, Object)] the key-value pair
1791
1786
  def pair = [key, value]
1792
1787
  end
1788
+
1789
+ # Allocator for RBTree nodes.
1790
+ #
1791
+ # @api private
1792
+ class RBTree::NodeAllocator
1793
+ # Allocates a new node.
1794
+ #
1795
+ # @param key [Object] the key
1796
+ # @param value [Object] the value
1797
+ # @param color [Boolean] the color (true=red, false=black)
1798
+ # @param left [Node] the left child
1799
+ # @param right [Node] the right child
1800
+ # @param parent [Node] the parent node
1801
+ def allocate(key, value, color, left, right, parent) = RBTree::Node.new(key, value, color, left, right, parent)
1802
+
1803
+ # Releases a node.
1804
+ #
1805
+ # @param node [Node] the node to release
1806
+ def release(node) = nil
1807
+ end
1808
+
1809
+ # Internal node pool for RBTree.
1810
+ #
1811
+ # Manages recycling of Node objects to reduce object allocation overhead.
1812
+ #
1813
+ # @api private
1814
+ class RBTree::NodePool < RBTree::NodeAllocator
1815
+ def initialize
1816
+ @pool = []
1817
+ end
1818
+
1819
+ # Allocates a new node or recycles one from the pool.
1820
+ #
1821
+ # @param key [Object] the key
1822
+ # @param value [Object] the value
1823
+ # @param color [Boolean] the color (true=red, false=black)
1824
+ # @param left [Node] the left child
1825
+ # @param right [Node] the right child
1826
+ # @param parent [Node] the parent node
1827
+ def allocate(key, value, color, left, right, parent)
1828
+ node = @pool.pop
1829
+ if node
1830
+ node.key = key
1831
+ node.value = value
1832
+ node.color = color
1833
+ node.left = left
1834
+ node.right = right
1835
+ node.parent = parent
1836
+ node
1837
+ else
1838
+ super
1839
+ end
1840
+ end
1841
+
1842
+ # Releases a node back to the pool.
1843
+ #
1844
+ # @param node [Node] the node to release
1845
+ def release(node)
1846
+ node.left = node.right = node.parent = node.value = node.key = nil
1847
+ @pool << node
1848
+ end
1849
+ end
1850
+
1851
+ # Internal node pool for RBTree.
1852
+ #
1853
+ # Manages recycling of Node objects to reduce object allocation overhead.
1854
+ # Includes an auto-shrink mechanism to release memory back to GC when
1855
+ # the pool size exceeds the fluctuation range of recent active node count.
1856
+ #
1857
+ # This class can be used to customize the node allocation strategy by passing
1858
+ # an instance to {RBTree#initialize}.
1859
+ class RBTree::AutoShrinkNodePool < RBTree::NodePool
1860
+ # Initializes a new AutoShrinkNodePool.
1861
+ #
1862
+ # @param max_maintenance_interval [Integer] maximum interval between maintenance checks (default: 1000)
1863
+ # @param target_check_interval [Float] target interval in seconds for maintenance checks (default: 1.0)
1864
+ # @param history_size [Integer] duration in seconds to keep history for fluctuation analysis (default: 120)
1865
+ # @param buffer_factor [Float] buffer factor to apply to observed fluctuation (default: 1.25)
1866
+ # @param reserve_ratio [Float] minimum reserve capacity as a ratio of max active nodes (default: 0.1)
1867
+ def initialize(
1868
+ max_maintenance_interval: 1000,
1869
+ target_check_interval: 1.0,
1870
+ history_size: 120,
1871
+ buffer_factor: 1.25,
1872
+ reserve_ratio: 0.1)
1873
+ @pool = []
1874
+
1875
+ @max_maintenance_interval = max_maintenance_interval
1876
+ @target_check_interval = target_check_interval
1877
+ @history_limit = history_size
1878
+ @buffer_ratio = buffer_factor
1879
+ @reserve_ratio = reserve_ratio
1880
+
1881
+ @maintenance_count = 0
1882
+ @check_interval = 1000
1883
+ @check_count = 0
1884
+ @avg_release_rate = nil
1885
+ @last_check_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1886
+
1887
+ @active_nodes = 0
1888
+ @global_max_active = 0
1889
+ @global_min_active = 0
1890
+ @max_active_in_interval = 0
1891
+ @min_active_in_interval = 0
1892
+ @history = []
1893
+ @current_target_capacity = Float::INFINITY
1894
+ end
1895
+
1896
+ # Allocates a new node or recycles one from the pool.
1897
+ #
1898
+ # @param key [Object] the key
1899
+ # @param value [Object] the value
1900
+ # @param color [Boolean] the color (true=red, false=black)
1901
+ # @param left [Node] the left child
1902
+ # @param right [Node] the right child
1903
+ # @param parent [Node] the parent node
1904
+ def allocate(key, value, color, left, right, parent)
1905
+ @active_nodes += 1
1906
+ @max_active_in_interval = @active_nodes if @active_nodes > @max_active_in_interval
1907
+ super
1908
+ end
1909
+
1910
+ # Releases a node back to the pool.
1911
+ #
1912
+ # Checks auto-shrink logic to decide whether to keep the node or let it be GC'd.
1913
+ #
1914
+ # @param node [Node] the node to release
1915
+ def release(node)
1916
+ @active_nodes -= 1
1917
+ @min_active_in_interval = @active_nodes if @active_nodes < @min_active_in_interval
1918
+
1919
+ @check_count += 1
1920
+
1921
+ perform_maintenance if @check_count >= @check_interval
1922
+
1923
+ super if @pool.size < @current_target_capacity
1924
+ end
1925
+
1926
+ private
1927
+
1928
+ def perform_maintenance
1929
+ @maintenance_count += 1
1930
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1931
+ elapsed = now - @last_check_time
1932
+ return if elapsed <= 0
1933
+
1934
+ current_rate = @check_count / elapsed
1935
+
1936
+ if @avg_release_rate.nil?
1937
+ @avg_release_rate = current_rate
1938
+ else
1939
+ @avg_release_rate = (@avg_release_rate * 3 + current_rate) / 4
1940
+ end
1941
+
1942
+ @check_interval = [[(@avg_release_rate * @target_check_interval).to_i, 1].max, @max_maintenance_interval].min
1943
+
1944
+ expired_min = false
1945
+ expired_max = false
1946
+ needs_recalc = false
1947
+
1948
+ cutoff_time = now - @history_limit
1949
+ while !@history.empty? && @history.first[0] < cutoff_time
1950
+ if !expired_min && @history.first[1] == @global_min_active
1951
+ expired_min = true
1952
+ needs_recalc = true
1953
+ end
1954
+ if !expired_max && @history.first[2] == @global_max_active
1955
+ expired_max = true
1956
+ needs_recalc = true
1957
+ end
1958
+ @history.shift
1959
+ end
1960
+
1961
+ @history << [now, @min_active_in_interval, @max_active_in_interval]
1962
+
1963
+ if @min_active_in_interval < @global_min_active
1964
+ @global_min_active = @min_active_in_interval
1965
+ expired_min = false
1966
+ needs_recalc = true
1967
+ end
1968
+ if @max_active_in_interval > @global_max_active
1969
+ @global_max_active = @max_active_in_interval
1970
+ expired_max = false
1971
+ needs_recalc = true
1972
+ end
1973
+
1974
+ @global_min_active = @history.map { |_, min, _| min }.min if expired_min
1975
+ @global_max_active = @history.map { |_, _, max| max }.max if expired_max
1976
+ if needs_recalc
1977
+ fluctuation = @global_max_active - @global_min_active
1978
+ reserve = (@reserve_ratio * @global_max_active).to_i
1979
+ @current_target_capacity = [(fluctuation * @buffer_ratio).to_i, reserve].max
1980
+ end
1981
+
1982
+ @check_count = 0
1983
+ @last_check_time = now
1984
+ @max_active_in_interval = @active_nodes
1985
+ @min_active_in_interval = @active_nodes
1986
+ end
1987
+ 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.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masahito Suzuki