Skip to content

Commit 14d243e

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

7 files changed

Lines changed: 121 additions & 12 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ proc onClose*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl
1414
streamCtx.isEof = true
1515
streamCtx.closed.fire()
1616
streamCtx.abortPendingWrites("stream closed")
17+
GC_unref(streamCtx)
1718

1819
type StreamReadContext = object
1920
stream: ptr lsquic_stream_t
@@ -40,12 +41,11 @@ proc onRead*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.
4041
if nread < 0:
4142
error "could not read from stream", nread, streamId = lsquic_stream_id(stream)
4243
streamCtx.abort()
43-
44+
4445
if lsquic_stream_wantread(stream, 0) == -1:
4546
error "could not set stream wantread", streamId = lsquic_stream_id(stream)
4647
streamCtx.abort()
4748

48-
4949
proc onWrite*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.} =
5050
trace "onWrite"
5151

lsquic/stream.nim

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ type Stream* = ref object
1818
toWrite*: seq[WriteTask]
1919

2020
proc new*(T: typedesc[Stream], quicStream: ptr lsquic_stream_t = nil): T =
21-
Stream(
21+
let s = Stream(
2222
quicStream: quicStream,
2323
incoming: newAsyncQueue[seq[byte]](),
2424
closed: newAsyncEvent(),
2525
)
26+
GC_ref(s) # Keep it pinned until stream_if.on_close is executed
27+
s
2628

2729
proc abortPendingWrites*(stream: Stream, reason: string = "") =
2830
for pendingWrite in stream.toWrite.mitems:
@@ -103,7 +105,8 @@ proc write*(
103105
discard lsquic_stream_wantwrite(stream.quicStream, 1)
104106
let raceFut = await race(closedFut, doneFut)
105107
if raceFut == closedFut:
106-
doneFut.fail(newException(StreamError, "stream closed"))
108+
if not doneFut.finished:
109+
doneFut.fail(newException(StreamError, "stream closed"))
107110
stream.closeWrite = true
108111

109112
await doneFut

tests/test_connection.nim

Lines changed: 103 additions & 5 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,104 @@ 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 serverHandler = proc() {.async.} =
59+
let stream = await incomingConn.incomingStream()
60+
61+
# Step 1: Read download size (8 bytes)
62+
let clientDownloadSize = await stream.read()
63+
64+
# Step 2: Read upload data until EOF
65+
var totalBytesRead = 0
66+
while true:
67+
let chunk = await stream.read()
68+
if chunk.len == 0:
69+
break
70+
totalBytesRead += chunk.len
71+
72+
# Step 3: Send download data back
73+
var remainingToSend = uint64.fromBytesBE(clientDownloadSize)
74+
while remainingToSend > 0:
75+
let toSend = min(remainingToSend, chunkSize)
76+
await stream.write(newSeq[byte](toSend))
77+
remainingToSend -= toSend
78+
79+
stream.close()
80+
81+
# Start server handler
82+
asyncSpawn serverHandler()
83+
84+
let startTime = Moment.now()
85+
86+
# Step 1: Send download size, activate stream first
87+
let clientStream = await outgoingConn.openStream()
88+
await clientStream.write(toSeq(downloadSize.uint64.toBytesBE()))
89+
90+
# Step 2: Send upload data in chunks
91+
var remainingToSend = uploadSize
92+
while remainingToSend > 0:
93+
let toSend = min(remainingToSend, chunkSize)
94+
await clientStream.write(newSeq[byte](toSend))
95+
remainingToSend -= toSend
96+
97+
# Step 3: Close write side
98+
clientStream.close()
99+
100+
# Step 4: Start reading download data
101+
var totalDownloaded = 0
102+
while totalDownloaded < downloadSize:
103+
let chunk = await clientStream.read()
104+
totalDownloaded += chunk.len
105+
106+
let duration = Moment.now() - startTime
107+
108+
await listener.stop()
109+
await client.stop()
110+
111+
return duration
112+
113+
suite "perf protocol simulation":
114+
asyncTest "test":
115+
var total: Duration
116+
for i in 0 ..< runs:
117+
let duration = await runPerf()
118+
total += duration
119+
echo "\trun #" & $(i + 1) & " duration: " & $duration
120+
121+
echo "\tavrg duration: " & $(total div runs)
27122

123+
suite "tests":
28124
asyncTest "test":
29125
let logger = struct_lsquic_logger_if(log_buf: logging)
30126
discard lsquic_set_log_level("debug")
@@ -76,7 +172,6 @@ suite "connections":
76172

77173
let incomingBehaviour = proc() {.async.} =
78174
try:
79-
echo "HERE"
80175
let stream = await incomingConn.incomingStream()
81176
echo "Received stream in server"
82177

@@ -107,12 +202,15 @@ suite "connections":
107202
outgoingConn.close()
108203
incomingConn.close()
109204

205+
# Cannot create a stream once closed
206+
expect ConnectionError:
207+
discard await outgoingConn.openStream()
208+
110209
await sleepAsync(1.seconds)
111210

112211
await client.stop()
113212
await listener.stop()
114213

115-
# TODO: perf example
116214
# TODO: destructors: (nice to have:)
117215
# - lsquic_global_cleanup() to free global resources.
118216
# - lsquic_engine_destroy(engine)

0 commit comments

Comments
 (0)