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 ); } }