[Vm-dev] An event driven Squeak VM

Igor Stasenko siguctua at gmail.com
Tue Nov 10 21:39:20 UTC 2009


2009/11/10 Andreas Raab <andreas.raab at gmx.de>:
>
> Folks -
>
> I had an interesting thought today that I'd like to run by you because I
> think it might just work. I have been thinking for a long time how to make
> the Squeak VM be "truly event driven" that is invoke it in response to OS or
> other events instead of having the VM poll. There are lots of good reasons
> for this starting from not blocking when popping up an OS context menu or
> standard dialog, over being able to embed the VM into other apps (browser
> plugin etc), up to properly dealing with suspend/resume. There are various
> problems that could be dealt with more easily if the VM would be truly event
> driven.
>
> Today it occurred to me that there might be a relatively simple way to deal
> with that problem merely by having interpret() run until "there is no more
> work to do" and return from interpret() when it's all said and done. The
> trick is that instead of running the idle loop, the VM would determine that
> it has no more work to do when there is no runnable process, so when it
> finds that there is no runnable process it would return from interpret
> saying "my work's done here, there is no more code to run at this point, ask
> me again when an external event comes in".
>
> The changes would be fairly straight forward: First, nuke the idle loop and
> allow wakeHighestPriority to return nil when there's no runnable process.
> Second, have transferTo: do a longjmp to the registered vmExitBuf to leave
> interpret(). Third, have interpret register the vmExitBuf and wake up the
> highest priorty process like here:
>
> interpret
>    "install jmpbuf for main interpreter"
>    (self setjmp: vmExitBuf) == 0 ifTrue:[
>        self checkForInterrupts. "timers etc"
>        "transferTo: longjmps if arg is nil so no need to check"
>        self transferTo: self wakeHighestPriority.
>
>        "this is the current interpret() implementation"
>        self internalizeIPandSP.
>        self fetchNextBytecode.
>        [true] whileTrue: [self dispatchOn: currentBytecode in:
> BytecodeTable].
>
>    ].
>
> At this point we can write a client loop that effectively looks like:
>
>  /* run the interpreter */
>  while(!done) {
>    /* check for new events */
>    ioProcessEvents();
>    /* run processes resulting from the events */
>    interpret();
>  }
>
> Now, obviously this is inefficient, we'd want to replace the
> ioProcessEvents() call with something more elaborate that reacts to the
> incoming OS events, takes the next scheduled delay into account, checks for
> socket handles etc. But I'm sure you're getting the idea. Instead of wasting
> our time in the idleProcess, we just return when there's no more work to do
> and it's up to the caller to run interpret() as often or as rarely as
> desired.
>
> I also think that this scheme could be made backwards compatible by ensuring
> that we never call interpret() recursively. In this case an "old" image with
> the idle process would run the way it does today, and a "new" image without
> the idle process would live in the shiny new event driven world and return
> as needed.
>
> What do you think? Any reasons why this wouldn't work?
>
First, a big +1 :)

But i don't like using setjmp/longjumps and instead it would be good
to have a way to tell to return normally.

I did this in Hydra, by using:

internalQuickCheckForInterrupts
	"With new event-driven architecture in HydraVM, here we need to check
	if there any event in event queue, and if it there, return from
interpret() function"

	self inline: true.

	(self ioIsQueueEmpty: eventQueue cReference) ifFalse: [
		self externalizeIPandSP.
		self returnFromInterpret.
	]

where #returnFromInterpret is a macro:

#define returnFromInterpret() return

of course, it should be used only inside intepret() function,
otherwise it will return from something else with unpredictable
results :)

It could be generalized by adding 'needToLeaveInterpret' flag so, in
the code above it won't check the event queue (since Squeak VM doesn't
have one), but this flag instead. Then any primitive could tell to
leave the interpret() when its needed (#forceInterruptCheck comes to
mind), and since each primitive call followed by
#internalQuickCheckForInterrupts, we can be sure that we are indeed
return from interpret() at safe point and without need in using
longjumps.
Take a look at senders of #internalQuickCheckForInterrupts in Hydra VMMaker.

And indeed, an outer loop which calling interpret() looking similar to
what you proposed:

interpretLoop
	[true] whileTrue: [
		self interpret.
		self unlockInterpreter. "give a chance other threads to do something with us"
		self lockInterpreter.
		self handleEvents.
	]

Another thing, in Hydra , a VMMaker code never calls ioProcessEvents directly.
Instead i'm using a native thread, which runs in background and
periodically producing an event , which when handled, calls
ioProcessEvents(), but this logic is totally out of control of core
interpreter and platform specific.
Also, same loop cares about producing a delay semaphore signaling.

On platforms, which not support threading, i suggest just change the
#checkForInterrupts, and instruct to leave interpret() function when
counter goes zero, by setting the 'needToLeaveInterpret' flag. Then, a
platform-specific code could call ioProcessEvents() or whatever it
likes to, in the loop which enclosing the call to intepret().

Apart from this, lies the callbacks support. I prefer the function
which expects a callback to be called outside interpret().
Something like following:

needToLeaveInterpret := 1.
interpreterProxy callThisFunctionWhenOutsideOfInterpret: #myFunc

and myFunc() calls something which expecting the callback:

myFunc() {

  result = callSomeExternalFunction(mycallback);

}

mycallback( x, y ,z )
{
/*  ... convert/preserve passed-in arguments or whatever needed */
....
/*  here , we are outside the 'interpret()' but inside some
platform-specific interpreterLoop();
  lets call special interpreterLoopInsideCallback() , which returns
when user code calls leaveCallback()...*/

 interpreterLoopInsideCallback();

/*  ... convert return value  or whatever needed */
 return someResult;
}

and voila, no recursive calls to interpret(), and no need in setjmp/longjump :)

P.S. one of the major reasons to leave interpret() and do something
outside of it, that we are much safer at this point to call any
platform-specific code or external functions without any risk that VM
state (like context/process/GC) will be damaged or handled improperly.
We will benefit much from such separation.

> Cheers,
>  - Andreas
>
>



-- 
Best regards,
Igor Stasenko AKA sig.


More information about the Vm-dev mailing list