PowerShell monitor memory leak

Hello!

I have built two custom PowerShell monitors to replace IIS 8 Web Site and IIS 8 App Pool Availability monitors. The reason is that the original ones do not have suppression option, for example I cannot tell it not to alert if Pool or Site is down for a minute or so. We get a lot of IIS restarts and hence many unwanted alerts.

The monitors work well and I even implemented cookdown which reduced resource usage significantly. However, MonitoringHost Private Bytes are through the roof restarting the agent roughly every hour.
I have pinpointed the single command that causes this issue:

  • Get-WebSiteState - for IIS 8 Web Site Availability Monitor

  • Get-WebAppPoolState - for IIS 8 App Pool Availability Monitor

I have used Kevin Holman’s Fragments to build this management pack, and here is one of the two monitors, which are pretty much similar:

<ManagementPackFragment SchemaVersion="2.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<!--
%%
Description:
	This monitor supports cookdown.
	PowerShell script runs so that it returns multiple propery bags - for each IIS Application Pool
	Each property bag containg App Pool name (PoolID) and status
	Later on, in Condition Detection we filter each of these property bags and "connect" it to an SCOM IIS App Pool object

  A MONITOR which runs a timed PowerShell script as the DataSource and outputs a propertybag as GOOD or BAD to drive Monitor state and ALERT
  This is a simple script monitor where the script is not passed in any external parameters
  CompanyID - is a short abbreviation for your company with NO SPACES OR SPECIAL CHARACTERS ALLOWED
  AppName - is a short name for your app with NO SPACES OR SPECIAL CHARACTERS ALLOWED  
  ClassID - is the targeted class such as your custom class or Windows!Microsoft.Windows.Server.OperatingSystem
  UniqueID - Is a unique short description of the monitor purpose (NO SPACES OR SPECIAL CHARACTERS ALLOWED) such as "MonitorFilesInFolder"  
  
Version: 1.4
LastModified: 25-May-2019
%%

In this fragment you need to replace:
  CORP
  IIS8
  MWI2!Microsoft.Windows.InternetInformationServices.6.2.ApplicationPool
  IIS8ApplicationPoolAvailabilityCustom  
  
This fragment depends on references:
  RequiredReference: Alias="System", ID="System.Library"
  RequiredReference: Alias="Windows", ID="Microsoft.Windows.Library"
  RequiredReference: Alias="Health", ID="System.Health.Library"	
  
@@Author=Kevin Holman@@  
-->
	<TypeDefinitions>
		<ModuleTypes>
			<DataSourceModuleType ID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.DataSource" Accessibility="Internal" Batching="false">
				<Configuration>
					<xsd:element minOccurs="1" type="xsd:integer" name="IntervalSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
					<xsd:element minOccurs="1" type="xsd:integer" name="TimeoutSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
				</Configuration>
				<OverrideableParameters>
					<OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int" />
					<OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int" />
				</OverrideableParameters>
				<ModuleImplementation Isolation="Any">
					<Composite>
						<MemberModules>
							<DataSource ID="Scheduler" TypeID="System!System.Scheduler">
								<Scheduler>
									<SimpleReccuringSchedule>
										<Interval Unit="Seconds">$Config/IntervalSeconds$</Interval>
									</SimpleReccuringSchedule>
									<ExcludeDates />
								</Scheduler>
							</DataSource>
							<ProbeAction ID="PA" TypeID="Windows!Microsoft.Windows.PowerShellPropertyBagTriggerOnlyProbe">
								<ScriptName>CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.DataSource.ps1</ScriptName>
								<ScriptBody>
									#=================================================================================
									#  Describe Script Here
									#
									#  Author:
									#  v1.0
									#=================================================================================


									# Constants section - modify stuff here:
									#=================================================================================
									# Assign script name variable for use in event logging.
									$ScriptName = "CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.DataSource.ps1"
									$EventID = "1234"
									#=================================================================================


									# Starting Script section - All scripts get this
									#=================================================================================
									# Gather the start time of the script
									$StartTime = Get-Date
									#Set variable to be used in logging events
									$whoami = whoami
									# Load MOMScript API
									$momapi = New-Object -comObject MOM.ScriptAPI
									# Load PropertyBag function
									# $bag = $momapi.CreatePropertyBag()  WE ARE CREATING PROPERTY BAG FOR EACH OBJECT BELOW
									#Log script event that we are starting task
									$momapi.LogScriptEvent($ScriptName,$EventID,0,"`n Script is starting. `n Running as ($whoami).")
									#=================================================================================


									# Begin MAIN script section
									#=================================================================================

									#$strCondition = Get-WebAppPoolState

									foreach ($strCond in $strCondition) {

									$bag = $momapi.CreatePropertyBag()
									$PoolID = $strCond.ItemXPath.split("'")[1]
									$AppPoolStatus = $strCond.Value
									$bag.AddValue('PoolID',$PoolID)

									#Check the value of $strCond
									IF ($AppPoolStatus -eq 'Started') {
									$momapi.LogScriptEvent($ScriptName,$EventID,0,'Good Condition Found')
									$bag.AddValue('Result','Running')
									} else {
									$momapi.LogScriptEvent($ScriptName,$EventID,0,'Bad Condition Found')
									$bag.AddValue('Result','NotRunning')
									}

									$bag
									}

									Remove-Module WebAdministration
									Remove-Variable strCondition
									Remove-Variable bag

									# End of script section
									#=================================================================================
									#Log an event for script ending and total execution time.
									$EndTime = Get-Date
									$ScriptTime = ($EndTime - $StartTime).TotalSeconds
									$momapi.LogScriptEvent($ScriptName,$EventID,0,"`n Script Completed. `n Script Runtime: ($ScriptTime) seconds.")
									#=================================================================================
									# End of script
								</ScriptBody>
								<TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds>
							</ProbeAction>
						</MemberModules>
						<Composition>
							<Node ID="PA">
								<Node ID="Scheduler" />
							</Node>
						</Composition>
					</Composite>
				</ModuleImplementation>
				<OutputType>System!System.PropertyBagData</OutputType>
			</DataSourceModuleType>
		</ModuleTypes>
		<MonitorTypes>
			<UnitMonitorType ID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.MonitorType" Accessibility="Internal">
				<MonitorTypeStates>
					<MonitorTypeState ID="Running" NoDetection="false" />
					<MonitorTypeState ID="NotRunning" NoDetection="false" />
				</MonitorTypeStates>
				<Configuration>
					<xsd:element minOccurs="1" type="xsd:integer" name="IntervalSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
					<xsd:element minOccurs="1" type="xsd:integer" name="TimeoutSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
					<xsd:element minOccurs="1" type="xsd:integer" name="MatchCount" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
					<xsd:element minOccurs="1" type="xsd:string" name="PoolID" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
				</Configuration>
				<OverrideableParameters>
					<OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int" />
					<OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int" />
					<OverrideableParameter ID="MatchCount" Selector="$Config/MatchCount$" ParameterType="int" />
				</OverrideableParameters>
				<MonitorImplementation>
					<MemberModules>
						<DataSource ID="DS" TypeID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.DataSource">
							<IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
							<TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds>
						</DataSource>
						<!-- added this condition detection for cookdown BEGIN -->
						<ConditionDetection ID="CookDownFilter" TypeID="System!System.ExpressionFilter">
							<Expression>
								<SimpleExpression>
									<ValueExpression>
										<XPathQuery Type="String">Property[@Name="PoolID"]</XPathQuery>
									</ValueExpression>
									<Operator>Equal</Operator>
									<ValueExpression>
										<Value Type="String">$Config/PoolID$</Value>
									</ValueExpression>
								</SimpleExpression>
							</Expression>
						</ConditionDetection>
						<!-- added this condition detection for cookdown END -->
						<ConditionDetection ID="RunningFilter" TypeID="System!System.ExpressionFilter">
							<Expression>
								<SimpleExpression>
									<ValueExpression>
										<XPathQuery Type="String">Property[@Name="Result"]</XPathQuery>
									</ValueExpression>
									<Operator>Equal</Operator>
									<ValueExpression>
										<Value Type="String">Running</Value>
									</ValueExpression>
								</SimpleExpression>
							</Expression>
						</ConditionDetection>
						<ConditionDetection ID="NotRunningFilter" TypeID="System!System.ExpressionFilter">
							<Expression>
								<SimpleExpression>
									<ValueExpression>
										<XPathQuery Type="String">Property[@Name="Result"]</XPathQuery>
									</ValueExpression>
									<Operator>Equal</Operator>
									<ValueExpression>
										<Value Type="String">NotRunning</Value>
									</ValueExpression>
								</SimpleExpression>
							</Expression>
							<SuppressionSettings>
								<MatchCount>$Config/MatchCount$</MatchCount>
							</SuppressionSettings>
						</ConditionDetection>
					</MemberModules>
					<RegularDetections>
						<RegularDetection MonitorTypeStateID="Running">
							<Node ID="RunningFilter">
								<Node ID="CookDownFilter">
									<Node ID="DS" />
								</Node>
							</Node>
						</RegularDetection>
						<RegularDetection MonitorTypeStateID="NotRunning">
							<Node ID="NotRunningFilter">
								<Node ID="CookDownFilter">
									<Node ID="DS" />
								</Node>
							</Node>
						</RegularDetection>
					</RegularDetections>
					<OnDemandDetections>
						<OnDemandDetection MonitorTypeStateID="Running">
							<Node ID="RunningFilter">
								<Node ID="CookDownFilter">
									<Node ID="DS" />
								</Node>
							</Node>
						</OnDemandDetection>
						<OnDemandDetection MonitorTypeStateID="NotRunning">
							<Node ID="NotRunningFilter">
								<Node ID="CookDownFilter">
									<Node ID="DS" />
								</Node>
							</Node>
						</OnDemandDetection>
					</OnDemandDetections>
				</MonitorImplementation>
			</UnitMonitorType>
		</MonitorTypes>
	</TypeDefinitions>
	<Monitoring>
		<Monitors>
			<UnitMonitor ID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor" Accessibility="Public" Enabled="true" Target="MWI2!Microsoft.Windows.InternetInformationServices.6.2.ApplicationPool" ParentMonitorID="Health!System.Health.AvailabilityState" Remotable="true" Priority="Normal" TypeID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.MonitorType" ConfirmDelivery="true">
				<Category>AvailabilityHealth</Category>
				<AlertSettings AlertMessage="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.AlertMessage">
					<AlertOnState>Error</AlertOnState>
					<!-- Warning or Error should match OperationalStates below  -->
					<AutoResolve>true</AutoResolve>
					<AlertPriority>Normal</AlertPriority>
					<AlertSeverity>MatchMonitorHealth</AlertSeverity>
					<!-- Common options for AlertSeverity are MatchMonitorHealth, Information, Warning, Error -->
				</AlertSettings>
				<OperationalStates>
					<OperationalState ID="Running" MonitorTypeStateID="Running" HealthState="Success" />
					<OperationalState ID="NotRunning" MonitorTypeStateID="NotRunning" HealthState="Error" />
					<!-- HealthState = Warning or Error -->
				</OperationalStates>
				<Configuration>
					<!-- has to go in order like it goes in MonitorType / Configuration at line 144  -->
					<IntervalSeconds>120</IntervalSeconds>
					<TimeoutSeconds>60</TimeoutSeconds>
					<MatchCount>3</MatchCount> <!-- This is the number of consecutive matches that must be met before the monitor will change state.  Also a good example of passing in Integer data. -->
					<PoolID>$Target/Property[Type="MWIC!Microsoft.Windows.InternetInformationServices.ApplicationPool"]/PoolID$</PoolID>
				</Configuration>
			</UnitMonitor>
		</Monitors>
	</Monitoring>
	<Presentation>
		<StringResources>
			<StringResource ID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.AlertMessage" />
		</StringResources>
	</Presentation>
	<LanguagePacks>
		<LanguagePack ID="ENU" IsDefault="true">
			<DisplayStrings>
				<DisplayString ElementID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor">
					<Name>Application Pool Availability - IIS8 Custom Monitor</Name>
					<Description></Description>
				</DisplayString>
				<DisplayString ElementID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor" SubElementID="Running">
					<Name>Good Condition</Name>
				</DisplayString>
				<DisplayString ElementID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor" SubElementID="NotRunning">
					<Name>Bad Condition</Name>
				</DisplayString>
				<DisplayString ElementID="CORP.IIS8.IIS8ApplicationPoolAvailabilityCustom.Monitor.AlertMessage">
					<Name>Application Pool Availability - IIS8 Custom Monitor: Failure</Name>
					<Description>Application Pool Availability - IIS8 Custom Monitor: detected a bad condition</Description>
				</DisplayString>
			</DisplayStrings>
		</LanguagePack>
	</LanguagePacks>
</ManagementPackFragment>

I wonder why would these commands cause this? On every interval, Private Bytes raise for roughly 70 MB. As you can see in the code above, I have tried cleaning variables and unloading module, but no change.
When I run the script portion in ISE, it works normally - no performance issues at all.

Hopefully someone can shed some light to this.