-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Fix ServiceController name population perf #32072
Conversation
{ | ||
Interop.Advapi32.CloseServiceHandle(databaseHandle); | ||
Exception inner = new Win32Exception(); |
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.
There is a bunch of code between GetServiceDisplayName and this place. It may corrupt the last error. Last error should be captured right after the PInvoke returns. (It may look better to have the buffer marshaling here and not under Common.)
Marshal.FreeHGlobal(memory); | ||
if (databaseHandle != IntPtr.Zero) | ||
// We must have _name | ||
string result = Interop.Advapi32.GetServiceDisplayName(_serviceManagerHandle.DangerousGetHandle(), _name); |
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.
Is this really so performance critical that you have to use DangerousGetHandle here?
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.
No, was following existing pattern. I will SafeHandle all the things.
{ | ||
// Get the size of buffer required | ||
int bufLen = 0; | ||
bool success = GetServiceDisplayNamePrivate(SCMHandle, serviceName, null, ref bufLen); |
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 display name can change. Do we need to re-loop to handle races?
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.
It may look better with ValueStringBuilder
:
var builder = new ValueStringBuilder(4096);
int bufLen;
for (;;)
{
fixed (char * pBuffer = builder.GetPinnableReference())
{
bufLen = builder.Capacity;
if (GetServiceDisplayNamePrivate(SCMHandle, serviceName, pBuffer, ref int bufLen))
break;
}
int lastError = Marshal.GetLastWin32Error();
if (lastError != Interop.Errors.ERROR_INSUFFICIENT_BUFFER)
... throw exception ...
builder.EnsureCapacity(bufLen + 1); // Does not include null
}
builder.Length = bufLen;
return builder.ToString();
Notice that it is calling GetServiceDisplayName
just once in the typical case, so it should be more efficient as well.
@@ -38,7 +39,6 @@ public class ServiceController : Component | |||
private ServiceStartMode _startType; | |||
|
|||
private const int SERVICENAMEMAXLENGTH = 80; |
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.
This constant is unused as well. Delete?
@@ -449,12 +449,6 @@ private static bool CheckMachineName(string value) | |||
} | |||
|
|||
public void Close() |
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.
This change is going to change the behavior with user-overiden Dispose methods. Implementing Close
by calling Dispose
is fine - not sure why you have changed it.
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.
To match NETFX. It calls Close() to reset state, without setting disposed
, for example when names need to be regenerated. Once disposed
is set, the class can no longer be used. Do you still want me to change it? I suppose I could just rename it Reset().
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.
It violates design guidelines for Close method. From https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/dispose-pattern:
CONSIDER providing method Close(), in addition to the Dispose(), if close is standard terminology in the area. When doing so, it is important that you make the Close implementation identical to Dispose
I am sure what the right thing to do here is ... but it would be nice to add a comment about design guidelines violation at least if you keep it as it used to be in netfx.
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.
Commented.
private string _displayName; | ||
private string _name = ""; // Never null | ||
private string _eitherName = ""; // Never null | ||
private string _displayName = ""; // Never 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.
This has potential to create corner case bugs when the service name is empty string. Using null
to mean not-yet-computed looks better.
@@ -19,13 +19,14 @@ namespace System.ServiceProcess | |||
/// and manipulate it or get information about it. | |||
public class ServiceController : Component | |||
{ | |||
private string _machineName; | |||
private string _machineName = DefaultMachineName; // Never null, always a valid name |
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.
var n = new ServiceController().DisplayName;
would throw NullReferenceException before this change, but succeeds after this change. Do we need a test 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.
Actually it throws IOE now, but I'll add a test.
@@ -19,13 +19,14 @@ namespace System.ServiceProcess | |||
/// and manipulate it or get information about it. | |||
public class ServiceController : Component | |||
{ | |||
private string _machineName; | |||
private string _machineName = DefaultMachineName; // Never null, always a valid name |
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.
Since the _machineName
is set in all but one constructor - it may be better to just fix the one constructor that is not setting it to set it.
Updated. |
IntPtr structPtr; | ||
|
||
for (int index = 0; index < services.Length; ++index) | ||
using (var entriesPointer = new SafeAllocHHandle(Marshal.AllocHGlobal(checked((services.Length + 1) * sizeOfSERVICE_TABLE_ENTRY)))) |
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.
Seems like it might be nice to add SafeAllocHHandle.CreateNewAlloc(int size)
if (throwOnError) | ||
throw new Win32Exception(lastError); | ||
|
||
return null; // Caller may want to try something else |
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.
If this is expected to be a "common" path, this should dispose the buffer.
[DllImport(Libraries.Advapi32, EntryPoint = "GetServiceKeyNameW", CharSet = System.Runtime.InteropServices.CharSet.Unicode, SetLastError = true)] | ||
private static extern bool GetServiceKeyNamePrivate(SafeServiceHandle SCMHandle, string displayName, ref char KeyName, ref int KeyNameLength); | ||
|
||
public static unsafe string GetServiceKeyName(SafeServiceHandle SCMHandle, string displayName, bool throwOnError) |
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 think the throwOnError
is over-engineered since this is always used with the same value.
It is why we typically have the wrapper code like this in implementation, and not under Common.
IntPtr enumBuffer = Marshal.AllocHGlobal((IntPtr)bytesNeeded); | ||
|
||
try | ||
using (var enumBuffer = new SafeAllocHHandle(Marshal.AllocHGlobal((IntPtr)bytesNeeded))) |
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 pattern where both alloc and free is done within same function and the thing does not escape anywhere is fine without safe handles. Safe handles just add overhead, without really improving safety.
Safe handles are worth it for the extra safety when the handle is object state (stored in a field). Safe handles prevent situation where some other thread frees it while it is being used.
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.
Reversed, but if not a SafeHandle a disposable holder seems like it might have helped avoid the leak in Run()
in ServiceBase.
Updated. |
@dotnet-bot test UWP CoreCLR x64 Debug Build |
internal partial class Advapi32 | ||
{ | ||
[DllImport(Libraries.Advapi32, EntryPoint = "GetServiceDisplayNameW", CharSet = System.Runtime.InteropServices.CharSet.Unicode, SetLastError = true)] | ||
internal static extern bool GetServiceDisplayName(SafeServiceHandle SCMHandle, string serviceName, ref char displayName, ref int displayNameLength); |
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 think it is better to use char*
for string pointers in PInvoke signatures. ref char
obfuscates the intent. It is hard to see that it is actually pointer to string.
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.
Changed, but I was following the pattern used in Environment and Path. It avoids the fixed
statement?
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.
We are not consistent with 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.
cc @JeremyKuhne - we should probably be consistent.
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 think we had settled on not using ref char instead of char* to potentially avoid an extra frame in addition to clarity. In this case we're stuck with extra frames no matter what due to the safe handle.
On a side note: one weird gotcha we have in this space is that pinning an empty array behaves differently than pinning an empty span.
static unsafe void PinMe()
{
int[] emptyArray = new int[0];
Span<int> emptySpan = new Span<int>(emptyArray);
fixed (int* ea = emptyArray)
fixed (int* es = emptySpan)
fixed (int* esr = &MemoryMarshal.GetReference(emptySpan))
{
Console.WriteLine($"Array: 0x{(ulong)ea:X}, Span: 0x{(ulong)es:X}, Address of ref span: 0x{(ulong)esr:X}");
}
}
Yields something like:
Array: 0x0, Span: 0x0, Address of ref span: 0x27D97CD1588
If the source array actually had length, these would all return the same pointer. This actually generated regressions in System.Drawing as GDI+ cared about the pointer in APIs when we sent the pointer and a length of zero (user passed us no points and we passed no points along to GDI+).
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.
pinning an empty array behaves differently than pinning an empty span.
Your example shows that pinning empty array works same way as pinning empty span.
It is only when you use the low-level MemoryMarshal.GetReference
API that you will see the difference. We have a number of places in CoreFX that use MemoryMarshal.GetReference
to pin Span that were introduced before we have proper support for pinning Span. We should move away from using MemoryMarshal.GetReference
for pinning Span.
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.
Your example shows that pinning empty array works same way as pinning empty span.
Yes, I should have been clearer. &MemoryMarshal.GetReference
was the only "pinning" option prior to C# 7.3. I'll open a tracking issue to pull usages.
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.
@jkotas Incidentally, does using ref char
instead of char*
have the same impact with the marshaller as MemoryMarshal.GetReference
? I'll validate when I get some time.
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.
Should we make a breaking change to MemoryMarshal.GetReference
to change to the ideal behavior? I imagine not much code has been written to use it on spans yet.
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.
MemoryMarshal.GetReference
was designed to give you the Span pointer with zero overhead and without any normalization.
If we added normalization to it, it would make it impossible to write low-level high perf code that works on Span. I am pretty sure number of our micro-benchmarks would regress.
try | ||
{ | ||
using (var serviceHandle = GetServiceHandle(Interop.Advapi32.ServiceOptions.SERVICE_PAUSE_CONTINUE)) | ||
{ |
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.
Nit: Indentation
Updated. |
while (true) | ||
{ | ||
bufLen = builder.Capacity; | ||
fixed (char* c = &builder.GetPinnableReference()) |
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 think this can be just fixed (char* c = builder)
with the latest C# compiler.
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 had to add a parameterless overload to ValueStringBuilder to achieve this useful trick.
Thanks for all the reviews! |
</data> | ||
<data name="ServiceRemoved" xml:space="preserve"> | ||
<value>Service {0} was successfully removed from the system.</value> | ||
<value>Service '{0}'' was successfully removed from the system.</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.
Nit: extra '
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.
Good eye, thanks!
* Fix ServiceController name population perf * Split tests * Remove dead field * Remove new use of DangerousGetHandle * SafeHandle all the things! * VSB dotnet#1 * VSB dotnet#2 * Fix GLE * Initialize machineName in ctor * Test for empty name ex * Null names * Inadvertent edit * Unix build * Move interop into class * Reverse SafeHandle for HAllocGlobal * Fix tests * Disable test for NETFX * CR feedback * Pattern matching on VSB * Direct call * typo Signed-off-by: dotnet-bot <[email protected]>
* Fix ServiceController name population perf * Split tests * Remove dead field * Remove new use of DangerousGetHandle * SafeHandle all the things! * VSB #1 * VSB dotnet#2 * Fix GLE * Initialize machineName in ctor * Test for empty name ex * Null names * Inadvertent edit * Unix build * Move interop into class * Reverse SafeHandle for HAllocGlobal * Fix tests * Disable test for NETFX * CR feedback * Pattern matching on VSB * Direct call * typo Signed-off-by: dotnet-bot <[email protected]>
* Fix ServiceController name population perf * Split tests * Remove dead field * Remove new use of DangerousGetHandle * SafeHandle all the things! * VSB #1 * VSB #2 * Fix GLE * Initialize machineName in ctor * Test for empty name ex * Null names * Inadvertent edit * Unix build * Move interop into class * Reverse SafeHandle for HAllocGlobal * Fix tests * Disable test for NETFX * CR feedback * Pattern matching on VSB * Direct call * typo Signed-off-by: dotnet-bot <[email protected]>
* Fix ServiceController name population perf * Split tests * Remove dead field * Remove new use of DangerousGetHandle * SafeHandle all the things! * VSB #1 * VSB #2 * Fix GLE * Initialize machineName in ctor * Test for empty name ex * Null names * Inadvertent edit * Unix build * Move interop into class * Reverse SafeHandle for HAllocGlobal * Fix tests * Disable test for NETFX * CR feedback * Pattern matching on VSB * Direct call * typo Signed-off-by: dotnet-bot <[email protected]>
* Fix ServiceController name population perf * Split tests * Remove dead field * Remove new use of DangerousGetHandle * SafeHandle all the things! * VSB dotnet/corefx#1 * VSB dotnet/corefx#2 * Fix GLE * Initialize machineName in ctor * Test for empty name ex * Null names * Inadvertent edit * Unix build * Move interop into class * Reverse SafeHandle for HAllocGlobal * Fix tests * Disable test for NETFX * CR feedback * Pattern matching on VSB * Direct call * typo Commit migrated from dotnet/corefx@76c8587
Fix https://github.com/dotnet/corefx/issues/31877
I also noticed that .NET Framework was using raw IntPtr for the SCM handle but apparently that doesn't help it significantly: