You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
asyncio tcp transport on Windows reads bytearray instead of bytes #99941
asyncio.Protocol.data_received prototype not respected: when using create_connection to create a tcp transport data_received is being called with a bytearray object instead of bytes.
If this is expected behaviour libraries like httpx and such should be warned or the prototype modified, although I doubt it's intended because it would suppose the generator doesn't hold reference to it otherwise data could change while stored in buffered stuff.
Correct output: @@@@@@@@@@ b'HTTP/1.0 ...WHATEVER THE SERVER ANSWERS...'
Faulty output: @@@@@@@@@@ bytearray(b'HTTP/1.0 ...WHATEVER THE SERVER ANSWERS...')
Your environment
On Windows 11 x64
Tested on:
3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]
3.10.1 (tags/v3.10.1:2cd268a, Dec 6 2021, 19:10:37) [MSC v.1929 64 bit (AMD64)]
Working as expected in:
3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)]
Related problems
Link to the thread where I found a conflicting thing (it's being solved there as a workaround) encode/httpx#2305 (comment)
I'm not sure what you mean by asyncio.Protocol.data_received prototype -- are you referring to the type stubs used by static type checkers in typeshed perhaps? Those are not considered normative (they are often created using reverse engineering and bugs may be fixed when users complain). What does the doc say? That would be considered normative (in most cases).
Fixing this might require copying the data which would adversely affect performance, effectively copying all data received an extra time.
I've not tested it on linux, the docs clearly says https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol.data_received non empty bytes object although could make sense for perfomrance to return a bytes-like object (alhough non hashable that is the main problem, and slices of bytearrays are still byte arrays even tho they don't share memory and are a copy) https://github1s.com/python/cpython/blob/HEAD/Lib/_pyio.py#L621 as you can see searching for read functions it's always either casted to bytes or calling other reads that finally lead to os.read which correctly returns bytes, will test on linux probably on monday and see if I can setup a test env to delve deeper in where the bytearray comes from
I agree that the phrase "bytes object" in the docs refers to bytes, not bytearray.
The cause of the regression (if it is that) is presumably GH-21442, which switches to bytearray for performance reasons. It does this only for the proactor event loop, so Windows only.
Now, I am reluctant to switch back to bytes, because that would lose some of the performance gain. I'd rather update the docs. If a particular protocol class really want bytes, it's easy to put data = bytes(data) at the top of the data_received method.
Fair as long as the bytearray is not reused and is released by the source, otherwise if any protocol buffers stuff storing the object it will get corrupted, although I see self._data[:length] in the deferred call, so it's already copying, making it a bytes shouldn't have speed downsides cause slices for bytearrays are copies
+1 for updating the docs to something like data is a bytes-like object. An optimized protocol may choose to return memoryview of the data to avoid excessive copying so I would like to keep the docs a bit generic for future improvements.
Hm, memoryview is probably too different from bytes and bytearray to be acceptable. (In the typeshed world there's been discussion of dropping its inclusion in the bytes-like concept there, though this is not so simple either.)
It is indeed the case that the bytesarray passed to data_received() is a fresh slice. Unfortunately I think there's no API that I know of that creates a slice of a bytesarray and turns it into a bytes without an extra copy. (Unless that's possible using memoryview as an intermediate?)
Totally agree, should be possible (ofc requiring testing) to wrap a memoryview, slice that which is a noop and bytes it instead with almost no performance drop
So instead of self._data[:length] we could write bytes(memoryview(self._data)[:length]) and that doesn't copy bytes redundantly. It does create two memoryview objects (one for self._data and one for the slice) and each memoryview object costs 184 bytes on my machine, so maybe we could do something like bytes(memoryview(self._data)[:length]) if length >= 500 else bytes(self._data[:length]) (the 500 is a hunch, we could tune it using a micro-benchmark if you feel like it).
But since the current behavior is already out in 3.10 and 3.11 maybe it's not worth fixing and we should just document that it's "bytes-like". Thoughts?
In my opinion I'd keep bytes, cause passing mutable objects in the stream stack means that any protocol in between could mess up and break reading stuff (same with write in fact there was a discussion to disallow passing bytearray in writes, I think they settled in copying it with bytes on function call). Also all the reads (file like objects and everything) documentations must be changed or al least checked if we change this protocol. I'm not in the dev team for python tho, so I might not be the best person to ask this
Sure no problem, I'd see how to test performance with the example, I've also found that a similar thing is done in streams.py to uniform that we should decide what to do, imo unless the interpreter can figure out bytes(my_bytearray[:n]) as a simplified slice (since the slice reference is never stored) it's always best to go with the memoryview intermediary (if someone is reading with a very small buffer they are clearly not going for performance and adding a conditional will slow stuff down) so I'd change to that also in https://github1s.com/python/cpython/blob/HEAD/Lib/asyncio/streams.py#L691 and https://github1s.com/python/cpython/blob/HEAD/Lib/asyncio/streams.py#L733
Activity
gvanrossum commentedon Dec 3, 2022
Is it really only happening on Windows?
I'm not sure what you mean by
asyncio.Protocol.data_receivedprototype -- are you referring to the type stubs used by static type checkers in typeshed perhaps? Those are not considered normative (they are often created using reverse engineering and bugs may be fixed when users complain). What does the doc say? That would be considered normative (in most cases).Fixing this might require copying the data which would adversely affect performance, effectively copying all data received an extra time.
DarioDaF commentedon Dec 3, 2022
I've not tested it on linux, the docs clearly says https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol.data_received non empty
bytesobject although could make sense for perfomrance to return a bytes-like object (alhough non hashable that is the main problem, and slices of bytearrays are still byte arrays even tho they don't share memory and are a copy) https://github1s.com/python/cpython/blob/HEAD/Lib/_pyio.py#L621 as you can see searching forreadfunctions it's always either casted tobytesor calling other reads that finally lead toos.readwhich correctly returns bytes, will test on linux probably on monday and see if I can setup a test env to delve deeper in where the bytearray comes frompochmann commentedon Dec 3, 2022
Hmm, actually it says "bytes object", not "
bytesobject".That said, the doc for
bytesandbytearrayoften says "bytes object" and "bytearray object", referring tobytesandbytearray, respectively.gvanrossum commentedon Dec 4, 2022
I agree that the phrase "bytes object" in the docs refers to
bytes, notbytearray.The cause of the regression (if it is that) is presumably GH-21442, which switches to
bytearrayfor performance reasons. It does this only for the proactor event loop, so Windows only.Now, I am reluctant to switch back to
bytes, because that would lose some of the performance gain. I'd rather update the docs. If a particular protocol class really want bytes, it's easy to putdata = bytes(data)at the top of thedata_receivedmethod.DarioDaF commentedon Dec 4, 2022
Fair as long as the
bytearrayis not reused and is released by the source, otherwise if any protocol buffers stuff storing the object it will get corrupted, although I seeself._data[:length]in the deferred call, so it's already copying, making it abytesshouldn't have speed downsides cause slices for bytearrays are copieskumaraditya303 commentedon Dec 4, 2022
+1 for updating the docs to something like
data is a bytes-like object. An optimized protocol may choose to returnmemoryviewof the data to avoid excessive copying so I would like to keep the docs a bit generic for future improvements.gvanrossum commentedon Dec 4, 2022
Hm,
memoryviewis probably too different frombytesandbytearrayto be acceptable. (In the typeshed world there's been discussion of dropping its inclusion in the bytes-like concept there, though this is not so simple either.)It is indeed the case that the
bytesarraypassed todata_received()is a fresh slice. Unfortunately I think there's no API that I know of that creates a slice of abytesarrayand turns it into abyteswithout an extra copy. (Unless that's possible usingmemoryviewas an intermediate?)I think it's fine if the docs state bytes-like.
DarioDaF commentedon Dec 4, 2022
Totally agree, should be possible (ofc requiring testing) to wrap a memoryview, slice that which is a noop and bytes it instead with almost no performance drop
gvanrossum commentedon Dec 4, 2022
So instead of
self._data[:length]we could writebytes(memoryview(self._data)[:length])and that doesn't copy bytes redundantly. It does create twomemoryviewobjects (one forself._dataand one for the slice) and eachmemoryviewobject costs 184 bytes on my machine, so maybe we could do something likebytes(memoryview(self._data)[:length]) if length >= 500 else bytes(self._data[:length])(the500is a hunch, we could tune it using a micro-benchmark if you feel like it).But since the current behavior is already out in 3.10 and 3.11 maybe it's not worth fixing and we should just document that it's "bytes-like". Thoughts?
DarioDaF commentedon Dec 4, 2022
In my opinion I'd keep bytes, cause passing mutable objects in the stream stack means that any protocol in between could mess up and break reading stuff (same with write in fact there was a discussion to disallow passing bytearray in writes, I think they settled in copying it with bytes on function call). Also all the reads (file like objects and everything) documentations must be changed or al least checked if we change this protocol. I'm not in the dev team for python tho, so I might not be the best person to ask this
gvanrossum commentedon Dec 6, 2022
Do you feel comfortable submitting a PR? I think there are only two or three places that need to be changed, plus a unittest for each.
DarioDaF commentedon Dec 6, 2022
Sure no problem, I'd see how to test performance with the example, I've also found that a similar thing is done in
streams.pyto uniform that we should decide what to do, imo unless the interpreter can figure outbytes(my_bytearray[:n])as a simplified slice (since the slice reference is never stored) it's always best to go with the memoryview intermediary (if someone is reading with a very small buffer they are clearly not going for performance and adding a conditional will slow stuff down) so I'd change to that also in https://github1s.com/python/cpython/blob/HEAD/Lib/asyncio/streams.py#L691 and https://github1s.com/python/cpython/blob/HEAD/Lib/asyncio/streams.py#L733Added test case for python#99941
gh-99941: Ensure that asyncio.Protocol.data_received receives immutab…
msoxzw commentedon Jan 28, 2023
Another PR, #21446, efficiently implementing
BufferedProtocolfor proactor event loop, addresses this issue as well.DarioDaF commentedon Jan 29, 2023
I'll give a look at the PR, it seems more broad then the actual problem. Now there are test cases for this so if it passes should be fine