Wednesday, February 3, 2010

MapViewer performance tuning tips - part 3

Middletier (MapViewer) side of things

Lets now look at various factors within MapViewer itself that may affect overall performance. There are also settings in the J2EE container level that can affect both performance and scalability, which we will also touch upon later.

Types of map requests

In order to properly tune MapViewer performance and size your hardware for deployment, you need to first understand where and how MapViewer spends its resources. Because MapViewer resources are primarily spent on processing map requests, it is critical to understand the types of requests that MapViewer may receive.

XML map request

The most basic type of request is simply a single XML document containing a complete map request posted to a MapViewer server’s URL directly. In this request doc, you can specify a base map, one or more additional themes (pre-defined or dynamic), a map query window, and a response format, among many other things. When MapViewer receives such a request, it breaks down the basemap (if specified) into a list of composing themes, and then adds those additional themes to form a final theme list. It then queries and fetches data for all the themes on the list, rendering their data into one or more images. The result images are either directly streamed back to the client, or saved to the disk (in which case it is the URLs to the saved image files that are sent back to the client).

WMS requests

MapViewer can also act as a WMS (Web Mapping Service) server. When it receives an incoming WMS request, the request is first converted into an XML map request, followed by the same workflow as if the client had just sent the XML request directly.

Oracle Maps requests

With the Oracle Maps JavaScript API, a typical “map” displayed by your application is often composed of one or more map tile layers, plus one or more Feature Of Interest (FOI) layers. Whenever such a map is displayed or refreshed, the Mapviewer server will receive one FOI request for each visible FOI layer, plus multiple map tile image requests.

For each FOI request, you can think of it as a simplified XML map request containing just a single theme. Once the features of this theme are loaded, MapViewer can either render them into individual FOI images (one small image for each feature), or combine all the features into a single image equal to the size of the application map window (when the whole-image option is enabled on the FOI layer).

For each map tile request, if the tile (an image file stored on the server) does not already exist, MapViewer will need to generate it on the fly, by issuing itself an XML map request that contains just the name of the base map of the tile layer. The query window of this XML map request will be the data coverage area for this particular tile. The normal workflow then follows, and the result image is always stored to a specific folder on the server. It then serves this tile image file to the client.

Note that once a map tile is generated and saved, all subsequent requests to that tile can be fulfilled extremely quickly, because MapViewer no longer needs to process/render it. Serving 100s of tile images per second is a typical performance. Whether your browser can load and display that many images quickly enough however is another matter and depends on such factors as your network connection speed.

Because on the fly rendering of map tiles is expensive, you should consider tile pre-fetching by issuing an admin request to MapViewer when it is not under load (or on a non-production server). When MapViewer receives a pre-fetching request, it simply issues many concurrent map tile requests to itself, which are basically XML map requests as mentioned earlier.

JavaBean API requests

If your application uses the JavaBean API of MapViewer, what happens is that as soon as you call oracle.lbs.mapclient.MapViewer’s run, pan or zoom method, it will construct a XML request document based on the information you have provided (name of basemap, themes you added, et al), and send it to the MapViewer server for processing.

In the end it's all XML

So as you can see, no matter which API your application uses, all that the MapViewer rendering engine sees are XML map request docs. From now on we will shorten the verbose “XML map request doc” to just “map request”. Note that client side map actions (clicking, zooming, panning et al) may or may not result in a map request to the server. And sometimes one action on the client side may even lead to multiple map requests (such as when zooming an Oracle Maps map with multiple visible FOI layers).

While the structure of MapViewer’s XML map request document can be very complex (the DTD for which can be found in the MapViewer User Guide), for simplicity we can think of it simply as a flat list of themes that MapViewer needs to process. Sometimes this list contains just one theme, and yet other times it may contain dozens or even hundreds of themes.

Given the above understanding, let’s now look at how MapViewer processes the themes for a single map request.

Rendering themes of a map request

When MapViewer processes a theme, it begins by issuing a query to the database to load the data for that theme (for Spatial based themes anyway). This query typically contains a spatial filter so that it only fetches data within the current viewing window. In the first two posts we have already covered how to speed up this phase by reducing the database time.

Note that the rendering phase does not actually start until all the themes’ loading phases have completed. In other words, if your map request contains 10 themes, the rendering phase will not start until all 10 themes have finished loading their data from the database. In other words, all the data for all the themes will be held in memory until the map request itself is completed. This is why it is important to consciously limit the number of features MapViewer might load for any given theme. When a map request is completed (the final image rendered and saved or streamed back to the client), then all the loaded theme data and temporary shapes et al. are released from memory.

During the actual rendering process, MapViewer iterates through all the loaded features of each theme, converting each geometry object from its raw database format into a Java representation when necessary, followed by the creation of a Java2D shape or point. Any necessary viewport transformation is also carried out at this stage.

If a theme requires text annotation, then the labeling process occurs after all the themes have completed the shape-rendering phase. Note that rendering follows the order of themes as they are listed in the map request, but for labeling the order is actually reversed. In other words, the last theme in the list is always labeled first.

To summarize, the total time (excluding the database time) MapViewer spends on processing a single map request is affected by the following factors, roughly in decreasing order of significance:

  • The number of features loaded by each theme, and total number of features loaded.
  • The cost of rendering and labeling a single feature; this is in turn dictated by how complex each rendering and labeling style is, how detailed (in terms of number of vertices) are the geometries on average, et al.
  • The raw speed of the server’s disk, CPU and memory access
  • Whether anti-aliasing is requested
  • The amount of heap memory allocated to MapViewer
  • The version of JDK being used

Note that the first item (number of total features loaded) also has the biggest impact on MapViewer’s memory usage.

While understanding how MapViewer works on a single map request certainly helps, in a real world scenario your MapViewer instance is often flooded with many concurrent map requests initiated by a variety of applications and clients using different APIs. In order to improve the scalability of MapViewer and properly size its hardware platform, one needs to understand how MapViewer handles concurrent map requests. This is discussed next.

Database sessions, number of mappers and concurrent map requests

Prior to MapViewer version, themes in a single map request load their data in parallel. In other words, if a map request is composed of 10 themes, then 10 database connections are obtained right off the bat, each executing the query of one theme. This “liberal” usage of database sessions often leads to scalability issues while providing marginal if any performance gain.

Starting with MapViewer version, SQL execution of themes’ queries are carried out in a serial fashion within each map request. A theme cannot start loading its data until the theme before it in the list has finished its query execution and data loading. The end result is that only one database connection is ever needed when processing a map request, no matter how many themes may be involved.

For a given data source, the maximum number of concurrent map requests MapViewer will accept is determined by the number of mappers specified for that data source. In other words, if a data source’s number_of_mapper attribute is set to 10 (in the MapViewer config file), then a maximum of 10 concurrent map requests can be processed (for that data source) at any given time. Because each map request will use one database connection, there can be a maximum of 10 active database sessions for the data source schema. When the 11th map request arrives it must wait in the queue until a free mapper becomes available.

You may recall that in MapViewer’s data source definition, there is also a max_connection parameter. Starting with version, this parameter is no longer needed actually. This is because the maximum number of connections out there for a given data source is always bound by the number_of_mapper parameter.

Understanding the above, you may come to the conclusion that in order to increase the scalability you simply increase the number of mappers specified for a data source. The problem is that as you increase this number, you are also increasing the number of potential concurrent database sessions, the memory usage inside MapViewer and the amount of raw data it needs to push through its rendering pipeline. Finally, if you have multiple data sources defined, then the total concurrent map requests that can potentially arrive at the MapViewer is the sum of all the data sources' maximum concurrent requests.

In the end, it is difficult to provide a clear-cut formula for determining what level of hardware you will need for your MapViewer instances. For instance, to determine roughly how much heap memory you need to allocate to a MapViewer instance, you need to look through each defined data source, finding out the average number of features each theme will load into memory and their geometric complexity, the number of themes in a typical map request, and the maximum concurrent map requests you wish to sustain. You also need to factor in things such as the in-memory geometry cache which are permanent. The best approach is to simply perform load tests against your application and monitor various logs and stats from your database and MapViewer instances to see if they are being saturated.

In the next post we will dig in further and provide a number of practical tips on tuning MapViewer performance.