Skip to content

Commit 63e55f1

Browse files
authored
Fixed Lazy loader performance issue (#35835)
Use static AsyncLocal
1 parent e3ef74e commit 63e55f1

File tree

2 files changed

+89
-26
lines changed

2 files changed

+89
-26
lines changed

Diff for: src/EFCore/Infrastructure/Internal/LazyLoader.cs

+22-15
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public class LazyLoader : ILazyLoader, IInjectableService
2323
private bool _detached;
2424
private IDictionary<string, bool>? _loadedStates;
2525
private readonly Lock _isLoadingLock = new Lock();
26-
private readonly Dictionary<(object Entity, string NavigationName), (TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth)> _isLoading = new(NavEntryEqualityComparer.Instance);
26+
private readonly Dictionary<(object Entity, string NavigationName), TaskCompletionSource> _isLoading = new(NavEntryEqualityComparer.Instance);
27+
private static readonly AsyncLocal<int> _isLoadingCallDepth = new();
2728
private HashSet<string>? _nonLazyNavigations;
2829

2930
/// <summary>
@@ -112,26 +113,28 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName
112113
var navEntry = (entity, navigationName);
113114

114115
bool exists;
115-
(TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth) isLoadingValue;
116+
TaskCompletionSource isLoadingValue;
116117

117118
lock (_isLoadingLock)
118119
{
119120
ref var refIsLoadingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(_isLoading, navEntry, out exists);
120121
if (!exists)
121122
{
122-
refIsLoadingValue = (new(), new());
123+
refIsLoadingValue = new();
123124
}
125+
_isLoadingCallDepth.Value++;
124126
isLoadingValue = refIsLoadingValue!;
125-
isLoadingValue.Depth.Value++;
126127
}
127128

128129
if (exists)
129130
{
130-
// Only waits for the outermost call on the call stack. See #35528.
131-
if (isLoadingValue.Depth.Value == 1)
131+
// Only waits for the outermost call on the call stack. See #35528.
132+
// if _isLoadingCallDepth.Value > 1 the call is recursive, waiting probably generates a deadlock See #35832.
133+
if (_isLoadingCallDepth.Value == 1)
132134
{
133-
isLoadingValue.TaskCompletionSource.Task.Wait();
135+
isLoadingValue.Task.Wait();
134136
}
137+
_isLoadingCallDepth.Value--;
135138
return;
136139
}
137140

@@ -156,7 +159,8 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName
156159
}
157160
finally
158161
{
159-
isLoadingValue.TaskCompletionSource.TrySetResult();
162+
isLoadingValue.TrySetResult();
163+
_isLoadingCallDepth.Value--;
160164
lock (_isLoadingLock)
161165
{
162166
_isLoading.Remove(navEntry);
@@ -181,26 +185,28 @@ public virtual async Task LoadAsync(
181185
var navEntry = (entity, navigationName);
182186

183187
bool exists;
184-
(TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth) isLoadingValue;
188+
TaskCompletionSource isLoadingValue;
185189

186190
lock (_isLoadingLock)
187191
{
188192
ref var refIsLoadingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(_isLoading, navEntry, out exists);
189193
if (!exists)
190194
{
191-
refIsLoadingValue = (new(), new());
195+
refIsLoadingValue = new();
192196
}
197+
_isLoadingCallDepth.Value++;
193198
isLoadingValue = refIsLoadingValue!;
194-
isLoadingValue.Depth.Value++;
195199
}
196200

197201
if (exists)
198202
{
199-
// Only waits for the outermost call on the call stack. See #35528.
200-
if (isLoadingValue.Depth.Value == 1)
203+
// Only waits for the outermost call on the call stack. See #35528.
204+
// if _isLoadingCallDepth.Value > 1 the call is recursive, waiting probably generates a deadlock See #35832.
205+
if (_isLoadingCallDepth.Value == 1)
201206
{
202-
await isLoadingValue.TaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
207+
await isLoadingValue.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
203208
}
209+
_isLoadingCallDepth.Value--;
204210
return;
205211
}
206212

@@ -226,7 +232,8 @@ await entry.LoadAsync(
226232
}
227233
finally
228234
{
229-
isLoadingValue.TaskCompletionSource.TrySetResult();
235+
isLoadingValue.TrySetResult();
236+
_isLoadingCallDepth.Value--;
230237
lock (_isLoadingLock)
231238
{
232239
_isLoading.Remove(navEntry);

Diff for: test/EFCore.Specification.Tests/LoadTestBase.cs

+67-11
Original file line numberDiff line numberDiff line change
@@ -5059,14 +5059,31 @@ public virtual async Task Lazy_loading_is_thread_safe(bool noTracking, bool asyn
50595059
var parent = query.Single();
50605060

50615061
var children = (await parent.LazyLoadChildren(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5062+
var childrenInvert = (await parent.LazyLoadChildren(!async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5063+
50625064
var singlePkToPk = (await parent.LazyLoadSinglePkToPk(async))?.Id;
5065+
var singlePkToPkInvert = (await parent.LazyLoadSinglePkToPk(!async))?.Id;
5066+
50635067
var single = (await parent.LazyLoadSingle(async))?.Id;
5068+
var singleInvert = (await parent.LazyLoadSingle(!async))?.Id;
5069+
50645070
var childrenAk = (await parent.LazyLoadChildrenAk(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5071+
var childrenAkInvert = (await parent.LazyLoadChildrenAk(!async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5072+
50655073
var singleAk = (await parent.LazyLoadSingleAk(async))?.Id;
5074+
var singleAkInvert = (await parent.LazyLoadSingleAk(!async))?.Id;
5075+
50665076
var childrenShadowFk = (await parent.LazyLoadChildrenShadowFk(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5077+
var childrenShadowFkInvert = (await parent.LazyLoadChildrenShadowFk(!async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5078+
50675079
var singleShadowFk = (await parent.LazyLoadSingleShadowFk(async))?.Id;
5080+
var singleShadowFkInvert = (await parent.LazyLoadSingleShadowFk(!async))?.Id;
5081+
50685082
var childrenCompositeKey = (await parent.LazyLoadChildrenCompositeKey(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5083+
var childrenCompositeKeyInvert = (await parent.LazyLoadChildrenCompositeKey(!async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5084+
50695085
var singleCompositeKey = (await parent.LazyLoadSingleCompositeKey(async))?.Id;
5086+
var singleCompositeKeyInvert = (await parent.LazyLoadSingleCompositeKey(!async))?.Id;
50705087

50715088
var parent2 = query2.Single();
50725089

@@ -5075,18 +5092,57 @@ public virtual async Task Lazy_loading_is_thread_safe(bool noTracking, bool asyn
50755092
MaxDegreeOfParallelism = Environment.ProcessorCount * 500
50765093
};
50775094

5078-
await Parallel.ForAsync(0, 50000, parallelOptions, async (i, ct) =>
5079-
{
5080-
Assert.Equal(children, (await parent2.LazyLoadChildren(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5081-
Assert.Equal(singlePkToPk, (await parent2.LazyLoadSinglePkToPk(async))?.Id);
5082-
Assert.Equal(single, (await parent2.LazyLoadSingle(async))?.Id);
5083-
Assert.Equal(childrenAk, (await parent2.LazyLoadChildrenAk(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5084-
Assert.Equal(singleAk, (await parent2.LazyLoadSingleAk(async))?.Id);
5085-
Assert.Equal(childrenShadowFk, (await parent2.LazyLoadChildrenShadowFk(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5086-
Assert.Equal(singleShadowFk, (await parent2.LazyLoadSingleShadowFk(async))?.Id);
5087-
Assert.Equal(childrenCompositeKey, (await parent2.LazyLoadChildrenCompositeKey(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5088-
Assert.Equal(singleCompositeKey, (await parent2.LazyLoadSingleCompositeKey(async))?.Id);
5095+
await Parallel.ForAsync(0, 10000, parallelOptions, async (i, ct) =>
5096+
{
5097+
await Task.WhenAll(
5098+
AssertEqual(
5099+
(children, async () => (await parent2.LazyLoadChildren(async))?.Select(x => x.Id).OrderBy(x => x).ToList()),
5100+
(childrenInvert, async () => (await parent2.LazyLoadChildren(!async))?.Select(x => x.Id).OrderBy(x => x).ToList())
5101+
),
5102+
AssertEqual(
5103+
(singlePkToPk, async () => (await parent2.LazyLoadSinglePkToPk(async))?.Id),
5104+
(singlePkToPkInvert, async () => (await parent2.LazyLoadSinglePkToPk(!async))?.Id)
5105+
),
5106+
AssertEqual(
5107+
(single, async () => (await parent2.LazyLoadSingle(async))?.Id),
5108+
(singleInvert, async () => (await parent2.LazyLoadSingle(!async))?.Id)
5109+
),
5110+
AssertEqual(
5111+
(childrenAk, async () => (await parent2.LazyLoadChildrenAk(async))?.Select(x => x.Id).OrderBy(x => x).ToList()),
5112+
(childrenAkInvert, async () => (await parent2.LazyLoadChildrenAk(!async))?.Select(x => x.Id).OrderBy(x => x).ToList())
5113+
),
5114+
AssertEqual(
5115+
(singleAk, async () => (await parent2.LazyLoadSingleAk(async))?.Id),
5116+
(singleAkInvert, async () => (await parent2.LazyLoadSingleAk(!async))?.Id)
5117+
),
5118+
AssertEqual(
5119+
(childrenShadowFk, async () => (await parent2.LazyLoadChildrenShadowFk(async))?.Select(x => x.Id).OrderBy(x => x).ToList()),
5120+
(childrenShadowFkInvert, async () => (await parent2.LazyLoadChildrenShadowFk(!async))?.Select(x => x.Id).OrderBy(x => x).ToList())
5121+
),
5122+
AssertEqual(
5123+
(singleShadowFk, async () => (await parent2.LazyLoadSingleShadowFk(async))?.Id),
5124+
(singleShadowFkInvert, async () => (await parent2.LazyLoadSingleShadowFk(!async))?.Id)
5125+
),
5126+
AssertEqual(
5127+
(childrenCompositeKey, async () => (await parent2.LazyLoadChildrenCompositeKey(async))?.Select(x => x.Id).OrderBy(x => x).ToList()),
5128+
(childrenCompositeKeyInvert, async () => (await parent2.LazyLoadChildrenCompositeKey(!async))?.Select(x => x.Id).OrderBy(x => x).ToList())
5129+
),
5130+
AssertEqual(
5131+
(singleCompositeKey, async () => (await parent2.LazyLoadSingleCompositeKey(async))?.Id),
5132+
(singleCompositeKeyInvert, async () => (await parent2.LazyLoadSingleCompositeKey(!async))?.Id)
5133+
)
5134+
);
50895135
});
5136+
5137+
static async Task AssertEqual<T>((T Data, Func<Task<T>> Expected) data, (T Data, Func<Task<T>> Expected) dataInvert)
5138+
{
5139+
//Do the processing at the same time
5140+
var dataTask = data.Expected();
5141+
var dataInvertTask = dataInvert.Expected();
5142+
5143+
Assert.Equal(data.Data, await dataTask);
5144+
Assert.Equal(dataInvert.Data, await dataInvertTask);
5145+
}
50905146
}
50915147

50925148
private static void SetState(

0 commit comments

Comments
 (0)