Conversation
The previous value for an ON (to detect the it has changed) is still suing a global value determined at runtime so it can't store more than one of them, which means multiple ON's from the same line of code don't work right. WHENs are working though.
On triggers' previous values are now tracked separately merely by taking advantage of the local scoping that was made available several months ago for triggers. We just weren't using it because we still had the old code that tried using the Global instead of Local storage Opcodes in the trigger's boilerplate code blocks that tracked the previous value.
When Adding a trigger, the caller can now choose if that trigger should wait for the next KOSFixedUpdate, or if it should execute immediately. If Cpu.AddTrigger() is called with arg Immediate=False: The system works like before - the trigger will not get pushed onto the callstack until the start of the next KOSFixedUpdate. If there are instructions remaining this Update, then the rest of the IPU's worth of instructions will happen first, before this trigger will fire off. If Cpu.AddTrigger() is called with arg Immediate=True: The trigger's call will get pushed onto the callstack just before the next Opcode would have been executed in the ContinueExecution() main loop, even if that's in the middle of a KOSFixedUpdate. If we are not in the middle of a KOSFixedUpdate right now, then the effect ends up being the same as if Immediate=False, since the time the "next opcode would have been executed" is during the next KOSFixedUpdate anyway. From the point of view of C# code that's waiting for a return value from the user's callback, there's not really any difference between these two since it has to wait for the user's code to happen anyway before it can get the answer. Immediate=True should never be used in cases where a trigger gets re-inserted automatically on a timer, as that can make it recurse forever and never give time back to the mainline code.
The old stats gathering and printing that would appear at the bottom of a program run were totally wrong for the new way the priority interrupts work. Gutted that and re-did it.
|
This looks really good. Let me ask you few questions / describe few observations to be sure I undrestood it well: 1. Immediate callbacks = 2. All other GUI callbacks are Ignore the rest if the above is not true. If the above is true, then B. Changing suffix with callback assigned from Note to A: The only good solution I can think of is some way to de-elevate the priority somehow (by special command). We have already talked about this, no need to repeat it. Just for completeness. Note to B: Can probably be fixed by Last question: Can you find some example where trigger could try to interrupt itself and being blocked by the second rule (identity) and not by the first rule (lower priority)? It has same priority as self, thus cannot interrupt self. EDIT: Reworded 1. (guiCased=false => immediate) and removed |
What do you mean "fixed"? I thought you said this was exactly the behaviour you claimed you preferred - that callbacks caused by scripts altering a value rather than by user GUI activity happen synchronously, and at the same priority level. The ability to do this is precisely WHY I made
There's two trigger queues. The active one and the pending one. First things are moved into the pending one, then they only move from there into the active one at boundaries between physics ticks. The identity rule prevents it from even entering the pending queue one in the first place. The priority rule only really affects the active queue. identity rule -> Should I put this trigger in the pending queue, meaning it will move to the active queue on the next physics tick? priority rule -> Given that a trigger is in the active queue, does the priority allow it to happen right now? If it's the same identity, then it should never even reach the priority rule test. |
|
And I thought you don't like run levels (interrupt priorities) and specificaly wrote that changing The identity rule seems to prevent triggers/callbacks from flooding the queue (you can click the button like mad, but the callback won't enter the pending queue if it is already there or executing). Thanks, that is understandable. |
Be clear on what "that" refers to here. |
| if (guiCaused) | ||
| UserOnToggle.TriggerOnFutureUpdate(InterruptPriority.CallbackOnce, new BooleanValue(pressed)); | ||
| else | ||
| UserOnToggle.TriggerOnNextOpcode(InterruptPriority.NoChange, new BooleanValue(pressed)); |
There was a problem hiding this comment.
I would change these and all similar lines to this:
UserOnToggle.Trigger(InterruptPriority.CallbackOnce, !guiCaused, new BooleanValue(pressed));
Implement UserDelegate.Trigger like this:
public TriggerInfo Trigger(InterruptPriority priority, bool immediate, params Structure[] args) =>
CheckForDead(false) ? null : Cpu.AddTrigger(this, priority, Cpu.NextTriggerInstanceId, immediate, args);
Cpu.AddTrigger like this:
public TriggerInfo AddTrigger(int triggerFunctionPointer, InterruptPriority priority, int instanceId, bool immediate, List<VariableScope> closure)
{
TriggerInfo triggerRef = new TriggerInfo(currentContext, triggerFunctionPointer, priority, instanceId, closure) {
IsImmediateTrigger = immediate
};
currentContext.AddTrigger(triggerRef);
return triggerRef;
}
And finally ProgramContext.AddTrigger like this:
public void AddTrigger(TriggerInfo trigger)
{
bool immediate = trigger.IsImmediateTrigger && trigger.Priority >= CurrentPriority;
if (immediate)
Triggers.Add(trigger)
else if (! ContainsTrigger(trigger))
TriggersToInsert.Add(trigger);
}
The only thing I am not quite sure about is whether the check ContainsTrigger should be there for both or, as written above, for pending only. WinForms (and similar GUI toolkits) do not check such a thing and you have to do it manually in code (either use some flag or temporarily unsubscribe the callback), but that practice is so common, that I am tempted to make it a rule and always check, thus making it:
public void AddTrigger(TriggerInfo trigger)
{
if (ContainsTrigger(trigger)) return;
bool immediate = trigger.IsImmediateTrigger && trigger.Priority >= CurrentPriority;
if (immediate)
Triggers.Add(trigger)
else
TriggersToInsert.Add(trigger);
}
Reasoning for it all:
set button:pressed to true.from main should execute the callback immediatelly, but with elevated priority, that no other (non-immediate) callback can interrupt it (like click on other button).set button:pressed to true.from recurring trigger should delay the execution after all top-priority triggers are finished.set button:pressed to trueor more commonset editBox:text to "something"from inside the very callback that is handling the change, should, probably, do nothing. This is about theContainsTriggercheck.
P.S.: InterruptPriority.NoChange won't be a thing after this.
There was a problem hiding this comment.
Myabe the check should rather be written like this:
public void AddTrigger(TriggerInfo trigger)
{
bool immediate = trigger.IsImmediateTrigger && trigger.Priority >= CurrentPriority;
if (immediate) {
if (! Triggers.Contains(trigger)) Triggers.Add(trigger)
} else if (! TriggersToInsert.Contains(trigger))
TriggersToInsert.Add(trigger);
}
Because
guiCausedcallbacks happen asynchronously together with setting the property. You can executeset button:pressed to false, which would execute the callback, get interrupted by IPU, now you can click on the button but original won't schedule the callback now, althought you will now read the state as true andonTriggernever notified you about it. Ignoring current callback when scheduling pending should fix the situation.- Executing the callback immediately from inside itself is probably not desirable. Not a big deal, but rather prevention. It is quite common mistake WinForms starters forget about (that changing the text of edit box from inside OnTextChanged will call the handler recursively). I am still not 100% sure about it, as it could have some consequences I cannot see right now, but still think it is the better way to do it (prevent that recursion, even indirect if you change property of some other control that will in turn backfire at you changing the original property/attribute/suffix).
There was a problem hiding this comment.
This looks like running triggers are removed from ProgramContext.Triggers an I assumed they are still there, until they exit/return. It would require third list for those triggers (that were removed from Triggers and didn't finish yet).
BTW: The if (trigger is NoDelegate) can never be true, the classes are not compatible.
https://github.com/Dunbaratu/KOS-1/blob/3edfc4294cceb47876c11193e5e1db92760b0ede/src/kOS.Safe/Execution/CPU.cs#L1390
I have already changed that in one of my PRs to if (trigger.EntryPoint < 0)
There was a problem hiding this comment.
In OpcodeReturn, a new trigger is created when the old one returns. Triggers are removed entirely once they start executing, and the "return" from the code causes a new instance of the trigger to get constructed (a new structure pointing to the same entry point, same program context, etc) and put on the stack. This is done so that the infrastructure for handling recurring triggers was easily laid atop the already existing infrastructure for handling one-time triggers. It also ensures recurring triggers can't possibly re-interrupt when a trigger's code is already in the callstack, because its; the return at the end of the currently executing trigger subroutine that causes a new instance of the trigger to get put into the list, if the trigger is a recurring one. Until that return happens, there isn't even a trigger in the CPU's lists for it anymore.
The other reason for implementing all triggers as one-shots (with recurring ones being essentially one-shots that schedule a new instance of themselves as they return), is so that when a bit of C# code is querying the kerboscript program for something (i.e. like VecDraw:VECUPDATER does), it can treat each call as a single path from "scheulded" to "executing" to "finished and here's the answer" without having to handle what happens when that trigger is called again. (essentially THAT trigger isn't called again. A new one cloned from it is.)
There was a problem hiding this comment.
I will leave that to your decision. The first version:
public void AddTrigger(TriggerInfo trigger)
{
bool immediate = trigger.IsImmediateTrigger && trigger.Priority >= CurrentPriority;
if (immediate)
Triggers.Add(trigger)
else if (! ContainsTrigger(trigger))
TriggersToInsert.Add(trigger);
}
fixes the first two points (callback from main or recurring trigger), but does nothing with the third (allowing recursion of callbacks).
I could have said that I am OK (not preferring but OK) with callbacks being executed inside recurring triggers without changing the priority, but I have definitely warned about immediate callbacks executed from main. Why not solve both at once, especially when I now see the code and that it is not so difficult to actually delay the callback when inside recurring trigger.
I am still not sure about the third point (prevent recursion) and it would obviously need more book-keeping (third list for running triggers - added here when removed from Triggers and removed from it in OpcodeReturn, probably). Then the check could be made like this:
public void AddTrigger(TriggerInfo trigger)
{
bool immediate = trigger.IsImmediateTrigger && trigger.Priority >= CurrentPriority;
if (immediate) {
if (! RunningTriggers.Contains(trigger)) Triggers.Add(trigger)
} else if (! ContainsTrigger(trigger))
TriggersToInsert.Add(trigger);
}
Assuming ContainsTrigger is unchanged (checking Triggers and TriggersToInsert as active triggers are still somehow pending, but passed the priority test, if I understand it correctly).
Maybe it should check for both RunningTriggers and Triggers when handling immediate. You know the infrastructure much better than me, you know I was not able to implement it.
Change #1: AddTrigger opcode will now treat steering lock triggers as the same if they come from the same point in the program, like they're supposed to (and like they used to do). My pilot.ks program was bogging down very very slowly, to about 3 FPS with the new interrupt priority code. I found the cause: When a program did ``lock throttle`` in a loop, like so: ``` until false { lock throttle to 1. wait 0. } ``` Then each time the "lock throttle" statement was visited, it would insert a NEW trigger for it because it failed to recognize that the previous one was already pending in the trigger list. (The Equals() test was calling it a new trigger, because it was getting a new instance ID in the OpcodeTrigger.Execute() logic. Because it wasn't "Equal" to the existing one, the uniqueness test for inserting a trigger failed to deny the insertion.) Therefore after the program runs a while instead of having like 1 throttle trigger there were hundreds of them, clogging the execution, and making the trigger logic itself rather slow (doing sequential walks through the trigger lists between each opcode with hundreds of triggers in the list instead of typically about 10-ish like there should be in a typical program). Change #2: The new logic on how triggers move from the active list to the callstack means they no longer execute in reverse order because they don't all get stacked up at once (instead they move over one at a time, waiting for the prev one to finish, unless a higher priority one is there that is supposed to stack on top). What this means is that when moving from the trigger list to the stack, it shouldn't be doing so in reverse order like it used to.
|
There has been a minor update to the branch (a new commit). Nothing to do with the comments you made - just some problems I found in one of my scripts highlighted an issue and I wanted to deal with that first before looking at what you wrote. |
|
Also Fixes #2278 |
The comparison used to see if a trigger was present to remove it was using the '==' operator instead of the Equals() method. This can cause C# to do a reference-equals test when what you wanted was the overridden meaning of Equals() defined for the class. (Because we keep making new instances of recurring triggers, this might have been preventing the trigger removal since the next instance to execute is a different instance from the previous one even though they are "Equal()" to each other.)
When doing the equality check it only tested for "zero" Instance count (meaning don't-care) on one side of the two operands. That would mean that aaa.Equals(bbb) gave a different answer from bbb.Equals(aaa), which is a non-intuitive messy situation.
Fixes #2239
Fixes #2272
Fixes #2278
(Not sure if it fixes 2280, so I won't tag it here.)
I believe this is a more complete solution to the problems of these issues: (Note this includes some of the code that started a branch that was mean to solve those issues). If this is accepted, it should supplant PR #2277.
To get the idea of the change, I recommend starting by reading the user docs changes in doc/source/general/cpu_hardware.rst before looking at the code. It should (hopefully) make the code make more sense.