Tapestry applications are inherently stateful: during and between
requests, information in Tapestry components, value stored in fields,
stick around. This is a great thing: it lets you program a web
application in a sensible way, using stateful objects full of mutable
properties and methods to operate on those properties.
It also has its downside: Tapestry has to maintain a pool of page
instances. And in Tapestry, page instances are big: a tree of hundreds
or perhaps thousands of interrelated objects: the tree of Tapestry
structural objects that forms the basic page structure, the component
and mixin objects hanging off that tree, the binding objects that
connect parameters of components to properties of their containing
component, the template objects that represents elements and content
from component templates, and many, many more that most Tapestry
developers are kept unawares of.
This has proven to be a problem with biggest and busiest sites
constructed using Tapestry. Keeping a pool of those objects, checking
them in and out, and discarded them when no longer needed is draining
needed resources, especially heap space.
So that seems like an irreconcilable problem eh? Removing mutable state
from pages and components would turn Tapestry into something else
entirely. On the other hand, allowing mutable state means that
applications, especially big complex applications with many pages,
become memory hogs.
I suppose one approach would be to simply create a page instance for
the duration of a request, and discard it at the end. However, page
construction in Tapestry is very complicated and although some effort
was expended in Tapestry 5.1 to reduce the cost of page construction,
it is still present. Additionally, Tapestry is full of small
optimizations that improve performance ... assuming a page is reused
over time. Throwing away pages is a non-starter.
So we're back to square one ... we can't eliminate mutable state, but
(for large applications) we can't live with it either.
Tapestry has already been down this route: the way persistent fields
are handled gives the illusion that the page is kept around between
requests. You might think that Tapestry serializes the page and stores
the whole thing in the session. In reality, Tapestry is shuffling just
the individual persistent field values in to and out of the HttpSessio.
To both the end user and the Tapestry developer, it feels like the
entire page is live between requests, but it's a bit of a shell game,
providing an equivalent page instance that has the same values in its
fields.
What's going on in trunk right now is extrapolating that concept from
persistent fields to all mutable fields. Every access to every mutable
field in a Tapestry page is converted, as part of the class
transformation process, into an access against a per-thread Map of keys
and values. The end result is that a single page instance can be used
across threads without any synchronization issues and without any
conflicts. Each thread has its own per-thread Map.
This idea was suggested in years past, but the APIs to accomplish it
(as well as the necessary meta-programming savvy) just wasn't
available. However, as a side effect of rewriting and simplifying the
class transformation APIs in 5.2, it became very reasonable to do this.
Let's take an important example: handling typical, mutable fields. This
is the responsibility of the UnclaimedFieldWorker class, part of
Tapestry component class transformation pipeline. UnclaimedFieldWorker
finds fields that have not be "claimed" by some other part of the
pipeline and converts them to read and write their values to the
per-thread Map. A claimed field may store an injected service, asset or
component, or be a component parameter.
public class UnclaimedFieldWorker implements
ComponentClassTransformWorker { private final PerthreadManager
perThreadManager; private final ComponentClassCache classCache; static
class UnclaimedFieldConduit implements FieldValueConduit { private
final InternalComponentResources resources; private final
PerThreadValue<Object> fieldValue; // Set prior to the
containingPageDidLoad lifecycle event private Object fieldDefaultValue;
private UnclaimedFieldConduit(InternalComponentResources resources,
PerThreadValue<Object> fieldValue, Object fieldDefaultValue) {
this.resources = resources; this.fieldValue = fieldValue;
this.fieldDefaultValue = fieldDefaultValue; } public Object get() {
return fieldValue.exists() ? fieldValue.get() : fieldDefaultValue; }
public void set(Object newValue) { fieldValue.set(newValue); // This
catches the case where the instance initializer method sets a value for
the field. // That value is captured and used when no specific value
has been stored. if (!resources.isLoaded()) fieldDefaultValue =
newValue; } } public UnclaimedFieldWorker(ComponentClassCache
classCache, PerthreadManager perThreadManager) { this.classCache =
classCache; this.perThreadManager = perThreadManager; } public void
transform(ClassTransformation transformation, MutableComponentModel
model) { for (TransformField field :
transformation.matchUnclaimedFields()) { transformField(field); } }
private void transformField(TransformField field) { int modifiers =
field.getModifiers(); if (Modifier.isFinal(modifiers) ||
Modifier.isStatic(modifiers)) return;
ComponentValueProvider<FieldValueConduit> provider =
createFieldValueConduitProvider(field);
field.replaceAccess(provider); } private
ComponentValueProvider<FieldValueConduit>
createFieldValueConduitProvider(TransformField field) { final String
fieldName = field.getName(); final String fieldType = field.getType();
return new ComponentValueProvider<FieldValueConduit>() { public
FieldValueConduit get(ComponentResources resources) { Object
fieldDefaultValue = classCache.defaultValueForType(fieldType); String
key = String.format("UnclaimedFieldWorker:%s/%s",
resources.getCompleteId(), fieldName); return new
UnclaimedFieldConduit((InternalComponentResources) resources,
perThreadManager.createValue(key), fieldDefaultValue); } }; } }

That seems like a lot, but lets break it down bit by bit.
public void transform(ClassTransformation transformation,
MutableComponentModel model) { for (TransformField field :
transformation.matchUnclaimedFields()) { transformField(field); } }
private void transformField(TransformField field) { int modifiers =
field.getModifiers(); if (Modifier.isFinal(modifiers) ||
Modifier.isStatic(modifiers)) return;
ComponentValueProvider<FieldValueConduit> provider =
createFieldValueConduitProvider(field); field.replaceAccess(provider); }
The transform() method is the lone method for this class, as defined by
ComponentClassTransformWorker. It uses a method on the
ClassTransformation to locate all the unclaimed fields. TransformField
is the representation of a field of a component class during the
transformation process. As we'll see it is very easy to intercept
access to the field.
Some of those fields are final or static and are just ignored. A
ComponentValueProvider is a callback object: when the component
(whatever it is) is first instantiated, the provider will be invoked
and the return value stored into a new field. A FieldValueConduit is an
object that takes over responsibility for access to a TransformField:
internally, all read and write access to the field is passed through
the conduit object.
So, what we're saying is: when the component is first created, use the
callback to create a conduit, and change any read or write access to
the field to pass through the created conduit. If a component is
instantiated multiple times (either in different pages, or within the
same page) each instance of the component will end up with a specific
FieldValueConduit.
Fine so far; it comes down to what's inside the
createFieldValueConduitProvider() method:
private ComponentValueProvider<FieldValueConduit>
createFieldValueConduitProvider(TransformField field) { final String
fieldName = field.getName(); final String fieldType = field.getType();
return new ComponentValueProvider<FieldValueConduit>() { public
FieldValueConduit get(ComponentResources resources) { Object
fieldDefaultValue = classCache.defaultValueForType(fieldType); String
key = String.format("UnclaimedFieldWorker:%s/%s",
resources.getCompleteId(), fieldName); return new
UnclaimedFieldConduit((InternalComponentResources) resources,
perThreadManager.createValue(key), fieldDefaultValue); } }; }

Here we capture the name of the field and its type (expressed as
String). Inside the get() method we determine the initial default value
for the field: typically just null, but may be 0 (for a primitive
numeric field) or false (for a primitive boolean field).
Next we build a unique key used to store and retrieve the field's value
inside the per-thread Map. The key includes the complete id of the
component and the name of the field: thus two different component
instances, in the same page or across different pages, will have their
own unique key.
We use the PerthreadManager service to create a PerThreadValue for the
field.
Lastly, we create the conduit object. Let's look at the conduit in more
detail:
static class UnclaimedFieldConduit implements FieldValueConduit {
private final InternalComponentResources resources; private final
PerThreadValue<Object> fieldValue; // Set prior to the
containingPageDidLoad lifecycle event private Object fieldDefaultValue;
private UnclaimedFieldConduit(InternalComponentResources resources,
PerThreadValue<Object> fieldValue, Object fieldDefaultValue) {
this.resources = resources; this.fieldValue = fieldValue;
this.fieldDefaultValue = fieldDefaultValue; }

We use the special InternalComponentResources interface because we'll
need to know if the page is loading, or in normal operation (that's
coming up). We capture our initial guess at a default value for the
field (remember: null, false or 0) but that may change.
public Object get() { return fieldValue.exists() ? fieldValue.get() :
fieldDefaultValue; }

Whenever code inside the component reads the field, this method will be
invoked. It checks to see if a value has been stored into the
PerThreadValue object this request; if so the stored value is returned,
otherwise the field default value is returned.
Notice the distinction here between null and no value at all. Just
because the field is set to null doesn't mean we should switch over the
the default value (assuming the default is not null).
The last hurdle is updates to the field:
public void set(Object newValue) { fieldValue.set(newValue); // This
catches the case where the instance initializer method sets a value for
the field. // That value is captured and used when no specific value
has been stored. if (!resources.isLoaded()) fieldDefaultValue =
newValue; }

The basic logic is just to stuff the new value into the PerThreadValue.
However, there's one special case: a field initialization (whether it's
in the component's constructor, or where the field is defined) turns
into a call to set(). We can differentiate because that update occurs
before the page is marked as fully loaded, rather than in normal use of
the page.
And that's it! Now, to be honest, this is more detail than a typical
Tapestry developer ever needs to know. However, it's a good
demonstration of how Tapestry's class transformation APIs make Java
code fluid; capable of being changed dynamically (under carefully
controlled circumstances).
Back to pooling: how is this going to affect performance? That's an
open question, and putting together a performance testing environment
is another task at the top of my list. My suspicion is that the new
overhead will not make a visible difference for small applications
(dozens of pages, reasonable number of concurrent users) ... but for
high end sites (hundreds of pages, large numbre of concurrent users)
the avoidance of pooling and page construction will make a big
difference!

--
Posted By Howard to Tapestry Central at 7/14/2010 04:30:00 PM

Reply via email to