Skip to content

Commit

Permalink
Reduce number of window update frames sent. (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix authored Sep 28, 2024
1 parent 5f5dae1 commit 2d61fa4
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 11 deletions.
2 changes: 1 addition & 1 deletion lib/protocol/http2/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(framer, local_stream_id)
@decoder = HPACK::Context.new
@encoder = HPACK::Context.new

@local_window = LocalWindow.new()
@local_window = LocalWindow.new
@remote_window = Window.new
end

Expand Down
4 changes: 0 additions & 4 deletions lib/protocol/http2/flow_controlled.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,11 @@ def send_window_update(window_increment)
def receive_window_update(frame)
amount = frame.unpack

# Async.logger.info(self) {"expanding remote_window=#{@remote_window} by #{amount}"}

if amount != 0
@remote_window.expand(amount)
else
raise ProtocolError, "Invalid window size increment: #{amount}!"
end

# puts "expanded remote_window=#{@remote_window} by #{amount}"
end

# The window has been expanded by the given amount.
Expand Down
19 changes: 15 additions & 4 deletions lib/protocol/http2/window.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
module Protocol
module HTTP2
class Window
# When an HTTP/2 connection is first established, new streams are created with an initial flow-control window size of 65,535 octets. The connection flow-control window is also 65,535 octets.
DEFAULT_CAPACITY = 0xFFFF

# @param capacity [Integer] The initial window size, typically from the settings.
def initialize(capacity = 0xFFFF)
def initialize(capacity = DEFAULT_CAPACITY)
# This is the main field required:
@available = capacity

Expand Down Expand Up @@ -75,30 +78,38 @@ def inspect

# This is a window which efficiently maintains a desired capacity.
class LocalWindow < Window
def initialize(capacity = 0xFFFF, desired: nil)
def initialize(capacity = DEFAULT_CAPACITY, desired: nil)
super(capacity)

# The desired capacity of the window, may be bigger than the initial capacity.
# If that is the case, we will likely send a window update to the remote end to increase the capacity.
@desired = desired
end

# The desired capacity of the window.
attr_accessor :desired

def wanted
if @desired
# We must send an update which allows at least @desired bytes to be sent.
(@desired - @capacity) + @used
else
@used
super
end
end

def limited?
if @desired
@available < @desired
# Do not send window updates until we are less than half the desired capacity:
@available < (@desired / 2)
else
super
end
end

def inspect
"\#<#{self.class} used=#{@used} available=#{@available} capacity=#{@capacity} desired=#{@desired}>"
end
end
end
end
26 changes: 26 additions & 0 deletions test/protocol/http2/window.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Copyright, 2019-2024, by Samuel Williams.

require "protocol/http2/connection_context"
require "json"

describe Protocol::HTTP2::Window do
let(:window) {subject.new}
Expand Down Expand Up @@ -54,5 +55,30 @@
window.consume(window.available)
expect(window.wanted).to be == 200
end

it "is not limited if the half the desired capacity is available" do
expect(window).not.to be(:limited?)

# Consume the entire window:
window.consume(window.available)

expect(window).to be(:limited?)

# Expand the window by at least half the desired capacity:
window.expand(window.desired / 2)

expect(window).not.to be(:limited?)
end
end

with "#limited?" do
it "becomes limited after half the capacity is consumed" do
expect(window).not.to be(:limited?)

# Consume a little more than half:
window.consume(window.capacity / 2 + 2)

expect(window).to be(:limited?)
end
end
end
23 changes: 21 additions & 2 deletions test/protocol/http2/window_update_frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,12 @@ def before
expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame
end

expect(client).to receive(:receive_window_update)
expect(client).to receive(:receive_window_update).twice

stream.send_data("*" * client.available_size)
expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame

frame = client.read_frame
expect(frame).to be_a(Protocol::HTTP2::WindowUpdateFrame)
expect(frame).to be(:connection?) # stream_id = 0

Expand Down Expand Up @@ -237,5 +241,20 @@ def before
end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot update window of idle stream/)
end
end
end

with "desired capacity" do
it "should send window updates only as needed" do
expect(client.local_window.desired).to be == 0xFFFF

server_stream = server[stream.id]

# Send a data frame that will consume less than half of the desired capacity:
server_stream.send_data("*" * 0xFF)

expect(client.read_frame).to be_a Protocol::HTTP2::DataFrame
expect(client.local_window.used).to be == 0xFF
expect(client.local_window).not.to be(:limited?)
end
end
end
end

0 comments on commit 2d61fa4

Please sign in to comment.