@@ -122,9 +122,12 @@ type Collector struct {
122122 store darwinBookmarkStore
123123 createWatcher darwinReportWatcherFactory
124124 watcher darwinReportWatcher
125- knownDirs map [string ]reportDirectory
126- retryDirs map [string ]reportDirectory
127- stagedRuntime * stagedDarwinScanRuntime
125+ // watcherEverAttached distinguishes the initial attach from later ones so
126+ // only the latter count as restarts.
127+ watcherEverAttached bool
128+ knownDirs map [string ]reportDirectory
129+ retryDirs map [string ]reportDirectory
130+ stagedRuntime * stagedDarwinScanRuntime
128131
129132 state * darwinBookmarkState
130133 stagedScan * stagedDarwinScan
@@ -137,6 +140,8 @@ type Collector struct {
137140 identityRetention time.Duration
138141 maxAcknowledged int
139142
143+ counters collectorCounters
144+
140145 started bool
141146 closed bool
142147 cancel context.CancelFunc
@@ -286,6 +291,45 @@ func (c *Collector) Pending() []Event {
286291 return events
287292}
288293
294+ // Stats returns an aggregate snapshot of collector health. It never acquires
295+ // scanMu, which a scan holds across report I/O, so it stays responsive while
296+ // scanning. Gauges owned by scanMu are read from their atomic mirrors.
297+ func (c * Collector ) Stats () CollectorStats {
298+ stats := CollectorStats {
299+ PendingEventsMax : maxDarwinPendingEvents ,
300+ TrackedFilesMax : maxDarwinTotalFiles ,
301+ TrackedDirectoriesMax : maxDarwinDirectories ,
302+ AcknowledgedIdentitiesMax : c .maxAcknowledged ,
303+ RetryDirectories : int (c .counters .retryDirectories .Load ()),
304+ WatcherActive : c .counters .watcherActive .Load (),
305+ PersistenceErrors : c .counters .persistenceErrors .Load (),
306+ CapacityDeferrals : c .counters .capacityDeferrals .Load (),
307+ BaselineSuppressedFirstRun : c .counters .baselineSuppressedFirstRun .Load (),
308+ BaselineSuppressedAfterSaturation : c .counters .baselineSuppressedAfterSaturation .Load (),
309+ FSEventsDrops : c .counters .fseventsDrops .Load (),
310+ WatcherErrors : c .counters .watcherErrors .Load (),
311+ WatcherRestarts : c .counters .watcherRestarts .Load (),
312+ }
313+
314+ c .stateMu .Lock ()
315+ defer c .stateMu .Unlock ()
316+ stats .PendingEvents = len (c .state .Pending )
317+ stats .AcknowledgedIdentities = len (c .state .Acknowledged )
318+ stats .TrackedDirectories = len (c .state .Directories )
319+ stats .BookmarkUnsaved = c .unsaved
320+ stats .BookmarkStagePending = c .stagedScan != nil
321+ for _ , dirState := range c .state .Directories {
322+ if dirState == nil {
323+ continue
324+ }
325+ stats .TrackedFiles += len (dirState .Files )
326+ if dirState .Saturated {
327+ stats .SaturatedDirectories ++
328+ }
329+ }
330+ return stats
331+ }
332+
289333// Ack atomically moves pending IDs to retained acknowledgement state. Unknown
290334// and already acknowledged IDs are no-ops. If persistence fails, no in-memory
291335// event is removed and callers can safely retry.
@@ -337,6 +381,7 @@ func (c *Collector) Ack(ids []string) error {
337381 c .stateMu .Unlock ()
338382
339383 if err := c .store .Save (next ); err != nil {
384+ c .counters .persistenceErrors .Add (1 )
340385 c .stateMu .Lock ()
341386 c .releaseCommitLocked (reservation )
342387 c .stateMu .Unlock ()
@@ -412,11 +457,23 @@ func (c *Collector) run(ctx context.Context) {
412457 c .scanMu .Unlock ()
413458 continue
414459 }
460+ c .recordWatcherError (err )
415461 log .Warnf ("macOS DiagnosticReports watcher error: %v" , err )
416462 }
417463 }
418464}
419465
466+ // recordWatcherError separates dropped-event notifications, which imply lost
467+ // change notifications, from other asynchronous watcher failures.
468+ func (c * Collector ) recordWatcherError (err error ) {
469+ var dropped * fseventsDroppedError
470+ if errors .As (err , & dropped ) {
471+ c .counters .fseventsDrops .Add (1 )
472+ return
473+ }
474+ c .counters .watcherErrors .Add (1 )
475+ }
476+
420477// watcherChannelsLocked returns nil-safe watcher channels while scanMu is held.
421478func (c * Collector ) watcherChannelsLocked () (<- chan string , <- chan error ) {
422479 if c .watcher == nil {
@@ -429,6 +486,7 @@ func (c *Collector) watcherChannelsLocked() (<-chan string, <-chan error) {
429486func (c * Collector ) closeWatcherLocked () error {
430487 watcher := c .watcher
431488 c .watcher = nil
489+ c .counters .watcherActive .Store (false )
432490 if watcher == nil {
433491 return nil
434492 }
@@ -447,6 +505,11 @@ type directoryScanResult struct {
447505 BaselineIncidentIDs map [string ]struct {}
448506 BaselineCompletions []darwinBaselineReportCompletion
449507 Deliverables map [string ]darwinScanDeliverable
508+ // BaselineSuppressedFirstRun and BaselineSuppressedAfterSaturation count
509+ // reports recorded without delivery, split by why the directory was being
510+ // baselined.
511+ BaselineSuppressedFirstRun int
512+ BaselineSuppressedAfterSaturation int
450513}
451514
452515type darwinBaselineReportCompletion struct {
@@ -469,6 +532,9 @@ type darwinReportSource struct {
469532type darwinScanIncidents struct {
470533 baselineIDs map [string ]struct {}
471534 deliverables map [string ]darwinScanDeliverable
535+ // capacityDeferrals counts events reconcile could not queue because
536+ // pending delivery was full.
537+ capacityDeferrals int
472538}
473539
474540// scanOnce refreshes report directories and reconciles their current contents.
@@ -503,6 +569,11 @@ func (c *Collector) ensureWatcherLocked() {
503569 return
504570 }
505571 c .watcher = watcher
572+ c .counters .watcherActive .Store (true )
573+ if c .watcherEverAttached {
574+ c .counters .watcherRestarts .Add (1 )
575+ }
576+ c .watcherEverAttached = true
506577}
507578
508579// processWatcherEvent coalesces queued changes and scans only affected known directories.
@@ -588,9 +659,10 @@ func (c *Collector) retryDirectoriesLocked() []reportDirectory {
588659// restoreWatcherLocked recreates watcher coverage after an asynchronous watcher failure.
589660func (c * Collector ) restoreWatcherLocked () {
590661 c .ensureWatcherLocked ()
591- if c .watcher ! = nil {
592- c . refreshWatchedDirectoriesLocked ( c . knownDirectoriesLocked ())
662+ if c .watcher = = nil {
663+ return
593664 }
665+ c .refreshWatchedDirectoriesLocked (c .knownDirectoriesLocked ())
594666}
595667
596668// knownDirectoriesLocked returns a stable snapshot of currently discovered report directories.
@@ -637,6 +709,8 @@ func (c *Collector) scanDirectoriesLocked(ctx context.Context, dirs []reportDire
637709
638710 result , err := c .scanDirectory (ctx , dir , candidate )
639711 dirty = dirty || result .StateChanged
712+ addUint64 (& c .counters .baselineSuppressedFirstRun , result .BaselineSuppressedFirstRun )
713+ addUint64 (& c .counters .baselineSuppressedAfterSaturation , result .BaselineSuppressedAfterSaturation )
640714 retryResults [directoryRuntimeKey (dir .path )] = result .ShouldRetry
641715 results = append (results , result )
642716 incidents .addDirectoryResult (result )
@@ -652,6 +726,7 @@ func (c *Collector) scanDirectoriesLocked(ctx context.Context, dirs []reportDire
652726 if incidents .reconcile (candidate , retryResults , c .currentTime ()) {
653727 dirty = true
654728 }
729+ addUint64 (& c .counters .capacityDeferrals , incidents .capacityDeferrals )
655730 for index := range results {
656731 result := & results [index ]
657732 dirState := candidate .Directories [result .DirectoryKey ]
@@ -737,6 +812,7 @@ func (i *darwinScanIncidents) reconcile(state *darwinBookmarkState, retryResults
737812 continue
738813 }
739814 if len (state .Pending ) >= maxDarwinPendingEvents {
815+ i .capacityDeferrals ++
740816 for key := range deliverable .ContributingDirKeys {
741817 retryResults [key ] = true
742818 }
@@ -814,6 +890,7 @@ func (c *Collector) persistCandidateLocked(
814890 c .stateMu .Unlock ()
815891
816892 if err := c .store .Save (cloneDarwinBookmarkState (staged .candidate )); err != nil {
893+ c .counters .persistenceErrors .Add (1 )
817894 c .stagedRuntime = runtime
818895 c .stateMu .Lock ()
819896 c .stagedScan = staged
@@ -851,6 +928,7 @@ func (c *Collector) persistStagedScanLocked() ([]reportDirectory, bool) {
851928 c .stateMu .Unlock ()
852929
853930 if err := c .store .Save (cloneDarwinBookmarkState (staged .candidate )); err != nil {
931+ c .counters .persistenceErrors .Add (1 )
854932 c .stateMu .Lock ()
855933 c .releaseCommitLocked (reservation )
856934 c .stateMu .Unlock ()
@@ -966,6 +1044,7 @@ func (c *Collector) refreshKnownDirectoriesLocked(dirs []reportDirectory) {
9661044 delete (c .retryDirs , key )
9671045 }
9681046 }
1047+ c .counters .retryDirectories .Store (int64 (len (c .retryDirs )))
9691048}
9701049
9711050// refreshWatchedDirectoriesLocked synchronizes FSEvents coverage with watchable directories.
@@ -994,6 +1073,7 @@ func (c *Collector) setDirectoryRetryLocked(dir reportDirectory, shouldRetry boo
9941073 } else {
9951074 delete (c .retryDirs , key )
9961075 }
1076+ c .counters .retryDirectories .Store (int64 (len (c .retryDirs )))
9971077}
9981078
9991079// scanDirectoryInternal securely reads reports and updates only the private
@@ -1060,6 +1140,10 @@ func (c *Collector) scanDirectoryInternal(ctx context.Context, dir reportDirecto
10601140 }
10611141
10621142 baselineScan := ! dirState .Initialized || dirState .Saturated
1143+ // A directory that never completed a baseline has never delivered anything,
1144+ // so attribute its suppressions to first run even when it is also
1145+ // saturated. Saturation only costs real events once delivery was working.
1146+ saturationBaseline := dirState .Initialized && dirState .Saturated
10631147 presentFiles := make (map [string ]struct {})
10641148
10651149 for _ , entry := range entries {
@@ -1126,6 +1210,15 @@ func (c *Collector) scanDirectoryInternal(ctx context.Context, dir reportDirecto
11261210 }
11271211
11281212 if baselineScan || suppressOnSuccess {
1213+ // Only a baseline scan counts. The suppressOnSuccess-only path
1214+ // completes a report that an earlier baseline already counted.
1215+ if baselineScan {
1216+ if saturationBaseline {
1217+ result .BaselineSuppressedAfterSaturation ++
1218+ } else {
1219+ result .BaselineSuppressedFirstRun ++
1220+ }
1221+ }
11291222 if suppressOnSuccess && ! baselineScan {
11301223 result .BaselineCompletions = append (result .BaselineCompletions , darwinBaselineReportCompletion {
11311224 Name : name ,
0 commit comments