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 &quot;selected area&quot;
   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 &quot;selected area&quot;
   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  }