Over the years, I’ve invested a lot of time in tuning Fiddler to improve performance in the most common user-scenarios. Most of the work occurs “under the covers” inside the session-processing pipeline. By minimizing the overhead of the core proxy, I can help ensure that the “observer effect” is minimized when Fiddler is used to measure web performance.

However, from time to time, I find scenarios where Fiddler’s user-interface isn’t fast enough. These scenarios mostly occur when Fiddler’s Web Sessions list contains thousands of Sessions. The typical workaround is to use CTRL+X, Filters, or the Keep Only toolbar command to minimize the number of Sessions in the Session list. Limiting the size of the list by preemptively filtering unwanted data is a great strategy, but sometimes you’ll start with many thousands of Sessions and you want to delete thousands of them at one time. It turns out this can be very slow. For example, earlier this week, I had 20000 items in my Session list and I selected 6000 of them to delete. Deleting the Sessions pegged my i7 CPU for over 80 seconds, an entirely unacceptable delay.

Fortunately, Telerik offers a great solution for finding .NET performance bottlenecks—JustTrace. While the tool offers memory profiling, in this case I was interested in its ability to instrument code execution to determine where the CPU time was being burned. Was it something my code was doing? Or was there a bug in the framework?

While it offers many advanced features, getting started with JustTrace was simple. I chose New Profiling Session > .NET Executable and provided the path to Fiddler:

image

I then set the profiling options and clicked Run:

image

The application being profiled starts and JustTrace begins collecting data. You can view the CPU usage in the Timeline graph at the top of the profiler.

My next step is to reproduce the problem. In Fiddler’s QuickExec box, I type !fake 10000 to create 10,000 dummy Sessions to play with. Next, I type ?6 to select any Session whose URL contains a 6, which amounts to about 3200 of the sessions. I then hit Enter to focus the Session list and hit Delete to delete the sessions. Fiddler immediately goes to work deleting the Sessions, which results in a prolonged spike in the CPU usage:

image

Now that we’ve reproduced the problem under the profiler, it’s time to figure out where it’s coming from. Click into the Timeline and select the large block of CPU time and click the Show Snapshot button:

image

Next, select Hot Spots in the left sidebar to see where your code was spending its cycles during the selected block of time. In this case, you can see that the majority of the time was spent inside the DisplayIndexToID method of the Framework’s ListViewNativeItemCollection object.

image

I’m happy to see that none of Fiddler’s code was responsible for horrible performance (phew!), but I’m still curious about what’s going on inside the framework here.

Now, I could go download the .NET Framework’s source code but JustTrace makes it even simpler than that. I right-click the DisplayIndexToID line and choose View Source and the target assembly is instantly decompiled by Telerik’s free decompiler (JustDecompile) and the offending code is displayed directly within the JustTrace UI.

This function is fairly simple and it appears that the bulk of the time is spent calling into the native Windows API for the ListView control, in order to obtain the Display Index of each item being deleted. By selecting the Call Trees view, I can see that this function is called 12 million times:

image

No wonder this function is taking so much time—it’s getting hammered! Looking at the call tree, it’s obvious that the IndexOf function (called just 3241 times) is the real culprit—that function is calling the underlying native API an average of 3800 times for each item being deleted. JustDecompile reveals the simplistic code of the function:

image

While deleting any single node is only O(n), deleting many nodes requires O(n) time for each node being deleted!

Alas, what can I do now? If the Framework implementation is slow, my only choice is to re-wrap the underlying native control, right?

Maybe.

JustDecompile’s shown me the IndexOf function, and it’s obvious that the problem is that for each item being deleted, the code must walk (on average) half the elements of the list (assuming the items being deleted are randomly distributed) to find the item.

But what if the items to be deleted aren’t randomly distributed? What if they’re at the very front of the list? We’ll always be deleting the first element of the list, which means that we only hit get_Item and DisplayIndextoID once per item we’re deleting. Instead of spending 132 seconds looking up item indexes, we spend under 100 milliseconds.

And now the solution becomes obvious. If we’re ever in a situation where we have to delete a large number of items from a huge list:

  1. First sort the list so that the items to be deleted are at the top,
  2. Then delete the items
  3. Then re-sort the list back to however it was sorted originally.

The result is pure magic – fast deletion without changing the Framework.

internal void RemoveSelected()
{
    BeginUpdate();
    ListView.SelectedListViewItemCollection oSelectedItems = SelectedItems;

    bool bLargeDelete = (Items.Count > 2000) && (SelectedCount > 60);
    if (bLargeDelete)
    {
        IComparer oOriginalSorter = this.ListViewItemSorter;
        this.ListViewItemSorter = new SelectedItemComparer();
        foreach (ListViewItem lvi in oSelectedItems)
        {
            lvi.Remove();
        }
        this.ListViewItemSorter = oOriginalSorter;
        return;
    }

    foreach (ListViewItem lvi in oSelectedItems)
    {
        lvi.Remove();
    }
    EndUpdate();

}

/// <summary>
/// Comparer used for bulk-delete sorting; sorts selected items to the top of the listview.
/// </summary>
class SelectedItemComparer : System.Collections.IComparer
{
    public int Compare(object x, object y)
    {
        ListViewItem lviX = (ListViewItem)x;
        ListViewItem lviY = (ListViewItem)y;
        if (lviX.Selected && !lviY.Selected) return -1;
        if (!lviX.Selected && lviY.Selected) return 1;
        return lviX.GetHashCode().CompareTo(lviY.GetHashCode());
    }
}

This performance optimization will appear in a future version of Fiddler.


About the Author

Eric Lawrence

(@ericlaw) has built websites and web client software since the mid-1990s. After over a decade of working on the web for Microsoft, Eric joined Telerik in October 2012 to enhance the Fiddler Web Debugger on a full-time basis. With his recent move to Austin, Texas, Eric has now lived in the American South, North, West, and East.

Related Posts

Comments

Comments are disabled in preview mode.