Skip to content

Reduce asyncstatemachine cost in readLineAsyncInternal. - #62479

Closed
Trapov wants to merge 2 commits into
dotnet:mainfrom
Trapov:optimization/net7.0/stream-reader
Closed

Reduce asyncstatemachine cost in readLineAsyncInternal.#62479
Trapov wants to merge 2 commits into
dotnet:mainfrom
Trapov:optimization/net7.0/stream-reader

Conversation

@Trapov

Copy link
Copy Markdown

No description provided.

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.IO labels Dec 7, 2021
@ghost

ghost commented Dec 7, 2021

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

null

Author:Trapov
Assignees:-
Labels:

area-System.IO, community-contribution

Milestone:-

@Trapov

Copy link
Copy Markdown
Author

Before:

MethodMeanErrorStdDevMedianMinMaxGen 0Gen 1Allocated
ReadLine695.4 ms5.83 ms5.45 ms695.5 ms688.4 ms706.7 ms179000.0000-1 GB
ReadLineAsync1,138.0 ms9.84 ms8.73 ms1,136.1 ms1,125.5 ms1,156.8 ms294000.00001000.00002 GB

After:

MethodMeanErrorStdDevMedianMinMaxGen 0Gen 1Allocated
ReadLine685.6 ms9.11 ms8.08 ms684.4 ms675.9 ms703.8 ms179000.0000-1 GB
ReadLineAsync983.7 ms16.58 ms14.69 ms978.8 ms955.5 ms1,005.9 ms294000.00001000.00002 GB

@Trapov

Copy link
Copy Markdown
Author

#62061 <- referenced

@Trapov

Copy link
Copy Markdown
Author

Did I broke the CI for arm?

@deeprobindeeprobin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@deeprobin

Copy link
Copy Markdown
Contributor

Did I broke the CI for arm?

I think the CI just needs to be restarted, happens often.

/cc @adamsitnik

@Trapov
Trapov marked this pull request as ready for review December 7, 2021 19:36
@TrapovTrapov changed the title (DRAFT) Reduce asyncstatemachine cost in readLineAsyncInternal.Reduce asyncstatemachine cost in readLineAsyncInternal.Dec 7, 2021

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've left some minor style-related comments. Otherwise than that, the changes look good, I am really happy to see the improvements! 👍

I have few more ideas that you could try (in a separate PR):

  • using [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] to annotate ReadBufferAsync method (more context: #49903)
  • using vectorized IndexOfAny similarly to what @nietras did in #60463
  • optimizing for happy-path scenario where we know that we don't need to fetch any more data by eliminating awaits

@Trapov big thanks for your contribution!

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs Outdated
@adamsitnikadamsitnik added this to the 7.0.0 milestone Dec 8, 2021
i++;
} while (i < tmpCharLen);
i = tmpCharLen - tmpCharPos;
sb ??= new StringBuilder(i + 80);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@adamsitnik I was wondering whether it would be possible to do pooling of the StringBuilder instance, to avoid this allocation in long line cases?

@adamsitnikadamsitnikDec 8, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@nietras it's definitely a good idea! Perhaps we could even replace the private char array ( _charBuffer) with a StringBuilder, write the chars directly to it and avoid the need of appending? The only thing I am not sure of is what side effects using StringBuilder.ToString(int startIndex, int length) might have

@Trapov

Copy link
Copy Markdown
Author

I've left some minor style-related comments. Otherwise than that, the changes look good, I am really happy to see the improvements! 👍

I have few more ideas that you could try (in a separate PR):

@Trapov big thanks for your contribution!

Will open the second PR with those suggestions in mind. Feel free to merge this one, I guess?

@nietras

nietras commented Dec 8, 2021

Copy link
Copy Markdown
Contributor

@Trapov note that we have a common micro-benchmark for this now with dotnet/performance#2170

FYI: You can compare your changes with main by calling something like:

C:\git\oss\performance\src\benchmarks\micro > dotnet run -c Release -f net6.0 --filter *StreamReaderReadLine* --coreRun "C:\git\oss\runtime-m\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe" "C:\git\oss\runtime-pr\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe"

@Trapov

Copy link
Copy Markdown
Author

Before:

MethodLineLengthRangeMeanErrorStdDevMedianMinMaxGen 0Gen 1Allocated
ReadLine[ 0, 0]100.46 us0.571 us0.476 us100.55 us99.56 us101.02 us0.3981-3 KB
ReadLineAsync[ 0, 0]533.46 us6.246 us4.877 us531.86 us527.88 us546.85 us187.50002.23211,155 KB
ReadLine[ 0, 1024]24.01 us0.154 us0.144 us23.99 us23.84 us24.24 us10.11450.095462 KB
ReadLineAsync[ 0, 1024]30.02 us0.388 us0.363 us30.10 us29.25 us30.44 us10.47690.120465 KB
ReadLine[ 1, 1]112.55 us0.670 us0.594 us112.40 us111.75 us114.04 us31.7029-195 KB
ReadLineAsync[ 1, 1]331.33 us6.225 us5.823 us331.43 us322.56 us344.43 us125.00001.2755771 KB
ReadLine[ 1, 8]83.02 us1.070 us1.001 us82.83 us81.65 us84.88 us18.1878-112 KB
ReadLineAsync[ 1, 8]167.10 us1.645 us1.539 us166.82 us164.92 us170.07 us55.70650.6793344 KB
ReadLine[ 9, 32]37.91 us0.228 us0.191 us37.93 us37.66 us38.37 us9.0799-56 KB
ReadLineAsync[ 9, 32]58.02 us0.517 us0.404 us58.13 us57.36 us58.65 us18.0000-110 KB
ReadLine[ 33, 128]25.94 us0.124 us0.103 us25.96 us25.76 us26.08 us7.2434-45 KB
ReadLineAsync[ 33, 128]35.75 us0.556 us0.520 us35.73 us34.96 us36.76 us9.6047-59 KB
ReadLine[ 129, 1024]23.14 us0.129 us0.114 us23.13 us22.96 us23.39 us10.33900.093163 KB
ReadLineAsync[ 129, 1024]29.28 us0.262 us0.232 us29.27 us28.92 us29.70 us10.57160.116265 KB
ReadLine[1025, 2048]28.67 us2.759 us3.177 us26.73 us25.87 us35.63 us14.73480.245691 KB
ReadLineAsync[1025, 2048]32.10 us0.555 us0.492 us31.96 us31.51 us33.18 us14.89310.381992 KB

After:

MethodLineLengthRangeMeanErrorStdDevMedianMinMaxGen 0Gen 1Allocated
ReadLine[ 0, 0]102.96 us1.509 us1.260 us102.80 us100.77 us104.61 us0.4058-3 KB
ReadLineAsync[ 0, 0]525.29 us3.205 us2.841 us525.50 us520.59 us530.45 us187.50002.08331,156 KB
ReadLine[ 0, 1024]23.41 us0.116 us0.108 us23.43 us23.23 us23.60 us10.08970.093462 KB
ReadLineAsync[ 0, 1024]20.35 us0.123 us0.109 us20.32 us20.12 us20.56 us10.51100.161765 KB
ReadLine[ 1, 1]113.25 us1.522 us1.424 us113.25 us111.54 us116.38 us31.4748-195 KB
ReadLineAsync[ 1, 1]360.37 us4.846 us4.047 us358.64 us355.89 us370.21 us125.00001.4205772 KB
ReadLine[ 1, 8]84.47 us0.855 us0.800 us84.36 us83.23 us85.80 us18.0556-112 KB
ReadLineAsync[ 1, 8]172.03 us2.201 us1.951 us171.35 us169.75 us176.19 us55.63190.6868344 KB
ReadLine[ 9, 32]38.08 us0.267 us0.250 us37.94 us37.66 us38.46 us9.0799-56 KB
ReadLineAsync[ 9, 32]56.39 us0.582 us0.486 us56.42 us55.31 us57.40 us17.82570.2201110 KB
ReadLine[ 33, 128]26.08 us0.521 us0.487 us25.94 us25.46 us27.11 us7.2627-45 KB
ReadLineAsync[ 33, 128]27.65 us0.213 us0.178 us27.71 us27.37 us27.87 us9.63220.109559 KB
ReadLine[ 129, 1024]23.15 us0.172 us0.161 us23.10 us22.92 us23.45 us10.29410.091963 KB
ReadLineAsync[ 129, 1024]20.28 us0.193 us0.171 us20.25 us20.07 us20.58 us10.59320.163065 KB
ReadLine[1025, 2048]24.81 us0.253 us0.224 us24.73 us24.51 us25.32 us14.76380.295391 KB
ReadLineAsync[1025, 2048]20.52 us0.131 us0.109 us20.52 us20.34 us20.67 us14.94020.415092 KB

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! :shipit:

else
// Note the following common line feed chars:
// \n - UNIX \r\n - DOS \r - Mac
if (ch is '\r' or '\n')

@stephentoubstephentoubDec 8, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This produces identical IL and asm, doesn't it? I would not expect a perf improvement from this. What will be different and might yield a micro-improvement is ch == '\r' | ch == '\n', removing a (likely-well-predicted) branch.

return null;

return await ConsumeBufferedData().ConfigureAwait(false);
}

@stephentoubstephentoubDec 8, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't this make the NoDataInTheBuffer case more expensive? Does the perf test being run stress this case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It does but it might not be the most used one case, at least that's what I hope for. The benchmarks -> dotnet/performance@529f33c#diff-64fc887dbc1598f2ea871592946f4077593925f80b22cab6f586ea7e1e1b0723


return await ConsumeBufferedData().ConfigureAwait(false);
}
async Task<string?> ConsumeBufferedData()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a theory you can share for why this improves perf? There are three cases here:

  • There's no data in the buffer and we're at EOF.
  • There's no data in the buffer but there's remaining data.
  • There's data in the buffer.

In all three cases, previously you would be calling a single async method. In all three cases, now you're still calling at least one async method, and in the second case, it's now calling a second and doing more work than before. The third case is just adding an extra layer of indirection. And the first case should only occur once per file.

@TrapovTrapovDec 8, 2021

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I thought that I could cut the state-machine in half (and eleminate awaits) and switch in two paths without any state-machines where in one part the third case is making less work and in another part the first/second ones (NoDataInBuffer) are doing more work (but they're not the most used ones probably), at least that was my reasoning behind it. But thinking about it again and looking at the perf-results not every case is a winner. Specificaly short lines are the worst.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Gen 1Allocated
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 0, 0]535.71 us3.930 us3.281 us534.51 us529.92 us541.75 us1.000.00187.50002.15521,155 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 0, 0]512.69 us4.709 us4.174 us512.40 us504.80 us519.53 us0.960.01187.50002.01611,156 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 0, 1024]29.21 us0.369 us0.345 us29.12 us28.65 us29.84 us1.000.0010.43600.116065 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 0, 1024]19.96 us0.153 us0.143 us19.95 us19.70 us20.18 us0.680.0110.48280.158865 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 1, 1]320.86 us1.957 us1.634 us321.01 us317.07 us323.00 us1.000.00125.00001.3021771 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 1, 1]322.20 us1.850 us1.640 us322.16 us319.25 us325.77 us1.000.01125.00001.2755772 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 1, 8]169.12 us2.478 us2.069 us169.55 us164.25 us172.48 us1.000.0055.77960.6720344 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 1, 8]172.79 us2.950 us2.760 us172.04 us168.83 us177.28 us1.020.0255.70650.6793344 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 9, 32]58.74 us0.469 us0.391 us58.80 us58.05 us59.53 us1.000.0017.92280.2298110 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 9, 32]56.41 us0.530 us0.470 us56.53 us55.72 us57.13 us0.960.0118.02010.2281110 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 33, 128]35.15 us0.317 us0.281 us35.13 us34.73 us35.60 us1.000.009.5291-59 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 33, 128]27.54 us0.216 us0.192 us27.53 us27.33 us27.93 us0.780.019.68310.110059 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 129, 1024]29.31 us0.279 us0.233 us29.27 us28.95 us29.81 us1.000.0010.59320.117765 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[ 129, 1024]20.26 us0.105 us0.088 us20.29 us20.05 us20.34 us0.690.0110.6041-65 KB
ReadLineAsyncJob-VRCDUV/testhost-main/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[1025, 2048]31.22 us0.457 us0.405 us31.31 us30.56 us31.99 us1.000.0014.94570.370692 KB
ReadLineAsyncJob-UBMMAT/testhost/net7.0-OSX-Release-x64/shared/Microsoft.NETCore.App/7.0.0/corerun[1025, 2048]20.79 us0.195 us0.182 us20.75 us20.50 us21.13 us0.670.0114.94020.415092 KB

@stephentoubdotnet/performance@529f33c#diff-64fc887dbc1598f2ea871592946f4077593925f80b22cab6f586ea7e1e1b0723

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Commented out of benchmarks the sync method for cleaner results between main and this branch

@nietras

Copy link
Copy Markdown
Contributor

@Trapov FYI you can filter benchmarks by adding method name to command line .e.g --filter *StreamReaderReadLine.ReadLineAsync*:

 dotnet run -c Release -f net6.0 --filter *StreamReaderReadLine.ReadLineAsync* --coreRun "C:\git\oss\runtime-m\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe" "C:\git\oss\runtime-pr\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe"

by using this with m and pr you also get ratio column in output that makes it easy to compare relative perf differences.

@Trapov

Copy link
Copy Markdown
Author

@Trapov FYI you can filter benchmarks by adding method name to command line .e.g --filter *StreamReaderReadLine.ReadLineAsync*:

 dotnet run -c Release -f net6.0 --filter *StreamReaderReadLine.ReadLineAsync* --coreRun "C:\git\oss\runtime-m\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe" "C:\git\oss\runtime-pr\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe"

by using this with m and pr you also get ratio column in output that makes it easy to compare relative perf differences.

#62479 (comment)

@stephentoub

stephentoub commented Dec 8, 2021

Copy link
Copy Markdown
Member

Thanks for your efforts here, @Trapov.

I spent some time today looking into this, as I wouldn't have expected this change to make as meaningful a difference as the numbers show: in either case you're still invoking an async method, and the await being removed from one of those async methods isn't even changing the size of that state machine (it's removing an await using an awaiter type that's also used elsewhere in the method), and on top of that the benchmark is using a stream type whose read methods always complete synchronously.

That said, I do see the same results on my machine when I pull down your PR. What's interesting, though, is why the improvement happens. Fundamentally, it looks to be due to the code the C# compiler happens to generate for the exact circumstance in play here. You can see this if you update your commit to just add back the _charPos == _charLen && (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false)) == 0 check at the beginning of the ConsumeBufferedData method. Even though, due to the guard in ReadLineAsyncInternal, that ReadBufferAsync call will never be hit because _charPos will never be equal to _charLen when we get here, just the presence of that additional await causes a bunch of changes in how the C# compiler is generating the code for this method, e.g. whether it uses a switch or cascading ifs for the jump table, and, at least for me, the previously observed performance difference evaporates. I'm very hestitant to take a change that complicates the code for a win that's likely to be transient.

There are also other improvements possible in ReadLineAsync that will make a much larger impact, like the ones @adamsitnik mentioned earlier. For example, here's a quick hackjob that a) uses IndexOfAny and b) caches a StringBuilder onto StreamReader:
stephentoub@0daab80
With that, I get numbers like the following: here main is what's currently in main, pr is with my cited commit, and pr2 is that commit as well as the change from this PR:

MethodToolchainLineLengthRangeMeanErrorStdDevRatioAllocated
ReadLineAsync\main\corerun.exe[ 0, 0]227.284 us4.4884 us4.6093 us1.00579 KB
ReadLineAsync\pr2\corerun.exe[ 0, 0]261.052 us4.3191 us3.8288 us1.15580 KB
ReadLineAsync\pr\corerun.exe[ 0, 0]266.604 us4.9058 us4.5889 us1.18579 KB
ReadLineAsync\main\corerun.exe[ 1, 1]194.254 us0.9095 us0.8062 us1.00517 KB
ReadLineAsync\pr2\corerun.exe[ 1, 1]214.027 us3.4172 us3.1964 us1.10516 KB
ReadLineAsync\pr\corerun.exe[ 1, 1]217.440 us0.7555 us0.6308 us1.12516 KB
ReadLineAsync\main\corerun.exe[ 0, 1024]23.460 us0.1394 us0.1235 us1.0064 KB
ReadLineAsync\pr2\corerun.exe[ 0, 1024]7.523 us0.0327 us0.0290 us0.3248 KB
ReadLineAsync\pr\corerun.exe[ 0, 1024]7.342 us0.0382 us0.0319 us0.3148 KB
ReadLineAsync\main\corerun.exe[ 1, 8]126.456 us0.3767 us0.3146 us1.00289 KB
ReadLineAsync\pr2\corerun.exe[ 1, 8]125.753 us0.2666 us0.2082 us0.99286 KB
ReadLineAsync\pr\corerun.exe[ 1, 8]128.383 us0.5712 us0.4459 us1.02286 KB
ReadLineAsync\main\corerun.exe[ 9, 32]49.919 us0.6183 us0.5784 us1.00106 KB
ReadLineAsync\pr2\corerun.exe[ 9, 32]38.612 us0.0569 us0.0504 us0.77102 KB
ReadLineAsync\pr\corerun.exe[ 9, 32]38.819 us0.5596 us0.4960 us0.78102 KB
ReadLineAsync\main\corerun.exe[ 33, 128]29.226 us0.2058 us0.1825 us1.0059 KB
ReadLineAsync\pr2\corerun.exe[ 33, 128]15.316 us0.0662 us0.0552 us0.5255 KB
ReadLineAsync\pr\corerun.exe[ 33, 128]15.215 us0.0933 us0.0827 us0.5255 KB
ReadLineAsync\main\corerun.exe[ 129, 1024]23.351 us0.0945 us0.0789 us1.0063 KB
ReadLineAsync\pr2\corerun.exe[ 129, 1024]7.196 us0.0620 us0.0484 us0.3146 KB
ReadLineAsync\pr\corerun.exe[ 129, 1024]7.355 us0.0455 us0.0380 us0.3146 KB
ReadLineAsync\main\corerun.exe[1025, 2048]24.515 us0.0724 us0.0642 us1.0091 KB
ReadLineAsync\pr2\corerun.exe[1025, 2048]6.944 us0.0212 us0.0177 us0.2847 KB
ReadLineAsync\pr\corerun.exe[1025, 2048]6.816 us0.0580 us0.0514 us0.2847 KB

This shows much larger wins from main to that commit, and then the improvements seen in this PR largely disappear, I believe because enough was perturbed in the C# codegen to make those transient wins go away.

I'd like to suggest this PR be closed and we instead focus on making some of those other improvements.

@Trapov

Copy link
Copy Markdown
Author

Closing this one because of how strongly the changes depend on the compiler and open a new one in a few moments.

@TrapovTrapov closed this Dec 8, 2021
@Trapov
Trapov deleted the optimization/net7.0/stream-reader branch December 8, 2021 21:40
@Trapov

Copy link
Copy Markdown
Author

#62552

@ghostghost locked as resolved and limited conversation to collaborators Jan 8, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@Trapov@deeprobin@nietras@stephentoub@adamsitnik