With the Load Test Editor, you can configure one load test profile for a given scenario. However, in the real-world testing, you may want to observe the performance of your applications / system under tests under different load patterns. And this can be achieved by writing custom load profile management classes and a load test plug-in. (Sample code included and available for downloading from codeplex)

For example, in the below graph for a 30-min Load run, several load profiles, including const, stepUp, stepDown, CustomSinWave, are simulated about every 7.5 minutes.

image

This post will show you step by step on how to

1) Create customized user load profiles and profile group management classes (Covered in Step 1)

2) Simulate multiple and repeated user load patterns in a load test run (Covered in Step 2)

Step1 – Create CustomLoadProfiles Library

In the sample code, I have implemented two *.cs files to manage the custom load patterns.

CustomLoadProfiles.cs – This file contains

-- Two custom Step load profiles. One for linear stepping down and the other for simulating user load oscillations with a Math.Sin() curve.

-- CustomLoadProfile wrapper classes for existing and customized LoadTestLoadProfiles. A wrapper class allows you to specify the duration you want to simulate the user pattern.

LoadProfileManager.cs – This files contains two classes.

-- LoadProfileGroup class

   The class contains all the profiles you want to simulate. For example, a const load profile for 60 seconds, a sin wave for 120 seconds, and a step down load for 40 seconds.  You  can specify the duration of the profile group. For example, if the duration of this group is set to 20 min. The Load test plug-in will cycle through all the load profiles added to this group till the end of 20th minute if the duration of the load test is longer than 20 mins.

-- LoadProfileGroupManagers class

   It allows you to simulate multiple pattern groups in your run. It will find the right load test profile to be applied at run time.

1) Create a class library and add the reference to the Microsoft.VisualStudio.TestTools.LoadTesting.

2) Create the custom load profiles. (CustomLoadProfiles.cs)

Details on how to create custom load profiles can be found in http://blogs.msdn.com/billbar/pages/load-test-api-enhancements-in-vsts-2008-sp1-beta.aspx.

Add the following code to your library and add your own custom load profiles. 

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.LoadTesting;

// CustomLoadProfiles.cs
// Add more cutom profiles in this file
namespace CustomLoadProfiles
{
    #region Custom LoadProfile

    // The base class for customized load profile
    // Add different validations as needed
    [Serializable]
    public class CustomLoadProfile : LoadTestStepLoadProfile
    {
        int m_currentUserLoad;
        public CustomLoadProfile()
        {
            m_currentUserLoad = 0;
        }

        public int CurrentUserLoad
        {
            get { return m_currentUserLoad; }
            set { m_currentUserLoad = value; }
        }

        public override int GetLoad(int elaspsedTimeInProfile)
        {
            return 0;
        }

        public override void  Validate() {}
    }

    // Custom step up/down - Sin wave simulation
    // y(t) = A * sin(2* PI * f * t / unitTimeInterval)
    // t - time
    // f - number of oscillations occur in an unit time interval
    [Serializable]
    public class LoadTestSinLoadProfile : CustomLoadProfile
    {
        int m_amplitude;
        int m_unitTimeInterval;
        int m_freq;
        public LoadTestSinLoadProfile(int initCount, int amplitude, int unitTmeInterval, int frequency, int stepDuration)
        {
            InitialUserCount = initCount;
            StepDuration = stepDuration;
            m_amplitude = amplitude;
            m_unitTimeInterval = unitTmeInterval;
            m_freq = frequency;
        }

        public override int GetLoad(int elapsedTimeInProfile)
        {
            if (elapsedTimeInProfile % StepDuration == 0)
            {
               CurrentUserLoad = CalculateUserLoad(elapsedTimeInProfile);
            }

            return (int)Math.Max(CurrentUserLoad, MinUserCount);
        }

        // If you are familiar with Sin curves, you can modify the formula to calculate user load
        int CalculateUserLoad(int elapsedTimeInProfile)
        {
            double userLoad = 0;
            userLoad = Math.Sin(2* Math.PI * m_freq * elapsedTimeInProfile / m_unitTimeInterval) * m_amplitude;
            userLoad = InitialUserCount + userLoad;

            return (Int32)(userLoad);
        }
    }

    // Custom step down - linear
    [Serializable]
    public class LoadTestStepDownLoadProfile : CustomLoadProfile
    {
        public LoadTestStepDownLoadProfile(int initCount, int minCount, int maxCount, int stepCount, int stepDuration)
        {
            InitialUserCount = initCount;
            StepUserCount = stepCount;
            StepDuration = stepDuration;
            MinUserCount = minCount;
            MaxUserCount = maxCount;
            CurrentUserLoad = initCount;
        }

        public override int GetLoad(int elapsedTimeInProfile)
        {
            if (elapsedTimeInProfile % StepDuration == 0)
            {
                CurrentUserLoad -= StepUserCount;
            }

            return (int)Math.Max(CurrentUserLoad, MinUserCount);
        }
    }

    #endregion

    #region CustomLoadProfile Warpper Classes
    // The base class (wrapper) for a customized load profile
    public abstract class LoadProfileWrapper
    {
        bool m_inUse;
        int m_duration;  // How long this pattern will be simulated

        public int Duration
        {
            get { return m_duration; }
            set { m_duration = value; }
        }

        public bool InUse
        {
            get { return m_inUse; }
            set { m_inUse = value; }
        }

        public abstract LoadTestLoadProfile Profile
        {
            get;
            set;
        }

        public virtual void UpdateProfileStatus(bool inUse, int currentUserLoad)
        {
            InUse = inUse;
        }
    }

    public class CustomLoadWrapper : LoadProfileWrapper
    {
        LoadTestLoadProfile m_profile;
        public CustomLoadWrapper(int patternDuration)
        {
            m_profile = new LoadTestConstantLoadProfile(); // instantiate it as an const load file
            InUse = false;
            Duration = patternDuration;
        }

        public override LoadTestLoadProfile Profile
        {
            get { return m_profile; }
            set { m_profile = (LoadTestLoadProfile)value; }
        }

        public override void UpdateProfileStatus(bool inUse, int currentUserCount)
        {
            InUse = inUse;
            if (m_profile is CustomLoadProfile)
            {
                ((CustomLoadProfile)m_profile).CurrentUserLoad = currentUserCount;
            }
        }
    }

    #endregion
}

3) Add the LoadProfileManager.cs to your library

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.LoadTesting;

namespace CustomLoadProfiles
{
    // This the pattern group class
    // It contains the list of profiles to be simulated in a specified time window
    // Each group can contain several load test profiles
    // During a load run, the test engine will cycle through the LoadTestProfiles and ajust the user load
    public class LoadProfileGroup
    {
        string m_name;
        List<LoadProfileWrapper> m_profileGroup;
        int m_startTime;
        int m_endTime;
        int m_duration;
        int m_patternCycleDuration;
        int m_timeInGroup;
        int m_previousCycleCount;

        public LoadProfileGroup(string name, int duration)
        {
            m_name = name;
            m_duration = duration;
            m_timeInGroup = 0;
            m_previousCycleCount = 0;
            m_patternCycleDuration = 0;
            m_profileGroup = new List<LoadProfileWrapper>();
        }

        public void AddProfile(LoadProfileWrapper profile)
        {
            m_profileGroup.Add(profile);
            m_patternCycleDuration += profile.Duration;
        }

        // The load profiles to be simulated
        public List<LoadProfileWrapper> LoadProfiles
        { get { return m_profileGroup; } }

        public int StartTime
        {
            get { return m_startTime; }
            set { m_startTime = value; }
        }

        public int EndTime
        {
            get { return m_endTime; }
            set { m_endTime = value; }
        }

        public int Duration
        { get { return m_duration; } }

        public String Name
        { get { return m_name; } }

        // This function will find out what load test profile will be used for current heartbeat event
        // If no change will be made, null will be returned
        public LoadTestLoadProfile GetLoadProfile(int elapsedTime, int currentUserLoad)
        {
            // Calculate the elapased time in the group
            m_timeInGroup = elapsedTime - m_startTime;

            int currentCycleCount = m_timeInGroup / m_patternCycleDuration;

            // Reset the InUse status of each profile to false at the beginning of each cycle
            if (m_previousCycleCount < currentCycleCount && m_timeInGroup % m_patternCycleDuration != 0)
            {
                m_previousCycleCount++;

                for (int i = 0; i < LoadProfiles.Count; i++)
                {
                    LoadProfileWrapper profileWrapper = LoadProfiles[i];
                    profileWrapper.InUse = false;
                }
            }

            // Find out if new load test profile should be applied
            int durationLength = 0;
            foreach (LoadProfileWrapper profileWrapper in LoadProfiles)
            {
                durationLength += profileWrapper.Duration;

                if ((m_timeInGroup == 0) || // the first profile
                    (m_timeInGroup > 0 && m_timeInGroup % m_patternCycleDuration <= durationLength))
                {
                    if (!profileWrapper.InUse)
                    {
                        // Found the new profile to be applied
                        profileWrapper.UpdateProfileStatus(true, currentUserLoad);
                        return (LoadTestLoadProfile)profileWrapper.Profile;
                    }

                    break;
                }
            }

            return null;
        }
    }

    /// <summary>
    /// Manages all pattern groups to be simulated in the load test run
    /// </summary>
    public class LoadProfileManager
    {
        List<LoadProfileGroup> m_profileGroups;
        private string previousGroup;

        public LoadProfileManager()
        {
            m_profileGroups = new List<LoadProfileGroup>();
        }

        // All the profiles included in this pattern
        public List<LoadProfileGroup> ProfileGroups
        {
            get { return m_profileGroups; }
            set { m_profileGroups = value; }
        }

        // Get the current Load profile based on the elapsed time
        public LoadTestLoadProfile GetCurrentLoadProfile(int elapsedSeconds, int currentUserLoad)
        {
            LoadTestLoadProfile profile = null;
            bool foundProfileGroup = false;

            foreach (LoadProfileGroup group in m_profileGroups)
            {
                // Get the pattern group to be simulated
                if (elapsedSeconds <= group.EndTime && elapsedSeconds > group.StartTime)
                {
                    foundProfileGroup = true;

                    if (!String.Equals(previousGroup, group.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        previousGroup = group.Name;
                    }

                    // Get the profile
                    profile = group.GetLoadProfile(elapsedSeconds, currentUserLoad);
                    break;
                }
            }

            // If no profile group is found, the load test run will continue with a const load

            if (foundProfileGroup == false)
            {
                profile = new LoadTestConstantLoadProfile();
                ((LoadTestConstantLoadProfile)profile).UserCount = currentUserLoad;
            }

            return profile;
        }

        // Set the start/end times for each pattern group
        public void SetStartEndTimeForProfileGroups(int elapsedSeconds)
        {
            int prevGroupEndTime = elapsedSeconds;
            int duration = 0;
            foreach (LoadProfileGroup group in m_profileGroups)
            {
                // set the start time
                group.StartTime = prevGroupEndTime;

                // set the end time
                duration += group.Duration;
                group.EndTime = duration;

                // update the start time for next pattern group
                prevGroupEndTime = duration;
            }
        }
    }
}

4) Add the class library for assembly binding

Drop the class library to the %Program Files\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies to avoid any serialization issues at run-time.

If you want the initialization code of load profiles in the Load test plug-in to be simple and clean,  you can add the wrapper classes for the build-in load profiles to CustomLoadProfiles.cs

// The const load profile wrapper
    public class ConstLoadWrapper : LoadProfileWrapper
    {
        LoadTestConstantLoadProfile m_profile;
        public ConstLoadWrapper(int userCount, int duration)
        {
            m_profile = new LoadTestConstantLoadProfile();
            m_profile.UserCount = userCount;
            Duration = duration;
        }

        public override LoadTestLoadProfile Profile
        {
            get { return m_profile; }
            set { m_profile = (LoadTestConstantLoadProfile)value; }
        }
    }

    // The goal based load profile wrapper
    public class GoalBasedLoadWrapper : LoadProfileWrapper
    {
        LoadTestGoalBasedLoadProfile m_profile;
        public GoalBasedLoadWrapper(int initialCount, int minCount, int maxCount, int maxIncrement, int maxDecrement, int patternDuration)
        {
            m_profile = new LoadTestGoalBasedLoadProfile();
            m_profile.InitialUserCount = initialCount;
            m_profile.MinUserCount = minCount;
            m_profile.MaxUserCount = maxCount;
            m_profile.MaxUserCountIncrease = maxIncrement;
            m_profile.MaxUserCountDecrease = maxDecrement;
            Duration = patternDuration;
        }

        public void SetCounter(string machine, string category, string counter, string instance, bool stopAdjustingAtGoal)
        {
            m_profile.MachineName = machine;
            m_profile.InstanceName = instance;
            m_profile.CategoryName = category;
            m_profile.CounterName = counter;
            m_profile.StopAdjustingAtGoal = stopAdjustingAtGoal;
        }

        public override LoadTestLoadProfile Profile
        {
            get { return m_profile; }
            set { m_profile = (LoadTestGoalBasedLoadProfile)value; }
        }
    }

    // The step load profile wrapper
    public class StepLoadWrapper : LoadProfileWrapper
    {
        LoadTestStepLoadProfile m_profile;
        public StepLoadWrapper(int initCount, int minCount, int maxCount, int stepCount, int stepDuration, int stepRampTime, int patternDuration)
        {
            m_profile = new LoadTestStepLoadProfile();
            m_profile.InitialUserCount = initCount;
            m_profile.MinUserCount = minCount;
            m_profile.MaxUserCount = maxCount;
            m_profile.StepUserCount = stepCount;
            m_profile.StepDuration = stepDuration;
            m_profile.StepRampTime = stepRampTime;
            InUse = false;
            Duration = patternDuration;
        }

        public override LoadTestLoadProfile Profile
        {
            get { return m_profile; }
            set { m_profile = (LoadTestStepLoadProfile)value; }
        }
    }