diff --git a/box.json b/box.json
index 7cb8a1c3c..c08ee5260 100644
--- a/box.json
+++ b/box.json
@@ -75,6 +75,12 @@
"log:lucee":"server log coldbox-lucee@5 --follow",
"log:boxlang":"server log coldbox-boxlang-cfml@1 --follow",
"log:boxlangprime":"server log coldbox-boxlang@1 --follow",
- "log:adobe":"server log coldbox-adobe@2025 --follow"
+ "log:adobe":"server log coldbox-adobe@2025 --follow",
+ "perf:run":"task run tests/perf-harness/PerformanceSuite.cfc",
+ "perf:run:quick":"task run tests/perf-harness/PerformanceSuite.cfc engines=boxlang-cfml iterations=10 warmup=3 coldStart=false",
+ "perf:be":"task run tests/perf-harness/PerformanceSuite.cfc versions=be",
+ "perf:stable":"task run tests/perf-harness/PerformanceSuite.cfc versions=stable",
+ "perf:lucee":"task run tests/perf-harness/PerformanceSuite.cfc engines=lucee-7",
+ "perf:adobe":"task run tests/perf-harness/PerformanceSuite.cfc engines=adobe-2025"
}
}
diff --git a/tests/perf-harness/PerformanceSuite.cfc b/tests/perf-harness/PerformanceSuite.cfc
new file mode 100644
index 000000000..549f359de
--- /dev/null
+++ b/tests/perf-harness/PerformanceSuite.cfc
@@ -0,0 +1,847 @@
+/**
+ * ColdBox Performance Analysis Suite
+ *
+ * Compares bleeding-edge (BE) vs stable ColdBox 8.1 across four CFML engines:
+ * BoxLang, BoxLang-CFML, Adobe CF 2025, Lucee 7
+ *
+ * Measures:
+ * • Engine cold start (server restart + no bytecode cache)
+ * • App bootstrap time (ColdBox onApplicationStart)
+ * • Warm request latency per scenario (min/avg/P95/P99)
+ * • Sequential throughput (RPS)
+ *
+ * Usage (from repo root):
+ * box task run tests/perf-harness/PerformanceSuite.cfc
+ * box task run tests/perf-harness/PerformanceSuite.cfc engines=boxlang-cfml
+ * box task run tests/perf-harness/PerformanceSuite.cfc versions=be iterations=100 coldStart=false
+ * box run-script perf:run
+ * box run-script perf:run:quick
+ */
+component {
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // CONFIGURATION
+ // ══════════════════════════════════════════════════════════════════════════
+
+ variables.TASK_DIR = getDirectoryFromPath( getCurrentTemplatePath() )
+ variables.REPO_ROOT = reReplaceNoCase( variables.TASK_DIR, "tests[/\\]perf-harness[/\\]", "" )
+ variables.BASE_URL = "http://localhost:8599"
+ variables.REPORT_DIR = variables.TASK_DIR & "reports/"
+
+ variables.ENGINES = {
+ "boxlang" : {
+ name : "BoxLang",
+ serverConfig : variables.TASK_DIR & "server-perf-boxlang.json",
+ serverName : "coldbox-perf-boxlang",
+ cacheDir : variables.REPO_ROOT & ".engine/boxlang/",
+ cacheDirs : [ ".boxlang/classes", "home" ]
+ },
+ "boxlang-cfml" : {
+ name : "BoxLang CFML",
+ serverConfig : variables.TASK_DIR & "server-perf-boxlang-cfml.json",
+ serverName : "coldbox-perf-boxlang-cfml",
+ cacheDir : variables.REPO_ROOT & ".engine/boxlang-cfml-1/",
+ cacheDirs : [ ".boxlang/classes", "home" ]
+ },
+ "adobe-2025" : {
+ name : "Adobe CF 2025",
+ serverConfig : variables.TASK_DIR & "server-perf-adobe2025.json",
+ serverName : "coldbox-perf-adobe2025",
+ cacheDir : variables.REPO_ROOT & ".engine/adobe2025/",
+ cacheDirs : [ "WEB-INF/cfclasses" ]
+ },
+ "lucee-7" : {
+ name : "Lucee 7",
+ serverConfig : variables.TASK_DIR & "server-perf-lucee7.json",
+ serverName : "coldbox-perf-lucee7",
+ cacheDir : variables.REPO_ROOT & ".engine/lucee7/",
+ cacheDirs : [ "WEB-INF/lucee/web/cfclasses", "WEB-INF/lucee/web/tmp" ]
+ }
+ }
+
+ variables.SCENARIOS = [
+ {
+ id : "health",
+ name : "Health Check",
+ description : "Minimal ColdBox lifecycle — no DI, no view, text response",
+ bePath : "/tests/perf-harness/be-app/index.cfm?event=Main.health",
+ stablePath : "/tests/perf-harness/stable-app/index.cfm?event=Main.health"
+ },
+ {
+ id : "view",
+ name : "Simple View",
+ description : "View rendering + layout pipeline",
+ bePath : "/tests/perf-harness/be-app/index.cfm?event=Main.index",
+ stablePath : "/tests/perf-harness/stable-app/index.cfm?event=Main.index"
+ },
+ {
+ id : "api",
+ name : "JSON API",
+ description : "WireBox DI + JSON serialization via renderData",
+ bePath : "/tests/perf-harness/be-app/index.cfm?event=Api.list",
+ stablePath : "/tests/perf-harness/stable-app/index.cfm?event=Api.list"
+ },
+ {
+ id : "complex",
+ name : "Complex View",
+ description : "Multiple model injections + view with data loops",
+ bePath : "/tests/perf-harness/be-app/index.cfm?event=Main.complex",
+ stablePath : "/tests/perf-harness/stable-app/index.cfm?event=Main.complex"
+ },
+ {
+ id : "module",
+ name : "Module Request",
+ description : "Full HMVC module routing + module-scoped DI",
+ bePath : "/tests/perf-harness/be-app/index.cfm?event=perf-module%3AItems.index",
+ stablePath : "/tests/perf-harness/stable-app/index.cfm?event=perf-module%3AItems.index"
+ }
+ ]
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // ENTRY POINT
+ // ══════════════════════════════════════════════════════════════════════════
+
+ /**
+ * Run the full performance analysis suite.
+ *
+ * @engines Comma-separated engine IDs or "all". Options: boxlang, boxlang-cfml, adobe-2025, lucee-7
+ * @versions Comma-separated versions to test. Options: be, stable
+ * @iterations Number of warm requests per scenario for latency measurement
+ * @warmup Number of warmup requests to discard before measuring
+ * @throughputSecs Seconds to run the sequential throughput test per version
+ * @coldStart Whether to measure cold start (requires server restart)
+ * @generateReport Whether to produce HTML + Markdown reports in reports/
+ */
+ function run(
+ string engines = "all",
+ string versions = "be,stable",
+ numeric iterations = 50,
+ numeric warmup = 10,
+ numeric throughputSecs = 10,
+ boolean coldStart = true,
+ boolean generateReport = true
+ ){
+ log( "" )
+ log( "╔═══════════════════════════════════════════════════════════════╗" )
+ log( "║ ColdBox Performance Analysis Suite ║" )
+ log( "╚═══════════════════════════════════════════════════════════════╝" )
+ log( "" )
+ log( " Repo root : #variables.REPO_ROOT#" )
+ log( " Engines : #arguments.engines#" )
+ log( " Versions : #arguments.versions#" )
+ log( " Iterations : #arguments.iterations#" )
+ log( " Warmup : #arguments.warmup#" )
+ log( " Cold start : #arguments.coldStart#" )
+ log( "" )
+
+ var engineList = parseEngineList( arguments.engines )
+ var versionList = listToArray( arguments.versions )
+
+ // Ensure stable ColdBox is installed before tests begin
+ if( versionList.findNoCase( "stable" ) ){
+ ensureStableColdBox()
+ }
+
+ var results = {
+ generated : now(),
+ iterations : arguments.iterations,
+ warmup : arguments.warmup,
+ throughputSecs: arguments.throughputSecs,
+ coldStartRun : arguments.coldStart,
+ engines : {}
+ }
+
+ // ── Main test loop ────────────────────────────────────────────────────
+ for( var engineId in engineList ){
+ var engine = variables.ENGINES[ engineId ]
+ results.engines[ engineId ] = { name: engine.name, versions: {} }
+
+ log( "" )
+ log( "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
+ log( " Engine: #engine.name#" )
+ log( "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
+
+ for( var version in versionList ){
+ log( "" )
+ log( " ▶ Version: #version#" )
+
+ var vData = {
+ version : version,
+ coldStart : {},
+ appBootstrap : {},
+ scenarios : {},
+ throughput : {}
+ }
+
+ // 1. Cold start — stop server, wipe class cache, restart, time first response
+ if( arguments.coldStart ){
+ log( " → Measuring cold start (server restart + cache clear)..." )
+ vData.coldStart = measureColdStart( engine, version )
+ log( " Cold start: #vData.coldStart.totalMs#ms (server up in #vData.coldStart.serverStartMs#ms, first response in #vData.coldStart.firstResponseMs#ms)" )
+ }
+
+ // 2. Ensure server is running (may already be up from cold start)
+ if( !arguments.coldStart ){
+ log( " → Starting server..." )
+ startServer( engine )
+ var healthUrl = variables.BASE_URL & ( version == "be" ? variables.SCENARIOS[ 1 ].bePath : variables.SCENARIOS[ 1 ].stablePath )
+ if( !waitForServer( healthUrl, 120 ) ){
+ log( " ✗ Server did not start in time — skipping #engine.name# #version#" )
+ continue
+ }
+ }
+
+ // 3. App bootstrap (re-init ColdBox, time first request)
+ log( " → Measuring app bootstrap time..." )
+ vData.appBootstrap = measureAppBootstrap( version )
+ log( " Bootstrap: #vData.appBootstrap.ms#ms" )
+
+ // 4. Warmup
+ log( " → Warming up (#arguments.warmup# requests)..." )
+ var warmupUrl = variables.BASE_URL & ( version == "be" ? variables.SCENARIOS[ 1 ].bePath : variables.SCENARIOS[ 1 ].stablePath )
+ for( var w = 1; w <= arguments.warmup; w++ ){
+ try { cfhttp( url=warmupUrl, method="GET", timeout=15, result="wr" ) } catch( any e ){}
+ }
+
+ // 5. Per-scenario latency
+ for( var scenario in variables.SCENARIOS ){
+ var scenarioUrl = variables.BASE_URL & ( version == "be" ? scenario.bePath : scenario.stablePath )
+ log( " → Scenario [#scenario.name#] (#arguments.iterations# req)..." )
+ vData.scenarios[ scenario.id ] = measureScenario( scenarioUrl, arguments.iterations )
+ var s = vData.scenarios[ scenario.id ]
+ log( " avg=#s.avg#ms p95=#s.p95#ms p99=#s.p99#ms errors=#s.errors#" )
+ }
+
+ // 6. Throughput (sequential)
+ var tUrl = variables.BASE_URL & ( version == "be" ? variables.SCENARIOS[ 1 ].bePath : variables.SCENARIOS[ 1 ].stablePath )
+ log( " → Throughput (#arguments.throughputSecs#s sequential)..." )
+ vData.throughput = measureThroughput( tUrl, arguments.throughputSecs )
+ log( " #vData.throughput.rps# RPS (#vData.throughput.totalRequests# requests)" )
+
+ results.engines[ engineId ].versions[ version ] = vData
+
+ // Stop server after each version test to avoid port conflicts
+ log( " → Stopping server..." )
+ stopServer( engine )
+ sleep( 2000 )
+ }
+ }
+
+ // ── Generate reports ──────────────────────────────────────────────────
+ if( arguments.generateReport ){
+ log( "" )
+ log( " Generating reports..." )
+ var ts = dateTimeFormat( results.generated, "yyyymmdd_HHnnss" )
+ var mdPath = variables.REPORT_DIR & "perf-report-#ts#.md"
+ var htmlPath = variables.REPORT_DIR & "perf-report-#ts#.html"
+ generateMarkdownReport( results, mdPath )
+ generateHTMLReport( results, htmlPath )
+ log( " ✓ Markdown : #mdPath#" )
+ log( " ✓ HTML : #htmlPath#" )
+ }
+
+ log( "" )
+ log( " ✓ Performance analysis complete!" )
+ log( "" )
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // SETUP
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private array function parseEngineList( required string engines ){
+ if( arguments.engines == "all" ) return variables.ENGINES.keyArray()
+ return listToArray( arguments.engines )
+ }
+
+ private void function ensureStableColdBox(){
+ var stableDir = variables.TASK_DIR & "stable-app/"
+ var coldboxDir = stableDir & "coldbox/"
+ if( directoryExists( coldboxDir ) ){
+ log( " ✓ Stable ColdBox already installed at #coldboxDir#" )
+ return
+ }
+ log( " Installing ColdBox stable 8.1.x into stable-app/..." )
+ try {
+ command( "cd '#stableDir#'" ).run()
+ command( "install" ).run()
+ command( "cd '#variables.REPO_ROOT#'" ).run()
+ log( " ✓ Stable ColdBox installed." )
+ } catch( any e ){
+ log( " ✗ Could not install stable ColdBox: #e.message#. Skipping stable version." )
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // SERVER MANAGEMENT
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private void function startServer( required struct engine ){
+ try {
+ command( "server start" )
+ .params( serverConfigFile=arguments.engine.serverConfig )
+ .flag( "force" )
+ .run()
+ } catch( any e ){
+ log( " ✗ Server start error: #e.message#" )
+ }
+ }
+
+ private void function stopServer( required struct engine ){
+ try {
+ command( "server stop" )
+ .params( name=arguments.engine.serverName )
+ .flag( "force" )
+ .run()
+ } catch( any e ){
+ // Server may not be running — ignore
+ }
+ }
+
+ private boolean function waitForServer( required string healthUrl, numeric timeout=120 ){
+ var deadline = getTickCount() + ( arguments.timeout * 1000 )
+ while( getTickCount() < deadline ){
+ try {
+ cfhttp( url=arguments.healthUrl, method="GET", timeout=5, result="probe" )
+ if( probe.statusCode contains "200" ) return true
+ } catch( any e ){}
+ sleep( 2000 )
+ }
+ return false
+ }
+
+ private void function clearEngineCache( required struct engine ){
+ for( var subDir in arguments.engine.cacheDirs ){
+ var fullPath = arguments.engine.cacheDir & subDir & "/"
+ if( directoryExists( fullPath ) ){
+ try {
+ directoryDelete( fullPath, true )
+ log( " Cleared: #fullPath#" )
+ } catch( any e ){
+ log( " Warning — could not clear #fullPath#: #e.message#" )
+ }
+ }
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // MEASUREMENT
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private struct function measureColdStart( required struct engine, required string version ){
+ var healthPath = ( arguments.version == "be" ) ? variables.SCENARIOS[ 1 ].bePath : variables.SCENARIOS[ 1 ].stablePath
+ var healthUrl = variables.BASE_URL & healthPath
+
+ // Stop any running instance
+ stopServer( arguments.engine )
+ sleep( 3000 )
+
+ // Wipe compiled class caches
+ clearEngineCache( arguments.engine )
+
+ // Start server and time until first response
+ var serverStart = getTickCount()
+ startServer( arguments.engine )
+
+ var serverReady = false
+ var firstResponseMs = 0
+ var deadline = getTickCount() + 180000 // 3 minute max
+
+ while( getTickCount() < deadline ){
+ try {
+ var reqStart = getTickCount()
+ cfhttp( url=healthUrl, method="GET", timeout=10, result="cr" )
+ if( cr.statusCode contains "200" ){
+ firstResponseMs = getTickCount() - reqStart
+ serverReady = true
+ break
+ }
+ } catch( any e ){}
+ sleep( 1000 )
+ }
+
+ var totalMs = getTickCount() - serverStart
+ var serverStartMs = totalMs - firstResponseMs
+
+ return {
+ success : serverReady,
+ totalMs : totalMs,
+ serverStartMs : serverStartMs,
+ firstResponseMs: firstResponseMs
+ }
+ }
+
+ private struct function measureAppBootstrap( required string version ){
+ var healthPath = ( arguments.version == "be" ) ? variables.SCENARIOS[ 1 ].bePath : variables.SCENARIOS[ 1 ].stablePath
+ var reinitUrl = variables.BASE_URL & healthPath & "&bsReinit=1"
+ var healthUrl = variables.BASE_URL & healthPath
+
+ // Trigger ColdBox re-initialization
+ try {
+ cfhttp( url=reinitUrl, method="GET", timeout=30, result="ri" )
+ } catch( any e ){}
+
+ sleep( 500 )
+
+ // Time the first post-reinit request (full bootstrap cost)
+ var start = getTickCount()
+ try {
+ cfhttp( url=healthUrl, method="GET", timeout=30, result="br" )
+ } catch( any e ){}
+
+ return { ms: getTickCount() - start }
+ }
+
+ private struct function measureScenario( required string url, required numeric count ){
+ var times = []
+ var errors = 0
+
+ for( var i = 1; i <= arguments.count; i++ ){
+ var start = getTickCount()
+ try {
+ cfhttp( url=arguments.url, method="GET", timeout=30, result="sr" )
+ if( !( sr.statusCode contains "200" ) ) errors++
+ } catch( any e ){
+ errors++
+ }
+ times.append( getTickCount() - start )
+ }
+
+ if( times.isEmpty() ){
+ return { count: 0, errors: errors, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0, errorPct: 100 }
+ }
+
+ var sorted = duplicate( times )
+ sorted.sort( "numeric" )
+ var total = 0
+ for( var t in times ) total += t
+
+ return {
+ count : arguments.count,
+ errors : errors,
+ errorPct : round( ( errors / arguments.count ) * 100 * 100 ) / 100,
+ min : sorted[ 1 ],
+ max : sorted[ sorted.len() ],
+ avg : round( total / times.len() ),
+ p50 : percentile( sorted, 50 ),
+ p95 : percentile( sorted, 95 ),
+ p99 : percentile( sorted, 99 )
+ }
+ }
+
+ // Sequential throughput — measures requests per second for a fixed duration
+ private struct function measureThroughput( required string url, required numeric durationSecs ){
+ var start = getTickCount()
+ var deadline = start + ( arguments.durationSecs * 1000 )
+ var requests = 0
+ var errors = 0
+
+ while( getTickCount() < deadline ){
+ try {
+ cfhttp( url=arguments.url, method="GET", timeout=10, result="tr" )
+ if( tr.statusCode contains "200" ) requests++
+ else errors++
+ } catch( any e ){
+ errors++
+ }
+ }
+
+ var elapsed = ( getTickCount() - start ) / 1000
+ return {
+ totalRequests : requests,
+ errors : errors,
+ durationSecs : elapsed,
+ rps : ( elapsed > 0 ) ? round( ( requests / elapsed ) * 100 ) / 100 : 0,
+ note : "Sequential single-threaded"
+ }
+ }
+
+ private numeric function percentile( required array sorted, required numeric p ){
+ if( arguments.sorted.isEmpty() ) return 0
+ var idx = max( 1, ceiling( arguments.sorted.len() * ( arguments.p / 100 ) ) )
+ return arguments.sorted[ min( idx, arguments.sorted.len() ) ]
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // REPORT GENERATION — MARKDOWN
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private void function generateMarkdownReport( required struct results, required string filePath ){
+ var md = []
+ var r = arguments.results
+ var ts = dateTimeFormat( r.generated, "yyyy-mm-dd HH:nn:ss" )
+
+ md.append( "# ColdBox Performance Analysis Report" )
+ md.append( "" )
+ md.append( "Generated: #ts# | Iterations: #r.iterations# | Warmup: #r.warmup# | Cold Start: #r.coldStartRun#" )
+ md.append( "" )
+
+ // ── Cold Start Table ──────────────────────────────────────────────────
+ if( r.coldStartRun ){
+ md.append( "## Engine Cold Start (First Request, No Bytecode Cache)" )
+ md.append( "" )
+ md.append( "| Engine | Version | Server Start (ms) | First Response (ms) | Total (ms) |" )
+ md.append( "|--------|---------|:-----------------:|:-------------------:|:----------:|" )
+ for( var engineId in r.engines ){
+ var eng = r.engines[ engineId ]
+ for( var ver in eng.versions ){
+ var vd = eng.versions[ ver ]
+ if( !vd.coldStart.isEmpty() && vd.coldStart.success ){
+ md.append( "| #eng.name# | #ver# | #vd.coldStart.serverStartMs# | #vd.coldStart.firstResponseMs# | #vd.coldStart.totalMs# |" )
+ }
+ }
+ }
+ md.append( "" )
+ }
+
+ // ── App Bootstrap Table ───────────────────────────────────────────────
+ md.append( "## ColdBox App Bootstrap Time (Re-init)" )
+ md.append( "" )
+ md.append( "| Engine | BE (ms) | Stable (ms) | Delta |" )
+ md.append( "|--------|:-------:|:-----------:|:-----:|" )
+ for( var engineId in r.engines ){
+ var eng = r.engines[ engineId ]
+ var beMs = eng.versions.keyExists( "be" ) ? eng.versions.be.appBootstrap.ms : "-"
+ var stMs = eng.versions.keyExists( "stable" ) ? eng.versions.stable.appBootstrap.ms : "-"
+ var delta = ( isNumeric( beMs ) && isNumeric( stMs ) && stMs > 0 ) ? formatDelta( beMs, stMs ) : "-"
+ md.append( "| #eng.name# | #beMs# | #stMs# | #delta# |" )
+ }
+ md.append( "" )
+
+ // ── Scenario Latency Tables ───────────────────────────────────────────
+ md.append( "## Warm Request Latency by Scenario" )
+ md.append( "" )
+ md.append( "> All times in milliseconds. Delta shows BE change vs Stable (negative = BE faster)." )
+ md.append( "" )
+
+ for( var scenario in variables.SCENARIOS ){
+ md.append( "### #scenario.name#" )
+ md.append( "" )
+ md.append( "_#scenario.description#_" )
+ md.append( "" )
+ md.append( "| Engine | Version | Min | Avg | P95 | P99 | Max | Errors |" )
+ md.append( "|--------|---------|:---:|:---:|:---:|:---:|:---:|:------:|" )
+
+ for( var engineId in r.engines ){
+ var eng = r.engines[ engineId ]
+ for( var ver in eng.versions ){
+ var vd = eng.versions[ ver ]
+ if( vd.scenarios.keyExists( scenario.id ) ){
+ var s = vd.scenarios[ scenario.id ]
+ md.append( "| #eng.name# | #ver# | #s.min# | #s.avg# | #s.p95# | #s.p99# | #s.max# | #s.errors# (#s.errorPct#%%) |" )
+ }
+ }
+ }
+
+ // Delta row (BE vs stable per engine)
+ for( var engineId in r.engines ){
+ var eng = r.engines[ engineId ]
+ if( eng.versions.keyExists( "be" ) && eng.versions.keyExists( "stable" ) ){
+ var beS = eng.versions.be.scenarios[ scenario.id ] ?: {}
+ var stS = eng.versions.stable.scenarios[ scenario.id ] ?: {}
+ if( !beS.isEmpty() && !stS.isEmpty() ){
+ md.append( "| **#eng.name# Δ** | be vs stable | #deltaMs(beS.min,stS.min)# | #deltaMs(beS.avg,stS.avg)# | #deltaMs(beS.p95,stS.p95)# | #deltaMs(beS.p99,stS.p99)# | #deltaMs(beS.max,stS.max)# | — |" )
+ }
+ }
+ }
+ md.append( "" )
+ }
+
+ // ── Throughput Table ──────────────────────────────────────────────────
+ md.append( "## Throughput (Sequential RPS on Health Check, #r.throughputSecs#s)" )
+ md.append( "" )
+ md.append( "| Engine | BE RPS | Stable RPS | Delta | BE Requests | Stable Requests |" )
+ md.append( "|--------|:------:|:----------:|:-----:|:-----------:|:---------------:|" )
+ for( var engineId in r.engines ){
+ var eng = r.engines[ engineId ]
+ var beT = eng.versions.keyExists( "be" ) ? eng.versions.be.throughput : {}
+ var stT = eng.versions.keyExists( "stable" ) ? eng.versions.stable.throughput : {}
+ var beRps = !beT.isEmpty() ? beT.rps : "-"
+ var stRps = !stT.isEmpty() ? stT.rps : "-"
+ var beReqs = !beT.isEmpty() ? beT.totalRequests : "-"
+ var stReqs = !stT.isEmpty() ? stT.totalRequests : "-"
+ var delta = ( isNumeric( beRps ) && isNumeric( stRps ) && stRps > 0 ) ? formatDelta( beRps, stRps ) : "-"
+ md.append( "| #eng.name# | #beRps# | #stRps# | #delta# | #beReqs# | #stReqs# |" )
+ }
+ md.append( "" )
+
+ // ── Footer ────────────────────────────────────────────────────────────
+ md.append( "---" )
+ md.append( "_Generated by ColdBox Performance Suite — https://github.com/coldbox/coldbox-platform_" )
+
+ fileWrite( arguments.filePath, md.toList( chr(10) ) )
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // REPORT GENERATION — HTML
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private void function generateHTMLReport( required struct results, required string filePath ){
+ var r = arguments.results
+ var ts = dateTimeFormat( r.generated, "yyyy-mm-dd HH:nn:ss" )
+ var json = serializeJSON( r )
+
+ // Build engine labels and colour-coded bars
+ var engineNames = []
+ for( var eid in r.engines ) engineNames.append( r.engines[ eid ].name )
+
+ var beBootstrap = []
+ var stableBootstrap = []
+ var beColdStart = []
+ var stableColdStart = []
+ var beRPS = []
+ var stableRPS = []
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ beBootstrap.append( eng.versions.keyExists( "be" ) && eng.versions.be.appBootstrap.keyExists("ms") ? eng.versions.be.appBootstrap.ms : 0 )
+ stableBootstrap.append( eng.versions.keyExists( "stable" ) && eng.versions.stable.appBootstrap.keyExists("ms") ? eng.versions.stable.appBootstrap.ms : 0 )
+ beColdStart.append( eng.versions.keyExists( "be" ) && !eng.versions.be.coldStart.isEmpty() ? eng.versions.be.coldStart.totalMs : 0 )
+ stableColdStart.append( eng.versions.keyExists( "stable" ) && !eng.versions.stable.coldStart.isEmpty() ? eng.versions.stable.coldStart.totalMs : 0 )
+ beRPS.append( eng.versions.keyExists( "be" ) && !eng.versions.be.throughput.isEmpty() ? eng.versions.be.throughput.rps : 0 )
+ stableRPS.append( eng.versions.keyExists( "stable" ) && !eng.versions.stable.throughput.isEmpty() ? eng.versions.stable.throughput.rps : 0 )
+ }
+
+ // Build per-scenario chart data
+ var scenarioCharts = ""
+ var scenarioTablesHTML = ""
+ var sidx = 0
+ for( var scenario in variables.SCENARIOS ){
+ sidx++
+ var beAvgs = []
+ var stableAvgs = []
+ var beP95s = []
+ var stableP95s = []
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ var beS = ( eng.versions.keyExists("be") && eng.versions.be.scenarios.keyExists(scenario.id) ) ? eng.versions.be.scenarios[ scenario.id ] : {}
+ var stS = ( eng.versions.keyExists("stable") && eng.versions.stable.scenarios.keyExists(scenario.id) ) ? eng.versions.stable.scenarios[ scenario.id ] : {}
+ beAvgs.append( !beS.isEmpty() ? beS.avg : 0 )
+ stableAvgs.append( !stS.isEmpty() ? stS.avg : 0 )
+ beP95s.append( !beS.isEmpty() ? beS.p95 : 0 )
+ stableP95s.append( !stS.isEmpty() ? stS.p95 : 0 )
+ }
+
+ scenarioCharts &= "
+ {
+ id: 'chart_scenario_#sidx#',
+ title: '#jsStringFormat(scenario.name)# — Avg Response Time (ms)',
+ labels: #serializeJSON(engineNames)#,
+ datasets: [
+ { label: 'BE Avg', data: #serializeJSON(beAvgs)#, backgroundColor: 'rgba(59,130,246,0.7)' },
+ { label: 'Stable Avg', data: #serializeJSON(stableAvgs)#, backgroundColor: 'rgba(168,85,247,0.7)' },
+ { label: 'BE P95', data: #serializeJSON(beP95s)#, backgroundColor: 'rgba(59,130,246,0.3)' },
+ { label: 'Stable P95', data: #serializeJSON(stableP95s)#, backgroundColor: 'rgba(168,85,247,0.3)' }
+ ]
+ },"
+
+ // Table
+ scenarioTablesHTML &= "
#scenario.name# — #scenario.description# "
+ scenarioTablesHTML &= ""
+ scenarioTablesHTML &= "Engine Version Min Avg P95 P99 Max Errors "
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ for( var ver in eng.versions ){
+ var vd = eng.versions[ ver ]
+ if( vd.scenarios.keyExists( scenario.id ) ){
+ var s = vd.scenarios[ scenario.id ]
+ var cls = ( ver == "be" ) ? "table-primary" : "table-light"
+ scenarioTablesHTML &= "#eng.name# #ver# "
+ scenarioTablesHTML &= "#s.min# #s.avg# #s.p95# #s.p99# #s.max# #s.errors# (#s.errorPct#%%) "
+ }
+ }
+ }
+ scenarioTablesHTML &= "
"
+ }
+
+ // Build cold-start table HTML
+ var coldStartHTML = ""
+ if( r.coldStartRun ){
+ coldStartHTML = "Engine Version Server Start (ms) First Response (ms) Total (ms) "
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ for( var ver in eng.versions ){
+ var vd = eng.versions[ ver ]
+ var cls = ( ver == "be" ) ? "table-primary" : "table-light"
+ if( !vd.coldStart.isEmpty() && vd.coldStart.success ){
+ coldStartHTML &= "#eng.name# #ver# "
+ coldStartHTML &= "#vd.coldStart.serverStartMs# #vd.coldStart.firstResponseMs# #vd.coldStart.totalMs# "
+ }
+ }
+ }
+ coldStartHTML &= "
"
+ }
+
+ // Bootstrap table HTML
+ var bootstrapTableHTML = "Engine BE (ms) Stable (ms) Delta "
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ var beMs = eng.versions.keyExists("be") ? eng.versions.be.appBootstrap.ms : "-"
+ var stMs = eng.versions.keyExists("stable") ? eng.versions.stable.appBootstrap.ms : "-"
+ var delt = ( isNumeric(beMs) && isNumeric(stMs) && stMs > 0 ) ? formatDeltaHTML(beMs, stMs) : "— "
+ bootstrapTableHTML &= "#eng.name# #beMs# #stMs# #delt# "
+ }
+ bootstrapTableHTML &= "
"
+
+ // Throughput table HTML
+ var throughputTableHTML = "Engine BE RPS Stable RPS Delta BE Requests Stable Requests "
+ for( var eid in r.engines ){
+ var eng = r.engines[ eid ]
+ var beT = eng.versions.keyExists("be") ? eng.versions.be.throughput : {}
+ var stT = eng.versions.keyExists("stable") ? eng.versions.stable.throughput : {}
+ var beRps2 = !beT.isEmpty() ? beT.rps : "-"
+ var stRps2 = !stT.isEmpty() ? stT.rps : "-"
+ var delt = ( isNumeric(beRps2) && isNumeric(stRps2) && stRps2 > 0 ) ? formatDeltaHTML(beRps2, stRps2) : "— "
+ throughputTableHTML &= "#eng.name# #beRps2# #stRps2# #delt# #(!beT.isEmpty()?beT.totalRequests:'-')# #(!stT.isEmpty()?stT.totalRequests:'-')# "
+ }
+ throughputTableHTML &= "
"
+
+ var html = "
+
+
+
+
+ColdBox Performance Report — #ts#
+
+
+
+
+
+
+
+
+
ColdBox Performance Report
+ #ts#
+
+
+
+
Iterations: #r.iterations#
+
Warmup: #r.warmup#
+
Throughput: #r.throughputSecs#s
+
Cold Start: #r.coldStartRun#
+
+
+
+
+ BE Bleeding edge (development branch)
+ Stable ColdBox 8.1.x
+
+
+
+ #r.coldStartRun ? '
Engine Cold Start (No Bytecode Cache)
' & coldStartHTML & '
' : ''#
+
+
+
+
ColdBox App Bootstrap Time (ms)
+
+
+
+
+
+
Throughput — Sequential RPS (Health Check, #r.throughputSecs#s)
+
+
+
+
+
+
Scenario Latency
+
+
+ #scenarioTablesHTML#
+
+
+
+
+
+
+"
+
+ fileWrite( arguments.filePath, html )
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // HELPERS
+ // ══════════════════════════════════════════════════════════════════════════
+
+ private string function formatDelta( required numeric be, required numeric stable ){
+ if( arguments.stable == 0 ) return "-"
+ var pct = round( ( ( arguments.be - arguments.stable ) / arguments.stable ) * 100 * 10 ) / 10
+ return ( pct < 0 ) ? "#pct#%% ✓" : "+#pct#%%"
+ }
+
+ private string function deltaMs( required numeric be, required numeric stable ){
+ var diff = arguments.be - arguments.stable
+ return ( diff < 0 ) ? "#diff#ms ✓" : "+#diff#ms"
+ }
+
+ private string function formatDeltaHTML( required numeric be, required numeric stable ){
+ if( arguments.stable == 0 ) return "— "
+ var pct = round( ( ( arguments.be - arguments.stable ) / arguments.stable ) * 100 * 10 ) / 10
+ var cls = ( pct < 0 ) ? "delta-better" : "delta-worse"
+ var pfx = ( pct < 0 ) ? "" : "+"
+ return "#pfx##pct#%% "
+ }
+
+ private void function log( required string msg ){
+ try {
+ print.line( arguments.msg )
+ } catch( any e ){
+ systemOutput( arguments.msg, true )
+ }
+ }
+
+}
diff --git a/tests/perf-harness/app/config/ColdBox.cfc b/tests/perf-harness/app/config/ColdBox.cfc
new file mode 100644
index 000000000..335c36898
--- /dev/null
+++ b/tests/perf-harness/app/config/ColdBox.cfc
@@ -0,0 +1,61 @@
+/**
+ * Shared ColdBox configuration for performance harness.
+ * appName and appKey are overridden per-version in be-app/config and stable-app/config.
+ */
+component {
+
+ function configure(){
+ variables.coldbox = {
+ appName : "ColdBoxPerfHarness",
+ eventName : "event",
+ reinitPassword : "",
+ reinitKey : "fwreinit",
+ handlersIndexAutoReload : false,
+ debugMode : false,
+ defaultEvent : "Main.index",
+ requestStartHandler : "",
+ requestEndHandler : "",
+ applicationStartHandler : "",
+ applicationEndHandler : "",
+ sessionStartHandler : "",
+ sessionEndHandler : "",
+ missingTemplateHandler : "",
+ applicationHelper : "",
+ viewsHelper : "",
+ modulesExternalLocation : [],
+ viewsExternalLocation : "",
+ layoutsExternalLocation : "",
+ handlersExternalLocation: "",
+ requestContextDecorator : "",
+ exceptionHandler : "",
+ invalidEventHandler : "",
+ customErrorTemplate : "",
+ handlerCaching : true,
+ eventCaching : false,
+ proxyReturnCollection : false
+ };
+
+ variables.layoutSettings = {
+ defaultLayout : "Main.cfm",
+ defaultView : ""
+ };
+
+ variables.modules = {
+ autoReload : false,
+ include : [ "perf-module" ],
+ exclude : []
+ };
+
+ variables.interceptors = [
+ { class : "#appMapping#.interceptors.PerfInterceptor" }
+ ];
+
+ variables.logBox = {
+ appenders : {
+ console : { class : "ConsoleAppender" }
+ },
+ root : { levelmax : "WARN", appenders : "*" }
+ };
+ }
+
+}
diff --git a/tests/perf-harness/app/config/Router.cfc b/tests/perf-harness/app/config/Router.cfc
new file mode 100644
index 000000000..7c7b206d9
--- /dev/null
+++ b/tests/perf-harness/app/config/Router.cfc
@@ -0,0 +1,40 @@
+/**
+ * Performance harness router — five measurable test scenarios.
+ */
+component {
+
+ /**
+ * ColdBox hook: called by RoutingService instead of reading CGI.PATH_INFO.
+ * When the Tuckey URL rewriter does a forward(), the original request URI
+ * is in the javax.servlet.forward.request_uri attribute; CGI.PATH_INFO is empty.
+ * This provider extracts the original path and strips the app sub-path prefix.
+ */
+ function pathInfoProvider( event ){
+ var forwardURI = getPageContext().getRequest().getAttribute( "javax.servlet.forward.request_uri" )
+ if ( !isNull( forwardURI ) && len( forwardURI ) ) {
+ return reReplaceNoCase( forwardURI, "^/tests/perf-harness/(be-app|stable-app)", "" )
+ }
+ return CGI.PATH_INFO
+ }
+
+ function configure(){
+ // Health check — minimal response, no DI, no view
+ route( "/perf/health" ).to( "Main.health" )
+
+ // Simple view — renders view + layout
+ route( "/perf/view" ).to( "Main.index" )
+
+ // JSON API — DI injection + renderData
+ route( "/perf/api" ).to( "Api.list" )
+
+ // Complex view — multiple model injections + data loop
+ route( "/perf/complex" ).to( "Main.complex" )
+
+ // Module request — full HMVC module routing
+ route( "/perf/module" ).to( "perf-module:Items.index" )
+
+ // Default convention routing
+ route( "/:handler/:action?" ).end()
+ }
+
+}
diff --git a/tests/perf-harness/app/handlers/Api.cfc b/tests/perf-harness/app/handlers/Api.cfc
new file mode 100644
index 000000000..906f59842
--- /dev/null
+++ b/tests/perf-harness/app/handlers/Api.cfc
@@ -0,0 +1,23 @@
+/**
+ * REST API handler — tests DI + JSON serialization pipeline.
+ */
+component extends="coldbox.system.EventHandler" {
+
+ property name="userService" inject="UserService";
+
+ // JSON list — injects UserService and renders JSON
+ function list( event, rc, prc ){
+ var data = {
+ status : "success",
+ count : 10,
+ engine : server.keyExists( "coldfusion" ) ? "Adobe CF" : ( server.keyExists( "lucee" ) ? "Lucee" : "BoxLang" ),
+ users : userService.getUsers( 10 ),
+ metadata : {
+ generated : now(),
+ framework : "ColdBox"
+ }
+ }
+ event.renderData( type="json", data=data )
+ }
+
+}
diff --git a/tests/perf-harness/app/handlers/Main.cfc b/tests/perf-harness/app/handlers/Main.cfc
new file mode 100644
index 000000000..f2afa382e
--- /dev/null
+++ b/tests/perf-harness/app/handlers/Main.cfc
@@ -0,0 +1,29 @@
+/**
+ * Main handler for performance test scenarios.
+ */
+component extends="coldbox.system.EventHandler" {
+
+ property name="userService" inject="UserService";
+ property name="productService" inject="ProductService";
+
+ // Baseline — no DI usage, no view, minimal processing
+ function health( event, rc, prc ){
+ event.renderData( type="text", data="ok", statusCode=200 )
+ }
+
+ // Simple view — renders main/index with layout
+ function index( event, rc, prc ){
+ prc.message = "ColdBox Performance Harness"
+ prc.timestamp = now()
+ prc.version = getColdBoxSetting( "version", "unknown" )
+ event.setView( "main/index" )
+ }
+
+ // Complex view — resolves two model dependencies, loops data
+ function complex( event, rc, prc ){
+ prc.users = userService.getUsers( 10 )
+ prc.products = productService.getProducts( 5 )
+ event.setView( "main/complex" )
+ }
+
+}
diff --git a/tests/perf-harness/app/interceptors/PerfInterceptor.cfc b/tests/perf-harness/app/interceptors/PerfInterceptor.cfc
new file mode 100644
index 000000000..bf777a55a
--- /dev/null
+++ b/tests/perf-harness/app/interceptors/PerfInterceptor.cfc
@@ -0,0 +1,19 @@
+/**
+ * Records per-request timing into the PRC scope for debugging.
+ */
+component {
+
+ void function configure(){
+ }
+
+ void function preProcess( event, data, rc, prc ){
+ arguments.prc._perfStart = getTickCount()
+ }
+
+ void function postProcess( event, data, rc, prc ){
+ if( arguments.prc.keyExists( "_perfStart" ) ){
+ arguments.prc._perfElapsed = getTickCount() - arguments.prc._perfStart
+ }
+ }
+
+}
diff --git a/tests/perf-harness/app/layouts/Main.cfm b/tests/perf-harness/app/layouts/Main.cfm
new file mode 100644
index 000000000..aef8e0390
--- /dev/null
+++ b/tests/perf-harness/app/layouts/Main.cfm
@@ -0,0 +1,23 @@
+
+
+
+
+
+ ColdBox Perf Harness
+
+
+
+
+
+ ColdBox Performance Harness
+
+
+ #renderView()#
+
+
+
+
diff --git a/tests/perf-harness/app/models/ProductService.cfc b/tests/perf-harness/app/models/ProductService.cfc
new file mode 100644
index 000000000..74ddb817f
--- /dev/null
+++ b/tests/perf-harness/app/models/ProductService.cfc
@@ -0,0 +1,24 @@
+/**
+ * Product service singleton — provides test product data.
+ */
+component singleton {
+
+ variables.CATEGORIES = [ "Electronics", "Clothing", "Books", "Food", "Tools" ]
+
+ function getProducts( numeric count=5 ){
+ var result = []
+ for( var i = 1; i <= arguments.count; i++ ){
+ result.append({
+ id : i,
+ name : "Product #i#",
+ sku : "SKU-#numberFormat( i, "00000" )#",
+ price : precisionEvaluate( i * 9.99 ),
+ category : variables.CATEGORIES[ ( ( i - 1 ) mod variables.CATEGORIES.len() ) + 1 ],
+ inStock : ( i mod 4 != 0 ),
+ tags : [ "tag#i#", "perf", "test" ]
+ })
+ }
+ return result
+ }
+
+}
diff --git a/tests/perf-harness/app/models/UserService.cfc b/tests/perf-harness/app/models/UserService.cfc
new file mode 100644
index 000000000..b2beda9a3
--- /dev/null
+++ b/tests/perf-harness/app/models/UserService.cfc
@@ -0,0 +1,34 @@
+/**
+ * User service singleton — provides test user data.
+ */
+component singleton {
+
+ function getUsers( numeric count=10 ){
+ var result = []
+ for( var i = 1; i <= arguments.count; i++ ){
+ result.append({
+ id : i,
+ firstName : "User",
+ lastName : "Number#i#",
+ email : "user#i#@perf.test",
+ role : ( i mod 3 == 0 ) ? "admin" : "user",
+ active : true,
+ createdAt : now()
+ })
+ }
+ return result
+ }
+
+ function getUserById( required numeric id ){
+ return {
+ id : arguments.id,
+ firstName : "User",
+ lastName : "Number#arguments.id#",
+ email : "user#arguments.id#@perf.test",
+ role : "user",
+ active : true,
+ createdAt : now()
+ }
+ }
+
+}
diff --git a/tests/perf-harness/app/modules_app/perf-module/ModuleConfig.cfc b/tests/perf-harness/app/modules_app/perf-module/ModuleConfig.cfc
new file mode 100644
index 000000000..c99d498a9
--- /dev/null
+++ b/tests/perf-harness/app/modules_app/perf-module/ModuleConfig.cfc
@@ -0,0 +1,21 @@
+/**
+ * Performance test module — exercises HMVC module routing and module-scoped DI.
+ */
+component {
+
+ this.title = "Perf Module"
+ this.author = "Ortus Solutions"
+ this.description = "HMVC module for ColdBox performance testing"
+ this.version = "1.0.0"
+ this.entrypoint = "perf-module"
+ this.modelNamespace = "perf-module"
+ this.autoMapModels = true
+
+ function configure(){
+ routes = [
+ { pattern : "/items", handler : "Items", action : "index" },
+ { pattern : "/", handler : "Items", action : "index" }
+ ]
+ }
+
+}
diff --git a/tests/perf-harness/app/modules_app/perf-module/handlers/Items.cfc b/tests/perf-harness/app/modules_app/perf-module/handlers/Items.cfc
new file mode 100644
index 000000000..c377ea42b
--- /dev/null
+++ b/tests/perf-harness/app/modules_app/perf-module/handlers/Items.cfc
@@ -0,0 +1,20 @@
+/**
+ * Module handler — returns item list as JSON.
+ */
+component extends="coldbox.system.EventHandler" {
+
+ property name="itemService" inject="ItemService@perf-module";
+
+ function index( event, rc, prc ){
+ event.renderData(
+ type = "json",
+ data = {
+ status : "success",
+ module : "perf-module",
+ count : 10,
+ items : itemService.getItems( 10 )
+ }
+ )
+ }
+
+}
diff --git a/tests/perf-harness/app/modules_app/perf-module/models/ItemService.cfc b/tests/perf-harness/app/modules_app/perf-module/models/ItemService.cfc
new file mode 100644
index 000000000..429fe1362
--- /dev/null
+++ b/tests/perf-harness/app/modules_app/perf-module/models/ItemService.cfc
@@ -0,0 +1,20 @@
+/**
+ * Module-scoped item service singleton.
+ */
+component singleton {
+
+ function getItems( numeric count=10 ){
+ var result = []
+ for( var i = 1; i <= arguments.count; i++ ){
+ result.append({
+ id : i,
+ name : "Item #i#",
+ code : "ITEM-#i#",
+ value : i * 1.5,
+ active : true
+ })
+ }
+ return result
+ }
+
+}
diff --git a/tests/perf-harness/app/views/api/list.cfm b/tests/perf-harness/app/views/api/list.cfm
new file mode 100644
index 000000000..6df796565
--- /dev/null
+++ b/tests/perf-harness/app/views/api/list.cfm
@@ -0,0 +1 @@
+#serializeJSON( prc.data ?: {} )#
diff --git a/tests/perf-harness/app/views/main/complex.cfm b/tests/perf-harness/app/views/main/complex.cfm
new file mode 100644
index 000000000..4f5362ffa
--- /dev/null
+++ b/tests/perf-harness/app/views/main/complex.cfm
@@ -0,0 +1,21 @@
+
+
+
+ Users (#prc.users.len()#)
+
+
+ #u.id# — #u.firstName# #u.lastName# <#u.email#> [#u.role#]
+
+
+
+
+
+ Products (#prc.products.len()#)
+
+
+ #p.id# — #p.name# (#p.sku#) $#numberFormat( p.price, "9.99" )# — #p.category#
+
+
+
+
+
diff --git a/tests/perf-harness/app/views/main/index.cfm b/tests/perf-harness/app/views/main/index.cfm
new file mode 100644
index 000000000..7029a0b51
--- /dev/null
+++ b/tests/perf-harness/app/views/main/index.cfm
@@ -0,0 +1,10 @@
+
+
+
#prc.message#
+
Rendered at: #dateTimeFormat( prc.timestamp, "yyyy-mm-dd HH:nn:ss" )#
+
ColdBox: #prc.version#
+
+ Request elapsed: #prc._perfElapsed#ms
+
+
+
diff --git a/tests/perf-harness/be-app/Application.cfc b/tests/perf-harness/be-app/Application.cfc
new file mode 100644
index 000000000..88237c3af
--- /dev/null
+++ b/tests/perf-harness/be-app/Application.cfc
@@ -0,0 +1,81 @@
+/**
+ * Bootstrap for the Bleeding Edge ColdBox performance app.
+ * Maps /coldbox to the repo root (current development branch).
+ * Maps /cbperfapp to the shared ../app/ directory.
+ * COLDBOX_APP_ROOT_PATH points to ../app/ so ColdBox discovers handlers/views there.
+ */
+component {
+
+ // ─── Application properties ───────────────────────────────────────────────
+ this.name = "ColdBoxPerfBE_" & hash( getCurrentTemplatePath() )
+ this.sessionManagement = true
+ this.sessionTimeout = createTimespan( 0, 0, 10, 0 )
+ this.setClientCookies = false
+ this.timezone = "UTC"
+
+ // ─── Path resolution ──────────────────────────────────────────────────────
+ // beAppPath = /…/tests/perf-harness/be-app/
+ // repoRoot = /…/coldbox-platform/
+ // sharedApp = /…/tests/perf-harness/app/
+ beAppPath = getDirectoryFromPath( getCurrentTemplatePath() )
+ repoRoot = reReplaceNoCase( beAppPath, "tests[/\\]perf-harness[/\\]be-app[/\\]", "" )
+ sharedApp = repoRoot & "tests/perf-harness/app/"
+
+ // ─── CF Mappings ──────────────────────────────────────────────────────────
+ // /coldbox → bleeding edge framework (repo root)
+ this.mappings[ "/coldbox" ] = repoRoot
+ // /cbperfapp → shared ColdBox application components
+ this.mappings[ "/cbperfapp" ] = sharedApp
+
+ // ─── ColdBox bootstrap settings ───────────────────────────────────────────
+ COLDBOX_APP_ROOT_PATH = sharedApp
+ COLDBOX_CONFIG_FILE = "cbperfapp.config.ColdBox"
+ COLDBOX_APP_KEY = "cbperf_be"
+ COLDBOX_APP_MAPPING = "cbperfapp"
+ COLDBOX_WEB_MAPPING = "tests/perf-harness/be-app"
+ COLDBOX_FAIL_FAST = true
+
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
+ public boolean function onApplicationStart(){
+ application.cbBootstrap = new coldbox.system.Bootstrap(
+ COLDBOX_CONFIG_FILE,
+ COLDBOX_APP_ROOT_PATH,
+ COLDBOX_APP_KEY,
+ COLDBOX_APP_MAPPING,
+ COLDBOX_FAIL_FAST,
+ COLDBOX_WEB_MAPPING
+ )
+ application.cbBootstrap.loadColdbox()
+ return true
+ }
+
+ public boolean function onRequestStart( string targetPage ){
+ // Allow reinit via ?bsReinit=1
+ if( structKeyExists( url, "bsReinit" ) || !structKeyExists( application, "cbBootstrap" ) ){
+ lock name="cbperf_be_reinit" type="exclusive" timeout="10" throwonTimeout=true {
+ structDelete( application, "cbBootstrap" )
+ onApplicationStart()
+ }
+ }
+ application.cbBootstrap.onRequestStart( arguments.targetPage )
+ return true
+ }
+
+ public boolean function onApplicationEnd( struct appScope ){
+ arguments.appScope.cbBootstrap.onApplicationEnd( arguments.appScope )
+ return true
+ }
+
+ public void function onSessionStart(){
+ application.cbBootstrap.onSessionStart()
+ }
+
+ public void function onSessionEnd( struct sessionScope, struct appScope ){
+ arguments.appScope.cbBootstrap.onSessionEnd( argumentCollection=arguments )
+ }
+
+ public boolean function onMissingTemplate( string template ){
+ return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments )
+ }
+
+}
diff --git a/tests/perf-harness/be-app/index.cfm b/tests/perf-harness/be-app/index.cfm
new file mode 100644
index 000000000..8ea6044a3
--- /dev/null
+++ b/tests/perf-harness/be-app/index.cfm
@@ -0,0 +1,8 @@
+
+
+
+ _forwardURI = getPageContext().getRequest().getAttribute( "javax.servlet.forward.request_uri" )
+ if ( !isNull( _forwardURI ) && len( _forwardURI ) ) {
+ CGI.PATH_INFO = reReplaceNoCase( _forwardURI, "^/tests/perf-harness/be-app", "" )
+ }
+
diff --git a/tests/perf-harness/reports/.gitkeep b/tests/perf-harness/reports/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/perf-harness/server-perf-adobe2025.json b/tests/perf-harness/server-perf-adobe2025.json
new file mode 100644
index 000000000..7574fad36
--- /dev/null
+++ b/tests/perf-harness/server-perf-adobe2025.json
@@ -0,0 +1,24 @@
+{
+ "app": {
+ "cfengine": "adobe@2025",
+ "serverHomeDirectory": "../../.engine/adobe2025"
+ },
+ "name": "coldbox-perf-adobe2025",
+ "force": true,
+ "openBrowser": false,
+ "web": {
+ "webroot": "../../",
+ "directoryBrowsing": true,
+ "http": { "port": "8599" },
+ "rewrites": { "enable": true },
+ "aliases": { "/coldbox": "../../" }
+ },
+ "JVM": {
+ "heapSize": "1024",
+ "javaHome": "/usr/lib/jvm/java-21-openjdk-amd64"
+ },
+ "cfconfig": { "file": "../../.cfconfig.json" },
+ "scripts": {
+ "onServerInstall": "cfpm install caching,zip,orm,mysql,postgresql --noSave"
+ }
+}
diff --git a/tests/perf-harness/server-perf-boxlang-cfml.json b/tests/perf-harness/server-perf-boxlang-cfml.json
new file mode 100644
index 000000000..f0e4a30ab
--- /dev/null
+++ b/tests/perf-harness/server-perf-boxlang-cfml.json
@@ -0,0 +1,36 @@
+{
+ "app":{
+ "cfengine":"boxlang@1",
+ "serverHomeDirectory":"../../.engine/boxlang-cfml-1"
+ },
+ "name":"coldbox-perf-boxlang-cfml",
+ "force":true,
+ "openBrowser":false,
+ "web":{
+ "webroot":"../../",
+ "directoryBrowsing":true,
+ "http":{
+ "port":"8599"
+ },
+ "rewrites":{
+ "enable":true,
+ "config":"urlrewrite.xml"
+ },
+ "aliases":{
+ "/coldbox":"../../"
+ }
+ },
+ "JVM":{
+ "heapSize":"1024",
+ "javaHome":"/usr/lib/jvm/java-21-openjdk-amd64"
+ },
+ "cfconfig":{
+ "file":"../../.cfconfig.json"
+ },
+ "env":{
+ "BOXLANG_DEBUG":false
+ },
+ "scripts":{
+ "onServerInitialInstall":"install bx-compat-cfml --noSave"
+ }
+}
diff --git a/tests/perf-harness/server-perf-boxlang.json b/tests/perf-harness/server-perf-boxlang.json
new file mode 100644
index 000000000..ebb49cd53
--- /dev/null
+++ b/tests/perf-harness/server-perf-boxlang.json
@@ -0,0 +1,22 @@
+{
+ "app": {
+ "cfengine": "boxlang@1",
+ "serverHomeDirectory": "../../.engine/boxlang"
+ },
+ "name": "coldbox-perf-boxlang",
+ "force": true,
+ "openBrowser": false,
+ "web": {
+ "webroot": "../../",
+ "directoryBrowsing": true,
+ "http": { "port": "8599" },
+ "rewrites": { "enable": true },
+ "aliases": { "/coldbox": "../../" }
+ },
+ "JVM": {
+ "heapSize": "1024",
+ "javaHome": "/usr/lib/jvm/java-21-openjdk-amd64"
+ },
+ "cfconfig": { "file": "../../.cfconfig.json" },
+ "env": { "BOXLANG_DEBUG": false }
+}
diff --git a/tests/perf-harness/server-perf-lucee7.json b/tests/perf-harness/server-perf-lucee7.json
new file mode 100644
index 000000000..035747ce0
--- /dev/null
+++ b/tests/perf-harness/server-perf-lucee7.json
@@ -0,0 +1,21 @@
+{
+ "app": {
+ "cfengine": "lucee@7",
+ "serverHomeDirectory": "../../.engine/lucee7"
+ },
+ "name": "coldbox-perf-lucee7",
+ "force": true,
+ "openBrowser": false,
+ "web": {
+ "webroot": "../../",
+ "directoryBrowsing": true,
+ "http": { "port": "8599" },
+ "rewrites": { "enable": true },
+ "aliases": { "/coldbox": "../../" }
+ },
+ "JVM": {
+ "heapSize": "1024",
+ "javaHome": "/usr/lib/jvm/java-21-openjdk-amd64"
+ },
+ "cfconfig": { "file": "../../.cfconfig.json" }
+}
diff --git a/tests/perf-harness/stable-app/.gitignore b/tests/perf-harness/stable-app/.gitignore
new file mode 100644
index 000000000..1bdbe1f5b
--- /dev/null
+++ b/tests/perf-harness/stable-app/.gitignore
@@ -0,0 +1 @@
+coldbox/
diff --git a/tests/perf-harness/stable-app/Application.cfc b/tests/perf-harness/stable-app/Application.cfc
new file mode 100644
index 000000000..dabfbdc96
--- /dev/null
+++ b/tests/perf-harness/stable-app/Application.cfc
@@ -0,0 +1,80 @@
+/**
+ * Bootstrap for the Stable ColdBox 8.1 performance app.
+ * Maps /coldbox to ./coldbox/ (installed via box install coldbox@8.1.x).
+ * Maps /cbperfapp to the shared ../app/ directory.
+ * COLDBOX_APP_ROOT_PATH points to ../app/ so ColdBox discovers handlers/views there.
+ */
+component {
+
+ // ─── Application properties ───────────────────────────────────────────────
+ this.name = "ColdBoxPerfStable_" & hash( getCurrentTemplatePath() )
+ this.sessionManagement = true
+ this.sessionTimeout = createTimespan( 0, 0, 10, 0 )
+ this.setClientCookies = false
+ this.timezone = "UTC"
+
+ // ─── Path resolution ──────────────────────────────────────────────────────
+ // stableAppPath = /…/tests/perf-harness/stable-app/
+ // sharedApp = /…/tests/perf-harness/app/
+ stableAppPath = getDirectoryFromPath( getCurrentTemplatePath() )
+ repoRoot = reReplaceNoCase( stableAppPath, "tests[/\\]perf-harness[/\\]stable-app[/\\]", "" )
+ sharedApp = repoRoot & "tests/perf-harness/app/"
+
+ // ─── CF Mappings ──────────────────────────────────────────────────────────
+ // /coldbox → stable 8.1 installed in stable-app/coldbox/
+ this.mappings[ "/coldbox" ] = stableAppPath & "coldbox/"
+ // /cbperfapp → shared ColdBox application components
+ this.mappings[ "/cbperfapp" ] = sharedApp
+
+ // ─── ColdBox bootstrap settings ───────────────────────────────────────────
+ COLDBOX_APP_ROOT_PATH = sharedApp
+ COLDBOX_CONFIG_FILE = "cbperfapp.config.ColdBox"
+ COLDBOX_APP_KEY = "cbperf_stable"
+ COLDBOX_APP_MAPPING = "cbperfapp"
+ COLDBOX_WEB_MAPPING = "tests/perf-harness/stable-app"
+ COLDBOX_FAIL_FAST = true
+
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
+ public boolean function onApplicationStart(){
+ application.cbBootstrap = new coldbox.system.Bootstrap(
+ COLDBOX_CONFIG_FILE,
+ COLDBOX_APP_ROOT_PATH,
+ COLDBOX_APP_KEY,
+ COLDBOX_APP_MAPPING,
+ COLDBOX_FAIL_FAST,
+ COLDBOX_WEB_MAPPING
+ )
+ application.cbBootstrap.loadColdbox()
+ return true
+ }
+
+ public boolean function onRequestStart( string targetPage ){
+ // Allow reinit via ?bsReinit=1
+ if( structKeyExists( url, "bsReinit" ) || !structKeyExists( application, "cbBootstrap" ) ){
+ lock name="cbperf_stable_reinit" type="exclusive" timeout="10" throwonTimeout=true {
+ structDelete( application, "cbBootstrap" )
+ onApplicationStart()
+ }
+ }
+ application.cbBootstrap.onRequestStart( arguments.targetPage )
+ return true
+ }
+
+ public boolean function onApplicationEnd( struct appScope ){
+ arguments.appScope.cbBootstrap.onApplicationEnd( arguments.appScope )
+ return true
+ }
+
+ public void function onSessionStart(){
+ application.cbBootstrap.onSessionStart()
+ }
+
+ public void function onSessionEnd( struct sessionScope, struct appScope ){
+ arguments.appScope.cbBootstrap.onSessionEnd( argumentCollection=arguments )
+ }
+
+ public boolean function onMissingTemplate( string template ){
+ return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments )
+ }
+
+}
diff --git a/tests/perf-harness/stable-app/box.json b/tests/perf-harness/stable-app/box.json
new file mode 100644
index 000000000..d595cb5a0
--- /dev/null
+++ b/tests/perf-harness/stable-app/box.json
@@ -0,0 +1,10 @@
+{
+ "name": "ColdBox Stable Perf App",
+ "version": "1.0.0",
+ "dependencies": {
+ "coldbox": "8.1.x"
+ },
+ "installPaths": {
+ "coldbox": "coldbox/"
+ }
+}
diff --git a/tests/perf-harness/stable-app/index.cfm b/tests/perf-harness/stable-app/index.cfm
new file mode 100644
index 000000000..edd40ece1
--- /dev/null
+++ b/tests/perf-harness/stable-app/index.cfm
@@ -0,0 +1,8 @@
+
+
+
+ _forwardURI = getPageContext().getRequest().getAttribute( "javax.servlet.forward.request_uri" )
+ if ( !isNull( _forwardURI ) && len( _forwardURI ) ) {
+ CGI.PATH_INFO = reReplaceNoCase( _forwardURI, "^/tests/perf-harness/stable-app", "" )
+ }
+
diff --git a/tests/perf-harness/urlrewrite.xml b/tests/perf-harness/urlrewrite.xml
new file mode 100644
index 000000000..049398ff5
--- /dev/null
+++ b/tests/perf-harness/urlrewrite.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ BE App SES
+ ^/tests/perf-harness/be-app/index\.cfm
+ ^/tests/perf-harness/be-app/(.+)$
+ ^/tests/perf-harness/be-app/(.+)$
+ /tests/perf-harness/be-app/index.cfm
+
+
+
+
+ Stable App SES
+ ^/tests/perf-harness/stable-app/index\.cfm
+ ^/tests/perf-harness/stable-app/(.+)$
+ ^/tests/perf-harness/stable-app/(.+)$
+ /tests/perf-harness/stable-app/index.cfm
+
+
+