Running Memory Leak Detection After Every ColdFusion Request
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 thevariables
scope of the current context.Loop over the private keys. Skip over any ColdFusion-internals such as the
this
scope and theCFThread
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 aCFFunction
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
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.
@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!
@Ben Nadel,
So essentially in FW1, you just do:
// controllers/user.cfc
And then call the service like:
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 theaccessors=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 🙂
@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.OK. Thanks for the heads-up 🙏
@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:
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?
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.@Ben Nadel,
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
Of course, I am getting ahead of myself, here, as I actually need to build this feature? 🤪
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
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.
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.
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
It will also log slow queries or queries with many records.
@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 🙌
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.
@Harry,
Ah, I see, because it's using the
cflog
and not writing to the response. Thanks for clarifying that.@Ben Nadel,
Sorry, I am a little lost, here 😬
Where does:
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?
@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
@Harry Klein,
Thanks for this information.
Interesting. 🤔
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 →