Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DuplicatePropertyNameChecker to ObjectPool #2328

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b8f75ec
Add DuplicatePropertyNameChecker to ObjectPool
KenitoInc Feb 25, 2022
8dff302
Fix Action delegate and add tests
KenitoInc Feb 28, 2022
9ecf332
Release DuplicatePropertyNameChecker at the end of scope
KenitoInc Mar 2, 2022
505af7e
Remove wrong tags
KenitoInc Mar 4, 2022
4a03fbf
Remove unused Lists
KenitoInc Mar 7, 2022
884059f
Change list ops
KenitoInc Mar 7, 2022
23359a8
Refactor ObjectPool
KenitoInc Mar 8, 2022
24ed770
More optimizations
KenitoInc Mar 8, 2022
6facb2f
Fix csharp version issue
KenitoInc Mar 8, 2022
e0ea495
Add docstring
KenitoInc Mar 8, 2022
c351053
Fix based on review comments
KenitoInc Mar 16, 2022
eea468b
Fixes named argument specifications
KenitoInc Mar 16, 2022
734388a
review comments changes
KenitoInc Mar 18, 2022
16791b4
Return elements to pool
KenitoInc Mar 18, 2022
7d481f7
Return object to pool
KenitoInc Mar 18, 2022
1f3e72c
Code refactoring
KenitoInc Mar 23, 2022
abb2f5c
Remove unused property
KenitoInc Mar 23, 2022
d103f5b
Refactor how we handle NullDuplicatePropertyNameChecker
KenitoInc Mar 23, 2022
39dffb8
Return instance from LightParameterWriter
KenitoInc Mar 24, 2022
a78ceaf
Return more objects to the pool
KenitoInc Mar 24, 2022
6c8be85
modify ObjectPool
KenitoInc Mar 24, 2022
409faea
Dispose ObjectPool and excess array items
KenitoInc Mar 31, 2022
2a27da5
Code cleanup
KenitoInc Apr 1, 2022
4afdaa5
Use DefaultObjectPool
KenitoInc Apr 11, 2022
2b95462
Cleanup WriterValidator
KenitoInc Apr 12, 2022
0090551
Minor style fixes
KenitoInc Apr 12, 2022
af6293b
Fix review comments
KenitoInc Apr 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/Microsoft.OData.Core/IWriterValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ namespace Microsoft.OData
internal interface IWriterValidator
{
/// <summary>
/// Creates a DuplicatePropertyNameChecker instance.
/// Get a DuplicatePropertyNameChecker instance from the object pool.
/// </summary>
/// <returns>The created instance.</returns>
IDuplicatePropertyNameChecker CreateDuplicatePropertyNameChecker();
/// <returns>The instance retrieved from the object pool.</returns>
IDuplicatePropertyNameChecker GetDuplicatePropertyNameChecker();

/// <summary>
/// Return a DuplicatePropertyNameChecker instance to the object pool.
/// </summary>
void ReturnDuplicatePropertyNameChecker(IDuplicatePropertyNameChecker duplicatePropertyNameChecker);

/// <summary>
/// Validates a resource in an expanded link to make sure that the types match.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,13 @@ internal void WriteInstanceAnnotation(
ODataResourceValue resourceValue = value as ODataResourceValue;
if (resourceValue != null)
{
IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.valueSerializer.GetDuplicatePropertyNameChecker();
this.WriteInstanceAnnotationName(propertyName, name);
this.valueSerializer.WriteResourceValue(resourceValue,
expectedType,
treatLikeOpenProperty,
this.valueSerializer.CreateDuplicatePropertyNameChecker());
duplicatePropertyNameChecker);
this.valueSerializer.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
return;
}

Expand Down Expand Up @@ -389,12 +391,14 @@ await this.valueSerializer.WriteNullValueAsync()
ODataResourceValue resourceValue = annotationValue as ODataResourceValue;
if (resourceValue != null)
{
IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.valueSerializer.GetDuplicatePropertyNameChecker();
await this.WriteInstanceAnnotationNameAsync(propertyName, annotationName)
.ConfigureAwait(false);
await this.valueSerializer.WriteResourceValueAsync(resourceValue,
expectedType,
treatLikeOpenProperty,
this.valueSerializer.CreateDuplicatePropertyNameChecker()).ConfigureAwait(false);
duplicatePropertyNameChecker).ConfigureAwait(false);
this.valueSerializer.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ internal void WriteTopLevelProperty(ODataProperty property)

// Note we do not allow named stream properties to be written as top level property.
this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.GetDuplicatePropertyNameChecker();

this.WriteProperty(
property,
null /*owningType*/,
true /* isTopLevel */,
this.CreateDuplicatePropertyNameChecker(),
null /* metadataBuilder */);
this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
owningType: null,
isTopLevel: true,
duplicatePropertyNameChecker: duplicatePropertyNameChecker,
metadataBuilder : null);

this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
this.JsonWriter.EndObjectScope();
});
}
Expand Down Expand Up @@ -276,14 +279,17 @@ await this.WriteContextUriPropertyAsync(kind, () => contextInfo)

// Note we do not allow named stream properties to be written as top level property.
this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.GetDuplicatePropertyNameChecker();

await this.WritePropertyAsync(
property,
null /*owningType*/,
true /* isTopLevel */,
this.CreateDuplicatePropertyNameChecker(),
null /* metadataBuilder */).ConfigureAwait(false);
this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
owningType : null,
isTopLevel : true,
duplicatePropertyNameChecker : duplicatePropertyNameChecker,
metadataBuilder : null).ConfigureAwait(false);

this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
await this.AsynchronousJsonWriter.EndObjectScopeAsync().ConfigureAwait(false);
});
}
Expand Down Expand Up @@ -645,11 +651,15 @@ private void WriteResourceProperty(
Debug.Assert(!this.currentPropertyInfo.IsTopLevel, "Resource property should not be top level");
this.JsonWriter.WriteName(property.Name);

IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.GetDuplicatePropertyNameChecker();

this.JsonLightValueSerializer.WriteResourceValue(
resourceValue,
this.currentPropertyInfo.MetadataType.TypeReference,
isOpenPropertyType,
this.CreateDuplicatePropertyNameChecker());
duplicatePropertyNameChecker);

this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
}

/// <summary>
Expand Down Expand Up @@ -1071,11 +1081,15 @@ private async Task WriteResourcePropertyAsync(
await this.AsynchronousJsonWriter.WriteNameAsync(property.Name)
.ConfigureAwait(false);

IDuplicatePropertyNameChecker duplicatePropertyNameChecker = this.GetDuplicatePropertyNameChecker();

await this.JsonLightValueSerializer.WriteResourceValueAsync(
resourceValue,
this.currentPropertyInfo.MetadataType.TypeReference,
isOpenPropertyType,
this.CreateDuplicatePropertyNameChecker()).ConfigureAwait(false);
duplicatePropertyNameChecker).ConfigureAwait(false);

this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public virtual void WriteCollectionValue(
{
if (duplicatePropertyNamesChecker == null)
{
duplicatePropertyNamesChecker = this.CreateDuplicatePropertyNameChecker();
duplicatePropertyNamesChecker = this.GetDuplicatePropertyNameChecker();
}

this.WriteResourceValue(
Expand Down Expand Up @@ -286,6 +286,11 @@ public virtual void WriteCollectionValue(
}
}
}

if (duplicatePropertyNamesChecker != null)
{
this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNamesChecker);
}
}

// End the array scope which holds the items
Expand Down Expand Up @@ -567,7 +572,7 @@ await this.AsynchronousODataAnnotationWriter.WriteODataTypeInstanceAnnotationAsy
{
if (duplicatePropertyNamesChecker == null)
{
duplicatePropertyNamesChecker = this.CreateDuplicatePropertyNameChecker();
duplicatePropertyNamesChecker = this.GetDuplicatePropertyNameChecker();
}

await this.WriteResourceValueAsync(
Expand Down Expand Up @@ -608,6 +613,11 @@ await this.WriteResourceValueAsync(
}
}
}

if (duplicatePropertyNamesChecker != null)
{
this.ReturnDuplicatePropertyNameChecker(duplicatePropertyNamesChecker);
}
}

// End the array scope which holds the items
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ protected override void EndResource(ODataResource resource)

this.jsonLightResourceSerializer.WriteResourceEndMetadataProperties(resourceScope, resourceScope.DuplicatePropertyNameChecker);

// Release the DuplicatePropertyNameChecker to the ObjectPool since an instance is removed from the ObjectPool each time we create a ResourceScope.
this.jsonLightResourceSerializer.ReturnDuplicatePropertyNameChecker(resourceScope.DuplicatePropertyNameChecker);

// Close the object scope
this.jsonWriter.EndObjectScope();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OData.Core/ODataCollectionWriterCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ protected IDuplicatePropertyNameChecker DuplicatePropertyNameChecker
{
return duplicatePropertyNameChecker
?? (duplicatePropertyNameChecker
= outputContext.MessageWriterSettings.Validator.CreateDuplicatePropertyNameChecker());
= outputContext.MessageWriterSettings.Validator.GetDuplicatePropertyNameChecker());
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OData.Core/ODataParameterWriterCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected IDuplicatePropertyNameChecker DuplicatePropertyNameChecker
return this.duplicatePropertyNameChecker ??
(this.duplicatePropertyNameChecker =
outputContext.MessageWriterSettings.Validator
.CreateDuplicatePropertyNameChecker());
.GetDuplicatePropertyNameChecker());
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/Microsoft.OData.Core/ODataSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,20 @@ internal IEdmModel Model
}

/// <summary>
/// Creates a new instance of a duplicate property names checker.
/// Get an instance of a duplicate property names checker from the object pool.
/// </summary>
/// <returns>The newly created instance of duplicate property names checker.</returns>
internal IDuplicatePropertyNameChecker CreateDuplicatePropertyNameChecker()
internal IDuplicatePropertyNameChecker GetDuplicatePropertyNameChecker()
{
return MessageWriterSettings.Validator.CreateDuplicatePropertyNameChecker();
return MessageWriterSettings.Validator.GetDuplicatePropertyNameChecker();
}

/// <summary>
/// Returns an instance of a duplicate property names checker to the object pool.
/// </summary>
internal void ReturnDuplicatePropertyNameChecker(IDuplicatePropertyNameChecker duplicatePropertyNameChecker)
{
MessageWriterSettings.Validator.ReturnDuplicatePropertyNameChecker(duplicatePropertyNameChecker);
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.OData.Core/ODataWriterCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4133,7 +4133,7 @@ internal ResourceBaseScope(WriterState state, ODataResourceBase resource, ODataR

if (resource != null)
{
duplicatePropertyNameChecker = writerSettings.Validator.CreateDuplicatePropertyNameChecker();
duplicatePropertyNameChecker = writerSettings.Validator.GetDuplicatePropertyNameChecker();
}

this.serializationInfo = serializationInfo;
Expand Down
100 changes: 100 additions & 0 deletions src/Microsoft.OData.Core/ObjectPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//---------------------------------------------------------------------
// <copyright file="ObjectPool.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

using System;

namespace Microsoft.OData
{
/// <summary>
/// Manages a pool of reusable objects. An object retrieved from the pool should be returned when it's no longer being used so that it can be later recycled.
/// </summary>
/// <typeparam name="T">The type of objects to pool.</typeparam>
/// <remarks>This object pool is not thread-safe and not intended to be shared across concurrent requests.
/// Consider using the DefaultObjectPool in 8.x once we drop support for netstandard 1.1
/// </remarks>
internal class ObjectPool<T> where T : class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be trivializing this, but why isn't this the implementation:

public sealed class ObjectPool<T>
{
  private readonly Func<T> factory;
  private readonly T[] items;
  private readonly int next;
  private readonly numberOfObjectsCreated;

  public ObjectPool(Func<T> factory, int size)
  {
    this.factory = factory;
    this.items = new T[size];
    this.next = 0;
    this.numberOfObjectsCreated = 0;
  }

  public T Get()
  {
    // do we need to create new items?
    if (this.next == this.numberOfObjectsCreated)
    {
      // are we out of slots for new items?
      if (this.numberOfObjectsCreated > this.items.Length)
      {
        throw new Exception("no more available");
      }

      this.items[this.next] = this.factory();
      this.numberOfObjectsCreated++;
    }

    return this.items[this.next++];
  }

  public void Return()
  {
    this.next--;
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's a good idea to throw an exception when we're out of slots. I think in that case we should either "resize" the array (which is maybe more costly than we want to pay) or create a new object that's not stored in the pool. If there's more demand than what can fit in the pool, then the excess is not pooled, but doesn't throw either. My 2 cents.

Copy link
Contributor

@corranrogue9 corranrogue9 Apr 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've consolidated my different feedback threads into this one because I feel like something is getting lost.

  1. The majority of my feedback revolves around the fact that we are not just using indices to track the next items. Doing so removes so much of the code, and seems to make the total number of operations consistent for each scenario. Particularly, notice how the implementation above is O(1), while the current implementation is O(n). There is a lot of extra code to try to make it mostly perform as O(1), but I don't think we should be optimizing an O(n) algorithm when an O(1) option is available.
  2. By being fixed size, we lose out on the optimization if the nesting level ever changes. I'm not actually proposing that we just throw an exception if the nesting level changes, and that the customer ends up perplexed. I'm saying that a fixed size ObjectPool should have in its contract that it throws when it runs out of space. We should catch this exception somewhere else (probably in the ODataSerializer). If we don't throw an exception and we increase the nesting level, we will never notice that we are losing out on this optimization. I think we should either use a dynamically sized object pool, or we should throw an exception that we surface in Debug configurations and don't surface in Release configurations.
    EDIT: another note on this, the case I'm describing here is just the obvious example of nesting levels change. Any bug that returns an extra object will cause the count to be off, and we will start leaking instances and overload the garbage collector just as we do today.

Ultimately, I think we should be using the .NET object pool implementation and we don't use an object pool for versions that don't have it. This should incentivize users to move to newer versions of .NET in order to receive these performance benefits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corranrogue9

  1. Your solution still doesn't the resolve the issue where we have a fixed size object pool. We shouldn't be throwing an exception.
  2. My solution is flexible when the nesting is larger than the array size. We will still spin more objects. What will happen is when returning the objects to the pool, the extra objects will be disposed. The solution is modelled around the .Net object pool.
  3. In the ObjectPool docstring I have indicated we should switch to the DefaultObjectPool in 8.x once we drop support for netstandard 1.1. Having 2 types object pools in ODL now only adds extra complexity.

Conclusion: This PR was intended on to reduce heap allocation. At the same time not regress cpu time. Further optimizations can be done later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your solution still doesn't the resolve the issue where we have a fixed size object pool

It does. It throws an exception.

We shouldn't be throwing an exception

Yes we should. We need to notice when we have made an unrelated change that causes us to go beyond the current nesting level and therefore lose out on this optimization. Please address this concern that I have: if we don't throw an exception, how do we, as a team, know that we have inadvertently increased the nesting level?

My solution is flexible when the nesting is larger than the array size. We will still spin more objects.

What is the benefit of doing this? From what I see, it only serves to hide potential bugs.

The solution is modelled around the .Net object pool.

The .NET implementation has an extra requirement of being thread-safe, meaning that Interlocked.Exchange is imperative to its functioning. Because of this, reference types are needed and keeping a reference to the next item is essential. Without that requirement, we can improve performance by using a different algorithm rather than by optimizing the .NET algorithm.

In the ObjectPool docstring I have indicated we should switch to the DefaultObjectPool in 8.x once we drop support for netstandard 1.1

Please create an issue for this so that the work can be planned for and assigned

Having 2 types object pools in ODL now only adds extra complexity

I am not proposing that we have 2. I am proposing that we don't ship this optimization for older versions of .NET. This will incentivize customers to use newer versions of .NET. Alternatively, we could simply take the .NET implementation and place it directly into our repo, referencing where it came from. Then, we can ship down-level, but have complete consistency across versions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to throw an exception, because that's implying that having responses with depths that exceed the object pool size is an error. The size of the object pool shouldn't impose a limit on the depth of the response (there's a separate setting for that).

If the goal of the exception is for sanity checks to ensure we are not exceeding the object pool size due a bug in our code rather than due to the depth of the response, maybe we could have debug messages or assertions when the object pool size is exceeded, when the number of returned objects is not equal the number of requested objects, etc.

Or did you mean that we conditionally throw exceptions based on #if DEBUG or something like that?

I prefer the simpler implementation provided by @corranrogue9, but I still think it's valuable to delay allocating the array until the second element is requested, that way we avoid unnecessary allocations for non-nested responses. Unless we can demonstrate that the cost of having the if statement that's always checked is worse.

Also, since the point of this PR is to reduce allocations (and GC), I think it's okay to only care about reference types.

{
private readonly Func<T> objectGenerator;
private ObjectWrapper[] items;
private T firstItem;

// The pool size is equal to the level of nesting in the response.
// 32 is an arbitrary figure. Could be adjusted appropriately.
private const int POOL_SIZE = 8;

/// <summary>
/// To initialize the object pool.
/// </summary>
/// <param name="objectGenerator">Used to create an instance of a <typeparamref name="T"/>.</param>
public ObjectPool(Func<T> objectGenerator)
{
this.objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
}

/// <summary>
/// Gets an object from the pool if one is available, otherwise creates one. The object will need to be returned to the pool when no longer in use.
/// </summary>
/// <returns>An object of type <typeparamref name="T"/> from the pool.</returns>
/// <remarks>It's the responsibility of the caller to clean up the object before using it.</remarks>
public T Get()
{
T item = firstItem;

if (item != null)
{
firstItem = default(T);
return item;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where you should create the array if it's null:

if (items == null) {
   items = new ObjectWrapper[POOL_SIZE];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what was here before, but there's really no need to have an instance of ObjectPool with a null value for items. I don't see us ever set it back to null, so we could mark it readonly if we initialize it in the constructor. Also, doing this means that we will check the conditional every time Get is called, which seems suboptimal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about calling if (items == null) on every Get() call seeming suboptimal. My suggestion was based on eliminating an array allocation (and deallocation) when it's not necessary. I would assume it would be much easier for JIT or CPU to optimize if (items == null) away via branch prediction or some other techniques if items == null evaluates to the same value most of the times. So in both cases there's trade-off to be made. But this lazy initialization pattern seems common in BCL source code, e.g.:

Can this be evaluated using benchmarks?

if (items == null)
{
items = new ObjectWrapper[POOL_SIZE];
}

for (int i = 0; i < items.Length; i++)
{
item = items[i].Element;

if (item != null)
{
items[i].Element = default(T);
return item;
}
}

return objectGenerator();
}

/// <summary>
/// Return an object to the pool.
/// </summary>
/// <param name="obj">The object to return to the pool.</param>
public void Return(T obj)
{
if (firstItem == null)
{
firstItem = obj;
return;
}

for (int i = 0; i < items.Length; ++i)
{
if (items[i].Element == null)
{
items[i].Element = obj;
break;
}
}
}

// PERF: the struct wrapper avoids array-covariance-checks from the runtime when assigning to elements of the array.
// See comment in this PR https://github.com/dotnet/extensions/pull/314#discussion_r169390645
private struct ObjectWrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we quantify this performance benefit for our scenarios? I strongly consider this an anti-pattern, given that we can never actually refer to an individual ObjectWrapper without introducing impossible to find bugs. Just this line from the Return method items[i].Element = obj; would not work if written as var wrapper = items[i]; wrapper.Element = obj even though they look to be doing identical things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By avoiding runtime checks, we reduce CPU time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a noticeable CPU difference in profiler or benchmarks with and without ObjectWrapper?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some sort of numbers is really necessary to justify introducing this complexity.

{
public T Element;
}
}
}
Loading