-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Switch DirectoryControl to use AsnWriter, AsnDecoder #101512
Conversation
Replaced this with the managed AsnDecoder, removing PInvoke from a potential hot path.
Also removed the manual API calls to ldap_create_sort_control - this is now built in managed code. This then has knock-on effects to eliminate the SortKeyInterop classes.
Most of the Control tests were hardcoded to the output of BerConverter, which uses four-byte lengths in all cases. This behaviour is now different: the same output is returned across all platforms for .NET, and remains unchanged for .NET Framework. This should also close issue 34679.
Reduce number of copies required in TransformControls, and enable these copies to take advantage of newer intrinsics where available.
Windows domain controllers may return a distinguished name starting with OU=, rather than ou=.
Out of curiosity, any benchmarks for perf. numbers before/after switching to |
I've not got benchmarks right now, but will write some in the next few days. In advance of these, I expect there'll be a modest reduction in managed and unmanaged memory usage, and that execution time will reduce (while remaining within the margin of error for the network request itself.) |
Preallocating space for AsnWriter buffers to reduce memory usage. Correctly handling attribute names in SortControls.
Benchmarks are below. To summarize:
Performance header
AsqRequestControl.GetValue: -90% execution time, -35% Gen0 memory allocation
CrossDomainMoveControl.GetValue: -54% execution time, -62% Gen0 memory allocation
DirSyncRequestControl.GetValue: -89% execution time, -35% Gen0 memory allocation
ExtendedDNControl.GetValue: -89% execution time, -30% Gen0 memory allocation
PageResultRequestControl.GetValue: -90% execution time, -40% Gen0 memory allocation
QuotaControl.GetValue: -92% execution time, -28% Gen0 memory allocation
SearchOptionsControl.GetValue: -90% execution time, -30% Gen0 memory allocation
SecurityDescriptorFlagControl.GetValue: -88% execution time, -30% Gen0 memory allocation
SortRequestControl.GetValue: -79% execution time, +4% Gen0 memory allocation
DirectoryControl.TransformControls: -75% execution time, -76% Gen0 memory allocation
VerifyNameControl.GetValue: -87% execution time, -46% Gen0 memory allocation
VlvRequestControl.GetValue: -84% execution time, -69% Gen0 memory allocation
I've made a performance adjustment by specifying the initial size of AsnWriter, since this is trivial to calculate (or always static.) AsnWriter grows in 1KB increments, which is much larger than the size of a normal directory control and causes memory usage to balloon. One inefficiency which I couldn't eliminate is that when writing strings as ASN.1 octet strings, I want to manually select the encoding to use and encode directly into the AsnWriter buffer. This isn't possible, (probably to keep AsnWriter specification-compliant) so I have to reserve/allocate a byte array, encode into that and write that out as an octet string. An example of this behaviour is in Edit: the updated build has completed and the test failures are unrelated, so I'm now happy that the benchmarks are valid @PaulusParssinen |
...irectoryServices.Protocols/src/System/DirectoryServices/Protocols/common/DirectoryControl.cs
Outdated
Show resolved
Hide resolved
...irectoryServices.Protocols/src/System/DirectoryServices/Protocols/common/DirectoryControl.cs
Outdated
Show resolved
Hide resolved
* Handles the switch to AsnDecoder * One data correction in SortResponseControlTests - one field has a tag of [0] which BerConverter was ignoring
* AsqResponseControl now verifies that there's no trailing data within the control value's decoded BER sequence. * All response controls now verify that there's no trailing data after their encoded value in the control value.
Previously, a zero-length octet string interpreted via the "a" format string would have resulted in a null value in Windows 8.1, and an empty string in every other case. This now returns an empty string in all cases.
Following the merge of #107201, I've updated this PR with three commits; it's ready for review at leisure. Commits 1 & 3
Commit 2
|
...irectoryServices.Protocols/src/System/DirectoryServices/Protocols/common/DirectoryControl.cs
Outdated
Show resolved
Hide resolved
...irectoryServices.Protocols/src/System/DirectoryServices/Protocols/common/DirectoryControl.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@edwardneal I verified test coverage for the new code, and that looks good with the exception of the two cases I mentioned.
Reading a long attribute name would have failed due to an invalid expected ASN.1 tag. Correct, and added a test.
Added test to validate that passing an invalid UTF8 string as the target parameter of a VlvRequestControl will now throw an EncoderFallbackException.
No longer null coalescing _directoryControlValue; replaced with a Debug.Assert that it's not null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
internal static class AsnWriterExtensions | ||
{ | ||
public static void WriteLdapString(this AsnWriter writer, string value, Encoding stringEncoding, bool mandatory = true, Asn1Tag? tag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is a good name. There's no construct (that I see) called LdapString
, and not all strings in LDAP are sent as "A Utf8String, except using tag 04 instead of 0C".
WriteUtf8OctetString
, maybe?
The bool mandatory
has no peer on AsnWriter methods. I recommend removing it here (making it always behave as true
, and making the one "optional" caller bring that logic closer to home... so it looks like any other conditional write for an ASN OPTIONAL or DEFAULT value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one caller passes Encoding.Unicode. So either two functions, or "WriteStringAsOctetString" might be a better name for the current shape.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name LdapString
partially comes from RFC2251, as the backing type for AttributeDescription
. Do you still want the name to change?
It was primarily used for writing the sort controls, and the other control logic piggybacks on the same method by explicitly specifying the encoding. I'll see if two methods would be clearer for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried a couple of different methods to see what the semantics look like, and agree - WriteStringAsOctetString
it is. That's rolled up and done now.
[ThreadStatic] | ||
private static AsnWriter? t_writer; | ||
|
||
[MemberNotNull(nameof(t_writer))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how well MemberNotNull
behaves with ThreadStatic. No one should be touching t_writer
except this function, so why is the annotation needed/warranted at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it'd help bridge the gap between a nullable local variable and a non-nullable return value. I've removed it.
|
||
[MemberNotNull(nameof(t_writer))] | ||
internal static AsnWriter GetWriter() | ||
=> t_writer ??= new AsnWriter(AsnEncodingRules.BER); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correct behavior for every caller is to call Reset()
on the writer when they get it, because they don't know if they have one that was abandoned due to an exception.
Maybe GetWriter
should do that for them, instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I've shifted this around.
Had a few small things. Looked at all the commits since my last review. |
This change doesn't take effect on .NET Framework, so any test expecting an exception will fail.
Removed the unnecessary nullability annotations, and moved the Reset call into GetWriter.
Also adjusted method signature to better align to the rest of the AsnWriter API surface.
@edwardneal do you have any further action items or planned changes? If not, I'll merge. Thanks. |
Thanks - I've responded to bartonjs' code review in-line, so don't have any further code changes planned. An earlier comment asked for a breaking change doc to be created though, and with the work settled I'll do this today/tomorrow. |
Thanks @steveharter and @bartonjs for your reviews. The breaking change doc is dotnet/docs#43885. |
Relates to #97540.
This PR replaces all references to
BerConverter
in LDAP directory control generation/parsing to use AsnWriter and AsnDecoder. SortRequestControl didn't use BerConverter directly - it called the OpenLDAP and WLDAPldap_create_sort_control
APIs instead. This class was the only thing in S.DS.P which referenced theldap_create_sort_control
andSortKeyInterop
struct, so I deleted them both.SortRequestControl
The change to SortRequestControl's generation mechanism might also resolve #34679, since there shouldn't be any mechanism for the heap corruption to occur.
Most of the SortRequestControl's new ASN.1 encoding is pretty uncontroversial, but there was a bit of discussion in PR #65548 around the encoding of the sort key's attribute name, and this was marshalled (as part of SortKeyInterop) with different encodings between Windows and Linux. In the RFC, this is defined (indirectly) as an LdapString; this is described as ISO10646 characters, encoded as a UTF-8 string and represented as an OCTET STRING. I'm fairly sure that UTF8Encoding.GetBytes fulfils this, and running the associated test case against a real AD domain controller passes.
Test changes
There are also test changes, but these are largely to change the special-casing of expected byte values between OpenLDAP and WLDAP - .NET now generates these values in a consistent format (the OpenLDAP format) regardless of platform. The .NET Framework tests continued to use the version of S.DS.P from the GAC in my environment, so I've special-cased by the framework version rather than by the platform.
Misc. optimizations
There were a handful of byte-by-byte array copies, which I've switched over to using span-based copies in hopes that they'll benefit slightly from vectorisation. TransformControls and GetValue have a related change: where they used to reference properties returning byte arrays (which took defensive copies) they now reference the property values directly. These should both reduce GC traffic slightly.