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

Use pooled objects in more locations #10598

Merged
merged 7 commits into from
Jul 11, 2024
Merged

Conversation

ryzngard
Copy link
Contributor

@ryzngard ryzngard commented Jul 9, 2024

Cleaning up some places that are newing up Lists intead of using the object pool. I also moved a few things to ImmutableArray instead of List since they weren't mutated after initial collection

@ryzngard ryzngard requested review from a team as code owners July 9, 2024 20:27
@@ -38,7 +39,7 @@ public static async Task<TextEdit[]> GetUsingStatementEditsAsync(RazorCodeDocume
var oldUsings = await FindUsingDirectiveStringsAsync(originalCSharpText, cancellationToken).ConfigureAwait(false);
var newUsings = await FindUsingDirectiveStringsAsync(changedCSharpText, cancellationToken).ConfigureAwait(false);

var edits = new List<TextEdit>();
using var _ = ListPool<TextEdit>.GetPooledObject(out var edits);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure which would be better here, ListPool or ArrayBuilderPool

Copy link
Member

Choose a reason for hiding this comment

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

ListPool<T> is fine, but if you just want an array in the end, consider using PooledArrayBuilder<T> like so:

using var edits = new PooledArrayBuilder<TextEdit>();

For small arrays (4 or fewer elements), it uses stack space. For larger arrays, it grabs a builder from ArrayBuilderPool<T>.

Copy link
Contributor

@davidwengier davidwengier Jul 9, 2024

Choose a reason for hiding this comment

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

To be read in the least authoratative voice you can imagine, but I've always been of the opinion that if something is never realized as an immutable collection, then using an immutable collection builder is a bit silly, and List is fine. I think they work the same way (resizing internal arrays), but my hope is that List, being the more common type, might get some tiny jit advantage.

EDIT: Ignore me, Dustin knows best!

Copy link
Member

Choose a reason for hiding this comment

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

To be fair, you're completley right. ListPool<T> is fine when you're just going to call ToArray(). However, if it's likely to be a small array, PooledArrayBuilder<T> we'll be better because it won't even hit the pool.

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 much happier when there is one specific guidance we can give. So "just use pooled array builder, and call builder.toimmutable" is an easy rule to remember.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I've arrived at for this change

  • Default to PooledArrayBuilder<T> for places that are just building them and then ToArray/ToImmutable
  • Use ArrayBuilderPool<T> when there's lots of functions passing around the list but one cental start/end of the chain to manage lifetime and are synchronous (use AsRef)
  • Use ListPool<T> for the async version of above

Copy link
Member

Choose a reason for hiding this comment

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

  • Use ArrayBuilderPool when there's lots of functions passing around the list but one cental start/end of the chain to manage lifetime and are synchronous (use AsRef)
  • Use ListPool for the async version of above

FWIW, ArrayBuilderPool<T> can be used for both of those situations. ImmutableArray<T>.Builder is a reference type and does not need to be passed by ref. Of course, PooledArrayBuilder<T> can't be passed to an async method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the last and most nit-picky way, I just find it more annoying to write ImmutableArray<T>.Builder as the type. If there's a perf benefit I'm more than happy to amend my thinking, otherwise I'm lazy and like typing less

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or better put, I should have merged the first two dots

  • PooledArrayBuilder<T> for all cases, and if passed sync pass by ref.
  • ListPool<T> for passing to async methods

Copy link
Member

Choose a reason for hiding this comment

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

I just find it more annoying to write ImmutableArray<T>.Builder as the type

For sure. I totally agree with you on that. 😄

Copy link
Member

@DustinCampbell DustinCampbell left a comment

Choose a reason for hiding this comment

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

Sorry to "request changes", but there are a few spots that'll cause problems if this goes in. In general, I have a few pieces of feedback that apply throughout:

  1. Prefer PooledArrayBuilder<T> when the goal is to create an ImmutableArray<T> or a T[]. This data structure is generally more efficient because it stores the first four elements on the stack and only acquires an ImmutableArray<T>.Builder from ArrayBuilderPool<T> if there are more than four elements. For a completely temporary array (i.e. collecting a few items and just iterating them), PooledArrayBuilder<T> is especially useful. Also, PooledArrayBuider<T> is better ergonomically, since it doesn't require an out var.
  2. Call ImmutableArray<T>.Builder.ToImmutable() rather than ToImmutableArray(). ToImmutable() is the instance method provided on all immutable collection builder types to produce the final collection. ToImmutableArray() is an extension method provided for converting other data structures to an ImmutableArray<T>. Ultimately, ToImmutableArray() will work, but it ends up making an unnecessary extra null check.
  3. Don't spread builders into collection expressions. The compiler does not generate optimal code for this scenario. Instead, call ToImmutable() or ToArray().
  4. Be sure to set the capacity when known. I spotted one place where the capacity was no longer set. For many structures, we have SetCapacityIflarger(...) extension methods. PooledArrayBuilder<T> can take a capacity in its constructor.
  5. Do not allow pooled objects to escape from methods! This is very dangerous, and I marked a couple of spots where this was happening. When this happens to a pooled object retrieved in a using, the object will still be used outside the method after it has been returned to the pool.

@@ -38,7 +39,7 @@ public static async Task<TextEdit[]> GetUsingStatementEditsAsync(RazorCodeDocume
var oldUsings = await FindUsingDirectiveStringsAsync(originalCSharpText, cancellationToken).ConfigureAwait(false);
var newUsings = await FindUsingDirectiveStringsAsync(changedCSharpText, cancellationToken).ConfigureAwait(false);

var edits = new List<TextEdit>();
using var _ = ListPool<TextEdit>.GetPooledObject(out var edits);
Copy link
Member

Choose a reason for hiding this comment

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

ListPool<T> is fine, but if you just want an array in the end, consider using PooledArrayBuilder<T> like so:

using var edits = new PooledArrayBuilder<TextEdit>();

For small arrays (4 or fewer elements), it uses stack space. For larger arrays, it grabs a builder from ArrayBuilderPool<T>.

Copy link
Contributor

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

Thoughts:

  • Legacy editor change is scary
  • I never know whether to call .ToImmutableArray() or .DrainToImmutable() I think Dustin answered this above
  • Our use of ListPool vs ArrayBuilderPool is inconsistent but I don't know if thats an issue or not I think Dustin answered this above

Overall a very good change and cleanup though, thank you

@ryzngard ryzngard marked this pull request as draft July 10, 2024 01:28
@ryzngard
Copy link
Contributor Author

Converting to draft until I've responded/fixed feedback. Going to do it incrementally

Copy link
Member

@DustinCampbell DustinCampbell left a comment

Choose a reason for hiding this comment

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

This is looking much better! Happy to approve this change. However, be mindful of the loop reversal in VisualStudioRazorParser. Please don't merge without addressing that.

@@ -77,7 +77,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V

private async Task<WorkspaceEdit?> HandleMappingsAsync(VSInternalMapCodeMapping[] mappings, Guid mapCodeCorrelationId, CancellationToken cancellationToken)
{
using var _ = ArrayBuilderPool<TextDocumentEdit>.GetPooledObject(out var changes);
using var _ = ListPool<TextDocumentEdit>.GetPooledObject(out var changes);
Copy link
Member

Choose a reason for hiding this comment

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

List<> is good here. You could also use a PooledArrayBuilder<T> and pass it by ref as I described above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is used in TryMapCodeAsync so I think it has to be a non-ref argument

Copy link
Member

Choose a reason for hiding this comment

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

Yes -- good point.

@@ -38,7 +39,7 @@ public static async Task<TextEdit[]> GetUsingStatementEditsAsync(RazorCodeDocume
var oldUsings = await FindUsingDirectiveStringsAsync(originalCSharpText, cancellationToken).ConfigureAwait(false);
var newUsings = await FindUsingDirectiveStringsAsync(changedCSharpText, cancellationToken).ConfigureAwait(false);

var edits = new List<TextEdit>();
using var _ = ListPool<TextEdit>.GetPooledObject(out var edits);
Copy link
Member

Choose a reason for hiding this comment

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

  • Use ArrayBuilderPool when there's lots of functions passing around the list but one cental start/end of the chain to manage lifetime and are synchronous (use AsRef)
  • Use ListPool for the async version of above

FWIW, ArrayBuilderPool<T> can be used for both of those situations. ImmutableArray<T>.Builder is a reference type and does not need to be passed by ref. Of course, PooledArrayBuilder<T> can't be passed to an async method.

@ryzngard ryzngard marked this pull request as ready for review July 10, 2024 19:43
{
var typeAccessibilityCodeActions = new List<RazorVSInternalCodeAction>(1);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DustinCampbell this had a list set to 1 and I'm not really sure why... I think I wouldn't need to do that here because that would just keep it on the stack?

Copy link
Member

Choose a reason for hiding this comment

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

Super weird! I would just ignore the capacity in that case.

@ryzngard
Copy link
Contributor Author

Ran an integration test just to be sure: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=9862176&view=results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants