Sunday, August 31, 2008

Hey, whaddya know? It works!

I finished the first part of my task to create a custom NAnt task that utilizes the SDC Tasks that were originally built for MSBuild. What I am interested in is deleting and creating custom AppPools for IIS as well as deleting and creating vdirs bound to custom AppPools and not DefaultAppPool.

I did some quick and dirty coding today and came up with this first pass:



   1:  using System;

   2:  using System.Text;

   3:  using NAnt.Core;

   4:  using NAnt.Core.Attributes;

   5:  using Microsoft.Sdc.Tasks;

   6:  using Microsoft.Build.Utilities;

   7:  using Microsoft.Build.Framework;

   8:  namespace Auswipe.NAntTasks.AppPoolTasks {

   9:    # region DeleteAppPool

  10:    [TaskName("deleteapppool")]

  11:    public class DeleteAppPool : NAnt.Core.Task {

  12:      private string appPoolName;

  13:      private string machineName;

  14:      [TaskAttribute("apppoolname", Required = true)]

  15:      [StringValidator(AllowEmpty = false)]

  16:      public string AppPoolName {

  17:        get { return appPoolName; }

  18:        set { appPoolName = value; }

  19:      }

  20:      [TaskAttribute("machinename", Required = true)]

  21:      [StringValidator(AllowEmpty = false)]

  22:      public string MachineName {

  23:        get { return machineName; }

  24:        set { machineName = value; }

  25:      }

  26:      protected override void ExecuteTask() {

  27:        Project.Log(Level.Info, "Auswipe.NAntTasks.AppPoolTasks : The following AppPool will be deleted: '" + appPoolName + "'");

  28:        try {

  29:          Microsoft.Sdc.Tasks.Web.DeleteAppPool.DeleteAppPool delAppPoolObject = new Microsoft.Sdc.Tasks.Web.DeleteAppPool.DeleteAppPool();

  30:          delAppPoolObject.AppPoolName = appPoolName;

  31:          delAppPoolObject.MachineName = machineName;

  32:          if (delAppPoolObject.Execute()) {

  33:            Project.Log(Level.Info, "Auswipe.NAntTasks.AppPoolTasks : The AppPool '" + appPoolName + "' was successful deleted.");

  34:          } else {

  35:            Project.Log(Level.Error, "Auswipe.NAntTasks.AppPoolTasks : ERROR: The AppPool '" + appPoolName + "' was NOT deleted but an exception was not thrown.");

  36:          };

  37:        } catch (Exception e) {

  38:          Project.Log(Level.Error, "Auswipe.NAntTasks.AppPoolTasks : AppPool deletion failed with the resulting exception:");

  39:          Project.Log(Level.Error, e.ToString());

  40:        };

  41:      }

  42:    }

  43:    #endregion

  44:    # region Create AppPool

  45:    [TaskName("createapppool")]

  46:    public class CreateAppPool : NAnt.Core.Task {

  47:      private string appPoolName;

  48:      private string userName;

  49:      private string passWord;

  50:      private string userDomain;

  51:      private string machineName;

  52:      [TaskAttribute("apppoolname", Required = true)]

  53:      [StringValidator(AllowEmpty = false)]

  54:      public string AppPoolName {

  55:        get { return appPoolName; }

  56:        set { appPoolName = value; }

  57:      }

  58:      [TaskAttribute("username", Required = true)]

  59:      [StringValidator(AllowEmpty = false)]

  60:      public string UserName {

  61:        get { return userName; }

  62:        set { userName = value; }

  63:      }

  64:      [TaskAttribute("password", Required = true)]

  65:      [StringValidator(AllowEmpty = false)]

  66:      public string PassWord {

  67:        get { return passWord; }

  68:        set { passWord = value; }

  69:      }

  70:      [TaskAttribute("machinename", Required = true)]

  71:      [StringValidator(AllowEmpty = false)]

  72:      public string MachineName {

  73:        get { return machineName; }

  74:        set { machineName = value; }

  75:      }

  76:      protected override void ExecuteTask() {

  77:        Project.Log(Level.Info, "Auswipe.NAntTasks.AppPoolTasks : The following AppPool will be created: '" + appPoolName + "'");

  78:        try {

  79:          Microsoft.Sdc.Tasks.Web.AppPool.Create createAppPoolObject = new Microsoft.Sdc.Tasks.Web.AppPool.Create();

  80:          createAppPoolObject.AppPoolName  = appPoolName;

  81:          createAppPoolObject.IdentityType = "SpecifiedUserAccount";

  82:          createAppPoolObject.Identity     = userName;

  83:          createAppPoolObject.Password     = passWord;

  84:          createAppPoolObject.MachineName  = machineName;

  85:          if (createAppPoolObject.Execute()) {

  86:            Project.Log(Level.Info, "Auswipe.NAntTasks.AppPoolTasks : The AppPool '" + appPoolName + "' was successfuly created.");

  87:          } else {

  88:            Project.Log(Level.Error, "Auswipe.NAntTasks.AppPoolTasks : ERROR: The AppPool '" + appPoolName + "' was NOT created but an exception was not thrown.");

  89:          };

  90:        } catch (Exception e) {

  91:          Project.Log(Level.Error, "Auswipe.NAntTasks.AppPoolTasks : AppPool creation failed with the resulting exception:");

  92:          Project.Log(Level.Error, e.ToString());

  93:        };

  94:      }

  95:    }

  96:    #endregion

  97:  }



After I created my Auswipe.NAntTasks.AppPoolTasks.dll I had to copy both Auswipe.NAntTasks.AppPoolTasks.dll and Microsoft.Sdc.Tasks.dll to the subdir where I put my default.build that I've been playing with so that the code could properly execute and create my AppPool.

Here is my default.build script for NAnt:

8<-----------------------------------------


   1:   

   2:  <?xml version="1.0"?>

   3:  <project name="Task Test" default="test-task" basedir="." xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd">

   4:   

   5:    <loadtasks assembly="Auswipe.NAntTasks.AppPoolTasks.dll" />

   6:   

   7:    <target name="test-task">    

   8:      <echo message="Deleting AppPool."/>

   9:      <deleteapppool apppoolname = "AuswipeAppPool"

  10:                     machinename = "Auswipel-rfs2we"

  11:       />

  12:   

  13:      <echo message="Create AppPool."/>

  14:      <createapppool apppoolname = "AuswipeAppPool"

  15:                     username    = "AuswipeAppPoolUser"

  16:                     password    = "supersekrit"

  17:                     machinename = "Auswipel-rfs2we"

  18:      />

  19:    </target>                                   

  20:      

  21:  </project>


----------------------------------------->8

And here is the output from the execution:

8<-----------------------------------------


   1:  C:\customtask>nant /f:default.build test-task

   2:  NAnt 0.85 (Build 0.85.2478.0; release; 10/14/2006)

   3:  Copyright (C) 2001-2006 Gerry Shaw

   4:  http://nant.sourceforge.net

   5:   

   6:  Buildfile: file:///C:/customtask/default.build

   7:  Target framework: Microsoft .NET Framework 2.0

   8:  Target(s) specified: test-task

   9:   

  10:  [loadtasks] Scanning assembly "Auswipe.NAntTasks.AppPoolTasks" for extensions.

  11:   

  12:  test-task:

  13:   

  14:       [echo] Deleting AppPool.

  15:  Auswipe.NAntTasks.AppPoolTasks : The following AppPool will be deleted: 'AuswipeAppPool'

  16:  Auswipe.NAntTasks.AppPoolTasks : The AppPool 'AuswipeAppPool' was successful deleted.

  17:       [echo] Create AppPool.

  18:  Auswipe.NAntTasks.AppPoolTasks : The following AppPool will be created: 'AuswipeAppPool'

  19:  Creating app pool "AuswipeAppPool".

  20:  Auswipe.NAntTasks.AppPoolTasks : The AppPool 'AuswipeAppPool' was successfuly created.

  21:   

  22:  BUILD SUCCEEDED

  23:   

  24:  Total time: 0.1 seconds.


----------------------------------------->8

Here we see a screen shot of the system before NAnt execution:



Now I execute the NAnt script and re-fresh the IIS manager view of AppPools and lo-and-behold! It worked!



I checked the properties of the newly created AppPool and we see that it has the credentials I specified in the default.build. Hazzah!



Now the next step is to go forth and code the deletion and creation of vdirs with the SDC Tasks.

Nifty!

Friday, August 29, 2008

Creating my own custom NAnt tasks in C#

One of the things that I find myself doing when not crunching numbers as a performance enginerd is learning the ways of the Buildmiester. We use NAnt for deploying our code from the build server to a target test machine and UAT test environment.

During the process we use NAnt to delete and create vdirs under IIS with mkiisdir which is handy dandy and all but it doesn't fully get the job done as some of the vdirs we need should not belong to DefaultAppPool. And also unfortunately it appears that NAntContrib doesn't have all the tools that I need to delete and create a custom AppPool that I need to create with a special local username/password.

I found the command line code to create the AppPool that I want and I was just going to wrap the execution of the command line stuff with an <exec> task but I still couldn't find how to create a vdir on the command line and have it binding to my custom AppPool that I wanted.

This morning after Scrum another coder working on a sister project that uses MSBuild instead of NAnt suggested that I take a gander at the SDC Tasks for MSBuild up on CodePlex. After pulling down the latest and greatest SDC Tasks it appears that I can delete, then create my custom AppPool and then delete/create a customer vdir bound to my custom AppPool. How great is that?

Except that SDC Tasks was designed for MSBuild and we are already heavily into NAnt. Bummer, right? Well. I know that NAnt has a <msbuild> task but going from NAnt into MSBuild that would reference a custom MSBuild script and pass the params around seemed like a real PITA, not to mention a documentation nightmare. But then again, I really want this functionality of the SDC Tasks.

What do I do? What do I do?

After consulting the Great Google, I decided that it was time to "Man up, Nancy Boy!" as one of the female trainers at my gym would say (actually shout during a middle of a grueling spin class with the grunting and sweating and the pain!) and learn how to code my own custom NAnt task and build a custom wrapper for the SDC stuff so that I could invoke the functionality from within my NAnt script.

After consulting some blogs and some pain and suffering I've been able to create a basic useless NAnt task. I haven't written the wrapper code yet, but I think that'll be simple compared to actually getting a custom NAnt task up and running.

This is what I did to create my very own useless NAnt task.

First, I created a .NET Class project by the name of "NAntCustomAuswipeTasks." It seems to be very important that the assembly name postfix be "Tasks.dll." The second thing is that it is important to use class decorators for specifying various things that NAnt needs to get the job done. Two of those being "TaskName" and "TaskAttribute."

Another important thing that I learned is that when calling the task from inside the NAnt script that the task name is case sensitive to the decorator string.

Below is my code for my useless NAnt task:



   1:  using System;

   2:  using NAnt.Core;

   3:  using NAnt.Core.Attributes;

   4:  namespace NAntCustomAuswipeTasks {

   5:    [TaskName("auswipetask")]

   6:    public class AuswipeTask : Task {

   7:      private string customData;

   8:      [TaskAttribute("customdata", Required = true)]

   9:      [StringValidator(AllowEmpty = false)]

  10:      public string CustomData {

  11:        get { 

  12:          return customData; 

  13:        }

  14:        set { 

  15:          customData = value; 

  16:        }

  17:      }

  18:      protected override void ExecuteTask() {

  19:        Project.Log(Level.Info, "[AuwipeTask] The parameter passed is '" + customData + "'.");

  20:      }

  21:    }

  22:  }



Take notice of the TaskName, TaskAttribute and StringValidator decorators. Very important or otherwise your attempts to call your custom NAnt task will fail. The blog entry that I was learning on didn't have the TaskName decorator and so my call to my task was failing. Very frustrating.

After compiling into my assembly of "NAntCustomAuswipeTasks.dll" I copied the DLL into the same subdir where my NAnt build file resides. In my case, this is a folder off of my Desktop subdir where I am playing around checking to see if it really works. I'm using a default.build file right now just to make things simple:

8<--------------------------------
C:\Users\Auswipe\Desktop\NAntTask>dir
Volume in drive C has no label.
Volume Serial Number is A463-3DC5

Directory of C:\Users\Auswipe\Desktop\NAntTask

08/29/2008 11:44 PM <DIR> .
08/29/2008 11:44 PM <DIR> ..
08/29/2008 11:44 PM 404 default.build
08/29/2008 11:45 PM 4,608 NAntCustomAuswipeTasks.dll
2 File(s) 5,012 bytes
2 Dir(s) 332,392,656,896 bytes free
-------------------------------->8

8<--------------------------------


   1:  <?xml version="1.0"?>

   2:  <project name="Custom NAnt Task Test" default="test-task" basedir="." xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd">

   3:    <loadtasks assembly="NAntCustomAuswipeTasks.dll" />

   4:    <target name="test-task">

   5:      <echo message="Here goes nothing...." />

   6:      <auswipetask customdata="Killroy was here." />

   7:    </target>

   8:  </project>


-------------------------------->8

And here I am testing out my useless NAnt task:

8<--------------------------------
C:\Users\auswipe\Desktop\NAntTask>nant /f:default.build test-task
NAnt 0.85 (Build 0.85.2478.0; release; 10/14/2006)
Copyright (C) 2001-2006 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///C:/Users/auswipe/Desktop/NAntTask/default.build
Target framework: Microsoft .NET Framework 2.0
Target(s) specified: test-task

[loadtasks] Scanning assembly "NAntCustomAuswipeTasks" for extensions.

test-task:

[echo] Here goes nothing....
[AuwipeTask] The parameter passed is 'Killroy was here.'.

BUILD SUCCEEDED

Total time: 0 seconds.

-------------------------------->8

Notice that I go ahead and specify the default.build file on the command line along with the target despite having the test-task as the defacto target of default.build and knowing full well that NAnt looks for default.build. Why'd I do that? Just because I hate seeing demonstrations of some technology where the author assumes that the readers know all the ins and out of the subject. Sure, I can get the same results with `nant` as I did with `nant /f:default.build test-task` but having been in the RTFM mode before, I can gleam more information from `nant /f:default.build test-task` and learn the rest later. But that is just my own opinion. YMMV, IANAL, ROFLWTFBBQ.

So, now that I've created my useless NAnt task successfully I can work on wrapping the SDC Tasks that I need to make my world (or at least my deploy) a better place. And the less time I'm spending cleaning up after a deploy changing settings is more time I can be sitting on my butt surfing the web, or something like that.

Thursday, August 28, 2008

Interesting blog post about Apdex

Here is an interesting post from all around smart dude, Tim Koopmans at 90kts.com concerning reporting response time to managers and other non-techy folks and it is interesting.

Check it out here.

Basically, it is a way to gonkulate a value and color code scheme to show if the response time for a transaction falls within acceptable limits for the type of transaction.

Checking out http://www.apdex.org/using.html you can see some of the target response times for the Apdex coefficient gonkulations.

This might be a better a cool way to supplement the way that I am planning on comparing before/after tests with Z-score values and a lot easier for management folk.

Thursday, August 14, 2008

w00t! I can keep Hyper-V after all!

I can keep Hyper-V after all! How did I get back Client for Microsoft Networking do you ask?

In my intarweb searchings I came across this blog entry.

Here is the solution in a nutshell.

Blow away any pre-existing virtual NICs with the Hyper-V manager. With your physical NIC go ahead and un-bind all protocols. Once that is done, go back into Hyper-V manager and re-create a virtual NIC and viola, you'll have a virtual NIC with Client for Microsoft Networks bound. Just reboot your machine and it should be good to go.

Back to using Hyper-V for prototyping! Yay!

Thanks, Jeff! You rock!

Thursday, August 7, 2008

Coding like Ike and Tina...

I'm working on a WebTestRequest that changes an address and in the post back the POST data contains a bunch of information that is displayed in the HTML and I need to extract data from text boxes, check boxes and drop down lists. Extracting the textboxes and checkboxes are pretty simple, it is the drop down boxes that are a pain.

The .NET HTML parser treats the HtmlTags that reside inside the SELECT tag separately so I cannot just get the attributes for the select and then slice and dice and get the default selected value for the drop in.

I tried to be "elegant" in my solution and use the HTML Agility Pack and a XPath pulled from FireBug but alas, there was no love to be found. I'm not sure what the exact problem is other than me being a total XPath n00b (I normally use an open source Java project to extract XPaths from XML docs) but something was amiss. After wasting several hours trying to get the elegant solution I decided to be like Ike and Tina and just brute force the solution of identifying the default selected value for the drop down lists. I populate the Context of the WebTestRequest with all the values that are extracted and just reference them by element name in the address change POST ala the HIDDEN1$ that VS2008 does for hidden field extraction and it seems to work pretty good.

Here is the extraction code that I wrote to pull the fields that I needed to reference in the Context:



   1:  void ExtractCustomerDataToContext(object sender, ExtractionEventArgs e) {

   2:    Dictionary<string, string> customerData = new Dictionary<string, string>();

   3:    string lastDropDown = null;

   4:    foreach (HtmlTag tag in e.Response.HtmlDocument.HtmlTags) {

   5:      string type = tag.GetAttributeValueAsString("type");

   6:      if (type != null) {

   7:        lastDropDown = null ;

   8:        if (type == "text" || type == "checkbox") {

   9:          if (tag.GetAttributeValueAsString("checked") != null) {

  10:            if (tag.GetAttributeValueAsString("checked") == "checked") {

  11:              customerData.Add(tag.GetAttributeValueAsString("name"), "on");

  12:            } else {

  13:              customerData.Add(tag.GetAttributeValueAsString("name"), "off");

  14:            };

  15:          } else {

  16:            customerData.Add(tag.GetAttributeValueAsString("name"), tag.GetAttributeValueAsString("value"));

  17:          };

  18:        };

  19:      } else {

  20:        if (tag.GetAttributeValueAsString("class") != null) {

  21:          if (tag.GetAttributeValueAsString("class") == "dropDownListDefault") {

  22:            lastDropDown = tag.GetAttributeValueAsString("name");

  23:          };

  24:        } else {

  25:          if (tag.GetAttributeValueAsString("selected") != null) {

  26:            if (lastDropDown != null) {

  27:              customerData.Add(lastDropDown, tag.GetAttributeValueAsString("value"));

  28:            };

  29:          };

  30:        };

  31:      };

  32:    };

  33:    foreach (KeyValuePair<string, string> kvp in customerData) {

  34:      if (this.Context.ContainsKey("CUSTOMER_UPDATE_DATA." + kvp.Key)) {

  35:        this.Context.Remove("CUSTOMER_UPDATE_DATA." + kvp.Key);

  36:      };

  37:      if (kvp.Value == null) {

  38:        this.Context.Add("CUSTOMER_UPDATE_DATA." + kvp.Key, "");

  39:      } else {

  40:        this.Context.Add("CUSTOMER_UPDATE_DATA." + kvp.Key, kvp.Value);

  41:      };

  42:    };

  43:  }



So, if the previous page HTML has a text field by the name of "txtCustomerAccountNumber" I can simply add that information to the post with:



   1:  changeAddressRequestBody.FormPostParameters.Add("txtCustomerAccountNumber", this.Context["CUSTOMER_UPDATE_DATA.txtCustomerAccountNumber"].ToString());



As long as the information was displayed via HTML in the prior hit, I should be good to go.

Wednesday, August 6, 2008

Revisiting CSV Data Binding in VS2008 Coded Web Tests

I've been going through the WinDBG labs located here. Tess has done a great job with the labs and done all the hard work, so w00t for her!

I needed to add another .CSV data file for my main VUser that I am currently working on so I did a copypasta of a pre-existing DataBinding and DataSource. When I first created the coded web test I had added so much custom code that I deleted the web test and left only the coded web test. The fear was that I might accidently re-gen the code from the web test and over write my work. That in and of itself shouldn't be too bad since we all know to use a code repository, right? Of course we do.

Anyhoo, I made the code changes and was greeted by an error when my file couldn't be loaded. Huh? How rude!

I finally got it to work, but I wanted to note the way .CSV data binding works in a coded web test in greater detail just to remind myself next time this happens. There are plenty of examples of binding a .CSV file to a web test, but few on a pre-existing coded web test. So, here goes!

First we have to have a DataSource declaration that takes five parameters for a .CSV file:

1: dataSourceName
This is the name we are giving the DSN for our .CSV file.
2: providerName
In my case for a .CSV file I am using "Microsoft.VisualStudio.TestTools.DataSource.CSV"
3: connectionString.
For my use, this is the path with escaped back-whacks to the .CSV file. In my case, it is "c:\\data\\streetNames.csv"
4: DataBindingAccessMethod
For my use, I want a random selection of data from the .CSV file so I am using "Microsoft.VisualStudio.TestTools.WebTesting.DataBindingAccessMethod.Random"
5: tableName
I thought that I could provide my own table name to reference the .CSV file but that didn't work and was forced to use the form of #csv, like this: "streetNames#csv". When I tried to use the name "streetNameTable" I would get back an error of "cannot find streetTableName.txt" from VS2008. But, the #csv form works, so no biggie.

Here is what I am using for my entire Data Source:



   1:    [DataSource("streetNameDataSource",

   2:              "Microsoft.VisualStudio.TestTools.DataSource.CSV",

   3:              "C:\\Data\\streetNames.csv",

   4:              Microsoft.VisualStudio.TestTools.WebTesting.DataBindingAccessMethod.Random,

   5:              "streetNames#csv")]



Next, we have to define what column we are pulling from the .CSV file. In my case, it is a single column CSV file but we still have to have a column name defined. For my CSV file I am using the form of:

8<---------------------------
streetNames
"ABBINGTON"
"ALDRICH"
"ALEXANDER"
"ALICE"
"ALLISON"
"ALMOND"
"ALTA"
"AMHERST"
--------------------------->8

These are all the street names in Grover's Mill, NJ as reported by Melissa Data.

The DataBinding declaration that I am using takes four parameters.

1: dataSourceName
This is the DSN that we previously defined.
2: tableName
This is the tableName that we defined in the form of #csv.
3: columnName
This is the column name of the CSV that we are extracting. I've found this to be case insensitive.
4: contextVariableName
This is the key name of the entry that will be created in the Context for the iteration.

Here is what I am using for my DataBinding:



   1:    [DataBinding("streetNameDataSource", "streetNames#csv", "streetNames", "streetNames")]



Now with each iteration of the vuser I can reference the Context for the randomized street name, ala:



   1:  Debug.WriteLine("Street name is " + this.Context["streetNames"].ToString());



Works like a champ!

Saturday, August 2, 2008

Tasty memory! Om Nom Nom!

So, I created a scenario and had 45 virtual users doing nothing but browsing the application that I am testing. After one hour I noticed that the w3wp private bytes are running around 1.5 gigs of memory. That's right, just browsing. I was hitting it at a rate about 26 hits per second and running about 90% CPU utilization and 1.5 gigs of private bytes being consumed with almost 700 megs of hit in Gen 0.

Yeah, something ain't right.

But, that gives me an excuse to brush up on my WinDBG skills. I fired up adplus.vbs and took a -hang dump that resulted in a 1.5 gig dump. Woo! So tonight I've been re-learning the art of poking around in a dump file with a debugger.

Along the way I found a most excellent blog for a MS dudette in Sweden that really knows her stuff and has some great tutorials for debuggery:

http://blogs.msdn.com/tess/default.aspx

Check out the most excellent labs located here.

Friday, August 1, 2008

Let's be deviant with our think times!

I mentioned in another post that I stopped using the WebTestRequest.ThinkTime as the think time is reported in the Load Test Repository and that annoys me, especially because the default option for think time is for the MS tools to use a normally distributed variable think time on the time requested.

Now, I think the normally distributed think time is awesome to help prevent VUsers from getting into a lock step situation and most of the time we want the VUsers to be in route step.

I created a ThinkTime routine the other day that just used a slightly randomized think time but of course, it was not normally distributed and I really like the idea of a normally distributed think time. So, off to the Internets and I found a simple to implement algorithm by the name of Box-Muller that I was able to use for my VUsers think times.

Below is a graph with an example of a 5 second think time with multiple values of deviation, s.



Below is the main code that I used to write the class to be invoked from inside the coded web test and of course, think time must be invoked outside of any named transactions otherwise you end up in the same situation before with variable think time being reported in the transaction time.

Because of my slackard like nature, I went ahead and made all the methods static so that I don't have in instantiate any objects for later referencing.

I simply call the code like this:



   1:        string transactionName = "Login.PageHit";

   2:        this.BeginTransaction(transactionName);

   3:        WebTestRequest loginPageHit = new WebTestRequest("http://" + targetWebServer.ToString() + "/SomeWebApp/login.aspx");

   4:        ExtractHiddenFields loginExtractionRule = new ExtractHiddenFields();

   5:        loginExtractionRule.Required = true;

   6:        loginExtractionRule.HtmlDecode = true;

   7:        loginExtractionRule.ContextParameterName = "1";

   8:        loginPageHit.ExtractValues += new EventHandler<ExtractionEventArgs>(loginExtractionRule.Extract);

   9:        yield return loginPageHit;

  10:        loginPageHit = null;

  11:        this.EndTransaction(transactionName);

  12:        #if THINKTIME

  13:          LoadTestThinkTime.ThinkTime(5, 0.1);

  14:        #endif 



Below is the actual code that I wrote:



   1:  using System;

   2:  using System.Threading;

   3:  namespace LoadTestThinkTime {

   4:    public class LoadTestThinkTime {

   5:      /// <summary>

   6:      /// This routine will generate a randomized variable that is in a standard distribution with a deviation of 0

   7:      /// ± s/2 for given s and then invoke Thread.Sleep for the given amount of seconds converted to milliSeconds.

   8:      /// </summary>

   9:      /// <param name="secondsToSleep"></param>

  10:      /// <param name="s"></param>

  11:      static private void RandomizedSleep(int secondsToSleep, double s) {

  12:        if (s > 0) {

  13:          if (s <= 1.5) {

  14:            if (secondsToSleep > 0) {

  15:              if (secondsToSleep < (int.MaxValue / 1000)) {

  16:                double u  = 0;

  17:                double v  = 0;

  18:                double w  = 0;

  19:                double z0 = 0;

  20:                double z1 = 0;

  21:                Random randomizer = new Random();

  22:                do {

  23:                  u = 2.0 * randomizer.NextDouble() - 1.0;

  24:                  v = 2.0 * randomizer.NextDouble() - 1.0;

  25:                  w = u * u + v * v;

  26:                } while (w >= 1.0);

  27:                w = Math.Sqrt(((-2.0 * Math.Log(w)) / w));

  28:                z0 = u * w;

  29:                z1 = v * w; /* I don't use z1, but it is part of the original algorithm so here it is. */

  30:                /* When using small time values, i.e, 1 second, large values of s can generate negative values. In that

  31:                 * situation go ahead and take the absolute value and invoke the call with that to prevent errors during

  32:                 * loadtests. */

  33:                int milliSecondSleepTime = (int)((secondsToSleep + z0 * s) * 1000.0);

  34:                if (milliSecondSleepTime < 0) {

  35:                  milliSecondSleepTime = Math.Abs(milliSecondSleepTime);

  36:                };

  37:                Thread.Sleep(milliSecondSleepTime);

  38:              } else {

  39:                throw new Exception("LoadTestThinkTime: Maximum sleep time is " + ((int)int.MaxValue / 1000).ToString() + " seconds.");

  40:              };

  41:            } else {

  42:              throw new Exception("LoadTestThinkTime: Sleep time cannot be negative.");

  43:            };

  44:          } else {

  45:            throw new Exception("LoadTestThinkTime: Deviation cannot be larger than 1.5");

  46:          };

  47:        } else {

  48:          throw new Exception("LoadTestThinkTime: Deviation cannot be negative");

  49:        }

  50:      } 

  51:      /// <summary>

  52:      /// Public method to invoke variable sleep time with a given deviation of s where s is in the domain of [0, 1.5].

  53:      /// </summary>

  54:      /// <param name="secondsToSleep"></param>

  55:      /// <param name="s"></param>

  56:      static public void ThinkTime(int secondsToSleep, double s) {

  57:        RandomizedSleep(secondsToSleep, s);

  58:      }

  59:      /// <summary>

  60:      /// Public method to invoke variable sleep time with a default deviation of 0.25.

  61:      /// </summary>

  62:      /// <param name="secondsToSleep"></param>

  63:      static public void ThinkTime(int secondsToSleep) {

  64:        RandomizedSleep(secondsToSleep, 0.25);

  65:      }

  66:    }

  67:  }