Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Heather Metcalf and Faith Chouteau
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Heather Metcalf Faith Chouteau

Running Memory Leak Detection After Every ColdFusion Request

By
Published in Comments (18)

In the comments of a post over on LinkedIn, I was talking to Charles Robertson about how unnerving it is to have unscoped local variables leak into the variables scope of a persisted component. This type of memory leak can lead to the cross-contamination of requests; and, in a worst case scenario, will cause one user's data to be shown to another user. Inspired by that conversation, I decided to add memory leak detection to the post-processing of every ColdFusion request in my feature flags playground application.

This is not the first time that I've talked about memory leak detection in ColdFusion. My notion of a "Snooper" was first discussed in 2015; and then, years later, I start to think about "snap-shotting" memory for time-based deltas. But, in both of those cases, memory leak detection was still something that I performed as an after thought—a last step in development process.

Ideally, memory leak detection should be something that I don't have to remember to do—it's too important. So why not just have it running all the time?

To enable this, I added an .inspect() call within the onRequestEnd() event handler in my Application.cfc ColdFusion framework component. This logic will only run in my development environment as this is where I have the opportunity to find and fix the issue.

component {

	// ... truncated ...

	/**
	* I get called once to finalize the request.
	*/
	public void function onRequestEnd() {

		if ( this.config.isLive ) {

			return;

		}

		// Since the memory leak detection only runs in the development environment, I'm
		// not going to put any safe-guards around it. The memory leak detector both reads
		// from and writes to shared memory, which can be inherently unsafe. However, the
		// risks here are minimal.
		request.ioc.get( "core.lib.MemoryLeakDetector" )
			.inspect()
		;

	}

}

In this application, request.ioc is my implementation of a simple Dependency-Injection (DI) framework for ColdFusion. This Inversion of Control (IoC) container is responsible for instantiating and wiring all of my ColdFusion components together.

Since ioc has all cached CFCs in its own memory space, it turns out to be the perfect place to start looking for memory leaks. So, not only am I using the request.ioc to access my MemoryLeakDetector.cfc, you'll see below that my MemoryLeakDetector.cfc then turns around and uses the ioc internals as the queue-initializer for memory leak detection.

To get this working, I had to add a .getAll() method to my Injector.cfc (the CFC behind the request.ioc reference):

component {

	// ... truncated ...

	/**
	* I return all of the cached services.
	*/
	public struct function getAll() {

		return services.copy();

	}

}

My MemoryLeakDetector.cfc then uses this .getAll() method to populate a queue of components to inspect. As each component is inspected, any new components found in the private (variables) scope are subsequently queued-up for future inspection. It's not a perfect algorithm; but, since almost all memory leaks will be due to unscoped local variables in an IoC-persisted component, it's sufficient for my use-case.

The algorithm for leak detection is relatively simple:

  • Queue-up cached components for inspection.

  • Loop over the queue.

  • Extract the private variables scope of a given cached component. This is done by injecting a tunneling function that can return the variables scope of the current context.

  • Loop over the private keys. Skip over any ColdFusion-internals such as the this scope and the CFThread function artifacts. Also skip over any CFC that's already been inspected as part of a repeated reference.

  • Check to see if each private key maps to a CFProperty tag or a CFFunction tag.

  • If there's no corresponding mapping, log a warning to the console that includes the CFC path and the variable name.

  • If the given key represents a CFC, add it to the queue for inspection.

In the ColdFusion component below, notice that there are several CFProperty tags. And, that two of them have ioc:skip attributes. Since the memory leak detection works by checking variables keys against the collection of CFProperty tags, all private keys must have a corresponding CFProperty tag, even if they aren't there to power dependency-injection.

component
	output = false
	hint = "I help detect memory leaks in ColdFusion components."
	{

	// Define properties for dependency-injection.
	property name="ioc" ioc:type="core.lib.Injector";
	property name="magicFunctionName" ioc:skip;
	property name="magicTokenName" ioc:skip;
	property name="utilities" ioc:type="core.lib.util.Utilities";

	/**
	* I initialize the memory leak detector.
	*/
	public void function $init() {

		variables.magicTokenName = "$$MemoryLeakDetector$$Version$$";
		variables.magicFunctionName = "$$MemoryLeakDetector$$Inspect$$";

	}

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

	/**
	* I scan the services in the Injector, looking for memory leaks.
	*/
	public void function inspect() {

		var version = createUuid();
		var queue = utilities.structValueArray( ioc.getAll() );

		// We're going to perform a breadth-first search of the components, starting with
		// the Injector services and then collecting any additional components we find
		// along the way.
		while ( queue.isDefined( 1 ) ) {

			var target = queue.shift();

			if ( ! utilities.isComponent( target ) ) {

				continue;

			}

			// If this target has already been inspected, skip it. However, since memory
			// leaks may develop over time based on the user's interaction, we need to
			// check the version number (of the current inspection). Only skip if we're
			// in the same inspection workflow and we're revisiting this component.
			// --
			// Note: In Adobe ColdFusion, CFC's don't have a .keyExists() member method.
			// As such, in this case, I have to use the built-in function.
			if ( structKeyExists( target, magicTokenName ) && ( target[ magicTokenName ] == version ) ) {

				continue;

			}

			// Make sure we don't come back to this target within the current inspection.
			target[ magicTokenName ] = version;

			var targetMetadata = getMetadata( target );
			var targetName = targetMetadata.name;
			var targetScope = getVariablesScope( target );
			var propertyIndex = utilities.indexBy( targetMetadata.properties, "name" );
			var functionIndex = utilities.indexBy( targetMetadata.functions, "name" );

			for ( var key in targetScope ) {

				// Skip the public scope - memory leaks only show up in the private scope.
				if ( key == "this" ) {

					continue;

				}

				// Skip hidden functions created by the CFThread tag.
				if ( key.reFindNoCase( "^_cffunccfthread" ) ) {

					continue;

				}

				// Treat top-level null values as suspicious.
				if ( ! targetScope.keyExists( key ) ) {

					logMessage( "Possible memory leak in [#targetName#]: [null]." );
					continue;

				}

				if (
					! propertyIndex.keyExists( key ) &&
					! functionIndex.keyExists( key )
					) {

					logMessage( "Possible memory leak in [#targetName#]: [#key#]." );

				}

				// If the value is, itself, a component, add it to the queue for
				// subsequent inspection.
				if ( utilities.isComponent( targetScope[ key ] ) ) {

					queue.append( targetScope[ key ] );

				}

			}

		}

	}

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

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

		// Caution: This method has been injected into a targeted component and is being
		// executed in the context of that targeted component.
		return variables;

	}


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

		// Inject the spy method so that we'll 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[ magicFunctionName ] = variables.dangerouslyAccessVariablesInCurrentContext;

		return invoke( target, magicFunctionName );

	}


	/**
	* I log the given message to the standard out (console).
	*/
	private void function logMessage( required string message ) {

		cfdump(
			var = message,
			output = "console"
		);

	}

}

Now that we have the MemoryLeakDetector.cfc in place; and the .inspect() method is being called at the end of every request; let's go in and create a memory leak. One place that memory leaks often come up are in for-loop index variables. So, let's go into my Utilities.cfc and remove the var within the .indexBy() method:

component {

	// ... truncated ...

	/**
	* I index the given collection using the given key as the associative entry.
	*/
	public struct function indexBy(
		required array collection,
		required string key
		) {

		var index = {};

		// !!!! CAUTION: No VAR keyword. !!!!
		for ( element in collection ) {

			index[ element[ key ] ] = element;

		}

		return index;

	}

}

Notice that there is no var before the element variable declaration in the for loop. This will cause the element variable to be stored into the Utilities.cfc variables scope. And, if we now run my application and look at the Docker console logging, we can see this output:

As you can see, the MemoryLeakDetector.cfc was able to locate and identify this variable which has shown up in the wrong place.

Right now, this runs at the end of every single request. In a small application (like my feature flags playground), this shouldn't pose any performance issue (development computers are so freaking fast these days). However, if this does pose a problem for larger applications (or slower machines), it should be easy enough to limit the inspection to only certain requests or to throttle the inspection to a certain time interval.

Generic vs. Home Grown Solution

As I was building this out, I was forced to think about the differences in complexity between a generic solution and a home grown solution. In my case, by using a home grown solution, I was able to leverage my existing Injector.cfc and my existing Utilities.cfc; and, I was able to lean on the ioc:skip attribute in the CFProperty tag in order to mandate that all variables-scope keys have to have a corresponding CFProperty tag.

If I were to build this out as a generic solution, it would've been much more complex. I'd have to re-implement utilities and I'd probably have to provide configuration options for features like an allow-list of keys.

Often times, the home grown approach (or the generic solution copy-pasted and modified) is the simplest solution.

Want to use code from this post? Check out the license.

Reader Comments

448 Comments

Great stuff!

I might see if I can rework this for FW1.
I know FW1 uses DI, so there should be quite a lot of commonality here.

Also like you, I have built a custom DI, using an XML config file. I was inspired by Coldspring.
I reckon every CF Dev worth anything, should have a go at building a DI. It is a great way of learning about how Dependency Injection really works, under the hood.
So, I might start with this first.

15,848 Comments

@Charles,

That would be cool to see. I've dabbled a bit with the internals of FW/1 before; and I remember having do some "funky" stuff in order to get access to some of the cache components. I think the Controllers, especially, where harder to get through. Step 1 might be updating DI/1 or FW/1 to make the internal state more accessible without hacks.

Other than that, though, yeah there should be a lot of overlap in how the two systems works. I think DI/1 might allow for nested injectors (or maybe I'm getting confused with Angular now). But, more or less, it's all just cached values.

Looking forward to hearing about your adventure!

448 Comments

@Ben Nadel,

So essentially in FW1, you just do:

// controllers/user.cfc

component accessors = true{

    property userService; // model.services.user

    …

}

And then call the service like:

variables.userService

In the controller.

Not entirely sure how everything works behind the scenes.
When I created my own custom DI, I actually wrote out the explicit getters/setters, but I hear that cfproperty creates implicit getters/setters alongside the accessors=true, which is a little easier to manage.
Anyway, I will have a rummage around in the FW1 core and see how everything works?

Or perhaps, Sean Corfield reads this blog and he can tell us, himself 🙂

15,848 Comments

@Charles,

Sorry, what I meant was that if you wanted to do meta-programming against the cached instances, in your example, it's hard to get at the cached instance of controllers/user.cfc. At least it was the last time I checked; though, maybe I just missed it.

Essentially, in the way that I added the .getAll() method to my inject, you would need something similar in FW/1.

448 Comments

@Ben Nadel,

The one thing I do remember is that in FW1, controllers/services are singletons, so are probably accessible via the application scope.
I presume that means they are cached, in some way?

I think you can do something like:

userController = application[ variables.framework.applicationKey ].cache.controllers.user

I think this drags the controller out of the application scope? Then I could access the components metadata?
Maybe I could use something like this to emulate your methodology?

15,848 Comments

Yeah, totally. I don't know the low-level details of the caching; but, yeah, as long as you can get at the singletons, you could theoretically do what I'm doing. And, the nice thing is, you don't need the whole ioc:skip thing that I did. With FW/1, if there's no setter function, I believe that the FW/1 will just skip the injection for a given property.... I think.

448 Comments

@Ben Nadel,

With FW/1, if there's no setter function, I believe that the FW/1 will just skip the injection for a given property

Thanks for this tip. 💪

I know that FW1 is being updated again:

https://github.com/framework-one/fw1

By Mr Springle.

I may see if I can add this as a dev feature, which could be switched on like:

application.cfc

variables.framework.environments = {
	  dev = {
		varScopeChecker = true,
		…
	  },
	  prod = {
             …
        }
}

Of course, I am getting ahead of myself, here, as I actually need to build this feature? 🤪

448 Comments

I have been looking at the fw1 framework folder with all the core components. It's really impressive stuff.

I then came across this interesting method, in framework/beanProxy:

https://github.com/framework-one/fw1/blob/develop/framework/beanProxy.cfc

getTargetBeanMetadata()

Which might just help with the task of inspecting controllers!
Apparently, a controller/service/bean is actually a bean, despite the ambiguity.
So, I can use this method for any type of cfc.

15,848 Comments

I'll have to take a look. The version of FW/1 that we have was literally like a decade old; so I'm sure it doesn't have all of the newer more moderny stuff. One thing that I always wished that it had was a subsystem-local error handler. We always had to handle errors in the root Application.cfc; and then inspect the event to see which subsystem it was, and then programmatically reach back into the subsystem to call some specific error handling. Always felt very dirty. Maybe this is something they've changed in the last decade, though.

4 Comments

Hi Ben,

in order to check unscoped variables in Lucee I just a custom debugging template with this code:

/opt/lucee/tomcat/lucee-server/context/context/admin/debug/Contens.cfc

<cfloop query="local.implicitAccess">
  <cfif
    right(local.implicitAccess.template, 3) EQ "cfc" AND  NOT listFindNoCase("cfquery,cfcatch,cfstoredproc,cfhttp,cflock,cffile,file,cfthread,cfdocument,cfftp,request", local.implicitAccess.name)>
      <cflog file="contens_implicit" text="#serialize(queryslice(local.implicitAccess, local.implicitAccess.currentrow, 1))#">
  </cfif>
</cfloop>

<cfloop query="local.queries">
  <cfif local.queries.time GT (1 * 10^9) OR local.queries.count GT 20000><!--- 1 sec (in ns) --->
      <cflog file="lucee_queries" text="#reReplace( serialize( queryslice(local.queries, local.queries.currentrow, 1) ), "(" & chr( 10 ) & "|" & chr( 13 ) & ")+[[:space:]]{     2,}     ", chr( 13 ), "all" )#">
  </cfif>
</cfloop>

It will also log slow queries or queries with many records.

15,848 Comments

@Harry,

That's so cool! What a clever idea. My default application settings usually turn off all debugging (since it breaks JSON-based responses); so I somewhat fell out of familiarity with the local debugging template after using a Single-Page App (SPA) for so long. But, this is such smooth move 🙌

4 Comments

Thank you. This template produces no output so nothing can happen - we also have a lot of JSON responses.
I check the log files regularly and if it logs some implicit scoped vars I fix that.

448 Comments

@Ben Nadel,

Sorry, I am a little lost, here 😬

Where does:

local.implicitAccess

Come from? And does this work with ACF?

I had a look for an equivalent of:

/opt/lucee/tomcat/lucee-server/context/context/admin/debug/Contens.cfc

ACF2023:

\\wsl.localhost\Debian\opt\ColdFusion2023\cfusion\runtime\conf\server.xml

And couldn't find anything?

4 Comments

@Charles Robertson,
no, this only works for Lucee.
There you have to create a custom debug template and save it into the folder "lucee-server/context/context/admin/debug/mydebugtemplate.cfc".
local.implicitAccess comes from Lucee if you enable "Implicit variable Access" in the Debugging/Settings

15,848 Comments

One thing I've run across now is that this gets a bit noisy when you start loading 3rd-party vendor scripts that you don't really want to go in and edit. As such, I've updated my code to allow for a CFProperty attribute that will prevent a given variable from being inspected:

property name="foo" memoryLeakDetector:skip

Now, as the memory leak detector is iterating through the dependency-graph, it won't recurse into variables that correspond to properties with the memoryLeakDetector:skip attribute.

Of course, this won't matter for injectables that are provided directly to the IoC container, since those don't have corresponding CFProperty tags. This would only impact properties that instantiated internally to a CFC.

In some ways, I think this is actually a "Good Thing" because it will push me to create custom ColdFusion components that wrap vendor classes instead of just willy-nilly passing them around. Or maybe I'm just trying to put a positive slant on it.

In my case, this came up with the JavaLoaderFactory, which is popular in the Adobe ColdFusion world for loading Java classes from .jar files.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel