lep: pull based event loop
I'm going to leave what I said in https://github.com/libuv/leps/pull/3/files#r20995602 out of this LEP for the time being. We'll merge this in draft state anyway, so we could revisit / amend when we get closer. What @creationix and @txdv suggest won't be covered by this, as I mentioned here: https://github.com/libuv/leps/pull/3#discussion_r21078116
It's ok, I can hack it away in my c# wrapper, since I don't have the performance penalty.
I wasn't sure libuv would take my suggested change either. My backup plan is to see if I can wrap libuv in some C code that exposes what I want. C code calling into C code is plenty fast :).
I'm wrapping it at the callback level, I guess you need to write another C lib which wraps the callbacks and queues them for you to consume. (or modify libuv).
I wasn't sure libuv would take my suggested change either. My backup plan is to see if I can wrap libuv in some C code that exposes what I want. C code calling into C code is plenty fast :).
That would be best for now, I'd say. Once you get the feeling of it we can revisit. But there is a ton of code to be written first :-)
Notwithstanding that it may take a long time to get this implemented, I am with @creationix here.
Separating poll from dispatch makes sense, but it's not quite a "pull based" event loop. With a pull based event loop I imagine something more like this:
uv_req_t* reqs[64];
int r = uv_poll(loop, &reqs, ARRAY_SIZE(reqs));
for (int i = 0; i < r; i++) {
// Do whatever needs to be done after completing this req, e.g.:
my_request_wrapper_t* req = container_of(reqs[i], my_reqest_wrapper_t, uv_req);
req->my_callback(req);
}
Notwithstanding that it may take a long time to get this implemented, I am with @creationix here.
Separating poll from dispatch makes sense, but it's not quite a "pull based" event loop. With a pull based event loop I imagine something more like this:
I suggested something like that here: https://github.com/libuv/leps/pull/3#discussion_r20995602. It's not quite the same, but it does have the advantage of not needing to expose internals.
I don't understand the issue about exposing internals. Libuv already has
the practice of not exposing private struct members in the main uv.h file.
I don't need any more data than what's already exposed via the public
APIs. The only data that's not already in the request or handle (soon to
be all requests) is the other arguments in the callback. If public slots
were added to the various request types so that a callback was nothing more
than cb(req), then it shouldn't be any more work to just give me the
mostly opaque request and let me dispatch how I see fit.
Maybe you're talking about some of the requests that would be invisible to the end-user in your current design?
On Mon, Dec 1, 2014 at 4:14 PM, Saúl Ibarra Corretgé < [email protected]> wrote:
Notwithstanding that it may take a long time to get this implemented, I am with @creationix https://github.com/creationix here.
Separating poll from dispatch makes sense, but it's not quite a "pull based" event loop. With a pull based event loop I imagine something more like this:
I suggested something like that here: #3 (comment) https://github.com/libuv/leps/pull/3#discussion_r20995602. It's not quite the same, but it does have the advantage of not needing to expose internals.
— Reply to this email directly or view it on GitHub https://github.com/libuv/leps/pull/3#issuecomment-65144555.
@creationix
I don't think that's what @saghul is concerned about. To give you some context: we're also questioning the design decision of the very early libuv days that the user provides storage for libuv's internal state. (you should be in Amsterdam more often :))
Instead, libuv could allocate/manage memory for it's internal state internally, and from the user's perspective a request would have a predictable size, or handle could even just be represented by an "opaque value". In the latter case we wouldn't be able to store the return value in the struct.
Let me propose an even more strange idea. What if we treated an uv_req_t as a "stack frame" with room for for parameters and results, and nothing else. Sample code to get the idea:
typedef struct uv_write_s {
int r;
uv_stream_ref_t stream;
uv_buf_t bufs[];
size_t nbufs;
uv_req_ref_t internal;
} uv_write_t;
// Write
const uv_buf_t buf = uv_buf_init("Hello, world", 11);
uv_write_t wr = { .stream = stream, .bufs = &buf, .nbufs = 1 };
uv_write(&wr);
// Poll
req = uv_poll_xxx(...);
// Look at the result;
assert(req == &wr);
if (req->r < 0) print("fail!");
@piscisaureus I love the idea of keeping the request simple and having nothing more than inputs and outputs. That certainly simplifies my life.
I don't understand why your example has the internal member though? It looks like it's my responsibility to manage memory for the uv_write_t, but that includes the inline uv_req_ref_t meaning I'd need to manage it's memory too and know exactly how big it is.
Also since you have no void* data member I assume that you're encouraging me to use container_of to embed the uv_write_t inside my struct that has the extra data I need to associate with the request?
If you wanted to manage the internal stuff and let me manage the external/public stuff, wouldn't it make more sense for your internal struct to contain a pointer to the uv_write_t I send you and then give me back only my uv_write_t when the request resolves?
Sorry if I'm missing something obvious. I've only been doing C for a few years.
@creationix
I was afraid that would raise some questions...
The reason I put that there is that libuv can't efficiently map the pointer to the user's uv_write_t to the place where it stored it's internal state, unless there's a field in the uv_write_t that points there. That was the purpose of the internal_handle field.
Now usually there would be no need for libuv to do this mapping. But consider uv_cancel(req) - the question is what reference the user passes for req. If we let the user pass the adress of the uv_write_t libuv needs to do that mapping. If libuv provides an opaque value, that could be passed to uv_cancel. But then the user needs to have access to that information in there - for example by inspecting the internal_handle field.
By exposing internals I mean exposing requests that libuv would use which should never reach the user. These requests are those used by handles which just take a callback for their operation:
- uv_fs_event events
- uv_fs_poll events
- uv_poll events
- uv_process exit event
- uv_signal event
Those requests are not created by the user, and our interface with the user are callbacks. Windows already uses this model with several types of internal requests: UV_PROCESS_EXIT, UV_SIGNAL_REQ to name a few.
If one only considers uv_read, uv_write and the like it could work (even though I don't like it much) but nobody has proposed a solution other than "expose everything" to this problem yet.
Moreover, it's possible that the concept of processing a request involves more work that just firing a callback. For process handles (on Unix), for example, we use a signal handle to listen for SIGCHLD. When the last process exits we stop it, which could be done as a part of dispatchig the process exit internal request. Likewise with stopping the actual process handle. Some generic pseudocode:
void uv__process_foo_req(uv_foo_t* req) {
...
uv__req_unregister(req);
req->cb(...);
uv__foo_req_cleanup(req);
uv__handle_stop(req->handle);
}
Not to mention that requests are linked in an active requests queue, which they need to be removed from when they fire, so the loop will stop if there is no more work to do.
I don't see how all this can be achieved without encapsulating the dispatch mechanism. By encapsulating I mean something like what I proposed here, which is more flexible that what's currently written on the document: https://github.com/libuv/leps/pull/3#discussion_r20995602
This is all I can think of right now, without writing any code yet, FWIW.
@piscisaureus I see, I had forgot about uv_cancel. But in that case, you just need a void* pointer in my struct where you can set the opaque pointer so I can give it back to you if I call cancel.
@saghul That makes more sense. I agree that there is a lot of implementing left to do before we dig too deep into specifics. I just wanted to raise my concerns early while there was still a chance to take them into account.
As far as removing the request from the queue, I expected you would do that before (or soon after nextTick style) handing me the request as a return value to the poll function. It would then be my responsibility to put something else new in the event loop or the loop would unblock and end.
Though I do wonder how a nextTick-like mechanism would work if I only called into libuv and it never called into me. I guess you'd do the cleanup when I next called in to poll for the next event since I'll always be polling in a loop.
uv__req_unregister(req);
// TODO: put req in check queue somehow
return req;
Then next time I call in to poll for the next event, process the check queue first thing.
// req is pulled from check queue
uv__foo_req_cleanup(req);
uv__handle_stop(req->handle);
And carry on processing my poll request.
In other words, it works exactly like your callback behavior now, but instead of calling into my C code and waiting for me to return, you return to me and wait for me to call back.