Evaluating Continuations for Wee in Ruby 1.9
The last couple of days I spent refactoring my web application framework Wee. Wee is a Seaside-like framework for Ruby which I started back in 2004 with a lot of mental help from Avi Bryant who is one of the main guys behind Seaside. It’s now approaching a 2.0 release.
Wee is now fully Rack based (Rack is a commonly used webserver interface for Ruby) and the code is in general even cleaner than it was before as huge parts were rewritten. Plus one cool feature: continuations.
Continuations
Yesterday I finally thought it’s time to put back in continuations mainly because of some very interesting developments that happened in Ruby 1.8.x which seem to fix memory leaks related to the use of continuations.
Continuations in Wee were never used as extensively as they were used in Seaside. And it’s interesting to read that Seaside 2.8 reduced heavy usage of continuations – something that Wee did from the beginning :)
An example says more than thousands words, so here we go:
require 'wee'
class Page < Wee::Component
def initialize
add_decoration Wee::PageDecoration.new('Title')
super
end
def render(r)
r.anchor.callback {
if callcc YesNoMessageBox.new('Really delete?')
callcc InfoMessageBox.new('Deleted!')
else
callcc InfoMessageBox.new('Deleted action aborted')
end
}.with("delete?")
end
end
class InfoMessageBox < Wee::Component
def initialize(msg)
@msg = msg
super()
end
def render(r)
r.h1(@msg)
r.anchor.callback { answer }.with('OK')
end
end
class YesNoMessageBox < InfoMessageBox
def render(r)
r.h1(@msg)
r.anchor.callback { answer true }.with('YES')
r.space(1)
r.anchor.callback { answer false }.with('NO')
end
end
Wee.runcc(Page)
The interesting part of this example is the anchor tag callback handler of
component Page
. When clicked it will display a YES/NO message box which will
return true if you clicked on YES, otherwise false. When it returns, execution
resumes exactly at the point after the callcc
call. In fact, callcc returns
the return value of the called component. A called component returns by
calling the answer
method (it behaves mostly like the regular return
statement). The concrete mechanism how this all works out is a bit more
complicated as it involves installing and removing several decorations to
delegate rendering and catch exceptions upon answer, but this is totally
unrelated to continuations.
So how would it look without the use of continuations? Well, the callback handler would turn from:
if callcc YesNoMessageBox.new('Really delete?')
callcc InfoMessageBox.new('Deleted!')
else
callcc InfoMessageBox.new('Deleted action aborted')
end
into:
call YesNoMessageBox.new('Really delete?') do |res|
if res
call InfoMessageBox.new('Deleted!')
else
call InfoMessageBox.new('Deleted action aborted')
end
end
Actually in this concrete example it’s not hard to rewrite the code without using continuations. But for more complex examples it would get much worse. For example, using continuations, a simple sequential flow like the following one:
callcc page1
callcc page2
callcc page3
would turn into the much less readable equivalent using continuation passing style (CPS):
call page1 do
call page2 do
call page3
end
end
Which one would you prefer? The good thing: In Wee you can use both, that’s why
I have the two methods call
and callcc
, the latter not to mix up with
Kernel.callcc
.
Performance and Memory Usage
The reason why continuations were basically unsupported in Wee for a very long time was that they leaked memory.
Since a few months patches circulate on the ruby-core
mailing list that seem
to fix those leaks. Actually the reason seams to be that some parts of the
stack are not overwritten when calling a function and as such old values keep
referenced: a leak is born!
I haven’t tried those patches, but I think they work. But I tried Ruby 1.9.1, and hey, it’s incredible! Memory usage stays constant at 12 MB, regardless of the number of requests. And performance scales nearly linearily as I increase the number of threads.
For 10000 requests (with one thread), Ruby 1.9.1 takes 16 seconds and requires 12 MB of memory. The same example with Ruby 1.8.7 grows to 329 MB of memory and takes 56 seconds. That’s an increase in performance of factor 3.5 and a 27-fold reduction of memory.
The example I’m using for the benchmark is the following 2-level nested callcc component call:
class Benchmark < Wee::Component
#
# calls Called2 then returns
#
class Called1 < Wee::Component
def render(r)
r.anchor.callback { callcc Called2.new; answer }.with('back')
end
end
class Called2 < Wee::Component
def render(r)
r.anchor.callback { answer }.with('back')
end
end
def render(r)
r.anchor.callback { callcc Called1.new }.with("click")
end
end
Conclusion
Continuations seem to be stable enough and not too expensive in terms of memory and performance in Ruby 1.9 so that there is no longer a reason against using them (wisely) within Wee. This makes Wee the only web framework for Ruby to my knowledge that uses continuations.
Coupled with other great features provided by Wee, for example reusable components, backtracking or the programmatic HTML generation, this undoubtly makes Wee one of the most advanced web application frameworks for Ruby.
It is worth noting that Wee does not focus on RESTful, high-throughput applications. Instead, Wee focuses on very complex applications, similar to as found in traditional GUIs. And to get the job done quick and beautifully.