QueryPerformanceChart.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import { Loader2 } from 'lucide-react'
  2. import { useMemo, useState } from 'react'
  3. import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
  4. import type { ChartDataPoint } from '../QueryInsights/QueryInsights.types'
  5. import { QUERY_PERFORMANCE_CHART_TABS } from './QueryPerformance.constants'
  6. import { ComposedChart } from '@/components/ui/Charts/ComposedChart'
  7. import type { MultiAttribute } from '@/components/ui/Charts/ComposedChart.utils'
  8. interface QueryPerformanceChartProps {
  9. dateRange?: {
  10. period_start: { date: string; time_period: string }
  11. period_end: { date: string; time_period: string }
  12. interval: string
  13. }
  14. onDateRangeChange?: (from: string, to: string) => void
  15. chartData: ChartDataPoint[]
  16. isLoading: boolean
  17. error: any
  18. currentSelectedQuery: string | null
  19. parsedLogs: any[]
  20. }
  21. const QueryMetricBlock = ({
  22. label,
  23. value,
  24. }: {
  25. label: string
  26. value: string | number | undefined
  27. }) => {
  28. return (
  29. <div className="flex flex-col gap-0.5 text-xs">
  30. <span className="font-mono text-xs text-foreground-lighter uppercase">{label}</span>
  31. <span className="text-lg tabular-nums">{value}</span>
  32. </div>
  33. )
  34. }
  35. const formatTimeValue = (value: number): string => {
  36. if (value >= 1000) {
  37. return `${(value / 1000).toFixed(1)}s`
  38. }
  39. return `${value.toFixed(1)}ms`
  40. }
  41. const formatNumberValue = (value: number): string => {
  42. return value.toLocaleString()
  43. }
  44. export const QueryPerformanceChart = ({
  45. onDateRangeChange,
  46. chartData,
  47. isLoading,
  48. error,
  49. currentSelectedQuery,
  50. parsedLogs,
  51. }: QueryPerformanceChartProps) => {
  52. const [selectedMetric, setSelectedMetric] = useState('query_latency')
  53. const currentMetrics = useMemo(() => {
  54. if (!chartData || chartData.length === 0) return []
  55. switch (selectedMetric) {
  56. case 'query_latency': {
  57. const totalCalls = chartData.reduce((sum, d) => sum + d.calls, 0)
  58. const trueP95 =
  59. totalCalls > 0
  60. ? chartData.reduce((sum, d) => sum + d.p95_time * d.calls, 0) / totalCalls
  61. : 0
  62. return [
  63. {
  64. label: 'Average p95',
  65. value: `${Math.round(trueP95)}ms`,
  66. },
  67. ]
  68. }
  69. case 'rows_read': {
  70. const totalRowsRead = chartData.reduce((sum, d) => sum + d.rows_read, 0)
  71. return [
  72. {
  73. label: 'Total Rows Read',
  74. value: totalRowsRead.toLocaleString(),
  75. },
  76. ]
  77. }
  78. case 'calls': {
  79. const totalCalls = chartData.reduce((sum, d) => sum + d.calls, 0)
  80. return [
  81. {
  82. label: 'Total Calls',
  83. value: totalCalls.toLocaleString(),
  84. },
  85. ]
  86. }
  87. case 'cache_hits': {
  88. const totalHits = chartData.reduce((sum, d) => sum + d.cache_hits, 0)
  89. const totalMisses = chartData.reduce((sum, d) => sum + d.cache_misses, 0)
  90. const total = totalHits + totalMisses
  91. const hitRate = total > 0 ? (totalHits / total) * 100 : 0
  92. return [
  93. {
  94. label: 'Cache Hit Rate',
  95. value: `${hitRate.toFixed(2)}%`,
  96. },
  97. ]
  98. }
  99. default:
  100. return []
  101. }
  102. }, [chartData, selectedMetric])
  103. const transformedChartData = useMemo(() => {
  104. if (selectedMetric !== 'query_latency') return chartData
  105. const transformed = chartData.map((dataPoint) => ({
  106. ...dataPoint,
  107. p50_time: parseFloat((dataPoint.p50_time / 1000).toFixed(3)),
  108. p95_time: parseFloat((dataPoint.p95_time / 1000).toFixed(3)),
  109. }))
  110. return transformed
  111. }, [chartData, selectedMetric])
  112. const querySpecificData = useMemo(() => {
  113. if (!currentSelectedQuery || !parsedLogs.length) return null
  114. const normalizedSelected = currentSelectedQuery.replace(/\s+/g, ' ').trim()
  115. const queryLogs = parsedLogs.filter((log) => {
  116. const normalized = (log.query || '').replace(/\s+/g, ' ').trim()
  117. return normalized === normalizedSelected
  118. })
  119. const queryDataMap = new Map<
  120. number,
  121. {
  122. time: number
  123. rows_read: number
  124. calls: number
  125. cache_hits: number
  126. }
  127. >()
  128. queryLogs.forEach((log) => {
  129. const time = new Date(log.timestamp).getTime()
  130. const meanTime = log.mean_time ?? log.mean_exec_time ?? log.mean_query_time ?? 0
  131. const rowsRead = log.rows_read ?? log.rows ?? 0
  132. const calls = log.calls ?? 0
  133. const cacheHits = log.shared_blks_hit ?? log.cache_hits ?? 0
  134. queryDataMap.set(time, {
  135. time: parseFloat(String(meanTime)),
  136. rows_read: parseFloat(String(rowsRead)),
  137. calls: parseFloat(String(calls)),
  138. cache_hits: parseFloat(String(cacheHits)),
  139. })
  140. })
  141. return queryDataMap
  142. }, [currentSelectedQuery, parsedLogs])
  143. const mergedChartData = useMemo(() => {
  144. if (!querySpecificData || !currentSelectedQuery) {
  145. return transformedChartData
  146. }
  147. return transformedChartData.map((dataPoint) => {
  148. const queryData = querySpecificData.get(dataPoint.period_start)
  149. return {
  150. ...dataPoint,
  151. selected_query_time:
  152. queryData?.time !== undefined
  153. ? selectedMetric === 'query_latency'
  154. ? queryData.time / 1000
  155. : queryData.time
  156. : null,
  157. selected_query_rows_read: queryData?.rows_read !== undefined ? queryData.rows_read : null,
  158. selected_query_calls: queryData?.calls !== undefined ? queryData.calls : null,
  159. selected_query_cache_hits:
  160. queryData?.cache_hits !== undefined ? queryData.cache_hits : null,
  161. }
  162. })
  163. }, [transformedChartData, querySpecificData, currentSelectedQuery, selectedMetric])
  164. const getChartAttributes = useMemo((): MultiAttribute[] => {
  165. const attributeMap: Record<string, MultiAttribute[]> = {
  166. query_latency: [
  167. {
  168. attribute: 'p50_time',
  169. label: 'p50',
  170. provider: 'logs',
  171. type: 'line',
  172. color: { light: '#8B5CF6', dark: '#8B5CF6' },
  173. },
  174. {
  175. attribute: 'p95_time',
  176. label: 'p95',
  177. provider: 'logs',
  178. type: 'line',
  179. color: { light: '#65BCD9', dark: '#65BCD9' },
  180. },
  181. ],
  182. rows_read: [
  183. {
  184. attribute: 'rows_read',
  185. label: 'Rows Read',
  186. provider: 'logs',
  187. },
  188. ],
  189. calls: [
  190. {
  191. attribute: 'calls',
  192. label: 'Calls',
  193. provider: 'logs',
  194. },
  195. ],
  196. cache_hits: [
  197. {
  198. attribute: 'cache_hits',
  199. label: 'Cache Hits',
  200. provider: 'logs',
  201. type: 'line',
  202. color: { light: '#10B981', dark: '#10B981' },
  203. },
  204. ],
  205. }
  206. const baseAttributes = attributeMap[selectedMetric] || []
  207. if (currentSelectedQuery && querySpecificData) {
  208. const dimmedBaseAttributes = baseAttributes.map((attr) => ({
  209. ...attr,
  210. color: attr.color
  211. ? { light: attr.color.light + '4D', dark: attr.color.dark + '4D' }
  212. : attr.color,
  213. }))
  214. const selectedQueryAttributes: Record<string, MultiAttribute> = {
  215. query_latency: {
  216. attribute: 'selected_query_time',
  217. label: 'Selected Query',
  218. provider: 'logs',
  219. type: 'line',
  220. color: { light: '#10B981', dark: '#10B981' },
  221. strokeWidth: 3,
  222. },
  223. rows_read: {
  224. attribute: 'selected_query_rows_read',
  225. label: 'Selected Query',
  226. provider: 'logs',
  227. type: 'line',
  228. color: { light: '#F59E0B', dark: '#F59E0B' },
  229. strokeWidth: 3,
  230. },
  231. calls: {
  232. attribute: 'selected_query_calls',
  233. label: 'Selected Query',
  234. provider: 'logs',
  235. type: 'line',
  236. color: { light: '#EC4899', dark: '#EC4899' },
  237. strokeWidth: 3,
  238. },
  239. cache_hits: {
  240. attribute: 'selected_query_cache_hits',
  241. label: 'Selected Query',
  242. provider: 'logs',
  243. type: 'line',
  244. color: { light: '#8B5CF6', dark: '#8B5CF6' },
  245. strokeWidth: 3,
  246. },
  247. }
  248. const selectedQueryAttr = selectedQueryAttributes[selectedMetric]
  249. if (selectedQueryAttr) {
  250. return [...dimmedBaseAttributes, selectedQueryAttr]
  251. }
  252. }
  253. return baseAttributes
  254. }, [selectedMetric, currentSelectedQuery, querySpecificData])
  255. const updateDateRange = (from: string, to: string) => {
  256. onDateRangeChange?.(from, to)
  257. }
  258. const getYAxisFormatter = useMemo(() => {
  259. if (selectedMetric === 'query_latency') {
  260. return formatTimeValue
  261. }
  262. return formatNumberValue
  263. }, [selectedMetric])
  264. return (
  265. <div className="bg-surface-200 border-t">
  266. <Tabs_Shadcn_
  267. value={selectedMetric}
  268. onValueChange={(value) => setSelectedMetric(value as string)}
  269. className="w-full"
  270. >
  271. <TabsList_Shadcn_ className="flex justify-start rounded-none gap-x-4 border-b mt-0! pt-0 px-6">
  272. {QUERY_PERFORMANCE_CHART_TABS.map((tab) => (
  273. <TabsTrigger_Shadcn_
  274. key={tab.id}
  275. value={tab.id}
  276. className="flex items-center gap-2 text-xs py-3 border-b font-mono uppercase"
  277. >
  278. {tab.label}
  279. </TabsTrigger_Shadcn_>
  280. ))}
  281. </TabsList_Shadcn_>
  282. <TabsContent_Shadcn_ value={selectedMetric} className="bg-surface-200 mt-0 h-inherit">
  283. <div className="w-full flex items-center justify-center min-h-[282px]">
  284. {isLoading ? (
  285. <Loader2 size={20} className="animate-spin text-foreground-lighter" />
  286. ) : error ? (
  287. <p className="text-sm text-foreground-light text-center h-full flex items-center justify-center">
  288. Error loading chart data
  289. </p>
  290. ) : (
  291. <div className="w-full flex flex-col h-full px-6 py-4">
  292. <div className="flex gap-6 mb-4">
  293. {currentMetrics.map((metric, index) => (
  294. <QueryMetricBlock key={index} label={metric.label} value={metric.value} />
  295. ))}
  296. </div>
  297. <ComposedChart
  298. data={mergedChartData as any}
  299. attributes={getChartAttributes}
  300. yAxisKey={getChartAttributes[0]?.attribute || ''}
  301. xAxisKey="period_start"
  302. title=""
  303. customDateFormat="MMM D, YYYY hh:mm A"
  304. hideChartType={true}
  305. hideHighlightArea={true}
  306. showTooltip={true}
  307. showGrid={true}
  308. showLegend={true}
  309. showTotal={false}
  310. showMaxValue={false}
  311. updateDateRange={updateDateRange}
  312. YAxisProps={{
  313. tick: true,
  314. width: 60,
  315. tickFormatter: getYAxisFormatter,
  316. }}
  317. xAxisIsDate={true}
  318. className="mt-2"
  319. />
  320. </div>
  321. )}
  322. </div>
  323. </TabsContent_Shadcn_>
  324. </Tabs_Shadcn_>
  325. </div>
  326. )
  327. }