1313 */
1414public final class JvmSnapshotCollector {
1515
16+ private static final List <String > warnings = new ArrayList <>();
17+
1618 /**
1719 * Collect snapshot β routes to local or remote based on PID.
1820 */
1921 public static JvmSnapshot collect (long pid ) {
22+ warnings .clear ();
2023 if (pid <= 0 || pid == ProcessHandle .current ().pid ()) {
2124 return collectLocal ();
2225 }
2326 return collectRemote (pid );
2427 }
2528
26- /**
27- * Collect snapshot for the current JVM (in-process mode).
28- */
29+ /** Get warnings from the last collection (e.g., jcmd failures). */
30+ public static List <String > lastWarnings () {
31+ return List .copyOf (warnings );
32+ }
33+
2934 @ SuppressWarnings ("deprecation" )
3035 public static JvmSnapshot collectLocal () {
3136 MemoryMXBean mem = ManagementFactory .getMemoryMXBean ();
@@ -92,31 +97,51 @@ public static JvmSnapshot collectLocal() {
9297
9398 /**
9499 * Collect snapshot for a remote JVM by PID using jcmd.
95- * Parses GC.heap_info, Thread.print, VM.version, VM.uptime, VM.flags .
100+ * Errors are collected in warnings list instead of silently swallowed .
96101 */
97102 public static JvmSnapshot collectRemote (long pid ) {
103+ int failures = 0 ;
104+
98105 // Heap via GC.heap_info
99106 long heapUsed = 0 , heapMax = 0 , heapCommitted = 0 , nonHeapUsed = 0 ;
100107 Map <String , JvmSnapshot .PoolInfo > pools = new LinkedHashMap <>();
101108 try {
102109 String heapInfo = JcmdExecutor .execute (pid , "GC.heap_info" );
103110 parseHeapInfo (heapInfo , pools );
104111 for (var p : pools .values ()) {
105- if (p .type ().equalsIgnoreCase ("HEAP" ) || p .name ().toLowerCase ().contains ("heap" )) {
106- heapUsed += p .used ();
107- if (p .max () > 0 ) heapMax = Math .max (heapMax , p .max ());
108- }
109- }
110- // Fallback: sum all pool used values
111- if (heapUsed == 0 ) {
112- for (var p : pools .values ()) heapUsed += p .used ();
112+ heapUsed += p .used ();
113+ if (p .max () > 0 ) heapMax = Math .max (heapMax , p .max ());
113114 }
114- } catch (RuntimeException ignored ) {}
115+ } catch (RuntimeException e ) {
116+ warnings .add ("GC.heap_info failed: " + e .getMessage ());
117+ failures ++;
118+ }
115119
116- // GC via jcmd (parse from VM.flags to detect GC, count from GC.heap_info not available β use defaults)
120+ // GC stats via jcmd β parse collector info
117121 List <JvmSnapshot .GcInfo > gcInfos = new ArrayList <>();
118122 long totalGcCount = 0 , totalGcTime = 0 ;
119123 String gcAlgorithm = "unknown" ;
124+ try {
125+ // Use VM.info which contains GC collector stats
126+ String vmInfo = JcmdExecutor .execute (pid , "VM.info" );
127+ for (String line : vmInfo .split ("\n " )) {
128+ String t = line .trim ();
129+ // Parse: "garbage-first heap" or "PS Young Generation" etc.
130+ if (t .contains ("invocations" ) && t .contains ("ms" )) {
131+ // Try to parse GC invocation lines from VM.info
132+ var matcher = Pattern .compile ("(\\ d+)\\ s+invocations.*?(\\ d+)\\ s*ms" ).matcher (t );
133+ if (matcher .find ()) {
134+ long count = Long .parseLong (matcher .group (1 ));
135+ long time = Long .parseLong (matcher .group (2 ));
136+ totalGcCount += count ;
137+ totalGcTime += time ;
138+ }
139+ }
140+ }
141+ } catch (RuntimeException e ) {
142+ warnings .add ("VM.info failed (GC stats unavailable): " + e .getMessage ());
143+ failures ++;
144+ }
120145
121146 // VM version
122147 String vmName = "" , vmVersion = "" ;
@@ -127,7 +152,10 @@ public static JvmSnapshot collectRemote(long pid) {
127152 if (vmName .isEmpty () && !t .isEmpty ()) vmName = t ;
128153 else if (vmVersion .isEmpty () && t .contains ("build" )) vmVersion = t ;
129154 }
130- } catch (RuntimeException ignored ) {}
155+ } catch (RuntimeException e ) {
156+ warnings .add ("VM.version failed: " + e .getMessage ());
157+ failures ++;
158+ }
131159
132160 // VM uptime
133161 long uptimeMs = 0 ;
@@ -141,7 +169,10 @@ public static JvmSnapshot collectRemote(long pid) {
141169 break ;
142170 } catch (NumberFormatException ignored2 ) {}
143171 }
144- } catch (RuntimeException ignored ) {}
172+ } catch (RuntimeException e ) {
173+ warnings .add ("VM.uptime failed: " + e .getMessage ());
174+ failures ++;
175+ }
145176
146177 // VM flags β detect GC algorithm
147178 List <String > vmFlags = new ArrayList <>();
@@ -159,7 +190,10 @@ public static JvmSnapshot collectRemote(long pid) {
159190 }
160191 }
161192 }
162- } catch (RuntimeException ignored ) {}
193+ } catch (RuntimeException e ) {
194+ warnings .add ("VM.flags failed: " + e .getMessage ());
195+ failures ++;
196+ }
163197
164198 // Threads via Thread.print
165199 int threadCount = 0 , daemonCount = 0 ;
@@ -177,45 +211,41 @@ public static JvmSnapshot collectRemote(long pid) {
177211 String state = t .split (":\\ s*" )[1 ].split ("\\ s+" )[0 ];
178212 threadStates .merge (state , 1 , Integer ::sum );
179213 }
180- if (t .contains ("Found one Java-level deadlock" ) || t . contains ( "Found " ) && t .contains ("deadlock" )) {
214+ if (t .contains ("Found" ) && t .contains ("deadlock" )) {
181215 deadlockCount ++;
182216 }
183217 }
184- } catch (RuntimeException ignored ) {}
218+ } catch (RuntimeException e ) {
219+ warnings .add ("Thread.print failed: " + e .getMessage ());
220+ failures ++;
221+ }
185222
186- // Class loading via jcmd VM.classloader_stats
187- int loadedClasses = 0 ;
188- long totalLoaded = 0 , unloaded = 0 ;
189- try {
190- String classOut = JcmdExecutor .execute (pid , "VM.classloaders" );
191- // Count lines with class counts
192- for (String line : classOut .split ("\n " )) {
193- if (line .trim ().matches (".*\\ d+.*classes.*" )) loadedClasses ++;
223+ // Print warnings to stderr
224+ if (failures > 0 ) {
225+ System .err .println ("[Argus] WARNING: " + failures + " jcmd call(s) failed for PID " + pid + ":" );
226+ for (String w : warnings ) {
227+ System .err .println (" β " + w );
228+ }
229+ if (failures >= 4 ) {
230+ System .err .println ("[Argus] Most data unavailable. Are you running as the same user as PID " + pid + "?" );
231+ System .err .println ("[Argus] Try: sudo argus doctor " + pid );
194232 }
195- } catch ( RuntimeException ignored ) {}
233+ }
196234
197235 return new JvmSnapshot (
198236 heapUsed , heapMax , heapCommitted , nonHeapUsed ,
199237 Map .copyOf (pools ),
200238 List .copyOf (gcInfos ), totalGcCount , totalGcTime , uptimeMs ,
201239 -1 , -1 , Runtime .getRuntime ().availableProcessors (),
202- threadCount , daemonCount , threadCount , // peak = current for remote
240+ threadCount , daemonCount , threadCount ,
203241 Map .copyOf (threadStates ), deadlockCount ,
204- List .of (), // buffers not available via jcmd
205- loadedClasses , totalLoaded , unloaded ,
206- 0 , // finalizer not available via jcmd
242+ List .of (),
243+ 0 , 0 , 0 , 0 ,
207244 vmName , vmVersion , gcAlgorithm ,
208245 List .copyOf (vmFlags )
209246 );
210247 }
211248
212- /**
213- * Parse GC.heap_info output into pool map.
214- * Format varies by GC but generally:
215- * garbage-first heap total 262144K, used 45678K [...]
216- * region size 1024K, 10 young, 2 survivors
217- * Metaspace used 34567K, ...
218- */
219249 private static void parseHeapInfo (String output , Map <String , JvmSnapshot .PoolInfo > pools ) {
220250 if (output == null ) return ;
221251 Pattern sizePattern = Pattern .compile ("(\\ w[\\ w\\ s-]*)\\ s+(?:total\\ s+)?(\\ d+)K.*used\\ s+(\\ d+)K" );
0 commit comments