Thursday, 13 November 2014

GemStone based reports & views

My current project is a port of a VisualWorks & GemStone fat client application to Seaside. Part of the porting effort was to map a few thousand VW windowSpec views to a Seaside web views. It all works fine, but it's not ideal. Ported fixed layout views look like fat client windows; they lack a web aesthetic. We want all new views to be more 'web-centric', where positioning and sizing is adjusted by the browser, especially from tablets and mobile devices.

We also need to provide reports. For a web app, answering a PDF for a report works well.

We combined these two requirements and ended up with reports generated on GS using a Seaside-like coding pattern, which is then rendered in by Seaside in VW, and can be viewed as a PDF.

To build the reports we use Report4PDF, something I wrote a few years ago.  It uses PDF4Smalltalk to generate a PDF document. PDF4Smalltalk has not been ported to GemStone, something I'd like to do when time allows (and to VA & Pharo). Fortunately, Report4PDF generates intermediate output before requiring PDF4Smalltalk. This output can be created on GS, which is then moved to VW, where PDF4Smalltalk is used for the final output.

Our VW to GS interface uses only strings, either XML or evaluated command strings. In this case, the report objects are packaged as XML, and then recreated on VW. For most reports building and parsing the content takes about 200ms (we may move this to a command string, which is typically a third faster).

Once the report is in VW we use a 'report component' for the rendering, which reads the report content and builds the Seaside output. Because Report4PDF has a Seaside-like coding style, the mapping is relatively simple.

For example, a table is defined as...

aTable row: [:row | 
row cell: [:cell | cell widthPercent: 20. cell text bold; string: 'Job'].
row cell: [:cell | cell widthPercent: 30. cell text; string: self job description].
row cell: [:cell | cell widthPercent: 20. cell text bold; string: 'Our Job ID'].
row cell: [:cell | cell widthPercent: 30. cell text; string: self job id]].

...and gets rendered as...

...the PDF output is... build the PDF content we use the data already in VW.  No additional GS call is needed.

R4PObject, the root Report4PDF class, has a #properties instance variable to support extensions. We use this to add link and update capabilities to the report when it is rendered in Seaside.

For example, a link to another domain object is coded as...
row cell right bold string: 'Designer'.
row cell text normal string linkOop: self designer domainOop; string: self designer displayKeyString.

...and displayed as...

...but is ignored in the PDF output...

The beauty of this approach is that all of the report generation is done on GemStone, with generic rendering and PDF generation in our VW Seaside code.

Our users are happy with this approach. They like the look of the web rendered report and the option to get the content as a PDF. Having link and simple update capabilities means that most users will not need to use the old fat clients views, which tend to be used by power users, for data entry and for detailed updates.

Simple things should be simple. Complex things should be possible. - Alan Key

Thursday, 22 May 2014

Smalltalk performance measurement

The application I'm working on uses VisualWorks and GemStone. As we've built out our application and loaded more test data we find ourselves spending more time turning performance. If there is one thing I've learned over the years that is performance problems are never what they seem: always measure before you change the code. Premature optimization makes your code ugly and, more likely than not, adds no value.

On VW we use TimeProfiler & KaiProfiler, and on GS we use ProfMonitor. All are useful for getting a sense of where to look, after which we switch to more basic tools, like...

Time>>millisecondsToRun:, along with some convenience methods.

In VW you can use the Transcript to show performance measurements.
You could write something like...

Transcript cr; show: 'tag for this code'; show: (Time millisecondsToRun: [some code]) printString.

...but that's a pain. And you'd need the 'tag for this code' if you have several measurements spread throughout the code. To make that easier, we use...

'tag for this code' echoTime: [some code]

...which is implemented as...

echoTime: aBlock
| result microseconds | 
microseconds := Time microsecondsToRun: [result := aBlock value].
self echo: microseconds displayMicroseconds.

...the #echo: method is commented on in a previous post and #displayMicroseconds is just...

self > 1000 ifTrue: [^(self // 1000) displayTime].
^self printString, '┬Ás'

...and displayTime shows hh:mm:ss.mmm with hh and mm displayed if needed.

In GS you could use the VW transcript with a client forwarder, but our application uses a simplified GS interface model with only one forwarder and no replication (an XML string is the only returned value from GS), so adding a client forwarder was something I did not want to do. I also wanted to run some tests from a Topaz script.

Instead of a Transcript I use a String WriteStream held in a GS session variable and use Time class methods to add measurements and show the results. To measure a block of code, we add nested time measurements with...

Time log: 'tag for this code' run: [some code]

...and we wrap the top method send with...

Time showLog: [top method] 

...some methods get called a lot, so we'd like a total time. For that we use...

Time sum: 'tag for this method' run: [some code] 

...because each Time method answers the block result we can insert the code easily...

someValue := self bigMethod
someValue := Time log: 'bigMethod' run: [self bigMethod]

These are the methods...

Time>>showLog: aBlock
self timeSumDictionary: Dictionary new.
self timeLogStream: String new writeStream.
self timeLogStream nextPutAll: 'Time...'.
self log: 'time' run: aBlock.
^self timeLogStream contents , self displayTimeSums

...each time* variable is stored in the GS session array, like...

^System __sessionStateAt: 77

timeLogStream: anObject
System __sessionStateAt: 77 put:  anObject

log: aMessage run: aBlock 
"Time showLog: [Time log: 'test' run: [(Delay forSeconds: 1) wait] ]"
| result microseconds | 
microseconds := self millisecondsToRun: [result := aBlock value].
self timeLogStreamAt: aMessage put: microseconds.

timeLogStreamAt: aMessage put: anInteger
| stream | 
stream := self timeLogStream.
stream isNil ifTrue: [
stream := String new writeStream.
self timeLogStream: stream].
cr; nextPutAll: aMessage; tab; 
nextPutAll: anInteger displayTime.
self timeSumDictionaryAt: aMessage add: anInteger.

sum: aMessage run: aBlock 
"Time showLog: [
Time sum: 'test' run: [(Delay forSeconds: 1) wait].
Time sum: 'test' run: [(Delay forSeconds: 1) wait]]"
| result milliseconds | 
milliseconds := self millisecondsToRun: [result := aBlock value].
self timeSumDictionaryAt: aMessage add: milliseconds.

timeSumDictionaryAt: aKey add: aValue
| dictionary total | 
dictionary := self timeSumDictionary.
dictionary isNil ifTrue: [
dictionary := Dictionary new.
self timeSumDictionary: dictionary].
total := dictionary at: aKey ifAbsent: [0].
total := total + aValue.
dictionary at: aKey put: total.

Simple things should be simple. Complex things should be possible.