github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/pages/exemplars/ExemplarsSingleView.tsx (about) 1 import React, { useEffect, useState } from 'react'; 2 import clsx from 'clsx'; 3 import { Tabs, Tab, TabPanel } from '@webapp/ui/Tabs'; 4 import useColorMode from '@webapp/hooks/colorMode.hook'; 5 import useTimeZone from '@webapp/hooks/timeZone.hook'; 6 import useTags from '@webapp/hooks/tags.hook'; 7 import { useAppSelector, useAppDispatch } from '@webapp/redux/hooks'; 8 import { 9 actions, 10 fetchTagValues, 11 selectQueries, 12 setQuery, 13 } from '@webapp/redux/reducers/continuous'; 14 import { 15 fetchExemplarsSingleView, 16 fetchSelectionProfile, 17 } from '@webapp/redux/reducers/tracing'; 18 import Box from '@webapp/ui/Box'; 19 import NoData from '@webapp/ui/NoData'; 20 import { LoadingOverlay } from '@webapp/ui/LoadingOverlay'; 21 import LoadingSpinner from '@webapp/ui/LoadingSpinner'; 22 import StatusMessage from '@webapp/ui/StatusMessage'; 23 import { Tooltip } from '@webapp/ui/Tooltip'; 24 import { TooltipInfoIcon } from '@webapp/ui/TooltipInfoIcon'; 25 import Toolbar from '@webapp/components/Toolbar'; 26 import TagsBar from '@webapp/components/TagsBar'; 27 import PageTitle from '@webapp/components/PageTitle'; 28 import { Heatmap } from '@webapp/components/Heatmap'; 29 import ExportData from '@webapp/components/ExportData'; 30 import ChartTitle from '@webapp/components/ChartTitle'; 31 /* eslint-disable css-modules/no-undef-class */ 32 import ChartTitleStyles from '@webapp/components/ChartTitle.module.scss'; 33 import { DEFAULT_HEATMAP_PARAMS } from '@webapp/components/Heatmap/constants'; 34 import { FlamegraphRenderer } from '@pyroscope/flamegraph/src/FlamegraphRenderer'; 35 import type { Profile } from '@pyroscope/models/src'; 36 import { diffTwoProfiles } from '@pyroscope/flamegraph/src/convert/diffTwoProfiles'; 37 import { subtract } from '@pyroscope/flamegraph/src/convert/subtract'; 38 import { formatTitle } from '../formatTitle'; 39 import { isLoadingOrReloading, LoadingType } from '../loading'; 40 import heatmapSelectionPreviewGif from './heatmapSelectionPreview.gif'; 41 import { HeatmapSelectionIcon, HeatmapNoSelectionIcon } from './HeatmapIcons'; 42 43 import styles from './ExemplarsSingleView.module.scss'; 44 import { filterNonCPU } from './filterNonCPU'; 45 46 function ExemplarsSingleView() { 47 const [tabIndex, setTabIndex] = useState(0); 48 const { colorMode } = useColorMode(); 49 const { offset } = useTimeZone(); 50 const tags = useTags().regularTags; 51 52 const { query } = useAppSelector(selectQueries); 53 const { from, until } = useAppSelector((state) => state.continuous); 54 const { exemplarsSingleView } = useAppSelector((state) => state.tracing); 55 const dispatch = useAppDispatch(); 56 57 useEffect(() => { 58 if (from && until && query) { 59 const fetchData = dispatch( 60 fetchExemplarsSingleView({ 61 query, 62 from, 63 until, 64 ...DEFAULT_HEATMAP_PARAMS, 65 }) 66 ); 67 return () => fetchData.abort('cancel'); 68 } 69 return undefined; 70 }, [from, until, query]); 71 72 const handleHeatmapSelection = ( 73 minValue: number, 74 maxValue: number, 75 startTime: number, 76 endTime: number 77 ) => { 78 dispatch( 79 fetchSelectionProfile({ 80 from, 81 until, 82 query, 83 heatmapTimeBuckets: DEFAULT_HEATMAP_PARAMS.heatmapTimeBuckets, 84 heatmapValueBuckets: DEFAULT_HEATMAP_PARAMS.heatmapValueBuckets, 85 selectionMinValue: minValue, 86 selectionMaxValue: maxValue, 87 selectionStartTime: startTime, 88 selectionEndTime: endTime, 89 }) 90 ); 91 }; 92 93 const heatmap = (() => { 94 switch (exemplarsSingleView.type) { 95 case 'loaded': 96 case 'reloading': { 97 return exemplarsSingleView.heatmap !== null ? ( 98 <Heatmap 99 heatmap={exemplarsSingleView.heatmap} 100 onSelection={handleHeatmapSelection} 101 timezone={offset === 0 ? 'utc' : 'browser'} 102 sampleRate={exemplarsSingleView.profile?.metadata.sampleRate || 100} 103 /> 104 ) : ( 105 <NoData /> 106 ); 107 } 108 109 default: { 110 return ( 111 <div className={styles.spinnerWrapper}> 112 <LoadingSpinner /> 113 </div> 114 ); 115 } 116 } 117 })(); 118 119 const differenceProfile = 120 exemplarsSingleView.profile && 121 exemplarsSingleView.selectionProfile && 122 subtract(exemplarsSingleView.profile, exemplarsSingleView.selectionProfile); 123 124 return ( 125 <div> 126 <PageTitle title={formatTitle('Tracing single', query)} /> 127 <div className="main-wrapper"> 128 <Toolbar 129 onSelectedApp={(query) => { 130 dispatch(setQuery(query)); 131 }} 132 filterApp={filterNonCPU} 133 /> 134 <TagsBar 135 query={query} 136 tags={tags} 137 onRefresh={() => dispatch(actions.refresh())} 138 onSetQuery={(q) => dispatch(actions.setQuery(q))} 139 onSelectedLabel={(label, query) => { 140 dispatch(fetchTagValues({ query, label })); 141 }} 142 /> 143 144 <Box> 145 <p className={styles.heatmapTitle}>Heatmap</p> 146 {heatmap} 147 </Box> 148 {!exemplarsSingleView.selectionProfile && exemplarsSingleView.heatmap && ( 149 <Box> 150 <div className={styles.heatmapSelectionGuide}> 151 <StatusMessage 152 type="info" 153 message="Select an area in the heatmap to get started" 154 /> 155 <img 156 className={styles.gif} 157 src={heatmapSelectionPreviewGif} 158 alt="heatmap-selection-gif" 159 /> 160 </div> 161 </Box> 162 )} 163 {exemplarsSingleView.heatmap && 164 exemplarsSingleView.selectionProfile && 165 differenceProfile ? ( 166 <> 167 <Tabs value={tabIndex} onChange={(e, value) => setTabIndex(value)}> 168 <Tab label="Single" /> 169 <Tab label="Comparison" /> 170 <Tab label="Diff" /> 171 </Tabs> 172 <TabPanel value={tabIndex} index={0}> 173 <SingleTab 174 colorMode={colorMode} 175 type={exemplarsSingleView.type} 176 selectionProfile={exemplarsSingleView.selectionProfile} 177 /> 178 </TabPanel> 179 <TabPanel value={tabIndex} index={1}> 180 <ComparisonTab 181 colorMode={colorMode} 182 type={exemplarsSingleView.type} 183 differenceProfile={differenceProfile} 184 selectionProfile={exemplarsSingleView.selectionProfile} 185 /> 186 </TabPanel> 187 <TabPanel value={tabIndex} index={2}> 188 <DiffTab 189 colorMode={colorMode} 190 type={exemplarsSingleView.type} 191 differenceProfile={differenceProfile} 192 selectionProfile={exemplarsSingleView.selectionProfile} 193 /> 194 </TabPanel> 195 </> 196 ) : null} 197 </div> 198 </div> 199 ); 200 } 201 202 export default ExemplarsSingleView; 203 204 interface TabProps { 205 colorMode: ReturnType<typeof useColorMode>['colorMode']; 206 type: LoadingType; 207 selectionProfile: Profile; 208 } 209 210 function SingleTab({ colorMode, type, selectionProfile }: TabProps) { 211 return ( 212 <Box> 213 <LoadingOverlay 214 active={isLoadingOrReloading([type])} 215 spinnerPosition="baseline" 216 > 217 <FlamegraphRenderer 218 showCredit={false} 219 profile={selectionProfile} 220 colorMode={colorMode} 221 ExportData={ 222 <ExportData 223 flamebearer={selectionProfile} 224 exportPNG 225 exportJSON 226 exportPprof 227 exportHTML 228 /> 229 } 230 /> 231 </LoadingOverlay> 232 </Box> 233 ); 234 } 235 236 function ComparisonTab({ 237 colorMode, 238 type, 239 differenceProfile, 240 selectionProfile, 241 }: TabProps & { differenceProfile: Profile }) { 242 return ( 243 <div className={styles.comparisonTab}> 244 <Box className={styles.comparisonTabHalf}> 245 <LoadingOverlay 246 active={isLoadingOrReloading([type])} 247 spinnerPosition="baseline" 248 > 249 <ChartTitle 250 titleKey="selection_included" 251 icon={<HeatmapSelectionIcon />} 252 postfix={ 253 <Tooltip 254 placement="top" 255 title={ 256 <div className={styles.titleInfoTooltip}> 257 Represents the aggregated result of all profiles{' '} 258 <b>included within</b> the orange "selected area" 259 </div> 260 } 261 > 262 <TooltipInfoIcon /> 263 </Tooltip> 264 } 265 /> 266 <FlamegraphRenderer 267 showCredit={false} 268 profile={selectionProfile} 269 colorMode={colorMode} 270 panesOrientation="vertical" 271 ExportData={ 272 <ExportData 273 flamebearer={selectionProfile} 274 exportPNG 275 exportJSON 276 exportPprof 277 exportHTML 278 /> 279 } 280 /> 281 </LoadingOverlay> 282 </Box> 283 <Box className={styles.comparisonTabHalf}> 284 <LoadingOverlay 285 active={isLoadingOrReloading([type])} 286 spinnerPosition="baseline" 287 > 288 <ChartTitle 289 titleKey="selection_excluded" 290 icon={<HeatmapNoSelectionIcon />} 291 postfix={ 292 <Tooltip 293 placement="top" 294 title={ 295 <div className={styles.titleInfoTooltip}> 296 Represents the aggregated result of all profiles{' '} 297 <b>excluding</b> the orange "selected area" 298 </div> 299 } 300 > 301 <TooltipInfoIcon /> 302 </Tooltip> 303 } 304 /> 305 <FlamegraphRenderer 306 showCredit={false} 307 profile={differenceProfile} 308 colorMode={colorMode} 309 panesOrientation="vertical" 310 ExportData={ 311 <ExportData 312 flamebearer={differenceProfile} 313 exportPNG 314 exportJSON 315 exportPprof 316 exportHTML 317 /> 318 } 319 /> 320 </LoadingOverlay> 321 </Box> 322 </div> 323 ); 324 } 325 326 function DiffTab({ 327 colorMode, 328 type, 329 differenceProfile, 330 selectionProfile, 331 }: TabProps & { differenceProfile: Profile }) { 332 const subtractedCopy = JSON.parse(JSON.stringify(differenceProfile)); 333 const selectionCopy = JSON.parse(JSON.stringify(selectionProfile)); 334 const diffProfile = diffTwoProfiles(subtractedCopy, selectionCopy); 335 336 return ( 337 <Box> 338 <LoadingOverlay 339 active={isLoadingOrReloading([type])} 340 spinnerPosition="baseline" 341 > 342 <ChartTitle 343 postfix={ 344 <Tooltip 345 placement="top" 346 title={ 347 <div className={styles.titleInfoTooltip}> 348 Represents the diff between an aggregated flamegraph 349 representing the selected area and an aggregated flamegraph 350 excluding the selected area 351 </div> 352 } 353 > 354 <TooltipInfoIcon /> 355 </Tooltip> 356 } 357 > 358 <span 359 className={clsx( 360 ChartTitleStyles.colorOrIcon, 361 ChartTitleStyles.icon 362 )} 363 > 364 <HeatmapSelectionIcon /> 365 </span> 366 Selection-included vs 367 <span 368 className={clsx( 369 ChartTitleStyles.colorOrIcon, 370 ChartTitleStyles.icon 371 )} 372 > 373 <HeatmapNoSelectionIcon /> 374 </span> 375 Selection-excluded Diff Flamegraph 376 </ChartTitle> 377 <FlamegraphRenderer 378 showCredit={false} 379 profile={diffProfile} 380 colorMode={colorMode} 381 ExportData={ 382 <ExportData 383 flamebearer={diffProfile} 384 exportPNG 385 exportJSON 386 exportPprof 387 exportHTML 388 /> 389 } 390 /> 391 </LoadingOverlay> 392 </Box> 393 ); 394 }