@@ -38,10 +38,11 @@ const DISK_FREE_MIN_GB = 2;
3838const SENTRY_RSS_ALERT_MB = 512 ;
3939const SENTRY_COUNT_ALERT = 2 ;
4040const STUCK_RSS_ALERT_MB = 768 ;
41+ const JAEGER_ANON_ALERT_MB = 2 * 1024 ;
4142/**
42- * A process is "hot" when it holds ≥STUCK_RSS_ALERT_MB of RSS or averages
43- * ≥ SUSTAINED_CPU_PERCENT of a core, and it only alerts once it has stayed hot
44- * for SUSTAINED_TICKS consecutive ticks.
43+ * A process is "hot" when it exceeds its memory policy or averages at least
44+ * SUSTAINED_CPU_PERCENT of a core, and it only alerts once it has stayed hot for
45+ * SUSTAINED_TICKS consecutive ticks.
4546 *
4647 * This measures a *rate* (Δcpu / Δwall between ticks), not cumulative CPU time:
4748 * a long-lived-but-idle process (e.g. one that gathered 200s of CPU over hours
@@ -77,11 +78,38 @@ const RUNAWAY_PATTERNS: ReadonlyArray<{
7778export interface ProcInfo {
7879 readonly pid : number ;
7980 readonly rssMb : number ;
81+ readonly rssAnonMb : number ;
82+ readonly rssFileMb : number ;
8083 readonly cpuSeconds : number ;
8184 readonly cmd : string ;
8285 readonly label : string ;
8386}
8487
88+ export interface ProcMemoryPolicy {
89+ readonly valueMb : number ;
90+ readonly thresholdMb : number ;
91+ readonly kind : "anonymous" | "rss" ;
92+ }
93+
94+ export function memoryPolicyForProcess (
95+ proc : ProcInfo ,
96+ defaultRssMbThreshold = STUCK_RSS_ALERT_MB ,
97+ ) : ProcMemoryPolicy {
98+ if ( / (?: ^ | [ / \s ] ) j a e g e r (?: $ | [ / \s - ] ) / i. test ( proc . cmd ) ) {
99+ const hasRssBreakdown = proc . rssAnonMb > 0 || proc . rssFileMb > 0 ;
100+ return {
101+ valueMb : hasRssBreakdown ? proc . rssAnonMb : proc . rssMb ,
102+ thresholdMb : JAEGER_ANON_ALERT_MB ,
103+ kind : hasRssBreakdown ? "anonymous" : "rss" ,
104+ } ;
105+ }
106+ return {
107+ valueMb : proc . rssMb ,
108+ thresholdMb : defaultRssMbThreshold ,
109+ kind : "rss" ,
110+ } ;
111+ }
112+
85113/** Per-process tracker state carried between ticks to derive a CPU rate. */
86114export interface ProcSustainState {
87115 readonly cpuSeconds : number ;
@@ -96,6 +124,11 @@ export interface ProcSustainState {
96124export interface SustainedHotProcess {
97125 readonly pid : number ;
98126 readonly rssMb : number ;
127+ readonly rssAnonMb : number ;
128+ readonly rssFileMb : number ;
129+ readonly memoryValueMb : number ;
130+ readonly memoryThresholdMb : number ;
131+ readonly memoryKind : ProcMemoryPolicy [ "kind" ] ;
99132 /** Average CPU over the last tick gap, as percent of a single core. */
100133 readonly cpuPercent : number ;
101134 /** How long it has been continuously hot. */
@@ -116,6 +149,7 @@ export function trackSustainedHotProcesses(input: {
116149 readonly nowMs : number ;
117150 readonly cpuPercentThreshold : number ;
118151 readonly rssMbThreshold : number ;
152+ readonly memoryPolicyFor ?: ( proc : ProcInfo ) => ProcMemoryPolicy ;
119153 readonly sustainedTicks : number ;
120154} ) : {
121155 readonly next : Map < number , ProcSustainState > ;
@@ -136,9 +170,14 @@ export function trackSustainedHotProcesses(input: {
136170 ? ( Math . max ( 0 , proc . cpuSeconds - previous . cpuSeconds ) / ( elapsedMs / 1_000 ) ) * 100
137171 : null ;
138172
173+ const memoryPolicy = input . memoryPolicyFor ?.( proc ) ?? {
174+ valueMb : proc . rssMb ,
175+ thresholdMb : input . rssMbThreshold ,
176+ kind : "rss" ,
177+ } ;
139178 const isHot =
140179 ( cpuPercent !== null && cpuPercent >= input . cpuPercentThreshold ) ||
141- proc . rssMb >= input . rssMbThreshold ;
180+ memoryPolicy . valueMb >= memoryPolicy . thresholdMb ;
142181 const hotTicks = isHot ? ( previous ?. hotTicks ?? 0 ) + 1 : 0 ;
143182 const hotSinceMs = isHot
144183 ? previous ?. hotTicks
@@ -157,14 +196,19 @@ export function trackSustainedHotProcesses(input: {
157196 hot . push ( {
158197 pid : proc . pid ,
159198 rssMb : proc . rssMb ,
199+ rssAnonMb : proc . rssAnonMb ,
200+ rssFileMb : proc . rssFileMb ,
201+ memoryValueMb : memoryPolicy . valueMb ,
202+ memoryThresholdMb : memoryPolicy . thresholdMb ,
203+ memoryKind : memoryPolicy . kind ,
160204 cpuPercent : cpuPercent ?? 0 ,
161205 sustainedMs : input . nowMs - hotSinceMs ,
162206 label : proc . label ,
163207 } ) ;
164208 }
165209 }
166210
167- hot . sort ( ( a , b ) => b . cpuPercent - a . cpuPercent || b . rssMb - a . rssMb ) ;
211+ hot . sort ( ( a , b ) => b . cpuPercent - a . cpuPercent || b . memoryValueMb - a . memoryValueMb ) ;
168212 return { next, hot : hot . slice ( 0 , 8 ) } ;
169213}
170214
@@ -354,13 +398,32 @@ function readDisk(path: string): DiskInfo | null {
354398 }
355399}
356400
357- function readRssMb ( pid : number ) : number {
401+ export function parseProcMemoryStatus ( status : string ) : {
402+ readonly rssMb : number ;
403+ readonly rssAnonMb : number ;
404+ readonly rssFileMb : number ;
405+ } {
406+ const readMb = ( field : string ) => {
407+ const match = new RegExp ( `^${ field } :\\s+(\\d+)\\s+kB` , "m" ) . exec ( status ) ;
408+ return match ? Number ( match [ 1 ] ) / 1024 : 0 ;
409+ } ;
410+ return {
411+ rssMb : readMb ( "VmRSS" ) ,
412+ rssAnonMb : readMb ( "RssAnon" ) ,
413+ rssFileMb : readMb ( "RssFile" ) ,
414+ } ;
415+ }
416+
417+ function readProcMemory ( pid : number ) : {
418+ readonly rssMb : number ;
419+ readonly rssAnonMb : number ;
420+ readonly rssFileMb : number ;
421+ } {
358422 try {
359423 const status = NodeFS . readFileSync ( `/proc/${ pid } /status` , "utf8" ) ;
360- const match = / ^ V m R S S : \s + ( \d + ) \s + k B / m. exec ( status ) ;
361- return match ? Number ( match [ 1 ] ) / 1024 : 0 ;
424+ return parseProcMemoryStatus ( status ) ;
362425 } catch {
363- return 0 ;
426+ return { rssMb : 0 , rssAnonMb : 0 , rssFileMb : 0 } ;
364427 }
365428}
366429
@@ -414,9 +477,10 @@ function listProcesses(): ReadonlyArray<ProcInfo> {
414477 if ( pid === process . pid ) continue ;
415478 const cmd = readCmdline ( pid ) ;
416479 if ( cmd === "" ) continue ;
480+ const memory = readProcMemory ( pid ) ;
417481 out . push ( {
418482 pid,
419- rssMb : readRssMb ( pid ) ,
483+ ... memory ,
420484 cpuSeconds : readCpuSeconds ( pid ) ,
421485 cmd,
422486 label : shortCmd ( cmd ) ,
@@ -456,6 +520,7 @@ function listFatProcesses(
456520 nowMs,
457521 cpuPercentThreshold : SUSTAINED_CPU_PERCENT ,
458522 rssMbThreshold : STUCK_RSS_ALERT_MB ,
523+ memoryPolicyFor : ( proc ) => memoryPolicyForProcess ( proc , STUCK_RSS_ALERT_MB ) ,
459524 sustainedTicks : SUSTAINED_TICKS ,
460525 } ) ;
461526 sustainState = next ;
@@ -803,13 +868,20 @@ export const runAlertWatchdog = (botConfig: DiscordBotConfig) =>
803868 yield * postAlert (
804869 "stuck-proc" ,
805870 [
806- "**Sustained high RSS / CPU process(es)**" ,
807- ...fatNonRunaway . map (
808- ( p ) =>
809- `• pid=${ p . pid } rss=${ p . rssMb . toFixed ( 0 ) } MiB cpu≈${ p . cpuPercent . toFixed ( 0 ) } % ` +
810- `for ${ Math . round ( p . sustainedMs / 60_000 ) } m ${ p . label } ` ,
811- ) ,
812- `_Sustained ≥${ SUSTAINED_TICKS } ticks with RSS≥${ STUCK_RSS_ALERT_MB } MiB or CPU≥${ SUSTAINED_CPU_PERCENT } % of a core (not auto-killed)._` ,
871+ "**Sustained high memory / CPU process(es)**" ,
872+ ...fatNonRunaway . map ( ( p ) => {
873+ const memory =
874+ p . memoryKind === "anonymous"
875+ ? `anon=${ p . rssAnonMb . toFixed ( 0 ) } MiB file-cache=${ p . rssFileMb . toFixed ( 0 ) } MiB rss=${ p . rssMb . toFixed ( 0 ) } MiB`
876+ : `rss=${ p . rssMb . toFixed ( 0 ) } MiB` ;
877+ return (
878+ `• pid=${ p . pid } ${ memory } ` +
879+ `(memory alert ${ p . memoryKind } ≥${ p . memoryThresholdMb . toFixed ( 0 ) } MiB) ` +
880+ `cpu≈${ p . cpuPercent . toFixed ( 0 ) } % ` +
881+ `for ${ Math . round ( p . sustainedMs / 60_000 ) } m ${ p . label } `
882+ ) ;
883+ } ) ,
884+ `_Sustained ≥${ SUSTAINED_TICKS } ticks with process-specific high memory or CPU≥${ SUSTAINED_CPU_PERCENT } % of a core (not auto-killed)._` ,
813885 ] . join ( "\n" ) ,
814886 ) ;
815887 }
0 commit comments