Optimizing Practical Map Server


Introduction

This document discusses recent optimizations to the map server to improve its performance and stability. It focuses primarily on the request pipeline for the MapViewer applet.

Conversion to Servlet Container

The original map server ran on its own multi-threaded, minimalist HTTP server. The profiler indicated two major problems problems with the implementation of this server:

  1. It spawned a new thread for every request. Requests that encountered errors during processing were not exiting properly. These "orphaned" threads and their associated data were piling up over time, leading the VM to eventually run out of memory.
  2. It parsed templates with a StreamTokenizer for every request to the HTML-based interface, generated tens of thousands of String objects for every request.

The conversion to a standard Servlet container such as Tomcat resolved both these issues. All modern Servlet containers manage a limited pool of threads, rather than spawning a new one for each request. Also, it allowed the map server pages to be reimplemented as JSP, in which the page is only parsed and compiled once.

Database Connection Pooling

The original map server used its own simple database connection pooling scheme. While the link was never firmly established, it appears to be the case that the map server could fail to lose track of which connections were being used, leading the map server to accumulate database handles over time.

The move of Scorecard to Oracle 8.1.7 provided the opportunity to use the recently available connection pool from Oracle itself. In addition, database connections are linked to threads in the map server, rather than to individual requests. In combination with thread pooling by the application server, this should drastically reduce the chance that the server can accumulate database connections (sustained production use over a period of time will be necessary to guarantee this).

Overview of MapViewer Request Processing

Most requests to the map server originate from the MapViewer applet running in a client browser. These requests are dispatched to the pms.server.MapApplet Servlet, which orchestrates the construction of a data bundle for returning to the applet. The data bundle includes a map image as well as text data for each map layer (All maps consist of a set of layers, such as water features, county boundaries, census tracts, cities, etc.). The data bundle has the following general structure:

  1. Map Image
  2. Data for First Layer
  3. Data for Second Layer
  4. Data for Third Layer
  5. ...

The minimal data for each map layer consists of a set of properties or key-value pairs used to provide the applet with legend text. If the layer is interactive, the layer properties also specify how to apply feature data to support dynamic labels, charting and reports.

If the layer is interactive, its data must include a feature table that provides the geometric coordinates and relevant attributes of each feature. The attributes are used for drawing charts and labels and providing report links to each feature. The coordinates are used to track mouse movements over the map image and ensure that dynamic features are kept in sync with the position of the mouse.

Handling MapViewer Requests

When the pms.server.MapApplet servlet receives a request, its has to perform the following tasks:

  1. convert the request parameters into a pms.Carta object that specifies the map to draw. This step takes a negligible amount of time (less than a millisecond).
  2. prepare feature data and render each map layer. This step takes up most of the time (greater than 80%) for each request and is discussed in detail below. The time varies widely depending on the number and complexity of the layers involved, but averages about 2 seconds.
  3. prepare the data bundle for the response, including exporting the image as a GIF and the writing the layer data as described above. This step takes about 150 to 200 milliseconds, depending on the size of the image and the number of visible features. Most of this time is spent inside the GIF conversion routine.

Preparing Feature Data and Rendering Layers

To complete a map request, the server prepares and renders each map layer in succession, as if a person were drawing map features onto sheets of transparent plastic and then stacking the sheets to produce a finished composite map. As mentioned above, this "stacking" procedure occupies most of the time.

The steps involved in processing each layer are as follows:

  1. Open a shape file and related index files. (Map features are not cached in memory for two major reasons: (a) disk I/O does not appear to be the major performance bottleneck compared to graphics rendering; and (b) the amount of memory required to cache a large data set would be prohibitive.
  2. Select features in the visible extent of the map. The selection process is highly optimized by use of a tesselation or 2-dimensional index, which looks up map features based on their overlap with the cells of a grid.
  3. If the source of attribute data is an RDBMS, then execute a query and fetch the results into a temporary attribute table. Note that the results must be fetched in advance, because the order in which features are rendered on the map may not correspond to the order in which they are returned from the database. The query is a simple primary key lookup for the set of visible features and is therefore relatively efficient.
  4. Loop over the selected features. For each feature, the following steps must be performed:
    1. Retrieve the geographic coordinates of the map feature from the shape file (point, line or polygon).
    2. If the map layer is associated with dynamically displayed data (pop-up labels on the map or associated charts), copy the relevant values from the attribute table.
    3. If the map layer is associated with static labels (such as city names), read the label value and add a label to the label layer. This step involves complex calculations to ensure that labels are displayed as legibly as possible, and has been optimized for maximum efficiency.
    4. Render the feature to the image buffer using an appropriate graphics symbol.

For most layers, the layer-rendering process has been optimized to the point where 60% to 80% of the time is spent within Java2D graphics API methods, beyond the scope of the map application itself. Further work in this area is unlikely to yield significant additional performance improvements.

Preparing the Server Response

Once the map has been drawn to the image buffer and any associated dynamic data has been prepared, the final step is to prepare the data stream that the server will send back to the client. The stream consists of a combination of binary data (the GIF image of the map) and character data (text and numbers for dynamic labels and charts). This step represents less than 20% of the time required to handle a typical request.

To improve server responsiveness, the character data is compressed prior to being written out the client, thus reducing the amount of data that needs to be transferred over a potentially slow connection (compression reduces the size of the character data stream by 90% or more). Because compression is a CPU-intensive process, it imposes a slight performance penalty. However, the tradeoff was deemed worthy because of the overall improvement in server responsiveness by decreasing transmission time.



Last modified: $Id: optimize.html,v 1.1 2001/08/19 20:42:55 karl Exp $