#Is eio the way to go?
1 messages ยท Page 1 of 1 (latest)
How is this possible ? 0 reaction ?
If object become mainstream they will get improvement. Is it part of the API or internal change that can be changed later ?
I sincerely hope objects are improved. It does seem that with buffering, the impact isn't bad.
For example, if a new type of object was added that had to derive from a class and didn't rely on row-polymorphism, it would immediately solve the performance problems.
No direct experience really, but it seems to be flexible enough to get a remarkably fast and simple implementation for cohttp
No direct experience yet but as a heavy user of async io a lot of things look sane to me. I am a bit less convinced by the io_uring backend but it is purely internal so not a great issue
so like ..records?
nope. like C++ objects
what makes you less convinced about uring ?
Using it a lot from c (caveat debian 11 kernel so not bleeding edge Linux), performance of io_uring is tricky. The interface is great, but wether using it is worth it depends on the usecase
On network io it is not really faster than epoll, and you better still use poll via uring anyway to match performance. On disk io, it can be great (reached some crazy numbers like 100Gb/s from single core in c). But it can be less great. If you don't batch enough blocking io can be a lot faster. Depends on the disk model too. Same for fs name operation. Blocking is faster in a lot of cases. Though then you would have to compare it with thread pool so maybe it is always worth
when you say "still use poll via uring", you mean that doing N io_uring_submits is worse than setting N pollfd and doing one poll(2) ?
I'm surprised how epoll can more or less match uring, sure, for one connection yes, but while I'm in userland handling whatever one connection maybe, other N can be ready and the copytouser() is already done when I get back to them
would be nice if you could share your experiences with uring on an issue there in ocaml-uring, we can learn something
@joris you en always use luv back-end if you want
I think eio looks very interesting, and you don't even have to touch objects to use it. All methods have a function to call them, and provide better error messages if you misuse them
Batching is important but eio now has both buffered readers and writers (similar to angstrom and faraday)
in fact, the buffered writer is a fork of faraday
heh, yeah
I also looked at (part of) the code, and it looks pretty good. Very modern.
I'd like to get a libevent backend at some point, and maybe add uring to libevent so eio only talks to libevent in the future
but that's a long pipe for now
I think it's nice that eio builds on different backends from the start
but then you have to maintain N backends :/
sure, but they're separate libraries right?
I imagine luv would cover 90% of the cases
yes, but we have one scheduler for each
and luv is unmaintained, and libuv is realllllly bloated
right. Is it impossible to change, that there's a scheduler for each?
can it be functorized or parametrized?
I've found async's approach a bit nicer in this respect. The core scheduler is independent of how i/o is done, and was a lot easier to extend for non async_unix backends, while still being able to leverage the high precision timer, and scheduler from async_kernel.
sure, but it's more complexity than just talking to one thing
libuv is like 100, we need like ~5
(disclaimer I also just really dislike libuv)
Not sure if I'll get around to this at any point, but if you are interested you could take https://github.com/anuragsoni/poll out for a spin for potentially using with eio. No timers here, but it is a really thin wrapper that abstracts over kqueue, epoll and wepoll
@autumn tartan I've seen that eio has no API for subprocesses yet
would that bring libuv to, not 5, but 10? 20? :)
I'm not sure an API for that would likely be wanted (but I know nothing)
I know that Unix's subprocess capabilities are one of the pain points for me. When starting subprocesses, I'd like to:
- communicate on stdin/err/out as it goes
- kill it with a signal
- wait for it to return, get the return code
- (optionally) stop it after a timeout, which might be doable using structured concurrency + kill?
there's an issue opened for it.
I need to improve the linux backend a little more as I don't use timerfd for timeouts at the moments. As a result the the precision for timeouts on linux is milliseconds, whereas kqueue can do nanoseconds
ah cool saw it now
I mean, having subprocesses is a bit of a sine qua non, I think; almost as much as not having sockets ;)
I didn't know objects were even used by EIO. Guess I should study the implementation a bit...
they make a decent point for it. The repo is full of documentation, including a rationale.md that goes into "why objects"
Very rough idea of something for processes here https://github.com/patricoferris/eio/blob/processes/lib_eio/process.mli which I needed whilst hacking together https://github.com/ocurrent/obuilder/pull/113 (also v. recently announced that uring should be getting a spawn I think https://www.phoronix.com/news/Linux-LPC2022-io_uring_spawn) :))
Is there a reason it's process and not t btw?
Otherwise, yeah, that looks nice
I guess it's likeUnix in the sense that methods of processneed to document what effects they have (like status will block until the process exited)
Taking arbitrary flow sources/sinks for redirection would be amazing though.
yes on my workload, doing N io_uring_read on socket is worst than doing N io_uring non-blocking read and reverting to epoll. Which is on par to epoll. But doing straigth read without poll operation (once again on debian 11 kernel). But network is not a concern, io_uring without poll_add scales to reasonable number. I have more issues on disk io personally, where doing async io is often not worth it depending on the disks and batching you use
and just to be clear, it is not a criticism in eio. It looks really nice for the small tests i did. But i am a bit unsure how making every io async on the general case with the current state of io_uring and some cases falling back to kernel side threadpool is the good idea in the general case right now.
i think personally i will start with libuv backend
the thing is, it is really hardware specific. I spent 2 weeks recently converting a c++ app to io_uring from blocking reads with ~10 nvmes. Even with the gain of zero copy read with io_uring fixed buffers + direct io, the latency ended up to be higher with io_uring and the throughput lower than direct blocking reads because batching was ~4 per io_uring_enter, and the cost of processing submit+completion was dominating. Otoh when it scale to like 20 reads per submit it totally destroys blocking read
honnestly i don't feel smart enough to understand the performance implications of this kind of api since it interacts with so much kernel features :/
but then i guess, io_uring is not bad default if you would not write blocking read in your app anyway. Idk.
The thing is we only use the "scaling" of io_uring within different fibers, essentially each Fiber is always waiting for one CQE (on virtually every case)
and all FDs are set to nonblock, so the kernel should only be threadpooling when it's getting really overloaded
but the SQE->CQE back pressuring needs some work as well
I mean the point of the gain is to not synchronously do the copyfromuser() for each "Fiber that blocked"
i've actually read the implementation of eio yeah and i have nothing to say about it. As far as i know io_uring eio backend is "perfect". My main concern is more about the predictability of switching to async operation everywhere because in my experience it is hard to understand how worthy it is.
I mean the point of the gain is to not synchronously do the copyfromuser() for each "Fiber that blocked"
Yes this makes perfect sense. Especially the fact that you use fixed buffer which saves one copy compared to traditional reads with unmovable buffer. Again i have no criticism about the implementation
i'm just not sure if on "low intensity" io code, io_uring is the right choice compared to blocking ios with threadpool which is more predictable and easier to understand. For for sure eio with io_uring probably scales better. (again i only read the code didn't try it)
oh there are some bad things we need to fix haha, we were restricting total number of CQEs as maximum "possible things we can wait", but it's getting fixed
and for instance i have a feeling most of lwt code in the wild is "low intensity". With not much parallelism
I've written some really basic stuff using ocaml-uring directly and comparing against blocking normal stdlib stuff, it was mostly the same (but ofc really simple stuff that is not very expressive)
aye, but low intensity also means "don't care much about performance", no ?
heh. You may have a point there. I have a lot of low intensity with strong latency requirement, but i guess in the scope of community it is not really an issue
just write it with ocaml-dpdk and problem solved, oh shizzle we don't have one ๐
(and btw i am using ocaml-uring in prod right now, without eio because 4.14. I guess you are the author, so i will take this opportunity to thank you. Saved me the headache to write this tool in C)
oh I'm not the author, I just kinda got into it, I work with talex5@ and I'm trying to offload uring from him, he busy smart man, I'm simple minded former kernel dev cosplaying in ocaml ๐
I think anil is the original author
oh right i heard anil talk about this a long time ago ๐ anyway it is cool to have this binding
I like it too, though I would like to have it in provo's libevent, and then just support libevent
libevent ? not libev ?
I'm very biased cause I love libevent
yes, libevent (the one in Chrome), not libev
it's very popular in BSD land
this one: https://github.com/libevent/libevent
hm ok. Why so ? We were using lwt with libevent backend but had a lot of headache debugging some random segfaults and switched everything with lwt + libev and now everything is good. I must admit i know nothing about the internals of those libs
oh I didn't even know lwt had a libevent backend
I've always used it directly, and we kinda had our fork diverged a bit, it was originally developed inside openbsd by provos, and then we continued using
lwt has a nice object interface for engine, so it has a backend for whaterver you want it to have a backend for. The libevent backend is in ahrefs devkit lib, so yeah it is kind of hidden ๐ฅฒ
makes sense. I never read the api. On this topic, lwt has kind of an anti partern in several part of its io api (not all)
where it polls before trying to read/write. Which is kind of stupid. (with many part of the lib checking if fd is readable / writable before trying to read/write)
i'm glad eio does not do that, it is already a huge win
you mean I do a normal Lwt.read or whatever and after it returns the FD as readable, I check it again or ?
yes there are cases where lwt will poll first to check it is readable before trying to read. It is not stupid as in a bug, because it happens in corner case, where lwt is not sure if the fd is a socket or a file and scheduling to a threadpool is costly, but still it is an issue in practice
lwt will poll first, get "available", then attempt read
You mean, it could just read and see if it gets EAGAIN?
Lazy.force ch.blocking >>= function
| true ->
wait_read ch >>= fun () ->
run_job (read_job ch.fd buf pos len)
| false ->
wrap_syscall Read ch (fun () -> stub_read ch.fd buf pos len)
yes this.
ohh I see: wait_read -> readable -> unix_readable -> poll]
it makes perfect sense to implement this like this, it is not a bug. but in practice it is not so great.
it's weird cause wrap_syscall() does exactly what you suggested
scratch that, it's doing unix_readable() as well.
honnestly right now i just flew back from icfp and i'm a bit tired, and i never totally understood the internal invariants of lwt. But last time i checked i convinced myself it made sense.
but stracing services on prod, it does not seem optimal at all
from what I see they do a POLLIN check only if it's the FD is blocking so it makes sense I think
I was shooting myself in the foot cause I forgot that poll(,,0) is not poll(,,INFTIM)
For anyone interested in "why objects": https://github.com/ocaml-multicore/eio/blob/main/doc/rationale.md
I think because the "process capability" is the t. Thinking about it more, cwd should probably be a Path.t too, although presumably once you are spawning subprocesses you can't really restrict them to a directory (I'm not familiar enough with that ๐
)
Even with pledge ?
I didn't know about pledge, perhaps thanks!
It's going to be hard to promise that in a portable way though!
pledge is so cool though, someone wrote a pledge interface with eBPF for linux recently https://justine.lol/pledge/. But yeah anything like that is pretty impossible to make portable ๐ฆ
and for this functionality it wouldn't be pledge, it would be unveil, which is like a sister subsystem of pledge
I think this is probably out of scope for eio, but maybe libraries will be able to modify the std env to bring their own process spawning? Eg on Linux using containers/cgroups/..
maybe, Eio could also take some f() that is called after fork and before exec, so you could do something evil before exec.
๐ do you know https://github.com/dinosaure/esperanto is build on cosmopolitan (the same author of this blog post) so we can have pledge/unveil on non bsd.
but pledge/unveil are syscalls, how would that fit ?
I didn't dig but this test binary suggest that it is available on linux at least https://github.com/jart/cosmopolitan/blob/83d41e45880255727066b3d904a35e871af6d891/tool/build/unveil.c#L61 and on unsuported platform don't add protection mechanism is fine if they don't exist ๐คท๐ปโโ๏ธ
ack, I think she was testing her pledge@eBPF thing with that
it's sad she got a lot of hate from openbsd people with that :/
Basically because on the article she shows a binary that fork+pledge+exec, which is kinda missing the point of pledge. I thought it was kinda ok
the original intend was to pledge the whole OpenBSD base, not to protect unknown things you're running
Ok self restrict . I won't be shocked with the first pattern either.
and about syscall technically we call Libc wrapper so they could inject our wrapper with LD_PRELOAD trickery (only way to escape is raw asm or setuid programs immune to that)
I was meaning the problem is not injecting the syscall, is actually getting the semantics (which need to happen at kernel level)
With wrapper we could filter some argument before sending to the kernel
the point about the binary is that she is porting something that kinda only makes sense when you control the full base, to someplace where she has no control, so I think the binary was ok.