Now that we've learned about unit testing, let's discuss another important topic: performance. Your app needs to be responsive, or your users will simply not use it or will complain about it, which results in negative press for you or your company. The good news is that Esri has developed ArcGIS Runtime to be performant, both on the server side and the client side.
When speaking of the server side, we are referring to the fact that ArcGIS Runtime can use online content from a wide variety of sources. However, you as the developer can only control those web services that you have either developed yourself or use in your organization. As a result, we will limit our discussion to on-premises ArcGIS Server. We also won't discuss much about what we can do with ArcGIS Online sources such as basemaps, because we can assume that Esri scales it up as more users make requests for it. It is after all built on top of Amazon Web Services, which scales dynamically based on usage. We do, on the other hand, have some or complete control over an on-premises ArcGIS Server environment.
On the client side of things, we also have a great deal of control when working with the API. We can control what layers to display, how to display them, what fields to query, and so on. In this section, we're going to discuss most of the options that we have, in order to make our apps as responsive as possible while at the same time considering the costs associated with these choices. When we refer to costs, we're not just talking about monetary cost, we're also talking about cost in terms of battery usage, display modes, and so on.
If your app consumes online resources, you'll want to make sure those services are optimized for desktop and mobile users. Fortunately, there's a lot that you can do to optimize performance. In this section, we'll go over at a high level what can be done and where you can go to learn more about enhancing performance on the server side.
If your organization has deployed ArcGIS Server on-premises, you have the following options to make your apps perform well:
As shown here, you can specify the type of server, whether it's 2D or 3D, density, map complexity, percent cache, resolution, and output of services.
Geographic data is key to any mapping app so this section will list several techniques for making your app as fast as possible:
Please consult Esri's help for more details.
When it comes to ArcGIS Server, there are a few key tips to maximize performance:
Now that we've reviewed some things you can do on the server side, let's turn our attention to what we can do to make ArcGIS Runtime perform optimally. In order to discuss this subject, we're going to have to take a deeper dive into the rendering engine, and that means we will discuss Graphics Processing Unit (GPU), Central Processing Unit (CPU), and the decisions that one must make when deciding on how to render layers. Before we do that, however, we first need to understand a little about graphics programming APIs such as OpenGL and DirectX. ArcGIS Runtime SDK for .NET actually uses DirectX, but we're only going to discuss this at a high level, so these comments generally apply to OpenGL too.
When you add a layer to the map or scene using symbology individually or via a renderer, the layer is eventually rendered to the GPU. To understand how this works, we're going to talk very briefly about graphics programming. If you were to build your own mapping API, you would not only have to build your own data formats, tools, and so on, you'd also have to build a means to render this content to a GPU so that it renders it quickly.
This process works by taking the original Esri geometries (with symbology applied) you created in geographic space and converting them into screen space as vertices, as shown here in the first image, in the upper-left corner of this screenshot:
This presentation is the graphics pipeline. Note that these are vertices for graphics rendering to the GPU, which originally comes from the vertices we created with the Esri geometries. Once the vertices are in screen space, they are converted into triangles. It doesn't matter if the original geometry was an Esri point, line, or polygon, the symbol used to render them on the map is eventually converted into triangles. Here is an example of a polygon that has been converted into triangles:
Once it has been converted into a set of triangles, it is rasterized into pixels, and then the screen coordinates are converted into texture coordinates so that the original Esri symbol is displayed correctly on the GPU. Texturing is the process of converting graphic primitives, such as an Esri MapPoint
class with a symbol, into a picture that can then be rendered on the GPU. Even if the symbol for the polygon is a simple blue color, this requires a texture that appears blue to be applied to the rasterized triangles (pixels) so that they appear correct to the human eye. Coloring is then applied by the GPU. Once that is complete, the final rasterized image is sent to the frame buffer, where it is rendered to the screen using an OpenGL or DirectX call.
Instead of texturizing the graphics, a more recent advancement has been made, called path rendering. In a basic sense, path rendering is the process of taking text or a graphic and drawing an outline around it, as shown in the preceding screenshot. Path rendering is much more precise-looking than textures, because textures are just pictures while paths are detailed outlines of the text or graphic. See here:
The F letter on the left is a texture (picture) while the F letter on the right is a path (outline). The outlined F letter is much more precise-looking. Path rendering has many advantages and has been created from the preceding graphics.
One other concept that you need to understand is that when you move though the graphics pipeline, this requires changes in state. Every step requires the graphics API to switch modes of operation. Moving from creating triangles to building a texture requires a change in what the GPU is doing. The goal of any graphics engine developer is to reduce the number of state changes that occur because they degrade performance.
Another goal of building a rendering API is to reduce the number of passes that the API has to make when producing an image. Take, for example, a tiled basemap. If you recall from earlier discussions, a tiled service or package is made up of pre-generated tiles that are just images. From the perspective of OpenGL or DirectX, these are just textures, which are added to the display one tile at a time. The primary goal of using OpenGL or DirectX is to reduce the number of times these tiles have to be passed over along with other layers to create a final image. Remember that a nontrivial map will contain several layers that contain basemaps, operational layers, and dynamic layers, which we discussed in Chapter 1, Introduction to ArcGIS Runtime. See here:
As this basemap is generated, it requires that each tile is created as a texture and placed in its proper location. Each tile is added one at a time until the basemap is finished. Then, depending on how the other layers are configured, the remaining layers may be added while the basemap's tiles are added to the display, or they may not be added until another pass. It can take one or more passes to generate the final image that you actually see on the map. The Runtime Core gives you some control over how many passes it takes to generate an image.
For more information on how the graphics pipeline works with DirectX or OpenGL, see here:
https://msdn.microsoft.com/en-us/library/windows/desktop/ff476882(v=vs.85).aspx
Also see here:
ArcGIS Runtime comes with two ways of rendering your layers on the map or scene using the CPU or GPU:
Static rendering is the process of rendering when the user finishes an operation, such as zooming in. Static rendering is primarily done through the CPU. Dynamic rendering is the process of always rendering to the GPU. Each of these modes has pros and cons to consider. Static rendering does not occur until there is a context change, that is, the completion of an operation such as a pan or zoom. When that occurs, the symbols appear to pop. In other words, they abruptly change from one size to another very quickly during a zoom in operation. The zoom operation is being handled by the CPU, but the rendering is the responsibility of the CPU or GPU, depending on the mode. Once the CPU has completed performing the zoom operation, the CPU takes over in static mode or the GPU takes over in dynamic mode. In other words, the rendering occurs on demand, that is, when a zoom operation completes. When in dynamic mode, this popping sensation does not occur, because the GPU is always rendering the Esri geometry (there is no context change).
Static mode is all about cartographic quality, and that is achieved with path rendering. The entire graphic is rendered as a path. The other big advantage of static mode is that it scales very well. The Runtime Core only regenerates textures when it needs to, when a change occurs in a part of the map. As a result, increasing or decreasing the number of features or graphics doesn't influence performance. The downside of static mode is that it can be memory intensive because it is CPU bound.
For dynamic mode, cartographic quality is sacrificed for improved performance. The reason for this is that textures are used instead of paths. These textures reside in a texture mosaic, which would look something like this:
A texture mosaic is simply a list of textures that come from your symbols in your layers. The texture mosaic always exists on the GPU, which means less work for the CPU, and that means less battery drain. Because it resides on the GPU, textures are very quickly accessed.
To set the rendering mode for your layers, you simply set the rendering mode with RenderingMode
. Both feature layers and graphics layers support both static and dynamic mode. Tiled layers and map service layers only support static mode rendering, so the dynamic option isn't available for these layer types. When working in dynamic mode, the symbols are converted to graphic primitives, which are then converted into triangles and texturized, as we discussed in the previous section. These symbols are then rendered to the GPU. Static rendering converts the images to paths and renders them to the GPU.
When it comes to text and the labeling of layers, the Runtime Core uses path rendering instead of textures when in static mode. Starting with the 10.2.4 version of ArcGIS Runtime, the Runtime Core now uses texture fonts, which are stored in a texture mosaic on the GPU. Using this approach, the rendering engine can reuse letters on the texture mosaic.
Dynamic mode is best for rendering data during an animation or when navigating. This mode is excellent for tracking yourself using the built-in GPS because the system doesn't have to wait to render. That's because the graphics reside on the GPU at all times in a texture mosaic. As a result, the CPU isn't being used when panning, zooming, or rotating the map. This provides a smooth display experience, but it should be used sparingly when displaying a large number of features. For example, if you have 10,000 points on the map, 10,000 vertices have to be rendered to the GPU, which can result in display degradation. Furthermore, if the symbols are complex, this too can degrade performance. Along with this is that the cartographic quality isn't as good as it is with static mode. But the graphics are screen aligned so that if you rotate the map, the symbols and text stay aligned with how you set them, no matter the orientation of the map.
Static mode, on the other hand, is good for rendering lots of features on the map and achieving high cartographic quality (path rendering). But, because static mode relies more on the CPU, it consumes more of your battery and memory. Furthermore, if one graphic or feature changes, it will then require an update to an entire tile (tiled layer) or map image (dynamic map layer). Furthermore, if the map rotates, the graphics or text also rotate (they are not screen aligned).
So, which rendering mode should you use? Well, it depends on the balance between the number of graphics versus the GPU memory and power. Are you going to be offline? Is battery life a concern? Such similar factors need to be considered. It really comes down to the use case.
Static mode will create the same number of textures no matter the number of graphics, which consumes CPU cycles, which in turn burns battery power. Also, the number of layers and the order of the layers really play a role when in static mode. On the other hand, when in dynamic mode, graphics live in GPU memory all the time, so more graphics means more GPU processing power will be required, and that can result in a slower UI experience. This is especially the case if you have a large number of graphics and a slower GPU, such as on phones. The number of layers and their order is not as important as with static mode.
When in dynamic mode, the number and order of layers isn't that important because resources are shared between all layers on the GPU. It's important that the static layers are placed beside each other on the map, as this reduces the number of passes the rendering engine has to make. If you place a dynamic layer between two static layers, this requires a pass for all three layers. If you place the two static layers beside each other, only two passes are necessary. The reason for this is that the static mode layers don't share resources (textures). Even though the Runtime Core attempts to do some optimizations for you, it's important that you keep this in mind when adding layers to the map.
If a graphics or feature layer contains polygons, static mode is best for feature services if you don't know how many features there are, the kind of symbols used, or the geometry complexity. On the other hand, dynamic mode is good for sketching or interactive layers, real-time feeds, and point layers.
Now that we've covered some of the basics of how Runtime Core's display rendering engine works, let's consider how we can optimize graphics layers. Here are some recommendations:
List<T>
) when populating a GraphicsLayer
class.Using an ArcGIS Runtime renderer, on the other hand, only requires one state change if there is only one symbol used to make the renderer. As we will see, using a renderer will be much faster than adding graphics individually.
GraphicsLayer
class. However, if you add points and polygons to the same graphics layer, that means that the display engine has to switch states, which reduces performance. It switches states because it has to draw the points, generate textures, draw the polygons, generate the textures, and so on.Let's now test some different ways of adding graphics to a map so that you can see some differences in performance, depending on how you add the graphics to the GraphicsLayer
class. Look at the project called Chapter11c
with the code that came with this book. First, let's add 50,000 graphics individually to a map, using a for
loop:
// get the graphics layer GraphicsLayer graphicsLayer = MyMapView.Map.Layers["graphicsLayer"] as GraphicsLayer; this.random = new Random(); // create a stopwatch Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // create a bunch of graphics for (int i = 0; i < 50000; i++) { int latitude = random.Next(-90, 90); int longitude = random.Next(-180, 180); MapPoint mapPoint = new MapPoint(longitude, latitude); SimpleMarkerSymbol sms = new SimpleMarkerSymbol(); sms = new Esri.ArcGISRuntime.Symbology.SimpleMarkerSymbol(); sms.Color = System.Windows.Media.Colors.Red; sms.Style = Esri.ArcGISRuntime.Symbology.SimpleMarkerStyle.Circle; sms.Size = 2; Graphic graphic = new Graphic(mapPoint, sms); graphicsLayer.Graphics.Add(graphic); } // stop timing stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Console.WriteLine("Time elapsed: {0}", stopWatch.Elapsed);
Give this code a go and see how long it takes. Basically, it will add 50,000 points to the map in random locations using latitude/longitude.
Now that we've done this the slow way, let's try a couple of faster approaches. In the next example, let's add 50,000 graphics using a generic list that then has the graphics added to the GraphicsLayer.GraphicSource
property:
// get the graphics layer GraphicsLayer graphicsLayer = MyMapView.Map.Layers["graphicsLayer"] as GraphicsLayer; this.random = new Random(); // create a stopwatch Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // create a enerable list List<Graphic> graphics = new List<Graphic>(50001); // create a bunch of graphics for (int i = 0; i < 50000; i++) { int latitude = random.Next(-90, 90); int longitude = random.Next(-180, 180); MapPoint mapPoint = new MapPoint(longitude, latitude); SimpleMarkerSymbol sms = new SimpleMarkerSymbol(); sms = new Esri.ArcGISRuntime.Symbology.SimpleMarkerSymbol(); sms.Color = System.Windows.Media.Colors.Red; sms.Style = Esri.ArcGISRuntime.Symbology.SimpleMarkerStyle.Circle; sms.Size = 2; Graphic graphic = new Graphic(mapPoint, sms); graphics.Add(graphic); } graphicsLayer.GraphicsSource = graphics; // stop timing stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Console.WriteLine("Time elapsed: {0}", stopWatch.Elapsed);
Once you run this example code, you will see that it loads the graphics faster than the previous example, because the graphics are being assigned to the layer as a source directly.
One last example will illustrate adding the graphics using a simple renderer:
// get the graphics layer GraphicsLayer graphicsLayer = MyMapView.Map.Layers["graphicsLayer"] as GraphicsLayer; this.random = new Random(); // create a stopwatch Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); SimpleMarkerSymbol sms = new SimpleMarkerSymbol(); sms = new Esri.ArcGISRuntime.Symbology.SimpleMarkerSymbol(); sms.Color = System.Windows.Media.Colors.Red; sms.Style = Esri.ArcGISRuntime.Symbology.SimpleMarkerStyle.Circle; sms.Size = 2; SimpleRenderer simpleRenderer = new SimpleRenderer(); simpleRenderer.Symbol = sms; // create a enerable list List<Graphic> graphics = new List<Graphic>(50001); // create a bunch of graphics for (int i = 0; i < 50000; i++) { int latitude = random.Next(-90, 90); int longitude = random.Next(-180, 180); MapPoint mapPoint = new MapPoint(longitude, latitude); Graphic graphic = new Graphic(mapPoint); graphics.Add(graphic); } graphicsLayer.Renderer = simpleRenderer; graphicsLayer.GraphicsSource = graphics; ; // stop timing stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Console.WriteLine("Time elapsed: {0}", stopWatch.Elapsed);
Using the laptop this book was written with, the times for each approach are summarized here:
Added graphics using |
Time (in seconds) |
---|---|
Individually ( |
0.1866448 |
|
0.1078491 |
|
0.0429644 |
These are very fast times, because the laptop used has a high-end graphics card and processor. You may need to reduce the number of graphics when you try it on your PC or laptop. Regardless, from these simple changes we can easily see that when we change from adding the graphics individually to adding them with GraphicsSource
, the time it takes to render them decreases by about 42 percent. When we go from GraphicsSource
to using SimpleRenderer
, the decrease in time is about 60 percent. When we go from adding graphics individually to a GraphicsLayer
class to using SimpleRenderer,
the time it takes to render the graphics decreases by about 77 percent. That's outstanding! You will of course get different results, depending on the symbology, number of graphics, and hardware (RAM, processors, and graphics card).
When it comes to working with layers, there are a few other tips you can use to improve performance:
Query.OutFields.Add
to *
is fine for a layer with a small number of fields, but using this approach with a large number of fields will degrade performance.OutputLines
property on OnlineRouteTask
. Setting this option can reduce the response size of the data.InitialViewpoint
to that area. This can reduce the amount of data coming across the Internet. Note that it's not possible to do this with Windows Store and Phone via XAML; it can be done with code, however.