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