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.
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:
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.
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).
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:
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.
When the pms.server.MapApplet
servlet receives a
request, its has to perform the following tasks:
pms.Carta
object that specifies the map to draw. This step takes a
negligible amount of time (less than a millisecond).
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:
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.
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.