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 }