Recently about Web Development
Looking through the web for standards on how to make your web pages more performant (i.e. using tools like YSlow!), one of the things that is mentioned is to put your css at the top and your javascript at the bottom. The reason to do this is so that your content will render faster (and be styled) and not have to wait for the parsing of the javascript in order to display.
Recently I have been getting into using jQuery UI a lot more. I like the common look and feel, the ability to theme it easily, and am excited for all of the new enhancements and widgets that are coming. The issue that I have found is that when jQuery UI initializes your widgets it adds the theming classes from the javascript. This causes the page content to render (in a very ugly unstyled fashion) and then it "pops" into the nice styled page. As a user also, I tend to hate this effect and find it very annoying. So I set out to see what I could do to fix this and still keep the site performant.
First attempt - pure CSS
At first I thought, well what if I just put the jQuery UI classes on the elements that I know will already receive those classes when jQuery UI ran? This worked well for some of the page but there were still issues:
- Some of the jQuery UI widgets add in extra DOM elements to accomplish the look they are going for, so this still had the jumping effect
- I was now bound to a specific version of jQuery UI because if they ever changed or tweaked their class names I would have to update all of my elements with the changes
- By adding the classes on the server I increased the page size and content that needed to be downloaded to the browser
Second attempt - rearrange the JS
Since the CSS way wasn't ideal and would cause a lot of maintenance I decided to see what would happen if I rearranged where I loaded the JS. I put all of the JS back in the head tag, so it would load (and parse) the JS before any content would display to the user. This solution was better, however, there was still a little jumping of the page before it was fully styled.
Third attempt - tweaking the JS calls
Finally, I took a look at how I was calling the jQuery UI widget initializers. I found that I was calling them inside of jQuery(window).load call. I then did some research and realized that the difference between jQuery(window).load and jQuery(document).ready is actually pretty significant if you have a lot of images. This is due to the fact that window load waits for the images to finish before being called where as document ready is called when the DOM is finished loading. Changing those calls to be inside of a jQuery(document).ready block did the trick and the page loads without any jumping.
So from this exercise I found that as the web world moves towards more and more javascript based UI's we will need to look more closely at these blanket performance statements and include usability in those measurements.
Extra nugget:
The pages in question above also load various content through ajax after the page loads. I found a small performance gain when I kept the jQuery UI initializers in the document ready block and moved the ajax calls into the window load block.
Today is the Near Infinity Spring Conference. We have one conference in the fall and one in the spring for all our developers as well as invited guests. Today I gave a presentation on CoffeeScript and shared the slides here.
Technologies used:
- Ruby on Rails
- mod_xsendfile for Apache2/Apache2.2
- ExtJs
- javascript
At a high level, the following happens:
1. Gather and send the GridPanel state/configuration to the controller.
2. The controller runs a query and generates Word XML based on the query results.
3. The Word XML file is presented to the user via an "Open/Save As" dialog using XSendFile.
Section 1: Getting the current state/configuration of the GridPanel
If you're familiar with ExtJs GridPanel's you know they can be configured by each user viewing the panel. Columns can be added, removed, grouped, shortened, lengthened and repositioned. To capture the GridPanel's current state and pass it to the exporter, I wrote the following javascript. It calculates and returns an array containing the column order, which column data indexes belong to which titles, the grouped field (if any), the sorted field, the sorted direction, and the widths of each column. You will see where this function is used when I define the GridPanel in the next section.
var prepareExport = function(gridStore, gridColumnModel) {
var sort = null;
var direction = null;
if (gridStore.getSortState()) {
sort = gridStore.getSortState().field();
direction = gridStore.getSortState().direction;
}
var columnOrder = []; //ordered list of columns reflecting the GridPanel's state
var columnsToTitles = {}; //map of ext field mappings to column titles
var columnsToWidths = {}; //width of each column
var totalWidth = 0;
var groupByField = gridStore.groupField; //the store's current grouping field ('false' if not grouped)
var storeReaderMapping = gridStore.fields.items;
//check to see if the groupField has a corresponding mapping
for (var i = 0; i < gridReaderMapping.length; i++) {
if (gridReaderMapping[i].name == gridStore.groupField && gridReaderMapping[i].mapping != null) {
groupByField = gridReaderMapping[i].mapping;
break;
}
}
//get the total width of the displayed columns (this will be used later when generating the Word XML)
gridColumnModel.getColumnsBy(function(c) {
if (!c.hidden) {
totalWidth = totalWidth + c.width;
}
});
//iterate over the grid's column model. For displayed columns, get the ext field names (mappings) and column titles
gridColumnModel.getColumnsBy(function(c) {
if (!c.hidden) {
for (var i = 0; i < gridReaderMapping.length; i++) {
if (gridReaderMapping[i].name == c.dataIndex && gridReaderMapping[i].mapping == null) {
columnOrder.push(gridReaderMapping[i].name);
var key = gridReaderMapping[i].name;
columnsToTitles[key] = c.header;
columnsToWidths[key] = c.width/totalWidth;
break;
}
else if (gridReaderMapping[i].name == c.dataIndex && gridReaderMapping[i].mapping != null) {
//if the name and mapping are different (i.e. a mapping exists), we need to push the mapping and not the name
columnOrder.push(gridReaderMapping[i].mapping);
var key = gridReaderMapping[i].mapping;
columnsToTitles[key] = c.header;
columnsToWidths[key] = c.width/totalWidth;
break;
}
}
}
});
return [columnOrder, columnsToTitles, groupByField, sort, direction, columnsToWidths];
};Section 2: The GridPanel
Here's a basic ExtJs GridPanel. Note the "exportButton" is defined in Section 3.
var gridReaderMapping = [
{name: 'id'},
{name: 'col_one'},
{name: 'col_two', mapping: 'some_mapping'},
{name: 'col_three'}
];
var gridColumnModel = new Ext.grid.ColumnModel({
defaults: {sortable: true},
columns: [
{header: 'ID', dataIndex: 'id'},
{header: 'Column One', dataIndex: 'col_one'},
{header: 'Column Two', dataIndex: 'col_two'},
{header: 'Column Three', dataIndex: 'col_three'}
]
});
var store = new Ext.data.GroupingStore({
proxy: new Ext.dataHttpProxy({
url: 'give/me/my/data'
method: 'GET'
}),
reader: new Ext.data.JsonReader({
root: 'data',
totalProperty: 'total',
messageProperty: 'message'
}, gridReaderMapping)
});
var grid = new Ext.grid.GridPanel({
id: 'gridpanel',
ds: gridStore,
cm: gridColumnModel,
sm: new Ext.grid.RowSelectionModel({
singleSelect: true
}),
tbar: [exportButton],
view: etc, etc, etc, etc...
});
Section 3: The Export Button
When the export button is clicked it gathers all the necessary data from the GridPanel (columns, widths, etc) and passes that data along with query parameters to the controller. The controller runs a query and generates the Word XML file. Section 4 describes what happens when the Ajax request returns the data successfully.
As I said at the beginning of this post, I will follow on with another post about the actual Word XML generation. The Word XML generation isn't too scary. If you're impatient, here are a few tips on Word XML generation.
1. You can start by creating a small Word XML file with a single table using MS Word or Open Office.
2. Open the XML file with the XML viewer/editor of your choice. Your table will be buried somewhere between <w:body> and </w:body>. All the other sections of the XML can remain the same.
3. You can create a series of templates for Word XML tables and rows based on the patterns you see in your Word XML sample.
4. In your Word XML generator, substitute values into these templates to build tables.
5. Insert the generated templates into a "master" template.
var exportButton = new Ext.Button({
renderTo: this.wrap,
listeners: {
click: function (node, event) {
var exportConfig = prepareExport(grid.getStore(), grid.getColumnModel());
Ext.Ajax.request({
url: 'url/to/query/for/data'
params: {
'columnsToTitles': Ext.util.JSON.encode(exportConfig[1]),
'columnsToWidths': Ext.util.JSON.encode(exportConfig[5]),
'columnOrder[]': exportConfig[0],
'groupByExport': exportConfig[2],
'groupBy': gridStore.groupField,
'groupDir': 'ASC',
'sort': exportConfig[3],
'dir': exportConfig[4],
'export': true
'query': build_some_query_params_to_pass_to_the_controller
},
success: function(response, opts) {
window.location.href = 'path/to/exporter_controller/export_word_XML'
},
failure: function(response, opts) {
//output error message
},
scope: this
});
}
}
});Section 4: Downloading the Word XML
Using XSendFile to prompt the user with an "Open/Save As" dialog box.
class ExportController < ApplicationController
def export_word_XML
filename = "#{X_SENDFILEPATH}/word_doc.xml"
if (File.exist?(filename))
send_file filename, :x_sendfile => true
else
render :nothing => true
end
end
end
Stay tuned for the upcoming Word XML generation blog post!
Environment Used
For this blog I'm using:- a T61 Thinkpad running Ubuntu 10.10(64Bit) and 4G ram
- Java JDK 1.6.0_21
- Glassfish Version 3. (I tried Tomcat 7 and Jetty 8, but had the best luck at this point with Glassfish)
DispatcherServlet Code
My first step was to extend Spring's DispatcherServlet.@WebServlet(urlPatterns = {"/async/*"}, asyncSupported = true, name = "async")
public class AsyncDispatcherServlet extends DispatcherServlet {
private ExecutorService executor;
private static final int NUM_ASYNC_TASKS = 15;
private static final long TIME_OUT = 10 * 1000;
private final Log log = LogFactory.getLog(AsyncDispatcherServlet.class);
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
executor = Executors.newFixedThreadPool(NUM_ASYNC_TASKS);
}
@Override
public void destroy() {
executor.shutdownNow();
super.destroy();
}
@Override
protected void doDispatch(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
final AsyncContext ac = request.startAsync(request, response);
ac.setTimeout(TIME_OUT);
FutureTask task = new FutureTask(new Runnable() {
@Override
public void run() {
try {
log.debug("Dispatching request " + request);
AsyncDispatcherServlet.super.doDispatch(request,response );
log.debug("doDispatch returned from processing request " + request);
ac.complete();
} catch (Exception ex) {
log.error("Error in async request", ex);
}
}
}, null);
ac.addListener(new AsyncDispatcherServletListener(task));
executor.execute(task);
}
The only methods overridden were init, destroy and doDispatch. I won't go into detail on init and destroy, what they do is obvious. All the interesting work is done in doDispatch. The doDispatch method starts the asynchronous request, then wraps the call to super.doDispatch in a runnable and passes that into an executor service. There are a few key points to consider here:
- The @WebServlet annotation at the class definition level. This is part of Servlet 3.0 specification, you can now declare servlets, filters etc via annotations, although you can still use web.xml. To enable asynchronous support set the 'asyncSupported' attribute to true.
- On line 22 the setting of a timeout for the asyncContext object. In this case the timeout is 10 seconds
- On line 38 setting an AsyncContextEventListener.
- The application server thread returns almost immediately.
Listener Code
Getting the request to run asynchronously is only half the battle. The other half is setting up hooks to handle different events during the life-cycle of the asynchronous request. The Servlet 3.0 spec added the AsyncListener interface. AsyncListener has 4 methods, onStartAsync, onComplete, onError and onTimeout. For the AsyncDispatcherServlet we have the inner class AsyncDispatcherServletListener that takes a FutureTask object as a constructor argument.private class AsyncDispatcherServletListener implements AsyncListener {
private FutureTask futureTask;
public AsyncDispatcherServletListener(FutureTask futureTask) {
this.futureTask = futureTask;
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
log.warn("Async request did not complete timeout occured");
handleTimeoutOrError(event, "Request timed out");
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
log.debug("Completed async request");
}
@Override
public void onError(AsyncEvent event) throws IOException {
log.error("Error in async request", event.getThrowable());
handleTimeoutOrError(event, "Error processing " + event.getThrowable().getMessage());
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
log.debug("Async Event started..");
}
private void handleTimeoutOrError(AsyncEvent event, String message) {
PrintWriter writer = null;
try {
future.cancel(true);
HttpServletResponse response = (HttpServletResponse) event.getAsyncContext().getResponse();
//HttpServletRequest request = (HttpServletRequest) event.getAsyncContext().getRequest();
//request.getRequestDispatcher("/app/error.htm").forward(request, response);
writer = response.getWriter();
writer.print(message);
writer.flush();
} catch (IOException ex) {
log.error(ex);
} finally {
event.getAsyncContext().complete();
if (writer != null) {
writer.close();
}
}
}
}
The onStartAsync and onComplete methods merely log a statement, but certainly could be used to open and close resources respectively. The only methods that do any work are onTimeout and onError, delegating to the handleTimeoutOrError method, passing a message and the AsyncEvent object. In handleTimeoutOrError we will call cancel on the futureTask object, write the message to the response stream, then mark the asyncContext as completed. While we are writing the error directly to the response stream, we could have just as easily forwarded to an error page by using the commented out call to request.getRequestDispatcher().forward (obviously you would eliminate lines 38-40).
Web Application Structure
This web application is very simple and has only two controllers - SimpleViewControler and SearchController.
@Controller
public class SimpleViewController {
@RequestMapping({"/","/index.htm"})
public String showHome(){
return "index";
}
@RequestMapping({"/error.htm"})
public String error(){
return "error";
}
@Controller
public class SearchController {
@RequestMapping("/search.htm")
public String doSearch(@RequestParam(value = "latency", defaultValue = "2000") long latency,
@RequestParam(value = "blowup", defaultValue = "false") boolean blowUp,
Model model) throws Exception {
String searchResult = getSearchResult(latency, blowUp);
model.addAttribute("result", searchResult);
return "searchResult";
}
@RequestMapping("/search.ajax")
public void doSearchAjax(@RequestParam(value = "latency", defaultValue = "2000") long latency,
@RequestParam(value = "blowup", defaultValue = "false") boolean blowUp,
HttpServletResponse response) throws Exception {
String searchResult = getSearchResult(latency, blowUp);
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print(searchResult);
writer.flush();
} finally {
if (writer != null) {
writer.close();
}
}
}
private String getSearchResult(long latency, boolean blowUp) throws Exception {
if (blowUp) {
throw new RuntimeException("Bad error happened in controller");
}
Thread.sleep(latency);
StringBuilder builder = new StringBuilder("Some search/whatever results being returned");
Date now = new Date();
builder.append(" @").append(now);
return builder.toString();
}
The latency and blowup parameters in SearchController are used to simulate different response times and errors respectively. Although there is the doSearchAjax method which writes directly to the response stream, in the tests that are run, we will only be using the doSearch method. There are the usual context files, which are very light due to the annotation configuration and a web.xml file (needed for the regular DispatcherServlet).
Testing
Now it's time to see if this experiment works at all. JMeter is a great tool and it is what I used to load test our simple web application. I have set up three tests.- A "control" test - There are two thread-groups consisting of 50 threads each and will ramp up to run all threads in 3 seconds. One thread group will make requests to /app/index.htm and the other thread group will make requests to /app/search.htm. The thread-groups will execute simultaneously and loop 3 times for a total of 300 requests. Each thread-group has a "listener" attached to it to measure throughput, and there is a listener attached to the test to measure overall throughput. This test will give us our baseline. The requests to /app/search.htm will not set any parameters, so each request will have the default value of 2 seconds for latency.
- The "asynchronous" test - This test will measure the effect of using asynchronous servlets in the application. Setup is identical to the control test above with one exception - the search requests will go to /async/search.htm and hit the AsynchronousDispatcherServlet.
- An error condition test - This test will be structured a little differently. The thread-group for /app/index.htm has a longer ramp up time, but will remain the same otherwise. The thread group for /async/search.htm will add a JMeter option known as a 'RandomController'. There will be 3 possible search requests sent, a valid request, a request with the latency parameter set to 12 seconds causing a timeout and a request with the blowup parameter set to true, so a RuntimeException will be thrown.
Test Results
|
|
|
||||||||||||||||||||||||||||||
As we can see from the test results, sending the search requests through the AsyncronousRequestDispatcher increased application throughput. The request per minute numbers don't mean that much though, given that the web application was so simple and the test was very contrived. What matters more is that the index requests had roughly the same response time and were seemingly unaffected when asynchronous support was used for the search requests
Summary
For me there were two main takeaways from this experiment:
- Even though asynchronous support seemed help with throughput, it is still using a thread pool which consumes server resources, so it should be only be applied to very select parts of an application.
- By setting timeouts and getting a chance to handle them gracefully via the event listener, asynchronous support acts a "circuit breaker" of sorts. This could be valuable when your application makes requests to outside resources that may be down or otherwise unresponsive.
Resources
Source for everything is available on github.
To run the JMeter tests
- Download JMeter and extract the tar/zip file to some directory
- Copy all of the *.jmx files in the jmeter directory from the github site for the code into <JMeter install>/bin. From the bin directory run jmeter or jmeter.bat depending on your platform. Once JMeter is up and running select File and you should see AsyncWebTestControl.jmx, AsyncWebTestErrors.jmx, AsyncWebTest.jmx in the File menu. Just click on one of those to open then Ctrl+r to run a test
- Download the war file and deploy to glassfish. I placed the war file in the autodeploy directory in glassfish. On my laptop it's in /usr/local/servers/glassfishv3/glassfish/domains/domain1/autodeploy.

