WPF performance and .NET Framework Client Profile related blogs provided by Jossef Goldberg.
It is increasingly common for users to run their client application remotely, either connected to another Windows Client machine (Remote Desktop) or to a Windows Server (Terminal Service). In both of these scenarios the sources and the target machines are communicating over a protocol called Remote Desktop Protocol (or RDP).
In this blog, I wanted to share some of our findings while testing Visual Studio 2010 (VS 2010) over RDP as well as provide best practices to improve VS 2010 and your WPF-base app performance over RDP.
You may have seen the announcements (Soma , Jason Zander, Scott Guthris's) for the public release of Visual Studio 2010 RC . Some of the principles mentioned in this blog are also implemented by VS 2010.
While this blog is focusing on Microsoft remoting technologies some of the ideas discussed here should be applicable to other non-Microsoft technologies.
This blog is few pages long, the first part is a summary for folks who just want the short version. More details follow below in the document.
Summary: Best Practices for Improving VS 2010 and WPF Performance with Remote Desktop
A) Tune your Remote Desktop Connection (RDC) settings.
In slow bandwidth connections (such as slow DSL) or in high latency scenarios (such as connecting from US coast-to-coast) below tips can provide huge perf improvement. We for example measured ~4x gain in some scenarios.
Note that by doing above you are improving responsiveness but sacrificing some visual "prettiness" . Here is a summary of what we discussed above:
Connecting From:
Win7/R2
Vista SP+
XP SP3
XP SP2
Default RDC version installed:
RDC 7.0
RDC 6.1
RDC 5.2
For VS10 & WPF in slow network consider: (e.g. slow DSL, or coast-to-coast connection)
1.Use 16-bit color. 2. Use "WAN"/"Satellite" in high-latency cases. 3. User lower window size
Install RDC 7.0. Repeat the Win7 suggestions
Install RDC 7.0. Repeat the Win7 suggestions.
Upgrade to XP3. Repeat XP SP3 suggestions.
B) Optimize your WPF app to be Remote Desktop aware. Detect when you are running in Remote Desktop session and make sure to: 1. Reduce amount of your app UI updates while running over RDP, by:
2. Simplify content that is redrawn frequently so that the RDC compressor can be more efficient. (E.g. use solid colors over gradients or images.) 3. Avoid operations that are slow to render in software. (e.g. BitmapEffects & 3D) 4. Use the new VisualScrollableAreaClip NET4 API if your scenarios include line-scrolling.
C) During development, test your app to verify it is optimized for RDP
History and background
WPF Remoting architectures changes
Until .NET Framework 3.5 SP1 (NET 3.5 SP1) and earlier, remoting between Vista to Vista with DWM on, leveraged a custom WPF primitive remoting protocol. In all other scenarios content was remoted as bitmaps. Starting with the release of NET 3.5 SP1 (including NET 4), WPF renders the application content using a software rasterizer on the server and then remotes the content as bitmaps in all cases. Note:
Bitmaps are highly compressed by the underlying RDC stack and only regions that changed are being updated. Also note that WPF does not currently have efficient occlusion support, so for instance animations that are completely hidden behind other opaque WPF elements will still force invalidation and RDP update.
When apps use GDI (such as many Win32 and Winforms apps do), only the GDI primitives are remoted. In many cases this can be more efficient than remoting WPF apps since WPF apps remotes bitmaps which typically result in more content being sent over the wire than a similar GDI-based app. The additional data may result in slower performance depending on network bandwidth and the size and frequency of updates. However, in most scenarios, on reasonably fast connections, this is not an issue and in some scenarios (e.g. complex 3D scenes) remoting bitmaps can even present an advantage. Noticeable performance issues can appear in low bandwidth situations and when there is significant amount of data that must be remoted. For example, fast scrolling text file, playing video, or lots of animations.
Window 7 & Windows Server 2008 R2 changes
Windows Server 2008 R2 and Windows 7 operating systems includes a much improved Remote Desktop, called RDP 7.0. Read more in the RDP white-paper.
In addition to these improvements, the Windows 7 "Remote Desktop Connection" (RDC) client has few other changes. The interesting ones, which can have big impact on WPF performance and that we will discuss in more details later are:
It turns out that using 32-bit Color Depth, can improve many scenarios, however not all scenarios will improve.
The charts (taken from the RDP white-paper) show that PowerPoint scenarios would produce better RDP performance using RDP 7.0 with 32bpp Color Depth over RDP 7.0/6.1 16bpp. However, you are better off using 16bpp if you care more about better scrolling in Word/IE8.
Visual Studio 2010 over RDP is one of the scenarios that will benefit from switching to 16-bit color on slow connections.
It is important to note that in Remote Desktop scenario, even though the Client can specify that it wants to use 32-bit color, the Server can override this and set a maximum color depth.
By default, Windows 2008 Server/Windows 2008 R2 set the limit to 16-bpp color (On W2k8R2 see: "Start->Administrative Tools->Remote Desktop Services"->Remote Desktop Session Host COnfiguration". Right Click on the connection name listed).
Tuning RDP for optimal Visual Studio 2010 usage
As you may already know, many (but not all) UI components of VS 2010 are built using WPF 4.0 for the very first time. This implies that over RDP, VS 2010 remotes bitmaps for the portions of the UI that are built using WPF and remote GDI primitives for the non-WPF UI. The Editor is one of the VS 2010 UI features which are WPF-based.
During VS 2010 development, we worked really hard to optimize the Editor performance especially during continuous Scroll. By optimizing the Editor for the local scenario, we also improved the RDP scenario. Some of these principles we used are listed below.
We used a network emulator tool to simulate a slow network and measured amount of data transmitted on the wire during Editor scrolling. We also solicited feedback from users on the other side of the world who remoted to VS 2010 on machines in Redmond and compared the experience to Visual Studio 2008 side-by-side.
Some of our key findings were: Over fast network with little latency:
VS 2010 RDP performance approximately matches the performance of VS 2010 on a local machine with average GPU performance.
VS 2010 RDP performance almost matches VS 2008 RDP performance
Over slow network with high latency (we simulated 512 kbps, 75 msec delay each way to imitate a typical coast-to-coast connection):
VS 2010 RDP performance significantly slower than VS08 if the default 32-bit color and the "LAN (10Mbps or higher)" are kept on Windows7-to-Windows7 connection.
VS 2010 RDP performance is only slightly slower than VS08 when we changed to 16-bit color and selected "WAN (10Mbps or higher with high latency)" or "Satellite (2Mbps or higher with high latency)".
The chart and table below show we can improve VS 2010 scrolling perf by ~74% (from 82 sec to 22 sec) on a slow network just by picking the right RDC settings!
Page Scroll Scenario
RDP6/Vista 16bpp LAN Settings
Win7 16bpp - LAN settings
Win7 32bpp - LAN settings
Win7 16bpp - WAN settings
Win7 32bpp - WAN settings
Win7 16bpp - SAT settings
Win7 32bpp - SAT settings
VS 2010
28sec
28
82
22
75
65
We saw similar results in other VS 2010 scenarios, such as using menus, Intelisense, etc
In summary, to improve your VS 2010 perf over RDP consider:
If running over slow connection (e.g. slow DSL connection, for example 768kbs or lower), change RDC setting to use 16-bit color. This could significantly reduce number of bytes sent across the RDP wire. (Windows XP/vista already uses 16-bit color by default)
If running over high latency connection, for example when connecting from coast–to-coast (e.g. 150+ms round-trip latency), set "WAN (10Mbps or higher with high latency)" or "Satellite (2Mbps or higher with high latency)". Consider doing so as long as you have latency, even if the bandwidth is fast! This helps the underlying Remote Desktop client decide how to do its optimizations and improve perf. You can determine your latency by doing "ping <your_server>" from a command line. (Windows XP/Vista do not have these settings by default)
In Windows XP/Vista: Install, "Remote Desktop Connection 7.0 client update" and repeat steps from above. In addition to providing improve performance, installing RDP 7.0 it also allows you to select "high latency" in the "Remote Desktop Connection" Experience Tab UI which is not available in RDC 6.1/5.2.
Disable all check boxes (e.g. Font Smoothing, Visual Styles, etc) other than "Persistence bitmap caching" in the Experience tab. This also helps reduce number of bytes sent across the RDP wire.
Consider selecting lower size for your Remote Desktop window in the RDC settings. This will send smaller size bitmaps across the wire.
The above steps should help especially in low-bandwidth scenarios. As always check if performance actually improves after you made these changes. You may need to revert these changes if you use other apps or connect to another target machine with different network characteristics.
Optimizing your WPF app for RDP
In addition to selecting the correct RDC settings mentioned earlier, reducing the amount of content that needs to be sent across the wire is the key for better performance.
Your app for example, can detect that it is running over RDP and reduce the amount of content that needs to be updated and sent as bitmaps over the wire. It can also use graphic elements that can be compressed more efficiently by RDC bitmap compressor (e.g. use solid colors vs. gradients). In addition, since WPF renders the content on the server in software, the app should avoid using elements that are slow to render in software. Here are some more specific guidelines of what you can do:
Reduce the amount of UI updates while running over RDP. You can even consider doing so only when connection is slow (techniques on how to detect connection bandwidth is not provided here). Some of the common concepts are:
If possible consider turning off all animations during Remote Desktop session. For example: Creating a window background to an animating gradient or a video will cause the entire window to redraw every frame and be transmitted over the wire.
WPF does not currently ignore occluded parts of your app, so turn off all animations that are hidden behind some other opaque element.
For animations that you must keep, use the DesiredFrameRate property to decrease animation frame-rate and reduce traffic.
How we optimized Visual Studio 2010 for Remote Desktop
Visual Studio 2010 uses many of the approaches mentioned above. VS 2010 has a new "Enable rich client visual experience" checkbox in the Tools/Option menu. By default, the "Automatically adjust visual experience based on client performance" option is checked and Visual Studio 2010 will try to choose settings that are optimized for best performance. For example, if Visual Studio 2010 detects that it is running in a Remote Desktop session (as well as for other scenarios such as Virtual Machine environment or on machine w/ low-end graphics where WPF rendering tier return 0 or 1) then some visual effects are turned off. Here is a non-exhaustive list of things that happen in that situation:
In addition, regardless of whether “Enable rich client visual experience” is turned on or off, during rapid scrolling in the text editor, VS 2010: Postpones selected visual updates (e.g. the outlining margin and underline squiggles) until Idle. Reduces the number of layouts during thumb-tracking in the vertical scrollbar (when the scroll thumb is moved rapidly up or down).
In addition, regardless of whether “Enable rich client visual experience” is turned on or off, during rapid scrolling in the text editor, VS 2010:
Resources:
Below are related useful blogs:
Attached sample:
Below (also attached) sample code show how you can listen to the various Remote Desktop events and as discussed earlier you can use these events to trigger your app "RDP" features. In my sample, the window background is comprised of animating gradient. The app detects a Remote Desktop session connection and then switches the window background to be a solid color and vice versa. If I keep the animated background, then the entire window is invalidated and would have to be transmitted over the wire on each frame (e.g. 60 frame-per-seconds on most monitors). With animation removed and by using a solid color instead no network traffic is needed. Here is the network traffic I measured when I was experimenting with my sample:
App runs for 5 seconds (1280x1024)
When background animates
When background uses solid color
Amount of RDP traffic over 5 sec:
11 Mb
5 bytes
As you can see this would make a huge difference in low-bandwidth scenarios.
Figure 1- Animated background when run locally
Figure 2- Solid background when over Remote Desktop
1: <Window x:Class="Animation_Sample_For_RDP.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Sample RDP-aware WPF application" Height="600" Width="750"
5: Loaded="Window_Loaded" Unloaded="Window_Unloaded"
6: xmlns:src="clr-namespace:Animation_Sample_For_RDP" >
7:
8: <Window.Resources>
9: <src:RemoteDesktopClass x:Key="myDataSource" />
10: </Window.Resources>
11: <Window.Template>
12: <ControlTemplate TargetType="{x:Type src:Window1}">
13: <Border Name="myBorder">
14: <Border.Background>
15: <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
16: <GradientStop Color="Green" Offset="0"/>
17: <GradientStop Color="LightBlue" Offset="0.2" x:Name="Foo"/>
18: <GradientStop Color="Blue" Offset="1"/>
19: </LinearGradientBrush>
20: </Border.Background>
21: <StackPanel Margin="20,0,0,0">
22: <StackPanel.DataContext>
23: <Binding Source="{StaticResource myDataSource}"/>
24: </StackPanel.DataContext>
25: <StackPanel Margin="20,0,0,0" Orientation="Horizontal">
26: <TextBlock Margin="0,15,0,0" FontWeight="Bold">Remote Desktop Session status:</TextBlock>
27: <TextBlock Margin="5,15,0,0" Name="RD_status"
28: Text="{Binding Path=RemoteDesktopStatus, UpdateSourceTrigger=PropertyChanged}"/>
29: </StackPanel>
30: <StackPanel Margin="20,0,0,0" Orientation="Horizontal">
31: <TextBlock Margin="0,15,0,0" FontWeight="Bold">In Remote Desktop Session:</TextBlock>
32: <TextBlock Margin="5,15,0,0"
33: Text="{Binding Path=IsRemoteDesktopSession, UpdateSourceTrigger=PropertyChanged}"/>
34: </StackPanel>
35: </StackPanel>
36: </Border>
37: <ControlTemplate.Triggers>
38: <EventTrigger RoutedEvent="Window.Loaded">
39: <BeginStoryboard>
40: <Storyboard AutoReverse="True" BeginTime="0" >
41: <DoubleAnimation Storyboard.TargetName="Foo" Storyboard.TargetProperty="Offset"
42: AutoReverse="True" From="0.1" To="0.9" Duration="0:0:3" RepeatBehavior="Forever"/>
43: </Storyboard>
44: </BeginStoryboard>
45: </EventTrigger>
46: <!--Set to Solid background when a Remote Session was triggered -->
47: <DataTrigger Binding="{Binding Source={StaticResource myDataSource},
48: Path=IsRemoteDesktopSession}" Value="true">
49: <Setter TargetName="myBorder" Property="Border.Background" Value="LightBlue"/>
50: </DataTrigger>
51: </ControlTemplate.Triggers>
52: </ControlTemplate>
53: </Window.Template>
54: </Window>
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Windows;
6: using System.Windows.Controls;
7: using System.Windows.Data;
8: using System.Windows.Documents;
9: using System.Windows.Input;
10: using System.Windows.Media;
11: using System.Windows.Media.Imaging;
12: using System.Windows.Navigation;
13: using System.Windows.Shapes;
14: using System.Windows.Interop;
15: using System.Runtime.InteropServices;
16: using System.ComponentModel;
17: using System.Windows.Threading;
18:
19: namespace Animation_Sample_For_RDP
20: {
21: public partial class Window1 : Window
22: {
23: [DllImport("wtsapi32.dll", SetLastError = true)]
24: static extern bool WTSRegisterSessionNotification(IntPtr hWnd, [MarshalAs(UnmanagedType.U4)] int dwFlags);
25: [DllImport("wtsapi32.dll", SetLastError = true)]
26: static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd); // constants passed in the dwFlags parameter
27: const int NOTIFY_FOR_THIS_SESSION = 0;
28: const int NOTIFY_FOR_ALL_SESSIONS = 1; // message id to look for when processing the message (see sample code)
29: const int WM_WTSSESSION_CHANGE = 0x2b1; // WParam values that can be received:
30: const int WTS_CONSOLE_CONNECT = 0x1; // A session was connected to the console terminal.
31: const int WTS_CONSOLE_DISCONNECT = 0x2; // A session was disconnected from the console terminal.
32: const int WTS_REMOTE_CONNECT = 0x3; // A session was connected to the remote terminal.
33: const int WTS_REMOTE_DISCONNECT = 0x4; // A session was disconnected from the remote terminal.
34: const int WTS_SESSION_LOGON = 0x5; // A user has logged on to the session.
35: const int WTS_SESSION_LOGOFF = 0x6; // A user has logged off the session.
36: const int WTS_SESSION_LOCK = 0x7; // A session has been locked.
37: const int WTS_SESSION_UNLOCK = 0x8; // A session has been unlocked.
38: const int WTS_SESSION_REMOTE_CONTROL = 0x9; // A session has changed its remote controlled status.
39: public enum SystemMetric
40: {
41: SM_REMOTESESSION = 0x1000,
42: SM_REMOTECONTROL = 0x2001,
43: }
44: [DllImport("user32.dll")]
45: static extern int GetSystemMetrics(SystemMetric smIndex);
46: static IntPtr hwnd;
47: static Window1 myClass;
48: public Window1()
49: {
50: InitializeComponent();
51: myClass = this;
52: }
53: private void Window_Loaded(object sender, RoutedEventArgs e)
54: {
55: Window w = Application.Current.MainWindow;
56:
57: WindowInteropHelper wih = new WindowInteropHelper(this);
58: hwnd = wih.Handle;
59:
60: if (!WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION))
61: MessageBox.Show("WTSRegisterSessionNotification failure");
62: HwndSource MainWindowHwndSource = PresentationSource.FromVisual(w) as HwndSource;
63: if (MainWindowHwndSource != null)
64: MainWindowHwndSource.AddHook(new HwndSourceHook(MainWindowHwndMessageFilter));
65: else
66: Console.WriteLine("MainWindowHwndSource == null");
67:
68: if (GetSystemMetrics(SystemMetric.SM_REMOTECONTROL) != 0)
69: Console.WriteLine("Session is currently remotely controlled.");
70: else
71: Console.WriteLine("Session is NOT currently remotely controlled.");
72: if (GetSystemMetrics(SystemMetric.SM_REMOTESESSION) != 0)
73: {
74: Console.WriteLine("Process is associated with a Terminal Services client session.");
75: SetRemoteSessionStatus("WTS_CONSOLE_CONNECT");
76: SetRemoteSessionState(true); // we are over Remote Session !
77: }
78: else
79: {
80: Console.WriteLine("Process is NOT associated with a Terminal Services client session.");
81: SetRemoteSessionState(false); // we are connected locally !
82: }
83: }
84: static void SetRemoteSessionStatus(string status)
85: {
86: myClass.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
87: {
88: RemoteDesktopClass db = myClass.FindResource("myDataSource") as RemoteDesktopClass;
89: db.RemoteDesktopStatus = status;
90: }));
91: }
92: static void SetRemoteSessionState(bool b)
93: {
94: myClass.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
95: {
96: RemoteDesktopClass db = myClass.FindResource("myDataSource") as RemoteDesktopClass;
97: db.IsRemoteDesktopSession = b;
98:
99: }));
100: }
101: static IntPtr MainWindowHwndMessageFilter(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
102: {
103: switch (msg)
104: {
105: case WM_WTSSESSION_CHANGE:
106:
107: Console.Write("WM_WTSSESSION_CHANGE ");
108: int param = ((int)wParam);
109: switch (param)
110: {
111: case WTS_CONSOLE_CONNECT:
112: Console.Write("WTS_CONSOLE_CONNECT\n");
113: SetRemoteSessionStatus("WTS_CONSOLE_CONNECT");
114: SetRemoteSessionState(false); // we are connected locally !
115: break;
116: case WTS_CONSOLE_DISCONNECT:
117: Console.Write("WTS_CONSOLE_DISCONNECT\n");
118: SetRemoteSessionStatus("WTS_CONSOLE_DISCONNECT");
119: break;
120: case WTS_REMOTE_CONNECT:
121: Console.Write("WTS_REMOTE_CONNECT\n");
122: SetRemoteSessionStatus("WTS_REMOTE_CONNECT");
123: SetRemoteSessionState(true); // we are over Remote Session !
124: break;
125: case WTS_REMOTE_DISCONNECT:
126: Console.Write("WTS_REMOTE_DISCONNECT\n");
127: SetRemoteSessionStatus("WTS_REMOTE_DISCONNECT");
128: break;
129: case WTS_SESSION_LOGON:
130: Console.Write("WTS_SESSION_LOGON\n");
131: SetRemoteSessionStatus("WTS_SESSION_LOGON");
132: break;
133: case WTS_SESSION_LOGOFF:
134: Console.Write("WTS_SESSION_LOGOFF\n");
135: SetRemoteSessionStatus("WTS_SESSION_LOGOFF");
136: break;
137: case WTS_SESSION_LOCK:
138: Console.Write("WTS_SESSION_LOCK\n");
139: SetRemoteSessionStatus("WTS_SESSION_LOCK");
140: break;
141: case WTS_SESSION_UNLOCK:
142: Console.Write("WTS_SESSION_UNLOCK\n");
143: SetRemoteSessionStatus("WTS_SESSION_UNLOCK");
144: break;
145: case WTS_SESSION_REMOTE_CONTROL:
146: Console.Write("WTS_SESSION_REMOTE_CONTROL\n");
147: SetRemoteSessionStatus("WTS_SESSION_REMOTE_CONTROL");
148: break;
149: }
150: break;
151: default:
152: break;
153: }
154: return IntPtr.Zero;
155: }
156: private void Window_Unloaded(object sender, RoutedEventArgs e)
157: {
158: if (!WTSUnRegisterSessionNotification(hwnd))
159: Console.WriteLine("WTSUnRegisterSessionNotification failure");
160: }
161: }
162: public class RemoteDesktopClass : INotifyPropertyChanged
163: {
164: string _RemoteDesktopStatus;
165: bool _IsRemoteDesktopSession;
166: public RemoteDesktopClass()
167: {
168: }
169: public RemoteDesktopClass(string value)
170: {
171: this.RemoteDesktopStatus = value;
172: }
173: public string RemoteDesktopStatus
174: {
175: get { return _RemoteDesktopStatus; }
176: set
177: {
178: _RemoteDesktopStatus = value;
179: OnPropertyChanged("RemoteDesktopStatus");
180: }
181: }
182: public bool IsRemoteDesktopSession
183: {
184: get { return _IsRemoteDesktopSession; }
185: set
186: {
187: _IsRemoteDesktopSession = value;
188: OnPropertyChanged("IsRemoteDesktopSession");
189: }
190: }
191: public event PropertyChangedEventHandler PropertyChanged; //OnPropertyChanged -update property value in binding
192: private void OnPropertyChanged(String info)
193: {
194: if (PropertyChanged != null)
195: PropertyChanged(this, new PropertyChangedEventArgs(info));
196: }
197: }
198: }
Don't the fonts look terrible and unreadable if you disable font smoothing?
Ihis depends on the scenarios and fonts being used.
That is also why it says “…you are improving responsiveness but sacrificing some visual "prettiness"” …
(Remember that WPF content will remote them as bitmaps regardless.)
Changing the font to something like Courier New seems to make VS 2010 over RDP much more responsiveness.
Thx for all the configuration info but are you referring to the host computer or the remote computer?
Its a crucial detail.
Hi Brent, If you refer to the table above, it says "Connecting From:" which means the Host computer.
I feel it's a bit ridiculous that it's recommended to disable animations during remote desktop, but there's no built in way in the framework to do just that in one go.
We're expected to retemplate every control with animations in order to remove them. It's not even just a quick retemplating either, every template/animation will have to have a ton of triggers and bindings setup just to detect if animations are disabled or not.
In reality, WPF needs to allow for a global setting that can be used to disable all animations application-wide.
I am currently developing a website in VS2010 on a remote machine. It runs on a fast computer (Intel i7), using a relatively fast connection (3Mbps).
The remote desktop is quite fast, even with all visual enhancements enabled. The large project compiles in just a few seconds.
Then, considerable delays arise when VS2010 starts up the project in the Development Server on the remote machine. The Development Server should only run locally on the remote machine, but it seems like it's using the RDP connection speed.
It would be great if there were a setting that affects or controls this behavior.