For my postgrad dissertation I’ve decided to program an engineering design tool. I’d like to focus more on the application domain rather than the programming minutiae. Based purely on gut feel from a long-held but unproven belief, I’ve selected Smalltalk as the development platform. Specifically I’ve selected Squeak with its Morphic graphics system, which seems to have a lot of useful “direct manipulation” functionality. Now some validation of this choice would be useful and one place to start is performance testing.
Herbert Konig on [squeak-dev] suggested:…as soon as possible calculate how many Morphs you want to be on the screen and respond to events (like changing a wire number). Then just crate that number of Morphs at random positions. !!! Be sure to use a throw away image !!!. Years ago I tried with 30000 points (EllipseMorphs) and a few thousand lines (PolygonMorphs) which rendered the image unresponsive. Computers and VM’s became faster but I assume there are still limits.
So based on part of a sample electrical connection diagram…
…my back of envelope calculation for the number of morphs is:
1000 == 2 x terminal strips x 100 terminals x 5 columns (core,ferrule,term,link,core)
2400 == 2 x 100 terminals x 4 wires x 3 line segments (two wires per terminal per side)
400 == wire cores identifiers
20 == 20 x 1 cable label per each 20 core cable
500 == miscellaneous border and drawing title info
==> ~4500. Double it for safety ==> ~10,000 morphs. This is for an individual drawing. For an industrial plant the number of drawings numbers in the thousands.
I developed the following code to show the performance of EllipseMorphs creation. I’ve used variables to contain blocks of code to make the main loop more concise.
"Morphic Performance Testing v1" rand := Random new. creating := [ m := EllipseMorph new. m color: Color random. m position: ActiveWorld extent * ((rand next)@(rand next)). m openInWorld. m goBehind. "ActiveWorld doOneCycle." ]. deleting := [ EllipseMorph allInstances do: [ :item | item delete ]]. hiding := [ EllipseMorph allInstances do: [ :item | item hide ]]. showing := [ EllipseMorph allInstances do: [ :item | item show ]]. #( 1 10 100 200 300 400 500 600 700 800 900 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 ) do: [ :amount | createTime := [ amount timesRepeat: creating ] timeToRun. hideTime := [ hiding value ] timeToRun. showTime := [ showing value ] timeToRun. deleteTime := [ deleting value ] timeToRun. Transcript show: 'amount=', amount asString, ' createTime=', createTime asString, ' hideTime=', hideTime asString, ' showTime=', showTime asString, ' deleteTime=', deleteTime asString; cr]
These times were using vanilla Squeak 4.2 All-in-One.app. Two morph creation times were considered. A fresh image was unzipped for each run.
The first creation time createTime(gui lockup) was the result of the code executed exactly as above. The creation of so many morphs did indeed lock up the GUI while the code was executing. However after that the interactivity returned to normal. Any ellipse could be picked up and moved around with no perceptible delay. (This of course required the delete block to be commented out so morphs remained to interact with.)
Up to a certain delay, that initial GUI lock up might be considered standard operation for a file load. Interestingly, the deleteTime(gui lockup) for the morphs takes longer than creating them. I am speculating that has something to do with required garbage collection.
It can be seen that hideTime and showTime are much faster than create and delete. An alternative to “loading” a file might be for the morphs to exist permanently in the image a hidden onscreen and just toggle their visibility. I wonder about keeping a standard set of morphs onscreen and swapping out the underlying object model per drawing. I’ll have to learn a bit more to decide how that might be best achieved.
The temporary gui lockup seemed due to the event processing loop not running. This is common for long running calculations in a single threaded application. The standard solution is to periodically invoke event processing from within the calculation. Rummaging through the system code I found my way to “ActiveWorld doOneCycle.” You can see this line commented out in the code above. By uncommenting that line, interactivity during morph creation was absolutely perfect during morph creation! Not a hint of gui lockup! However the trade-off was greatly increased overall createTime(doOneCycle). The original deleteTime(gui lockup) was no different from deleteTime(doOneCycle), so only the first is shown.
Perhaps using doOneCycle that way is a bit naïve – I’m open to suggestions. Some off the cuff alternatives are:
- Balance morph creation into batches.
- See if Morphic can track the overall interactivity during any instance creation.
- See if Morphic event checking can be timer interrupt driven.
- Run the morph creation in another thread using ‘fork.’ I had tried that however and the experience was a bit weird.
Update 18 March 2011
On [squeak-dev] Juan Vuletich wrote:
That code is good for benchmarking, but not for a real app. These minor tweaks allow opening a lot (really a lot) of morphs under one second.
"Morphic Performance Testing v2" rand := Random new. creating := [ m := EllipseMorph new. ellipses add: m. m color: Color random. m position: ActiveWorld extent * ((rand next)@(rand next)) ]. deleting := [ World removeAllMorphsIn: ellipses]. hiding := [ ellipses do: [ :item | item hide ] ]. showing := [ ellipses do: [ :item | item show ] ]. Transcript show: 'Morphic Performance Test Code v3.0 - ##FLAVOUR## '; cr. Transcript show: 'amount, createTime, hideTime, showTime, deleteTime'; cr. #( 1 10 100 200 300 400 500 600 700 800 900 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 30000 100000) do: [ :amount | ellipses := OrderedCollection new. createTime := [ amount timesRepeat: creating. World addAllMorphs: ellipses. ] timeToRun. hideTime := [ hiding value ] timeToRun. showTime := [ showing value ] timeToRun. deleteTime := [ deleting value ] timeToRun. Transcript show: amount asString, ', ', createTime asString, ', ', hideTime asString, ', ', showTime asString, ', ', deleteTime asString; cr. ]
In fact, those tweaks enable dealing with an order of magnitude more morphs (thx Juan.) The creation time for 10,000 morphs went from 10 seconds to 0.9 seconds. Alternatively in 10 seconds, 90,000 more morphs can be created. This is reflected in the updated graph below as createTime(addAllMorphs). Deletion of morphs is a massive two orders of magnitude faster, as seen by deleteTime(addAllMorphs). Can you see more?