Skip to content

Commit 8c5e550

Browse files
authored
Use IO::Buffer for masking. (#16)
1 parent c4e6848 commit 8c5e550

File tree

3 files changed

+80
-13
lines changed

3 files changed

+80
-13
lines changed

.github/workflows/coverage.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- macos
2222

2323
ruby:
24+
- "3.1"
2425
- "3.2"
2526

2627
steps:

benchmark/mask_xor.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
def mask_xor_buffer(data, mask)
2+
buffer = data.dup
3+
mask_buffer = IO::Buffer.for(mask)
4+
5+
IO::Buffer.for(buffer) do |buffer|
6+
buffer.xor!(mask_buffer)
7+
end
8+
9+
return buffer
10+
end
11+
12+
def mask_xor_string(data, mask)
13+
result = String.new(encoding: Encoding::BINARY)
14+
15+
for i in 0...data.bytesize do
16+
result << (data.getbyte(i) ^ mask.getbyte(i % 4))
17+
end
18+
19+
return result
20+
end
21+
22+
require 'benchmark/ips'
23+
require 'securerandom'
24+
25+
MASK = SecureRandom.bytes(4)
26+
DATA = SecureRandom.bytes(1024 * 1024)
27+
28+
Benchmark.ips do |x|
29+
# Configure the number of seconds used during
30+
# the warmup phase (default 2) and calculation phase (default 5)
31+
x.config(:time => 5, :warmup => 2)
32+
33+
# These parameters can also be configured this way
34+
x.time = 5
35+
x.warmup = 2
36+
37+
x.report("IO::Buffer") {mask_xor_buffer(DATA, MASK)}
38+
x.report("String") {mask_xor_string(DATA, MASK)}
39+
40+
# Compare the iterations per second of the various reports!
41+
x.compare!
42+
end
43+
44+
# Warming up --------------------------------------
45+
# IO::Buffer 152.000 i/100ms
46+
# String 1.000 i/100ms
47+
# Calculating -------------------------------------
48+
# IO::Buffer 1.534k (± 0.1%) i/s - 7.752k in 5.051915s
49+
# String 11.929 (± 0.0%) i/s - 60.000 in 5.029975s
50+
51+
# Comparison:
52+
# IO::Buffer: 1534.5 i/s
53+
# String: 11.9 i/s - 128.63x slower

lib/protocol/websocket/frame.rb

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ def continued?
8686
attr_accessor :length
8787
attr_accessor :payload
8888

89+
if IO.const_defined?(:Buffer) && IO::Buffer.respond_to?(:for) && IO::Buffer.method_defined?(:xor!)
90+
private def mask_xor(data, mask)
91+
buffer = data.dup
92+
mask_buffer = IO::Buffer.for(mask)
93+
94+
IO::Buffer.for(buffer) do |buffer|
95+
buffer.xor!(mask_buffer)
96+
end
97+
98+
return buffer
99+
end
100+
else
101+
warn "IO::Buffer not available, falling back to slow implementation of mask_xor!"
102+
private def mask_xor(data, mask)
103+
result = String.new(encoding: Encoding::BINARY)
104+
105+
for i in 0...data.bytesize do
106+
result << (data.getbyte(i) ^ mask.getbyte(i % 4))
107+
end
108+
109+
return result
110+
end
111+
end
112+
89113
def pack(data = "")
90114
length = data.bytesize
91115

@@ -94,12 +118,7 @@ def pack(data = "")
94118
end
95119

96120
if @mask
97-
@payload = String.new(encoding: Encoding::BINARY)
98-
99-
for i in 0...data.bytesize do
100-
@payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
101-
end
102-
121+
@payload = mask_xor(data, mask)
103122
@length = length
104123
else
105124
@payload = data
@@ -111,13 +130,7 @@ def pack(data = "")
111130

112131
def unpack
113132
if @mask and !@payload.empty?
114-
data = String.new(encoding: Encoding::BINARY)
115-
116-
for i in 0...@payload.bytesize do
117-
data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
118-
end
119-
120-
return data
133+
return mask_xor(@payload, @mask)
121134
else
122135
return @payload
123136
end

0 commit comments

Comments
 (0)