Skip to content

Commit f9c1b0d

Browse files
[BaseTasks] improve Task settings in AsyncTaskExtensions (#129)
* [BaseTasks] improve Task settings in AsyncTaskExtensions Context: https://devblogs.microsoft.com/premier-developer/limiting-concurrency-for-faster-and-more-responsive-apps/ A work item came in from VS where AOT compilation is causing responsiveness issues. The culprit came down to some of the `TaskScheduler` and `TaskCreationOptions` settings in `AsyncTaskExtensions`. @davkean's recommendations are: > This code needs to do two things: > 1) It needs to pass `TaskCreationOptions.LongRunning`, so that we > don't burn queues meant for short lived tasks. > 2) It needs to limit how many tasks it starts at once, to avoid CPU > contention and reduce the number of threads that we end up > burning. Looking at `AsyncTaskExtensions` usage, it would happen during regular builds as well, because aapt2-related tasks use these. To improve these, I'm changing: * New overloads to pass in `int maxConcurrencyLevel` and `TaskCreationOptions`. * These default to `Environment.ProcessorCount * 2` and `LongRunning`. When this change lands, we should potentially pass in `$(Aapt2DaemonMaxInstanceCount)` where appropriate for `maxConcurrencyLevel`. I wrote a few tests to just check general sanity of `AsyncTaskExtensions`. We didn't have any. * Update src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs
1 parent efc9b67 commit f9c1b0d

File tree

2 files changed

+134
-19
lines changed

2 files changed

+134
-19
lines changed
Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// https://github.com/xamarin/xamarin-android/blob/83854738b8e01747f9536f426fe17ad784cc2081/src/Xamarin.Android.Build.Tasks/Utilities/AsyncTaskExtensions.cs
1+
// https://github.com/xamarin/xamarin-android/blob/83854738b8e01747f9536f426fe17ad784cc2081/src/Xamarin.Android.Build.Tasks/Utilities/AsyncTaskExtensions.cs
22

33
using System;
44
using System.Collections.Generic;
@@ -12,17 +12,24 @@ public static class AsyncTaskExtensions
1212
/// <summary>
1313
/// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll
1414
/// </summary>
15-
public static Task WhenAll<TSource>(this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body)
15+
public static Task WhenAll<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body) =>
16+
asyncTask.WhenAll (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
17+
18+
/// <summary>
19+
/// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll
20+
/// </summary>
21+
public static Task WhenAll<TSource>(this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning)
1622
{
23+
var scheduler = GetTaskScheduler (maxConcurrencyLevel);
1724
var tasks = new List<Task> ();
1825
foreach (var s in source) {
19-
tasks.Add (Task.Run (() => {
26+
tasks.Add (Task.Factory.StartNew (() => {
2027
try {
2128
body (s);
2229
} catch (Exception exc) {
2330
LogErrorAndCancel (asyncTask, exc);
2431
}
25-
}, asyncTask.CancellationToken));
32+
}, asyncTask.CancellationToken, creationOptions, scheduler));
2633
}
2734
return Task.WhenAll (tasks);
2835
}
@@ -31,28 +38,42 @@ public static Task WhenAll<TSource>(this AsyncTask asyncTask, IEnumerable<TSourc
3138
/// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll
3239
/// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject)
3340
/// </summary>
34-
public static Task WhenAllWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body)
41+
public static Task WhenAllWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body) =>
42+
asyncTask.WhenAllWithLock (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
43+
44+
/// <summary>
45+
/// Creates a collection of Task with proper CancellationToken and error handling and waits via Task.WhenAll
46+
/// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject)
47+
/// </summary>
48+
public static Task WhenAllWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning)
3549
{
50+
var scheduler = GetTaskScheduler (maxConcurrencyLevel);
3651
var lockObject = new object ();
3752
var tasks = new List<Task> ();
3853
foreach (var s in source) {
39-
tasks.Add (Task.Run (() => {
54+
tasks.Add (Task.Factory.StartNew (() => {
4055
try {
4156
body (s, lockObject);
4257
} catch (Exception exc) {
4358
LogErrorAndCancel (asyncTask, exc);
4459
}
45-
}, asyncTask.CancellationToken));
60+
}, asyncTask.CancellationToken, creationOptions, scheduler));
4661
}
4762
return Task.WhenAll (tasks);
4863
}
4964

5065
/// <summary>
5166
/// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling.
5267
/// </summary>
53-
public static ParallelLoopResult ParallelForEach<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body)
68+
public static ParallelLoopResult ParallelForEach<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body) =>
69+
asyncTask.ParallelForEach (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
70+
71+
/// <summary>
72+
/// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling.
73+
/// </summary>
74+
public static ParallelLoopResult ParallelForEach<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource> body, int maxConcurrencyLevel)
5475
{
55-
var options = ParallelOptions (asyncTask);
76+
var options = ParallelOptions (asyncTask, maxConcurrencyLevel);
5677
return Parallel.ForEach (source, options, s => {
5778
try {
5879
body (s);
@@ -66,9 +87,16 @@ public static ParallelLoopResult ParallelForEach<TSource> (this AsyncTask asyncT
6687
/// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling.
6788
/// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject)
6889
/// </summary>
69-
public static ParallelLoopResult ParallelForEachWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body)
90+
public static ParallelLoopResult ParallelForEachWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body) =>
91+
asyncTask.ParallelForEachWithLock (source, body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
92+
93+
/// <summary>
94+
/// Calls Parallel.ForEach() with appropriate ParallelOptions and exception handling.
95+
/// Passes an object the inner method can use for locking. The callback is of the form: (T item, object lockObject)
96+
/// </summary>
97+
public static ParallelLoopResult ParallelForEachWithLock<TSource> (this AsyncTask asyncTask, IEnumerable<TSource> source, Action<TSource, object> body, int maxConcurrencyLevel)
7098
{
71-
var options = ParallelOptions (asyncTask);
99+
var options = ParallelOptions (asyncTask, maxConcurrencyLevel);
72100
var lockObject = new object ();
73101
return Parallel.ForEach (source, options, s => {
74102
try {
@@ -79,28 +107,47 @@ public static ParallelLoopResult ParallelForEachWithLock<TSource> (this AsyncTas
79107
});
80108
}
81109

82-
static ParallelOptions ParallelOptions (AsyncTask asyncTask) => new ParallelOptions {
110+
static ParallelOptions ParallelOptions (AsyncTask asyncTask, int maxConcurrencyLevel) => new ParallelOptions {
83111
CancellationToken = asyncTask.CancellationToken,
84-
TaskScheduler = TaskScheduler.Default,
112+
TaskScheduler = GetTaskScheduler (maxConcurrencyLevel),
85113
};
86114

115+
static TaskScheduler GetTaskScheduler (int maxConcurrencyLevel)
116+
{
117+
var pair = new ConcurrentExclusiveSchedulerPair (TaskScheduler.Default, maxConcurrencyLevel);
118+
return pair.ConcurrentScheduler;
119+
}
120+
121+
static int DefaultMaxConcurrencyLevel => Math.Max (1, Environment.ProcessorCount - 1);
122+
87123
static void LogErrorAndCancel (AsyncTask asyncTask, Exception exc)
88124
{
89125
asyncTask.LogCodedError ("XA0000", Properties.Resources.XA0000_Exception, exc);
90126
asyncTask.Cancel ();
91127
}
92128

93129
/// <summary>
94-
/// Calls Task.Run() with a proper CancellationToken.
130+
/// Calls Task.Factory.StartNew() with a proper CancellationToken, TaskScheduler, and TaskCreationOptions.LongRunning.
131+
/// </summary>
132+
public static Task RunTask (this AsyncTask asyncTask, Action body) =>
133+
asyncTask.RunTask (body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
134+
135+
/// <summary>
136+
/// Calls Task.Factory.StartNew() with a proper CancellationToken
95137
/// </summary>
96-
public static Task RunTask (this AsyncTask asyncTask, Action body) =>
97-
Task.Run (body, asyncTask.CancellationToken);
138+
public static Task RunTask (this AsyncTask asyncTask, Action body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) =>
139+
Task.Factory.StartNew (body, asyncTask.CancellationToken, creationOptions, GetTaskScheduler (maxConcurrencyLevel));
98140

141+
/// <summary>
142+
/// Calls Task.Factory.StartNew<T>() with a proper CancellationToken, TaskScheduler, and TaskCreationOptions.LongRunning.
143+
/// </summary>
144+
public static Task<TSource> RunTask<TSource> (this AsyncTask asyncTask, Func<TSource> body) =>
145+
asyncTask.RunTask (body, maxConcurrencyLevel: DefaultMaxConcurrencyLevel);
99146

100147
/// <summary>
101-
/// Calls Task.Run<T>() with a proper CancellationToken.
148+
/// Calls Task.Factory.StartNew<T>() with a proper CancellationToken.
102149
/// </summary>
103-
public static Task<TSource> RunTask<TSource> (this AsyncTask asyncTask, Func<TSource> body) =>
104-
Task.Run (body, asyncTask.CancellationToken);
150+
public static Task<TSource> RunTask<TSource> (this AsyncTask asyncTask, Func<TSource> body, int maxConcurrencyLevel, TaskCreationOptions creationOptions = TaskCreationOptions.LongRunning) =>
151+
Task.Factory.StartNew (body, asyncTask.CancellationToken, creationOptions, GetTaskScheduler (maxConcurrencyLevel));
105152
}
106153
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using Microsoft.Android.Build.Tasks;
4+
using NUnit.Framework;
5+
using Xamarin.Build;
6+
7+
namespace Microsoft.Android.Build.BaseTasks.Tests
8+
{
9+
[TestFixture]
10+
public class AsyncTaskExtensionsTests
11+
{
12+
const int Iterations = 32;
13+
14+
[Test]
15+
public async Task RunTask ()
16+
{
17+
bool set = false;
18+
await new AsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type
19+
Assert.IsTrue (set);
20+
}
21+
22+
[Test]
23+
public async Task RunTaskOfT ()
24+
{
25+
bool set = false;
26+
Assert.IsTrue (await new AsyncTask ().RunTask (() => set = true), "RunTask should return true");
27+
Assert.IsTrue (set);
28+
}
29+
30+
[Test]
31+
public async Task WhenAll ()
32+
{
33+
bool set = false;
34+
await new AsyncTask ().WhenAll (new [] { 0 }, _ => set = true);
35+
Assert.IsTrue (set);
36+
}
37+
38+
[Test]
39+
public async Task WhenAllWithLock ()
40+
{
41+
var input = new int [Iterations];
42+
var output = new List<int> ();
43+
await new AsyncTask ().WhenAllWithLock (input, (i, l) => {
44+
lock (l) output.Add (i);
45+
});
46+
Assert.AreEqual (Iterations, output.Count);
47+
}
48+
49+
[Test]
50+
public void ParallelForEach ()
51+
{
52+
bool set = false;
53+
new AsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true);
54+
Assert.IsTrue (set);
55+
}
56+
57+
[Test]
58+
public void ParallelForEachWithLock ()
59+
{
60+
var input = new int [Iterations];
61+
var output = new List<int> ();
62+
new AsyncTask ().ParallelForEachWithLock (input, (i, l) => {
63+
lock (l) output.Add (i);
64+
});
65+
Assert.AreEqual (Iterations, output.Count);
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)