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 <hls...@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-unsubscr...@tapestry.apache.org For additional commands, e-mail: users-h...@tapestry.apache.org