Browse Source

feat(profiling): Link functions to example profiles (#34802)

The functions page surfaces the slowest functions for a transaction + version.
This adds links to each of the slowest functions to example profiles that
contain the slow function.
Tony Xiao 2 years ago
parent
commit
e1a7d527bd

+ 1 - 0
static/app/types/profiling/core.tsx

@@ -59,6 +59,7 @@ export type FunctionCall = {
   line: number;
   main_thread_percent: Record<string, number>;
   path: string;
+  profile_id_to_thread_id: Record<string, number>;
   profile_ids: string[];
   symbol: string;
   transaction_names;

+ 7 - 8
static/app/views/profiling/flamegraphSummary.tsx

@@ -23,7 +23,7 @@ import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
 
 import {useProfileGroup} from './profileGroupProvider';
-import {generateFlamegraphRoute} from './routes';
+import {generateFlamegraphRouteWithQuery} from './routes';
 
 const RESULTS_PER_PAGE = 50;
 
@@ -212,13 +212,12 @@ function ProfilingFunctionsTableCell({
       return (
         <Container>
           <Link
-            to={
-              generateFlamegraphRoute({
-                orgSlug: orgId,
-                projectSlug: projectId,
-                profileId: eventId,
-              }) + `?tid=${dataRow.thread}`
-            }
+            to={generateFlamegraphRouteWithQuery({
+              orgSlug: orgId,
+              projectSlug: projectId,
+              profileId: eventId,
+              query: {tid: dataRow.thread},
+            })}
           >
             {value}
           </Link>

+ 76 - 0
static/app/views/profiling/functions/arrayLinks.tsx

@@ -0,0 +1,76 @@
+import {useState} from 'react';
+import {Link} from 'react-router';
+import styled from '@emotion/styled';
+import {LocationDescriptor} from 'history';
+
+import {t} from 'sentry/locale';
+import overflowEllipsis from 'sentry/styles/overflowEllipsis';
+import space from 'sentry/styles/space';
+
+type Item = {
+  target: LocationDescriptor;
+  value: string;
+};
+
+interface ArrayLinksProps {
+  items: Item[];
+}
+
+function ArrayLinks({items}: ArrayLinksProps) {
+  const [expanded, setExpanded] = useState(false);
+
+  return (
+    <ArrayContainer expanded={expanded}>
+      {items.length > 0 && <LinkedItem item={items[0]} />}
+      {items.length > 1 &&
+        expanded &&
+        items
+          .slice(0, items.length - 1)
+          .map(item => <LinkedItem key={item.value} item={item} />)}
+      {items.length > 1 ? (
+        <ButtonContainer>
+          <button onClick={() => setExpanded(!expanded)}>
+            {expanded ? t('[collapse]') : t('[+%s more]', items.length - 1)}
+          </button>
+        </ButtonContainer>
+      ) : null}
+    </ArrayContainer>
+  );
+}
+
+function LinkedItem({item}: {item: Item}) {
+  return (
+    <ArrayItem>
+      <Link to={item.target}>{item.value}</Link>
+    </ArrayItem>
+  );
+}
+
+const ArrayContainer = styled('div')<{expanded: boolean}>`
+  display: flex;
+  flex-direction: ${p => (p.expanded ? 'column' : 'row')};
+`;
+
+const ArrayItem = styled('span')`
+  flex-shrink: 1;
+  display: block;
+
+  ${overflowEllipsis};
+  width: unset;
+`;
+
+const ButtonContainer = styled('div')`
+  white-space: nowrap;
+
+  & button {
+    background: none;
+    border: 0;
+    outline: none;
+    padding: 0;
+    cursor: pointer;
+    color: ${p => p.theme.blue300};
+    margin-left: ${space(0.5)};
+  }
+`;
+
+export {ArrayLinks};

+ 31 - 2
static/app/views/profiling/functions/content.tsx

@@ -16,15 +16,22 @@ import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearch
 import {MAX_QUERY_LENGTH} from 'sentry/constants';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
+import {Project} from 'sentry/types';
 import {FunctionCall, VersionedFunctionCalls} from 'sentry/types/profiling/core';
 import {Container, NumberContainer} from 'sentry/utils/discover/styles';
+import {getShortEventId} from 'sentry/utils/events';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 
+import {generateFlamegraphRouteWithQuery} from '../routes';
+
+import {ArrayLinks} from './arrayLinks';
+
 interface FunctionsContentProps {
   error: string | null;
   isLoading: boolean;
+  project: Project;
   version: string;
   versionedFunctions: VersionedFunctionCalls | null;
 }
@@ -56,9 +63,22 @@ function FunctionsContent(props: FunctionsContentProps) {
         p90Frequency: functionCall.frequency.p90,
         p95Frequency: functionCall.frequency.p95,
         p99Frequency: functionCall.frequency.p99,
+        profileIdToThreadId: Object.entries(functionCall.profile_id_to_thread_id).map(
+          ([profileId, threadId]) => {
+            return {
+              value: getShortEventId(profileId),
+              target: generateFlamegraphRouteWithQuery({
+                orgSlug: organization.slug,
+                projectSlug: props.project.slug,
+                profileId,
+                query: {tid: threadId.toString()},
+              }),
+            };
+          }
+        ),
       };
     });
-  }, [props.version, props.versionedFunctions]);
+  }, [organization.slug, props.project.slug, props.version, props.versionedFunctions]);
 
   const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
     (searchQuery: string) => {
@@ -154,6 +174,8 @@ function ProfilingFunctionsTableCell({
           <PerformanceDuration nanoseconds={value} abbreviation />
         </NumberContainer>
       );
+    case 'profileIdToThreadId':
+      return <ArrayLinks items={value} />;
     default:
       return <Container>{value}</Container>;
   }
@@ -172,7 +194,8 @@ type TableColumnKey =
   | 'p75Frequency'
   | 'p90Frequency'
   | 'p95Frequency'
-  | 'p99Frequency';
+  | 'p99Frequency'
+  | 'profileIdToThreadId';
 
 type TableDataRow = Record<TableColumnKey, any>;
 
@@ -186,6 +209,7 @@ const COLUMN_ORDER: TableColumnKey[] = [
   'mainThreadPercent',
   'p75Frequency',
   'p99Frequency',
+  'profileIdToThreadId',
 ];
 
 // TODO: looks like these column names change depending on the platform?
@@ -255,6 +279,11 @@ const COLUMNS: Record<TableColumnKey, TableColumn> = {
     name: t('P99 Frequency'),
     width: COL_WIDTH_UNDEFINED,
   },
+  profileIdToThreadId: {
+    key: 'profileIdToThreadId',
+    name: t('Example Profiles'),
+    width: COL_WIDTH_UNDEFINED,
+  },
 };
 
 const ActionBar = styled('div')`

+ 1 - 0
static/app/views/profiling/functions/index.tsx

@@ -111,6 +111,7 @@ function FunctionsPage(props: Props) {
               />
               <Layout.Body>
                 <FunctionsContent
+                  project={project}
                   isLoading={requestState.type === 'loading'}
                   error={requestState.type === 'errored' ? requestState.error : null}
                   version={version}

+ 20 - 8
static/app/views/profiling/routes.tsx

@@ -44,15 +44,18 @@ export function generateFlamegraphSummaryRoute({
 export function generateProfilingRouteWithQuery({
   location,
   orgSlug,
+  query,
 }: {
-  location: Location;
   orgSlug: Organization['slug'];
+  location?: Location;
+  query?: Location['query'];
 }): LocationDescriptor {
   const pathname = generateProfilingRoute({orgSlug});
   return {
     pathname,
     query: {
-      ...location.query,
+      ...location?.query,
+      ...query,
     },
   };
 }
@@ -63,18 +66,21 @@ export function generateFunctionsRouteWithQuery({
   projectSlug,
   transaction,
   version,
+  query,
 }: {
-  location: Location;
   orgSlug: Organization['slug'];
   projectSlug: Project['slug'];
   transaction: string;
   version: string;
+  location?: Location;
+  query?: Location['query'];
 }): LocationDescriptor {
   const pathname = generateFunctionsRoute({orgSlug, projectSlug});
   return {
     pathname,
     query: {
-      ...location.query,
+      ...location?.query,
+      ...query,
       transaction,
       version,
     },
@@ -86,17 +92,20 @@ export function generateFlamegraphRouteWithQuery({
   orgSlug,
   projectSlug,
   profileId,
+  query,
 }: {
-  location: Location;
   orgSlug: Organization['slug'];
   profileId: Trace['id'];
   projectSlug: Project['slug'];
+  location?: Location;
+  query?: Location['query'];
 }): LocationDescriptor {
   const pathname = generateFlamegraphRoute({orgSlug, projectSlug, profileId});
   return {
     pathname,
     query: {
-      ...location.query,
+      ...location?.query,
+      ...query,
     },
   };
 }
@@ -106,17 +115,20 @@ export function generateFlamegraphSummaryRouteWithQuery({
   orgSlug,
   projectSlug,
   profileId,
+  query,
 }: {
-  location: Location;
   orgSlug: Organization['slug'];
   profileId: Trace['id'];
   projectSlug: Project['slug'];
+  location?: Location;
+  query?: Location['query'];
 }): LocationDescriptor {
   const pathname = generateFlamegraphSummaryRoute({orgSlug, projectSlug, profileId});
   return {
     pathname,
     query: {
-      ...location.query,
+      ...location?.query,
+      ...query,
     },
   };
 }