Introduction
Many articles have been written about the advantages of moving CPU bound tasks from the user interface (UI) thread to a background thread. But what if the background threads are creating the problem via a high message rate? A high rate of message displays to the UI will create response problems that mirror a CPU intensive task running in the UI thread. It is even possible all interaction with the form may be blocked until the high volume of data is reduced or stopped. The GuiMonitor
class solves the problem by dynamically monitoring the form's responsiveness.
The GuiMonitor
class works with ListBox
and ListView
controls and has an option that allows you to customize usage for any control. Your code must be modified to use the GuiMonitor
class.
Background
Window forms receive events from the message queue which is also known as the message pump. Mouse clicks and key presses are examples of events as are timer events and Invoke
calls from non-UI threads. If the user interface thread is too busy, for any reason, events on the message queue will queue up thus delaying events. If event handling is delayed too long, the form is perceived as unresponsive.
The .NET framework supports three timer classes: System.Windows.Forms.Timer
, System.Timers.Timer
and System.Threading.Timer
. The latter two timer events execute in a worker thread obtained from the common language runtime thread pool. Code running in a worker thread can not act upon UI components. System.Windows.Forms.Timer
executes in the application's UI thread where UI control modification is allowed. Furthermore, events raised by the System.Windows.Forms.Timer
are processed only when the GUI thread is not busy. These characteristics make the System.Windows.Forms.Timer
almost perfect for determining form responsiveness.
You can read more about the differences between the timers at
Monitoring Form Responsiveness
The GuiMonitor
class takes advantage of the timer event being delivered via the message queue. The form's responsiveness is measured by scheduling a timer event to run approximately ten times per second. The UI is deemed to be responsive as long as timer events scheduled by the GuiMonitor
class occur within the expected time ranges. If two consecutive timer events are missed, responsiveness is deemed to be unacceptable and corrective action is taken. Corrective action either changes the operational mode of the UI control or requests a brief pause from routines writing to the UI. The GuiMonitor
has four operational modes: normal, accelerated, delay and extended delay.
Normal mode does nothing more than monitor the timer events scheduled by the GuiMonitor
. When two consecutive timer events are missed, the GuiMonitor
class will shift to accelerated mode.
Accelerated mode provides the ability to handle brief periods of high data volume. In accelerated mode, a BeginUpdate()
function is issued for the ListBox
or ListView
control. This BeginUpdate()
function temporarily stops displaying messages which will allow the processing of many more messages per second than when the control is in normal display mode. If scheduled timer events are being met, an EndUpdate()
function is issued and normal mode is resumed. However, if timer events continue to be missed, further corrective action is needed so an EndUpdate()
command is issued to the control and the GuiMonitor
class enters the delay mode.
In the delay mode, the GuiMonitor
class will issue a request asking for a brief delay in the routines producing the data. If scheduled events are being met, the ListBox
or ListView
control will return to normal mode with no delays requested. Otherwise, the GuiMonitor
class will enter the extended delay mode.
In extended delay mode, the delay period will will be periodically increased until scheduled timer events are being met at which point the ListBox
or ListView
control will be returned to normal display mode with no requested delay times.
The Sample Code
Download the source code for a working example of the GUI monitor class in action. The example allows you to try ListBox
and ListView
controls with and without monitoring and/or automatic scrolling.
Without the GuiMonitor
, the form will "freeze" with a high number of messages and multiple threads. You can easily see the freeze or the lack thereof by moving your cursor over the various buttons. The buttons will quickly change color with a responsive screen. Pressing either of the "Test response" buttons will display a message just under the buttons. These actions will be delayed for non-responsive screens.
"Maximum random delay" allows you to simulate random delivery times for messages. A positive number will cause a delay of the specified milliseconds between each message. A negative number generate a random delay between zero and the positive value of the specified number of milliseconds.
The "Turn message display off" button toggles between message displaying or not. It illustrates how fast the accelerated mode is.
"Autoscroll", "Maximum random delay" and "Turn message display off" may be changed while a test is running.
The more writer threads you start in the example, the more likely you will create a non-responsive form, especially on multi-core machines. Your form should never be non-responsive when the GUI monitor is activated.
Using the code
The GuiMonitor
class fully supports the ListBox
or ListView
controls via two constructors. A third constructor performs monitoring only. This allows the programmer to provide support for any other type of form control or to provide customized handling of the ListBox
or ListView
controls.
I will first describe how to use the ListBox
and ListView
constructors and then explain the more generic constructor.
ListBox and ListView Constructors
This example uses a ListBox
but almost the same code applies equally well to a ListView
. Begin by copying the GuiMonitor
class into your test program.
Now, in your test program, define the object and start the monitor.
using RAMSystems;
GuiMonitor guiMonitor = new GuiMonitor(listBox1);
guiMonitor.Enabled = true;
If automatic scrolling is being used, inform the guiMonitor object when scrolling is turned on or off. For example:
guiMonitor.AutoScroll = AutoScrollCheckBox.Checked;
Inform the guiMonitor object when the GUI control is updated. The GuiMonitor.TotalMessages
property must be updated when the UI is updated otherwise the UI response time can not be checked. Any value may be used for the update.
listBox1.Items.Add(text);
guiMonitor.TotalMessages = listBox1.Items.Count;
Delay and extended delay modes require cooperation from threads writing to the control. There are two techniques. Both techniques assume a delegate has been set up and initialized.
private delegate int DelegateSendText(string textLine);
private DelegateSendText m_DelegateSendText;
m_DelegateSendText = this.SendText;
The first example illustrates a caller that honors the pause request.
private int SendText(string textLine)
{ if (this.InvokeRequired)
{ try { this.Invoke(m_DelegateSendText, textLine); }
catch (Exception) { }
}
else
{ listBox1.Items.Add(textLine);
if (guiMonitor.AutoScroll)
listBox1.TopIndex = listBox1.Items.Count - 1;
guiMonitor.TotalMessages = listBox1.Items.Count;
}
return guiMonitor.PauseValue;
} private void WriterThread(string myTextMessage)
{
int pause = SendText(myTextMessage);
if(0 != pause) Thread.Sleep(pause); }
The second example illustrates a caller that does not honor the pause request. The pause request is honored in the SendText function.
private delegate void DelegateSendText(string textLine);
private DelegateSendText m_DelegateSendText;
m_DelegateSendText = this.SendText;
private void SendText(string textLine)
{ if (this.InvokeRequired)
{ try { this.Invoke(m_DelegateSendText, textLine); }
catch (Exception) { }
if (guiMonitor.PauseValue > 0)
Thread.Sleep(guiMonitor.PauseValue);
}
else
{ listBox1.Items.Add(textLine);
if (guiMonitor.AutoScroll)
listBox1.TopIndex = listBox1.Items.Count - 1;
guiMonitor.TotalMessages = listBox1.Items.Count;
}
} private void WriterThread(string myTextMessage)
{
SendText(myTextMessage);
}
Do not issue the Thread.Sleep
in the GUI thread!
The update to TotalMessages
is very important. The GuiMonitor
uses this call to check for missed timer events. You may stop the monitor when updates to the GUI are complete. It is not an error to leave the monitor running but the 100 millisecond timer continues to run resulting in some light overhead. Use the following command to stop monitoring.
guiMonitor.Enabled = false;
You may monitor or customize the delay values by adding a GuiMonitor.DelegateNotifyModePauseValueChange
delegate to the constructor's parameter list. The delegate is called when either the mode and/or pause value changes. This allows you to coordinate or influence other controls on the form. You may change the value of the PauseValue property while in the GuiMonitor.DelegateNotifyModePauseValueChange
function but not the mode.
You may monitor or customize the delay values by adding a GuiMonitor.DelegateNotifyModePauseValueChange
delegate to the constructor's parameter list. The delegate is called when either the mode and/or pause value changes. This allows you to coordinate or influence other controls on the form. You may change the value of the PauseValue property while in the GuiMonitor.DelegateNotifyModePauseValueChange
function but not the mode.
int myPauseValue = 0; guiMonitor = new GuiMonitor(listBox1, this.HandlePauseValueChanged );
private void HandlePauseValueChanged(GuiMonitor.TrafficMode newMode,
int newValue)
{
switch (newMode)
{
case GuiMonitor.TrafficMode.Normal:
case GuiMonitor.TrafficMode.Accelerated:
myPauseValue = newValue;
break;
case GuiMonitor.TrafficMode.Delayed:
myPauseValue = guiMonitor.PauseValueMilliseconds + (workerThreads.Count * 2);
break;
case GuiMonitor.TrafficMode.ExtendedDelay:
myPauseValue += guiMonitor.IncreasedDelaySinceExtended);
break;
}
}
The Generic Constructor
GuiMonitor myGuiMonitor = new GuiMonitor(ModeSwitchRoutine);
The generic constructor is useful if the high CPU usage is from a control not supported by the GuiMonitor
class. The GuiMonitor
class will handle the scheduling and monitoring of timer events and call the function you provided via the constructor when an operating mode change is needed. Your routine should interpret the suggested operating mode to alleviate the delay.
The DataGridView
, GridView
and DataTable
classes are commonly used and would be perfect candidates for the GuiMonitor
except for the problem with BeginLoadData()
/ EndLoadData()
; checks for identical data already in the data tables fail when issued during the load operation. Microsoft states the BeginLoadData()
function "Turns off notifications, index maintenance, and constraints while loading data.". I suggest ignoring the "Accelerated" mode and going directly to the "Delayed" mode if using "LoadData" operations with a data table.
The sample code uses a status toolbar to demonstrate the generic constuctor. Realistically, the update of the status line was so fast I was never able to cause a delay problem.
Other ListBox / ListView functions
Use the GuiMonitor
version of BeginUpdate / EndUpdate
and SuspendLayout / ResumeLayout
when it is known in advance that a large number of changes will be made to a control. Monitoring is temporarily suspended between the begin and end calls.
Additional Class Properties
performance/statistical properties that may be of interest are:
EnteredAcceleratedMode number of times accelerated mode was entered
EnteredDelayedMode number of times delays were requested
IncreasedDelaySinceExtended number of times a delay increase was requested
Points of Interest
Checking response time ten times per second was a guess on my part; too short an interval slows down the user's program while too long an interval is ineffective. One tenth of a second seemed to be a reasonable compromise.
My original project supported only a ListBox
. The ListBox
is capable of holding and displaying more than 64K items but manually scrolling to the end of the list was problematic. Run the demo program in ListBox mode with message display off and auto scroll on. Produce over 64K of messages and turn the message display on. The display will show you the last entry as expected until you use the scroll bar to position to the first message and then attempt to reposition to the last message. The ListView
did not have a problem until the number of items in the list reached 100 million. I consider this to be more a scrollbar problem but it definitely impacks the design of your display.
History
October, 2015 Initial release