-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
improve fake network write() efficiency #1178
base: master
Are you sure you want to change the base?
Conversation
- new: class EthernetPacketEncoder, replaces all dynamically allocated write-buffers with a single, static buffer - new: class Uint8Stream, designed to replace TCPConnection.send_buffer[] with a dynamically growing ringbuffer - new: added internet checksum to synthesized UDP packets (required by mTCP) in function write_udp() - replaced internet checksum calculation with more efficient algorithm from RFC1071 (chapter 4.1) - increased TCP chunk size in TCPConnection.pump() from 500 to 1460 bytes (max. TCP payload size) - added function to efficiently copy an array into a DataView using TypedArray.set(), replaces a bunch of for-loops - added function to efficiently encode a string into a DataView using TextEncoder.encodeInto() - refactored all write_...()-functions to use EthernetPacketEncoder - added several named constants for ethernet/ipv4/udp/tcp-related offsets - removed several apparently unused "return true" statements
TCP FIN flag marks the sender's last package. This patch delays sending FIN in TCPConnection.close() until the send buffer is drained.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Overall, this looks like a reasonable set of changes.
I've left some comments.
- Renamed Uint8Stream to GrowableRingbuffer - Minimum capacity now guaranteed to be 16, and maximum (if given) to be greater or equal to minumum - Changed peek(dst_array, length) to peek(dst_array) - Removed misleading method resize(), moved code into write() - Now throws an Exception on capacity overflow error instead of console.error() - Removed debug code that is no longer needed When we have a capacity overflow the situation is as bad as if the Uint8Array constructor had failed with out-of-memory, which throws a RangeError.
- use actual dynamic TCP port range from RFC6335 - throw an Exception when pool of dynamic ports is exhausted
- removed class EthernetPacketEncoder and its global single instance ethernet_encoder - moved encoder buffer ownership to adapter (WispNetworkAdapter and FetchNetworkAdapter) - made view_{setArray,setString,setInetChecksum} global functions - changed prototype of all write_...(spec, ...) functions to uniform write_...(spec, out) - removed function make_packet(spec), added new function adapter_receive(adapter, spec) - replaced all calls of form "A.receive(make_packet(S))" with "adapter_receive(A, S)" (also in WispNetworkAdapter) Note that before this patch function make_packet() was always called in the form: adapter.receive(make_packet(spec)); This patch binds the lifetime and scope of the encoder buffer from class EthernetPacketEncoder to the WispNetworkAdapter/FetchNetworkAdapter instance "adapter".
- removed unnecessary comments around TCP_STATE_... - renamed TCPConnection.send_stream to send_buffer - calculate package length in write_udp() and write_tcp() along the same structure as in all other write_...() functions
For the most part, this patch adds graceful TCP connection shutdown behaviour to class TCPConnection in fake_network.js. Network-intense guest applications like "apt" run noticeably smoother with support for graceful TCP shutdown. Changes in detail: - completed SYN-related state mechanics (TCP connection establishment) - added FIN-related state mechanics (TCP connection shutdown) - reordered and regrouped TCP packet handling steps in TCPConnection.process() - reduced minimum IPv4 packet size from 46 to 40 bytes to reduce noise in logs (why is this even checked) - added explicit detection of TCP Keep-Alive packets to reduce noise in logs - added dbg_log() calls to trace TCP connection state (could be removed if unwanted, or maybe use smth. like LOG_TCP instead of LOG_FETCH?) - removed TCPConnection.seq_history as it was not used anywhere As should be expected, the changes related to TCP connection state only affect class TCPConnection internally and are mostly concentrated in its methods process() and pump(). A few TCP connection states are left unimplemented, reasons: - CLOSING: simultaneous close from both ends (deemed too unlikely) - TIME_WAIT: wait seconds to minutes after sending final ACK before entering CLOSED state (pointless in our specific case and would require a Timer) - LISTEN: are server sockets even supported? Both WISP and FETCH do not support them. Also, TCPConnection.connect() seems not to be called from anywhere (it used to be called from fake_tcp_connect() which was deleted in the previous patcH). As before, fake_network.js does not use any Timers (i.e. setTimeout()), the code is purely driven by outside callers. That means that there are no checks for lost packets (or resends), the internal network is assumed to be ideal (as was the case before). Even though I fixed and removed most of the TODOs I originally found in fake_network.js, I've added several new ones that are left to be discussed. Tested with V86 network providers WispNetworkAdapter and FetchNetworkAdapter, and with V86 network devices ne2k (FreeDOS) and virtio (Debian 12 32-bit).
- removed orphaned function siptolong() - refactored view_setInetChecksum() to calc_inet_checksum() - renamed view_setString() into view_set_string() and simplified it - renamed view_setArray() into view_set_array() - refactored adapter_receive() to make_packet() - bugfix: send FIN after receiving last ACK instead of tagging it to the last packet we send, fixes a corner case when TCPConnection.close() is called with empty send_buffer but with a yet to be acknowledged package in transit. Send a FIN packet in TCPConnection.close() only if both the send_buffer is empty and there is no package in transit, else delay that until we receive the last ACK.
I have a small idea for fetch-based networking that I would first like to check with you before committing. Currently, the full HTTP response (header and body) from fetch() is cached before being forwarded to the originating HTTP client. My proposal is to only wait for the HTTP response headers from fetch(), and then to forward the response body asynchronously chunk by chunk to the originating HTTP client using ReadableStream and HTTP Chunked transfer encoding. To test this, I changed on_data_http() in fetch_network.js based on a ReadableStream usage pattern from MDN, it's pretty straight-forward and it works well with my tests (all HTTP applications support chunked Transfer-Encoding to my knowledge). This would also remove the need for FetchNetworkAdapter.fetch(), I merged its relevant code into on_data_http(). The upside is that this much, much increases both the responsiveness as well as the effective speed of fetch-based networking, all the more with increasing size of the HTTP response. The downside is that the originating HTTP client never gets to see the Content-Length of the response it is receiving, thus it cannot show any meaningful progress statistics anymore other than about what it has received so far. Any Content-Length must be ignored if any Transfer-Encoding is used, and the Content-Length from the fetch response header can be incorrect (for example, for compressed response bodies). The only way around this seems to be the prefetching like it currently is. I might be wrong, but to my knowledge it's categorically either prefetching or the loss of Content-Length (for any request from the originating HTTP client). I wasn't aware of that until I had finished implementing it. I would of course wait with this until #1182 is merged, and include any changes from there. I'm really undecided which side outweighs the other here. What do you think, thumbs up or down? |
Generally thumbs up, but I'm unsure about the downsides. I wonder if HTTP Chunked transfer encoding is necessary at all. Can't the data be streamed in separate ethernet packets?
I believe for compressed bodies (via Content-Encoding), |
EDIT: The gist of the matter is that I cannot determine whether If I could rely on I build and send the response headers to the originating client when I receive the first response body chunk, but I don't have a In fetch(), my browser negotiates gzip compression with my Apache server and transparently decompresses the response before passing me the decompressed chunks (and passes me the compressed size in Content-Length, which is useless). I never get to know how long the response is going to be myself. I tried to suppress compression but I wonder, is there any other method besides Chunked transfer encoding that I could use when I don't know the length of the response? |
I see the problem now.
It seems to be legal to send an http reponse with neither |
I actually do all this to learn and also try out new things, so thank you very much for this information! It should fit in well since I'm already closing the HTTP connection to the originating client once the transaction is complete (current fetch-based networking can't do that because of the FIN packet that is sent too early). |
It works without both Content-Length and chunked encoding! :) Of course, the originating client still doesn't get to learn the Content-Length of the response body, so it cannot show any meaningfull download progress information besides the total bytes received so far. |
See https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg for a full TCP state diagram, all states (except TIME_WAIT) and state transitions below ESTABLISHED are now properly implemented in class TCPConnection. - fixed misconceptions regarding TCP close states and transitions - fixed incorrect behaviour when guest closes a TCP connection - added state CLOSING to state machine - added support to pass passive close up to WispNetworkAdapter - commented out all calls to dbg_log() that are part of this PR This improves interoperability by further completing the TCP state machine model in fake_network.js.
reply.tcp.psh = true; | ||
reply.tcp_data = data; | ||
this.net.receive(make_packet(reply)); | ||
reply.tcp_data = data.subarray(0, n_ready); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this line is not necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand. reply.tcp_data
is needed, it's the TCP payload for the packet we create with make_packet()
here. It is eventually used further down the call chain in write_tcp()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant the .subarray()
shouldn't be needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, data
has a fixed capacity of 1460 bytes of which only n_ready
bytes are used at this point, which can be less than the capacity.
If we remove this .subarray()
then write_tcp()
would somehow need to know n_ready
explicitely together with data
. It would need an extra field reply.tcp_data_length
next to reply.tcp_data
or such.
As it is only called once per generated TCP packet I would just leave it like it is. Also, write_tcp()
passes this subarray to view_set_array()
which also treats it as an array with implicit size.
Looks good to merge to me (one more small comment above). @chschnell Any objections? (asking because it's still marked as a draft) |
I'm still not sure whether you want the fetch()-improvement or not, I can check in the code any time, it's not that much code and also well-tested. The question is whether you prefer to have the performance boost or to keep the Content-Length intact. I tend to the performance boost, but I'm in no position to make that decision. |
How about using the default way for small responses or when the custom |
Yeah, let's do proper chunking and give up the |
Optimizes writing multiple buffers to the TCPConnection at once.
- replaced blocking call to fetch() in on_data_http() with async loop, response body chunks are now forwarded as soon as they arrive - commented out old code path in on_data_http() (for fallback) - commented out FetchNetworkAdapter.fetch() (for fallback), merged into on_data_http() - removed code to make type checking happy for data argument in on_data_http(), fixed JSC declaration instead
Ok, I hope this was the last commit, from my side this PR is ready to go. I've commented out two blocks of code in fetch_network.js (for fallback), and put a single new one in. Some kind of software switch could be added to toggle between the two implementations, but I tried to keep it simple. @SuperMaxusa It should be easy to extend the code as it is now to a two-way solution, for example to prefetch the body when it is not too large, or when the originating client's HTTP request contained a specific header. If anyone wants it I could add it in a later PR. |
Yes, since most HTTP clients can handle these responses without By the way, in many CI runs, the |
@SuperMaxusa Thanks a lot for testing, that is really helpful of you! By the way, this transfer model (transfer without Content-Length and to use end-of-connection to indicate end-of-response-body) is how HTTP 1.0 used to work, and support for it is still required in HTTP 1.1 (here the relevant section). I guess for these two reasons it is widely support. |
Unless there's more to it, Test #6 in tests/devices/fetch_network.js fails correctly, it attempts to fetch() from http://mocked.example.org/ which is an invalid DNS name. The next test nr. 7 attempts to fetch() from http://example.org/ wich is a valid URL, and this test succeeds. I am not sure, but in case you mean test nr. 6, maybe it is intended to just test that a failure in fetch() is handled correctly? Was this test ever meant to succeed? |
This test has a custom fetch for which When the
Interesting, I would never have thought this works as HTTP 1.0 compatibility, thanks for that information! |
Ah I see, thanks for the explanation. It's true that this test cannot work anymore because I removed I would also argue (with my limited oversight) that this test is no longer needed with the changed class layout. |
On a second thought, Perhaps a bit of a stretch, but maybe someone needs the blocking fetch() in the future and then |
Related note I just noticed since this touches wisp_network. If a connection was closed in wisp and the v86 VM tried sending a TCP packet it crashes the whole thing. It may be worth just adding some extra checks around that code just to make sure everything's behaving fine This PR might fix it since it touches closing logic, I'm just not sure |
Even though this method is currently only used ín a test, keep this function for possible future use.
Even though this method is conceptually correct, it is useless because neither WISP nor fetch() support closing only our half of the TCP connection.
Wisp does support bidirectional stream closure (I think I just screwed up how I handled it in v86), is there some issue you encountered with it? Not sending the voluntary close may cause resource leaks for both the client and the server |
I did not come across this when writing the PR. I mostly changed fake_network.js, wisp_network.js only minimally where neccessary. As a test just now, I killed my WISP server (epoxy) while V86 was running without using the network, and after a few seconds it throws an exception in the browser console. Then I restarted the WISP server, and the same V86 instance picked it up just fine, network was ok. Then I killed the WISP server while having a telnet session open in V86, caused some traffic, restarted the WISP server, tried to exit telnet which took around half a minute (two exceptions or so), but then it came back and I was able to start a new telnet session without problems, stil in the same V86 instance. It looks stable to me (V86 survived), but maybe I'm not testing right. How can I provoke this situation you mean? |
I added a WISP frame of type CLOSE with reason 0x02 (voluntary stream closure) to the V86 WISP client just a couple of days ago and removed it again a couple of hours ago after I was told yesterday (in discussion #1175) that this wasn't supported. I could add it back in easily, I left a comment in fake_network.js in my branch where this would belong (it's where we receive a TCP packet with FIN flag from V86 while in TCP state ESTABLISHED). The reason I added it was to close only V86's side of the TCP connection (so-called "half-close"), for reasons see here:
The example from the first link is quite simple (see there for the full explanation): ssh hostname sort < datafile This needs TCP half-close in order to work. IIRC then FTP could also be hindered from working without support for half-close. I'm a bit confused now. Does WISP support TCP half-close? What does CLOSE with reason 0x02 (from the client) exactly do, and is it generally supported by WISP servers? |
I've never heard of half close, okay I see, wisp doesn't support that. Does full closing still work? I thought that commit removed full closing and that's why I was concerned |
close Edit: fix wrong packet type |
Only WispServerCpp implements CLOSE/0x02 like that, neither epoxy-tls nor wisp-js do, in my observation. There was no CLOSE/0x02 in V86's WISP client before I added it a couple of days ago. So in the case of WispServerCpp that commit did remove full closing, but in the case of the other two WISP servers it didn't change anything. I did that because none of them implemented half-close. Now it all comes down to a timeout by the server, or a leaked connection in case that never happens. Maybe I should put the CLOSE/0x02 better back in, even though it will break certain applications and has no effect with some WISP servers. |
Are you saying the other wisp servers don't support closing or don't support half closing? Sorry I think I'm misunderstanding To my understanding every current wisp server does support full closing of streams from either direction (client and server can both report closed streams I believe) Also close is 0x04, the reason is 0x02. Just wanted to clarify because I stated it incorrectly above in my own message |
The protocol does not specify any special behavior on the server side for specific close reasons. I don't know why WispServerCpp behaves differently here, but wisp-js and epoxy-server are following protocol and closing fully.
On epoxy, you can use
|
Also wisp server Cpp sends a 0x04 Close packet back after the client sends one. It is the only server to do this, and it actually isn't in the spec. The client is supposed to assume the stream is closed immediately after sending the close packet. Just wanted to note that on this GitHub thread |
Another note: WispServerCpp doesn't actually close any sockets that it forwards, violating spec, so the remote server never sees the close from the wisp client. |
Several improvements in fake network code that deal with data flowing towards the guest OS (i.e. package encoding and downstream buffering) and TCP state mechanics.