Friday 30 May 2014

Hanging on to outdated knowledge: don't

G'day:
This has taken me a few weeks to sort out - I started on it well before CF.Objective() - but I think I've finally stared at log files for long enough to come up with my conclusion.

I'm working with a client's codebase. A lot of it was written a) a long time ago when come CFML coding practices were not as fleshed-out as they are now; b) initially on CFMX7 (which, of course, comes with penalties of its own). Their ColdFusion install has moved on to version 9, but some of their coding practices still languish in mid-last-decade.  Part of this is decision-makers arriving at a conclusion as to how ColdFusion does things based on how it did things in ColdFusionMX, and never re-assessing their position to see if it still had merit. Their position was never re-assessed partly because no-one ever questioned it. It was just the way things were done. Times and personnel have changed, so now questions are being asked. I hasten to add I don't think is at all unusual that this is the case, and often approaches stay static until someone new shows up and starts questioning stuff. We've had this @ work in the past, and I've noted it ("Hungarian Notation").

You might remember a while ago I had a look at one of CFML's favourite tropes: evaluate() is bad not least of all because it's slow ('"evalulate() is really slow". Is it now?'): that trope ends up being mostly bullshit. So one can't just settle in and accept what once might have been true will thereafter always be true.

The trope I'm looking at here is that on CFMX7 performance penalties in creating transient objects were such that they weren't a viable coding construct. The only feasible way of using object instances were as singleton's created with ColdSpring during application start-up, and using those as services throughout the life of the application. Beans were an inviable notion, so they must not be used.

Object creation in older versions of CF certainly wasn't an efficient process, I agree with that.

Despite now being on ColdFusion 9 - which was noted as having huge performance improvements over earlier versions in the area of object creation - the policy on transient object usage was never revised. Partly due to never bothering, partly due to "well it's probably still bad: we have no proof that it isn't". The latter does possibly have a ring of truth to it, but if one scrutinises it, it's specious. When one stands back and considers all the ColdBox, FW/1, even Fusebox code out there that relies heavily on transient CFCs... and contemporary bespoke applications that are also written in an object oriented fashion... we'd definitely be hearing about it if there was still an issue. I cannot remember the last time I heard someone in the outside world make claims like these. An I pay a lot of attention to this sort of thing, as we know.

So, anyway, if proof that that's an obsolete idea is what's needed, proof is what will be provided. Herein lies the proof.

General testing approach

The general coding approach in this codebase is if one needs to pass a "person" throughout the system, one will pass individual arguments to methods: one for firstName, one for lastName, one for date of birth, etc. This is to avoid the overhead of creating a Person object with those properties, then passing that about the place. This makes for an awful lot of tightly-coupled boilerplate maintenance code all over the place to ensure the firstName, lastName and DOB arguments are valid. This is in contrast to just having a Person object, which takes care of itself. Once.

I have contrived a neutral example to demonstrate this. The scenario is that we have a service CFC (and this is a legitimate service, so is appropriately a singleton) which has two dependency CFCs (also singletons here) which do some work within the service. Say the service is a Transaction service, and it has a Logger and a DAO which does some work. The transaction service works on four "groups" of data (in modern code, each "group" would be an object), and passes subsets of these groups to the dependencies.

Object-oriented approach

Let's have a look at the OO-friendly version of this, as it'll make more sense:

// Service.cfc
component {

    public Service function init(required FirstDependency firstDependency, required SecondDependency secondDependency){
        variables.firstDependency    = arguments.firstDependency;
        variables.secondDependency    = arguments.secondDependency;

        return this;
    }

    public void function entryPoint(required Group1 group1, required Group2 group2, Group3 group3, Group4 group4){
        internalMethodRequiringAllOFGroup1(group1=arguments.group1);

        var methodArgs = {
            group1 = group1
        };
        if (structKeyExists(arguments, "group3")){
            methodArgs.group3 = arguments.group3;
        }else{
            methodArgs.group3 = new Group3();
        }
        internalMethodRequiringAllOFGroup1AndGroup3(argumentCollection=methodArgs);

        variables.firstDependency.methodRequiringGroups1And2(group1=arguments.group1, group2=arguments.group2);

        methodArgs.group2 = arguments.group2;
        if (structKeyExists(arguments, "group4")){
            methodArgs.group4 = arguments.group4;
        }else{
            methodArgs.group4 = new Group4();
        }
        variables.secondDependency.methodRequiringAllArgs(argumentCollection=methodArgs);
    }

    private void function internalMethodRequiringAllOFGroup1(required Group1 group1){
    }

    private void function internalMethodRequiringAllOFGroup1AndGroup3(required Group1 group1, required Group3 group3){
    }

}

Here we have our Service.cfc which is:

  • init()-ed with two dependencies. The dependencies are purely there to mimic how this service operates in the actual code base; they're not intrinsic to the issue.
  • has an entryPoint() method which is the public interface of the service. It takes four objects as arguments; two required, two optional.
  • the code within entryPoint() passes various subsets of arguments through to various of its own internal methods, as well as methods of the dependencies.
  • Some arguments are optional, others are required. There's logic to populate the argument collections passed to these methods, based on whether optional arguments have actually been passed in to entryPoint().
Standard stuff.

Just FYI, my dependencies are very sparse:

// FirstDependency.cfc
component {

    public void function methodRequiringGroups1And2(required Group1 group1, required Group2 group2){
    }

}

// SecondDependency.cfc
component {

    public void function methodRequiringAllArgs(required Group1 group1, required Group2 group2, required Group3 group3, required Group4 group4){
    }
}

And the Group CFCs are likewise minimal:

// Group1.cfc
component accessors=true {
    property property1;
    property property2;
    property property3;
    property property4;

    Group1 function init(required property1, required property2, required property3, required property4){
        setProperty1(arguments.property1);
        setProperty2(arguments.property2);
        setProperty3(arguments.property3);
        setProperty4(arguments.property4);
        return this;
    }
    
}

All are variations on the same theme, just some have defaults on their optional args, eg:

// Group3.cfc
component accessors=true {
    property property1;
    property property2;
    property property3;
    property property4;

    Group3 function init(property1="property1default", property2="property2default", property3="property3default", property4="property4default"){
        setProperty1(arguments.property1);
        setProperty2(arguments.property2);
        setProperty3(arguments.property3);
        setProperty4(arguments.property4);
        return this;
    }
    
}

The code for the dependencies and the Group objects is not really that important to the testing I'm doing here. They merely need to exist as I need to call / create them.

But that's using a degree of object orientation we're not afforded in this code base. We need to look at a more procedural approach ("procedural" might not be the best term here, but I mean "not very OO").

Procedural approach

Now, I'm still using CFCs for the service and the dependencies here, but no transients for the groupings of arguments. So it's semi OO, semi-procedural. And is a bit grim.

Here's the procedural version of Service.cfc. It is functionally the same as the OO version above. Take a deep breath.

// Service.cfc
component {

    public Service function init(required FirstDependency firstDependency, required SecondDependency secondDependency){
        variables.firstDependency    = arguments.firstDependency;
        variables.secondDependency    = arguments.secondDependency;

        return this;
    }

    public void function entryPoint(
        required string grouping1Arg1,
        required string grouping1Arg2,
        required string grouping1Arg3,
        required string grouping1Arg4,
        required string grouping2Arg1,
        required string grouping2Arg2,
        required string grouping2Arg3,
        required string grouping2Arg4,
        string grouping3Arg1,
        string grouping3Arg2,
        string grouping3Arg3,
        string grouping3Arg4,
        string grouping4Arg1,
        string grouping4Arg2,
        string grouping4Arg3,
        string grouping4Arg4
    ){
        internalMethodRequiringAllOFGroup1(
            grouping1Arg1 = grouping1Arg1,
            grouping1Arg2 = grouping1Arg2,
            grouping1Arg3 = grouping1Arg3,
            grouping1Arg4 = grouping1Arg4
        );

        var methodArgs = {
            grouping1Arg1 = grouping1Arg1,
            grouping1Arg2 = grouping1Arg2,
            grouping1Arg3 = grouping1Arg3,
            grouping1Arg4 = grouping1Arg4
        };
        if (structKeyExists(arguments, "grouping3Arg1")){
            methodArgs.grouping3Arg1 = arguments.grouping3Arg1;
        }else{
            methodArgs.grouping3Arg1 = "grouping3Arg1Default";
        }
        if (structKeyExists(arguments, "grouping3Arg2")){
            methodArgs.grouping3Arg2 = arguments.grouping3Arg2;
        }else{
            methodArgs.grouping3Arg2 = "grouping3Arg2Default";
        }
        if (structKeyExists(arguments, "grouping3Arg3")){
            methodArgs.grouping3Arg3 = arguments.grouping3Arg3;
        }else{
            methodArgs.grouping3Arg3 = "grouping3Arg3Default";
        }
        if (structKeyExists(arguments, "grouping3Arg4")){
            methodArgs.grouping3Arg4 = arguments.grouping3Arg4;
        }else{
            methodArgs.grouping3Arg4 = "grouping3Arg4Default";
        }
        internalMethodRequiringAllOFGroup1AndGroup3(argumentCollection=methodArgs);

        variables.firstDependency.methodRequiringGroups1And2(
            grouping1Arg1 = grouping1Arg1,
            grouping1Arg2 = grouping1Arg2,
            grouping1Arg3 = grouping1Arg3,
            grouping1Arg4 = grouping1Arg4,
            grouping2Arg1 = grouping2Arg1,
            grouping2Arg2 = grouping2Arg2,
            grouping2Arg3 = grouping2Arg3,
            grouping2Arg4 = grouping2Arg4
        );

        methodArgs.grouping2Arg1 = grouping2Arg1;
        methodArgs.grouping2Arg2 = grouping2Arg2;
        methodArgs.grouping2Arg3 = grouping2Arg3;
        methodArgs.grouping2Arg4 = grouping2Arg4;
        if (structKeyExists(arguments, "grouping4Arg1")){
            methodArgs.grouping4Arg1 = arguments.grouping4Arg1;
        }else{
            methodArgs.grouping4Arg1 = "grouping4Arg1Default";
        }
        if (structKeyExists(arguments, "grouping4Arg2")){
            methodArgs.grouping4Arg2 = arguments.grouping4Arg2;
        }else{
            methodArgs.grouping4Arg2 = "grouping4Arg2Default";
        }
        if (structKeyExists(arguments, "grouping4Arg3")){
            methodArgs.grouping4Arg3 = arguments.grouping4Arg3;
        }else{
            methodArgs.grouping4Arg3 = "grouping4Arg3Default";
        }
        if (structKeyExists(arguments, "grouping4Arg4")){
            methodArgs.grouping4Arg4 = arguments.grouping4Arg4;
        }else{
            methodArgs.grouping4Arg4 = "grouping4Arg4Default";
        }
        variables.secondDependency.methodRequiringAllArgs(argumentCollection=methodArgs);
    }

    private void function internalMethodRequiringAllOFGroup1(
        required string grouping1Arg1,
        required string grouping1Arg2,
        required string grouping1Arg3,
        required string grouping1Arg4
    ){

    }

    private void function internalMethodRequiringAllOFGroup1AndGroup3(
        required string grouping1Arg1,
        required string grouping1Arg2,
        required string grouping1Arg3,
        required string grouping1Arg4,
        required string grouping3Arg1,
        required string grouping3Arg2,
        required string grouping3Arg3,
        required string grouping3Arg4
    ){

    }

}

BTW, to make matters worse... all the actual code requires tags for the function and argument definitions. Don't ask.

So that's a horrific nightmare of copy and pasted boilerplate code, and the service intrinsically needs to know all about each property being used (and it shouldn't), instead of foisting some of it off onto objects to take care of themselves.

This also bleeds out into the dependencies too:

// SecondDependency.cfc
component {

    public void function methodRequiringAllArgs(
        required string grouping1Arg1,
        required string grouping1Arg2,
        required string grouping1Arg3,
        required string grouping1Arg4,
        required string grouping2Arg1,
        required string grouping2Arg2,
        required string grouping2Arg3,
        required string grouping2Arg4,
        required string grouping3Arg1,
        required string grouping3Arg2,
        required string grouping3Arg3,
        required string grouping3Arg4,
        required string grouping4Arg1,
        required string grouping4Arg2,
        required string grouping4Arg3,
        required string grouping4Arg4
    ){
    }
}

We seriously want to stop having to write code like this. Also bear in mind that as awful as this lot is, it's a very pared down functionless example. Real-world examples of this result in methods that are hundreds of lines long, duplicated code, and are a maintenance nightmare.

All for the sake of a perceived performance penalty in using transient objects. Well I say "perceived", but it's not been put to the test yet, so there's no informed perception, merely a harkening back to where CFMX (and coding practices) was at 5-10yrs ago.

As an intermediary compromise, I also contrived code which uses structs to group the properties together, instead of full-blow objects. I'll spare you the code, but you can see it on Github.

Pummelling

Right. So I've decided to test this out, and see what's what. Over the last few weeks I've been fine-tuning a test environment which takes examples of both these approaches, and gives them a pummelling via JMeter. This is a variation on that testing approach I dislike: amplifying a test so that the different tests actually show some difference. Because on a request  by request basis, there is no meaningful difference between any approach here. That should actually be enough of a test, as this isn't a very busy site either. Well: the site as a whole is busy, but it's really over-catered for in the "tin" department, so for a given CF instance, "busy" is a few requests per second ("normal" is around one request per second per CF instance). So: not busy.

I don't know the best way to describe a JMeter test, but here's some screen caps which I'll comment on:



This will make more sense if you know the file structure of the tests, which is (again) on Github. The JMeter file is there too: Test Plan multiple.jmx.

Basically I've parameterised as much as I can, so I can simply change some values and paths and stuff on the first page, and run the test and it "just works".

JMeter calls a file multiple.cfm:

<cfparam name="URL.iterations" type="integer">
<cfloop index="i" from="1" to="#URL.iterations#">
    <cfinclude template="all.cfm">
</cfloop>

Which in turn calls all.cfm (this is a legacy of the original testing I was doing: I could probably simplify it):

<cfscript>
//all.cfm
application.service.entryPoint(
    grouping1Arg1="grouping1Arg1value",
    grouping1Arg2="grouping1Arg2value",
    grouping1Arg3="grouping1Arg3value",
    grouping1Arg4="grouping1Arg4value",
    grouping2Arg1="grouping2Arg1value",
    grouping2Arg2="grouping2Arg2value",
    grouping2Arg3="grouping2Arg3value",
    grouping2Arg4="grouping2Arg4value",
    grouping3Arg1="grouping3Arg1value",
    grouping3Arg2="grouping3Arg2value",
    grouping3Arg3="grouping3Arg3value",
    grouping3Arg4="grouping3Arg4value",
    grouping4Arg1="grouping4Arg1value",
    grouping4Arg2="grouping4Arg2value",
    grouping4Arg3="grouping4Arg3value",
    grouping4Arg4="grouping4Arg4value"
);
</cfscript>

That was the procedural version (scratch / blogExamples / cfml / objectInstantiation / current / individualargs / all.cfm ), as opposed to the OO version (scratch / blogExamples / cfml / objectInstantiation / current / transients / all.cfm):

<cfscript>
//all.cfm

group1 = new Group1(
    property1 = "property1value",
    property2 = "property2value",
    property3 = "property3value",
    property4 = "property4value"
);

group2 = new Group2(
    property1 = "property1value",
    property2 = "property2value",
    property3 = "property3value",
    property4 = "property4value"
);

group3 = new Group3(
    property1 = "property1value",
    property2 = "property2value",
    property3 = "property3value",
    property4 = "property4value"
);

group4 = new Group4(
    property1 = "property1value",
    property2 = "property2value",
    property3 = "property3value",
    property4 = "property4value"
);


application.service.entryPoint(
    group1=group1,
    group2=group2,
    group3=group3,
    group4=group4
);
</cfscript>

They're both doing the same thing.

The last piece of the puzzle is https://github.com/daccfml/scratch/blob/master/blogExamples/cfml/objectInstantiation/current/Application.cfc:

// Application.cfc
component {

    variables.baseSubdir = listLast(getDirectoryFromPath(getBaseTemplatePath()), "\/");

    this.name = "#variables.baseSubdir#26";
    this.applicationTimeout = createTimespan(0,0,0,30);


    function onApplicationStart(){
        application.firstDependency = new "#baseSubdir#.FirstDependency"();
        application.secondDependency = new "#baseSubdir#.SecondDependency"();

        application.service = new "#baseSubdir#.Service"(
            firstDependency = application.firstDependency,
            secondDependency = application.secondDependency
        );

        application.runtime = createObject("java","java.lang.Runtime").getRuntime();
        application.counter = createObject("java", "java.util.concurrent.atomic.AtomicInteger").init();
    }

    function onRequest(){
        var startTime    = getTickCount();
        var counter        = application.counter.incrementAndGet();
        var slowWarning    =  "";
        include arguments[1];
        var executionTime = getTickCount()-startTime;
        writeOutput("Execution time: #executionTime#ms");

        if (executionTime > URL.slowRequestThreshold){
            slowWarning = "SLOW REQUEST (above #URL.slowRequestThreshold#ms)";
        }
        writeLog(file="#this.name#", text="#executionTime#;#getFreeAllocatedMemory()#;#counter#;#slowWarning#");
    }

    function getFreeAllocatedMemory(){
        return application.runtime.freeMemory() / 1024^2;
    }

}

In this we diff the start and end tick counts either side of the requested file being called, and log that and some other metrics to file. So the requested file just does the work, I measure it's performance via onRequest().

And from there I batter it to the tune of say creating 50 objects per request, having 20 simultaneous threads hitting the code at once, and do that 500-odd times (the figures on the copy of the JMeter job I displayed do not match that: I was doing various different combinations of threads / objects / iterations to check if there were any behavioural twitches).

Control

As a baseline, I re-implemented that code to be runnable on CFMX7, and tested that first. In theory I should see show-stopping performance differences between using individual arguments as opposed to objects. The process of back-porting that code is a blog article in itself (well: it will be). The CFMX7 version of the code is also on GitHub.

Having blatted my CFMX7 instance (yes, I have a CFMX7 instance!), these are typical log entries:

"Information","web-0","05/30/14","14:55:23","INDIVIDUALARGS23","247;460.058326721;2538;SLOW REQUEST (above 200ms)"
"Information","web-49","05/30/14","14:55:23","INDIVIDUALARGS23","232;459.149017334;2539;SLOW REQUEST (above 200ms)"
"Information","web-55","05/30/14","14:55:23","INDIVIDUALARGS23","496;458.562438965;2540;SLOW REQUEST (above 200ms)"
"Information","web-47","05/30/14","14:55:23","INDIVIDUALARGS23","436;457.429603577;2541;SLOW REQUEST (above 200ms)"
"Information","web-33","05/30/14","14:55:23","INDIVIDUALARGS23","296;457.422554016;2542;SLOW REQUEST (above 200ms)"
"Information","web-43","05/30/14","14:55:23","INDIVIDUALARGS23","492;456.37878418;2543;SLOW REQUEST (above 200ms)"
"Information","web-8","05/30/14","14:55:23","INDIVIDUALARGS23","382;455.869132996;2544;SLOW REQUEST (above 200ms)"
"Information","web-6","05/30/14","14:55:23","INDIVIDUALARGS23","221;454.103164673;2545;SLOW REQUEST (above 200ms)"
"Information","web-50","05/30/14","14:55:23","INDIVIDUALARGS23","464;453.607002258;2546;SLOW REQUEST (above 200ms)"
"Information","web-54","05/30/14","14:55:23","INDIVIDUALARGS23","266;453.36782074;2547;SLOW REQUEST (above 200ms)"

Most of it's bumpf, but the important column is the first one, with the execution time of each iteration: 200-500ms each. This test was calling entryPoint() 50 times per request. Summarising the whole lot, I get this:

StatFigure
Basic
min63
mean334
max952

Percentile
10%189
20%233
30%265
40%295
50%325
60%354
70%389
80%429
90%489

(pardon my border=1 table design)

Anyway, if we discard the outliers, it's running between about 200-500ms.

What about for transients? Here's the random sample:

"Information","web-34","05/30/14","15:01:00","TRANSIENTS23","982;432.853713989;2448;SLOW REQUEST (above 200ms)"
"Information","web-28","05/30/14","15:01:00","TRANSIENTS23","1080;480.68447876;2449;SLOW REQUEST (above 200ms)"
"Information","web-40","05/30/14","15:01:00","TRANSIENTS23","1138;479.821510315;2450;SLOW REQUEST (above 200ms)"
"Information","web-44","05/30/14","15:01:01","TRANSIENTS23","940;465.780288696;2451;SLOW REQUEST (above 200ms)"
"Information","web-47","05/30/14","15:01:01","TRANSIENTS23","1014;464.901557922;2452;SLOW REQUEST (above 200ms)"
"Information","web-10","05/30/14","15:01:01","TRANSIENTS23","943;459.676376343;2453;SLOW REQUEST (above 200ms)"
"Information","web-9","05/30/14","15:01:01","TRANSIENTS23","818;458.663352966;2454;SLOW REQUEST (above 200ms)"
"Information","web-24","05/30/14","15:01:01","TRANSIENTS23","1000;452.846557617;2455;SLOW REQUEST (above 200ms)"
"Information","web-7","05/30/14","15:01:01","TRANSIENTS23","831;451.701271057;2456;SLOW REQUEST (above 200ms)"
"Information","web-32","05/30/14","15:01:01","TRANSIENTS23","938;447.906402588;2457;SLOW REQUEST (above 200ms)"

So quite a lot bigger! The breakdown is:

StatFigure
Basic
min99
mean974
max1402

Percentile
10%852
20%894
30%925
40%949
50%973
60%998
70%1024
80%1059
90%1107

So this is about 2-5x slower (give or take, and depending on which decile one is looking at). And for an already-not-great performance, I think there's grounds here for saying "that's actually a problem". I s'pose.

Test

OK, so that's a baseline. Let's look at how ColdFusion 9 compares. The CF9 instance is running on the same machine, but is running a different JVM: CF9's on 1.7.0_51, and CFMX7 is running on an old 1.4.2.05. CFMX7 is not supported on anything beyond 1.4... it might run on 1.7, but that's not what the client would have been running when they drew their conclusions, so I want to stick with what was real-world at the time. So this means that even before any ColdFusion improvements are considered, the underlying Java system is gonna be an awful lot better. Anyway, here's the breakdown of the individualArgs test on CF9 (I'll spare you the log file extracts):

StatFigure
Basic
min3
mean11
max190

Percentile
10%4
20%4
30%5
40%5
50%5
60%5
70%7
80%11
90%22

Wow. OK. Well I guess it's not surprise that CF9 shits all over CFMX7. But that's a huge improvement. Isn't that like 20-50x faster than the equivalent code on CFMX7?

But what about the main point of the exercise here: using transients instead...

StatFigure
Basic
min4
mean31
max9954

Percentile
10%8
20%8
30%9
40%11
50%18
60%25
70%33
80%44
90%66

So... interesting: transients are still 2-5x slower than individual args. And there's some shocking outliers there: look at that 10sec request! But it's well up in the top 1% of requests: here's the percentiles above 90%:

Percentile
91%70
92%73
93%78
94%82
95%89
96%98
97%108
98%126
99%144

In fact the next highest is 265ms (remember with the individualArgs test, it was 190ms... not much different).

Conclusion

Well: being completely dogmatic about it, transients are comparatively just as much slower than individual arguments as they have always been. However being pragmatic about it... at the performance we're getting here, I really don't think it's in the realms of needing to give a shit. I'd much rather be able to move the codebase into this decade than have to wring my hands over what amounts to be around 10ms per request under very very heavy load.

Additional tests

Railo

Here's the summary for Railo 4.2. Firstly individualArgs:

StatFigure
Basic
min1
mean26
max227

Percentile
10%2
20%5
30%13
40%20
50%25
60%29
70%34
80%40
90%50

Interesting. This is significantly slower than ColdFusion 9. I was only running on Railo Express, but I was using the same JVM and the same JVM settings, so that should be a like-for-like test?

And for transients:

StatFigure
Basic
min3
mean63
max493

Percentile
10%16
20%38
30%49
40%56
50%61
60%67
70%74
80%83
90%99

Again: significantly slower than ColdFusion 9. I am let down by this. And I'm sure I'll get some feedback from Gert and/or Micha on this!

ColdFusion 11


individualArgs

StatFigure
Basic
min3
mean24
max638

Percentile
10%5
20%6
30%7
40%9
50%12
60%16
70%21
80%31
90%57

And this is slower than CF9 too! Huh?! The only think I can think of is that I'm running my CF9 instance in a vanilla install of Tomcat, rather than JRun (and not Adobe's hacked-about version of Tomcat). I'm running CF9 on Tomcat here because - you might remember - CF9 won't install on Windows 8.1 ("ColdFusion 9 on Windows 8")

And, lastly, the transients test:

StatFigure
Basic
min6
mean44
max590

Percentile
10%10
20%11
30%17
40%24
50%29
60%37
70%49
80%67
90%95

Still a bit slower than my CF9 instance!

I'm actually a bit bemused by those Railo and ColdFusion 11 tests, but that's something for another day (disclosure: I am completely over this sort of testing, so I will not be investigating this!). But the bottom line is I simply don't think this aversion to transients that my client has has any merit. It used to have merit. It does not now. And whichever way the code performs... the code they are forcing themselves to use has very little merit indeed, given the reason for doing it!

Next stop... the same client's aversion to using custom tags in views, due to them having an insurmountable memory leak. Based on my own experience here: no they don't. I will demonstrate this soon (unless I can just get them to concede they're basing their decisions on outdated information, as they have been here. I do actually know what I'm talking about, sometimes, after all).

Right. I deserve a pint after that lot. Across the road for a Guinness (I'm in Galway today) it is.

Sláinte.

--
Adam