2026-03-15 17:42:48 -07:00
#!/usr/bin/env node
import { spawnSync } from "node:child_process" ;
2026-03-15 18:20:33 -07:00
import { mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
2026-03-15 17:42:48 -07:00
import os from "node:os" ;
import path from "node:path" ;
const isLinux = process . platform === "linux" ;
const isMac = process . platform === "darwin" ;
if ( ! isLinux && ! isMac ) {
console . log ( ` [startup-memory] Skipping on unsupported platform: ${ process . platform } ` ) ;
process . exit ( 0 ) ;
}
const repoRoot = process . cwd ( ) ;
const tmpHome = mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-startup-memory-" ) ) ;
2026-03-15 18:20:33 -07:00
const tmpDir = process . env . TMPDIR || process . env . TEMP || process . env . TMP || os . tmpdir ( ) ;
const rssHookPath = path . join ( tmpHome , "measure-rss.mjs" ) ;
const MAX _RSS _MARKER = "__OPENCLAW_MAX_RSS_KB__=" ;
writeFileSync (
rssHookPath ,
[
"process.on('exit', () => {" ,
" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;" ,
` if (usage && typeof usage.maxRSS === 'number') console.error(' ${ MAX _RSS _MARKER } ' + String(usage.maxRSS)); ` ,
"});" ,
"" ,
] . join ( "\n" ) ,
"utf8" ,
) ;
2026-03-15 17:42:48 -07:00
const DEFAULT _LIMITS _MB = {
help : 500 ,
2026-03-16 07:19:09 +00:00
statusJson : 925 ,
2026-03-15 17:42:48 -07:00
gatewayStatus : 900 ,
} ;
const cases = [
{
id : "help" ,
label : "--help" ,
2026-03-15 18:20:33 -07:00
args : [ "openclaw.mjs" , "--help" ] ,
2026-03-15 17:42:48 -07:00
limitMb : Number ( process . env . OPENCLAW _STARTUP _MEMORY _HELP _MB ? ? DEFAULT _LIMITS _MB . help ) ,
} ,
{
id : "statusJson" ,
label : "status --json" ,
2026-03-15 18:20:33 -07:00
args : [ "openclaw.mjs" , "status" , "--json" ] ,
2026-03-15 17:42:48 -07:00
limitMb : Number (
process . env . OPENCLAW _STARTUP _MEMORY _STATUS _JSON _MB ? ? DEFAULT _LIMITS _MB . statusJson ,
) ,
} ,
{
id : "gatewayStatus" ,
label : "gateway status" ,
2026-03-15 18:20:33 -07:00
args : [ "openclaw.mjs" , "gateway" , "status" ] ,
2026-03-15 17:42:48 -07:00
limitMb : Number (
process . env . OPENCLAW _STARTUP _MEMORY _GATEWAY _STATUS _MB ? ? DEFAULT _LIMITS _MB . gatewayStatus ,
) ,
} ,
] ;
2026-03-16 17:21:18 -05:00
function formatFixGuidance ( testCase , details ) {
const command = ` node ${ testCase . args . join ( " " ) } ` ;
const guidance = [
"[startup-memory] Fix guidance" ,
` Case: ${ testCase . label } ` ,
` Command: ${ command } ` ,
"Next steps:" ,
` 1. Run \` ${ command } \` locally on the built tree. ` ,
"2. If this is an RSS overage, compare the startup import graph against the last passing commit and look for newly eager imports, bootstrap side effects, or plugin loading on the command path." ,
"3. If this is a non-zero exit, inspect the first transitive import/config error in stderr and fix that root cause before re-checking memory." ,
"LLM prompt:" ,
` "OpenClaw startup-memory CI failed for ' ${ testCase . label } '. Analyze this failure, identify the first runtime/import side effect that makes startup heavier or broken, and propose the smallest safe patch. Failure output: \n ${ details } " ` ,
] ;
return ` ${ guidance . join ( "\n" ) } \n ` ;
}
function formatFailure ( testCase , message , details = "" ) {
const trimmedDetails = details . trim ( ) ;
const sections = [ message ] ;
if ( trimmedDetails ) {
sections . push ( trimmedDetails ) ;
}
sections . push ( formatFixGuidance ( testCase , trimmedDetails || message ) ) ;
return sections . join ( "\n\n" ) ;
}
2026-03-15 17:42:48 -07:00
function parseMaxRssMb ( stderr ) {
2026-03-16 10:27:44 +00:00
const matches = [ ... stderr . matchAll ( new RegExp ( ` ^ ${ MAX _RSS _MARKER } ( \\ d+) \\ s* $ ` , "gm" ) ) ] ;
const lastMatch = matches . at ( - 1 ) ;
if ( ! lastMatch ) {
2026-03-15 17:42:48 -07:00
return null ;
}
2026-03-16 10:27:44 +00:00
return Number ( lastMatch [ 1 ] ) / 1024 ;
2026-03-15 17:42:48 -07:00
}
2026-03-15 18:20:33 -07:00
function buildBenchEnv ( ) {
2026-03-15 17:42:48 -07:00
const env = {
HOME : tmpHome ,
2026-03-15 18:20:33 -07:00
USERPROFILE : tmpHome ,
2026-03-15 17:42:48 -07:00
XDG _CONFIG _HOME : path . join ( tmpHome , ".config" ) ,
XDG _DATA _HOME : path . join ( tmpHome , ".local" , "share" ) ,
XDG _CACHE _HOME : path . join ( tmpHome , ".cache" ) ,
2026-03-15 18:20:33 -07:00
PATH : process . env . PATH ? ? "" ,
TMPDIR : tmpDir ,
TEMP : tmpDir ,
TMP : tmpDir ,
LANG : process . env . LANG ? ? "C.UTF-8" ,
TERM : process . env . TERM ? ? "dumb" ,
2026-03-15 17:42:48 -07:00
} ;
2026-03-15 18:20:33 -07:00
if ( process . env . LC _ALL ) {
env . LC _ALL = process . env . LC _ALL ;
}
if ( process . env . CI ) {
env . CI = process . env . CI ;
}
if ( process . env . NODE _DISABLE _COMPILE _CACHE ) {
env . NODE _DISABLE _COMPILE _CACHE = process . env . NODE _DISABLE _COMPILE _CACHE ;
2026-03-16 07:19:09 +00:00
} else {
// Keep the regression check focused on app/runtime startup, not Node's
// one-shot compile cache overhead, which varies across runner builds.
env . NODE _DISABLE _COMPILE _CACHE = "1" ;
2026-03-15 18:20:33 -07:00
}
2026-03-16 10:27:44 +00:00
// Keep the benchmark on a single process so RSS reflects the actual command
// path rather than the warning-suppression respawn wrapper.
env . OPENCLAW _NO _RESPAWN = "1" ;
2026-03-15 18:20:33 -07:00
return env ;
}
function runCase ( testCase ) {
const env = buildBenchEnv ( ) ;
const result = spawnSync ( process . execPath , [ "--import" , rssHookPath , ... testCase . args ] , {
2026-03-15 17:42:48 -07:00
cwd : repoRoot ,
env ,
encoding : "utf8" ,
maxBuffer : 20 * 1024 * 1024 ,
} ) ;
const stderr = result . stderr ? ? "" ;
const maxRssMb = parseMaxRssMb ( stderr ) ;
const matrixBootstrapWarning = /matrix: crypto runtime bootstrap failed/i . test ( stderr ) ;
if ( result . status !== 0 ) {
throw new Error (
2026-03-16 17:21:18 -05:00
formatFailure (
testCase ,
` ${ testCase . label } exited with ${ String ( result . status ) } ` ,
stderr . trim ( ) || result . stdout || "" ,
) ,
2026-03-15 17:42:48 -07:00
) ;
}
if ( maxRssMb == null ) {
2026-03-16 17:21:18 -05:00
throw new Error ( formatFailure ( testCase , ` ${ testCase . label } did not report max RSS ` , stderr ) ) ;
2026-03-15 17:42:48 -07:00
}
if ( matrixBootstrapWarning ) {
2026-03-16 17:21:18 -05:00
throw new Error (
formatFailure ( testCase , ` ${ testCase . label } triggered Matrix crypto bootstrap during startup ` ) ,
) ;
2026-03-15 17:42:48 -07:00
}
if ( maxRssMb > testCase . limitMb ) {
throw new Error (
2026-03-16 17:21:18 -05:00
formatFailure (
testCase ,
` ${ testCase . label } used ${ maxRssMb . toFixed ( 1 ) } MB RSS (limit ${ testCase . limitMb } MB) ` ,
) ,
2026-03-15 17:42:48 -07:00
) ;
}
console . log (
` [startup-memory] ${ testCase . label } : ${ maxRssMb . toFixed ( 1 ) } MB RSS (limit ${ testCase . limitMb } MB) ` ,
) ;
}
try {
for ( const testCase of cases ) {
runCase ( testCase ) ;
}
} finally {
rmSync ( tmpHome , { recursive : true , force : true } ) ;
}