-
Notifications
You must be signed in to change notification settings - Fork 6
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
Yielding Event Loop #51
Comments
Comments below...
I don't understand what you mean by this.
I don't understand what you mean by "pipelining" in the context of event handling. Pipelining requires parallel processing, which, in JS, can only be realized using workers.
If a module uses the event loop to carry out something asynchronously then this isn't actually a problem; the result is not expected to be witnessed at the current logical time.
Right. To avoid this, I proposed to run such reactor as a worker.
OK.
With what value of
For an application programmer it is rather opaque as to what amounts to a desirable setting of the parameter. And how would the programmer even detect there is a problem to which changing the value of the parameter would offer a solution? I sincerely doubt the practicality of this solution. That said, my main objection is that this feature introduces nondeterminism. Pending JS events are not only capable of scheduling actions (which is useless, anyway, because they cannot be handled until logical time advances), but can also create side effects: they can change state observed by reactions; they can write to the console; they can send messages over the network; they can drive actuators. The ordering in which these kinds of side effects are observed should not depend on the physical execution time of reactions. In other words, this change breaks the reactor semantics. For that reason, I'd like to veto merging this into master. I think the right solution to address your concerns is to run an |
I'm very open to convincing on all points so I'd appreciate if you could elaborate. This has been bugging me for weeks :) I'll give some more concrete examples of these issues.
The app is expecting messages over some network connection, so it sets up an event handler for the messages. The app needs to get an accurate timestamp on when the message is received, so it registers a callback that looks something like this:
The message arrives at time
In this scenario the app is waiting for a message that comes in two chunks, so it registers handlers that look something like this
The problem is if the first handler is delayed by the event loop, it will be delayed on requesting message part 2 some arbitrarily large amount.
Good point, it won't be witnessed until later so it will have the correct behavior. But it could still be very bad for the module's performance. Consider a module with a significant total latency, but not a lot of processing that is triggered by an event emitter. The module won't be able to begin its high latency operation until after the event loop clears up
Yeah, I think this could work, but it's not ideal. There are limitations on the kinds of messages that can be sent from a worker, and the reactor wouldn't be able to invoke any callbacks from the other program. It's also a lot more complicated to use a worker than to just flip on a target parameter that solves the event loop blocking problem.
That was with
I imagine the thought process would go like this: "I'm having this bad performance because my event loop is being blocked. I don't want it to be blocked for more than about 50ms. So I'll pick that."
There's a program that will tell you exactly what the latency is on your event loop https://clinicjs.org/doctor/
I agree we shouldn't break the reactor semantics. But I don't see how this change breaks anything. The only thing it does is periodically hit pause between reactions so other stuff can happen in the background.
Wouldn't running the |
Time stamping is done in
OK, so you mean "chunking," not "pipelining."
This will always be the case if there are other parts of the program that block the event loop for extended periods of time. Your solution does not actually solve this problem, because, in JS, you cannot preempt on-going computation. The sole issue that you're addressing is that when Zeno behavior is occurs you interleave that Zeno behavior with background tasks. Since the reactor semantics demand that these background tasks are not reacted to until after time advances, there is no point in reducing the latency of said background tasks, unless they are entirely unrelated to the reactor program. If there are such unrelated concurrent tasks that the reactor program should not interfere with, then these tasks (or the reactor) should run in a separate worker.
Which is why one should not write such programs. If you want reactive programs, don't create Zeno conditions.
It doesn't solve the problem. If a reactor exhibits this kind of behavior while it is expected to react quickly to outside stimuli then there is something structurally wrong with it.
That's pretty bad...
Like I stated before, this only "helps" when reactions succeed one another in superdense time, which is a pathological behavior in reactive programming, let alone in cooperative multitasking.
The transparent solution in such case would be to introduce a time delay somewhere in the computation, which the reactor model already allows for.
I already wrote an explanation in my previous response.
Yes, which is why it provides isolation from other activities that might inadvertently be blocked by a reactor. If you don't buy my explanations, then feel welcome to come up with a use case where this kind of non-deterministic interleaving between reactions and background tasks is a) necessary and b) not achievable with the current framework. If you find such use case we can reopen this discussion. |
If we factor it into network latency, doesn't that mean an optimization to limit that latency is a good thing? A reactor that occasionally blocks the event loop for a while would have very bad worst case network latency for PTIDES.
A reactor doesn't have to be Zeno for this issue to come up, it just has to do CPU intensive work inside a couple reactions at the same (or sequential) microsteps. For example a reactor could very legitimately have five reactions triggering at the same time that each take 20ms to execute. Depending on how the app yields, event loop latency could be as low as 20ms or as high as 100ms.
But this is an example where changing the behavior of background tasks does has a direct effect on the arrival time of a physical action within the reactor program. Let's say message1 arrives at (all physical) time If the app does not yield the event loop, the earliest message2 can be received (and a physical action scheduled) is
They certainly could in this example. But why should we force a programmer to use a worker thread when they could instead write a simple single threaded program and just turn on a target parameter? Also, worker threads themselves communicate to the main thread through messages handled by the event loop. Worker thread communication would suffer from similar delays.
As before, CPU intensive work inside a couple reactions at the same (or sequential) microsteps is not Zeno and would be bad for module performance. I think the "using a module" scenario is still worth discussing.
I agree there's an overhead threshold where my proposed change should not be implemented. But I didn't really optimize this version yet, and for perspective performance improved by 60% on PingPong the first time I implemented a version that didn't yield whenever microsteps advanced. Also we're talking about performance here on the PingPong benchmark which is a Zeno reactor program with no I/O. If we were to benchmark an I/O heavy program with occasional CPU intensive reactions, performance would probably improve. PingPong is sort of an unrealistic worst case for these changes.
Yeah that would work. But isn't it better for the programmer to just write the reactor program they might write for the C target without having to change their design around a quirk in how the TypeScript runtime never yields the event loop?
I'm trying to understand it. You wrote that pending JS events can do bad nondeterministic things, and I agree with that. But pending JS events are also necessary to bring physical actions into the reactor model. Specifically you said "this feature introduces nondeterminism", but I don't see how. You can do bad things with exactly the same nondeterministic JS events with or without the feature.
The use cases are:
Do you think it would help this discussion if I were to implement sketches of these examples in code? We also don't have any examples yet of your "just do it in a worker thread" suggestion and it might be informative for me to try that. |
No. You're not reducing latency in any way by yielding to the event loop amid synchronous reactions.
You're just repeating what I wrote above, so I guess we agree.
I don't question this analysis. This is true for any node program.
I disagree. In the reactor model, "arrival" is denoted by an invocation of schedule.
The only thing this strategy achieves is that there is a greater different between the timestamp of the message and the logical time at which it is handled. What is the point of this?
Who is talking about forcing anything?
This comes with the territory of a having a single-threaded event loop. In JS, you have two options: 1) write small functions that are quick to execute to completion or 2) dispatch computation to a worker. There is no 3) preempt computation at arbitrary times to decrease latency. Conceptually, a sequence of reactions is atomic in the reactor model -- just like an event handler, it ought to run to completion before doing anything else.
Again, this holds true for any JS program. Programs that perform long synchronous computation are considered bad practice. Yes, you can use reactors to write bad JS code; you can do the same without them.
The situation for the C program is not much different. If you want to give physical actions a chance to be reacted to, you cannot overwhelm the reaction queue with rapidly succeeding events.
That was obviously not my point. I'm not discussing determinism in plain JS programs, I'm talking about determinism in reactor programs. The ability to block out asynchronous tasks is an advantage in the sense that it allows us to ensure that reactor state cannot be changed by those activities while reactions are occurring. If we allow them in, they can render the reactor's behavior nondeterministic.
I think "problem" is based a misconception.
Reaction code is synchronous. Event emitters are used for handling asynchronous events. Blocking
If two messages need to be handled simultaneously, you would store the first message and wait for the next to arrive. Once both messages have arrived, you schedule a physical action.
Sure -- implemented examples are always useful. |
Could you explain this more? I don't understand what you mean with both "event-loop latency effectively factors into the network latency" and "You're not reducing latency in any way by yielding to the event loop amid synchronous reactions." If event-loop latency is a factor of network latency, how does reducing event-loop latency not reduce latency in any way? The more I think about this, the more I think the non-deterministic event loop is going to be an obstacle for PTIDES however we choose to handle this issue on yielding.
My understanding is Zeno behavior usually means an infinite number of steps without time advancing. If you instead mean any number of steps without time advancing, then yes, we're talking about the same thing.
In this example I'm discussing the behavior of code that's not part of the reactor model. It's necessary to use non-reactor code from the underlying platform to bring physical actions into the reactor model. In this case blocking the event loop would make that code perform poorly.
I'm assuming here it's desirable to begin processing a reaction to the physical action as quickly as possible and the reactor program became idle after
If the only way to prevent a reactor program from interfering with unrelated concurrent tasks is to use a worker thread and you want to achieve that outcome, aren't you forced to use a worker thread?
With regard to JS I totally agree. But while a sequence of reactions is atomic in the reactor model, I don't see why it has to also be atomic for the underlying platform too. Breaking up a sequence of reactions in Node.js is like turning a long function into several smaller functions. And that should be invisible from the perspective of the reactor program.
If we can turn a reactor program into better JS code without changing the reactor program, I don't see why we shouldn't.
Then perhaps this would be an improvement over C. You could just write your TS reactor program without worrying about giving physical actions a chance to show up. You could only advance time when it makes sense within the logic of your reactor program.
It wasn't so obvious to me. :) I didn't realize you saw blocking activity during reactions as an advantage. Now more of what you wrote makes sense to me. But if some lazy programmer were to write a bad asynchronous function like this in the preamble:
and execute it at random times, wouldn't it be just as bad for that function to execute in the middle of a chain of reactions as when the reactor program is idle? Seems like the programmer's determinism is screwed either way to me.
I agree we're probably talking past each other on this. Can you elaborate on what you mean?
I don't see poor module performance as a feature. I view access to npm modules as one of the biggest advantages of the TypeScript target, and I think we should do everything we can to make TypeScript reactors play nice with them.
The nuance of this scenario is that message2 has to be specifically requested once message1 has been received.
Cool! I was also thinking it would be good to write a custom "http server" benchmark for network performance. A benchmark like that definitely wouldn't be part of the Savina suite. Anyway, I'm going to work on command line arguments for TS first before getting into this. |
I'd like to revisit the discussion on yielding the event loop from 25d8525. I think it's a very good thing to avoid the overhead of yielding the event loop when it's unnecessary. And I agree that in most cases the natural time to yield to the event loop is when time is advanced, because that's when physical actions are going to have a chance to execute anyway. But I still think it's a serious problem for reactor-ts to block the event loop indefinitely in these cases:
res.end()
which sends a message from the Node program to the client flushes the buffer immediately when you call it, so there is no delay even if the server's event loop is blocked.So I pushed a new version of reactor.ts to the branch "yield" that has a
_yieldPeriod
parameter. The main change it makes to _next is immediately after executing a reaction it checks if_yieldPeriod
seconds have elapsed since the last time the event loop has gotten to run and it yields when the period has been exceeded.If
_yieldPeriod
is null, the behavior of_next
reverts to yielding when time is advanced. That way if a programmer is willing to sacrifice some CPU performance to avoid blocking the event loop, they can do that.I didn't implement it yet, but I'd like to make
yieldperiod
an optional argument to the app constructor and TypeScript target parameter. The additional overhead on PingPong performance relative to the last commit in the repo (ce47460) is 5% (10 trials), but I bet that could be brought lower.Any objections to merging the branch and implementing the target parameter in LF?
The text was updated successfully, but these errors were encountered: