Fast Web Editing
I've talked about performance considerations for distributed apps across a (relatively) slow network in a blog about nimble clients in the RD Traveler community, but I wanted to cover it here too, from a slightly different angle.
A little over a year ago, I started working with a couple other folks on building something new - a web-based UI that would enable people to connect to a mainframe through a web browser. We weren't even sure if it would work, so we took a very lean, agile approach to this, in case it was something we had to abandon. My biggest technical concern: performance. Would it be fast enough?
BUT - you have to be careful. It's really important to think about the following things (and probably a dozen others):
- What is the minimal data for creating a 'grid' for display? (i.e. synchronous request to the server)
- What data can we pull asynchronously to augment the display, but isn't required immediately? (i.e. asynchronous request to the server)
- How can we keep the interfaces RESTful? (i.e. complex state information doesn't need to be managed between the UI and server so it has some hope of working 2 weeks from now)
So - to be more concrete, let's consider a couple examples:
For our application, we create a 'Navigator' view of datasets (MVS files) as a starting point. It has to be really fast. The native app (ISPF - which is 'vi for a mainframe user') creates a 'flat' view of the datasets that match a particular pattern. That works fine when it's all running on a green screen interface, where all you need to do is flow 3270 screen displays across the wire, but it doesn't work at all well if you need to send thousands of lines of data to the UI before you can display the data. Consider a 'flat' view of all the datasets that match: 'FULTONM.JUNIT.**' (which is similar to listing, recursively, all directories under /home/fultonm on a Unix system):
You can see there are 18 datasets listed of a possible 65. Paging up and paging down in ISPF works well - it processes the request on the mainframe, generates the 3270 display, and sends the 3270 screen over the wire. The simple (but ineffective) approach to providing a web UI would be to flow the entire set of 65 datasets to the browser and then have it display what it needs in a grid. For 65 datasets, it's probably not too bad. But - it's not uncommon to have filters that might match thousands of datasets. At best, the interface would be slow. At worst, it could cause out-of-memory issues in one or both of the server and browser.
We tackled this two-fold. We re-organized the RESTful service so that the UI requested a range of datasets (in a particular sorted order) from the resource server. We default to 200 datasets which is reasonably fast, but doesn't require data from the server every time the customer pages up or down to see a neighbouring screen. This creates a responsive interface requiring only a few thousand bytes to be sent over the wire. The second thing we do is re-organize how the datasets are displayed to create a virtual hierachical display (similar to a traditional file system like Unix), which provided another boost both in terms of usability and performance. The result is:
With this approach, the initial request for data is still small (about 25 entries on the right-hand panel), but with the more hierachical nature of display the datasets can be reached with a handful of clicks rather than paging down or searching for strings to find the right dataset as with ISPF). This results in a more efficient way to access the mainframe datasets both from a raw performance perspective as well as how long it takes for the customer to locate what he needs.
Reading and Writing Files
The next function to consider is editing files. Files can be big on a server. Really big. Again, a native app doesn't suffer much from data transmission - it only has to send screens of data over the wire. For reasonable sized files (less than a dozen megabytes or so), we download the entire file to the web browser, which does take up to a couple seconds. That's annoying - we can probably do better. But - even worse would be if everytime the customer changed a couple of lines in a file and saved it, they had to wait those couple seconds. Instead, we transform the request. There is no reason to upload the entire file when a save is done. All that is required is that the difference from the original be computed and sent from the browser to the mainframe. So - that's what we do. In practical terms, it works really well. It's very often the case that an update to a file may only be a few lines being added, deleted, or changed in a file, even when the file is several thousand lines long. To maintain data integrity, we flow the timestamp of the file on the read, and then compare it on the update to ensure no one has updated the file independently of us.
But what about those really big files I mentioned in passing - ones that are more than a dozen megabytes. Right now, we punt if someone wants to edit them. BUT - we do let people browse the file (i.e. open it up in read only mode), and it's really fast... At least, as fast as the mainframe can read the file on the server.
When someone browses a really big file, we don't read the entire file. We read the first hundred lines or so and display it in a grid (like we did for the datasets previously), but we make the grid look just like an editor window, and it behaves like an editor window (except you can't save the file). We broke down the services for browse so that we pull what we need. If the customer pages down past what is already in memory, we grab another hundred lines or so from the mainframe. These requests are fast over the network because we are only pulling a few thousand bytes to populate the grid. The grid has an LRU cache so that it doesn't use too much memory if significant browsing is performed. Similarly, the customer can goto a particular line, and we'll again request that range of the file from the mainframe for display, ending up with a sparsely populated grid being displayed.
A common need is to go to the end of the file and search backwards because often these massive files are server logs and they are continually being appended to. To make this work, we request, asynchronously, the file size when we first start browsing the file (because it can take a couple seconds to calculate the number of lines of a really big file, even on a fast mainframe).
That brings up the final complexity. People need to be able to search in the file. Forwards or backwards. The way we do this is to again send the request to the mainframe and request a small result set for a particular find request. So, if I am on line 25020, and want to find the next occurrence of 9:21:33, we send that request up to the mainframe and it returns a concise set of column/row pairs back for the next few hits of that string from that starting line (or a maximum number of lines searched - whichever comes first).
I've touched on a couple of the interesting services, but we did this same work for all the services that we provide in our web UI. Gathering data for processes is done in ranges, our shell command interpreter reads output in chunks to send back to browser for display, lists of files are done in ranges as with datasets, long running operations are broken into pieces so that the customer can cancel them if they make a mistake.
It's an exciting area of work that demands more than just slapping a new UI on front of existing code. To make it really hum requires stepping back, understanding usage patterns, recognizing where bottleknecks can occur in high performance operations, and optimizing.