github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/runtimestats/stats.go (about)

     1  package runtimestats
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"runtime"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/chat/globals"
    11  	"github.com/keybase/client/go/chat/utils"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/keybase1"
    14  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    15  	"golang.org/x/sync/errgroup"
    16  )
    17  
    18  type Runner struct {
    19  	globals.Contextified
    20  	sync.Mutex
    21  
    22  	started    bool
    23  	stopCh     chan struct{}
    24  	eg         errgroup.Group
    25  	perfEvents []keybase1.PerfEvent
    26  }
    27  
    28  var _ libkb.RuntimeStats = (*Runner)(nil)
    29  
    30  func NewRunner(g *globals.Context) *Runner {
    31  	r := &Runner{
    32  		Contextified: globals.NewContextified(g),
    33  	}
    34  	r.G().PushShutdownHook(func(mctx libkb.MetaContext) error {
    35  		<-r.Stop(mctx.Ctx())
    36  		return nil
    37  	})
    38  	return r
    39  }
    40  
    41  func (r *Runner) debug(ctx context.Context, msg string, args ...interface{}) {
    42  	r.G().Log.CDebugf(ctx, "RuntimeStats.Runner: %s", fmt.Sprintf(msg, args...))
    43  }
    44  
    45  func (r *Runner) Start(ctx context.Context) {
    46  	defer r.G().CTrace(ctx, "Runner.Start", nil)()
    47  	r.Lock()
    48  	defer r.Unlock()
    49  	if !r.G().Env.GetRuntimeStatsEnabled() {
    50  		r.debug(ctx, "not starting, not enabled in env")
    51  		return
    52  	}
    53  	if r.started {
    54  		return
    55  	}
    56  	r.stopCh = make(chan struct{})
    57  	r.started = true
    58  
    59  	r.eg.Go(func() error { return r.statsLoop(r.stopCh) })
    60  }
    61  
    62  func (r *Runner) Stop(ctx context.Context) chan struct{} {
    63  	defer r.G().CTrace(ctx, "Runner.Stop", nil)()
    64  	r.Lock()
    65  	defer r.Unlock()
    66  	ch := make(chan struct{})
    67  	if r.started {
    68  		close(r.stopCh)
    69  		r.started = false
    70  		go func() {
    71  			err := r.eg.Wait()
    72  			if err != nil {
    73  				r.debug(ctx, "Couldn't wait while stopping stats: %+v", err)
    74  			}
    75  			close(ch)
    76  		}()
    77  	} else {
    78  		close(ch)
    79  	}
    80  	return ch
    81  }
    82  
    83  func (r *Runner) statsLoop(stopCh chan struct{}) error {
    84  	ctx := context.Background()
    85  	r.updateStats(ctx)
    86  	for {
    87  		select {
    88  		case <-time.After(4 * time.Second):
    89  			r.updateStats(ctx)
    90  		case <-stopCh:
    91  			r.G().NotifyRouter.HandleRuntimeStatsUpdate(ctx, nil)
    92  			return nil
    93  		}
    94  	}
    95  }
    96  
    97  func (r *Runner) addDbStats(
    98  	ctx context.Context, dbType keybase1.DbType, db *libkb.JSONLocalDb,
    99  	stats *keybase1.RuntimeStats) {
   100  	var s keybase1.DbStats
   101  	s.Type = dbType
   102  	var err error
   103  	s.MemCompActive, s.TableCompActive, err = db.CompactionStats()
   104  	if err != nil {
   105  		r.debug(ctx, "Couldn't get compaction stats for %s: %+v", dbType, err)
   106  		return
   107  	}
   108  	stats.DbStats = append(stats.DbStats, s)
   109  }
   110  
   111  // GetProcessStats gets CPU and memory stats for the running process.
   112  func GetProcessStats(t keybase1.ProcessType) keybase1.ProcessRuntimeStats {
   113  	stats := getStats().Export()
   114  	stats.Type = t
   115  	var memstats runtime.MemStats
   116  	runtime.ReadMemStats(&memstats)
   117  	stats.Goheap = utils.PresentBytes(int64(memstats.HeapAlloc))
   118  	stats.Goheapsys = utils.PresentBytes(int64(memstats.HeapSys))
   119  	stats.Goreleased = utils.PresentBytes(int64(memstats.HeapReleased))
   120  	return stats
   121  }
   122  
   123  func (r *Runner) PushPerfEvent(event keybase1.PerfEvent) {
   124  	r.Lock()
   125  	defer r.Unlock()
   126  	r.perfEvents = append(r.perfEvents, event)
   127  }
   128  
   129  func (r *Runner) updateStats(ctx context.Context) {
   130  	var stats keybase1.RuntimeStats
   131  	r.Lock()
   132  	stats.PerfEvents = r.perfEvents
   133  	r.perfEvents = []keybase1.PerfEvent{}
   134  	r.Unlock()
   135  
   136  	serviceStats := GetProcessStats(keybase1.ProcessType_MAIN)
   137  	stats.ProcessStats = append(stats.ProcessStats, serviceStats)
   138  
   139  	stats.DbStats = make([]keybase1.DbStats, 0, 2)
   140  	r.addDbStats(ctx, keybase1.DbType_MAIN, r.G().LocalDb, &stats)
   141  	r.addDbStats(ctx, keybase1.DbType_CHAT, r.G().LocalChatDb, &stats)
   142  
   143  	stats.ConvLoaderActive = r.G().ConvLoader.IsBackgroundActive()
   144  	stats.SelectiveSyncActive = r.G().Indexer.IsBackgroundActive()
   145  
   146  	xp := r.G().ConnectionManager.LookupByClientType(keybase1.ClientType_KBFS)
   147  	if xp != nil {
   148  		sfsCli := &keybase1.SimpleFSClient{
   149  			Cli: rpc.NewClient(xp, libkb.NewContextifiedErrorUnwrapper(
   150  				r.G().ExternalG()), nil),
   151  		}
   152  
   153  		sfsStats, err := sfsCli.SimpleFSGetStats(ctx)
   154  		if err != nil {
   155  			r.debug(ctx, "KBFS stats error: %+v", err)
   156  		} else {
   157  			stats.ProcessStats = append(
   158  				stats.ProcessStats, sfsStats.ProcessStats)
   159  			stats.DbStats = append(stats.DbStats, sfsStats.RuntimeDbStats...)
   160  		}
   161  	}
   162  
   163  	r.G().NotifyRouter.HandleRuntimeStatsUpdate(ctx, &stats)
   164  	r.debug(ctx, "update: %+v", stats)
   165  }
   166  
   167  type statsResult struct {
   168  	TotalCPU      int
   169  	TotalResident int64
   170  	TotalVirtual  int64
   171  	TotalFree     int64
   172  }
   173  
   174  func (r statsResult) Export() keybase1.ProcessRuntimeStats {
   175  	return keybase1.ProcessRuntimeStats{
   176  		Cpu:              fmt.Sprintf("%.2f%%", float64(r.TotalCPU)/100.0),
   177  		Resident:         utils.PresentBytes(r.TotalResident),
   178  		Virt:             utils.PresentBytes(r.TotalVirtual),
   179  		Free:             utils.PresentBytes(r.TotalFree),
   180  		CpuSeverity:      r.cpuSeverity(),
   181  		ResidentSeverity: r.residentSeverity(),
   182  	}
   183  }
   184  
   185  func (r statsResult) cpuSeverity() keybase1.StatsSeverityLevel {
   186  	switch {
   187  	case r.TotalCPU >= 10000:
   188  		return keybase1.StatsSeverityLevel_SEVERE
   189  	case r.TotalCPU >= 6000:
   190  		return keybase1.StatsSeverityLevel_WARNING
   191  	default:
   192  		return keybase1.StatsSeverityLevel_NORMAL
   193  	}
   194  }
   195  
   196  func (r statsResult) residentSeverity() keybase1.StatsSeverityLevel {
   197  	val := r.TotalResident / 1e6
   198  	switch {
   199  	case val >= 900:
   200  		return keybase1.StatsSeverityLevel_SEVERE
   201  	case val >= 700:
   202  		return keybase1.StatsSeverityLevel_WARNING
   203  	default:
   204  		return keybase1.StatsSeverityLevel_NORMAL
   205  	}
   206  }