dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.66k forks source link

Principal.FindByIdentity() method is extremely slow #34598

Open iSazonov opened 4 years ago

iSazonov commented 4 years ago

In PowerShell repo we started porting Microsoft.PowerShell.LocalAccounts module to System.DirectoryServices.AccountManagement API (from p/invokes). We had to use workarounds because Principal.FindByIdentity() methods is extremely slow - up to 100x(!) vs Principal.PrincipalSearcher().

On my notebook Get-LocalUser name ported cmdlet takes 2.8 sec(!) vs 25 ms if a workaround is used.

ghost commented 4 years ago

Tagging @safern, @viktorhofer as an area owner

ericstj commented 4 years ago

@tquerec does this sound right? Perhaps the two different methods are considering a broader set of results than is expected.

simon-biber commented 3 years ago

UserPrincipal.FindByIdentity seems to be doing some sort of query on each network interface. Disabling the 'Client for Microsoft Networks' protocol on my VPN interface reduced the time for FindByIdentity from several seconds down to several milliseconds. I found this workaround here: https://stackoverflow.com/questions/7533790/findbyidentity-performance-differences

deeprobin commented 2 years ago

Has anyone profiled the problem yet? If it is due to slow network interface querying, perhaps we should create an external issue with Windows.

AloisKraus commented 2 years ago

I have found the same issue. Disabling Netios in TCP/IP helps to some extent, but the acutal issue is this call stack which tried to lookup for any passed user/group the domain controller via Netbios, regardless if the TCP Netbios settings are enabled or not.

System.DirectoryServices.AccountManagement.ni.dll!System.DirectoryServices.AccountManagement.SAMStoreCtx.FindNativeByNT4IdentRef(System.Type, System.String) |- System.DirectoryServices.ni.dll!System.DirectoryServices.PropertyCollection.get_Item(System.String) | System.DirectoryServices.ni.dll!System.DirectoryServices.PropertyValueCollection..ctor(System.DirectoryServices.DirectoryEntry, System.String) | System.DirectoryServices.ni.dll!System.DirectoryServices.PropertyValueCollection.PopulateList() | |- System.DirectoryServices.ni.dll!System.DirectoryServices.DirectoryEntry.get_AdsObject() | | System.DirectoryServices.ni.dll!System.DirectoryServices.DirectoryEntry.Bind() | | System.DirectoryServices.ni.dll!System.DirectoryServices.DirectoryEntry.Bind(Boolean) | | System.DirectoryServices.ni.dll!System.DirectoryServices.Interop.UnsafeNativeMethods.ADsOpenObject(System.String, System.String, System.String, Int3 | | System.DirectoryServices.ni.dll!DomainBoundILStubClass.IL_STUB_PInvoke(System.String, System.String, System.String, Int32, System.Guid ByRef, System | | activeds.dll!ADsOpenObject | | adsnt.dll!CWinNTNamespace::OpenDSObject | | adsnt.dll!GetObjectW | | adsnt.dll!GetGroupObject | | adsnt.dll!ValidateGroupObject | | |- adsnt.dll!WinNTGetCachedDCName | | | adsnt.dll!WinNTGetCachedObject | | | adsnt.dll!DsGetDcNameNTWrapper | | | logoncli.dll!DsGetDcNameW | | | logoncli.dll!DsGetDcNameWithAccountW | | | |- logoncli.dll!DsWsaGetDcName | | | | logoncli.dll!DsIGetDcName | | | | logoncli.dll!NetpDcGetName | | | | |- logoncli.dll!NetpDcGetNameNetbios

The NetDcGetNameNetbio waits for 40s until it times out. That happens even when you disable Netbios in TCP/IP settings.

Is there some way to disable this call somehow, when no NetBios server is available?

Later calls are cached so this impact is not always present, but during boot it will delay things a lot.

Update After disabling NetBios over TCP on ALL network interfaces the issue did go away.

pierophp commented 4 weeks ago

Is there a particular reason to .NET not having Principal.FindByIdentityAsync?

deeprobin commented 3 weeks ago

Is there a particular reason to .NET not having Principal.FindByIdentityAsync?

I think it's because the underlying Win32-API calls are not asynchronous (e.g. network calls with IO Completion Ports). See https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netgetdcname.

Well you could wrap the synchronous call in an async Task (e.g. Task.Run / Task.Factory.StartNew), but that's not "truly asynchronous".

AloisKraus commented 3 weeks ago

@pierophp:

Except for network communication I would not use truly async calls. In my experience async is just a synonym for making obvious delays harder to analyze for no gains. In the end you have threads waiting on your behalf without any context. If that pattern is so good why does the underlying OS use almost everywhere blocking synchronous calls? Things quickly fall apart of you mix async calls with parallel loops and hope for performance improvements. If you try to e.g. write from dozens of threads to a single disk to multiple files you will find out that a single thread will perform much better. There may be gains if you go multi threaded but you need to measure first how much async IO the underlying storage can efficiently handle.

simon-biber commented 3 weeks ago

FindByIdentity is network communication. It's an LDAP query to a directory server.

deeprobin commented 3 weeks ago

FindByIdentity is network communication. It's an LDAP query to a directory server.

That's right, but the underlying Win32-call is a synchronous call. I haven't found an async equivalent for this. So if you wan't it async, it's just blocking wrapped in a Task.