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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.ja.md +25 -11
- data/README.md +25 -11
- data/lib/rbtree/version.rb +1 -1
- data/lib/rbtree.rb +224 -29
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2145d91a96cc64b7e65c65460716b678301f74d6decdffa2dbd077a914273c21
|
|
4
|
+
data.tar.gz: 66503c542ce07672a493830afbfcb66ebfea14d8563e67b1b2dd1d5ecd1d9c48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
13
|
-
-
|
|
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
|
|
11
|
-
- **Multi-Value Support**: `MultiRBTree` class
|
|
12
|
-
- **Pure Ruby**: No C extensions required
|
|
13
|
-
- **
|
|
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:**
|
data/lib/rbtree/version.rb
CHANGED
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
|
-
@
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = @
|
|
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
|
|
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(
|
|
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
|