Skip to content

Interrupt priorities#2296

Merged
erendrake merged 13 commits intoKSP-KOS:developfrom
Dunbaratu:interrupt_priorities
Jul 27, 2018
Merged

Interrupt priorities#2296
erendrake merged 13 commits intoKSP-KOS:developfrom
Dunbaratu:interrupt_priorities

Conversation

@Dunbaratu
Copy link
Member

@Dunbaratu Dunbaratu commented Jun 26, 2018

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.

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.
@ghost
Copy link

ghost commented Jun 27, 2018

This looks really good.

Let me ask you few questions / describe few observations to be sure I undrestood it well:

1. Immediate callbacks = InterruptPriority.NoChange.
These are true callbacks (synchronous, result of instruction in the script, e.g. set button:pressed to true)
and are identified by Widget.Communicate which sets guiCaused to true on entrance and (back) to false before exiting. (guiCaused being false makes the callback immediate.) This fixes #2239

2. All other GUI callbacks are InterruptPriority.CallbackOnce.
These are one-shot and can interrupt main code only and can be interrupted by InterruptPriority.Recurring (steering, vector drawing etc.), thus addressing the main problem of running e.g. launch script from inside onClick - #2280

Ignore the rest if the above is not true. If the above is true, then
A. Clicks do not interrupt other callbacks, thus running script from onClick handler means blocking all GUI, both the GUI executing the callback (e.g. some ABORT button - won't respond) and any potential GUI created inside the script (no response to clicks).

B. Changing suffix with callback assigned from InterruptPriority.Recurring will result in calling that callback on that top priority immediatelly.

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 if (guiCaused || context.CurrentPriority > InterruptPriority.CallbackOnce) _Event_.TriggerOnFutureUpdate(InterruptPriority.CallbackOnce...

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 run from Note to A (special command for lowering priority would be nice, altering run is not a good idea).

@Dunbaratu
Copy link
Member Author

Note to B: Can probably be fixed

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 InterruptPriority.NoChange be a thing.

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.

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.

@ghost
Copy link

ghost commented Jun 28, 2018

And I thought you don't like run levels (interrupt priorities) and specificaly wrote that changing pressed from main loop would make the event handler interruptible if you do not rise its run level, which is even bigger problem than not lowering it to GUI-level if executed from top priority trigger. I think that CallbackOnce should always be executed on that run level / interrupt priority, no matter what.

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.

@Dunbaratu
Copy link
Member Author

I think that CallbackOnce should always be executed on that run level

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));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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).
  2. set button:pressed to true. from recurring trigger should delay the execution after all top-priority triggers are finished.
  3. set button:pressed to true or more common set editBox:text to "something" from inside the very callback that is handling the change, should, probably, do nothing. This is about the ContainsTrigger check.

P.S.: InterruptPriority.NoChange won't be a thing after this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. guiCaused callbacks happen asynchronously together with setting the property. You can execute set 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 and onTrigger never notified you about it. Ignoring current callback when scheduling pending should fix the situation.
  2. 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).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/Dunbaratu/KOS-1/blob/3edfc4294cceb47876c11193e5e1db92760b0ede/src/kOS.Safe/Execution/CPU.cs#L1434

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)

Copy link
Member Author

@Dunbaratu Dunbaratu Jun 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Dunbaratu
Copy link
Member Author

Dunbaratu commented Jul 1, 2018

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.
If you want to know why the changes were done, they're explained in the commit comment.

@Dunbaratu
Copy link
Member Author

Also Fixes #2278

Dunbaratu added 2 commits July 6, 2018 22:22
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.
@Dunbaratu Dunbaratu added this to the v1.1.6.0 milestone Jul 22, 2018
@erendrake erendrake merged commit b11a1ba into KSP-KOS:develop Jul 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants