ColdFusion Performance Experiment: Caching Per-Application Settings In Lucee CFML 5.3.3.62
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 callecho("done")
. The only differentiating work is in theApplication.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!
Want to use code from this post? Check out the license.
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).
@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, theApplication.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, likeonRequestStart()
are called on every single request.In your
Application.cfc
, if you just add something likesystemOutput( now() )
in your pseudo-constructor (where you would also definethis.name
) you will see this show up in your logs on every request.Hope that clarifies stuff a little bit.
@Ben,
Ah gotcha. Guess I'd never thought much about the
this
scope being re-evaluated etc!@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.
In your experience have you found that the lack of significant performance difference also applies to UDF library components in the Application scope? I'm exploring that very topic over in the Lucee dev forum.