@@ -33,6 +33,7 @@ type M3App struct {
3333 accessLogM3 * capture.AccessLogM3
3434 AsyncDotNetGCCapture * capture.DotnetGCAsync
3535 dotnetGCReadySeen map [int ]bool
36+ nodeGCTracker * capture.NodeGCTracker
3637}
3738
3839func NewM3App () * M3App {
@@ -49,6 +50,7 @@ func NewM3App() *M3App {
4950 accessLogM3 : accessLogM3 ,
5051 AsyncDotNetGCCapture : capture .NewDotnetGCAsync (dotNetGCBaseDir ),
5152 dotnetGCReadySeen : make (map [int ]bool ),
53+ nodeGCTracker : capture .NewNodeGCTracker (),
5254 }
5355}
5456
@@ -261,7 +263,8 @@ func (m3 *M3App) captureAndTransmit(pids map[int]string, endpoint string) {
261263 var gcPath string
262264 appRuntime := runtimeByPid [pid ]
263265
264- if appRuntime == "dotnet" {
266+ switch appRuntime {
267+ case "dotnet" :
265268 // High-level flow for .NET GC in M3 mode:
266269 // 1) Reconcile() starts/keeps one long-running async Dotnet GC collector per active .NET PID.
267270 // 2) In the same RunSingle cycle, uploadDotnetGCM3() reads collector output from session log path.
@@ -275,7 +278,10 @@ func (m3 *M3App) captureAndTransmit(pids map[int]string, endpoint string) {
275278
276279 logger .Log ("uploading dotnet heap stats for pid %d" , pid )
277280 uploadDotnetHeapM3 (endpoint , pid )
278- } else {
281+ case "nodejs" :
282+ logger .Log ("Using Node.js runtime for pid %d" , pid )
283+ gcPath = m3 .captureNodeM3 (endpoint , pid )
284+ default :
279285 logger .Log ("Using Java runtime for pid %d" , pid )
280286 logger .Log ("uploading gc log for pid %d" , pid )
281287 gcPath = uploadGCLogM3 (endpoint , pid )
@@ -304,6 +310,12 @@ func (m3 *M3App) captureAndTransmit(pids map[int]string, endpoint string) {
304310 delete (m3 .dotnetGCReadySeen , pid )
305311 }
306312 }
313+
314+ // Drop Node.js GC offset state for PIDs no longer monitored, so it does
315+ // not leak across process restarts / PID reuse.
316+ if m3 .nodeGCTracker != nil {
317+ m3 .nodeGCTracker .RetainOnly (pids )
318+ }
307319 }
308320
309321 topResult := <- top
@@ -845,6 +857,67 @@ Resp: %s
845857 return gcPath
846858}
847859
860+ func (m3 * M3App ) captureNodeM3 (endpoint string , pid int ) string {
861+ nodeCtx := capture .ResolveNodeCapture (pid )
862+
863+ outDir := filepath .Join ("yc-node-m3" , strconv .Itoa (pid ))
864+ if err := os .MkdirAll (outDir , 0o755 ); err != nil {
865+ logger .Log ("WARNING: failed creating node m3 dir %s: %s" , outDir , err )
866+ return ""
867+ }
868+
869+ stdoutPath := ""
870+ if capture .NodeHasTraceGCFlag (pid ) {
871+ // ResolveNodeGCStdoutPath (not the raw platform resolver) so a
872+ // configured -nodejsGCLogPath is honored here too - this path is used
873+ // below to tell uploadAppLogM3 which file to exclude from generic
874+ // app-log auto-discovery, not just by NodeGC.Run()'s own capture.
875+ if p , err := capture .ResolveNodeGCStdoutPath (pid ); err == nil {
876+ stdoutPath = p
877+ }
878+ }
879+
880+ pidParam := strconv .Itoa (pid )
881+
882+ // GC log (continuous delta; no dumpGC fallback under M3).
883+ nodeGC := & capture.NodeGC {
884+ Pid : pid ,
885+ Ctx : nodeCtx ,
886+ OutDir : outDir ,
887+ Tracker : m3 .nodeGCTracker ,
888+ M3 : true ,
889+ }
890+ nodeGC .SetEndpointParam ("pid" , pidParam )
891+ runNodeM3Capture (endpoint , "GC LOG" , nodeGC )
892+
893+ // Process overview
894+ nodePO := & capture.NodeProcessOverview {Pid : pid , Ctx : nodeCtx , OutDir : outDir }
895+ nodePO .SetEndpointParam ("pid" , pidParam )
896+ runNodeM3Capture (endpoint , "PROCESS OVERVIEW" , nodePO )
897+
898+ // Heap summary (safe every cycle).
899+ nodeHS := & capture.NodeHeapSummary {Pid : pid , Ctx : nodeCtx , OutDir : outDir }
900+ nodeHS .SetEndpointParam ("pid" , pidParam )
901+ runNodeM3Capture (endpoint , "HDSUB" , nodeHS )
902+
903+ return stdoutPath
904+ }
905+
906+ func runNodeM3Capture (endpoint , label string , task capture.Task ) {
907+ ch := capture .GoCapture (endpoint , capture .WrapRun (task ))
908+ if ch == nil {
909+ return
910+ }
911+ result := <- ch
912+ logger .Log (
913+ `NODE %s DATA
914+ Is transmission completed: %t
915+ Resp: %s
916+
917+ --------------------------------
918+ ` , label , result .Ok , result .Msg )
919+ }
920+
848921func uploadDotnetThreadM3 (endpoint string , pid int ) {
849922 dotnetTDCapture := & capture.DotnetThread {
850923 Pid : pid ,
0 commit comments