You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tapestry.apache.org by Howard Lewis Ship <hl...@gmail.com> on 2010/07/15 01:50:33 UTC

Re: [Tapestry Central] Everyone out of the Pool! Tapestry goes singleton!

Read it with formatting:
http://tapestryjava.blogspot.com/2010/07/everyone-out-of-pool-tapestry-goes.html

On Wed, Jul 14, 2010 at 4:30 PM, Howard <hl...@gmail.com> wrote:
> 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



-- 
Howard M. Lewis Ship

Creator of Apache Tapestry

The source for Tapestry training, mentoring and support. Contact me to
learn how I can get you up and productive in Tapestry fast!

(971) 678-5210
http://howardlewisship.com

---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscribe@tapestry.apache.org
For additional commands, e-mail: users-help@tapestry.apache.org