component
	output = false
	hint = "I help locate memory leaks within the application components."
	{

	/**
	* I initialize the memory leak helper with the given collection of components.
	* 
	* @container I hold the components to track.
	* @output false
	*/
	public void function init( required struct container ) {

		// As part of the initialization of the memory leak helper, gather the targets
		// and snapshot the initial state of the system. This means that any lazy
		// initialization of component properties (within the given container) will show
		// up in future delta calculations; however, this will make it easier to consume
		// the deltas behind a load balancer.
		variables.targets = gatherTargets( arguments.container );
		variables.snapshots = gatherSnapshops();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I return the list of target names being watched for state changes.
	*/
	public array function getTargets() {

		return( variables.targets.keyArray() );

	}


	/**
	* I return the collection of state-slices that have observable state change.
	* 
	* NOTE: This only ever compares the current state to the ORIGINAL snapshot - the new
	* state is NOT PERSISTED for future delta calculations. Because of this, "expected"
	* properties that use lazy-initialization within the targets will show up in the
	* resultant delta.
	*/
	public struct function findMemoryLeaks() {

		var delta = variables.snapshots
			.map(
				( name, oldSnapshot ) => {

					var target = variables.targets[ name ];
					var newSnapshot = generateSnapshot( target );

					return( calculateSnapshotDelta( target, oldSnapshot, newSnapshot ) );

				}
			)
			.filter(
				( name, snapshotDelta ) => {

					return( ! snapshotDelta.isEmpty() );

				}
			)
		;

		return( delta );

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I calculate the delta between the two snapshots. If a state change has been made,
	* the delta is returned with the new state value.
	* 
	* @target I am the target component that has been snapshotted.
	* @oldSnapshot I am the old state snapshot.
	* @newSnapshot I am the new state snapshot that is being compared.
	*/
	private struct function calculateSnapshotDelta(
		required any target,
		required struct oldSnapshot,
		required struct newSnapshot
		) {

		var delta = {};
		var targetVariables = getVariablesScope( target );

		// Look for keys that exist in the old snapshot but that are no longer present in
		// the new snapshot (this is likely never going to happen).
		for ( var key in oldSnapshot ) {

			if ( ! newSnapshot.keyExists( key ) ) {

				delta[ key ] = "[null]";

			}

		}

		// Look for new keys or existing keys that are different in the new snapshot.
		for ( var key in newSnapshot ) {

			// NOTE: The snapshot data uses simple value identifiers that can be safely
			// compared as the standard equality operators.
			if (
				! oldSnapshot.keyExists( key ) ||
				( oldSnapshot[ key ] != newSnapshot[ key ] )
				) {

				delta[ key ] = targetVariables[ key ];

			}

		}

		return( delta );

	}


	/**
	* I gather the initial set of snapshots for the collected targets.
	*/
	private struct function gatherSnapshops() {

		var initialSnapshots = variables.targets.map(
			( name, target ) => {

				return( generateSnapshot( target ) );

			}
		);

		return( initialSnapshots );

	}


	/**
	* I recursively inspect the given container, looking for ColdFusion components to
	* add to the targets collection so that state can be watched over time.
	* 
	* @rootContainer I am the root container being inspected.
	*/
	private struct function gatherTargets( required struct rootContainer ) {

		var initialTargets = {};

		// Starting at the root container, the "recursion" will be powered by a queue of
		// containers, rather than true recursion, so that we don't create StackOverflow
		// problems (which we were seeing in production). As we examine each container,
		// new containers will be added to this queue.
		var containersToExplore = [ rootContainer ];

		// Continue pulling containers off the FRONT of the queue until we've run out of
		// new containers to inspect.
		while ( containersToExplore.isDefined( 1 ) ) {

			// SHIFT first container off of the queue.
			var container = containersToExplore.first();
			containersToExplore.deleteAt( 1 );

			// Iterate over the keys in this container - we're going to be looking for
			// keys that reference ColdFusion Components (ie, nested containers).
			for ( var key in container ) {

				// If the key is NULL for some reason, move onto the next key.
				if ( ! container.keyExists( key ) ) {

					continue;

				}

				var target = container[ key ];

				// If the key is definitely NOT a ColdFusion component, move onto the
				// next key.
				if ( ! isObject( target ) ) {

					continue;

				}

				// The isObject() function will return true for both components and Java
				// objects as well. As such, we need to go one step further to see if we
				// can get at the component metadata before we can truly determine if the
				// target is a ColdFusion component.
				try {

					var targetMetadata = getComponentMetaData( target );
					var targetName = targetMetadata.name;

				} catch ( any error ) {

					// An error indicates that either the metadata call failed; or, that
					// the results didn't contain a "name" property. In either case, this
					// isn't a value that we will know how to consume. Move onto the
					// next key.
					continue;

				}

				// If we've already inspected this target, move onto the next key.
				if ( initialTargets.keyExists( targetName ) ) {

					continue;

				}

				initialTargets[ targetName ] = target;

				// Recursively explore the target component for nested components that
				// may not have been accessible in the top-level collection of targets.
				containersToExplore.append( getVariablesScope( target ) );

			} // END: For-Loop (key in container).

		} // END: While-Loop (containersToExplore).

		return( initialTargets );

	}


	/**
	* I return a snapshot of the given target's variables scope.
	* 
	* @target I am the target being inspected.
	*/
	private struct function generateSnapshot( required any target ) {

		var snapshot = getVariablesScope( target )
			.filter(
				( key, value ) => {

					// Skip types that are unlikely to be the result of a leak.
					if (
						isNull( value ) ||
						isObject( value ) ||
						isCustomFunction( value )
						) {

						return( false );

					}

					// Some of the native ColdFusion tags appear to dump debugging
					// information into the variables scope. Ignore these.
					if (
						( key == "cfquery" ) ||
						( key == "cflock" )
						) {

						return( false );

					}

					return( true );

				}
			)
			.map(
				( key, value ) => {

					return( getValueIdentifier( value ) );

				}
			)
		;

		return( snapshot );

	}


	/**
	* I get the value identifier for the given value. The result will be a "simple" value
	* that can be safely compared across snapshots.
	* 
	* The current approach is based on the Java HashCode, which provides some level of
	* insight into the memory usage without putting too much burden on performance. This
	* is not a prefect approach; but, as a first pass, it should be OK.
	* 
	* @value I am the value being identified.
	*/
	private string function getValueIdentifier( required any value ) {

		try {

			var valueIdentifier = value.hashCode();

		} catch ( any error ) {

			var valueIdentifier = 0;

		}

		// NOTE: A binary value tests as an "Array"; but, doesn't support the Array API.
		if ( isBinary( value ) ) {

			valueIdentifier &= ( ":" & arrayLen( value ) );

		} else if ( isStruct( value ) || isArray( value ) ) {

			valueIdentifier &= ( ":" & value.len() );

		}

		return( valueIdentifier );

	}


	/**
	* I return the variables scope for the given target.
	*/
	private struct function getVariablesScope( required any target ) {

		// Inspect the spy method so that we will be able to pierce the private scope of
		// the target and observe the internal state. It doesn't matter if we inject this
		// multiple times, we're the only consumers.
		target.getVariablesScope__scope_spy = variables.getVariablesScope__scope_spy;

		return( target.getVariablesScope__scope_spy() );

	}


	/**
	* I return the VARIABLES scope in the current execution context.
	*/
	private any function getVariablesScope__scope_spy() {

		// CAUTION: This method has been INJECTED INTO A TARGETED COMPONENT and is being
		// executed in the context of that targeted component.
		return( variables );

	}

}