@@ -186,30 +186,47 @@ type GarbageCollection struct {
186186 EndTime float64 `json:"endTime"`
187187}
188188
189+ // HeapTrackingReadyTimeout is how long Start waits for the trackingStart event
190+ // to confirm the event pipeline is healthy. Default 5s.
191+ var HeapTrackingReadyTimeout = 5 * time .Second
192+
189193// HeapTrackingResult holds the collected heap tracking data.
190- // Note: Heap.trackingStart/trackingComplete carry 50-200MB+ snapshot payloads
191- // that crash iwdp's WebSocket relay, so we intentionally skip them and only
192- // collect lightweight garbageCollected events. Use the dedicated heap_snapshot
193- // tool for snapshots (it uses Heap.snapshot which returns data in-band).
194194type HeapTrackingResult struct {
195- GCEvents []GarbageCollection `json:"gcEvents,omitempty"`
195+ GCEvents []GarbageCollection `json:"gcEvents,omitempty"`
196+ PipelineHealthy bool `json:"pipelineHealthy"`
196197}
197198
198199// HeapTrackingCollector collects Heap GC events during tracking.
199- // Snapshot events (trackingStart/trackingComplete) are intentionally ignored
200- // because their 50-200MB+ payloads crash iwdp's WebSocket relay.
200+ //
201+ // Heap.startTracking triggers a trackingStart event carrying a full heap
202+ // snapshot (50-200MB+). If iwdp can relay this massive event successfully,
203+ // the event pipeline is healthy and subsequent garbageCollected events will
204+ // arrive. If not, the pipeline is broken and no events will be captured.
205+ // Start() waits for the trackingStart event to diagnose this.
201206type HeapTrackingCollector struct {
202- mu sync.Mutex
203- gcEvents []GarbageCollection
204- started bool
207+ mu sync.Mutex
208+ gcEvents []GarbageCollection
209+ started bool
210+ ready chan struct {}
211+ pipelineHealthy bool
205212}
206213
207214// NewHeapTrackingCollector creates a new HeapTrackingCollector.
208215func NewHeapTrackingCollector () * HeapTrackingCollector {
209216 return & HeapTrackingCollector {}
210217}
211218
219+ // PipelineHealthy reports whether the event pipeline survived the massive
220+ // trackingStart event. If false, garbageCollected events won't arrive.
221+ func (c * HeapTrackingCollector ) PipelineHealthy () bool {
222+ c .mu .Lock ()
223+ defer c .mu .Unlock ()
224+ return c .pipelineHealthy
225+ }
226+
212227// Start begins heap tracking, collecting garbageCollected events.
228+ // It waits up to HeapTrackingReadyTimeout for the trackingStart event to
229+ // confirm the event pipeline is healthy.
213230func (c * HeapTrackingCollector ) Start (ctx context.Context , client * webkit.Client ) error {
214231 c .mu .Lock ()
215232 if c .started {
@@ -218,8 +235,27 @@ func (c *HeapTrackingCollector) Start(ctx context.Context, client *webkit.Client
218235 }
219236 c .started = true
220237 c .gcEvents = nil
238+ c .ready = make (chan struct {})
239+ c .pipelineHealthy = false
221240 c .mu .Unlock ()
222241
242+ // trackingStart handler: signals that the massive snapshot event arrived
243+ // at our client, confirming the event pipeline is healthy. We discard
244+ // the snapshot data (50-200MB+) — use heap_snapshot for snapshots.
245+ client .OnEvent ("Heap.trackingStart" , func (method string , params json.RawMessage ) {
246+ c .mu .Lock ()
247+ c .pipelineHealthy = true
248+ ch := c .ready
249+ c .mu .Unlock ()
250+ if ch != nil {
251+ select {
252+ case <- ch :
253+ default :
254+ close (ch )
255+ }
256+ }
257+ })
258+
223259 client .OnEvent ("Heap.garbageCollected" , func (method string , params json.RawMessage ) {
224260 var evt struct {
225261 Collection GarbageCollection `json:"collection"`
@@ -231,8 +267,34 @@ func (c *HeapTrackingCollector) Start(ctx context.Context, client *webkit.Client
231267 }
232268 })
233269
270+ // Enable Heap domain — required for events to be dispatched.
271+ _ , _ = client .Send (ctx , "Heap.enable" , nil )
272+
273+ // Pre-GC: reduce heap size before startTracking to minimize the snapshot
274+ // payload in the trackingStart event. A smaller snapshot is more likely
275+ // to survive iwdp's WebSocket relay (which has a 64MB message limit).
276+ _ , _ = client .Send (ctx , "Heap.gc" , nil )
277+
234278 _ , err := client .Send (ctx , "Heap.startTracking" , nil )
235- return err
279+ if err != nil {
280+ return err
281+ }
282+
283+ // Wait for trackingStart event to confirm the event pipeline survived
284+ // the massive snapshot payload. If it arrives, GC events will work.
285+ // If not, iwdp couldn't relay the 50-200MB+ event and the pipeline is broken.
286+ c .mu .Lock ()
287+ ch := c .ready
288+ c .mu .Unlock ()
289+ select {
290+ case <- ch :
291+ // Pipeline healthy — trackingStart event arrived.
292+ case <- time .After (HeapTrackingReadyTimeout ):
293+ // Timeout — event pipeline likely broken by massive snapshot.
294+ case <- ctx .Done ():
295+ }
296+
297+ return nil
236298}
237299
238300// Stop stops heap tracking and returns collected GC events.
@@ -244,6 +306,7 @@ func (c *HeapTrackingCollector) Stop(ctx context.Context, client *webkit.Client)
244306 c .mu .Unlock ()
245307 return & HeapTrackingResult {}, nil
246308 }
309+ healthy := c .pipelineHealthy
247310 c .mu .Unlock ()
248311
249312 // stopTracking triggers trackingComplete with a massive snapshot payload
@@ -253,7 +316,8 @@ func (c *HeapTrackingCollector) Stop(ctx context.Context, client *webkit.Client)
253316 c .mu .Lock ()
254317 c .started = false
255318 result := & HeapTrackingResult {
256- GCEvents : make ([]GarbageCollection , len (c .gcEvents )),
319+ GCEvents : make ([]GarbageCollection , len (c .gcEvents )),
320+ PipelineHealthy : healthy ,
257321 }
258322 copy (result .GCEvents , c .gcEvents )
259323 c .gcEvents = nil
0 commit comments