Skip to content

Commit

Permalink
feat(statistical-detectors): Link transaction durations to example pr… (
Browse files Browse the repository at this point in the history
#58101)

…ofiles

Use load the worst() timeseries and iterate before and after the
breakpoint to find candidate profiles to link to.
  • Loading branch information
Zylphrex authored Oct 16, 2023
1 parent 143bb38 commit 1339e91
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ import {space} from 'sentry/styles/space';
import {Event, Group, Project} from 'sentry/types';
import {Series} from 'sentry/types/echarts';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {tooltipFormatter} from 'sentry/utils/discover/charts';
import {Container, NumberContainer} from 'sentry/utils/discover/styles';
import {getDuration} from 'sentry/utils/formatters';
import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
import {useProfileTopEventsStats} from 'sentry/utils/profiling/hooks/useProfileTopEventsStats';
import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime';
import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes';
import {
generateProfileFlamechartRouteWithQuery,
generateProfileSummaryRouteWithQuery,
} from 'sentry/utils/profiling/routes';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import useOrganization from 'sentry/utils/useOrganization';

Expand All @@ -36,6 +40,8 @@ export function EventAffectedTransactions({
const evidenceData = event.occurrence?.evidenceData;
const fingerprint = evidenceData?.fingerprint;
const breakpoint = evidenceData?.breakpoint;
const frameName = evidenceData?.function;
const framePackage = evidenceData?.package || evidenceData?.module;

const isValid = defined(fingerprint) && defined(breakpoint);

Expand Down Expand Up @@ -64,6 +70,8 @@ export function EventAffectedTransactions({
<EventAffectedTransactionsInner
breakpoint={breakpoint}
fingerprint={fingerprint}
frameName={frameName}
framePackage={framePackage}
project={project}
/>
);
Expand All @@ -74,12 +82,16 @@ const TRANSACTIONS_LIMIT = 5;
interface EventAffectedTransactionsInnerProps {
breakpoint: number;
fingerprint: number;
frameName: string;
framePackage: string;
project: Project;
}

function EventAffectedTransactionsInner({
breakpoint,
fingerprint,
frameName,
framePackage,
project,
}: EventAffectedTransactionsInnerProps) {
const organization = useOrganization();
Expand Down Expand Up @@ -132,11 +144,38 @@ function EventAffectedTransactionsInner({
query: query ?? '',
enabled: defined(query),
others: false,
referrer: 'api.profiling.functions.regression.stats', // TODO: update this
referrer: 'api.profiling.functions.regression.transaction-stats',
topEvents: TRANSACTIONS_LIMIT,
yAxes: ['p95()', 'worst()'],
});

const examplesByTransaction = useMemo(() => {
const allExamples: Record<string, [string | null, string | null]> = {};
if (!defined(functionStats.data)) {
return allExamples;
}

const timestamps = functionStats.data.timestamps;
const breakpointIndex = timestamps.indexOf(breakpoint);
if (breakpointIndex < 0) {
return allExamples;
}

transactionsDeltaQuery.data?.data?.forEach(row => {
const transaction = row.transaction as string;
const data = functionStats.data.data.find(
({axis, label}) => axis === 'worst()' && label === transaction
);
if (!defined(data)) {
return;
}

allExamples[transaction] = findExamplePair(data.values, breakpointIndex);
});

return allExamples;
}, [breakpoint, transactionsDeltaQuery, functionStats]);

const timeseriesByTransaction: Record<string, Series> = useMemo(() => {
const allTimeseries: Record<string, Series> = {};
if (!defined(functionStats.data)) {
Expand All @@ -161,7 +200,7 @@ function EventAffectedTransactionsInner({
value: data.values[i],
};
}),
seriesName: 'p95()',
seriesName: 'p95(function.duration)',
};
});

Expand Down Expand Up @@ -192,15 +231,77 @@ function EventAffectedTransactionsInner({
};
}, []);

function handleGoToProfile() {
trackAnalytics('profiling_views.go_to_flamegraph', {
organization,
source: 'profiling.issue.function_regression.transactions',
});
}

return (
<EventDataSection type="transactions-impacted" title={t('Transactions Impacted')}>
<ListContainer>
{(transactionsDeltaQuery.data?.data ?? []).map(transaction => {
const series = timeseriesByTransaction[transaction.transaction as string] ?? {
const transactionName = transaction.transaction as string;
const series = timeseriesByTransaction[transactionName] ?? {
seriesName: 'p95()',
data: [],
};

const [beforeExample, afterExample] = examplesByTransaction[
transactionName
] ?? [null, null];

let before = (
<PerformanceDuration
nanoseconds={transaction[percentileBefore] as number}
abbreviation
/>
);

if (defined(beforeExample)) {
const beforeTarget = generateProfileFlamechartRouteWithQuery({
orgSlug: organization.slug,
projectSlug: project.slug,
profileId: beforeExample,
query: {
frameName,
framePackage,
},
});

before = (
<Link to={beforeTarget} onClick={handleGoToProfile}>
{before}
</Link>
);
}

let after = (
<PerformanceDuration
nanoseconds={transaction[percentileAfter] as number}
abbreviation
/>
);

if (defined(afterExample)) {
const afterTarget = generateProfileFlamechartRouteWithQuery({
orgSlug: organization.slug,
projectSlug: project.slug,
profileId: afterExample,
query: {
frameName,
framePackage,
},
});

after = (
<Link to={afterTarget} onClick={handleGoToProfile}>
{after}
</Link>
);
}

const summaryTarget = generateProfileSummaryRouteWithQuery({
orgSlug: organization.slug,
projectSlug: project.slug,
Expand Down Expand Up @@ -237,15 +338,9 @@ function EventAffectedTransactionsInner({
position="top"
>
<DurationChange>
<PerformanceDuration
nanoseconds={transaction[percentileBefore] as number}
abbreviation
/>
{before}
<IconArrow direction="right" size="xs" />
<PerformanceDuration
nanoseconds={transaction[percentileAfter] as number}
abbreviation
/>
{after}
</DurationChange>
</Tooltip>
</NumberContainer>
Expand All @@ -257,6 +352,66 @@ function EventAffectedTransactionsInner({
);
}

/**
* Find an example pair of profile ids from before and after the breakpoint.
*
* We prioritize profile ids from outside some window around the breakpoint
* because the breakpoint is not 100% accurate and giving a buffer around
* the breakpoint to so we can more accurate get a example profile from
* before and after ranges.
*
* @param examples list of example profile ids
* @param breakpointIndex the index where the breakpoint is
* @param window the window around the breakpoint to deprioritize
*/
function findExamplePair(
examples: string[],
breakpointIndex,
window = 3
): [string | null, string | null] {
let before: string | null = null;

for (let i = breakpointIndex - window; i < examples.length && i >= 0; i--) {
if (examples[i]) {
before = examples[i];
break;
}
}

if (!defined(before)) {
for (
let i = breakpointIndex;
i < examples.length && i > breakpointIndex - window;
i--
) {
if (examples[i]) {
before = examples[i];
break;
}
}
}

let after: string | null = null;

for (let i = breakpointIndex + window; i < examples.length; i++) {
if (examples[i]) {
after = examples[i];
break;
}
}

if (!defined(before)) {
for (let i = breakpointIndex; i < breakpointIndex + window; i++) {
if (examples[i]) {
after = examples[i];
break;
}
}
}

return [before, after];
}

const ListContainer = styled('div')`
display: grid;
grid-template-columns: 1fr auto auto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ function EventList({
onClick={() => {
trackAnalytics('profiling_views.go_to_flamegraph', {
organization,
source: 'profiling.issue.function_regression',
source: 'profiling.issue.function_regression.list',
});
}}
>
Expand Down
3 changes: 2 additions & 1 deletion static/app/utils/analytics/profilingAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ type ProfilingEventSource =
| 'profiling.function_trends.improvement'
| 'profiling.function_trends.regression'
| 'profiling.global_suspect_functions'
| 'profiling.issue.function_regression'
| 'profiling.issue.function_regression.list'
| 'profiling.issue.function_regression.transactions'
| 'profiling_transaction.suspect_functions_table'
| 'profiling_transaction.slowest_functions_table'
| 'profiling_transaction.regressed_functions_table'
Expand Down
29 changes: 14 additions & 15 deletions static/app/utils/profiling/hooks/useProfileEventsStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,21 @@ export function transformSingleSeries<F extends string>(
dataset: 'discover' | 'profiles' | 'profileFunctions',
yAxis: F,
rawSeries: any,
label?: string,
formatter?: any
label?: string
) {
if (!defined(formatter)) {
const type =
rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)];
formatter =
type === 'duration'
? makeFormatTo(
rawSeries.meta.units[yAxis] ??
rawSeries.meta.units[getAggregateAlias(yAxis)] ??
'nanoseconds',
'milliseconds'
)
: value => value;
}
const type =
rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)];
const formatter =
type === 'duration'
? makeFormatTo(
rawSeries.meta.units[yAxis] ??
rawSeries.meta.units[getAggregateAlias(yAxis)] ??
'nanoseconds',
'milliseconds'
)
: type === 'string'
? value => value || ''
: value => value;

const series: EventsStatsSeries<F>['data'][number] = {
axis: yAxis,
Expand Down
13 changes: 1 addition & 12 deletions static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte
import {EventsStatsSeries, PageFilters} from 'sentry/types';
import {defined} from 'sentry/utils';
import {transformSingleSeries} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
import {makeFormatTo} from 'sentry/utils/profiling/units/units';
import {useApiQuery} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
Expand Down Expand Up @@ -99,10 +98,6 @@ function transformTopEventsStatsResponse<F extends string>(

let firstSeries = true;

// TODO: the formatter should be inferred but the response does
// not contain the meta at this time
const formatter = makeFormatTo('nanoseconds', 'milliseconds');

for (const label of Object.keys(rawData)) {
for (const yAxis of yAxes) {
let dataForYAxis = rawData[label];
Expand All @@ -113,13 +108,7 @@ function transformTopEventsStatsResponse<F extends string>(
continue;
}

const transformed = transformSingleSeries(
dataset,
yAxis,
dataForYAxis,
label,
formatter
);
const transformed = transformSingleSeries(dataset, yAxis, dataForYAxis, label);

if (firstSeries) {
meta = transformed.meta;
Expand Down

0 comments on commit 1339e91

Please sign in to comment.