1import { Effect, Stream } from "effect"
2import * as Multipart from "effect/http/Multipart"
3import * as MultipartParser from "effect/http/MultipartParser"
4import { Bench } from "tinybench"
5
6const bench = new Bench()
7
8const boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
9const headers = {
10 "content-type": `multipart/form-data; boundary=${boundary}`
11}
12const encoder = new TextEncoder()
13
14const filePayload = (fileSize: number, chunkSize: number): Array<Uint8Array> => {
15 const head = encoder.encode(
16 `--${boundary}\r\ncontent-disposition: form-data; name="file"; filename="file.bin"\r\ncontent-type: application/octet-stream\r\n\r\n`
17 )
18 const body = new Uint8Array(fileSize)
19 for (let i = 0; i < fileSize; i++) {
20 body[i] = i % 251
21 }
22 const tail = encoder.encode(`\r\n--${boundary}--\r\n`)
23 const payload = new Uint8Array(head.length + body.length + tail.length)
24 payload.set(head, 0)
25 payload.set(body, head.length)
26 payload.set(tail, head.length + body.length)
27 const chunks: Array<Uint8Array> = []
28 for (let i = 0; i < payload.length; i += chunkSize) {
29 chunks.push(payload.subarray(i, i + chunkSize))
30 }
31 return chunks
32}
33
34const fieldsPayload = (fieldCount: number): Array<Uint8Array> => {
35 let out = ""
36 for (let i = 0; i < fieldCount; i++) {
37 out += `--${boundary}\r\ncontent-disposition: form-data; name="field${i}"\r\n\r\nvalue of field number ${i}\r\n`
38 }
39 out += `--${boundary}--\r\n`
40 return [encoder.encode(out)]
41}
42
43const file16MiB64KiB = filePayload(16 * 1024 * 1024, 64 * 1024)
44const file16MiB4KiB = filePayload(16 * 1024 * 1024, 4 * 1024)
45const file1MiB64KiB = filePayload(1024 * 1024, 64 * 1024)
46const fields100 = fieldsPayload(100)
47
48const runParser = (chunks: Array<Uint8Array>) => {
49 const parser = MultipartParser.make({
50 headers,
51 onField() {},
52 onFile() {
53 return () => {}
54 },
55 onError(error) {
56 throw new Error(`unexpected error: ${error._tag}`)
57 },
58 onDone() {}
59 })
60 for (let i = 0; i < chunks.length; i++) {
61 parser.write(chunks[i])
62 }
63 parser.end()
64}
65
66const runChannelDrain = (chunks: Array<Uint8Array>) =>
67 Effect.runPromise(
68 Stream.fromArray(chunks).pipe(
69 Stream.pipeThroughChannel(Multipart.makeChannel(headers)),
70 Stream.mapEffect((part) => part._tag === "File" ? Stream.runDrain(part.content) : Effect.void),
71 Stream.runDrain
72 )
73 )
74
75// Stream.fromArray emits the chunks in one batch, so these are single-pull controls.
76const runChannelCollect = (chunks: Array<Uint8Array>) =>
77 Effect.runPromise(
78 Stream.fromArray(chunks).pipe(
79 Stream.pipeThroughChannel(Multipart.makeChannel(headers)),
80 Stream.mapEffect((part) => part._tag === "File" ? part.contentEffect : Effect.void),
81 Stream.runDrain
82 )
83 )
84
85// rechunk(1) forces one upstream pull per chunk, simulating network reads and
86// guarding against quadratic accumulation across pulls.
87const runChannelCollectStreaming = (chunks: Array<Uint8Array>) =>
88 Effect.runPromise(
89 Stream.fromArray(chunks).pipe(
90 Stream.rechunk(1),
91 Stream.pipeThroughChannel(Multipart.makeChannel(headers)),
92 Stream.mapEffect((part) => part._tag === "File" ? part.contentEffect : Effect.void),
93 Stream.runDrain
94 )
95 )
96
97bench
98 .add("parser: 16MiB file / 64KiB chunks", function() {
99 runParser(file16MiB64KiB)
100 })
101 .add("parser: 16MiB file / 4KiB chunks", function() {
102 runParser(file16MiB4KiB)
103 })
104 .add("parser: 100 small fields", function() {
105 runParser(fields100)
106 })
107 .add("channel drain: 16MiB file / 64KiB chunks", async function() {
108 await runChannelDrain(file16MiB64KiB)
109 })
110 .add("channel collect: 16MiB file / 64KiB chunks", async function() {
111 await runChannelCollect(file16MiB64KiB)
112 })
113 .add("channel collect: 1MiB file / 64KiB chunks", async function() {
114 await runChannelCollect(file1MiB64KiB)
115 })
116 .add("channel collect streaming: 16MiB file / 64KiB chunks", async function() {
117 await runChannelCollectStreaming(file16MiB64KiB)
118 })
119
120await bench.run()
121
122console.table(bench.table())