Repository: encode/starlette

Issue:
Maybe the thread pool should no longer be used for reading and writing files?
I read performance check in https://github.com/mosquito/aiofile/issues/18. Compared to synchronization overhead, it seems that the overhead of the thread pool is greater, maybe we need some performance tests.

And I noticed that aiofiles has not responded to any questions or requests from the community for ten months, which is dangerous. If you insist on reading and writing files asynchronously, maybe starlette should be switched to aiofile?

If there is any need to modify this, I will be happy to do it.


Discussion:
https://github.com/Tinche/aiofiles/issues/47 
Someone has done a performance test here, maybe small file reading and writing should really give up using the thread pool?

Resolving patch:
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -433,17 +433,33 @@ def __init__(
             file = tempfile.SpooledTemporaryFile(max_size=self.spool_max_size)
         self.file = file
 
+    @property
+    def _in_memory(self) -> bool:
+        rolled_to_disk = getattr(self.file, "_rolled", True)
+        return not rolled_to_disk
+
     async def write(self, data: typing.Union[bytes, str]) -> None:
-        await run_in_threadpool(self.file.write, data)
+        if self._in_memory:
+            self.file.write(data)  # type: ignore
+        else:
+            await run_in_threadpool(self.file.write, data)
 
-    async def read(self, size: int = None) -> typing.Union[bytes, str]:
+    async def read(self, size: int = -1) -> typing.Union[bytes, str]:
+        if self._in_memory:
+            return self.file.read(size)
         return await run_in_threadpool(self.file.read, size)
 
     async def seek(self, offset: int) -> None:
-        await run_in_threadpool(self.file.seek, offset)
+        if self._in_memory:
+            self.file.seek(offset)
+        else:
+            await run_in_threadpool(self.file.seek, offset)
 
     async def close(self) -> None:
-        await run_in_threadpool(self.file.close)
+        if self._in_memory:
+            self.file.close()
+        else:
+            await run_in_threadpool(self.file.close)
 
 
 class FormData(ImmutableMultiDict):


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -1,5 +1,7 @@
 import io
 
+import pytest
+
 from starlette.datastructures import (
     URL,
     CommaSeparatedStrings,
@@ -8,6 +10,7 @@
     MultiDict,
     MutableHeaders,
     QueryParams,
+    UploadFile,
 )
 
 
@@ -210,6 +213,20 @@ def test_queryparams():
     assert QueryParams(q) == q
 
 
+class TestUploadFile(UploadFile):
+    spool_max_size = 1024
+
+
+@pytest.mark.asyncio
+async def test_upload_file():
+    big_file = TestUploadFile("big-file")
+    await big_file.write(b"big-data" * 512)
+    await big_file.write(b"big-data")
+    await big_file.seek(0)
+    assert await big_file.read(1024) == b"big-data" * 128
+    await big_file.close()
+
+
 def test_formdata():
     upload = io.BytesIO(b"test")
     form = FormData([("a", "123"), ("a", "456"), ("b", upload)])