raioquic 0.1.0

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.
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