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

Proposed refactor with ~90% performance improvement on Export-TargetResource (for affected resources) #5615

Open
niwamo opened this issue Jan 9, 2025 · 10 comments

Comments

@niwamo
Copy link
Contributor

niwamo commented Jan 9, 2025

Description of the issue

The interaction between Export-TargetResource and Get-TargetResource is highly inefficient for multiple-instance resources (e.g. AADUser or other resources where multiple instances are expected). In pseudo-code:

function Export-TargetResource
{
    Connect-ToWorkload
    SendTelemetry
    GetAllResources
    foreach ($instance in $instances)
    {
        Get-TargetResource -params
    }
}

function Get-TargetResource
{
    Connect-ToWorkload
    SendTelemetry
    FetchResource
    FormatResourceAndFetchAdditionalProperties 
    return
}

There are three significant performance impacts:

  1. Calling New-M365DSCConnection for every iteration of Get-TargetResource. While it avoids reconnecting unnecessarily, it still results in thousands of extraneous + identical API calls for Get-AcceptedDomain when it is invoked for the EXO workload (and I have verified this with several Fiddler traces). Even if that could be fixed, there's no need to call the function, and invoking it thousands of times unnecessarily should be avoided.
  2. Sending telemetry on every iteration of Get-TargetResource is enormously impactful to the performance of this module. Without any other changes, I determined that the same export took 8,925 seconds with telemetry enabled and 1,400 seconds without. In other words, telemetry - just telemetry - accounted for ~85% of my export's runtime. I would by no means suggest removing telemetry altogether, but it does seem highly redundant to log the invocation of Export-TargetResource and each invocation of Get-TargetResource from within the export. If there is a desire to track the number of Get-TargetResource calls or the average function time, I would suggest tracking these items within the export function and sending that data as a single telemetry call. There's just no good reason for anyone to keep telemetry enabled when it causes an export to run (literally) 8x slower; reducing the telemetry without reducing the useful information captured seems like a win for everyone.
  3. For many resources, the Get-TargetResource function actually re-fetches the same instance information from the same API as Export-TargetResource. This should clearly be avoided, and has been for many modules: Export-TargetResource declares $Script:exportedInstances and Get-TargetResources checks if this variable exists, then filters the array to find the right instance. This is better but can be further improved. Why not declare $Script:exportedInstance inside of the export's foreach loop and avoid the filtering inside of Get-TargetResource? For resources with large numbers of instances, this makes a meaningful impact.

Altogether, my code changes the relevant section of a typical Get-TargetResource from this:

New-M365DSCConnection -Workload 'MicrosoftGraph' `
        -InboundParameters $PSBoundParameters

Confirm-M365DSCDependencies

#region Telemetry
$ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', ''
$CommandName = $MyInvocation.MyCommand
$data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
    -CommandName $CommandName `
    -Parameters $PSBoundParameters
Add-M365DSCTelemetryEvent -Data $data
#endregion

$nullReturn = $PSBoundParameters
$nullReturn.Ensure = 'Absent'
try
{
    # fetch resource
    if ($null -eq $resource) { return $nullReturn }
    # format and enrich
    return
}
catch
{
    # log
}

to this:

try
{
    if (-not $Script:exportedInstance)
    {
        New-M365DSCConnection -Workload 'MicrosoftGraph' `
                -InboundParameters $PSBoundParameters
        
        Confirm-M365DSCDependencies
        
        #region Telemetry
        $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', ''
        $CommandName = $MyInvocation.MyCommand
        $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
            -CommandName $CommandName `
            -Parameters $PSBoundParameters
        Add-M365DSCTelemetryEvent -Data $data
        #endregion
        
        $nullReturn = $PSBoundParameters
        $nullReturn.Ensure = 'Absent'

        # fetch resource
        if ($null -eq $resource) { return $nullReturn }
    }
    else
    {
        $resource = $Script:exportedInstance
    }
    # format and enrich
    return
}
catch
{
    # log
}

I have made these changes locally for 61 resources. In my regression testing I found no "breakage" and actually found that this model fixed two bugs I was previously unaware of. (In both cases, the relevant API - strangely - returned more properties for bulk-retrieved items than individually retrieved items, so leveraging the exportedInstance resulted in keeping previously missing properties.

My performance test results with identical configurations (runtime, measured in seconds):

Telemetry Enabled Dev Branch without changes Dev Branch with recommended changes
True 8925 865
False 1400 650

Since this would be a large PR, I am hoping to get initial feedback from the maintainers before proceeding.
@NikCharlebois @ykuijs

Microsoft 365 DSC Version

DEV / 1.24.1218.1

Which workloads are affected

Azure Active Directory (Entra ID), Exchange Online, Security & Compliance Center, SharePoint Online, Office 365 Admin

The DSC configuration

Verbose logs showing the problem

Environment Information + PowerShell Version

@FabienTschanz
Copy link
Contributor

I love it. I've been thinking about improvements in the speed as well because I wasn't very satisfied with it in my environment too, and your changes look great to me. I'll take a look at the telemetry function and why it could take so long.

@FabienTschanz
Copy link
Contributor

The unnecessary calls to Get-AcceptedDomain will be removed with MSCloudLoginAssistant 1.1.34.

@FabienTschanz
Copy link
Contributor

@niwamo Quick question: What's the script you ran to measure the command execution time? Did you execute it in an elevated PowerShell session? I might have found something in the telemetry function which delays the export if you don't have admin privileges.

@niwamo
Copy link
Contributor Author

niwamo commented Jan 15, 2025

I measured execution time using the built-in 'Export took {x seconds}" message, as well as by using System.Diagnostics.Stopwatch, i.e.:

>> $sw = [System.Diagnostics.Stopwatch]::StartNew()
>> # export command
>> $sw.Elapsed.ToString()

The final output looked like:

Image

I believe I executed in a non-elevated session, though I would need to re-run tests to be 100% sure.

@ykuijs
Copy link
Member

ykuijs commented Jan 15, 2025

Sorry for the delay in response, have been a little busy this week. I love these improvements, really great work!

You are correct that some resources cache the objects in the Export method and then are passed as a script variable to the Get method. This is implemented in resource for which the export usually is very big and therefore time consuming, like AADUser, AADGroup, AADApplication, EXOMailbox, etc. Those resources can have lots and lots of objects and retrieving them individually doesn't make sense, so that is why we implemented this caching method.

Your changes make total sense, just one question: In some resources we still have to call individual cmdlet, like in AADUser for example we have to call Get-MgUserLicenseDetail for each user:
https://github.com/microsoft/Microsoft365DSC/blob/e362c1a5b84ec91d71a58c688bc3b6416a0184e2/Modules/Microsoft365DSC/DSCResources/MSFT_AADUser/MSFT_AADUser.psm1#L194C18-L194C40
If we do not initiate a new connection and this resource is the only resource you want to export (or it is the first exported resource in the list), the cmdlet most probably will fail. Or am I forgetting something?

@niwamo
Copy link
Contributor Author

niwamo commented Jan 15, 2025

If Get-TargetResource is being called from within the Export-TargetResource cmdlet, the export command will ensure we are connected to all necessary workloads before we ever call into the get command. In the resource you linked, this occurs on line 912. The connection is still active and available from within the get function.

If Get-TargetResource is called from outside of Export-TargetResource, then the connection is initiated/ensured from within the get command, just as it previously was.

The only possible issue is if an additional command is called from within Get-TargetResource that requires a module not connected inside of Export-TargetResource. However, this has not been the case for any resources I have updated so far.

@FabienTschanz
Copy link
Contributor

@niwamo If you executed it in a standard shell, there might be a performance hit by Get-DscLocalConfigurationManager which will throw an Access denied exception too. There will be a PR that improves the telemetry handling, although it will take some more time.

@FabienTschanz
Copy link
Contributor

@niwamo Found the cause of the telemetry engine being so slow. #5635 fixes it, then the execution speed even with telemetry enabled should be somewhere around the time without telemetry (minus a couple of percents because of the overhead of course).

@ykuijs
Copy link
Member

ykuijs commented Jan 17, 2025

PR #5629 fixes the following 61 resources:

  • AADAdministrativeUnit
  • AADApplication
  • AADAuthenticationMethodPolicy
  • AADAuthenticationStrengthPolicy
  • AADConditionalAcces- SPOlicy
  • AADGroup
  • AADNamedLocationPolicy
  • AADRoleDefinition
  • AADRoleSetting
  • AADServicePrincipal
  • AADSocialIdentityProvider
  • AADTokenLifetimePolicy
  • AADUser
  • EXOAddressList
  • EXODataClassification
  • EXODistributionGroup
  • EXOGlobalAddressList
  • EXOGroupSettings
  • EXOMailboxPermission
  • EXOManagementRole
  • EXOManagementRoleAssignment
  • EXOManagementRoleEntry
  • EXOMessageClassification
  • EXORoleAssignmentPolicy
  • EXORoleGroup
  • EXOSharedMailbox
  • EXOTran- SPOrtRule
  • O365Group
  • SCAuditConfigurationPolicy
  • SCAutoSensitivityLabelPolicy
  • SCAutoSensitivityLabelRule
  • SCCaseHoldPolicy
  • SCCaseHoldRule
  • SCComplianceCase
  • SCComplianceSearch
  • SCComplianceSearchAction
  • SCComplianceTag
  • SCDLPCompliancePolicy
  • SCDLPComplianceRule
  • SCDeviceConditionalAcces- SPOlicy
  • SCDeviceConditionalAccessRule
  • SCDeviceConfigurationPolicy
  • SCFilePlanPropertyAuthority
  • SCFilePlanPropertyCategory
  • SCFilePlanPropertyCitation
  • SCFilePlanPropertyDepartment
  • SCFilePlanPropertyReferenceId
  • SCFilePlanPropertySubCategory
  • SCLabelPolicy
  • SCProtectionAlert
  • SCRetentionCompliancePolicy
  • SCRetentionComplianceRule
  • SCRetentionEventType
  • SCRoleGroup
  • SCRoleGroupMember
  • SCSensitivityLabel
  • SCSupervisoryReviewPolicy
  • SCSupervisoryReviewRule
  • SPOHubSite
  • SPOSite
  • SPOSiteGroup

ykuijs added a commit that referenced this issue Jan 17, 2025
Export Performance Improvement - Addresses #5615
@ykuijs
Copy link
Member

ykuijs commented Jan 17, 2025

PR #5641 fixes the following 114 Intune resources:

  • IntuneASRRulesPolicyWindows10
  • IntuneAccountProtectionLocalAdministratorPasswordSolutionPolicy
  • IntuneAccountProtectionLocalUserGroupMembershipPolicy
  • IntuneAccountProtectionPolicy
  • IntuneAccountProtectionPolicyWindows10
  • IntuneAndroidManagedStoreAppConfiguration
  • IntuneAntivirusExclusionsPolicyLinux
  • IntuneAntivirusExclusionsPolicyMacOS
  • IntuneAntivirusPolicyLinux
  • IntuneAntivirusPolicyMacOS
  • IntuneAntivirusPolicyWindows10SettingCatalog
  • IntuneAppAndBrowserIsolationPolicyWindows10
  • IntuneAppAndBrowserIsolationPolicyWindows10ConfigMgr
  • IntuneAppCategory
  • IntuneAppConfigurationDevicePolicy
  • IntuneAppConfigurationPolicy
  • IntuneAppProtectionPolicyAndroid
  • IntuneAppProtectionPolicyiOS
  • IntuneAppleMDMPushNotificationCertificate
  • IntuneApplicationControlPolicyWindows10
  • IntuneAttackSurfaceReductionRulesPolicyWindows10ConfigManager
  • IntuneDerivedCredential
  • IntuneDeviceAndAppManagementAssignmentFilter
  • IntuneDeviceCategory
  • IntuneDeviceCleanupRule
  • IntuneDeviceCompliancePolicyAndroid
  • IntuneDeviceCompliancePolicyAndroidDeviceOwner
  • IntuneDeviceCompliancePolicyAndroidWorkProfile
  • IntuneDeviceCompliancePolicyMacOS
  • IntuneDeviceCompliancePolicyWindows10
  • IntuneDeviceCompliancePolicyiOs
  • IntuneDeviceConfigurationAdministrativeTemplatePolicyWindows10
  • IntuneDeviceConfigurationCustomPolicyWindows10
  • IntuneDeviceConfigurationDefenderForEndpointOnboardingPolicyWindows10
  • IntuneDeviceConfigurationDeliveryOptimizationPolicyWindows10
  • IntuneDeviceConfigurationDomainJoinPolicyWindows10
  • IntuneDeviceConfigurationEmailProfilePolicyWindows10
  • IntuneDeviceConfigurationEndpointProtectionPolicyWindows10
  • IntuneDeviceConfigurationFirmwareInterfacePolicyWindows10
  • IntuneDeviceConfigurationHealthMonitoringConfigurationPolicyWindows10
  • IntuneDeviceConfigurationIdentityProtectionPolicyWindows10
  • IntuneDeviceConfigurationImportedPfxCertificatePolicyWindows10
  • IntuneDeviceConfigurationKioskPolicyWindows10
  • IntuneDeviceConfigurationNetworkBoundaryPolicyWindows10
  • IntuneDeviceConfigurationPkcsCertificatePolicyWindows10
  • IntuneDeviceConfigurationPolicyAndroidDeviceAdministrator
  • IntuneDeviceConfigurationPolicyAndroidDeviceOwner
  • IntuneDeviceConfigurationPolicyAndroidOpenSourceProject
  • IntuneDeviceConfigurationPolicyAndroidWorkProfile
  • IntuneDeviceConfigurationPolicyMacOS
  • IntuneDeviceConfigurationPolicyWindows10
  • IntuneDeviceConfigurationPolicyiOS
  • IntuneDeviceConfigurationSCEPCertificatePolicyWindows10
  • IntuneDeviceConfigurationSecureAssessmentPolicyWindows10
  • IntuneDeviceConfigurationSharedMultiDevicePolicyWindows10
  • IntuneDeviceConfigurationTrustedCertificatePolicyWindows10
  • IntuneDeviceConfigurationVpnPolicyWindows10
  • IntuneDeviceConfigurationWindowsTeamPolicyWindows10
  • IntuneDeviceConfigurationWiredNetworkPolicyWindows10
  • IntuneDeviceControlPolicyWindows10
  • IntuneDeviceEnrollmentLimitRestriction
  • IntuneDeviceEnrollmentPlatformRestriction
  • IntuneDeviceEnrollmentStatusPageWindows10
  • IntuneDeviceManagementAndroidDeviceOwnerEnrollmentProfile
  • IntuneDeviceManagementComplianceSettings
  • IntuneDeviceManagementEnrollmentAndroidGooglePlay
  • IntuneDeviceRemediation
  • IntuneDiskEncryptionMacOS
  • IntuneDiskEncryptionPDEPolicyWindows10
  • IntuneDiskEncryptionWindows10
  • IntuneEndpointDetectionAndResponsePolicyLinux
  • IntuneEndpointDetectionAndResponsePolicyMacOS
  • IntuneEndpointDetectionAndResponsePolicyWindows10
  • IntuneExploitProtectionPolicyWindows10SettingCatalog
  • IntuneFirewallPolicyWindows10
  • IntuneFirewallRulesHyperVPolicyWindows10
  • IntuneFirewallRulesPolicyWindows10
  • IntuneFirewallRulesPolicyWindows10ConfigMgr
  • IntuneMobileAppsMacOSLobApp
  • IntuneMobileAppsWindowsOfficeSuiteApp
  • IntuneMobileThreatDefenseConnector
  • IntunePolicySets
  • IntuneRoleAssignment
  • IntuneRoleDefinition
  • IntuneRoleScopeTag
  • IntuneSecurityBaselineDefenderForEndpoint
  • IntuneSecurityBaselineMicrosoft365AppsForEnterprise
  • IntuneSecurityBaselineMicrosoftEdge
  • IntuneSecurityBaselineWindows10
  • IntuneSettingCatalogASRRulesPolicyWindows10
  • IntuneSettingCatalogCustomPolicyWindows10
  • IntuneTrustedRootCertificateAndroidDeviceOwner
  • IntuneTrustedRootCertificateAndroidEnterprise
  • IntuneTrustedRootCertificateAndroidWork
  • IntuneTrustedRootCertificateIOS
  • IntuneVPNConfigurationPolicyAndroidDeviceOwner
  • IntuneVPNConfigurationPolicyAndroidEnterprise
  • IntuneVPNConfigurationPolicyAndroidWork
  • IntuneVPNConfigurationPolicyIOS
  • IntuneWifiConfigurationPolicyAndroidDeviceAdministrator
  • IntuneWifiConfigurationPolicyAndroidEnterpriseDeviceOwner
  • IntuneWifiConfigurationPolicyAndroidEnterpriseWorkProfile
  • IntuneWifiConfigurationPolicyAndroidForWork
  • IntuneWifiConfigurationPolicyAndroidOpenSourceProject
  • IntuneWifiConfigurationPolicyIOS
  • IntuneWifiConfigurationPolicyMacOS
  • IntuneWifiConfigurationPolicyWindows10
  • IntuneWindowsAutopilotDeploymentProfileAzureADHybridJoined
  • IntuneWindowsAutopilotDeploymentProfileAzureADJoined
  • IntuneWindowsInformationProtectionPolicyWindows10MdmEnrolled
  • IntuneWindowsUpdateForBusinessDriverUpdateProfileWindows10
  • IntuneWindowsUpdateForBusinessFeatureUpdateProfileWindows10
  • IntuneWindowsUpdateForBusinessQualityUpdateProfileWindows10
  • IntuneWindowsUpdateForBusinessRingUpdateProfileWindows10

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

No branches or pull requests

3 participants