Skip to content

Commit e88fa14

Browse files
authored
[R&D Week Profiling] Add tests for sync over async scenarios (#6763)
## Summary of changes Add sync-over-async scenarios ## Reason for change Investigate insights related to async anti-patterns ## Implementation details Add scenarios for both .NET Framework and .NET Core ## Test coverage ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. -->
1 parent b75fab2 commit e88fa14

File tree

8 files changed

+216
-4
lines changed

8 files changed

+216
-4
lines changed

profiler/src/Demos/Samples.BuggyBits/Controllers/CompanyInformationController.cs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
44
// </copyright>
55

6+
using System.Threading;
67
using BuggyBits.Models;
78
using Microsoft.AspNetCore.Mvc;
89

@@ -17,12 +18,40 @@ public CompanyInformationController(DataLayer dataLayer)
1718
this.dataLayer = dataLayer;
1819
}
1920

20-
public IActionResult Index()
21+
public IActionResult Index(bool shortLived=false)
2122
{
22-
// bad blocking call
23-
ViewData["TessGithubPage"] = dataLayer.GetTessGithubPage().Result;
23+
if (shortLived)
24+
{
25+
ViewData["TessGithubPage"] = GetGitHubPageViaThread();
26+
return View();
27+
}
28+
else
29+
{
30+
// bad blocking call
31+
ViewData["TessGithubPage"] = dataLayer.GetTessGithubPage().Result;
32+
return View();
33+
}
34+
}
35+
36+
private string GetGitHubPageViaThread()
37+
{
38+
string result = string.Empty;
39+
Thread worker = new Thread(() =>
40+
{
41+
for (int i = 0; i < 5; i++)
42+
{
43+
Thread.Sleep(100);
44+
}
2445

25-
return View();
46+
result = dataLayer.GetTessGithubPage().Result;
47+
for (int i = 0; i < 5; i++)
48+
{
49+
Thread.Sleep(100);
50+
}
51+
});
52+
worker.Start();
53+
worker.Join();
54+
return result;
2655
}
2756

2857
[HttpPost]

profiler/src/Demos/Samples.BuggyBits/Controllers/ProductsController.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// </copyright>
55

66
using System;
7+
using System.Collections.Generic;
78
using System.Diagnostics;
89
using System.Text;
910
using System.Threading;
@@ -23,6 +24,49 @@ public ProductsController(DataLayer dataLayer)
2324
this.dataLayer = dataLayer;
2425
}
2526

27+
public async Task<IActionResult> GetSyncOverAsync(bool withResultProperty)
28+
{
29+
var sw = new Stopwatch();
30+
sw.Start();
31+
IEnumerable<Product> products = null;
32+
if (withResultProperty)
33+
{
34+
products = await dataLayer.GetAllProductWithResult($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host.Value}");
35+
}
36+
else
37+
{
38+
products = await dataLayer.GetAllProductGetAwaiterGetResult($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host.Value}");
39+
}
40+
41+
var productsTable = new StringBuilder(1024 * 100);
42+
productsTable.Append("<table><tr><th>Product Name</th><th>Description</th><th>Price</th></tr>");
43+
foreach (var product in products)
44+
{
45+
productsTable.Append($"<tr><td>{product.ProductName}</td><td>{product.Description}</td><td>${product.Price}</td></tr>");
46+
}
47+
48+
productsTable.Append("</table>");
49+
sw.Stop();
50+
51+
ViewData["ElapsedTimeInMs"] = sw.ElapsedMilliseconds;
52+
ViewData["ProductsTable"] = productsTable;
53+
return View("Index");
54+
}
55+
56+
// GET: Products with Result property - bad practice
57+
[Route("Products/WithResult")]
58+
public async Task<IActionResult> WithResult()
59+
{
60+
return await GetSyncOverAsync(true);
61+
}
62+
63+
// GET: Products with GetAwaiter().GetResult() - bad practice
64+
[Route("Products/GetAwaiterGetResult")]
65+
public async Task<IActionResult> GetAwaiterGetResult()
66+
{
67+
return await GetSyncOverAsync(false);
68+
}
69+
2670
// GET: Products asynchronously
2771
[Route("Products/Async")]
2872
public async Task<IActionResult> Async()

profiler/src/Demos/Samples.BuggyBits/Models/DataLayer.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,36 @@ public List<Product> GetProductsOnSaleEx()
156156
return allProducts;
157157
}
158158

159+
// -------------------------------------------------------
160+
// Different sync-over-async implementations
161+
public async Task<IEnumerable<Product>> GetAllProductGetAwaiterGetResult(string rootPath)
162+
{
163+
// Note: sync-over-async is much slower so reduce the number of product to return
164+
const int productCount = 2000;
165+
var path = GetProductInfoRoot(rootPath);
166+
var products = new List<Product>(productCount);
167+
for (int id = 0; id < productCount; id++)
168+
{
169+
products.Add(GetProductAsync(path, id).GetAwaiter().GetResult());
170+
}
171+
172+
return products;
173+
}
174+
175+
public async Task<IEnumerable<Product>> GetAllProductWithResult(string rootPath)
176+
{
177+
// Note: sync-over-async is much slower so reduce the number of product to return
178+
const int productCount = 2000;
179+
var path = GetProductInfoRoot(rootPath);
180+
var products = new List<Product>(productCount);
181+
for (int id = 0; id < productCount; id++)
182+
{
183+
products.Add(GetProductAsync(path, id).Result);
184+
}
185+
186+
return products;
187+
}
188+
159189
// -------------------------------------------------------
160190
// Different asynchronous implementations
161191
public async Task<IEnumerable<Product>> GetAllProductAsync(string rootPath)

profiler/src/Demos/Samples.BuggyBits/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public enum Scenario
3232
EndpointsCount = 128, // Specific test with '.' in endpoint name
3333
Spin = 256, // Requests that take a long time
3434
Redirect = 512, // triggers HTTP redirect
35+
GetAwaiterGetResult = 1024, // using GetAwaiter().GetResult() instead of await
36+
UseResultProperty = 2048, // using Result property instead of GetAwaiter().GetResult()
37+
ShortLived = 4096, // short lived threads
3538
}
3639

3740
public class Program

profiler/src/Demos/Samples.BuggyBits/SelfInvoker.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,21 @@ private List<string> GetEndpoints(string rootUrl)
188188
{
189189
urls.Add($"{rootUrl}/CompanyInformation");
190190
}
191+
192+
if ((_scenario & Scenario.GetAwaiterGetResult) == Scenario.GetAwaiterGetResult)
193+
{
194+
urls.Add($"{rootUrl}/Products/GetAwaiterGetResult");
195+
}
196+
197+
if ((_scenario & Scenario.UseResultProperty) == Scenario.UseResultProperty)
198+
{
199+
urls.Add($"{rootUrl}/Products/WithResult");
200+
}
201+
202+
if ((_scenario & Scenario.ShortLived) == Scenario.ShortLived)
203+
{
204+
urls.Add($"{rootUrl}/CompanyInformation?ShortLived=true");
205+
}
191206
}
192207

193208
return urls;

profiler/src/Demos/Samples.Computer01/ComputerService.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class ComputerService
5454
#endif
5555
private ThreadSpikes _threadSpikes;
5656
private StringConcat _stringConcat;
57+
private SyncOverAsync _syncOverAsync;
5758

5859
public void StartService(Scenario scenario, int nbThreads, int parameter)
5960
{
@@ -185,6 +186,16 @@ public void StartService(Scenario scenario, int nbThreads, int parameter)
185186
break;
186187
#endif
187188

189+
case Scenario.SyncOverAsyncWithGetAwaiterGetResult:
190+
_syncOverAsync = new SyncOverAsync(nbThreads, false);
191+
_syncOverAsync.Start();
192+
break;
193+
194+
case Scenario.SyncOverAsyncWithResult:
195+
_syncOverAsync = new SyncOverAsync(nbThreads, true);
196+
_syncOverAsync.Start();
197+
break;
198+
188199
default:
189200
throw new ArgumentOutOfRangeException(nameof(scenario), $"Unsupported scenario #{_scenario}");
190201
}
@@ -317,6 +328,11 @@ public void StopService()
317328
StopLinuxDlIteratePhdrDeadlock();
318329
break;
319330
#endif
331+
332+
case Scenario.SyncOverAsyncWithGetAwaiterGetResult:
333+
case Scenario.SyncOverAsyncWithResult:
334+
_syncOverAsync.Stop();
335+
break;
320336
}
321337
}
322338

@@ -448,6 +464,14 @@ public void Run(Scenario scenario, int iterations, int nbThreads, int parameter)
448464
RunStringConcat(parameter);
449465
break;
450466

467+
case Scenario.SyncOverAsyncWithGetAwaiterGetResult:
468+
RunSyncOverAsync(nbThreads, false);
469+
break;
470+
471+
case Scenario.SyncOverAsyncWithResult:
472+
RunSyncOverAsync(nbThreads, true);
473+
break;
474+
451475
default:
452476
throw new ArgumentOutOfRangeException(nameof(scenario), $"Unsupported scenario #{_scenario}");
453477
}
@@ -971,6 +995,12 @@ private void RunStringConcat(int count)
971995
test.Run();
972996
}
973997

998+
private void RunSyncOverAsync(int threadCount, bool useResultProperty)
999+
{
1000+
var test = new SyncOverAsync(threadCount, useResultProperty);
1001+
test.Run();
1002+
}
1003+
9741004
public class MySpecialClassA
9751005
{
9761006
}

profiler/src/Demos/Samples.Computer01/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public enum Scenario
4141
ThreadSpikes,
4242
StringConcat, // parameter = number of strings to concatenate
4343
LinuxDlIteratePhdrDeadlock,
44+
SyncOverAsyncWithResult,
45+
SyncOverAsyncWithGetAwaiterGetResult,
4446
}
4547

4648
public class Program
@@ -78,6 +80,8 @@ public static void Main(string[] args)
7880
// 25: create thread spikes
7981
// 26: string concatenation
8082
// 27: custom dl_iterate_phdr deadlock
83+
// 28: sync over async with Result property for n threads
84+
// 29: sync over async with GetAwaiter().GetResult() for n threads
8185
//
8286
Console.WriteLine($"{Environment.NewLine}Usage:{Environment.NewLine} > {Process.GetCurrentProcess().ProcessName} " +
8387
$"[--service] [--iterations <number of iterations to execute>] " +
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// <copyright file="SyncOverAsync.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
4+
// </copyright>
5+
6+
using System;
7+
using System.Threading.Tasks;
8+
9+
namespace Samples.Computer01
10+
{
11+
public class SyncOverAsync : ScenarioBase
12+
{
13+
private readonly bool _useResultProperty = false;
14+
15+
public SyncOverAsync(int nbThreads, bool useResultProperty)
16+
: base(nbThreads)
17+
{
18+
_useResultProperty = useResultProperty;
19+
}
20+
21+
public override void OnProcess()
22+
{
23+
Console.WriteLine("--> Before compute");
24+
int result;
25+
if (_useResultProperty)
26+
{
27+
result = Compute1().Result;
28+
}
29+
else
30+
{
31+
result = Compute1().GetAwaiter().GetResult();
32+
}
33+
34+
Console.WriteLine($"<-- After compute = {result}");
35+
}
36+
37+
private async Task<int> Compute1()
38+
{
39+
var result = await Compute2();
40+
return result;
41+
}
42+
43+
private async Task<int> Compute2()
44+
{
45+
var result = await Compute3();
46+
return result;
47+
}
48+
49+
private async Task<int> Compute3()
50+
{
51+
Console.WriteLine("in Compute3");
52+
await Task.Delay(1000);
53+
54+
return 42;
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)