I'm investigating the performance of uploading files to the Quart
web server
I'm looking at how Quart
parses file data when the request type is "multipart/form-data"
, and I see that synchronous temporary files are used to write files to the stream
def default_stream_factory( total_content_length: int | None, content_type: str | None, filename: str | None, content_length: int | None = None,) -> t.IO[bytes]: max_size = 1024 * 500 if SpooledTemporaryFile is not None: return t.cast(t.IO[bytes], SpooledTemporaryFile(max_size=max_size, mode="rb+")) elif total_content_length is None or total_content_length > max_size: return t.cast(t.IO[bytes], TemporaryFile("rb+")) return BytesIO()
I have a question: in the form parser (quart.formparser.MultiPartParser
) is it intentionally made synchronous writing to the container
(file data stream), instead of using asynchronous writing, for example with aiofiles
?
I want to make sure that the really synchronous recording of multipart chunks is done intentionally
For what reasons is this done, would this work better under high load and multiple coroutines?
I tried implementing the aiofiles
option and got a slight performance reduction (5-10%) with wrk
tool and script:
function read_file(path) local file, errorMessage = io.open(path, "rb") if not file then error("Could not read the file:" .. errorMessage .. "\n") end local content = file:read "*all" file:close() return content end local Boundary = "----WebKitFormBoundaryePkpFF7tjBAqx29L" local BodyBoundary = "--" .. Boundary local LastBoundary = "--" .. Boundary .. "--" local CRLF = "\r\n" local FileBody = read_file("./text_file.txt") local Filename = "file" local ContentDisposition = 'Content-Disposition: form-data; name="file"; filename="' .. Filename .. '"' local ContentType = 'Content-Type: text/plain' wrk.method = "POST" wrk.headers["Content-Type"] = "multipart/form-data; boundary=" .. Boundary wrk.headers["Transfer-Encoding"] = "chunked" wrk.body = BodyBoundary .. CRLF .. ContentDisposition .. CRLF .. ContentType .. CRLF .. CRLF .. FileBody .. CRLF .. LastBoundary
(note, I specified wrk.headers["Transfer-Encoding"] = "chunked"
, which required me to build wrk
with this PR - https://github.com/wg/wrk/pull/504)
with command:
wrk -t3 -c3 -d100s -s ufile.lua http://localhost:8000/
at a file size of 100 MB the result is:
without aiofiles
(native quart.formparser.MultiPartParser
):
Running 2m test @ http://localhost:8000/ 3 threads and 3 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.62s 99.26ms 1.93s 70.65% Req/Sec 0.00 0.00 0.00 100.00% 184 requests in 1.67m, 32.16KB readRequests/sec: 1.84Transfer/sec: 329.10B
with aiofiles
:
Running 2m test @ http://localhost:8000/ 3 threads and 3 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.67s 116.26ms 2.00s 70.30% Req/Sec 0.00 0.00 0.00 100.00% 176 requests in 1.67m, 30.77KB read Socket errors: connect 0, read 0, write 0, timeout 11Requests/sec: 1.76Transfer/sec: 314.79B
at a file size of 10 MB the result is:without aiofiles
(native quart.formparser.MultiPartParser
):
Running 2m test @ http://localhost:8000/ 3 threads and 3 connections Thread Stats Avg Stdev Max +/- Stdev Latency 173.16ms 38.79ms 503.89ms 81.60% Req/Sec 6.42 2.55 10.00 62.39% 1739 requests in 1.67m, 303.99KB readRequests/sec: 17.38Transfer/sec: 3.04KB
with aiofiles
:
Running 2m test @ http://localhost:8000/ 3 threads and 3 connections Thread Stats Avg Stdev Max +/- Stdev Latency 195.36ms 42.38ms 519.98ms 80.43% Req/Sec 5.49 2.16 10.00 71.86% 1539 requests in 1.67m, 269.16KB readRequests/sec: 15.38Transfer/sec: 2.69KB
route is implemented like this
@app.route("/", methods=["POST"])async def upload_file() -> str: await request.files return "uploaded"
the modified part of the parser looks like this
... if isinstance(event, Field): current_part = event container = [] _write = run_sync(container.append) elif isinstance(event, File): current_part = event container: AsyncBufferedReader = await self.start_file_streaming(event, content_length) _write = container.write elif isinstance(event, Data): await _write(event.data)...
If my tests are correct, I'd like to understand why this happens and if there are any useful ways to use aiofiles
when parsing form data?