Get ActiveSync device statistics faster

Get-MobileDeviceStatistics is an “expensive” cmdlet, in that it requires Exchange to connect to the server where the mailbox is located and get information that is stored in the mailbox. And for whatever reason, it is more expensive than other cmdlets that do the same thing, like Get-MailboxFolderStatistics. If you have Exchange servers in multiple sites, running Get-MobileDeviceStatistics from a server in a different site than the target mailbox, it can take minutes to get data back.

A customer has about 25,000 mailboxes on servers in the US, Europe, and APAC. They wanted to find out which users’ mobile devices have synced in the last 30 days and information about them. Running something like Get-Mailbox -resultsize unlimited | %{Get-MobileDeviceStatistics -Mailbox $_.Identity} is not efficient in such an environment. You end up making expensive calls to remote servers for users that don’t even have a mobile device. The customer’s script would eventually just time out because of network issues with disconnected sessions.

I wrote a script that completes in far less time because it connects directly to a user’s mailbox server to execute the cmdlet. To be efficient processing tens of thousands of users, the script checks whether a user has any ActiveSync partnerships so it doesn’t waste time querying a mailbox that will return no data, keeps track of which server a mailbox database is mounted, establishes a connection to a server only as needed, and imports only the one cmdlet that will be executed (which completes much faster than importing every cmdlet the admin’s RBAC roles provide).

Getting mobile device statistics for this many users still takes several hours to complete, but it does complete. The script uses progress bars to indicate how far along it is in processing all mailboxes, as indicating when it is looking up the active server for a database and when connecting to a server (though the latter two only take a few seconds each).

#Requires -Version 3
$activeServer = @{}
function Get-DatabaseActiveServer ($db)
	{
	if (-not($activeServer[$db]))
		{
		Write-Progress -Activity "Getting active server for $db" -Status " " -Id 2
		$mounted = Get-MailboxDatabaseCopyStatus -Identity $db | Where-Object {$_.Status -eq 'Mounted'}
		$activeServer[$db] = $mounted.ActiveDatabaseCopy
		Write-Progress -Activity "Getting active server for $db" -Id 2 -Completed
		}
	$activeServer[$db]
	}

#Get list of mailboxes to query
Write-Host "Getting list of mailboxes..."
$mb = Get-Mailbox -RecipientTypeDetails UserMailbox -ResultSize unlimited

#Loop through mailboxes and get mobile devices
for ($i=0;$i -lt $mb.Count;$i++)
	{
	Write-Progress -Activity "Processing mailbox for $($mb[$i].DisplayName)" -PercentComplete ($i/$mb.Count*100) `
		-Id 1 -Status "Overall progress:"
	
	#Check if mailbox has any EAS partnerships; avoids needing to query mailbox that will return no data
	if ((Get-CasMailbox -Identity $mb[$i].Identity).HasActiveSyncDevicePartnership)
		{
		$dbServer = Get-DatabaseActiveServer -db $mb[$i].Database
		
		#Determine if connection to remote server already exists by checking for server-prefixed command
		if (-not(Get-Command -Name Get-$($dbServer)MobileDeviceStatistics -ErrorAction SilentlyContinue))
			{
			Write-Progress -Activity "Connecting to $dbServer" -Status " " -Id 2
			#Limit imported session to only the needed command; avoids local session maximum function count and is faster
			Import-PSSession -Session (New-PSSession -ConfigurationName Microsoft.Exchange `
				-ConnectionUri http://$dbServer/powershell -Name 'ExchangeMDStat') -Prefix $dbServer `
				-DisableNameChecking -CommandName Get-MobileDeviceStatistics | Out-Null
			Write-Progress -Activity "Connecting to $dbServer" -Id 2 -Completed
			}
		
		#Build remote command to run as a string because the cmdlet name includes a variable
		$command = "Get-$($dbServer)MobileDeviceStatistics -Mailbox `"$($mb[$i].Identity)`""
		Invoke-Expression -Command $command | Where-Object {$_.LastSuccessSync -gt (Get-Date).AddDays(-30)} |
			Select-Object Identity,DeviceType,DeviceUserAgent,LastSuccessSync | Export-Csv -NoTypeInformation -Append `
			-Path c:\temp\MobileDevices-$(Get-Date -Format yyyy-MM-dd).csv
		}
	}
#Cleanup
Get-PSSession -Name 'ExchangeMDStat' -ErrorAction SilentlyContinue | Remove-PSSession

For those that wonder why I didn’t use Invoke-Command and avoid wasting time importing the remote session, I originally wrote this in the customer’s VDI on a Windows 7 workstation with PowerShell 2, which wouldn’t let me execute the command directly in the remote session because of the Exchange WinRM configuration of restricted language mode when sending local variables to be used in a remote session. This only happened with Powershell 2. On a Windows 10 client with PowerShell 5.1, it works fine. I measured the run time of the script several times with a sample of users and compared it to doing the same with Invoke-Command, and the difference was negligible. However, if you prefer to do it that way, here is a version that uses it:

#Requires -Version 3
$activeServer = @{}
function Get-DatabaseActiveServer ($db)
	{
	if (-not($activeServer[$db]))
		{
		Write-Progress -Activity "Getting active server for $db" -Status " " -Id 2
		$mounted = Get-MailboxDatabaseCopyStatus -Identity $db | Where-Object {$_.Status -eq 'Mounted'}
		$activeServer[$db] = $mounted.ActiveDatabaseCopy
		Write-Progress -Activity "Getting active server for $db" -Id 2 -Completed
		}
	$activeServer[$db]
	}

#Get list of mailboxes to query
Write-Host "Getting list of mailboxes..."
$mb = Get-Mailbox -RecipientTypeDetails UserMailbox -ResultSize unlimited

#Loop through mailboxes and get mobile devices
for ($i=0;$i -lt $mb.Count;$i++)
	{
	Write-Progress -Activity "Processing mailbox for $($mb[$i].DisplayName)" -PercentComplete ($i/$mb.Count*100) `
		-Id 1 -Status "Overall progress:"
	
	#Check if mailbox has any EAS partnerships; avoids needing to query mailbox that will return no data
	if ((Get-CasMailbox -Identity $mb[$i].Identity).HasActiveSyncDevicePartnership)
		{
		$dbServer = Get-DatabaseActiveServer -db $mb[$i].Database
		
		#Determine if connection to remote server already exists
		$session = Get-PSSession -Name "MDStat-$dbServer" -ErrorAction SilentlyContinue
		if (-not($session))
			{
			Write-Progress -Activity "Connecting to $dbServer" -Status " " -Id 2
			$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$dbServer/powershell `
				-Name "MDStat-$dbServer"
			Write-Progress -Activity "Connecting to $dbServer" -Id 2 -Completed
			}
		
		#Put mailbox identity in variable because you can't use expressions with the using: scope modifier
		$mbIdentity = $mb[$i].Identity
		Invoke-Command -Session $session -Command {Get-MobileDeviceStatistics -Mailbox $using:mbIdentity} |
			Select-Object Identity,DeviceType,DeviceUserAgent,LastSuccessSync | Export-Csv -NoTypeInformation -Append `
			-Path c:\temp\MobileDevices-$(Get-Date -Format yyyy-MM-dd).csv
		}
	}
#Cleanup
Get-PSSession  | Where-Object {$_.Name -like "MDStat*"} | Remove-PSSession

Leave a Reply

Your email address will not be published. Required fields are marked *

*