rbtree-ruby 0.3.1 → 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: df8aaee3fedf309a0079082fa38c10e3748bc68873131800e13f30dee1202273
4
- data.tar.gz: 5e6dd162b0ebb1b50929d9127c4ac57598c7391ecf63a7d1dca7e42107d4ca3a
3
+ metadata.gz: 327862d13e55ed6406023e1d10676e4b5e94541c59400d9205380af08822e4bf
4
+ data.tar.gz: 465e1a514390286bf0165db4dfa9f25ada4cd3d1410b0115b11eba15b03dcf33
5
5
  SHA512:
6
- metadata.gz: c1df7b56cad01d4a0725668f415510a3e65fd6409a0271143504ec1223d64a7e395a93a1dbd43881ceb75e5a36ea9328f60e86e95cdfcde653ab075fab81cb7b
7
- data.tar.gz: cecad1db1c9fba3eb4c91b20493dc849cfbf70f8d2e1fede1c09e8b22d59d925e724e26788e06627a02b620de42c655afef4a8be51d31fdeaff5fa5158a2d192
6
+ metadata.gz: 1f354523b8fb78b746cf256949887b89184ce1dcd190403a7024949cded391de24ce6f932720a58ae24592b4b453f3901b1896c9904f546aa03fe4b1524698c2
7
+ data.tar.gz: e1219a20ea771f3fe1ca2fc160d737c787a1a0e81cb5389b63f320dcd1d2ac9d2cc71d9df2b9698d73afc98069f76d39285ca8106691f0025f1d5a5fa8b6bf91
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ 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
+
8
15
  ## [0.3.1] - 2026-01-15
9
16
 
10
17
  ### Added
@@ -191,6 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
191
198
  - ASCII diagrams for tree rotation operations
192
199
  - MIT License (Copyright © 2026 Masahito Suzuki)
193
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
194
203
  [0.3.0]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.0
195
204
  [0.2.3]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.3
196
205
  [0.2.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.2
data/README.ja.md CHANGED
@@ -236,12 +236,6 @@ tree.max(last: true) # => [2, "b"] (最大キーの最後の値)
236
236
 
237
237
  全要素のイテレーションはO(n)時間。
238
238
 
239
- ### メモリ効率
240
-
241
- RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用:
242
- - 頻繁な挿入・削除時のGC負荷を大幅に削減
243
- - 100,000回の循環操作ベンチマークで**GC時間0.0秒**を達成
244
-
245
239
  ### RBTree vs Hash vs Array
246
240
 
247
241
  順序付き操作と空間的操作において、RBTreeは単に速いだけでなく、全く異なるクラスの性能を発揮。**50万件**でのベンチマーク:
@@ -254,6 +248,23 @@ RBTreeは内部的な**メモリプール**を使用してノードオブジェ
254
248
  | **ソート済みイテレーション** | **O(n)** | O(n log n) | **無料** | 常にソート済み vs 明示的な`sort` |
255
249
  | **キー検索** | **O(1)** | O(1) | **同等** | **ハイブリッドハッシュインデックスにより、Hashと同等のO(1)検索速度を実現** |
256
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
+
257
268
  ### RBTreeを使うべき場面
258
269
 
259
270
  ✅ **RBTreeが適している場合:**
data/README.md CHANGED
@@ -236,12 +236,6 @@ All major operations run in **O(log n)** time:
236
236
 
237
237
  Iteration over all elements takes O(n) time.
238
238
 
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
239
  ### RBTree vs Hash vs Array (Overwhelming Power)
246
240
 
247
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**:
@@ -254,6 +248,23 @@ For ordered and spatial operations, RBTree is not just faster—it is in a compl
254
248
  | **Sorted Iteration** | **O(n)** | O(n log n) | **FREE** | Always sorted vs explicit `sort` |
255
249
  | **Key Lookup** | **O(1)** | O(1) | **Equal** | **Hybrid Hash Index provides O(1) access like standard Hash** |
256
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
+
257
268
  ### When to Use RBTree
258
269
 
259
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.1"
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,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)
@@ -1326,30 +1329,17 @@ class RBTree
1326
1329
  # Allocates a new node or recycles one from the pool.
1327
1330
  # @return [Node]
1328
1331
  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
1332
+ node = @node_allocator.allocate(key, value, color, left, right, parent)
1341
1333
  @key_count += 1
1342
1334
  node
1343
1335
  end
1344
1336
 
1337
+ # Releases a node back to the pool.
1338
+ # @param node [Node] the node to release
1345
1339
  # Releases a node back to the pool.
1346
1340
  # @param node [Node] the node to release
1347
1341
  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
1342
+ @node_allocator.release(node)
1353
1343
  @key_count -= 1
1354
1344
  end
1355
1345
 
@@ -1790,3 +1780,203 @@ class RBTree::Node
1790
1780
  # @return [Array(Object, Object)] the key-value pair
1791
1781
  def pair = [key, value]
1792
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.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masahito Suzuki