Skip to content

Commit 9224630

Browse files
anthony-kellerclaude
authored andcommitted
fix(perf): Materialize trigger collections to eliminate ConcatIterator CPU waste
Replace IEnumerable<T> fields with List<T> to avoid deeply nested ConcatIterator chains from repeated .Concat() calls. Each WithAdditionalTrigger call now uses List.Add() instead. Also cache the service provider hash code, use O(1) List.Count property instead of LINQ .Count(), and add count-based fast paths in ShouldUseSameServiceProvider to short-circuit before SequenceEqual. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 0cc6aa1)
1 parent fc0ca32 commit 9224630

1 file changed

Lines changed: 54 additions & 48 deletions

File tree

src/EntityFrameworkCore.Triggered/Infrastructure/Internal/TriggersOptionExtension.cs

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class TriggersOptionExtension : IDbContextOptionsExtension
1717
sealed class ExtensionInfo : DbContextOptionsExtensionInfo
1818
{
1919
private string? _logFragment;
20+
private int? _serviceProviderHashCode;
2021
public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension)
2122
{
2223
}
@@ -44,14 +45,19 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
4445
throw new ArgumentNullException(nameof(debugInfo));
4546
}
4647

47-
debugInfo["Triggers:TriggersCount"] = (Extension._triggers?.Count() ?? 0).ToString();
48-
debugInfo["Triggers:TriggerTypesCount"] = (Extension._triggerTypes?.Count() ?? 0).ToString();
48+
debugInfo["Triggers:TriggersCount"] = (Extension._triggers?.Count ?? 0).ToString();
49+
debugInfo["Triggers:TriggerTypesCount"] = (Extension._triggerTypes?.Count ?? 0).ToString();
4950
debugInfo["Triggers:MaxCascadeCycles"] = Extension._maxCascadeCycles.ToString();
5051
debugInfo["Triggers:CascadeBehavior"] = Extension._cascadeBehavior.ToString();
5152
}
5253

5354
public override int GetServiceProviderHashCode()
5455
{
56+
if (_serviceProviderHashCode.HasValue)
57+
{
58+
return _serviceProviderHashCode.Value;
59+
}
60+
5561
var hashCode = new HashCode();
5662

5763
if (Extension._triggers != null)
@@ -78,28 +84,56 @@ public override int GetServiceProviderHashCode()
7884
hashCode.Add(Extension._serviceProviderTransform);
7985
}
8086

81-
return hashCode.ToHashCode();
87+
_serviceProviderHashCode = hashCode.ToHashCode();
88+
return _serviceProviderHashCode.Value;
8289
}
8390

8491
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
85-
=> other is ExtensionInfo otherInfo
86-
&& Enumerable.SequenceEqual(Extension._triggers ?? Enumerable.Empty<ValueTuple<object, ServiceLifetime>>(), otherInfo.Extension._triggers ?? Enumerable.Empty<ValueTuple<object, ServiceLifetime>>())
87-
&& Enumerable.SequenceEqual(Extension._triggerTypes ?? Enumerable.Empty<Type>(), otherInfo.Extension._triggerTypes ?? Enumerable.Empty<Type>())
88-
&& Extension._maxCascadeCycles == otherInfo.Extension._maxCascadeCycles
89-
&& Extension._cascadeBehavior == otherInfo.Extension._cascadeBehavior
90-
&& Extension._serviceProviderTransform == otherInfo.Extension._serviceProviderTransform;
92+
{
93+
if (other is not ExtensionInfo otherInfo)
94+
{
95+
return false;
96+
}
97+
98+
// Check cheap scalar comparisons first
99+
if (Extension._maxCascadeCycles != otherInfo.Extension._maxCascadeCycles
100+
|| Extension._cascadeBehavior != otherInfo.Extension._cascadeBehavior
101+
|| Extension._serviceProviderTransform != otherInfo.Extension._serviceProviderTransform)
102+
{
103+
return false;
104+
}
105+
106+
// Check list counts before doing full sequence comparison
107+
var triggersCount = Extension._triggers?.Count ?? 0;
108+
var otherTriggersCount = otherInfo.Extension._triggers?.Count ?? 0;
109+
if (triggersCount != otherTriggersCount)
110+
{
111+
return false;
112+
}
113+
114+
var triggerTypesCount = Extension._triggerTypes?.Count ?? 0;
115+
var otherTriggerTypesCount = otherInfo.Extension._triggerTypes?.Count ?? 0;
116+
if (triggerTypesCount != otherTriggerTypesCount)
117+
{
118+
return false;
119+
}
120+
121+
// Full sequence comparison only when counts match
122+
return Enumerable.SequenceEqual(Extension._triggers ?? Enumerable.Empty<ValueTuple<object, ServiceLifetime>>(), otherInfo.Extension._triggers ?? Enumerable.Empty<ValueTuple<object, ServiceLifetime>>())
123+
&& Enumerable.SequenceEqual(Extension._triggerTypes ?? Enumerable.Empty<Type>(), otherInfo.Extension._triggerTypes ?? Enumerable.Empty<Type>());
124+
}
91125
}
92126

93127
private ExtensionInfo? _info;
94-
private IEnumerable<(object typeOrInstance, ServiceLifetime lifetime)>? _triggers;
95-
private IEnumerable<Type> _triggerTypes;
128+
private List<(object typeOrInstance, ServiceLifetime lifetime)>? _triggers;
129+
private List<Type> _triggerTypes;
96130
private int _maxCascadeCycles = 100;
97131
private CascadeBehavior _cascadeBehavior = CascadeBehavior.EntityAndType;
98132
private Func<IServiceProvider, IServiceProvider>? _serviceProviderTransform;
99133

100134
public TriggersOptionExtension()
101135
{
102-
_triggerTypes = new[] {
136+
_triggerTypes = new List<Type> {
103137
typeof(IBeforeSaveTrigger<>),
104138
typeof(IBeforeSaveAsyncTrigger<>),
105139
typeof(IAfterSaveTrigger<>),
@@ -125,10 +159,10 @@ public TriggersOptionExtension(TriggersOptionExtension copyFrom)
125159
{
126160
if (copyFrom._triggers != null)
127161
{
128-
_triggers = copyFrom._triggers;
162+
_triggers = new List<(object typeOrInstance, ServiceLifetime lifetime)>(copyFrom._triggers);
129163
}
130164

131-
_triggerTypes = copyFrom._triggerTypes;
165+
_triggerTypes = new List<Type>(copyFrom._triggerTypes);
132166
_maxCascadeCycles = copyFrom._maxCascadeCycles;
133167
_cascadeBehavior = copyFrom._cascadeBehavior;
134168
_serviceProviderTransform = copyFrom._serviceProviderTransform;
@@ -263,17 +297,8 @@ public TriggersOptionExtension WithAdditionalTrigger(Type triggerType, ServiceLi
263297
}
264298

265299
var clone = Clone();
266-
var triggerEnumerable = Enumerable.Repeat(((object)triggerType, lifetime), 1);
267-
268-
if (clone._triggers == null)
269-
{
270-
clone._triggers = triggerEnumerable;
271-
}
272-
else
273-
{
274-
clone._triggers = clone._triggers.Concat(triggerEnumerable);
275-
}
276-
300+
clone._triggers ??= new List<(object typeOrInstance, ServiceLifetime lifetime)>();
301+
clone._triggers.Add(((object)triggerType, lifetime));
277302

278303
return clone;
279304
}
@@ -291,17 +316,8 @@ public TriggersOptionExtension WithAdditionalTrigger(object instance)
291316
}
292317

293318
var clone = Clone();
294-
var triggersEnumerable = Enumerable.Repeat((instance, ServiceLifetime.Singleton), 1);
295-
296-
if (clone._triggers == null)
297-
{
298-
clone._triggers = triggersEnumerable;
299-
}
300-
else
301-
{
302-
clone._triggers = clone._triggers.Concat(triggersEnumerable);
303-
}
304-
319+
clone._triggers ??= new List<(object typeOrInstance, ServiceLifetime lifetime)>();
320+
clone._triggers.Add((instance, ServiceLifetime.Singleton));
305321

306322
return clone;
307323
}
@@ -313,19 +329,9 @@ public TriggersOptionExtension WithAdditionalTriggerType(Type triggerType)
313329
throw new ArgumentNullException(nameof(triggerType));
314330
}
315331

316-
317332
var clone = Clone();
318-
var triggerTypesEnumerable = Enumerable.Repeat(triggerType, 1);
319-
320-
if (clone._triggerTypes == null)
321-
{
322-
clone._triggerTypes = triggerTypesEnumerable;
323-
}
324-
else
325-
{
326-
clone._triggerTypes = clone._triggerTypes.Concat(triggerTypesEnumerable);
327-
}
328-
333+
clone._triggerTypes ??= new List<Type>();
334+
clone._triggerTypes.Add(triggerType);
329335

330336
return clone;
331337
}

0 commit comments

Comments
 (0)