Click here to Skip to main content
15,881,757 members
Articles / DevOps / Load Testing

Measuring and Monitoring WCF Web Service Performance

Rate me:
Please Sign up or sign in to vote.
5.00/5 (17 votes)
4 Oct 2012GPL310 min read 55.2K   2.2K   47  
Using ServiceMon to obtain performance statistics for web services
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Framework;
using Kaleida.ServiceMonitor.Model;
using Kaleida.ServiceMonitor.Model.Parsing;
using Kaleida.ServiceMonitor.Model.Runtime;
using log4net;
using System.Linq;
using Monitor = Kaleida.ServiceMonitor.Model.Monitor;
using Timer = System.Timers.Timer;

namespace Kaleida.ServiceMonitor.UI
{
    public partial class MainForm : Form
    {
        private readonly Monitor monitor;

        private readonly string initialScriptName;
        
        private bool autoStart;
        
        private bool windowIsActive = true;

        private static readonly ILog log = LogManager.GetLogger(typeof (MainForm));
        private readonly NotifyIcon trayIcon;
        private readonly ContextMenu trayMenu;
        private readonly Timer pollTimer = new Timer();
        private bool updatingUI;
        readonly IList<INotificationPanel> notificationPanels;
        readonly ISet<object> naughtyList = new HashSet<object>();
        private AboutControl aboutControl;
        private readonly IList<IServiceMonExtension> operationWatchers;

        public MainForm([CanBeNull]string scriptName = "")
        {
            var configuration = new FileBasedConfiguration();
            monitor = new Monitor(configuration);
            initialScriptName = scriptName ?? configuration.InitialScriptName ?? "";

            Application.ThreadException += UnhandledExceptionHandler;

            TryInitialiseFromConfig();

            trayMenu = new ContextMenu();
            trayMenu.MenuItems.Add("Exit", TrayIconMenuExitClicked);

            trayIcon = new NotifyIcon {ContextMenu = trayMenu, Visible = true};
            trayIcon.MouseUp += OnTrayIconClick;

            InitializeComponent();

            lblScriptSummary.Text = "";
            
            workspaceControl.Workspace = monitor.Workspace;
            workspaceControl.ScriptDirectoryPath = ServiceMon.ScriptPath;

            monitor.ScriptCompilationComplete += ScriptCompilationComplete;
            monitor.Workspace.CurrentScriptChanged += SelectedScriptChanged;

            monitor.State.MonitorStarted += OnStartedMonitoring;
            monitor.State.MonitorStopped += OnStoppedMonitoring;
            
            monitor.OperationSuccessful += OperationSuccessful;
            monitor.OperationFailed += OperationFailed;
            monitor.ErrorsAcknowledged += ErrorsAcknowledged;
            
            operationWatchers = BuildOperationWatchers();


            notificationPanels = NotificationPanelFactory.BuildPanels();
            AddNotificationTabPanels();
            AddHelpTabPage();
        }

        private IList<IServiceMonExtension> BuildOperationWatchers()
        {
            var watchers = new List<IServiceMonExtension>
                               {
                                   new ThresholdChangeEmailer(monitor),
                                   new ScheduledSummaryEmailer(monitor)
                               };

            return watchers;
        }

        private void AddNotificationTabPanels()
        {
            var notificationTabPages = notificationPanels.Select(BuildAndInitialiseNotificationPanel);
            tabControl.TabPages.AddRange(notificationTabPages.Cast<TabPage>().ToArray());
        }

        private NotificationPanelTabPage BuildAndInitialiseNotificationPanel(INotificationPanel panel)
        {
            var page = new NotificationPanelTabPage(panel);
            panel.Initialise(monitor);
            return page;
        }

        private void AddHelpTabPage()
        {
            aboutControl = new AboutControl {Dock = DockStyle.Fill};
            var tabPage = new TabPage("Help");
            tabPage.Controls.Add(aboutControl);
            tabControl.TabPages.Add(tabPage);
        }

        public void TryInitialiseFromConfig()
        {
            try
            {
                autoStart = monitor.Configuration.AutoStart;
            }
            catch (Exception exception)
            {
                MessageBox.Show(string.Format("Error Reading config: {0}", exception));
            }
        }

        private void UnhandledExceptionHandler(object sender, ThreadExceptionEventArgs e)
        {
            log.Fatal(e.Exception);
            MessageBox.Show(string.Format("ERROR: {0}\r\nSee log files for more information", e.Exception.Message), "An unexpected error occured", MessageBoxButtons.OK, MessageBoxIcon.Error);
            Application.Exit();
        }

        protected override void OnLoad(EventArgs e)
        {
            ScriptParser.Initialise(ServiceMon.OperationsPath);
            
            aboutControl.PopulateOperationsHelp(ScriptParser.GetRuntimeEnvironment());
            
            UpdateWindowTitle();

            Icon = ApplicationIcon.Application;

            components = new Container();
            components.Add(trayIcon);

            pollTimer.Elapsed += PollTimerTick;

            workspaceControl.LoadScriptsFromDisk();
            workspaceControl.TrySelectScript(initialScriptName);

            monitor.AcknowledgeErrors();

            if (autoStart && monitor.State.HasSuccessfullyCompiledScript)
            {
                StartMonitoring();
            }
            else
            {
                StopMonitoring();
            }

            Visible = false;

            ThreadPool.QueueUserWorkItem(i => CheckForNewVersion());

            base.OnLoad(e);
        }

        private void CheckForNewVersion()
        {
            var versionCheck = new CodePlexWebVersionCheck();
            var result = versionCheck.CheckLatestVersion();

            BeginInvoke(new EventHandler(delegate
                                             {
                                                 newVersionLink.Text = "";
                                                 if (!result.Success)
                                                 {
                                                     aboutControl.SetVersionCheckStatus("Could not check for new version", result.Exception.Message);
                                                 }
                                                 else
                                                 {
                                                     if (ServiceMon.VersionNumber == result.LatestVersionNumber)
                                                     {
                                                         aboutControl.SetVersionCheckStatus("This is the latest recommended version");
                                                     }
                                                     else if (ServiceMon.VersionNumber > result.LatestVersionNumber)
                                                     {
                                                         aboutControl.SetVersionCheckStatus("This is a pre-release version. The latest recommended version is v" + result.LatestVersionNumber);
                                                     }
                                                     else
                                                     {
                                                         var linkText = string.Format("Version '{0}' is available. Click to download", result.LatestVersionNumber);
                                                         
                                                         newVersionLink.Text = linkText;
                                                         newVersionLink.Tag = result.LatestVersionUrl;
                                                         newVersionLink.Visible = true;
                                                     }
                                                 }
                                             }));
        }

        private void UpdateWindowTitle()
        {
            var title = new StringBuilder();

            title.AppendFormat("{0} - ", monitor.Workspace.CurrentScript.Name);

            title.AppendFormat("ServiceMon v{0}", ServiceMon.VersionNumber);
            Text = title.ToString();
        }

        private void OperationSuccessful(object sender, OperationSuccessfulEventArgs e)
        {
            SendMessageToExtensions(i => i.OnOperationSuccessful(e));
        }

        private void OperationFailed(object sender, OperationFailedEventArgs e)
        {
            SendMessageToExtensions(i => i.OnOperationFailed(e));
        }

        private void ErrorsAcknowledged(object sender, EventArgs e)
        {
            SendMessageToExtensions(i => i.OnErrorsAcknowledged());
        }

        private void SelectedScriptChanged(object sender, EventArgs e)
        {
            SendMessageToNotificationPanels(i => i.RefreshContent());

            UpdateWindowTitle();

            tabControl.SelectTab(tabPageScript);
        }

        private void SendMessageToExtensions(Action<IServiceMonExtension> message)
        {
            foreach (var watcher in operationWatchers)
            {
                if (naughtyList.Contains(watcher)) continue;

                try
                {
                    message(watcher);
                }
                catch (Exception exception)
                {
                    naughtyList.Add(watcher);
                    log.Error(string.Format("OperationWatcher '{0}' threw an unhandled exception: {1}", watcher.GetType().Name, exception));
                }
            }
        }

        private void SendMessageToNotificationPanels(Action<INotificationPanel> message)
        {
            foreach (var notificationPanel in notificationPanels)
            {
                if (naughtyList.Contains(notificationPanel)) continue;

                try
                {
                    message(notificationPanel);
                }
                catch (Exception exception)
                {
                    naughtyList.Add(notificationPanel);

                    log.Error(string.Format("Panel '{0}' threw an unhandled exception: {1}", notificationPanel.PanelName, exception));
                    notificationPanel.GetPanelControl().Controls.Clear();

                    var errorMessage = new StringBuilder();
                    errorMessage.AppendFormat("An unhandled Exception occured: {0}\r\n\r\n", exception.Message);
                    errorMessage.AppendFormat("This panel will not be sent new messages until ServiceMon is restarted\r\n");
                    errorMessage.AppendFormat("Please see logs for more information");

                    var errorLabel = new Label
                                         {
                                             AutoSize = true,
                                             ForeColor = Color.Red,
                                             Text = errorMessage.ToString()
                                         };

                    notificationPanel.GetPanelControl().Controls.Add(errorLabel);
                }
            }
        }

        private void ScriptCompilationComplete(object sender, ScriptCompilationCompleteEventArgs args)
        {
            var compilation = args.ScriptCompilation;

            lock (this)
            {
                if (compilation.Succeeded)
                {

                    monitor.Statistics.Initialise(compilation.CompiledScript.Operations);
                    pollFrequencyControl.Value = compilation.CompiledScript.PollFrequency;

                    SendMessageToExtensions(i => i.ScriptCompilationComplete(args));
                }
            }
        }


        private void OnTrayIconClick(object o, EventArgs e)
        {
            ShowInTaskbar = true;
            Visible = true;
            WindowState = FormWindowState.Normal;
        }

        protected override void OnResize(EventArgs e)
        {
            if (WindowState == FormWindowState.Minimized)
            {
                ShowInTaskbar = false;
                Visible = false;
            }
            else
            {
                base.OnResize(e);
            }
        }

        private void TrayIconMenuExitClicked(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void StartMonitoring()
        {
            if (!monitor.State.CanBeginMonitoring)
                return;

            monitor.State.StartMonitoring();

            if (tabControl.SelectedTab == tabPageScript)
            {
                TrySelectFirstNotificationPanel();
            }

            ThreadPool.QueueUserWorkItem(i => monitor.InvokeNextOperation());
        }

        private void TrySelectFirstNotificationPanel()
        {
            var firstPanel = tabControl.TabPages.Cast<TabPage>().FirstOrDefault(i => i is NotificationPanelTabPage);
            if (firstPanel != null)
            {
                tabControl.SelectTab(firstPanel);
            }
        }

        private void StopMonitoring()
        {
            if (!monitor.State.IsMonitoring)
                return;

            monitor.State.StopMonitoring();
            
            if (monitor.State.ScriptCompilation != null && monitor.State.ScriptCompilation.Succeeded)
                pollFrequencyControl.Value = monitor.State.ScriptCompilation.CompiledScript.PollFrequency;
        }

        private void OnStartedMonitoring(object sender, EventArgs args)
        {
            lock (this)
            {
                SendMessageToExtensions(i => i.OnStartedMonitoring());
                UpdateTimerFromState();
            }
        }

        private void OnStoppedMonitoring(object sender, EventArgs args)
        {
            lock (this)
            {
                UpdateTimerFromState();
            }
        }

        private void UpdateTimerFromState()
        {
            pollTimer.Interval = PollIntervalMilliseconds;
            pollTimer.Enabled = monitor.State.IsMonitoring;
        }

        private string GetSystemTrayTitle(string message)
        {
            var title = new StringBuilder();
            title.AppendFormat("{0}: ", monitor.Workspace.CurrentScript.Name);
            title.Append(message);

            return title.ToString();
        }

        private void SetSystemTray(string message, Icon icon)
        {
            var text = GetSystemTrayTitle(message);

            var shortText = text.Abbreviate(60);

            text = shortText;

            trayIcon.Icon = new Icon(icon, 16, 16);
            trayIcon.Text = text;
        }

        private void UpdateCurrentScriptSummary()
        {
            var compilation = monitor.State.ScriptCompilation;
            if (compilation == null)
            {
                lblScriptSummary.Text = "";
                lblScriptSummary.ForeColor = Color.Black;
            }
            else if (compilation.Succeeded)
            {
                lblScriptSummary.Text = compilation.CompiledScript.BuildSummary();
                lblScriptSummary.ForeColor = Color.Black;
            }
            else
            {
                var message = "Cannot Parse Script: " + compilation.Exception.Message;
                SetSystemTray(message, ApplicationIcon.Error);

                lblScriptSummary.Text = message;
                lblScriptSummary.ForeColor = Color.Red;
            }
        }

        private void UpdateUIFromModel()
        {
            lblPluginErrorSummary.Text = naughtyList.Any() ? "Error with plugin. Please see log." : "";
            lblPluginErrorSummary.Visible = naughtyList.Any();

            btnStart.Enabled = monitor.State.CanBeginMonitoring;
            btnStop.Enabled = monitor.State.IsMonitoring;
            
            pollFrequencyControl.Enabled = monitor.State.IsMonitoring;

            btnAckError.Enabled = monitor.State.HasOperationFailures;

            var focusedControl = FindFocusedControl(this);
            SendMessageToNotificationPanels(i=>i.RefreshContent());

            if (windowIsActive &&  focusedControl!= null)
            {
                focusedControl.Focus();
            }
            
            Icon systemTrayIcon = GetSystemTrayIcon();

            string statusSummary = monitor.State.DefaultPresentation.GetStateSummary();
            SetSystemTray(statusSummary, systemTrayIcon);

            string statusBarMessage = statusSummary + (monitor.State.IsMonitoring ? " " + GetSpinner() : "");
            tsStatusLabel.SetMessage(statusBarMessage, monitor.State.HasScriptError || monitor.State.HasOperationFailures ? Color.Red : monitor.State.IsMonitoring ? Color.Green : Color.Black);

            UpdateCurrentScriptSummary();
        }

        [CanBeNull]
        public static Control FindFocusedControl(Control control)
        {
            var container = control as ContainerControl;
            while (container != null)
            {
                control = container.ActiveControl;
                container = control as ContainerControl;
            }
            return control;
        }

        private Icon GetSystemTrayIcon()
        {
            return monitor.State.HasOperationFailures || monitor.State.HasScriptError
                       ? ApplicationIcon.Error
                       : monitor.State.IsMonitoring ? ApplicationIcon.Running : ApplicationIcon.Stopped;
        }

        private string GetSpinner()
        {
            const int width = 100;
            var position = monitor.Statistics.Combined.ProcessedCount % width;

            return new string('.', position) + new string(' ', width - position);
        }

        private int PollIntervalMilliseconds
        {
            get
            {
                var duration = pollFrequencyControl.Value;
                return (int) (duration.ToTimeSpan().TotalMilliseconds);
            }
        }

        private void StartButtonClick(object sender, EventArgs e)
        {
            log.DebugFormat("Starting to poll every {0}ms", PollIntervalMilliseconds);
            StartMonitoring();
        }

        private void StopButtonClick(object sender, EventArgs e)
        {
            log.Debug("Stopping poll");
            StopMonitoring();
        }

        private void UIUpdateTimerTick(object sender, EventArgs e)
        {
            if (updatingUI) return;

            updatingUI = true;
            UpdateUIFromModel();
            updatingUI = false;
        }

        private void PollTimerTick(object sender, EventArgs e)
        {
            monitor.InvokeNextOperation();
        }

        private void pollFrequencyControl_ValueChanging(object sender, PollFrequencyValueChangingEventArgs e)
        {
            e.NewValue = PollFrequencyLimit.EnsureMet(e.NewValue);
        }

        private void pollFrequencyControl_ValueChanged(object sender, EventArgs e)
        {
            pollTimer.Interval = PollIntervalMilliseconds;
        }

        private void AcknowledgeErrorClick(object sender, EventArgs e)
        {
            monitor.AcknowledgeErrors();
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = workspaceControl.AllowUserToSaveDirty();
        }
        
        private static void BrowseTo(string url)
        {
            var sInfo = new ProcessStartInfo(url);
            Process.Start(sInfo);
        }

        private void newVersionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            var url = (string) newVersionLink.Tag;

            if (!string.IsNullOrEmpty(url))
            {
                BrowseTo(url);
            }
        }

        private void MainForm_Enter(object sender, EventArgs e)
        {
            windowIsActive = true;
        }

        private void MainForm_Leave(object sender, EventArgs e)
        {
            windowIsActive = false;
        }

        private void MainForm_Resize(object sender, EventArgs e)
        {
            var isMini = Height < 300 || Width < 580;

            statusStrip.Visible = !isMini;
            pnlTopBar.Visible = !isMini;
            newVersionLink.Visible = newVersionLink.Text != "" && !isMini;

            if (isMini)
            {
                tabControl.Dock = DockStyle.Fill;
            }
            else
            {
                tabControl.Dock = DockStyle.None;
                tabControl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
                tabControl.Location = new Point(0, 36);
                tabControl.Size = new Size(Width - 16, Height - 100);
            }

            lblScriptSummary.Visible = !(Height < 450);
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Architect BlackJet Software Ltd
United Kingdom United Kingdom
Stuart Wheelwright is the Principal Architect and Software Developer at BlackJet Software Ltd.

He has over 16 years commercial experience producing robust, maintainable, web-based solutions and bespoke systems for Microsoft platforms.

His latest project is Shopping UK, an elegantly simple shopping list for iPhone.

Comments and Discussions