1import * as React from 'react' 2import * as Container from '../util/container' 3import * as Kb from '../common-adapters' 4import * as Styles from '../styles' 5import {isIPhoneX} from '../constants/platform' 6import * as RPCTypes from '../constants/types/rpc-gen' 7// @ts-ignore strict 8import lagRadar from 'lag-radar' 9 10type Props = { 11 stats: RPCTypes.RuntimeStats 12} 13 14const yesNo = (v?: boolean) => (v ? 'YES' : 'NO') 15 16const severityStyle = (s: RPCTypes.StatsSeverityLevel) => { 17 switch (s) { 18 case RPCTypes.StatsSeverityLevel.normal: 19 return styles.statNormal 20 case RPCTypes.StatsSeverityLevel.warning: 21 return styles.statWarning 22 case RPCTypes.StatsSeverityLevel.severe: 23 return styles.statSevere 24 } 25 return styles.statNormal 26} 27 28const processTypeString = (s: RPCTypes.ProcessType) => { 29 switch (s) { 30 case RPCTypes.ProcessType.main: 31 return 'Service' 32 case RPCTypes.ProcessType.kbfs: 33 return 'KBFS' 34 } 35} 36 37const dbTypeString = (s: RPCTypes.DbType) => { 38 switch (s) { 39 case RPCTypes.DbType.main: 40 return 'Core' 41 case RPCTypes.DbType.chat: 42 return 'Chat' 43 case RPCTypes.DbType.fsBlockCache: 44 return 'FSBlkCache' 45 case RPCTypes.DbType.fsBlockCacheMeta: 46 return 'FSBlkCacheMeta' 47 case RPCTypes.DbType.fsSyncBlockCache: 48 return 'FSSyncBlkCache' 49 case RPCTypes.DbType.fsSyncBlockCacheMeta: 50 return 'FSSyncBlkCacheMeta' 51 } 52} 53 54let destroyRadar: (() => void) | undefined 55let radarNode: HTMLDivElement | undefined 56const radarSize = 30 57 58const makeRadar = (show: boolean) => { 59 if (destroyRadar) { 60 destroyRadar() 61 destroyRadar = undefined 62 } 63 if (!radarNode || !show) { 64 return 65 } 66 67 destroyRadar = lagRadar({ 68 frames: 5, 69 inset: 1, 70 parent: radarNode, 71 size: radarSize, 72 speed: 0.0017 * 0.7, 73 }) 74} 75 76// simple bucketing of incoming log lines, we have a queue of incoming items, we bucket them 77// and choose a max to show. We use refs a lot since we only want to figure stuff out based on an interval 78// TODO mobile 79const LogStats = (props: {num?: number}) => { 80 const {num} = props 81 const maxBuckets = num ?? 5 82 83 const bucketsRef = React.useRef<Array<{count: number; label: string; labelFull: string; updated: boolean}>>( 84 [] 85 ) 86 const [, setDoRender] = React.useState(0) 87 const events = Container.useSelector(state => state.config.runtimeStats?.perfEvents) 88 const lastEventsRef = React.useRef(new WeakSet<Array<RPCTypes.PerfEvent>>()) 89 90 const eventsRef = React.useRef<Array<RPCTypes.PerfEvent>>([]) 91 if (events) { 92 // only if unprocessed 93 if (!lastEventsRef.current.has(events)) { 94 lastEventsRef.current.add(events) 95 eventsRef.current.push(...events) 96 } 97 } 98 99 Kb.useInterval(() => { 100 const events = eventsRef.current 101 eventsRef.current = [] 102 const incoming = events.map(e => { 103 const parts = e.message.split(' ') 104 if (parts.length >= 2) { 105 const [prefix, body] = parts 106 const bodyParts = body.split('.') 107 const b = bodyParts.length > 1 ? bodyParts.slice(-2).join('.') : bodyParts[0] 108 switch (prefix) { 109 case 'GET': 110 return `<w:${b}` 111 case 'POST': 112 return `>w:${b}` 113 case 'CallCompressed': 114 return `cc:${b}` 115 case 'FullCachingSource:': 116 return `fcs:${b}` 117 case 'Call': 118 return `c:${b}` 119 default: 120 return e.message 121 } 122 } else { 123 return e.message 124 } 125 }) 126 127 // copy existing buckets 128 let newBuckets = bucketsRef.current.map(b => ({...b, updated: false})) 129 130 // find existing or add new ones 131 incoming.forEach((i, idx) => { 132 const existing = newBuckets.find(b => b.label === i) 133 const labelFull = events[idx]?.message ?? i 134 if (existing) { 135 existing.updated = true 136 existing.count++ 137 existing.labelFull += '\n' + labelFull 138 } else { 139 newBuckets.push({count: 1, label: i, labelFull, updated: true}) 140 } 141 }) 142 143 // sort to remove unupdated or small ones 144 newBuckets = newBuckets.sort((a, b) => { 145 if (a.updated !== b.updated) { 146 return a.updated ? -1 : 1 147 } 148 149 return b.count - a.count 150 }) 151 152 // clamp buckets 153 newBuckets = newBuckets.slice(0, maxBuckets) 154 155 // if no new ones, lets eliminate the last item 156 if (!incoming.length) { 157 newBuckets = newBuckets.reverse() 158 newBuckets.some(b => { 159 if (b.label) { 160 b.label = '' 161 b.count = 0 162 return true 163 } 164 return false 165 }) 166 newBuckets = newBuckets.reverse() 167 } 168 169 // sort remainder by alpha so things don't move around a lot 170 newBuckets = newBuckets.sort((a, b) => a.label.localeCompare(b.label)) 171 172 bucketsRef.current = newBuckets 173 setDoRender(r => r + 1) 174 }, 2000) 175 176 return ( 177 <Kb.Box2 178 direction="vertical" 179 style={{ 180 backgroundColor: 'rgba(0,0,0, 0.3)', 181 minHeight: (Styles.isMobile ? 12 : 20) * maxBuckets, 182 }} 183 fullWidth={true} 184 > 185 {!Styles.isMobile && ( 186 <Kb.Text type="BodyTinyBold" style={styles.stat}> 187 Logs 188 </Kb.Text> 189 )} 190 {bucketsRef.current.map((b, i) => ( 191 <Kb.Text 192 key={i} 193 type={b.updated ? 'BodyTinyBold' : 'BodyTiny'} 194 style={styles.logStat} 195 lineClamp={1} 196 title={b.labelFull} 197 > 198 {b.label && b.count > 1 ? b.count : ''} {b.label} 199 </Kb.Text> 200 ))} 201 </Kb.Box2> 202 ) 203} 204 205const RuntimeStatsDesktop = ({stats}: Props) => { 206 const [showRadar, setShowRadar] = React.useState(false) 207 const refContainer = React.useCallback( 208 node => { 209 radarNode = node 210 makeRadar(showRadar) 211 }, 212 [showRadar] 213 ) 214 const toggleRadar = () => { 215 const show = !showRadar 216 setShowRadar(show) 217 makeRadar(show) 218 } 219 220 const [moreLogs, setMoreLogs] = React.useState(false) 221 222 return ( 223 <> 224 <Kb.BoxGrow style={styles.boxGrow}> 225 <Kb.ClickableBox onClick={() => setMoreLogs(m => !m)}> 226 <Kb.Box2 direction="vertical" style={styles.container} gap="xxtiny" fullWidth={true}> 227 {!moreLogs && 228 stats.processStats?.map((stat, i) => { 229 return ( 230 <Kb.Box2 direction="vertical" key={`process${i}`} fullWidth={true} noShrink={true}> 231 <Kb.Text type="BodyTinyBold" style={styles.stat}> 232 {processTypeString(stat.type)} 233 </Kb.Text> 234 <Kb.Text 235 style={Styles.collapseStyles([styles.stat, severityStyle(stat.cpuSeverity)])} 236 type="BodyTiny" 237 >{`CPU: ${stat.cpu}`}</Kb.Text> 238 <Kb.Text 239 style={Styles.collapseStyles([styles.stat, severityStyle(stat.residentSeverity)])} 240 type="BodyTiny" 241 >{`Res: ${stat.resident}`}</Kb.Text> 242 <Kb.Text style={styles.stat} type="BodyTiny">{`Virt: ${stat.virt}`}</Kb.Text> 243 <Kb.Text style={styles.stat} type="BodyTiny">{`Free: ${stat.free}`}</Kb.Text> 244 <Kb.Text style={styles.stat} type="BodyTiny">{`GoHeap: ${stat.goheap}`}</Kb.Text> 245 <Kb.Text style={styles.stat} type="BodyTiny">{`GoHeapSys: ${stat.goheapsys}`}</Kb.Text> 246 <Kb.Text style={styles.stat} type="BodyTiny">{`GoReleased: ${stat.goreleased}`}</Kb.Text> 247 <Kb.Divider /> 248 <Kb.Divider /> 249 </Kb.Box2> 250 ) 251 })} 252 {!moreLogs && <Kb.Divider />} 253 {!moreLogs && ( 254 <Kb.Text type="BodyTinyBold" style={styles.stat}> 255 Chat Bkg Activity 256 </Kb.Text> 257 )} 258 {!moreLogs && ( 259 <Kb.Text 260 style={Styles.collapseStyles([ 261 styles.stat, 262 stats.convLoaderActive ? styles.statWarning : styles.statNormal, 263 ])} 264 type="BodyTiny" 265 >{`BkgLoaderActive: ${yesNo(stats.convLoaderActive)}`}</Kb.Text> 266 )} 267 {!moreLogs && ( 268 <Kb.Text 269 style={Styles.collapseStyles([ 270 styles.stat, 271 stats.selectiveSyncActive ? styles.statWarning : styles.statNormal, 272 ])} 273 type="BodyTiny" 274 >{`IndexerSyncActive: ${yesNo(stats.selectiveSyncActive)}`}</Kb.Text> 275 )} 276 {!moreLogs && <Kb.Divider />} 277 {!moreLogs && ( 278 <Kb.Text type="BodyTinyBold" style={styles.stat}> 279 LevelDB Compaction 280 </Kb.Text> 281 )} 282 {!moreLogs && 283 stats.dbStats?.map((stat, i) => { 284 return ( 285 <Kb.Box2 direction="vertical" key={`db${i}`} fullWidth={true}> 286 <Kb.Text 287 type="BodyTiny" 288 style={Styles.collapseStyles([ 289 styles.stat, 290 stat.memCompActive || stat.tableCompActive ? styles.statWarning : styles.statNormal, 291 ])} 292 > 293 {`${dbTypeString(stat.type)}: ${yesNo(stat.memCompActive || stat.tableCompActive)}`} 294 </Kb.Text> 295 </Kb.Box2> 296 ) 297 })} 298 {!moreLogs && ( 299 <Kb.Box style={styles.radarContainer} forwardedRef={refContainer} onClick={toggleRadar} /> 300 )} 301 <LogStats num={moreLogs ? 25 : 5} /> 302 </Kb.Box2> 303 </Kb.ClickableBox> 304 </Kb.BoxGrow> 305 </> 306 ) 307} 308 309const compactionActive = (dbStats: Props['stats']['dbStats'], typs: Array<RPCTypes.DbType>) => 310 dbStats?.some(stat => typs.indexOf(stat.type) >= 0 && (stat.memCompActive || stat.tableCompActive)) 311 312const chatDbs = [RPCTypes.DbType.chat, RPCTypes.DbType.main] 313const kbfsDbs = [ 314 RPCTypes.DbType.fsBlockCache, 315 RPCTypes.DbType.fsBlockCacheMeta, 316 RPCTypes.DbType.fsSyncBlockCache, 317 RPCTypes.DbType.fsSyncBlockCacheMeta, 318] 319 320const RuntimeStatsMobile = ({stats}: Props) => { 321 const [showLogs, setShowLogs] = React.useState(true) 322 const processStat = stats.processStats?.[0] 323 const coreCompaction = compactionActive(stats.dbStats, chatDbs) 324 const kbfsCompaction = compactionActive(stats.dbStats, kbfsDbs) 325 return ( 326 <> 327 <Kb.Box2 328 direction="vertical" 329 style={showLogs ? styles.modalLogStats : styles.modalLogStatsHidden} 330 gap="xtiny" 331 > 332 <Kb.ClickableBox onClick={() => setShowLogs(s => !s)}> 333 <LogStats /> 334 </Kb.ClickableBox> 335 </Kb.Box2> 336 <Kb.Box2 direction="horizontal" style={styles.container} gap="xtiny" pointerEvents="none"> 337 {processStat && ( 338 <Kb.Box2 direction="vertical"> 339 <Kb.Box2 direction="horizontal" gap="xxtiny" alignSelf="flex-end"> 340 <Kb.Text 341 style={Styles.collapseStyles([styles.stat, severityStyle(processStat.cpuSeverity)])} 342 type="BodyTiny" 343 >{`C:${processStat.cpu}`}</Kb.Text> 344 <Kb.Text 345 style={Styles.collapseStyles([styles.stat, severityStyle(processStat.residentSeverity)])} 346 type="BodyTiny" 347 >{`R:${processStat.resident}`}</Kb.Text> 348 <Kb.Text style={styles.stat} type="BodyTiny">{`V:${processStat.virt}`}</Kb.Text> 349 <Kb.Text style={styles.stat} type="BodyTiny">{`F:${processStat.free}`}</Kb.Text> 350 </Kb.Box2> 351 <Kb.Box2 direction="horizontal" gap="xxtiny" alignSelf="flex-end"> 352 <Kb.Text style={styles.stat} type="BodyTiny">{`GH:${processStat.goheap}`}</Kb.Text> 353 <Kb.Text style={styles.stat} type="BodyTiny">{`GS:${processStat.goheapsys}`}</Kb.Text> 354 <Kb.Text style={styles.stat} type="BodyTiny">{`GR:${processStat.goreleased}`}</Kb.Text> 355 </Kb.Box2> 356 </Kb.Box2> 357 )} 358 <Kb.Box2 direction="vertical"> 359 <Kb.Text 360 style={Styles.collapseStyles([ 361 styles.stat, 362 stats.convLoaderActive ? styles.statWarning : styles.statNormal, 363 ])} 364 type="BodyTiny" 365 >{`CLA: ${yesNo(stats.convLoaderActive)}`}</Kb.Text> 366 <Kb.Text 367 style={Styles.collapseStyles([ 368 styles.stat, 369 stats.selectiveSyncActive ? styles.statWarning : styles.statNormal, 370 ])} 371 type="BodyTiny" 372 >{`SSA: ${yesNo(stats.selectiveSyncActive)}`}</Kb.Text> 373 </Kb.Box2> 374 <Kb.Box2 direction="vertical"> 375 <Kb.Text 376 style={Styles.collapseStyles([ 377 styles.stat, 378 coreCompaction ? styles.statWarning : styles.statNormal, 379 ])} 380 type="BodyTiny" 381 >{`LC: ${yesNo(coreCompaction)}`}</Kb.Text> 382 <Kb.Text 383 style={Styles.collapseStyles([ 384 styles.stat, 385 kbfsCompaction ? styles.statWarning : styles.statNormal, 386 ])} 387 type="BodyTiny" 388 >{`LK: ${yesNo(kbfsCompaction)}`}</Kb.Text> 389 </Kb.Box2> 390 </Kb.Box2> 391 </> 392 ) 393} 394 395const RuntimeStats = () => { 396 const stats = Container.useSelector(state => state.config.runtimeStats) 397 return stats ? ( 398 Styles.isMobile ? ( 399 <RuntimeStatsMobile stats={stats} /> 400 ) : ( 401 <RuntimeStatsDesktop stats={stats} /> 402 ) 403 ) : null 404} 405 406const styles = Styles.styleSheetCreate(() => ({ 407 boxGrow: Styles.platformStyles({ 408 isElectron: { 409 overflow: 'auto', 410 }, 411 }), 412 container: Styles.platformStyles({ 413 common: {backgroundColor: Styles.globalColors.blackOrBlack}, 414 isElectron: { 415 overflow: 'auto', 416 padding: Styles.globalMargins.tiny, 417 position: 'relative', 418 }, 419 isMobile: { 420 bottom: isIPhoneX ? 15 : 0, 421 position: 'absolute', 422 right: isIPhoneX ? 10 : 0, 423 }, 424 }), 425 logStat: Styles.platformStyles({ 426 common: {color: Styles.globalColors.whiteOrWhite}, 427 isElectron: {wordBreak: 'break-all'}, 428 isMobile: { 429 fontFamily: 'Courier', 430 fontSize: 12, 431 lineHeight: 16, 432 }, 433 }), 434 modalLogStats: { 435 position: 'absolute', 436 right: 0, 437 top: 20, 438 width: 130, 439 }, 440 modalLogStatsHidden: { 441 backgroundColor: 'yellow', 442 position: 'absolute', 443 right: 0, 444 top: 20, 445 width: 20, 446 }, 447 radarContainer: Styles.platformStyles({ 448 isElectron: { 449 backgroundColor: Styles.globalColors.white_20, 450 borderRadius: '50%', 451 height: radarSize, 452 position: 'absolute', 453 right: Styles.globalMargins.tiny, 454 top: Styles.globalMargins.tiny, 455 width: radarSize, 456 }, 457 }), 458 stat: Styles.platformStyles({ 459 common: {color: Styles.globalColors.whiteOrGreenDark}, 460 isElectron: {wordBreak: 'break-all'}, 461 isMobile: { 462 fontFamily: 'Courier', 463 fontSize: 10, 464 lineHeight: 14, 465 }, 466 }), 467 statNormal: { 468 color: Styles.globalColors.whiteOrGreenDark, 469 }, 470 statSevere: { 471 color: Styles.globalColors.red, 472 }, 473 statWarning: { 474 color: Styles.globalColors.yellowOrYellowAlt, 475 }, 476})) 477 478export default RuntimeStats 479