Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Jamie Samland
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Jamie Samland@jsamland )

ColdFusion Performance Experiment: Caching Per-Application Settings In Lucee CFML 5.3.3.62

By Ben Nadel on
Tags: ColdFusion

At InVision, the amount of work that we do in the Application.cfc ColdFusion framework component is a bit staggering. Not only are we setting up all of the Lucee CFML mappings, custom paths, data-sources, SMTP servers, and cache configurations, we're also defining hundreds of FW/1 routes. The pseudo-constructor of the Application.cfc gets evaluated on every single page request; which means, we're doing the aforementioned work on every single page request. Which got me thinking: can we just cache these settings and re-use them? To explore the potential performance implications of caching, I put together a very simple set of load-tested demos in Lucee CFML 5.3.3.62.

First, I started out with my "control" cohort - a ColdFusion application that defines all of the settings in the Application.cfc pseudo-constructor. The amount of "work" being done here is fairly small; but, it will be consistent across each version. As such, I hope that it will be just enough to bubble any performance differences to the surface:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	this.name = hash( getCurrentTemplatePath() );
	this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
	this.sessionManagement = false;

	variables.webrootDir = getDirectoryFromPath( getCurrentTemplatePath() );

	this.mappings = {
		"/a": "#webrootDir#vendor/a-v1.0.0/",
		"/b": "#webrootDir#vendor/b-v1.0.0/",
		"/c": "#webrootDir#vendor/c-v1.0.0/",
		"/d": "#webrootDir#vendor/d-v1.0.0/",
		"/e": "#webrootDir#vendor/e-v1.0.0/",
		"/f": "#webrootDir#vendor/f-v1.0.0/"
	};

	this.customTagPaths = [
		"#webrootDir#libs/cftags/"
	].toList();

	variables.db = {
		host: "my.db.host",
		database: "testing",
		username: "ben",
		password: "ben"
	};

	this.datasources.testing = {
		class: "com.mysql.cj.jdbc.Driver",
		connectionString: (
			"jdbc:mysql://#db.host#:3306/#db.database#?" &
			[
				"useUnicode=true",
				"characterEncoding=UTF-8",
				"zeroDateTimeBehavior=round",
				"serverTimezone=Etc/UTC",
				"autoReconnect=true",
				"allowMultiQueries=true",
				"useLegacyDatetimeCode=false",
				"tinyInt1isBit=false",
				"useDynamicCharsetInfo=false",
				"cachePrepStmts=true",
				"cacheCallableStmts=true",
				"cacheServerConfiguration=true",
				"useLocalSessionState=true",
				"elideSetAutoCommits=true",
				"alwaysSendSetIsolation=false",
				"enableQueryTimeouts=false"
			].toList( "&" )
		),
		username: db.username,
		password: db.password,
		blob: true,
		clob: true,
		connectionLimit: 10,
		connectionTimeout: 5
	};

	this.mailServers = [
		{
			host: "mail.host",
			port: 25,
			username: "ben",
			password: "ben",
			ssl: false,
			tls: false,
			lifeTimespan: createTimeSpan( 0, 0, 1, 0 ),
			idleTimespan: createTimeSpan( 0, 0, 0, 10 )
		}
	];

}

As you can see, nothing special is going on here. We're just defining standard ColdFusion application settings.

NOTE: All of the index.cfm files in these demos do nothing more than call echo("done"). The only differentiating work is in the Application.cfc ColdFusion component file.

Ok, now let's start to experiment with some caching techniques. First, I wanted to see what would happen if we put the setting calculations behind a memoized (cachedWithin) function:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	structAppend( this, getSettings() );

	/**
	* I define the application settings.
	* 
	* EXPERIMENT: Since these don't change on per-request basis, we are putting them
	* behind a memoized function so that we don't have to recalculate all of the hashes
	* and string interpolations. The THEORY being that this will be faster????
	*/
	public struct function getSettings() cachedWithin = createTimeSpan( 0, 1, 0, 0 ) {

		var settings = {
			name: hash( getCurrentTemplatePath() ),
			applicationTimeout: createTimeSpan( 1, 0, 0, 0 ),
			sessionManagement: false
		};

		var webrootDir = getDirectoryFromPath( getCurrentTemplatePath() );

		settings.mappings = {
			"/a": "#webrootDir#vendor/a-v1.0.0/",
			"/b": "#webrootDir#vendor/b-v1.0.0/",
			"/c": "#webrootDir#vendor/c-v1.0.0/",
			"/d": "#webrootDir#vendor/d-v1.0.0/",
			"/e": "#webrootDir#vendor/e-v1.0.0/",
			"/f": "#webrootDir#vendor/f-v1.0.0/"
		};

		settings.customTagPaths = [
			"#webrootDir#libs/cftags/"
		].toList();

		var db = {
			host: "my.db.host",
			database: "testing",
			username: "ben",
			password: "ben"
		};

		settings.datasources.testing = {
			class: "com.mysql.cj.jdbc.Driver",
			connectionString: (
				"jdbc:mysql://#db.host#:3306/#db.database#?" &
				[
					"useUnicode=true",
					"characterEncoding=UTF-8",
					"zeroDateTimeBehavior=round",
					"serverTimezone=Etc/UTC",
					"autoReconnect=true",
					"allowMultiQueries=true",
					"useLegacyDatetimeCode=false",
					"tinyInt1isBit=false",
					"useDynamicCharsetInfo=false",
					"cachePrepStmts=true",
					"cacheCallableStmts=true",
					"cacheServerConfiguration=true",
					"useLocalSessionState=true",
					"elideSetAutoCommits=true",
					"alwaysSendSetIsolation=false",
					"enableQueryTimeouts=false"
				].toList( "&" )
			),
			username: db.username,
			password: db.password,
			blob: true,
			clob: true,
			connectionLimit: 10,
			connectionTimeout: 5
		};

		settings.mailServers = [
			{
				host: "mail.host",
				port: 25,
				username: "ben",
				password: "ben",
				ssl: false,
				tls: false,
				lifeTimespan: createTimeSpan( 0, 0, 1, 0 ),
				idleTimespan: createTimeSpan( 0, 0, 0, 10 )
			}
		];

		return( settings );

	}

}

As you can see, we're defining all of the same application settings. Only, this time, we're trying to use the ColdFusion cache so that we don't actually have to re-calculate the settings on each request.

Of course, with this technique, I worried that the Function call itself might be adding a lot of overhead. Not to mention the fact that memoized function results are returned by value, not by reference. Which means, setting and getting the value into and out of the cache may be relatively expensive. As such, as the next experiment, I wanted to try caching the results by reference in the server scope:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	structAppend( this, getSettings() );

	/**
	* I define the application settings.
	* 
	* EXPERIMENT: Since these don't change on per-request basis, we are putting them
	* in the SERVER scope so that we don't have to recalculate all of the hashes and
	* string interpolations. The THEORY being that this will be faster????
	*/
	public struct function getSettings() {

		if ( server.keyExists( "appSettingsV3" ) ) {

			return( server.appSettingsV3 );

		}

		var settings = {
			name: hash( getCurrentTemplatePath() ),
			applicationTimeout: createTimeSpan( 1, 0, 0, 0 ),
			sessionManagement: false
		};

		var webrootDir = getDirectoryFromPath( getCurrentTemplatePath() );

		settings.mappings = {
			"/a": "#webrootDir#vendor/a-v1.0.0/",
			"/b": "#webrootDir#vendor/b-v1.0.0/",
			"/c": "#webrootDir#vendor/c-v1.0.0/",
			"/d": "#webrootDir#vendor/d-v1.0.0/",
			"/e": "#webrootDir#vendor/e-v1.0.0/",
			"/f": "#webrootDir#vendor/f-v1.0.0/"
		};

		settings.customTagPaths = [
			"#webrootDir#libs/cftags/"
		].toList();

		var db = {
			host: "my.db.host",
			database: "testing",
			username: "ben",
			password: "ben"
		};

		settings.datasources.testing = {
			class: "com.mysql.cj.jdbc.Driver",
			connectionString: (
				"jdbc:mysql://#db.host#:3306/#db.database#?" &
				[
					"useUnicode=true",
					"characterEncoding=UTF-8",
					"zeroDateTimeBehavior=round",
					"serverTimezone=Etc/UTC",
					"autoReconnect=true",
					"allowMultiQueries=true",
					"useLegacyDatetimeCode=false",
					"tinyInt1isBit=false",
					"useDynamicCharsetInfo=false",
					"cachePrepStmts=true",
					"cacheCallableStmts=true",
					"cacheServerConfiguration=true",
					"useLocalSessionState=true",
					"elideSetAutoCommits=true",
					"alwaysSendSetIsolation=false",
					"enableQueryTimeouts=false"
				].toList( "&" )
			),
			username: db.username,
			password: db.password,
			blob: true,
			clob: true,
			connectionLimit: 10,
			connectionTimeout: 5
		};

		settings.mailServers = [
			{
				host: "mail.host",
				port: 25,
				username: "ben",
				password: "ben",
				ssl: false,
				tls: false,
				lifeTimespan: createTimeSpan( 0, 0, 1, 0 ),
				idleTimespan: createTimeSpan( 0, 0, 0, 10 )
			}
		];

		return( server.appSettingsV3 = settings );

	}

}

As you can see, this version of the experiment uses the same approach, in so much as the settings are hidden behind a Function call. Only this time, instead of using the native cachedWithin function directive, we're imperatively caching the settings in the server scope.

Of course, I was still concerned that the Function call itself was going to be too expensive. So, as a last experiment, I stuck with the server scope based caching; but, am now implementing the caching logic directly in the pseudo-constructor of the Application.cfc itself:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	// EXPERIMENT: Since these don't change on per-request basis, we are putting them in
	// the SERVER scope so that we don't have to recalculate all of the hashes and string
	// interpolations. The THEORY being that this will be faster????
	if ( isNull( server.appSettingsV4 ) ) {

		variables.settings = {
			name: hash( getCurrentTemplatePath() ),
			applicationTimeout: createTimeSpan( 1, 0, 0, 0 ),
			sessionManagement: false
		};

		variables.webrootDir = getDirectoryFromPath( getCurrentTemplatePath() );

		settings.mappings = {
			"/a": "#webrootDir#vendor/a-v1.0.0/",
			"/b": "#webrootDir#vendor/b-v1.0.0/",
			"/c": "#webrootDir#vendor/c-v1.0.0/",
			"/d": "#webrootDir#vendor/d-v1.0.0/",
			"/e": "#webrootDir#vendor/e-v1.0.0/",
			"/f": "#webrootDir#vendor/f-v1.0.0/"
		};

		settings.customTagPaths = [
			"#webrootDir#libs/cftags/"
		].toList();

		variables.db = {
			host: "my.db.host",
			database: "testing",
			username: "ben",
			password: "ben"
		};

		settings.datasources.testing = {
			class: "com.mysql.cj.jdbc.Driver",
			connectionString: (
				"jdbc:mysql://#db.host#:3306/#db.database#?" &
				[
					"useUnicode=true",
					"characterEncoding=UTF-8",
					"zeroDateTimeBehavior=round",
					"serverTimezone=Etc/UTC",
					"autoReconnect=true",
					"allowMultiQueries=true",
					"useLegacyDatetimeCode=false",
					"tinyInt1isBit=false",
					"useDynamicCharsetInfo=false",
					"cachePrepStmts=true",
					"cacheCallableStmts=true",
					"cacheServerConfiguration=true",
					"useLocalSessionState=true",
					"elideSetAutoCommits=true",
					"alwaysSendSetIsolation=false",
					"enableQueryTimeouts=false"
				].toList( "&" )
			),
			username: db.username,
			password: db.password,
			blob: true,
			clob: true,
			connectionLimit: 10,
			connectionTimeout: 5
		};

		settings.mailServers = [
			{
				host: "mail.host",
				port: 25,
				username: "ben",
				password: "ben",
				ssl: false,
				tls: false,
				lifeTimespan: createTimeSpan( 0, 0, 1, 0 ),
				idleTimespan: createTimeSpan( 0, 0, 0, 10 )
			}
		];

		server.appSettingsV4 = settings;

	}

	structAppend( this, server.appSettingsV4 );

}

As you can see, the logic in v3 and v4 of this experiment are basically the same. It's just that v4 does it without the Function call.

Ok, so are there any performance improvements in these experiments? To test this, I created a trivial load-tester (running on the same machine and Lucee instance) that simply sees how many requests it can make to each application version. Since there are bound to be other processes running on this machine and outlier cases, I configured the load-tester to run many trials and then select the Top-N best performing results:

<cfscript>
	
	// Which version of the experiment are we running.
	param name="url.v" type="numeric";

	results = [];

	loop times = 30 {

		targetUrl = "http://127.0.0.1:57487/app-cachedwithin/v#url.v#/index.cfm";
		duration = ( 2 * 1000 );
		cutOffTickCount = ( getTickCount() + duration );
		callCount = 0;

		try {

			// Let's see how many HTTP calls we can make to that app in a fixed time.
			while ( getTickCount() < cutOffTickCount ) {

				loadTest = new Http(
					method = "get",
					url = targetUrl,
					timeout = 1
				);
				fileContent = loadTest.send().getPrefix().fileContent;

				// Only count this as a success if the content contains the expected
				// done indication.
				if ( fileContent.reFind( "v[1234]: done" ) ) {

					callCount++;

				}

			}

		} catch ( any error ) {

			// Sometimes, the Lucee dev server chokes under the load. If that happens,
			// let's just kill this test iteration, pause, and start the next one.
			systemOutput( error, true, true );
			sleep( 1000 );

		}

		results.append( callCount );
		sleep( 500 );

	}

	// Get the top results in the experiment.
	results.sort( "numeric", "desc" );
	sortedResults = results.slice( 1, 10 );

	for ( result in sortedResults ) {

		echo( "V#url.v# made " );
		echo( "#numberFormat( result, ',' )# requests in " );
		echo( "#numberFormat( ( duration / 1000 ), ',' )# second(s)." );
		echo( "<br />" );

	}

</cfscript>

As you can see, the load-tester uses a fixed duration; and then, sees how many successful HTTP requests is can make to the given Lucee CFML application in that time. We then take all of the results, for the given version, sort them based on count, and pick the top-N values.

Now, time to see how it all performed. Running the load-tester against each of the four versions yields the following results:

Control
V1 made 673 requests in 2 second(s).
V1 made 672 requests in 2 second(s).
V1 made 672 requests in 2 second(s).
V1 made 670 requests in 2 second(s).
V1 made 669 requests in 2 second(s).
V1 made 669 requests in 2 second(s).
V1 made 666 requests in 2 second(s).
V1 made 663 requests in 2 second(s).
V1 made 662 requests in 2 second(s).
V1 made 661 requests in 2 second(s).

CachedWithin Function
V2 made 666 requests in 2 second(s).
V2 made 665 requests in 2 second(s).
V2 made 665 requests in 2 second(s).
V2 made 664 requests in 2 second(s).
V2 made 664 requests in 2 second(s).
V2 made 662 requests in 2 second(s).
V2 made 662 requests in 2 second(s).
V2 made 661 requests in 2 second(s).
V2 made 660 requests in 2 second(s).
V2 made 660 requests in 2 second(s).

Server Scope Function
V3 made 676 requests in 2 second(s).
V3 made 675 requests in 2 second(s).
V3 made 675 requests in 2 second(s).
V3 made 674 requests in 2 second(s).
V3 made 667 requests in 2 second(s).
V3 made 658 requests in 2 second(s).
V3 made 654 requests in 2 second(s).
V3 made 654 requests in 2 second(s).
V3 made 653 requests in 2 second(s).
V3 made 653 requests in 2 second(s).

Server Scope Pseudo-Constructor
V4 made 681 requests in 2 second(s).
V4 made 680 requests in 2 second(s).
V4 made 677 requests in 2 second(s).
V4 made 676 requests in 2 second(s).
V4 made 676 requests in 2 second(s).
V4 made 668 requests in 2 second(s).
V4 made 667 requests in 2 second(s).
V4 made 666 requests in 2 second(s).
V4 made 664 requests in 2 second(s).
V4 made 664 requests in 2 second(s).

What we can see here is that each approach is roughly equivalent. Which is exciting because it means that we don't have to worry about all of the Per-application settings that we are re-defining on every single request to our ColdFusion application. No need to prematurely optimize anything here. Just keep on keeping-on! Life's a garden, dig it!



Reader Comments

I must have completely misunderstood the application scope then - I thought the whole point of the application scope was that it wasn't re-evaluated on every request, and only on an application reload or timeout (triggered differently depending on framework).

Reply to this Comment

@Tom,

Great question. You're just conflating two like-named things (naming for the win!). The application scope persists during the entire life of the application. However, the Application.cfc component gets reinstantiated on every single request.

Now, that's not to say that the application-life-cycle methods are all always called. For example, the onApplicationStart() method is only called once when the application is starting-up. But, some methods, like onRequestStart() are called on every single request.

In your Application.cfc, if you just add something like systemOutput( now() ) in your pseudo-constructor (where you would also define this.name) you will see this show up in your logs on every request.

Hope that clarifies stuff a little bit.

Reply to this Comment

@Tom,

Yeah, you basically never need to think about it :D which is nice. The only time it really ever matters is if you need to something non-standard, like turn on/off session management on a per-request basis. It's very rare that I ever think about it.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.