Skip to content

Commit 4dfb83d

Browse files
committed
feat: perf tests and fix: pin connections and streams
1 parent 875334a commit 4dfb83d

7 files changed

Lines changed: 163 additions & 19 deletions

File tree

lsquic.nimble

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ task format, "Format nim code using nph":
2121

2222
task test, "Run tests":
2323
when defined(windows):
24-
exec "nim c --mm:refc -d:nimDebugDlOpen -r --threads:on tests/test_connection.nim"
24+
exec "nim c --mm:refc -d:nimDebugDlOpen --threads:on tests/test_connection.nim"
2525
else:
26-
exec "nim c --mm:refc -r --threads:on tests/test_connection.nim"
26+
exec "nim c --mm:refc --threads:on tests/test_connection.nim"
27+
exec "./tests/test_connection --output-level=VERBOSE"

lsquic/connection.nim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ method incomingStream*(
9191
method incomingStream*(
9292
connection: IncomingConnection
9393
): Future[Stream] {.async: (raises: [CancelledError, ConnectionError]).} =
94+
if connection.isClosed:
95+
raise newException(ConnectionError, "connection is closed")
96+
9497
let closedFut = connection.closed.wait()
9598
let incomingFut = connection.quicConn.incomingStream()
9699
let raceFut = await race(closedFut, incomingFut)
@@ -108,6 +111,8 @@ method openStream*(
108111
method openStream*(
109112
connection: OutgoingConnection
110113
): Future[Stream] {.async: (raises: [CancelledError, ConnectionError]).} =
114+
if connection.isClosed:
115+
raise newException(ConnectionError, "connection is closed")
111116
let s = Stream.new()
112117
let created = connection.quicConn.addPendingStream(s)
113118
connection.quicContext.makeStream(connection.quicConn)

lsquic/context/client.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ proc onConnClosed(conn: ptr lsquic_conn_t) {.cdecl.} =
5252
)
5353
quicClientConn.cancelPending()
5454
quicClientConn.onClose()
55+
GC_unref(quicClientConn)
5556
lsquic_conn_set_ctx(conn, nil)
5657

5758
proc onNewStream(
@@ -90,7 +91,7 @@ method dial*(
9091

9192
let quicClientConn =
9293
QuicClientConn(connectedFut: connectedFut, local: local, remote: remote)
93-
94+
GC_ref(quicClientConn) # Keep it pinned until on_conn_closed is called
9495
let conn = lsquic_engine_connect(
9596
ctx.engine,
9697
N_LSQVER,

lsquic/context/server.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ proc onNewConn(
1919
remote: remote.toTransportAddress(),
2020
lsquicConn: conn,
2121
)
22+
GC_ref(quicServerConn) # Keep it pinned until on_conn_closed is called
2223
let serverCtx = cast[ServerContext](stream_if_ctx)
2324
serverCtx.incoming.putNoWait(quicServerConn)
2425
cast[ptr lsquic_conn_ctx_t](quicServerConn)

lsquic/context/stream.nim

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ proc onClose*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl
99
debug "stream_ctx is nil onClose"
1010
return
1111

12+
echo "CLOSING STREAM!!!!!!", lsquic_stream_id(stream)
1213
let streamCtx = cast[Stream](ctx)
1314
if not streamCtx.closeWrite:
1415
streamCtx.isEof = true
16+
echo "FIRING CLOSE 3"
1517
streamCtx.closed.fire()
16-
streamCtx.abortPendingWrites("stream closed")
18+
streamCtx.abortPendingWrites("stream closed 4")
19+
GC_unref(streamCtx)
1720

1821
type StreamReadContext = object
1922
stream: ptr lsquic_stream_t
@@ -40,12 +43,11 @@ proc onRead*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.
4043
if nread < 0:
4144
error "could not read from stream", nread, streamId = lsquic_stream_id(stream)
4245
streamCtx.abort()
43-
46+
4447
if lsquic_stream_wantread(stream, 0) == -1:
4548
error "could not set stream wantread", streamId = lsquic_stream_id(stream)
4649
streamCtx.abort()
4750

48-
4951
proc onWrite*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.} =
5052
trace "onWrite"
5153

@@ -55,6 +57,8 @@ proc onWrite*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl
5557

5658
let streamCtx = cast[Stream](ctx)
5759
if streamCtx.toWrite.len == 0:
60+
if not streamCtx.shouldClose.isNil and not streamCtx.shouldClose.finished:
61+
streamCtx.shouldClose.complete()
5862
if lsquic_stream_wantwrite(stream, 0) == -1:
5963
error "could not set stream wantwrite", streamId = lsquic_stream_id(stream)
6064
streamCtx.abort()
@@ -88,6 +92,9 @@ proc onWrite*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl
8892
return
8993

9094
if streamCtx.toWrite.len == 0:
95+
echo streamCtx.shouldClose.isNil
96+
if not streamCtx.shouldClose.isNil and not streamCtx.shouldClose.finished:
97+
streamCtx.shouldClose.complete()
9198
if lsquic_stream_wantwrite(stream, 0) == -1:
9299
error "could not set stream wantwrite", streamId = lsquic_stream_id(stream)
93100
streamCtx.abort()

lsquic/stream.nim

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ type Stream* = ref object
1616
closed*: AsyncEvent # This is called when on_close callback is executed
1717
isEof*: bool # Received a FIN from remote
1818
toWrite*: seq[WriteTask]
19+
shouldClose*: Future[void].Raising([CancelledError])
1920

2021
proc new*(T: typedesc[Stream], quicStream: ptr lsquic_stream_t = nil): T =
21-
Stream(
22+
let s = Stream(
2223
quicStream: quicStream,
2324
incoming: newAsyncQueue[seq[byte]](),
2425
closed: newAsyncEvent(),
2526
)
27+
GC_ref(s) # Keep it pinned until stream_if.on_close is executed
28+
s
2629

2730
proc abortPendingWrites*(stream: Stream, reason: string = "") =
2831
for pendingWrite in stream.toWrite.mitems:
@@ -33,27 +36,38 @@ proc abortPendingWrites*(stream: Stream, reason: string = "") =
3336
proc abort*(stream: Stream) =
3437
if stream.closeWrite and stream.isEof:
3538
if not stream.closed.isSet():
39+
echo "FIRING CLOSE 1"
3640
stream.closed.fire()
3741
stream.abortPendingWrites("stream aborted")
3842
return
3943

44+
echo "CLOSING ON ABORT"
4045
let ret = lsquic_stream_close(stream.quicStream)
4146
if ret != 0:
4247
error "could not abort stream", streamId = lsquic_stream_id(stream.quicStream)
4348

4449
stream.closeWrite = true
4550
stream.isEof = true
4651
stream.abortPendingWrites("stream aborted")
52+
echo "FIRING CLOSE 2"
4753
stream.closed.fire()
4854

49-
proc close*(stream: Stream) =
55+
proc close*(stream: Stream) {.async: (raises: [StreamError, CancelledError]).} =
5056
if stream.closeWrite:
5157
return
58+
echo "CALLED CLOSE!!!!!!!!!!!!!!!!!!"
59+
if stream.toWrite.len != 0:
60+
stream.shouldClose = Future[void].Raising([CancelledError]).init()
61+
62+
echo "AWAITING FOR CLOSE"
63+
await stream.shouldClose
5264

5365
# Closing only the write side
66+
echo "SHUTDOWN WRITE"
5467
let ret = lsquic_stream_shutdown(stream.quicStream, 1)
5568
if ret == 0:
5669
if stream.isEof:
70+
echo "CLOSE DUE TO EOF"
5771
if lsquic_stream_close(stream.quicStream) != 0:
5872
stream.abort()
5973
raise newException(StreamError, "could not close the stream")
@@ -78,12 +92,13 @@ proc read*(
7892
await incomingFut.cancelAndWait()
7993
stream.isEof = true
8094
stream.closeWrite = true
81-
raise newException(StreamError, "stream closed")
95+
raise newException(StreamError, "stream closed 1")
8296

8397
let incoming = await incomingFut
8498
if incoming.len == 0:
8599
if stream.closeWrite:
86100
# We were already closed for write. Close the stream completely
101+
echo "CLOSING AFTER READ IS EOF, and WRITE WAS CALLED BEFORE"
87102
if lsquic_stream_close(stream.quicStream) != 0:
88103
stream.abort()
89104
raise newException(StreamError, "could not close the stream")
@@ -95,15 +110,16 @@ proc write*(
95110
stream: Stream, data: seq[byte]
96111
) {.async: (raises: [CancelledError, StreamError]).} =
97112
if stream.closeWrite:
98-
raise newException(StreamError, "stream is closed")
113+
raise newException(StreamError, "stream closed 3")
99114

100115
let closedFut = stream.closed.wait()
101116
let doneFut = Future[void].Raising([CancelledError, StreamError]).init()
102117
stream.toWrite.add(WriteTask(data: data, doneFut: doneFut))
103118
discard lsquic_stream_wantwrite(stream.quicStream, 1)
104119
let raceFut = await race(closedFut, doneFut)
105120
if raceFut == closedFut:
106-
doneFut.fail(newException(StreamError, "stream closed"))
121+
if not doneFut.finished:
122+
doneFut.fail(newException(StreamError, "stream closed 2"))
107123
stream.closeWrite = true
108124

109125
await doneFut

tests/test_connection.nim

Lines changed: 121 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import ./helpers/certificate
1111
import lsquic/certificateverifier
1212
import lsquic/stream
1313
import lsquic/lsquic_ffi
14+
import stew/endians2
15+
import sequtils
1416

1517
proc logging(ctx: pointer, buf: cstring, len: csize_t): cint {.cdecl.} =
1618
echo $buf
@@ -21,10 +23,119 @@ proc certificateCb(
2123
): bool {.gcsafe.} =
2224
return derCertificates.len > 0
2325

24-
suite "connections":
25-
setup:
26-
let address = initTAddress("127.0.0.1:12345")
26+
let address = initTAddress("127.0.0.1:12345")
27+
28+
const
29+
runs = 1
30+
uploadSize = 100000 # 100KB
31+
downloadSize = 100000000 # 100MB
32+
chunkSize = 65536 # 64KB chunks like perf
33+
34+
proc runPerf(): Future[Duration] {.async.} =
35+
let customCertVerif: CertificateVerifier =
36+
CustomCertificateVerifier.init(certificateCb)
37+
let clientTLSConfig = TLSConfig.new(
38+
testCertificate(),
39+
testPrivateKey(),
40+
@["test"].toHashSet(),
41+
Opt.some(customCertVerif),
42+
)
43+
let serverTLSConfig = TLSConfig.new(
44+
testCertificate(),
45+
testPrivateKey(),
46+
@["test"].toHashSet(),
47+
Opt.some(customCertVerif),
48+
)
49+
let client = QuicClient.new(clientTLSConfig)
50+
let server = QuicServer.new(serverTLSConfig)
51+
let listener = server.listen(address)
52+
let accepting = listener.accept()
53+
let dialing = client.dial(address)
54+
55+
let outgoingConn = await dialing
56+
let incomingConn = await accepting
57+
58+
let serverDone = newFuture[void]()
59+
let serverHandler = proc() {.async.} =
60+
let stream = await incomingConn.incomingStream()
61+
62+
# Step 1: Read download size (8 bytes)
63+
let clientDownloadSize = await stream.read()
64+
65+
# Step 2: Read upload data until EOF
66+
var totalBytesRead = 0
67+
while true:
68+
let chunk = await stream.read()
69+
if chunk.len == 0:
70+
break
71+
totalBytesRead += chunk.len
72+
73+
# Step 3: Send download data back
74+
var remainingToSend = uint64.fromBytesBE(clientDownloadSize)
75+
while remainingToSend > 0:
76+
let toSend = min(remainingToSend, chunkSize)
77+
try:
78+
await stream.write(newSeq[byte](toSend))
79+
except StreamError:
80+
echo "STREAM ERROR 1"
81+
quit(1)
82+
remainingToSend -= toSend
83+
84+
await stream.close()
85+
serverDone.complete()
86+
87+
# Start server handler
88+
asyncSpawn serverHandler()
89+
90+
let startTime = Moment.now()
91+
92+
# Step 1: Send download size, activate stream first
93+
let clientStream = await outgoingConn.openStream()
94+
try:
95+
await clientStream.write(toSeq(downloadSize.uint64.toBytesBE()))
96+
except StreamError:
97+
echo "STREAM ERROR 2"
98+
quit(1)
99+
# Step 2: Send upload data in chunks
100+
var remainingToSend = uploadSize
101+
while remainingToSend > 0:
102+
let toSend = min(remainingToSend, chunkSize)
103+
try:
104+
await clientStream.write(newSeq[byte](toSend))
105+
except StreamError:
106+
echo "STRAM ERROR 4"
107+
quit(1)
108+
remainingToSend -= toSend
109+
110+
# Step 3: Close write side
111+
await clientStream.close()
112+
113+
# Step 4: Start reading download data
114+
var totalDownloaded = 0
115+
while totalDownloaded < downloadSize:
116+
let chunk = await clientStream.read()
117+
totalDownloaded += chunk.len
118+
119+
let duration = Moment.now() - startTime
120+
121+
await serverDone
122+
123+
await listener.stop()
124+
await client.stop()
125+
126+
return duration
127+
128+
suite "perf protocol simulation":
129+
asyncTest "test":
130+
var total: Duration
131+
for i in 0 ..< runs:
132+
let duration = await runPerf()
133+
total += duration
134+
echo "\trun #" & $(i + 1) & " duration: " & $duration
27135

136+
echo "\tavrg duration: " & $(total div runs)
137+
138+
suite "tests":
28139
asyncTest "test":
29140
let logger = struct_lsquic_logger_if(log_buf: logging)
30141
discard lsquic_set_log_level("debug")
@@ -69,14 +180,13 @@ suite "connections":
69180
echo "Closing client stream"
70181

71182
echo "Client closed"
72-
stream.close()
183+
await stream.close()
73184

74185
#echo "Client aborted"
75186
# stream.abort() # Not interested in RW anything else
76187

77188
let incomingBehaviour = proc() {.async.} =
78189
try:
79-
echo "HERE"
80190
let stream = await incomingConn.incomingStream()
81191
echo "Received stream in server"
82192

@@ -91,7 +201,7 @@ suite "connections":
91201
stream.isEof
92202

93203
echo "Server closed"
94-
stream.close()
204+
await stream.close()
95205

96206
#echo "Server aborted"
97207
#stream.abort() # Not interested in RW anything else
@@ -102,17 +212,20 @@ suite "connections":
102212

103213
discard allFutures(outgoingBehaviour(), incomingBehaviour())
104214

105-
await sleepAsync(1.seconds)
215+
await sleepAsync(10.seconds)
106216

107217
outgoingConn.close()
108218
incomingConn.close()
109219

220+
# Cannot create a stream once closed
221+
expect ConnectionError:
222+
discard await outgoingConn.openStream()
223+
110224
await sleepAsync(1.seconds)
111225

112226
await client.stop()
113227
await listener.stop()
114228

115-
# TODO: perf example
116229
# TODO: destructors: (nice to have:)
117230
# - lsquic_global_cleanup() to free global resources.
118231
# - lsquic_engine_destroy(engine)

0 commit comments

Comments
 (0)