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 &= "" + 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 &= "" + scenarioTablesHTML &= "" + } + } + } + scenarioTablesHTML &= "
EngineVersionMinAvgP95P99MaxErrors
#eng.name##ver##s.min##s.avg##s.p95##s.p99##s.max##s.errors# (#s.errorPct#%%)
" + } + + // Build cold-start table HTML + var coldStartHTML = "" + if( r.coldStartRun ){ + coldStartHTML = "
" + 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 &= "" + coldStartHTML &= "" + } + } + } + coldStartHTML &= "
EngineVersionServer Start (ms)First Response (ms)Total (ms)
#eng.name##ver##vd.coldStart.serverStartMs##vd.coldStart.firstResponseMs##vd.coldStart.totalMs#
" + } + + // Bootstrap table HTML + var bootstrapTableHTML = "
" + 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 &= "" + } + bootstrapTableHTML &= "
EngineBE (ms)Stable (ms)Delta
#eng.name##beMs##stMs##delt#
" + + // Throughput table HTML + var throughputTableHTML = "
" + 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 &= "" + } + throughputTableHTML &= "
EngineBE RPSStable RPSDeltaBE RequestsStable Requests
#eng.name##beRps2##stRps2##delt##(!beT.isEmpty()?beT.totalRequests:'-')##(!stT.isEmpty()?stT.totalRequests:'-')#
" + + 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)
+
+
#bootstrapTableHTML#
+
+
+ + +
+
Throughput — Sequential RPS (Health Check, #r.throughputSecs#s)
+
+
#throughputTableHTML#
+
+
+ + +
+
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 + + +