raioquic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.containerignore +4 -0
  3. data/.rubocop.yml +93 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Containerfile +6 -0
  7. data/Gemfile +24 -0
  8. data/Gemfile.lock +113 -0
  9. data/LICENSE +28 -0
  10. data/README.md +48 -0
  11. data/Rakefile +16 -0
  12. data/Steepfile +8 -0
  13. data/example/curlcatcher.rb +18 -0
  14. data/example/interoperability/README.md +9 -0
  15. data/example/interoperability/aioquic/aioquic_client.py +47 -0
  16. data/example/interoperability/aioquic/aioquic_server.py +34 -0
  17. data/example/interoperability/key.pem +28 -0
  18. data/example/interoperability/localhost-unasuke-dev.crt +21 -0
  19. data/example/interoperability/quic-go/sample_server.go +61 -0
  20. data/example/interoperability/raioquic_client.rb +42 -0
  21. data/example/interoperability/raioquic_server.rb +43 -0
  22. data/example/parse_curl_example.rb +108 -0
  23. data/lib/raioquic/buffer.rb +202 -0
  24. data/lib/raioquic/core_ext.rb +54 -0
  25. data/lib/raioquic/crypto/README.md +5 -0
  26. data/lib/raioquic/crypto/aesgcm.rb +52 -0
  27. data/lib/raioquic/crypto/backend/aead.rb +52 -0
  28. data/lib/raioquic/crypto/backend.rb +12 -0
  29. data/lib/raioquic/crypto.rb +10 -0
  30. data/lib/raioquic/quic/configuration.rb +81 -0
  31. data/lib/raioquic/quic/connection.rb +2776 -0
  32. data/lib/raioquic/quic/crypto.rb +317 -0
  33. data/lib/raioquic/quic/event.rb +69 -0
  34. data/lib/raioquic/quic/logger.rb +272 -0
  35. data/lib/raioquic/quic/packet.rb +471 -0
  36. data/lib/raioquic/quic/packet_builder.rb +301 -0
  37. data/lib/raioquic/quic/rangeset.rb +113 -0
  38. data/lib/raioquic/quic/recovery.rb +528 -0
  39. data/lib/raioquic/quic/stream.rb +343 -0
  40. data/lib/raioquic/quic.rb +20 -0
  41. data/lib/raioquic/tls.rb +1659 -0
  42. data/lib/raioquic/version.rb +5 -0
  43. data/lib/raioquic.rb +12 -0
  44. data/misc/export_x25519.py +43 -0
  45. data/misc/gen_rfc8448_keypair.rb +90 -0
  46. data/raioquic.gemspec +37 -0
  47. data/sig/raioquic/buffer.rbs +37 -0
  48. data/sig/raioquic/core_ext.rbs +7 -0
  49. data/sig/raioquic/crypto/aesgcm.rbs +20 -0
  50. data/sig/raioquic/crypto/backend/aead.rbs +11 -0
  51. data/sig/raioquic/quic/configuration.rbs +34 -0
  52. data/sig/raioquic/quic/connection.rbs +277 -0
  53. data/sig/raioquic/quic/crypto.rbs +88 -0
  54. data/sig/raioquic/quic/event.rbs +51 -0
  55. data/sig/raioquic/quic/logger.rbs +57 -0
  56. data/sig/raioquic/quic/packet.rbs +157 -0
  57. data/sig/raioquic/quic/packet_builder.rbs +76 -0
  58. data/sig/raioquic/quic/rangeset.rbs +17 -0
  59. data/sig/raioquic/quic/recovery.rbs +142 -0
  60. data/sig/raioquic/quic/stream.rbs +87 -0
  61. data/sig/raioquic/tls.rbs +444 -0
  62. data/sig/raioquic.rbs +9 -0
  63. metadata +121 -0
@@ -0,0 +1,528 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raioquic
4
+ module Quic
5
+ module Recovery
6
+ # loss detection
7
+ K_PACKET_THRESHOLD = 3
8
+ K_GRANULARITY = 0.001 # seconds
9
+ K_TIME_THRESHOLD = 9 / 8.0
10
+ K_MICRO_SECOND = 0.000001
11
+ K_SECOND = 1.0
12
+
13
+ # congestion control
14
+ K_MAX_DATAGRAM_SIZE = 1280
15
+ K_INITIAL_WINDOW = 10 * K_MAX_DATAGRAM_SIZE
16
+ K_MINIMUM_WINDOW = 2 * K_MAX_DATAGRAM_SIZE
17
+ K_LOSS_REDUCTION_FACTOR = 0.5
18
+
19
+ # QuicPacketSpace
20
+ class QuicPacketSpace
21
+ attr_accessor :sent_packets
22
+ attr_accessor :ack_at
23
+ attr_accessor :ack_eliciting_in_flight
24
+ attr_accessor :loss_time
25
+ attr_accessor :largest_acked_packet
26
+ attr_accessor :expected_packet_number
27
+ attr_accessor :largest_received_packet
28
+ attr_accessor :largest_received_time
29
+ attr_accessor :ack_queue
30
+ attr_accessor :discarded
31
+
32
+ def initialize
33
+ @ack_at = nil
34
+ @ack_queue = Rangeset.new
35
+ @discarded = false
36
+ @expected_packet_number = 0
37
+ @largest_received_packet = -1
38
+ @largest_received_time = nil
39
+
40
+ # sent packets and loss
41
+ @ack_eliciting_in_flight = 0
42
+ @largest_acked_packet = 0
43
+ @loss_time = nil
44
+ @sent_packets = {}
45
+ end
46
+ end
47
+
48
+ # QuicPacketPacer
49
+ class QuicPacketPacer
50
+ attr_reader :bucket_max
51
+ attr_reader :bucket_time
52
+ attr_reader :packet_time
53
+
54
+ def initialize
55
+ @bucket_max = 0.0
56
+ @bucket_time = 0.0
57
+ @evaluation_time = 0.0
58
+ @packet_time = nil
59
+ end
60
+
61
+ def next_send_time(now:)
62
+ if @packet_time
63
+ update_bucket(now: now)
64
+ return now + @packet_time if @bucket_time <= 0.0
65
+ end
66
+ return nil
67
+ end
68
+
69
+ def update_after_send(now:)
70
+ if @packet_time # rubocop:disable Style/GuardClause
71
+ update_bucket(now: now)
72
+ if @bucket_time < @packet_time
73
+ @bucket_time = 0.0
74
+ else
75
+ @bucket_time -= @packet_time
76
+ end
77
+ end
78
+ end
79
+
80
+ def update_bucket(now:)
81
+ if now > @evaluation_time # rubocop:disable Style/GuardClause
82
+ @bucket_time = [@bucket_time + (now - @evaluation_time), @bucket_max].min
83
+ @evaluation_time = now
84
+ end
85
+ end
86
+
87
+ def update_rate(congestion_window:, smoothed_rtt:)
88
+ pacing_rate = congestion_window / [smoothed_rtt, K_MICRO_SECOND].max
89
+ @packet_time = [K_MICRO_SECOND, [K_MAX_DATAGRAM_SIZE / pacing_rate, K_SECOND].min].max
90
+
91
+ @bucket_max = [2 * K_MAX_DATAGRAM_SIZE, [(congestion_window / 4).floor(1), 16 * K_MAX_DATAGRAM_SIZE].min].max / pacing_rate
92
+
93
+ @bucket_time = @bucket_max if @bucket_time > @bucket_max
94
+ end
95
+ end
96
+
97
+ # New Reno congestion control.
98
+ class QuicCongestionControl
99
+ attr_reader :congestion_window
100
+ attr_reader :ssthresh
101
+
102
+ attr_accessor :bytes_in_flight
103
+
104
+ def initialize
105
+ @bytes_in_flight = 0
106
+ @congestion_window = K_INITIAL_WINDOW
107
+ @congestion_recovery_start_time = 0.0
108
+ @congestion_stash = 0
109
+ @rtt_monitor = QuicRttMonitor.new
110
+ @ssthresh = nil
111
+ end
112
+
113
+ def on_packet_acked(packet:)
114
+ @bytes_in_flight -= packet.sent_bytes
115
+
116
+ # don't increase window in congestion recovery
117
+ return if packet.sent_time <= @congestion_recovery_start_time
118
+
119
+ if @ssthresh.nil? || @congestion_window < @ssthresh
120
+ # slow start
121
+ @congestion_window += packet.sent_bytes
122
+ else
123
+ # congestion avoidance
124
+ @congestion_stash += packet.sent_bytes
125
+ count = (@congestion_stash / @congestion_window.to_f).floor(1)
126
+ if count > 0.0
127
+ @congestion_stash -= count * @congestion_window
128
+ @congestion_window += count * K_MAX_DATAGRAM_SIZE
129
+ end
130
+ end
131
+ end
132
+
133
+ def on_packet_sent(packet:)
134
+ @bytes_in_flight += packet.sent_bytes
135
+ end
136
+
137
+ def on_packets_expired(packets:)
138
+ packets.each { |packet| @bytes_in_flight -= packet.sent_bytes }
139
+ end
140
+
141
+ def on_packets_lost(packets:, now:)
142
+ lost_largest_time = 0.0
143
+ packets.each do |packet|
144
+ @bytes_in_flight -= packet.sent_bytes
145
+ lost_largest_time = packet.sent_time
146
+ end
147
+
148
+ # start a new congestion event if packet was sent after the
149
+ # start of the previous congestioon recovery period.
150
+ if lost_largest_time > @congestion_recovery_start_time # rubocop:disable Style/GuardClause
151
+ @congestion_recovery_start_time = now
152
+ @congestion_window = [(@congestion_window * K_LOSS_REDUCTION_FACTOR).to_i, K_MINIMUM_WINDOW].max
153
+ @ssthresh = @congestion_window
154
+ end
155
+ # TODO: collapse congestion window if persistent congestion (from aioquic)
156
+ end
157
+
158
+ def on_rtt_measurement(latest_rtt:, now:)
159
+ if @ssthresh.nil? && @rtt_monitor.is_rtt_increasing(rtt: latest_rtt, now: now) # rubocop:disable Style/GuardClause
160
+ @ssthresh = @congestion_window
161
+ end
162
+ end
163
+ end
164
+
165
+ # Packet loss and congestion controller.
166
+ class QuicPacketRecovery
167
+ attr_reader :rtt_initialized
168
+ attr_reader :rtt_latest
169
+ attr_reader :rtt_min
170
+ attr_reader :rtt_smoothed
171
+ attr_reader :cc
172
+
173
+ attr_accessor :spaces
174
+ attr_accessor :peer_completed_address_validation
175
+ attr_accessor :pacer
176
+ attr_accessor :max_ack_delay
177
+
178
+ def initialize(initial_rtt:, peer_completed_address_validation:, send_probe: nil, logger: nil, quic_logger: nil)
179
+ @max_ack_delay = 0.025
180
+ @peer_completed_address_validation = peer_completed_address_validation
181
+ @spaces = []
182
+
183
+ # callbacks
184
+ @logger = logger
185
+ @quic_logger = quic_logger
186
+ @send_probe = send_probe
187
+
188
+ # loss detection
189
+ @pto_count = 0
190
+ @rtt_initial = initial_rtt
191
+ @rtt_initialized = false
192
+ @rtt_latest = 0.0
193
+ @rtt_min = Float::INFINITY
194
+ @rtt_smoothed = 0.0
195
+ @rtt_variance = 0.0
196
+ @time_of_last_sent_ack_eliciting_packet = 0.0
197
+
198
+ # congestion control
199
+ @cc = QuicCongestionControl.new
200
+ @pacer = QuicPacketPacer.new
201
+ end
202
+
203
+ def bytes_in_flight = @cc.bytes_in_flight
204
+
205
+ def congestion_window = @cc.congestion_window
206
+
207
+ def discard_space(space:)
208
+ raise ArgumentError unless @spaces.include?(space)
209
+
210
+ @cc.on_packets_expired(packets: space.sent_packets.values.filter(&:in_flight))
211
+ space.sent_packets.clear
212
+ space.ack_at = nil
213
+ space.ack_eliciting_in_flight = 0
214
+ space.loss_time = nil
215
+
216
+ # rest PTO count
217
+ @pto_count = 0
218
+
219
+ # TODO: logger
220
+ end
221
+
222
+ def get_loss_detection_time
223
+ # loss timer
224
+ loss_space = get_loss_space
225
+ return loss_space.loss_time if loss_space
226
+
227
+ # packet timer
228
+ if !peer_completed_address_validation || @spaces.sum(&:ack_eliciting_in_flight) > 0
229
+ timeout = get_probe_timeout * (2**@pto_count)
230
+ return @time_of_last_sent_ack_eliciting_packet + timeout
231
+ end
232
+
233
+ return nil
234
+ end
235
+
236
+ def get_probe_timeout
237
+ return 2 * @rtt_initial unless @rtt_initialized
238
+
239
+ return @rtt_smoothed + [4 * @rtt_variance, K_GRANULARITY].max + @max_ack_delay
240
+ end
241
+
242
+ # Update metrics as the result of an ACK being received.
243
+ def on_ack_received(space:, ack_rangeset:, ack_delay:, now:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
244
+ is_ack_eliciting = false
245
+ largest_acked = ack_rangeset.bounds.last - 1
246
+ largest_newly_acked = nil
247
+ largest_sent_time = nil
248
+ log_rtt = nil
249
+
250
+ space.largest_acked_packet = largest_acked if largest_acked > space.largest_acked_packet
251
+
252
+ space.sent_packets.keys.sort.each do |packet_number|
253
+ break if packet_number > largest_acked
254
+
255
+ if ack_rangeset.in?(packet_number) # rubocop:disable Style/Next
256
+ packet = space.sent_packets.delete(packet_number)
257
+
258
+ if packet.is_ack_eliciting
259
+ is_ack_eliciting = true
260
+ space.ack_eliciting_in_flight -= 1
261
+ end
262
+
263
+ @cc.on_packet_acked(packet: packet) if packet.in_flight
264
+
265
+ largest_newly_acked = packet_number
266
+ largest_sent_time = packet.sent_time
267
+
268
+ # trigger callbacks
269
+ packet.delivery_handlers&.each do |handler|
270
+ # TODO: hmm...
271
+ delivery = Quic::PacketBuilder::QuicDeliveryState::ACKED
272
+ case handler[0]&.name
273
+ when :on_data_delivery
274
+ handler[0].call(delivery: delivery, start: handler[1][0], stop: handler[1][1])
275
+ when :on_ack_delivery
276
+ handler[0].call(delivery: delivery, space: handler[1][0], highest_acked: handler[1][1])
277
+ when :on_new_connection_id_delivery
278
+ handler[0].call(delivery: delivery, connection_id: handler[1][0])
279
+ when :on_handshake_done_delivery, :on_reset_delivery, :on_stop_sending_delivery
280
+ handler[0].call(delivery: delivery)
281
+ when :on_ping_delivery
282
+ handler[0].call(delivery: delivery, uids: handler[1][0])
283
+ when :on_connection_limit_delivery
284
+ handler[0].call(delivery: delivery, limit: handler[1][0])
285
+ when :on_retire_connection_id_delivery
286
+ handler[0].call(delivery: delivery, sequence_number: handler[1][0])
287
+ else
288
+ raise NotImplementedError, handler[0]
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ return if largest_newly_acked.nil?
295
+
296
+ if largest_acked == largest_newly_acked && is_ack_eliciting
297
+ latest_rtt = now - largest_sent_time
298
+ log_rtt = true
299
+
300
+ # limit ACK delay to max_ack_delay
301
+ ack_delay = [ack_delay, @max_ack_delay].min
302
+
303
+ # update RTT estimate, which cannot be < 1 ms
304
+ @rtt_latest = [latest_rtt, 0.001].max
305
+ @rtt_min = @rtt_latest if @rtt_latest < @rtt_min
306
+ @rtt_latest -= ack_delay if @rtt_latest > @rtt_min + ack_delay
307
+
308
+ if !@rtt_initialized # rubocop:disable Style/NegatedIfElseCondition
309
+ @rtt_initialized = true
310
+ @rtt_variance = latest_rtt / 2.0
311
+ @rtt_smoothed = latest_rtt
312
+ else
313
+ @rtt_variance = (3.0 / 4.0 * @rtt_variance) + (1 / 4 * (@rtt_min - @rtt_latest).abs)
314
+ @rtt_smoothed - (7.0 / 8.0 * @rtt_smoothed) + (1 / 8 * @rtt_latest)
315
+ end
316
+
317
+ # inform congestion controller
318
+ @cc.on_rtt_measurement(latest_rtt: latest_rtt, now: now)
319
+ @pacer.update_rate(congestion_window: @cc.congestion_window, smoothed_rtt: @rtt_smoothed)
320
+ else
321
+ log_rtt = false
322
+ end
323
+
324
+ detect_loss(space: space, now: now)
325
+
326
+ # reset PTO count
327
+ @pto_count = 0
328
+
329
+ log_metrics_updated(log_rtt) if @quic_logger
330
+ end
331
+
332
+ def on_loss_detection_timeout(now:)
333
+ loss_space = get_loss_space
334
+ if loss_space
335
+ detect_loss(space: loss_space, now: now)
336
+ else
337
+ @pto_count += 1
338
+ reschedule_data(now: now)
339
+ end
340
+ end
341
+
342
+ def on_packet_sent(packet:, space:)
343
+ space.sent_packets[packet.packet_number] = packet
344
+
345
+ space.ack_eliciting_in_flight += 1 if packet.is_ack_eliciting
346
+
347
+ if packet.in_flight # rubocop:disable Style/GuardClause
348
+ @time_of_last_sent_ack_eliciting_packet = packet.sent_time if packet.is_ack_eliciting
349
+
350
+ # add packet to bytes in flight
351
+ @cc.on_packet_sent(packet: packet)
352
+ log_metrics_updated if @quic_logger
353
+ end
354
+ end
355
+
356
+ # Schedule some data for retransmission.
357
+ def reschedule_data(now:)
358
+ # if there is any outstanding CRYPTO, retransmit it
359
+ crypto_scheduled = false
360
+ @spaces.each do |space|
361
+ packets = space.sent_packets.values.filter(&:is_crypto_packet)
362
+ unless packets.empty?
363
+ on_packets_lost(packets: packets, space: space, now: now)
364
+ crypto_scheduled = true
365
+ end
366
+ end
367
+
368
+ # TODO: logger debug if crypto_scheduled && @logger
369
+
370
+ # ensure an ACK-eliciting packet is sent
371
+ @send_probe&.call
372
+ end
373
+
374
+ # Check whether any packets should be declared lost.
375
+ def detect_loss(space:, now:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
376
+ loss_delay = K_TIME_THRESHOLD * (@rtt_initialized ? [@rtt_latest, @rtt_smoothed].max : @rtt_initial)
377
+ packet_threshold = space.largest_acked_packet - K_PACKET_THRESHOLD
378
+ time_threshold = now - loss_delay
379
+
380
+ lost_packets = []
381
+ space.loss_time = nil
382
+
383
+ space.sent_packets.each do |packet_number, packet|
384
+ break if packet_number > space.largest_acked_packet
385
+
386
+ if packet_number <= packet_threshold || packet.sent_time <= time_threshold
387
+ lost_packets << packet
388
+ else
389
+ packet_loss_time = packet.sent_time + loss_delay
390
+ space.loss_time = packet_loss_time.to_f if space.loss_time.nil? || space.loss_time > packet_loss_time
391
+ end
392
+ end
393
+ on_packets_lost(packets: lost_packets, space: space, now: now)
394
+ end
395
+
396
+ def get_loss_space
397
+ loss_space = nil
398
+ @spaces.each do |space|
399
+ loss_space = space if space.loss_time && (loss_space.nil? || space.loss_time < loss_space.loss_time)
400
+ end
401
+ return loss_space
402
+ end
403
+
404
+ def log_metrics_updated(log_rtt = false) # rubocop:disable Style/OptionalBooleanParameter
405
+ data = {
406
+ bytes_in_flight: @cc.bytes_in_flight,
407
+ cwnd: @cc.congestion_window,
408
+ }
409
+ data[:ssthresh] = @cc.ssthresh if @cc.ssthresh
410
+ if log_rtt # rubocop:disable Style/GuardClause
411
+ data[:latest_rtt] = @quic_logger&.encode_time(@rtt_latest)
412
+ data[:min_rtt] = @quic_logger&.encode_time(@rtt_min)
413
+ data[:smoothed_rtt] = @quic_logger&.encode_time(@rtt_smoothed)
414
+ data[:rtt_variance] = @quic_logger&.encode_time(@rtt_variance)
415
+ end
416
+
417
+ @quic_logger&.log_event(category: "recovery", event: "metrics_updated", data: data)
418
+ end
419
+
420
+ def on_packets_lost(packets:, space:, now:) # rubocop:disable Metrics/CyclomaticComplexity
421
+ lost_packets_cc = []
422
+ packets.each do |packet|
423
+ space.sent_packets.delete(packet.packet_number)
424
+
425
+ lost_packets_cc << packet if packet.in_flight
426
+ space.ack_eliciting_in_flight -= 1 if packet.is_ack_eliciting
427
+
428
+ if @quic_logger
429
+ @quic_logger.log_event(
430
+ category: "recovery",
431
+ event: "packet_lost",
432
+ data: {
433
+ type: @quic_logger.packet_type(packet.packet_type),
434
+ packet_number: packet.packet_number,
435
+ },
436
+ )
437
+ end
438
+
439
+ # trigger callbacks
440
+ packet.delivery_handlers&.each do |handler|
441
+ # TODO: hmm...
442
+ case handler[0]&.name
443
+ when :on_data_delivery
444
+ handler[0].call(delivery: Quic::PacketBuilder::QuicDeliveryState::LOST, start: handler[1][0], stop: handler[1][1])
445
+ when :on_ack_delivery
446
+ handler[0].call(delivery: Quic::PacketBuilder::QuicDeliveryState::LOST, space: handler[1][0], highest_acked: handler[1][1])
447
+ when :on_handshake_done_delivery
448
+ handler[0].call(delivery: Quic::PacketBuilder::QuicDeliveryState::LOST)
449
+ when :on_new_connection_id_delivery
450
+ handler[0].call(delivery: Quic::PacketBuilder::QuicDeliveryState::LOST, connection_id: handler[1][0])
451
+ when :on_connection_limit_delivery
452
+ handler[0].call(delivery: Quic::PacketBuilder::QuicDeliveryState::LOST, limit: handler[1][0])
453
+ else
454
+ raise NotImplementedError, handler[0]
455
+ end
456
+ end
457
+ end
458
+
459
+ # inform congestion controller
460
+ if lost_packets_cc # rubocop:disable Style/GuardClause
461
+ @cc.on_packets_lost(packets: lost_packets_cc, now: now)
462
+ @pacer.update_rate(congestion_window: @cc.congestion_window, smoothed_rtt: @rtt_smoothed)
463
+ log_metrics_updated if @quic_logger
464
+ end
465
+ end
466
+ end
467
+
468
+ # Roundtrip time monitor for HyStart.
469
+ class QuicRttMonitor
470
+ attr_reader :samples
471
+ attr_reader :ready
472
+ attr_reader :increases
473
+
474
+ def initialize
475
+ @increases = 0
476
+ @last_time = nil
477
+ @ready = false
478
+ @size = 5
479
+ @filtered_min = nil
480
+ @sample_idx = 0
481
+ @sample_max = nil
482
+ @sample_min = nil
483
+ @sample_time = 0.0
484
+ @samples = @size.times.map { 0.0 }
485
+ end
486
+
487
+ def add_rtt(rtt:)
488
+ @samples[@sample_idx] = rtt
489
+ @sample_idx += 1
490
+
491
+ if @sample_idx >= @size
492
+ @sample_idx = 0
493
+ @ready = true
494
+ end
495
+
496
+ if @ready # rubocop:disable Style/GuardClause
497
+ @sample_max = @samples[0]
498
+ @sample_min = @samples[0]
499
+ @samples[1..].each do |sample|
500
+ @sample_min = sample if sample < @sample_min
501
+ @sample_max = sample if sample > @sample_max
502
+ end
503
+ end
504
+ end
505
+
506
+ def is_rtt_increasing(rtt:, now:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
507
+ if now > @sample_time + K_GRANULARITY
508
+ add_rtt(rtt: rtt)
509
+ @sample_time = now
510
+
511
+ if @ready
512
+ @filtered_min = @sample_max if @filtered_min.nil? || @filtered_min > @sample_max
513
+
514
+ delta = @sample_min - @filtered_min
515
+ if delta * 4 >= @filtered_min
516
+ @increases += 1
517
+ return true if @increases >= @size # rubocop:disable Metrics/BlockNesting
518
+ elsif delta > 0
519
+ @increases = 0
520
+ end
521
+ end
522
+ end
523
+ return false
524
+ end
525
+ end
526
+ end
527
+ end
528
+ end