-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[APM] Optimize traces overview #70200
[APM] Optimize traces overview #70200
Conversation
Pinging @elastic/apm-ui (Team:apm) |
I've made some changes in |
return arrayUnionToCallable( | ||
response.aggregations?.transaction_groups.buckets | ||
).map((bucket) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the equivalent of?
return arrayUnionToCallable( | |
response.aggregations?.transaction_groups.buckets | |
).map((bucket) => { | |
return (response.aggregations?.transaction_groups.buckets ?? []).map((bucket) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, it's this issue: https://ela.st/union-callable
return { | ||
data: memoizedData, | ||
data, | ||
status, | ||
error, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for cleaning up 👍
@@ -157,7 +157,7 @@ export function registerTransactionDurationAlertType({ | |||
|
|||
const { agg } = response.aggregations; | |||
|
|||
const value = 'values' in agg ? agg.values[0] : agg?.value; | |||
const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this an unrelated change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this is a bug fix now that I think of it. This code (written by me of course) assumes that the response of a percentiles aggregation is number[]
, but it's actually Record<string, number>
where the key is the percentile. I'll verify whether that is the case and file a separate bug for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aggs: { | ||
transaction_groups: { | ||
composite: { | ||
size: bucketSize + 1, // 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should retain the comment to explain the + 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a comment lower in the file as well, do you think we need both? (no strong opinions from my side)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-added the comment
field: TRANSACTION_NAME, | ||
size, | ||
}, | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to keep it as a composite agg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah: performance, and a terms aggregation is sorted by doc_count. Sorting on composite aggregations is unpredictable, so there's a higher risk of transaction groups with lots of transactions dropping off the list.
allMetrics.forEach((metric) => { | ||
// we use indexOf so we can replace the existing item with a new one | ||
// this will give us type safety (Object.assign is unsafe) | ||
let indexOf = findIndex(items, (i) => isEqual(i.key, metric.key)); | ||
let item = items[indexOf]; | ||
|
||
if (indexOf === -1) { | ||
const newItem = { | ||
key: metric.key, | ||
}; | ||
items.push(newItem); | ||
item = newItem; | ||
indexOf = items.length - 1; | ||
} | ||
|
||
const newItem = { | ||
...item, | ||
...metric, | ||
}; | ||
|
||
items[indexOf] = newItem; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty intimidating. Can we extract this logic and add some comments?
Is the new logic or what does it replace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hate it too! I had Object.assign({}) first which was cleaner, but I ran into a bug because there was no type checking of the values I was assigning.
Comments might clear it up but I'm open to suggestions as well. This code is trying to flatten buckets from multiple requests by either a grouping record (service.name
and transaction.name
) or a grouping key (transaction.name
).
const max = Math.max(...values); | ||
const min = Math.min(...values); | ||
|
||
const duration = moment.duration(setup.end - setup.start); | ||
const minutes = duration.asMinutes(); | ||
|
||
const itemsWithRelativeImpact: TransactionGroup[] = items | ||
.map((item) => { | ||
return { | ||
key: item.key, | ||
averageResponseTime: item.avg, | ||
transactionsPerMinute: (item.count ?? 0) / minutes, | ||
impact: | ||
item.sum !== null && item.sum !== undefined | ||
? ((item.sum - min) / (max - min)) * 100 || 0 | ||
: 0, | ||
p95: item.p95, | ||
sample: item.sample!, | ||
}; | ||
}) | ||
.filter((item) => item.sample); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not having this in a separate function?
|
||
const params = mergeProjection(projection, { | ||
const request = mergeProjection(projection, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I keep running into the same issue: when I try to review these changes I end up in the rabbit hole of projections.
I started flattening it back into a single file (mostly) and I think it really improves the readability and makes it easier to spot problems.
I think it might be a good idea to go back to the drawing board and find a way to simplify ui filters (and thus projections).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is that we're forgetting why we have projections, and end up duplicating large parts of our queries in separate files. Tbh, I don't think projections are the cause of - or the solution to - some of our issues. But curious to see what you come up with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think projections are the cause of - or the solution to - some of our issues
Projections most definitely make it harder for me to make sense of the queries. But I'm not sure my suggestions are any better (I obviously think they are but I also know that most things sound better before they are converted to code).
@elasticmachine merge upstream |
@elasticmachine merge upstream |
@elasticmachine merge upstream |
…na into optimize-traces-overview
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't notice a significant performance improvement, but the code looks good.
aggs: { | ||
timeseriesData: { | ||
date_histogram: AggregationOptionsByType['date_histogram']; | ||
aggs: Record<string, MetricsAggregationMap>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this just be:
aggs: Record<string, MetricsAggregationMap>; | |
aggs: Aggs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep! Changed it to MetricAggs
in all places.
date_histogram: AggregationOptionsByType['date_histogram']; | ||
aggs: Record<string, MetricsAggregationMap>; | ||
}; | ||
} & Record<string, MetricsAggregationMap>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this just be:
} & Record<string, MetricsAggregationMap>; | |
} & Aggs; |
sample: item.sample!, | ||
}; | ||
}) | ||
.filter((item) => item.sample); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the filter for itemsWithRelativeImpact
be .filter((item) => item.impact > 0)
or is that logically equivalent to whether or not there is a sample?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's pretty much the same, but the filter is mostly a safeguard for ensuring there's actually a sample as we can't deeplink without it. Previously a sample was guaranteed because it's all one request, but splitting it up into smaller requests means that is no longer the case - for instance, when there are more than 10k buckets.
<EuiToolTip id="trace-transaction-link-tooltip" content={name}> | ||
render: (_: string, { sample }: TransactionGroup) => ( | ||
<EuiToolTip | ||
id="trace-transaction-link-tooltip" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this line was just name changes, but still: I don't think the id trace-transaction-link-tooltip
is necessary anymore. I don't see it referenced anywhere, and i don't think it's used in any unit/functional testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
💚 Build SucceededBuild metricsasync chunks size
History
To update your PR or re-run it, just comment with: |
Co-authored-by: Elastic Machine <[email protected]>
💚 Backport successfulThe PR was backported to the following branches:
|
Co-authored-by: Elastic Machine <[email protected]>
Similar to #69648, achieves a 30%-50% performance improvement of the traces overview. Could possibly improve that even more by not loading a sample, as that seems to be the slowest request of the bunch, and is only used for deeplinking, so could be lazily-loaded.
Of note:
transform.ts
and apply the transformation logic in/after fetching.