Fork me on GitHub

NuGet Snippet:

C:\_tmp\performanceTest>nuget install -ExcludeVersion canopy
Attempting to resolve dependency 'Selenium.WebDriver (= 2.42.0)'.
Attempting to resolve dependency 'Selenium.Support (= 2.42.0)'.
Attempting to resolve dependency 'SizSelCsZzz (= 0.3.36.0)'.
Attempting to resolve dependency 'Newtonsoft.Json (= 6.0)'.
Installing 'Selenium.WebDriver 2.42.0'.
Successfully installed 'Selenium.WebDriver 2.42.0'.
Installing 'Selenium.Support 2.42.0'.
Successfully installed 'Selenium.Support 2.42.0'.
Installing 'Newtonsoft.Json 6.0.1'.
Successfully installed 'Newtonsoft.Json 6.0.1'.
Installing 'SizSelCsZzz 0.3.36.0'.
Successfully installed 'SizSelCsZzz 0.3.36.0'.
Installing 'canopy 0.9.11'.
Successfully installed 'canopy 0.9.11'.
 
C:\_tmp\performanceTest>

Script Snippet (DG.StressTest.Browser.cmd):

@echo off

:: Add the paths for the F# SDK 3.x (from higher version to lower)
set FSHARPSDK=^
C:\Program Files (x86)\Microsoft SDKs\F#\3.1\Framework\v4.0\;^
C:\Program Files (x86)\Microsoft SDKs\F#\3.0\Framework\v4.0\

cls

:: Execute the script "only" with the first "fsianycpu.exe" found
for %%i in (fsianycpu.exe) do "%%~$FSHARPSDK:i" DG.StressTest.Browser.fsx %*

pause

Code Snippet (DG.StressTest.Browser.fsx):

  1 //nuget install -ExcludeVersion canopy
  2 #r @"Selenium.Support\lib\net40\WebDriver.Support.dll"
  3 #r @"Selenium.WebDriver\lib\net40\WebDriver.dll"
  4 #r @"canopy\lib\canopy.dll"
  5  
  6 #load @"DG.Auth.fsx" // Just contains let usr = "usr" and let pwd = "pwd"
  7  
  8 open System
  9 open System.IO
 10  
 11 open canopy
 12 open runner
 13 open configuration
 14  
 15 /// Config canopy
 16 compareTimeout <- 30.0
 17  
 18 /// Utils
 19 let timestamp  () = DateTime.Now.ToString("o").Replace(":","")
 20 let timestamp' () = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
 21  
 22 let string2float = function
 23   | (n,s) -> match Double.TryParse s with | true, value -> value * n | _ -> 0.
 24  
 25 let parse (s:string) = 
 26   match s with
 27     | ms when s.Contains("ms")     ->    1., ms.Replace(" ms","")
 28     | KB when s.Contains("KB/sec") ->    1., KB.Replace(" KB/sec","")
 29     | MB when s.Contains("MB/sec") -> 1000., MB.Replace(" MB/sec","")
 30     | _ -> failwith "Not recognized unit"
 31   |> string2float |> int
 32  
 33 /// Local files / folders
 34 let output = @".\output.csv"
 35 let source = @".source\"
 36  
 37 /// Connection info:
 38 let uriMain = Uri(@"https://org.crm4.dynamics.com");
 39 let uriDiag = Uri(uriMain.AbsoluteUri + @"/tools/diagnostics/diag.aspx")
 40  
 41 /// Save to .source folder
 42 let save2source data =
 43   File.WriteAllText(source + timestamp() + ".log", data)
 44  
 45 /// Browser Performance Test MS CRM Online
 46 let rec performanceTestCrm date path = 
 47   match (date > DateTime.Now) with
 48   | true -> 
 49     click "#runBtn_all"
 50  
 51     waitFor (fun () -> (read "#td_status_all") = "complete")
 52  
 53     save2source (read "#resultConsole")
 54  
 55     let latency, speed, jsArray, jsMorph, jsBase64, jsDOM =
 56       parse (read "#td_result_latency"),
 57       parse (read "#td_result_bandwidth"),
 58       parse (read "#td_result_jsArrayBenchmark"),
 59       parse (read "#td_result_jsMorphBenchmark"),
 60       parse (read "#td_result_jsBase64Benchmark"),
 61       parse (read "#td_result_jsDomBenchmark")
 62  
 63     let sw = File.AppendText(path)
 64     sw.WriteLine(
 65       sprintf "%s;%i;%i;%i;%i;%i;%i;"
 66         (timestamp'()) latency speed jsArray jsMorph jsBase64 jsDOM)
 67     sw.Dispose()
 68  
 69     reload()
 70  
 71     performanceTestCrm date path
 72   | false -> ()
 73  
 74 /// Start Browser Response Test:
 75 start chrome
 76  
 77 "MS CRM Online Browser Performance Test" &&& fun _ ->
 78   // Clear output.csv and .source folder
 79   File.Exists(output) |> function 
 80     | true -> File.Delete(output)
 81     | false -> ()
 82   Directory.EnumerateFiles(@".source","*.log",SearchOption.AllDirectories)
 83   |> Seq.iter(fun x -> File.Delete(x))
 84  
 85   // Go to MS CRM Online
 86   url uriMain.AbsoluteUri
 87  
 88   // Login
 89   "#cred_userid_inputtext"   << DG.Auth.usr
 90   "#cred_password_inputtext" << DG.Auth.pwd
 91   click "#cred_sign_in_button"
 92   press enter
 93  
 94   // Go to diag url
 95   url uriDiag.AbsoluteUri
 96  
 97   // Start and Stop DateTimes
 98   let startDate = DateTime.Now
 99   let stopDate  = startDate.AddMinutes(60.)
100  
101   let sw = File.CreateText(output)
102   sw.WriteLine(
103     "Timestamp (ISO 8601);Latency (ms);Speed (KB/sec);" +
104     "JS Array (ms);JS Morph (ms);JS Base64 (ms);JS DOM (ms);"
105   )
106   sw.Dispose()
107  
108   performanceTestCrm stopDate output
109  
110 run()
111  
112 quit()

Code result:

Starting ChromeDriver (v2.10.267521) on port 64015
Only local connections are allowed.
Test: Browser Performance Test MS CRM Online
Passed
 
60 minutes 19 seconds to execute
1 passed
0 failed
Press any key to continue . . .

Code result (output.csv):

Timestamp (ISO 8601);Latency (ms);Speed (KB/sec);JS Array (ms);JS Morph (ms);JS Base64 (ms);JS DOM (ms);
2014-09-18 22:59:59;47;302;229;33;5;13;
2014-09-18 23:00:03;47;302;197;46;6;17;
2014-09-18 23:00:07;48;322;192;45;4;18;
2014-09-18 23:00:10;48;315;186;36;4;12;
2014-09-18 23:00:14;46;302;186;32;3;13;
2014-09-18 23:00:17;47;322;186;33;3;13;
...
2014-09-18 23:59:59;48;329;194;47;4;17;

Code result (.source\2014-09-18T225959.2540079+0200.log):

=== Latency Test Info ===
Number of times run: 20
Run 1 time: 46 ms
Run 2 time: 51 ms
Run 3 time: 48 ms
Run 4 time: 47 ms
Run 5 time: 48 ms
Run 6 time: 45 ms
Run 7 time: 46 ms
Run 8 time: 46 ms
Run 9 time: 46 ms
Run 10 time: 52 ms
Run 11 time: 50 ms
Run 12 time: 47 ms
Run 13 time: 47 ms
Run 14 time: 45 ms
Run 15 time: 44 ms
Run 16 time: 47 ms
Run 17 time: 48 ms
Run 18 time: 48 ms
Run 19 time: 45 ms
Run 20 time: 50 ms
Average latency: 47 ms
Client Time: Thu, 18 Sep 2014 20:59:57 GMT
 
=== Bandwidth Test Info ===
Run 1
  Time: 56 ms
  Blob Size: 15180 bytes
  Speed: 264 KB/sec
Run 2
  Time: 49 ms
  Blob Size: 15180 bytes
  Speed: 302 KB/sec
Run 3
  Time: 50 ms
  Blob Size: 15180 bytes
  Speed: 296 KB/sec
Run 4
  Time: 49 ms
  Blob Size: 15180 bytes
  Speed: 302 KB/sec
Run 5
  Time: 51 ms
  Blob Size: 15180 bytes
  Speed: 290 KB/sec
Run 6
  Time: 51 ms
  Blob Size: 15180 bytes
  Speed: 290 KB/sec
Run 7
  Time: 52 ms
  Blob Size: 15180 bytes
  Speed: 285 KB/sec
Run 8
  Time: 52 ms
  Blob Size: 15180 bytes
  Speed: 285 KB/sec
Run 9
  Time: 50 ms
  Blob Size: 15180 bytes
  Speed: 296 KB/sec
Run 10
  Time: 53 ms
  Blob Size: 15180 bytes
  Speed: 279 KB/sec
Max Download speed: 302 KB/sec
Client Time: Thu, 18 Sep 2014 20:59:58 GMT
 
=== Browser Info ===
Browser CodeName: Mozilla
Browser Name: Netscape
Browser Version: 5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36
Cookies Enabled: true
Platform: Win32
User-agent header: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36
Client Time: Thu, 18 Sep 2014 20:59:58 GMT
 
=== Machine Info ===
Client IP Address: XXX.XXX.XXX.XXX
Client Time: Thu, 18 Sep 2014 20:59:58 GMT
 
=== Array Manipultaion Benchmark ===
Time: 229 ms
Client Time: Thu, 18 Sep 2014 20:59:58 GMT
 
=== Morph Benchmark ===
Time: 33 ms
Client Time: Thu, 18 Sep 2014 20:59:58 GMT
 
=== Base 64 Benchmark ===
Time: 5 ms
Client Time: Thu, 18 Sep 2014 20:59:59 GMT
 
=== DOM Benchmark ===
Total Time: 13 ms
Breakdown:
  Append:  3ms
  Prepend: 5ms
  Index:   0ms
  Insert:  4ms
  Remove:  1ms
Client Time: Thu, 18 Sep 2014 20:59:59 GMT
 
=== Organization Info ===
Organization name: orgSomeIdNumber
Is Live: True
Server time: 9/18/2014 8:59:55 PM UTC
Url: https://org.crm4.dynamics.com//tools/diagnostics/diag.aspx
Client Time: Thu, 18 Sep 2014 20:59:59 GMT

Chart diagrams:

All

All

All

Architecture (Lenovo ThinkPad W540):

All

NuGet Snippet:

C:\_tmp\stressTest>nuget install -ExcludeVersion Microsoft.CrmSdk.CoreAssemblies
Attempting to resolve dependency 'Microsoft.IdentityModel (= 6.1.7600.16394)'.
Installing 'Microsoft.IdentityModel 6.1.7600.16394'.
Successfully installed 'Microsoft.IdentityModel 6.1.7600.16394'.
Installing 'Microsoft.CrmSdk.CoreAssemblies 6.1.0'.
Successfully installed 'Microsoft.CrmSdk.CoreAssemblies 6.1.0'.

C:\_tmp\stressTest>
C:\_tmp\stressTest>nuget install -ExcludeVersion FSharp.Data
Attempting to resolve dependency 'Zlib.Portable (= 1.9.2)'.
Installing 'Zlib.Portable 1.9.2'.
Successfully installed 'Zlib.Portable 1.9.2'.
Installing 'FSharp.Data 2.0.4'.
Successfully installed 'FSharp.Data 2.0.4'.

C:\_tmp\stressTest>

Script Snippet (DG.StressTest.cmd):

@echo off

:: Add the paths for the F# SDK 3.x (from higher version to lower)
set FSHARPSDK=^
C:\Program Files (x86)\Microsoft SDKs\F#\3.1\Framework\v4.0\;^
C:\Program Files (x86)\Microsoft SDKs\F#\3.0\Framework\v4.0\

cls

:: Execute the script "only" with the first "fsianycpu.exe" found
for %%i in (fsianycpu.exe) do "%%~$FSHARPSDK:i" DG.StressTest.fsx %*

pause

Code Snippet (DG.StressTest.fsx):

  1 #r @"System.Runtime.Serialization"
  2 #r @"System.ServiceModel"
  3  
  4 // nuget install -ExcludeVersion Microsoft.CrmSdk.CoreAssemblies
  5 #r @"Microsoft.CrmSdk.CoreAssemblies\lib\net40\Microsoft.Xrm.Sdk.dll"
  6  
  7 // nuget install -ExcludeVersion FSharp.Data
  8 #r @"FSharp.Data\lib\net40\FSharp.Data.dll"
  9  
 10 #load @"DG.Auth.fsx" // Just contains let usr = "usr" and let pwd = "pwd"
 11  
 12 open System
 13 open System.Runtime.Serialization
 14 open System.ServiceModel.Description
 15  
 16 open Microsoft.Xrm.Sdk
 17 open Microsoft.Xrm.Sdk.Client
 18 open Microsoft.Xrm.Sdk.Messages
 19 open Microsoft.Xrm.Sdk.Query
 20  
 21 open FSharp.Data
 22  
 23 /// Utils
 24 let r = new System.Random()
 25 
 26 let crmCount (proxy:OrganizationServiceProxy) logicalName (date:DateTime) = 
 27   let f = FilterExpression()
 28   f.AddCondition(@"createdon", ConditionOperator.GreaterEqual, date.ToUniversalTime())
 29  
 30   let q = QueryExpression(logicalName)
 31   q.ColumnSet <- ColumnSet(logicalName + "id")
 32   q.Criteria <- f
 33   q.PageInfo <- PagingInfo()
 34   q.PageInfo.PageNumber <- 1
 35  
 36   seq{ let resp = proxy.RetrieveMultiple(q)
 37        yield! resp.Entities
 38  
 39        let rec retrieveMultiple' (ec:EntityCollection) pn = seq{
 40          match ec.MoreRecords with
 41          | true ->
 42            q.PageInfo.PageNumber <- (pn + 1)
 43            q.PageInfo.PagingCookie <- ec.PagingCookie
 44  
 45            let resp' = proxy.RetrieveMultiple(q)
 46  
 47            yield! resp'.Entities
 48            yield! retrieveMultiple' resp' (pn + 1)
 49          | false -> () }
 50  
 51        yield! retrieveMultiple' resp 1 }
 52   |> Seq.length
 53 
 54 /// Connection info:
 55 let uri = Uri("https://org.api.crm4.dynamics.com/XRMServices/2011/Organization.svc");
 56  
 57 let ac = AuthenticationCredentials()
 58 ac.ClientCredentials.UserName.UserName <- DG.Auth.usr
 59 ac.ClientCredentials.UserName.Password <- DG.Auth.pwd
 60  
 61 let m = ServiceConfigurationFactory.CreateManagement<IOrganizationService>(uri)
 62 let tc = m.Authenticate(ac)
 63 let p = new OrganizationServiceProxy(m, tc.SecurityTokenResponse)
 64  
 65 /// Test data:
 66 let data = FreebaseData.GetDataContext()
 67  
 68 let names = data.Society.People.``Family names`` |> Seq.take 250
 69 let namesCache = names |> Seq.toArray |> Array.map(fun x -> x.Name)
 70  
 71 let titles = data.``Products and Services``.Business.``Job titles`` |> Seq.take 250
 72 let titlesCache = titles |> Seq.toArray |> Array.map(fun x -> x.Name)
 73  
 74 let products = data.``Products and Services``.``Food & Drink``.Foods |> Seq.take 250
 75 let productsCache = products |> Seq.toArray |> Array.map(fun x -> x.Name)
 76  
 77 let countries = data.Commons.Location.Countries |> Seq.take 250
 78 let countriesCache = countries |> Seq.toArray |> Array.map(fun x -> x.Name)
 79  
 80 let cities = data.Commons.Location.``City/Town/Villages`` |> Seq.take 250
 81 let citiesCache = cities |> Seq.toArray |> Array.map(fun x -> x.Name)
 82  
 83 let companyName name = name + " Company"
 84 let streetName name = name + " Street " + string(r.Next(1,1000))
 85 let phoneNumber () = "555-" + string(r.Next(1000,10000))
 86 let zipCode () = string(r.Next(1000,10000))
 87 let email firstname lastname = (firstname + "." + lastname + "@mail.co.dk").ToLower()
 88  
 89 /// Create as many accounts as possible
 90 let createAccount () =
 91   let a = Entity("account")
 92   a.Attributes.Add("name", 
 93     namesCache.[r.Next(0,250)] + " " + 
 94     namesCache.[r.Next(0,250)] |> companyName)
 95   a.Attributes.Add("telephone1", phoneNumber())
 96   a.Attributes.Add("address1_line1", namesCache.[r.Next(0,250)] |> streetName)
 97   a.Attributes.Add("address1_city", citiesCache.[r.Next(0,250)])
 98   a.Attributes.Add("address1_postalcode", zipCode())
 99   a.Attributes.Add("address1_country", countriesCache.[r.Next(0,250)])
100   a
101  
102 // One account per thread
103 let createAccounts date concurrency =
104   Array.Parallel.init concurrency (fun _ -> createAccount ())
105   |> Array.Parallel.map(
106     fun x -> 
107       try
108         match (date > DateTime.Now) with
109         | true -> 
110           let p' = new OrganizationServiceProxy(m, tc.SecurityTokenResponse)
111           p'.Create(x) |> Some
112         | false -> None
113       with ex -> None)
114  
115 // Ten accounts per thread
116 let createAccounts' date concurrency =
117   Array.Parallel.init concurrency
118     (fun _ -> 
119       let em = new ExecuteMultipleRequest()
120       em.Settings <- new ExecuteMultipleSettings()
121       em.Settings.ContinueOnError <- true
122       em.Settings.ReturnResponses <- true
123       em.Requests <- new OrganizationRequestCollection()
124       em)
125   |> Array.Parallel.map(
126     fun x -> 
127       try
128         Array.Parallel.init 10
129           (fun _ -> 
130             let cr = new CreateRequest()
131             cr.Target <- createAccount()
132             x.Requests.Add(cr)) |> ignore
133         match (date > DateTime.Now) with
134         | true -> 
135           let p' = new OrganizationServiceProxy(m, tc.SecurityTokenResponse)
136           p'.Execute(x) :?> ExecuteMultipleResponse |> Some
137         | false -> None
138       with ex -> None)
139  
140 /// Stress Test
141 let rec stressTestCrm date concurrency = 
142   match (date > DateTime.Now) with
143   | true -> 
144     createAccounts' date concurrency |> ignore
145     stressTestCrm date concurrency
146   | false -> ()
147  
148 /// Concurrent users (threads)
149 let concurrency = (1 <<< 10) // 1024
150  
151 /// Start and Stop DateTimes
152 let startDate = DateTime.Now
153 let stopDate  = startDate.AddMinutes(60.)
154  
155 /// Perfom stress test and print outcome
156 stressTestCrm stopDate concurrency
157  
158 (crmCount p "account" startDate, startDate.ToString("o"))
159 ||> printfn "Accounts created: %i, since: %s"

Code result:

Accounts created: 141225, since: 2014-09-16T22:31:53.9798783+02:00
Press any key to continue . . .

Architecture (Lenovo ThinkPad W540):

All

Code Snippet:

 1 #if INTERACTIVE
 2 #r "System.Data.Services.Client"
 3 #r "FSharp.Data.TypeProviders"
 4 #endif
 5 
 6 open System
 7 open System.Net
 8 open System.Data.Services.Client
 9 open Microsoft.FSharp.Data
10 
11 [<Literal>]
12 let url = @"https://demo.crm4.dynamics.com/XRMServices/2011/OrganizationData.svc/"
13 
14 [<Literal>] // "%TMP%/odata/OrganizationData.csdl"
15 let csdl = __SOURCE_DIRECTORY__  + @"/odata/OrganizationData.csdl" 
16 
17 type Xrm = 
18     TypeProviders.ODataService<
19         ServiceUri = url,
20         LocalSchemaFile = csdl,
21         ForceUpdate = false>
22 
23 let ctx = Xrm.GetDataContext()
24 
25 // To be used when writing JavaScript with OData
26 ctx.DataContext.SendingRequest.Add (
27     fun eventArgs -> printfn "-Url: %A" eventArgs.Request.RequestUri)
28 ctx.DataContext.SendingRequest.Add (
29     fun eventArgs -> printfn "-Query: %s" eventArgs.Request.RequestUri.Query)
30 
31 // Remember to "pipe" to a Sequence, in order to evaluate the Linq Query:
32 query { for a in ctx.AccountSet do
33         where (a.AccountNumber = "42")
34         select (a.AccountNumber, a.AccountId)
35         skip 5
36         take 1} 
37 |> Seq.map id

Code output:

val url : string =
  "https:demo.crm4.dynamics.com/XRMServices/2011/OrganizationData.svc/"
val csdl : string =
  "C:\Users\rsm\AppData\Local\Temp\odata\OrganizationData.csdl"
type Xrm =
  class
    static member GetDataContext : unit -> Xrm.ServiceTypes.SimpleDataContextTypes.demoContext
     + 1 overload
    nested type ServiceTypes
  end
val ctx : Xrm.ServiceTypes.SimpleDataContextTypes.demoContext
val it : unit = ()
-Uri: https:demo.crm4.dynamics.com/XRMServices/2011/OrganizationData.svc/AccountSet()?$filter=AccountNumber eq '42'&$skip=5&$top=1&$select=AccountNumber,AccountId
-Query: $filter=AccountNumber eq '42'&$skip=5&$top=1&$select=AccountNumber,AccountId
val it : seq<string * Guid> = seq []

Remark:

The reason LocalSchemaFile = csdl and ForceUpdate = false are set to static values are because Microsoft still doesn’t allow us to use OData from a server side context (we still need to login to CRM Online with a browser that supports JavaScript). If anybody have a hint on how to access the CSDL service from a .NET application, please write a comment below.

The point is that even though there is no data returned in the .NET application from CRM Online, it doesn’t matter as we just want to use the queries (nicely written in Linq with intellisense and type-safety) outputted from fun eventArgs -> printfn "-Query: %s" eventArgs.Request.RequestUri.Query for our JavaScript code in combination with the official SDK.REST.js library:

1 SDK.REST.retrieveMultipleRecords(
2   "Account",
3   "$filter=AccountNumber eq '42'&$skip=5&$top=1&$select=AccountNumber,AccountId", // query
4   function (results) {
5     // Do stuff
6   },
7   errorHandler,
8   onCompleteHandler
9 );

The current way it’s done (and built on @deprecated technologies) is just not very handy (and has never been) as it requires to expand your current CRM tenant with a 3rd party managed solution:

CRM 2011 OData Query Designer

References: