Export (0) Print
Expand All

Chapter 8 — Smart Client Application Performance

 

patterns & practices Developer Center

Smart Client Application Architecture and Design Guide

David Hill, Brenton Webster, Edward A. Jezierski, Srinath Vasireddy, Mohammad Al-Sabt, Microsoft Corporation, Blaine Wastell Ascentium Corporation, Jonathan Rasmusson and Paul Gale ThoughtWorks, and Paul Slater Wadeware LLC

July 2004

Related Links

Microsoft® patterns & practices Library http://msdn.microsoft.com/en-us/practices/default.aspx

Application Architecture for .NET: Designing Applications and Services http://msdn.microsoft.com/en-us/library/ms954595.aspx

Summary: This chapter discusses how to optimize your smart client for applications. It examines steps you can take at design time, and looks at how you can tune your smart client applications and diagnose any performance issues.

Contents

Designing for Performance
Performance Tuning and Diagnosis
Summary
References

Smart client applications can provide a richer and more responsive user interface than Web applications can, and can take advantage of local system resources. If a large portion of the application resides on the user's computer, the application does not require constant round trips to a Web server. This can result in an increase in performance and responsiveness. However, to realize the full potential of a smart client application, you should carefully consider performance issues during the application's design phase. Addressing performance issues when you architect and design your application can help you contain costs early and reduce the likelihood of running into performance problems later on.

Note   Improving the performance of smart client applications is not limited to application design issues. There are a number of steps that you can take throughout the application lifecycle to make .NET code perform well. Although the .NET common language runtime (CLR) is very efficient at executing code, there a number of techniques that you can use to increase the performance of your code and prevent performance problems from being introduced at the code level. For more information on these issues, see http://msdn.microsoft.com/en-us/library/ms973838.aspx.

Defining realistic performance requirements and identifying potential issues in the design of your application is clearly important, but often performance problems appear only after the code has been written, and is being tested. In this case, there are tools and techniques that can help you track down performance problems.

This chapter examines how to how to design and tune your smart client applications for optimum performance. It discusses a number of design and architectural issues, including threading and caching considerations, and examines how to enhance the performance of the Windows Forms portions of your application. The chapter also looks at some of the techniques and tools that you can use to track down and diagnose performance problems with your smart client applications.

Designing for Performance

There are many things you can do at an application design or architectural level to ensure that a smart client application performs well. You should make sure to set realistic and measurable performance goals as early as possible in the design phase, which allows you to evaluate design tradeoffs and provide the most cost effective way to address performance issues. Wherever possible, performance goals should be based on real user and business requirements because these are strongly influenced by the environment in which your application operates. Performance modeling is a structured and repeatable process you can use to manage and ensure your application meets the performance goals. For more information, see Chapter 2, "Performance Modeling" in Improving .NET Application Performance and Scalability, at http://msdn.microsoft.com/en-us/library/ms998537.aspx.

Smart clients are usually part of a larger distributed application. It is important to consider the performance of the smart client application in the context of the complete application, including all of the network-located resources that the client application uses. Fine tuning and optimizing every single component in an application is usually not required or possible. Instead, your performance tuning should be based on priorities, time, budget constraints, and risks. Pursuing high performance for its own sake is not usually a cost-effective strategy.

Smart clients will also need to coexist with other applications on your user's computers. As you design your smart client applications, you should take into account the fact that your applications will need to share system resources such as memory, CPU time, and network utilization with the other applications on the client computer.

Note   Information concerning the design of scalable, high performance remote services can be found in Improving .NET Performance and Scalability, at http://msdn.microsoft.com/en-us/library/ms998530.aspx. The guide contains detailed information about how to optimize your .NET code for best performance.

To design smart clients to perform efficiently, consider the following:

  • Caching data where appropriate. Data caching can dramatically improve the performance of a smart client application, allowing you to work with data locally rather than having to retrieve it from the network constantly. However, data that is sensitive or changes frequently is not usually appropriate for caching.
  • Optimizing network communications. Communication through chatty interfaces to remote tier services with multiple request/response round trips to perform a single logical operation can consume system and network resources, resulting in poor application performance.
  • Using threads efficiently. If you use a user interface (UI) thread to perform blocking I/O bound calls, the UI may seem unresponsive to the user. Creating a large number of unnecessary threads can result in poor performance because of the overhead of creating and shutting down threads.
  • Using transactions efficiently. If the client has local data, then using atomic transactions can help you to ensure that that data is consistent. Because the data is local, the transaction is also local rather than distributed. For smart clients that are working offline, any changes made to the local data are tentative. The client needs to synchronize the changes when it goes online again. For data that is not local, it is possible to use distributed transactions in some cases (for example when services are in the same physical location with good connectivity and where the service supports it). Services such as Web services and Message Queuing do not support distributed transactions.
  • Optimizing application startup time. Fast application startup time allows the user to begin interacting with the application more quickly, which gives the user an immediate and favorable perception of application performance and usability. Your application should be designed so that only those assemblies that are required are loaded on application startup. Avoid using large numbers of assemblies because loading each assembly incurs a performance cost.
  • Managing available resources efficiently. Poor design decisions, such as implementing finalizers when they are not needed, failing to suppress finalization in the Dispose method, or failing to release unmanaged resources, can lead to unnecessary delays in reclaiming resources and can create resource leaks that degrade application performance. Applications that fail to properly release resources, or explicitly force garbage collection, can prevent the CLR from efficiently managing memory.
  • Optimizing Windows Forms performance. Smart client applications rely on Windows Forms to provide a rich and responsive user interface. There are a number of techniques you can use to ensure that Windows Forms provide optimal performance. These include reducing the complexity of the user interface, and avoiding loading large amounts of data at once.

In many cases the perceived performance of your application from the user's perspective is at least as important as the actual performance of the application. You can create an application that appears to perform much more efficiently to the user by making certain changes to your design, such as using background asynchronous processing (to keep the UI responsive), showing a progress bar to indicate the progress of tasks, and providing the option for users to cancel long running tasks.

These issues are discussed in more detail in throughout this section.

Data Caching Guidelines

Caching is an important technique to improve application performance and provide a responsive user interface. You should consider the following options:

  • Caching frequently retrieved data to reduce roundtrips. If your application has to interact frequently with a network service to retrieve data, you should consider caching that data on the client, reducing the need to obtain the data repeatedly over the network. This can increase performance substantially, providing near instantaneous access to the data, and removing the risk of network delays and outages that can adversely affect the performance of your smart client application.
  • Caching read-only reference data. Read-only reference data is usually an ideal candidate for caching. Such data is used to provide data for validation and user interface display purposes, such as product descriptions, IDs, and so on. Because this kind of data cannot be changed by the client, it can usually be cached without any further special handling on the client.
  • Caching data that is to be sent to network-located services. You should consider caching data that is to be sent to a network-located service. For example, if your application allows users to enter order information that consists of a number of discrete items of data gathered over a number of forms, consider allowing the user to enter all of the data, and then send it in one network call at the end of the entry process.
  • Minimizing caching of highly volatile data. Before you can cache any volatile data, you need to consider how long it can be cached before it becomes stale or otherwise unusable. If data is highly volatile and your application relies on up-to-date information, it is likely that the data can only be cached for a short time, if at all.
  • Minimizing caching of sensitive data. You should avoid caching sensitive data on the client because, in most cases, you cannot guarantee the physical security of the client. However, if you do cache sensitive data on the client, you will generally need to encrypt the data, which has its own performance implications.

Further issues surrounding data caching are covered in more detail in Chapter 2 of this guide. Also see the "Caching" section of Improving .NET Application Performance and Scalability, Chapter 3, "Design Guidelines for Application Performance" (http://msdn.microsoft.com/en-us/library/ms998541.aspx) and Improving .NET Application Performance and Scalability, Chapter 4, "Architecture and Design Review of .NET Application for Performance and Scalability" (http://msdn.microsoft.com/en-us/library/ms998544.aspx).

Network Communications Guidelines

Another decision you will face is how to design and work with network services, such as Web services. In particular, you should consider the granularity, synchronicity, and frequency of interaction with network services. For the best performance and scalability, you should send more data in a single call rather than send smaller amounts of data in several calls. For example, if your application allows users to enter multiple items in a purchase order, it is better to collect data for all items, and then to send the completed purchase order to the service at one time, rather than send individual item details in multiple calls. In addition to reducing the overhead associated with making many network calls, this also reduces the need for complex state management within the service and/or the client.

Your smart client applications should be designed to use asynchronous communication whenever possible, as this will help to keep the user interface responsive and execute tasks in parallel. For more information on how to initiate calls and retrieve data asynchronously using BeginInvoke and EndInvoke methods see, "Asynchronous Programming Overview" (http://msdn.microsoft.com/en-us/library/2e08f6yc(VS.71).aspx).

Note   For more information on designing and building smart client applications that are occasionally connected to the network, see Chapter 3, "Getting Connected" and Chapter 4, "Occasionally Connected Smart Clients."

Threading Guidelines

Using multiple threads within your application can be a good way to increase its responsiveness and performance. In particular, you should consider using threads to carry out processing that can safely be done in the background and that does not require user interaction. Performing such work in the background allows the user to continue working with the application and allows the application's main user-interface thread to maintain the application's responsiveness.

Good candidates for processing that can be done on a separate thread include:

  • Application Initialization. Perform lengthy initialization on a background thread so that the user is able to interact with your application as soon as possible, especially if an important or major part of the application functionality does not depend on this initialization completing.
  • Remote Service Calls. Make all remote calls over the network on a separate background thread. It is difficult — if not impossible — to guarantee response times for services located on the network. Performing these calls on a separate thread reduces the risk of network outages or slowdowns adversely affecting application performance.
  • IO Bound Processing. Processing, such as searching and sorting data on disk, should be done on a separate thread. Typically, this kind of work is subject to the constraints of the disk I/O sub-system, and not processor availability, so your application can effectively maintain its responsiveness while this work is carried out in the background.

While the performance benefits of using multiple threads can be significant, it is important to note that threads consume resources of their own and using too many threads can create a burden on the processor, which needs to manage context switching between threads. To prevent this, consider using a thread pool instead of creating and managing your own threads. Thread pools will efficiently manage the threads for you, reusing existing thread objects and minimizing the overhead associated with thread creation and disposal.

If the user experience is impacted by work performed by background threads, you should always keep the user informed of the progress of the work. Providing feedback in this way enhances the user's perception of the performance of your application and prevents him or her from assuming that nothing is happening. Try to ensure that the user can cancel lengthy operations at any time.

You should also consider using the Idle event of the Application object to perform simple operations. The Idle event provides a simple alternative to using separate threads for background processing. This event fires when the application has no more user interface messages to handle and is about to enter the idle state. You can perform simple operations with this event and take advantage of user inactivity. For example:

[C#]

public Form1()
{
InitializeComponent();
Application.Idle += new EventHandler( OnApplicationIdle );
}

private void OnApplicationIdle( object sender, EventArgs e )
{
}

[Visual Basic .NET]

Public Class Form1
    Inherits System.Windows.Forms.Form

    Public Sub New()
        MyBase.New()

        InitializeComponent()

        AddHandler Application.Idle, AddressOf OnApplicationIdle
    End Sub

    Private Sub OnApplicationIdle(ByVal sender As System.Object, ByVal e As System.EventArgs)

    End Sub
End Class
Note   For more information on using multiple threads in Smart Clients, see Chapter 6, "Using Multiple Threads."

Transaction Guidelines

Transactions can provide essential support for ensuring that business rules are not violated and that data consistency is maintained. A transaction ensures that a set of related tasks either succeed or fail as a unit. You can use transactions to maintain consistency between a local database and other resources, including Message Queuing queues.

For smart client applications that need to work with offline cached data when network connectivity is not available, you should queue the transactional data and synchronize it with the server when network connectivity is available.

You should avoid using distributed transactions involving resources located on the network, as these scenarios may lead to performance problems due to varying network and resource response times. If your application needs to involve a network-located resource in a transaction, you should consider using compensating transactions, which allow your application to cancel a previous request when a local transaction fails. Though compensating transactions may not be suitable for all situations, they allow your application to interact with network resources within the context of a transaction in a loosely coupled manner, reducing the chance that a resource not under the control of the local computer can adversely affect the performance of your application.

Note   For more information on the user of transactions in smart clients, see Chapter 3, "Getting Connected."

Optimizing Application Startup Time

Fast application startup time allows the user to begin interacting with the application almost immediately, giving the user an immediate and favorable perception of your application's performance and usability.

When an application starts, first the CLR is loaded, then your application's main assembly, followed by all of the assemblies that are required to resolve the types of objects referenced from your application's main form. The CLR does not load all of the dependent assemblies at this stage; it loads only the assemblies that contain the type definitions for the member variables on your main form class. Once these assemblies are loaded, the just-in-time (JIT) compiler compiles the code for the methods as they are run, starting with the Main method. Again, the JIT compiler does not compile all of the code in your assembly. Instead, the code is compiled as required on a per method basis.

To minimize the startup time of your application, you should follow these guidelines:

  • Minimize member variables in your application's main form class. This will minimize the number of types that have to be resolved when the CLR loads the main form class.
  • Minimize the immediate use of types from large base class assemblies, such as the XML libraries or the ADO.NET libraries. These assemblies take time to load. Using the application configuration classes and the trace switch features will bring in the XML library. Avoid this if application startup time is a priority.
  • Lazy load where possible. Fetch data only when demanded instead of loading upfront and freezing the UI.
  • Design your applications to use fewer assemblies. Applications with large numbers of assemblies incur increased performance cost. The cost comes from loading metadata, accessing various memory pages in pre-compiled images in the CLR to load the assembly (if it is precompiled with the Native Image Generator tool, Ngen.exe), JIT compile time, security checks, and so on. You should consider merging assemblies based on their usage patterns to decrease the associated performance cost.
  • Avoid designing monolithic classes that combine the functionality of several components in one. Factor the design into smaller classes that only need to be compiled when they are actually called.
  • Design your applications to make parallel calls to network services during initialization. Calls to network services that can run parallel during initialization, can take advantage of asynchronous functionality provided by the service proxies. This helps free the current executing thread and calls services concurrently to get tasks done.
  • Use NGEN.exe to compile and experiment with NGen and non-NGen assemblies, and determine which saves the largest number of working set pages. NGEN.exe, which ships with the .NET Framework, is used to pre-compile an assembly to create a native image that is then stored in a special part of the global assembly cache, ready for the next time it is required by an application. Creating a native image of an assembly allows the assembly to load and execute faster because the CLR does not need to dynamically generate the code and data structures contained in the assembly. For more information, see the "Working Set Considerations" and "NGen.exe Explained" sections in Chapter 5, "Improving Managed Code Performance" of Improving. NET Application Performance and Scalability, at http://msdn.microsoft.com/en-us/library/ms998547.aspx.
    Note   If you use NGEN to pre-compile an assembly, all of its dependent assemblies will be immediately loaded.

Managing Available Resources

The Common Language Runtime (CLR) uses a garbage collector to manage object lifetime and memory usage. This means that objects that are no longer reachable are automatically collected by the garbage collector, with the memory being reclaimed automatically. Objects can be no longer reachable for a number of reasons. For example, there may be no references to the object or all references to the object may be from other objects that can be collected as part of the current collection cycle, While automatic garbage collection frees your code of the burden associated with managing object deletion, it means that your code no longer has explicit control over exactly when an object is deleted.

Consider the following guidelines to ensure that you manage available resources effectively:

  • Ensure that the Dispose method is called when the callee object provides one. If your code calls objects that support the Dispose method, you should ensure you call this method as soon as you finish using the object. Calling the Dispose method ensures that unmanaged resources are proactively released instead of waiting until garbage collection occurs. Some objects provide methods in addition to the Dispose method that manage resources, such as the Close method. In these cases, you should consult the documentation on how to use the additional methods. For example, with the SqlConnection object, calling either Close or Dispose is enough to proactively release the database connection back to the connection pool. One way to ensure that Dispose is called as soon as you are done with the object is to use the using statement in Visual C# .NET or Try/Finally blocks in Visual Basic .NET.

    The following code snippets demonstrate the use of Dispose.

    Example of using statement in C#:

    using( StreamReader myFile = new StreamReader("C:\\ReadMe.Txt")){
    string contents = myFile.ReadToEnd();
    //... use the contents of the file
    } // dispose is called and the StreamReader's resources released
    

    Example of Try/Finally block in Visual Basic .NET:

    Dim myFile As StreamReader
    myFile = New StreamReader("C:\\ReadMe.Txt")
    Try
    String contents = myFile.ReadToEnd()
    '... use the contents of the file
    Finally
    myFile.Close()
    End Try
    
    Note   In C# and C++, Finalize methods are implemented as destructors. In Visual Basic .NET, the Finalize method is implemented as an override of the Finalize subroutine on the Object base class.
  • Provide Finalize and Dispose methods if you hold unmanaged resources across client calls. If you create an object that accesses unmanaged resources in public or protected method calls, then the application needs to control the lifetime of the unmanaged resources. In Figure 8.1, the first case is a call to unmanaged resources where the resource is opened, fetched, and closed. In this case, your object does not need to provide Finalize and Dispose methods. In the second case, the unmanaged resource is held across method calls; therefore, your object should provide Finalize and Dispose methods so that the client can explicitly release the resource as soon as the client has finished using the object.

    Ff648204.ppt-pic_f01(en-us,PandP.10).gif

    Figure 8.1: Use of Dispose and Finalize method calls

Garbage collection is generally good for overall performance because it favors speed over memory usage. Objects need to be deleted only when memory resources are low; otherwise, all available application resources are used to the benefit of your application. However, if your object maintains a reference to an unmanaged resource, such as window handle, file, GDI objects, and network connections, then better performance can be achieved if the programmer explicitly releases these resources when they are no longer being used. If you are holding unmanaged resources across client method calls, then the object should allow the caller to explicitly manage resources using the IDisposable interface, which provides the Dispose method. By implementing IDisposable, an object is announcing that it can be asked to clean up deterministically, rather than waiting for garbage collection. The caller of an object that implements IDisposable simply calls the Dispose method when it has finished with the object so that it can free the resource as appropriate.

For more details on how to implement IDisposable on one of your objects, see Chapter 5, "Improving Managed Code Performance," in Improving .NET Application Performance and Scalability, at http://msdn.microsoft.com/en-us/library/ms998547.aspx.

Note   If your disposable object derives from another object that also implements the IDisposable interface, you should call the Dispose method of the base class to allow it to clean up its resources. You should also call Dispose on all objects that are owned by your object that implements the IDisposable interface.

The Finalize method also allows your object to explicitly release any resources that it has a reference to when the object is being deleted. Due to the non-deterministic nature of the garbage collector, in some cases the Finalize method may not be called for a long time. In fact, it may never be called if your application terminates before the object is deleted by the garbage collector. However, it important to use the Finalize method as a backup strategy in case the caller doesn't explicitly call the Dispose method (both the Dispose and Finalize methods share the same resource cleanup code). In this way, the resource is likely to be freed at some point, even if this occurs later than is optimal.

Note   To ensure that the cleanup code in Dispose and Finalize isn't called twice, you should call GC.SuppressFinalize, which tells the garbage collector not to call the Finalize method.

The garbage collector implements the Collect method, which forces the garbage collector to delete all objects pending deletion. This method should not be called from within your application, as the collection cycle runs on a high priority thread. The collection cycle may freeze all the UI threads, resulting in an unresponsive user interface.

For more information, see "Garbage Collection Guidelines," "Finalize and Dispose Guidelines," "Dispose Pattern," and "Finalize and Dispose Guidelines" in Improving .NET Application Performance and Scalability at http://msdn.microsoft.com/en-us/library/ms998547.aspx.

Optimizing Windows Forms Performance

Windows Forms provide a rich user interface for your smart client application and there are a number of techniques you can use to help ensure that Windows Forms provides optimal performance. Before discussing specific techniques, it is useful to review some high-level guidelines that can increase Windows Forms performance substantially.

  • Beware of handle creations. Windows Forms virtualizes handle creation (that is, it creates and re-creates window handle objects dynamically). Creating handle objects can be expensive; therefore, avoid making unnecessary border style changes or changing MDI parents.
  • Avoid creating applications with very many child controls. The Microsoft® Windows® operating system has a limit of 10,000 controls per process, but you should avoid having many hundreds of controls on a form as each control consumes memory resources.

The rest of this section discusses more specific techniques you can use to optimize the performance of your application's user interface.

Using BeginUpdate and EndUpdate

A number of Windows Forms controls (for example the ListView and TreeView controls) implement BeginUpdate and EndUpdate methods, which suppress repainting of the controls while the underlying data or control properties are manipulated. Using the BeginUpdate and EndUpdate methods allows you to make significant changes to your controls and avoid having the control repainting itself constantly while those changes are applied. Such repainting leads to a significant performance degradation and a flickering and unresponsive user interface.

For example, if your application has a tree control that requires a large number of node items to be added, you should call BeginUpdate, add all of the required node items, and then call EndUpdate. The following code example shows a tree control being used to display a hierarchical representation of a number of customers along with their order information.

[C#]

// Suppress repainting the TreeView until all the objects have been created.
treeView1.BeginUpdate();

// Clear the TreeView.
treeView1.Nodes.Clear();

// Add a root TreeNode for each Customer object in the ArrayList.
foreach( Customer customer2 in customerArray )
{
    treeView1.Nodes.Add( new TreeNode( customer2.CustomerName ) );

    // Add a child TreeNode for each Order object in the current Customer.
    foreach( Order order1 in customer2.CustomerOrders )
    {
        treeView1.Nodes[ customerArray.IndexOf(customer2) ].Nodes.Add(
             new TreeNode( customer2.CustomerName + "." + order1.OrderID ) );
    }
}

// Begin repainting the TreeView.
treeView1.EndUpdate();

[Visual Basic .NET]

       ' Suppress repainting the TreeView until all the objects have been created.
        TreeView1.BeginUpdate()

' Clear the TreeView
TreeView1.Nodes.Clear()

' Add a root TreeNode for each Customer object in the ArrayList
For Each customer2 As Customer In customerArray
     TreeView1.Nodes.Add(New TreeNode(customer2.CustomerName))

     ' Add a child TreeNode for each Order object in the current Customer.
     For Each order1 As Order In customer2.CustomerOrders
           TreeView1.Nodes(Array.IndexOf(customerArray, customer2)).Nodes.Add( _
                    New TreeNode(customer2.CustomerName & "." & order1.OrderID))
     Next
Next

' Begin repainting the TreeView.
TreeView1.EndUpdate()

You should use the BeginUpdate and EndUpdate methods even when you do not expect many objects to be added to the control. In most cases, you will not be aware of the exact number of items to be added until runtime. Therefore, to cope elegantly with an unusually large amount of data and for future requirements, you should always call the BeginUpdate and EndUpdate methods.

Note   Calling the AddRange method of many of the Collection classes used by Windows Forms controls will automatically call BeginUpdate and EndUpdate for you.

Using SuspendLayout and ResumeLayout

A number of Windows Forms controls (for example the ListView and TreeView controls) implement SuspendLayout and ResumeLayout methods, which prevent the control from creating multiple layout events while the child controls are being added.

If your controls programmatically add and remove child controls or perform dynamic layout, then you should call the SuspendLayout and ResumeLayout methods. The SuspendLayout method allows multiple actions to be performed on a control without having to perform a layout for each change. For example, if you resize and move a control, each operation would raise a separate layout event.

These methods operate in a similar manner to the BeginUpdate and EndUpdate methods and provide the same benefits in terms of performance and user interface stability.

The example below programmatically adds buttons to the parent form:

[C#]

private void AddButtons()
{
  // Suspend the form layout and add two buttons.
  this.SuspendLayout();
  Button buttonOK = new Button();
  buttonOK.Location = new Point(10, 10);
  buttonOK.Size = new Size(75, 25);
  buttonOK.Text = "OK";

  Button buttonCancel = new Button();
  buttonCancel.Location = new Point(90, 10);
  buttonCancel.Size = new Size(75, 25);
  buttonCancel.Text = "Cancel";

  this.Controls.AddRange(new Control[]{buttonOK, buttonCancel});
  this.ResumeLayout();
}

[Visual Basic .NET]

Private Sub AddButtons()
        ' Suspend the form layout and add two buttons
        Me.SuspendLayout()
        Dim buttonOK As New Button
        buttonOK.Location = New Point(10, 10)
        buttonOK.Size = New Size(75, 25)
        buttonOK.Text = "OK"

        Dim buttonCancel As New Button
        buttonCancel.Location = New Point(90, 10)
        buttonCancel.Size = New Size(75, 25)
        buttonCancel.Text = "Cancel"

        Me.Controls.AddRange(New Control() { buttonOK, buttonCancel } )
        Me.ResumeLayout()
End Sub

You should use the SuspendLayout and ResumeLayout methods whenever you add or remove controls, perform dynamic layout of the child controls, or set any properties that affect the layout of the control, such as the size, location, anchor, or dock properties.

Handling Images

If your application displays a large number of image files, such as .jpg and .gif files, then you can improve display performance significantly by pre-rendering the images into a bitmap format.

To use this technique, first load the image from file and then render to a bitmap using the PARGB format. The following code sample loads a file from disk and then uses the class to render the image into a pre-multiplied, alpha-blended RGB format. For example:

[C#]

if ( image != null && image is Bitmap )
{
Bitmap bm = (Bitmap)image;
Bitmap newImage = new Bitmap( bm.Width, bm.Height,
   System.Drawing.Imaging.PixelFormat.Format32bppPArgb );
using ( Graphics g = Graphics.FromImage( newImage ) )
{
g.DrawImage( bm, new Rectangle( 0,0, bm.Width, bm.Height ) );
}
image = newImage;
}

[Visual Basic .NET]

        If Not(image Is Nothing)  AndAlso (TypeOf image Is Bitmap) Then
            Dim bm As Bitmap = CType(image, Bitmap)
            Dim newImage As New Bitmap(bm.Width, bm.Height, _
                System.Drawing.Imaging.PixelFormat.Format32bppPArgb)

            Using g As Graphics = Graphics.FromImage(newImage)
                g.DrawImage(bm, New Rectangle(0, 0, bm.Width, bm.Height))
            End Using

            image = newImage
        End If

Use Paging and Lazy Loading

In most cases, you should retrieve or display data only when it is needed. If your application needs to retrieve and display a lot of information, you should consider breaking the data into pages and displaying the data one page at a time. This allows your user interface to perform better because it does not have to display a large amount of data. In addition, this can improve the usability of your application because the user is not confronted with an abundance of data at once and can navigate more easily to find the exact data he or she needs.

For example, if your application displays product data from a large product catalog, you could display the items in alphabetical order with all the products beginning with "A" displayed on one page and all the products beginning with "B" on the next page. You could then allow the user to navigate directly to the appropriate page so that he or she does not need to scroll through all of the pages to reach the data he or she needs.

Paging the data in this way can also allow you to fetch the data in the background as it is required. For instance, you might only need to fetch the first page of information to display and allow the user to interact with. You can then fetch the next page of data in the background ready for when the user needs it. This technique can be particularly effective when combined with data caching.

You can also increase the performance of your smart client application by using lazy loading techniques. Instead of immediately loading data or resources that you might need at some point in the future, you load them as they are needed. You can use lazy loading to increase the performance of your user interface when constructing large lists or tree structures. In this case, you can load the data when the user needs to see it, for example when a tree node is expanded.

Optimizing Display Speed

You can optimize your application's display speed in a number of different ways, according to the techniques you are using to display the user interface controls and application forms.

When your application starts, you should consider displaying as simple a user interface as possible. This will decrease startup time and present an uncluttered and easy to use user-interface to the user. Also, you should try to avoid referencing classes and loading any data at startup that is not immediately required. This will improve the application and .NET framework initialization time and improve the display speed of the application.

When you need to display a dialog box or form, you should keep them hidden until they are ready to be displayed, to reduce the amount of painting necessary. This will help to ensure that the form is only displayed once it has been initialized.

If your application has controls that contain child controls covering the entire client surface area, you should consider setting the control background style to opaque. This avoid redrawing the control's background on every paint event. You can set the control's style by using the SetStyle method. Use ControlsStyles.Opaque enumeration to specify an opaque control style.

You should avoid any unnecessary repainting of controls. One approach is to hide controls while you are setting their properties. Applications that have complex drawing code in the OnPaint event are able to redraw just the invalidated region of the form, instead of painting the entire form. The PaintEventArgs parameter of the OnPaint event contains a ClipRect structure that indicates which part of the window is invalidated. This reduces the time that the user waits to see a completed display.

Use standard drawing optimization, such as clipping, double buffering, and ClipRectangle. This will also help improve the display performance of your smart client application by preventing unnecessary drawing operations for portions of the display that are not visible or that require redrawing. For more information on enhancing painting performance, see Painting techniques using Windows Forms for the Microsoft .NET Framework at http://windowsclient.net/articles/windowsformspainting.aspx.

If your display includes animation or changes a display element often, you should use double or multiple buffering to prepare the next image while the current one is being painted. The ControlStyles enumeration in the System.Windows.Forms namespace applies to many controls, and the DoubleBuffer member can help prevent flickering. Turning on the DoubleBuffer style will cause your controls painting to be done to an off-screen buffer and then painted all at once to the screen. While this helps prevent flickering, it does use more memory for the allocated buffer.

Performance Tuning and Diagnosis

Tackling performance issues at the design and implementation stages is the most cost effective way to meet your application's performance goals. However, you can only be truly effective in optimizing the performance of your applications if you test your application's performance often and as early in the development phase as possible.

While designing and testing for performance are both important, optimizing every component and all of the code at these early stages is not an efficient use of resources, and so should be avoided. Consequently, your application may suffer from unexpected performance problems that you did not anticipate at the design stage. For example, you may experience performance problems due to the unforeseen interaction between two systems or components, or you may use pre-existing code that does not perform as hoped. In this case, you need to track down the source of the performance problem so that you can address it appropriately.

This section discusses a number of tools and techniques that will help you diagnose performance issues, and tune your application for optimum performance.

Setting Performance Goals

As you design and architect your smart client application, you should carefully consider the requirements in terms of performance, and define suitable performance goals. When defining these goals, consider how you are going to measure the application's actual performance. Your performance metrics should clearly represent the important performance characteristics of the application. Try to avoid ambiguous or incomplete goals that cannot be accurately measured, such as "the application must run fast" or "the application must load quickly." You need to know the performance and scalability goals of your application so that you can design to meet them and plan your tests around them. Be sure that your goals are measurable and verifiable.

Well-defined performance metrics allow you to track the performance of your application accurately so that you can determine whether the application meets its performance goals or not. These metrics should be included in your application's test plan, so that they can be measured during the testing phase of your application.

This section focuses on the definition of specific performance goals relevant to a smart client application. If you are also designing and building the network services that the client application will consume, you need to define appropriate performance goals for these as well. In this case, you should be sure to consider the performance requirements of the system as a whole and how the performance of each part of the application relates to the other parts and to the system in its entirety.

Considering the User's Perspective

As you determine suitable performance goals for a smart client application, you should carefully consider the perspective of the user. For a smart client application, performance is related to usability and user perception.. For example, a lengthy operation could be acceptable to the user as long as that user is able to keep working and is provided with adequate feedback on progress of the operation.

When determining requirements, it is often useful to break an application's functionality into a number of usage scenarios or use cases. You should identify the use cases and scenarios that are critical and required to meet specific performance objectives. Tasks that are common to many use cases and that are performed often should be designed to perform well. Similarly, tasks that demand the user's complete attention and from which they can't switch to perform other tasks need to provide an optimized and efficient user experience. Tasks that are not used very often or that do not stop the user from performing other tasks may not need to be highly tuned.

For each performance-sensitive task that you identify, you should precisely define what the user does and how the application responds. You should also determine which network and client resources or components each task uses. This information will influence the performance goals and will drive the tests that measure performance.

Usability studies provide a very valuable source of information and can greatly influence the definition of performance goals. A formal usability study can be very helpful in determining how users perform their work, which usage scenarios are common and which are not, what tasks the users perform often, and what characteristics of the application are important from a performance perspective. If you are building a new application, you should consider providing a prototype or mock-up of the application to allow rudimentary usability testing to be carried out.

Considering the Application Operating Environment

It is important to evaluate the environment in which your application will be operating, as this may impose constraints on your application that must be reflected in the performance goals you set.

Network-located services may impose performance constraints on your application. For example, you may be required to interact with a Web service over which you have no control. In such cases, it is important to determine the performance of the service and to determine whether this will have an effect on the performance of your client application.

You should also determine how the performance of any dependent services and components may vary with time. Some systems experience fairly constant usage while other experience wildly fluctuating usage at certain times of the day or week. These differences could adversely affect the performance of your application at critical times. For example, a service that provides application deployment and update services may be slow to respond on Monday morning at 9:00 AM as all users upgrade to the latest version of an application.

It is also important to accurately model the performance of all dependent systems and components, so that your application can be tested in an environment that closely mimics the real environment in which it will be deployed. For each system, you should determine the performance profile and the minimum, average, and peak performance characteristics. You can then use this data as appropriate when defining the performance requirements for your application.

You should also carefully consider the hardware on which your application will run. You will need to determine the target hardware configuration, in terms of processor, memory, graphics capability, and so on — or at least a minimum configuration below which you cannot guarantee performance.

Often the business environment in which your application will operate will dictate some of the more exacting performance requirements. For example, an application that executes real-time stock trading will be required to execute these trades and display all of the relevant data in a timely manner.

Performance Tuning Process

Performance tuning your application is an iterative process. This process consists of a number of stages that are repeated until the application meets its performance goals. (See Figure 8.2.)

Ff648204.chapter8_f02(en-us,PandP.10).gif

Figure 8.2: Performance tuning process

As Figure 8.2 illustrates, performance tuning requires that you complete the following processes:

  • Establish Baseline. Before you begin tuning your application for performance, you must have a well-defined baseline for the performance goals, objectives, and metrics. This could include specifics such as application working set size, time to load data (for example, a catalogue), transaction duration, and so on.
  • Collect Data. You will need to gauge your application's performance by measuring it against the performance goals that you have defined. Performance goals should embody specific and measurable metrics that allow you to quantify your application's performance at any point in time. To allow you to collect performance data, you may have to instrument your application so that the required performance data can be published and collected. Some of the options that you have to accomplish this are discussed in detail in the next section.
  • Analyze Results. After you have collected your application's performance data, you will be able to prioritize your performance tuning effort by determining which application features require the most attention. In addition, you can use this data to determine where any performance bottlenecks are. Often, you will only be able to determine the exact location of the bottleneck by gathering more detailed performance data: for example, by using application instrumentation. Performance profiling tools may help you to identify the bottleneck.
  • Tune Application. After you have identified a bottleneck, you will probably need to modify the application or its configuration to try and solve the problem. You should aim to minimize changes so that you can determine the effect of the changes on the application's performance. If you make more than one change at the same time, it can be difficult to determine what effect each change had on the application's overall performance.
  • Test and Measure. After you have changed your application or its configuration, you should test it again to determine what effect your changes have and to allow new performance data to be gathered. Performance work often requires architectural or other high-impact changes so thorough testing is critical. Your application's test plan should exercise the full range of functionality that your application implements, for all anticipated scenarios and on client machines configured with the appropriate hardware and software. If your application uses network resources, you should load these resources so that you can gain accurate measurements for how your application performs in such an environment.

The above process will allow you to focus on specific performance problems by measuring your applications overall performance against specific goals.

Performance Tools

There are number of tools available to you which can help you to collect and analyze your application's performance data. Each of the tools described in this section have different functionality that you can use to measure, analyze, and find performance bottlenecks in your application.

Note   In addition to the tools described here, there are a number of other options and third-party tools available. For a description of other logging and exception management options, see the Exception Management Architecture Guide, at
http://msdn.microsoft.com/en-us/library/ms229014(VS.80).aspx
You should carefully consider your exact requirements before deciding on which tools are most appropriate to your needs.

Using Performance Logs and Alerts

Performance Logs and Alerts is an administrative performance monitoring tool that ships as part of the Windows operating system. It relies on performance counters that are published by the various Windows components, subsystems, and applications to allow you to track resource usage and to plot them graphically against time.

You can use Performance Logs and Alerts to monitor standard performance counters, such as memory usage or processor usage, or you can define your own custom counters to monitor application-specific activity.

The .NET CLR provides a number of useful performance counters that can give you insight into how well your application is performing. Some of the more relevant performance objects are:

  • .NET CLR Memory. Provides data on the memory usage of a managed .NET application, including the amount of memory that your application is using and the time spent garbage collecting unused objects.
  • .NET CLR Loading. Provides data on the number of classes and application domains that your application is using and the rate at which they are being loaded and unloaded.
  • .NET CLR Locks and Threads. Provides performance data related to the threads used within your application, including the number of threads and the rate of contention between threads trying to get simultaneous access to a protected resource.
  • .NET CLR Networking. Provides performance counters that relate to sending and receiving data over the network, including the number of bytes sent and received per second and the number of active connections.
  • .NET CLR Exceptions. Provides reports on the number of exceptions being thrown and caught by your application.

To learn more about these counters, their thresholds, what to measure and how to measure them see the section, "CLR and Managed Code" in Chapter 15, "Measuring .NET Application Performance" of Improving .NET Application Performance and Scalability, at http://msdn.microsoft.com/en-us/library/ms998579.aspx.

Your application can also provide application-specific performance counters that you can easily monitor by using Performance Logs and Alerts. You can define a custom performance counter as shown in the following example:

[C#]

PerformanceCounter counter = new PerformanceCounter( "Category",
            "CounterName", false );

[Visual Basic .NET]

Dim counter As New PerformanceCounter("Category", "CounterName", False)

Once the performance counter object is created, you can specify a category for your custom performance counters and keep all related counters together. The PerformanceCounter class is defined in the System.Diagnostics namespace, along with a number of other classes that you can use to read and define performance counters and categories. For more information on creating custom performance counters see, Knowledge Base article 317679, "How to create and make changes to a custom counter for the Windows Performance Monitor by using Visual Basic .NET," at http://support.microsoft.com/default.aspx?scid=kb;en-us;317679.

Note   To register a performance counter, you must first register the category. You must have sufficient permissions to register a performance counter category, which may affect how you need to deploy your application.

Instrumentation

There are a number of tools and technologies you can use to help instrument your application and generate information needed to measure the application performance. These tools and technologies include:

  • Event Tracing for Windows (ETW). This ETW subsystem provides a low system overhead (as compared to Performance Logs and Alerts) means of monitoring performance of a system under load. This is primarily for server applications that must frequently log events, errors, warnings, or audits. For more information, see "Event Tracing" in the Microsoft Platform SDK at http://msdn.microsoft.com/en-us/library/bb968803(VS.85).aspx.
  • Enterprise Instrumentation Framework (EIF). The EIF is an extensible and configurable framework that you can use to instrument your smart client application. It provides an extensible event schema and unified API that uses existing events, logging, and tracing mechanisms built into Windows, including the Windows Management Instrumentation (WMI), the Windows Event Log, and Windows Event Tracing. It greatly simplifies the coding required to publish application events. If you are planning to use EIF, you need to install EIF on the client computer by using the EIF .msi. If you want to use the EIF in your smart client application, you need to consider this requirement when you decide how to deploy your application. For more information, see "How To: Use EIF" at http://msdn.microsoft.com/en-us/library/ms979206.aspx.
  • Logging Application Block. The Logging Application Block provides extensible and reusable code components to help you produce instrumented applications. It builds on capabilities of the EIF to provide functionalities such as enhancements to the event schema, multiple log levels, additional event sinks, and so on. For more information, see the "Logging Application Block" at http://msdn.microsoft.com/en-us/library/cc339349.aspx.
  • Windows Management Instrumentation (WMI). The WMI component is part of the Windows operating system and provides programming interfaces for accessing management information and control in an enterprise. This is most commonly used by system administrators to automate administration tasks using scripts that invoke the WMI component. For more information, see Windows Management Information at http://msdn.microsoft.com/en-us/library/aa394582(VS.85).aspx.
  • Debug and Trace Classes. The .NET framework provides Debug and Trace classes under the System.Diagnosis to instrument your code. The Debug class is primarily used for printing debug information and checking for assertions. The Trace class allows you to instrument release builds to monitor the health of your application at run time. In Visual Studio .NET, tracing is enabled by default. When using the command-line build you must add the /d:Trace flag for the compiler or #define TRACE in the your Visual C# .NET source code to enable tracing. For Visual Basic .NET source code, you must add /d:TRACE=True for the command-line compiler. For more information, see Knowledge Base article 815788, "HOW TO: Trace and Debug in Visual C# .NET," at http://support.microsoft.com/default.aspx?scid=kb;en-us;815788.

CLR Profiler

The CLR Profiler is a memory profiling tool provided by Microsoft and available for download from MSDN. It enables you to look at the managed heap of your application's process and investigate the behavior of the garbage collector. Using this tool, you can obtain useful information about the execution, memory allocation, and memory consumption of your application. This information can help you understand how your application is using memory and how you can optimize your application's memory use.

The CLR Profiler is available at http://msdn.microsoft.com/en-us/netframework/aa569269.aspx. Also see "How to use CLR Profiler" at http://msdn.microsoft.com/en-us/library/ms979205.aspx for details on how to use the CLR Profiler tool.

The CLR Profiler logs memory consumption and garbage collector behavior information in a log file. You can then analyze this data with the CLR Profiler by using number of different graphical views. Some of the more important views are:

  • Allocation Graph. Shows the call stack for how objects were allocated. You can use this view to see the cost of each allocation by method, isolate allocations that you were not expecting, and view possible excessive allocations by a method.
  • Assembly, Module, Function, and Class Graph. Shows which methods caused the loading of which assemblies, functions, modules, or classes.
  • Call Graph. Lets you see which methods call which other methods and how frequently. You can use this graph to determine the cost of library calls and which methods are called or how many calls are made to a specific method.
  • Time Line. Provides a text-based, chronological, hierarchical view of your application's execution. Use this view to see what types are allocated and their size. You can also use this view to see which assemblies are loaded as result of method calls and to analyze allocations that you were not expecting. You can analyze the use of finalizers and to identify methods where Close or Dispose have not been implemented or called, thereby causing bottlenecks.

You can use CLR Profiler.exe to identify and isolate problems related to garbage collection. These include memory consumption issues such as excessive or unknown allocations, memory leaks, long-lived objects, and the percentage of time spent performing garbage collection.

Note   For more detailed information on how to use the CLR Profiler tool, see "Improving .NET Application Performance and Scalability" at http://msdn.microsoft.com/en-us/library/ms979205.aspx.

Summary

To fully realize the potential of a smart client application, you need to carefully consider performance issues during the application's design phase. By addressing these issues at an early stage, you can contain costs during the application design process and reduce the likelihood of running into performance problems late in the development cycle.

This chapter examined different techniques that you can use as you architect and design your smart client applications to ensure that you optimize their performance. It has also looked at a number of tools and techniques you can use to determine performance problems within your smart client applications.

References

For more information, see the following:

patterns & practices Developer Center

Show:
© 2014 Microsoft