diff --git a/src/plugins/profiling/common/index.ts b/src/plugins/profiling/common/index.ts index 3f3df7b0a0835..432733119e6e1 100644 --- a/src/plugins/profiling/common/index.ts +++ b/src/plugins/profiling/common/index.ts @@ -23,41 +23,6 @@ export function getRoutePaths() { }; } -function toMilliseconds(seconds: string): number { - return parseInt(seconds, 10) * 1000; -} - -export function getTopN(obj: any) { - const data = []; - - if (obj.TopN!) { - for (const x in obj.TopN) { - if (obj.TopN.hasOwnProperty(x)) { - const values = obj.TopN[x]; - for (let i = 0; i < values.length; i++) { - const v = values[i]; - data.push({ x: toMilliseconds(x), y: v.Count, g: v.Value }); - } - } - } - } - - return data; -} - -export function groupSamplesByCategory(samples: any) { - const series = new Map(); - for (let i = 0; i < samples.length; i++) { - const v = samples[i]; - if (!series.has(v.g)) { - series.set(v.g, []); - } - const value = series.get(v.g); - value.push([v.x, v.y]); - } - return series; -} - export function timeRangeFromRequest(request: any): [number, number] { const timeFrom = parseInt(request.query.timeFrom!, 10); const timeTo = parseInt(request.query.timeTo!, 10); @@ -67,7 +32,7 @@ export function timeRangeFromRequest(request: any): [number, number] { // Converts from a Map object to a Record object since Map objects are not // serializable to JSON by default export function fromMapToRecord(m: Map): Record { - let output: Record = {}; + const output: Record = {}; for (const [key, value] of m) { output[key] = value; diff --git a/src/plugins/profiling/common/topn.ts b/src/plugins/profiling/common/topn.ts index b11ec56346f0c..3abc827bd4fa6 100644 --- a/src/plugins/profiling/common/topn.ts +++ b/src/plugins/profiling/common/topn.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { orderBy } from 'lodash'; + import { AggregationsHistogramAggregate, AggregationsHistogramBucket, @@ -13,42 +15,62 @@ import { import { StackFrameMetadata } from './profiling'; -type TopNBucket = { - Value: string; +export interface TopNSample { + Timestamp: number; Count: number; -}; - -type TopNBucketsByDate = { - TopN: Record; -}; + Category: string; +} -type TopNContainers = TopNBucketsByDate; -type TopNDeployments = TopNBucketsByDate; -type TopNHosts = TopNBucketsByDate; -type TopNThreads = TopNBucketsByDate; +export interface TopNSamples { + TopN: TopNSample[]; +} -type TopNTraces = TopNBucketsByDate & { +interface TopNTraces extends TopNSamples { Metadata: Record; -}; - -type TopN = TopNContainers | TopNDeployments | TopNHosts | TopNThreads | TopNTraces; +} -export function createTopNBucketsByDate( - histogram: AggregationsHistogramAggregate -): TopNBucketsByDate { - const topNBucketsByDate: Record = {}; +export function createTopNSamples(histogram: AggregationsHistogramAggregate): TopNSample[] { + const bucketsByTimestamp = new Map(); + const uniqueCategories = new Set(); + // Convert the histogram into nested maps and record the unique categories const histogramBuckets = (histogram?.buckets as AggregationsHistogramBucket[]) ?? []; for (let i = 0; i < histogramBuckets.length; i++) { - const key = histogramBuckets[i].key / 1000; - topNBucketsByDate[key] = []; - histogramBuckets[i].group_by.buckets.forEach((item: any) => { - topNBucketsByDate[key].push({ - Value: item.key, - Count: item.count.value, - }); - }); + const frameCountsByCategory = new Map(); + const items = histogramBuckets[i].group_by.buckets; + for (let j = 0; j < items.length; j++) { + uniqueCategories.add(items[j].key); + frameCountsByCategory.set(items[j].key, items[j].count.value); + } + bucketsByTimestamp.set(histogramBuckets[i].key, frameCountsByCategory); } - return { TopN: topNBucketsByDate }; + // Normalize samples so there are an equal number of data points per each timestamp + const samples: TopNSample[] = []; + for (const timestamp of bucketsByTimestamp.keys()) { + for (const category of uniqueCategories.values()) { + const frameCountsByCategory = bucketsByTimestamp.get(timestamp); + const sample: TopNSample = { + Timestamp: timestamp, + Count: frameCountsByCategory.get(category) ?? 0, + Category: category, + }; + samples.push(sample); + } + } + + return orderBy(samples, ['Timestamp', 'Count', 'Category'], ['asc', 'desc', 'asc']); +} + +export function groupSamplesByCategory(samples: TopNSample[]) { + const series = new Map(); + for (let i = 0; i < samples.length; i++) { + const v = samples[i]; + if (!series.has(v.Category)) { + series.set(v.Category, []); + } + const value = series.get(v.Category); + value.push([v.Timestamp, v.Count]); + } + return series; } diff --git a/src/plugins/profiling/public/app.tsx b/src/plugins/profiling/public/app.tsx index dd038a66fdff1..7111d07f84930 100644 --- a/src/plugins/profiling/public/app.tsx +++ b/src/plugins/profiling/public/app.tsx @@ -137,7 +137,14 @@ function App({ fetchTopN, fetchElasticFlamechart }: Props) { fetchTopN={fetchTopN} setTopN={setTopN} /> - + diff --git a/src/plugins/profiling/public/components/stacked-bar-chart.tsx b/src/plugins/profiling/public/components/stacked-bar-chart.tsx index 68187b45a2803..506c6f3c04c86 100644 --- a/src/plugins/profiling/public/components/stacked-bar-chart.tsx +++ b/src/plugins/profiling/public/components/stacked-bar-chart.tsx @@ -46,8 +46,8 @@ export const StackedBarChart: React.FC = ({ data={ctx.samples} xAccessor={x} yAccessors={[y]} + stackAccessors={[x]} splitSeriesAccessors={[category]} - stackAccessors={[category]} /> Number(d).toFixed(0)} /> diff --git a/src/plugins/profiling/public/components/stacktrace-nav.tsx b/src/plugins/profiling/public/components/stacktrace-nav.tsx index 030424b70c19b..f62f8ca4c90c2 100644 --- a/src/plugins/profiling/public/components/stacktrace-nav.tsx +++ b/src/plugins/profiling/public/components/stacktrace-nav.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useState } from 'react'; import { EuiButtonGroup } from '@elastic/eui'; -import { getTopN, groupSamplesByCategory } from '../../common'; +import { groupSamplesByCategory, TopNSample, TopNSamples } from '../../common/topn'; export const StackTraceNavigation = ({ index, projectID, n, timeRange, fetchTopN, setTopN }) => { const topnButtonGroupPrefix = 'topnButtonGroup'; @@ -59,11 +59,12 @@ export const StackTraceNavigation = ({ index, projectID, n, timeRange, fetchTopN console.log(new Date().toISOString(), 'started payload retrieval'); fetchTopN(topnValue[0].value, index, projectID, timeRange.unixStart, timeRange.unixEnd, n).then( - (response) => { + (response: TopNSamples) => { console.log(new Date().toISOString(), 'finished payload retrieval'); - const samples = getTopN(response); + const samples = response.TopN; const series = groupSamplesByCategory(samples); - setTopN({ samples, series }); + const samplesWithoutZero = samples.filter((sample: TopNSample) => sample.Count > 0); + setTopN({ samples: samplesWithoutZero, series }); console.log(new Date().toISOString(), 'updated local state'); } ); diff --git a/src/plugins/profiling/server/routes/topn.ts b/src/plugins/profiling/server/routes/topn.ts index cc63f9f2f0255..37d4bfdb783b2 100644 --- a/src/plugins/profiling/server/routes/topn.ts +++ b/src/plugins/profiling/server/routes/topn.ts @@ -15,7 +15,7 @@ import { import type { DataRequestHandlerContext } from '../../../data/server'; import { fromMapToRecord, getRoutePaths } from '../../common'; import { groupStackFrameMetadataByStackTrace, StackTraceID } from '../../common/profiling'; -import { createTopNBucketsByDate } from '../../common/topn'; +import { createTopNSamples } from '../../common/topn'; import { findDownsampledIndex } from './downsampling'; import { logExecutionLatency } from './logger'; import { autoHistogramSumCountOnGroupByField, newProjectTimeQuery } from './query'; @@ -69,11 +69,11 @@ export async function topNElasticSearchQuery( ); const histogram = getAggs(resEvents)?.histogram as AggregationsHistogramAggregate; - const topN = createTopNBucketsByDate(histogram); + const topN = createTopNSamples(histogram); if (searchField !== 'StackTraceID') { return response.ok({ - body: topN, + body: { TopN: topN }, }); } @@ -113,7 +113,7 @@ export async function topNElasticSearchQuery( ); return response.ok({ body: { - ...topN, + TopN: topN, Metadata: metadata, }, });