Flamegraph: Diff profile support (#72383)

This commit is contained in:
Andrej Ocenas
2023-08-01 16:08:46 +02:00
committed by GitHub
parent e0587dfb30
commit 91c7096eda
23 changed files with 1095 additions and 108 deletions

View File

@@ -4777,6 +4777,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"] [0, 0, 0, "Unexpected any. Specify a different type.", "2"]
], ],
"public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx:5381": [ "public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],

View File

@@ -29,6 +29,7 @@ title: TestDataDataQuery kind
| `datasource` | | No | | For mixed data sources the selected datasource is on the query level.<br/>For non mixed scenarios this is undefined.<br/>TODO find a better way to do this ^ that's friendly to schema<br/>TODO this shouldn't be unknown but DataSourceRef &#124; null | | `datasource` | | No | | For mixed data sources the selected datasource is on the query level.<br/>For non mixed scenarios this is undefined.<br/>TODO find a better way to do this ^ that's friendly to schema<br/>TODO this shouldn't be unknown but DataSourceRef &#124; null |
| `dropPercent` | number | No | | Drop percentage (the chance we will lose a point 0-100) | | `dropPercent` | number | No | | Drop percentage (the chance we will lose a point 0-100) |
| `errorType` | string | No | | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. | | `errorType` | string | No | | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. |
| `flamegraphDiff` | boolean | No | | |
| `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)<br/>Note this does not always imply that the query should not be executed since<br/>the results from a hidden query may be used as the input to other queries (SSE etc) | | `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)<br/>Note this does not always imply that the query should not be executed since<br/>the results from a hidden query may be used as the input to other queries (SSE etc) |
| `labels` | string | No | | | | `labels` | string | No | | |
| `levelColumn` | boolean | No | | | | `levelColumn` | boolean | No | | |

View File

@@ -119,6 +119,7 @@ export interface TestDataDataQuery extends common.DataQuery {
*/ */
dropPercent?: number; dropPercent?: number;
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable'); errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
flamegraphDiff?: boolean;
labels?: string; labels?: string;
levelColumn?: boolean; levelColumn?: boolean;
lines?: number; lines?: number;

View File

@@ -163,6 +163,7 @@ type TestDataDataQuery struct {
// Drop percentage (the chance we will lose a point 0-100) // Drop percentage (the chance we will lose a point 0-100)
DropPercent *float64 `json:"dropPercent,omitempty"` DropPercent *float64 `json:"dropPercent,omitempty"`
ErrorType *ErrorType `json:"errorType,omitempty"` ErrorType *ErrorType `json:"errorType,omitempty"`
FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"`
Labels *string `json:"labels,omitempty"` Labels *string `json:"labels,omitempty"`
LevelColumn *bool `json:"levelColumn,omitempty"` LevelColumn *bool `json:"levelColumn,omitempty"`
Lines *int64 `json:"lines,omitempty"` Lines *int64 `json:"lines,omitempty"`

View File

@@ -344,6 +344,17 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
</InlineField> </InlineField>
)} )}
{scenarioId === TestDataQueryType.FlameGraph && (
<InlineField label={'Diff profile'} grow>
<InlineSwitch
value={Boolean(query.flamegraphDiff)}
onChange={(e) => {
onUpdate({ ...query, flamegraphDiff: e.currentTarget.checked });
}}
/>
</InlineField>
)}
{scenarioId === TestDataQueryType.PredictablePulse && ( {scenarioId === TestDataQueryType.PredictablePulse && (
<PredictablePulseEditor onChange={onPulseWaveChange} query={query} ds={datasource} /> <PredictablePulseEditor onChange={onPulseWaveChange} query={query} ds={datasource} />
)} )}

View File

@@ -50,6 +50,8 @@ composableKinds: DataQuery: {
// Drop percentage (the chance we will lose a point 0-100) // Drop percentage (the chance we will lose a point 0-100)
dropPercent?: float64 dropPercent?: float64
flamegraphDiff?: bool
#TestDataQueryType: "random_walk" | "slow_query" | "random_walk_with_error" | "random_walk_table" | "exponential_heatmap_bucket_data" | "linear_heatmap_bucket_data" | "no_data_points" | "datapoints_outside_range" | "csv_metric_values" | "predictable_pulse" | "predictable_csv_wave" | "streaming_client" | "simulation" | "usa" | "live" | "grafana_api" | "arrow" | "annotations" | "table_static" | "server_error_500" | "logs" | "node_graph" | "flame_graph" | "raw_frame" | "csv_file" | "csv_content" | "trace" | "manual_entry" | "variables-query" @cuetsy(kind="enum", memberNames="RandomWalk|SlowQuery|RandomWalkWithError|RandomWalkTable|ExponentialHeatmapBucketData|LinearHeatmapBucketData|NoDataPoints|DataPointsOutsideRange|CSVMetricValues|PredictablePulse|PredictableCSVWave|StreamingClient|Simulation|USA|Live|GrafanaAPI|Arrow|Annotations|TableStatic|ServerError500|Logs|NodeGraph|FlameGraph|RawFrame|CSVFile|CSVContent|Trace|ManualEntry|VariablesQuery") #TestDataQueryType: "random_walk" | "slow_query" | "random_walk_with_error" | "random_walk_table" | "exponential_heatmap_bucket_data" | "linear_heatmap_bucket_data" | "no_data_points" | "datapoints_outside_range" | "csv_metric_values" | "predictable_pulse" | "predictable_csv_wave" | "streaming_client" | "simulation" | "usa" | "live" | "grafana_api" | "arrow" | "annotations" | "table_static" | "server_error_500" | "logs" | "node_graph" | "flame_graph" | "raw_frame" | "csv_file" | "csv_content" | "trace" | "manual_entry" | "variables-query" @cuetsy(kind="enum", memberNames="RandomWalk|SlowQuery|RandomWalkWithError|RandomWalkTable|ExponentialHeatmapBucketData|LinearHeatmapBucketData|NoDataPoints|DataPointsOutsideRange|CSVMetricValues|PredictablePulse|PredictableCSVWave|StreamingClient|Simulation|USA|Live|GrafanaAPI|Arrow|Annotations|TableStatic|ServerError500|Logs|NodeGraph|FlameGraph|RawFrame|CSVFile|CSVContent|Trace|ManualEntry|VariablesQuery")
#StreamingQuery: { #StreamingQuery: {

View File

@@ -116,6 +116,7 @@ export interface TestData extends common.DataQuery {
*/ */
dropPercent?: number; dropPercent?: number;
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable'); errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
flamegraphDiff?: boolean;
labels?: string; labels?: string;
levelColumn?: boolean; levelColumn?: boolean;
lines?: number; lines?: number;

View File

@@ -24,7 +24,7 @@ import { Scenario, TestData, TestDataQueryType } from './dataquery.gen';
import { queryMetricTree } from './metricTree'; import { queryMetricTree } from './metricTree';
import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils'; import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
import { runStream } from './runStreams'; import { runStream } from './runStreams';
import { flameGraphData } from './testData/flameGraphResponse'; import { flameGraphData, flameGraphDataDiff } from './testData/flameGraphResponse';
import { TestDataVariableSupport } from './variables'; import { TestDataVariableSupport } from './variables';
export class TestDataDataSource extends DataSourceWithBackend<TestData> { export class TestDataDataSource extends DataSourceWithBackend<TestData> {
@@ -243,7 +243,8 @@ export class TestDataDataSource extends DataSourceWithBackend<TestData> {
} }
flameGraphQuery(target: TestData): Observable<DataQueryResponse> { flameGraphQuery(target: TestData): Observable<DataQueryResponse> {
return of({ data: [{ ...flameGraphData, refId: target.refId }] }).pipe(delay(100)); const data = target.flamegraphDiff ? flameGraphDataDiff : flameGraphData;
return of({ data: [{ ...data, refId: target.refId }] }).pipe(delay(100));
} }
trace(target: TestData, options: DataQueryRequest<TestData>): Observable<DataQueryResponse> { trace(target: TestData, options: DataQueryRequest<TestData>): Observable<DataQueryResponse> {

View File

@@ -763,3 +763,449 @@ export const flameGraphData: DataFrameDTO = {
}, },
], ],
}; };
export const flameGraphDataDiff: DataFrameDTO = {
name: 'response',
meta: { preferredVisualisationType: 'flamegraph' },
fields: [
{
name: 'level',
values: [
0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 9, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 20, 21,
19, 20, 21, 19, 17, 18, 19, 20, 20, 21, 19, 20, 21, 19, 17, 18, 19, 20, 21, 22, 23, 24, 25, 23, 24, 21, 19, 20,
21, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 22, 15, 16, 17, 18, 19,
20, 17, 18, 19, 20, 21, 22, 23, 23, 24, 24, 25, 21, 17, 18, 19, 19, 17, 18, 19, 20, 16, 17, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 22, 22, 22, 22, 23, 20, 20, 16, 17, 18, 19, 16, 17, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 17, 18, 19, 20, 21, 22, 23, 15, 16, 15, 16, 12, 13, 9, 9, 10, 9, 10, 6, 6, 6, 6, 7, 8, 6, 7, 2, 3, 4, 5, 6,
7, 3, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 10, 11, 12, 13, 14, 15, 16, 16, 17, 18, 19, 20, 21, 22,
14, 15, 16, 14, 11, 12, 13, 14, 9, 10, 11, 12, 10, 9, 10, 11, 12, 13, 14, 15, 16, 17, 15, 16, 17, 16, 15, 16,
17, 16, 17, 16, 15, 16, 17, 18, 19, 20, 21, 19, 4, 5, 6, 7, 8, 9, 6, 7, 8, 6, 7, 8, 9, 10, 7, 4, 5, 6, 7, 8, 3,
4, 5, 6, 7, 8, 9, 10, 11, 12, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 6,
7, 8, 9, 10, 11, 7, 8, 6, 7, 8, 9, 10, 11, 12, 13, 6, 7, 8, 9, 10, 11, 12, 13, 14, 6, 7, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 3, 4, 5, 6, 1, 2,
],
},
{
name: 'label',
values: [
'total',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!Heartbeat.<>c.<.ctor>b__8_0',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!Heartbeat.TimerLoop',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!Heartbeat.OnHeartbeat',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!DateHeaderValueManager.SetDateValues',
'Microsoft.Net.Http.Headers!HeaderUtilities.FormatDate',
'System!DateTimeFormat.Format',
'System.Threading!PortableThreadPool.WorkerThread.WorkerThreadStart',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection<T>.System.Threading.IThreadPoolWorkItem.Execute',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection<T>.ExecuteAsync',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<ExecuteAsync>d__6>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection.<ExecuteAsync>d__6<T>.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnection..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnection.ProcessRequestsAsync<!T0>',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<ProcessRequestsAsync>d__12>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnection.<ProcessRequestsAsync>d__12<TContext>.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1OutputProducer..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.ProcessRequestsAsync<!T0>',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<ProcessRequestsAsync>d__222>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.<ProcessRequestsAsync>d__222<TContext>.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.ProcessRequests<!T0>',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<ProcessRequests>d__223>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.<ProcessRequests>d__223<TContext>.MoveNext',
'Microsoft.AspNetCore.HostFiltering!HostFilteringMiddleware.Invoke',
'Microsoft.AspNetCore.Routing!EndpointMiddleware.Invoke',
'Example!Program.<>c__DisplayClass0_0.<Main>b__0',
'Example!BikeService.Order',
'Example!OrderService.FindNearestVehicle',
'Pyroscope!LabelSet.BuildUpon',
'Pyroscope!LabelsWrapper.Do',
'Pyroscope!Profiler.get_Instance',
'Pyroscope!LabelSet.Builder.Add',
'System.Collections.Generic!Dictionary<TKey, TKey>.TryInsert',
'System.Collections.Generic!Dictionary<TKey, TKey>.Initialize',
'Pyroscope!LabelSet.Builder.Build',
'Example!Program.<>c__DisplayClass0_0.<Main>b__1',
'Example!ScooterService.Order',
'Example!OrderService.FindNearestVehicle',
'Pyroscope!LabelSet.BuildUpon',
'Pyroscope!LabelsWrapper.Do',
'Pyroscope!Profiler.get_Instance',
'Pyroscope!LabelSet.Builder.Add',
'System.Collections.Generic!Dictionary<TKey, TKey>.TryInsert',
'System.Collections.Generic!Dictionary<TKey, TKey>.Initialize',
'Pyroscope!LabelSet.Builder.Build',
'Example!Program.<>c__DisplayClass0_0.<Main>b__2',
'Example!CarService.Order',
'Example!OrderService.FindNearestVehicle',
'Pyroscope!LabelsWrapper.Do',
'Example!OrderService.<>c__DisplayClass0_1.<FindNearestVehicle>b__0',
'Example!OrderService.CheckDriverAvailability',
'Pyroscope!LabelSet.BuildUpon',
'System.Collections.Generic!Dictionary<TKey, TKey>..ctor',
'System.Collections.Generic!Dictionary<TKey, TKey>.Initialize',
'Pyroscope!LabelsWrapper.Do',
'Pyroscope!Profiler.get_Instance',
'Pyroscope!Profiler.get_Instance',
'Pyroscope!LabelSet.Builder.Add',
'System.Collections.Generic!Dictionary<TKey, TKey>.TryInsert',
'System.Collections.Generic!Dictionary<TKey, TKey>.Initialize',
'Microsoft.AspNetCore.Http!RequestDelegateFactory.ExecuteWriteStringResponseAsync',
'Microsoft.AspNetCore.Http!HttpResponseWritingExtensions.WriteAsync',
'Microsoft.AspNetCore.Http!HttpResponseWritingExtensions.WriteAsync',
'Microsoft.AspNetCore.Http!DefaultHttpResponse.StartAsync',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.StartAsync',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1OutputProducer.WriteResponseHeaders',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers!ConcurrentPipeWriter.GetSpan',
'System.IO.Pipelines!Pipe.DefaultPipeWriter.GetSpan',
'System.IO.Pipelines!Pipe.AllocateWriteHeadSynchronized',
'System.IO.Pipelines!Pipe.CreateSegmentUnsynchronized',
'Microsoft.Extensions.Logging!LoggerMessage.<>c__DisplayClass10_0<T1>.<Define>g__Log|0',
'Microsoft.Extensions.Logging!Logger<T>.Microsoft.Extensions.Logging.ILogger.Log<!LogValues>',
'Microsoft.Extensions.Logging!Logger.Log<!LogValues>',
'Microsoft.Extensions.Logging!Logger.<Log>g__LoggerLog|12_0<!LogValues>',
'Microsoft.Extensions.Logging.Console!ConsoleLogger.Log<!LogValues>',
'Microsoft.Extensions.Logging.Console!SimpleConsoleFormatter.Write<!LogValues>',
'Microsoft.Extensions.Logging!LoggerMessage.LogValues.<>c<T0>.<.cctor>b__12_0',
'Microsoft.Extensions.Logging!LoggerMessage.LogValues<T0>.ToString',
'System!String.FormatHelper',
'System!Span<System!Char>.ToString',
'System!String.Ctor',
'System.Text!StringBuilder.ToString',
'Microsoft.AspNetCore.Hosting!HostingApplication.CreateContext',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.BeginRequest',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.Log.RequestScope',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.Log.HostingLogScope..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature.get_TraceIdentifier',
'System!String.Create<System!ValueTuple>',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.LogRequestStarting',
'Microsoft.Extensions.Logging!Logger.Log<!T0>',
'Microsoft.Extensions.Logging!Logger.<Log>g__LoggerLog|12_0<!T0>',
'Microsoft.Extensions.Logging.Console!ConsoleLogger.Log<!T0>',
'Microsoft.Extensions.Logging.Console!SimpleConsoleFormatter.Write<!T0>',
'Microsoft.AspNetCore.Hosting!HostingRequestStartingLog.ToString',
'System.Buffers!TlsOverPerCoreLockedStacksArrayPool<System!Char>.Rent',
'System.Runtime.CompilerServices!DefaultInterpolatedStringHandler.ToStringAndClear',
'System!String.Ctor',
'System.Buffers!TlsOverPerCoreLockedStacksArrayPool<System!Char>.Return',
'System.Buffers!TlsOverPerCoreLockedStacksArrayPool<System!Char>.InitializeTlsBucketsAndTrimming',
'System.Text!StringBuilder.ToString',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.StartActivity',
'System.Diagnostics!Activity.Start',
'System!String.Create<System!ValueTuple>',
'System.Threading!ExecutionContext.SetLocalValue',
'Microsoft.Extensions.Logging!Logger.BeginScope<!T0>',
'Microsoft.Extensions.Logging!LoggerFactoryScopeProvider.Push',
'System.Threading!ExecutionContext.SetLocalValue',
'System.Threading!AsyncLocalValueMap.TwoElementAsyncLocalValueMap.Set',
'Microsoft.AspNetCore.Http!DefaultHttpContextFactory.Create',
'Microsoft.AspNetCore.Http!DefaultHttpContext..ctor',
'Microsoft.AspNetCore.Hosting!HostingApplication.DisposeContext',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.LogRequestFinished',
'Microsoft.Extensions.Logging!Logger.Log<!T0>',
'Microsoft.Extensions.Logging!Logger.<Log>g__LoggerLog|12_0<!T0>',
'Microsoft.Extensions.Logging.Console!ConsoleLogger.Log<!T0>',
'Microsoft.Extensions.Logging.Console!SimpleConsoleFormatter.Write<!T0>',
'Microsoft.AspNetCore.Hosting!HostingRequestFinishedLog.ToString',
'System!Number.FormatDouble',
'System.Text!ValueStringBuilder.ToString',
'System!Span<System!Char>.ToString',
'System!String.Ctor',
'System!Number.UInt32ToDecStr',
'System!String.Replace',
'System!String.Substring',
'System.Runtime.CompilerServices!DefaultInterpolatedStringHandler.ToStringAndClear',
'System!String.Ctor',
'System.Text!StringBuilder.ToString',
'System.Text!StringBuilder.set_Length',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.StopActivity',
'System.Diagnostics!Activity.Stop',
'System.Threading!ExecutionContext.SetLocalValue',
'System.Threading!AsyncLocalValueMap.TwoElementAsyncLocalValueMap.Set',
'Microsoft.Extensions.Logging!LoggerFactoryScopeProvider.Scope.Dispose',
'System.Threading!ExecutionContext.SetLocalValue',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.TryParseRequest',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.ParseRequest',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.TakeMessageHeaders',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.ParseHeaders',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.TryTakeSingleHeader',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.OnHeader',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpRequestHeaders.Append',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!HttpUtilities.GetRequestHeaderString',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!StringUtilities.GetAsciiOrUTF8StringNonNullCharacters',
'System!String.Create<System!IntPtr>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.TakeStartLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.ParseRequestLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.ParseRequestLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.OnStartLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.OnOriginFormTarget',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!StringUtilities.GetAsciiStringNonNullCharacters',
'System!String.Create<System!IntPtr>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.InitializeBodyControl',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!BodyControl..ctor',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.AwaitUnsafeOnCompleted<System.Runtime.CompilerServices!ValueTaskAwaiter, !<ProcessRequests>d__223>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<ProcessRequests>d__223>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.AwaitUnsafeOnCompleted<System.Runtime.CompilerServices!TaskAwaiter, !<ProcessRequestsAsync>d__222>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<ProcessRequestsAsync>d__222>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection.OnHeartbeat',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.AwaitUnsafeOnCompleted<System.Runtime.CompilerServices!TaskAwaiter, !<ProcessRequestsAsync>d__12>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<ProcessRequestsAsync>d__12>',
'System.Threading!CancellationToken.Register',
'System.Threading!CancellationTokenSource.Register',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnectionMiddleware<TContext>.OnConnectionAsync',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection.BeginConnectionScope',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!TimeoutControl..ctor',
'Microsoft.Extensions.Logging!Logger.BeginScope<!T0>',
'Microsoft.Extensions.Logging!LoggerFactoryScopeProvider.Push',
'System.Threading!ExecutionContext.SetLocalValue',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.AwaitUnsafeOnCompleted<System.Runtime.CompilerServices!TaskAwaiter, !<ExecuteAsync>d__6>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<ExecuteAsync>d__6>',
'System.Threading!ThreadPoolWorkQueue.Dispatch',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!IOQueue.System.Threading.IThreadPoolWorkItem.Execute',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<System.Threading.Tasks!VoidTaskResult, Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoSend>d__28>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoSend>d__28.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.Shutdown',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<>c.<FireConnectionClosed>b__29_0',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.CancelConnectionClosedToken',
'System.Threading!CancellationTokenSource.ExecuteCallbackHandlers',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnection.OnConnectionClosed',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.OnInputOrOutputCompleted',
'System.Net.Sockets!SocketAsyncEngine.System.Threading.IThreadPoolWorkItem.Execute',
'System.Net.Sockets!Socket.AwaitableSocketAsyncEventArgs.OnCompleted',
'System.Net.Sockets!Socket.AwaitableSocketAsyncEventArgs.InvokeContinuation',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets!SocketConnectionListener.<AcceptAsync>d__10.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets!SocketConnectionContextFactory.Create',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection..ctor',
'System.IO.Pipelines!DuplexPipe.CreateConnectionPair',
'System.IO.Pipelines!Pipe..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.Start',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.DoReceive',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<DoReceive>d__27>',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoReceive>d__27.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketReceiver.WaitForDataAsync',
'System.Net.Sockets!Socket.ReceiveAsync',
'System.Net.Sockets!SocketAsyncContext..ctor',
'System.Net.Sockets!SocketAsyncEventArgs.DoOperationReceive',
'System.Net.Sockets!SocketAsyncContext.ReceiveAsync',
'System.Net.Sockets!SocketAsyncContext.OperationQueue<TOperation>.StartAsyncOperation',
'System.Net.Sockets!SocketAsyncContext.TryRegister',
'System.Net.Sockets!SocketAsyncEngine.TryRegisterSocket',
'System.Net.Sockets!SocketAsyncEngine.TryRegisterCore',
'System.Collections.Concurrent!ConcurrentDictionary<System!IntPtr, System.Net.Sockets!SocketAsyncEngine.SocketAsyncContextWrapper>.TryAddInternal',
'System.IO.Pipelines!Pipe.GetMemory',
'System.IO.Pipelines!Pipe.AllocateWriteHeadSynchronized',
'System.IO.Pipelines!Pipe.CreateSegmentUnsynchronized',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<DoReceive>d__27>',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.DoSend',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<DoSend>d__28>',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoSend>d__28.MoveNext',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<DoSend>d__28>',
'System.Net.Sockets!Socket.get_LocalEndPoint',
'System.Net.Sockets!IPEndPointExtensions.Create',
'System.Net.Internals!SocketAddress.GetIPEndPoint',
'System.Net.Internals!SocketAddress.GetIPAddress',
'System.Net.Sockets!IPEndPointExtensions.Serialize',
'System.Threading.Tasks!Task<TResult>.TrySetResult',
'System.Threading.Tasks!Task.RunContinuations',
'System.Threading.Tasks!AwaitTaskContinuation.RunOrScheduleAction',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!ConnectionDispatcher.<>c__DisplayClass8_0.<<StartAcceptingConnectionsCore>g__AcceptConnectionsAsync|0>d<T>.MoveNext',
'Microsoft.AspNetCore.Connections!TransportConnection.Microsoft.AspNetCore.Http.Features.IFeatureCollection.Set<!T0>',
'System.Collections.Generic!List<T>.AddWithResize',
'System.Collections.Generic!List<T>.set_Capacity',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection<T>..ctor',
'Microsoft.AspNetCore.Connections!TransportConnection.Microsoft.AspNetCore.Http.Features.IFeatureCollection.Set<!T0>',
'Microsoft.AspNetCore.Connections!TransportConnection.ExtraFeatureSet',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!TransportConnectionManager.AddConnection',
'Microsoft.AspNetCore.Connections!TransportConnection.get_ConnectionId',
'System!String.Create<System!Int64>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!ConnectionManager.AddConnection',
'System.Collections.Concurrent!ConcurrentDictionary<TKey, TKey>.TryAddInternal',
'System.Collections.Concurrent!ConcurrentDictionary<TKey, TKey>.TryAddInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!TransportManager.GenericConnectionListener.AcceptAsync',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets!SocketConnectionListener.AcceptAsync',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<AcceptAsync>d__10>',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets!SocketConnectionListener.<AcceptAsync>d__10.MoveNext',
'System.Net.Sockets!Socket.AwaitableSocketAsyncEventArgs.AcceptAsync',
'System.Net.Sockets!Socket.AcceptAsync',
'System.Net.Sockets!SocketAsyncEventArgs.DoOperationAccept',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<TResult>.GetStateMachineBox<!<AcceptAsync>d__10>',
'System.Net.Sockets!SocketAsyncEventArgs.AcceptCompletionCallback',
'System.Net.Sockets!SocketAsyncEventArgs.FinishOperationSyncSuccess',
'System.Net.Sockets!IPEndPointExtensions.Create',
'System.Net.Internals!SocketAddress.GetIPEndPoint',
'System.Net.Internals!SocketAddress.GetIPAddress',
'System.Net!IPAddress..ctor',
'System.Net.Sockets!IPEndPointExtensions.Serialize',
'System.Net.Internals!SocketAddress..ctor',
'System.Net.Internals!SocketAddress..ctor',
'System.Net.Sockets!SocketAsyncEventArgs.FinishOperationAccept',
'System.Net.Sockets!IPEndPointExtensions.Create',
'System.Net.Internals!SocketAddress.GetIPEndPoint',
'System.Net.Internals!SocketAddress.GetIPAddress',
'System.Net!IPAddress..ctor',
'System.Net.Sockets!SocketPal.CreateSocket',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<System.Threading.Tasks!VoidTaskResult, Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoReceive>d__27>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DoReceive>d__27.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.FireConnectionClosed',
'System.Threading!ThreadPool.UnsafeQueueUserWorkItem<!T0>',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunFromThreadPoolDispatchLoop',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.<ProcessRequests>d__223<TContext>.MoveNext',
'Microsoft.AspNetCore.HostFiltering!HostFilteringMiddleware.Invoke',
'Microsoft.AspNetCore.Routing!EndpointMiddleware.Invoke',
'Example!Program.<>c__DisplayClass0_0.<Main>b__0',
'Example!BikeService.Order',
'Example!OrderService.FindNearestVehicle',
'Pyroscope!LabelsWrapper.Do',
'Pyroscope!Profiler.get_Instance',
'Microsoft.AspNetCore.Http!RequestDelegateFactory.ExecuteWriteStringResponseAsync',
'Microsoft.AspNetCore.Http!HttpResponseWritingExtensions.WriteAsync',
'Microsoft.AspNetCore.Http!HttpResponseWritingExtensions.WriteAsync',
'Microsoft.AspNetCore.Http!DefaultHttpResponse.StartAsync',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.StartAsync',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1OutputProducer.WriteResponseHeaders',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers!ConcurrentPipeWriter.GetSpan',
'System.IO.Pipelines!Pipe.DefaultPipeWriter.GetSpan',
'System.IO.Pipelines!Pipe.AllocateWriteHeadSynchronized',
'System.IO.Pipelines!Pipe.CreateSegmentUnsynchronized',
'Microsoft.Extensions.Logging!LoggerMessage.<>c__DisplayClass10_0<T1>.<Define>g__Log|0',
'Microsoft.Extensions.Logging!Logger<T>.Microsoft.Extensions.Logging.ILogger.Log<!LogValues>',
'Microsoft.Extensions.Logging!Logger.Log<!LogValues>',
'Microsoft.Extensions.Logging!Logger.<Log>g__LoggerLog|12_0<!LogValues>',
'Microsoft.Extensions.Logging.Console!ConsoleLogger.Log<!LogValues>',
'Microsoft.Extensions.Logging.Console!SimpleConsoleFormatter.Write<!LogValues>',
'Microsoft.Extensions.Logging!LoggerMessage.LogValues.<>c<T0>.<.cctor>b__12_0',
'Microsoft.Extensions.Logging!LoggerMessage.LogValues<T0>.ToString',
'System!String.FormatHelper',
'System!Span<System!Char>.ToString',
'System!String.Ctor',
'Microsoft.AspNetCore.Hosting!HostingApplication.CreateContext',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.BeginRequest',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.Log.RequestScope',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.Log.HostingLogScope..ctor',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature.get_TraceIdentifier',
'System!String.Create<System!ValueTuple>',
'Microsoft.AspNetCore.Http!DefaultHttpContextFactory.Create',
'Microsoft.AspNetCore.Http!DefaultHttpContext..ctor',
'Microsoft.AspNetCore.Hosting!HostingApplication.DisposeContext',
'Microsoft.AspNetCore.Hosting!HostingApplicationDiagnostics.LogRequestFinished',
'Microsoft.Extensions.Logging!Logger.Log<!T0>',
'Microsoft.Extensions.Logging!Logger.<Log>g__LoggerLog|12_0<!T0>',
'Microsoft.Extensions.Logging.Console!ConsoleLogger.Log<!T0>',
'Microsoft.Extensions.Logging.Console!SimpleConsoleFormatter.Write<!T0>',
'Microsoft.AspNetCore.Hosting!HostingRequestFinishedLog.ToString',
'System!Number.UInt32ToDecStr',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.TryParseRequest',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.ParseRequest',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.TakeStartLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.ParseRequestLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpParser<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1ParsingHandler>.ParseRequestLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.OnStartLine',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!Http1Connection.OnOriginFormTarget',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!StringUtilities.GetAsciiStringNonNullCharacters',
'System!String.Create<System!IntPtr>',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.InitializeBodyControl',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!BodyControl..ctor',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.SetResult',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.SetExistingTaskResult',
'System.Threading.Tasks!Task.RunContinuations',
'System.Threading.Tasks!AwaitTaskContinuation.RunOrScheduleAction',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http!HttpProtocol.<ProcessRequestsAsync>d__222<TContext>.MoveNext',
'System.Threading.Tasks!Task<System.Threading.Tasks!VoidTaskResult>.TrySetResult',
'System.Threading.Tasks!Task.RunContinuations',
'System.Threading.Tasks!AwaitTaskContinuation.RunOrScheduleAction',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal!HttpConnection.<ProcessRequestsAsync>d__12<TContext>.MoveNext',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.SetExistingTaskResult',
'System.Threading.Tasks!Task.RunContinuations',
'System.Threading.Tasks!AwaitTaskContinuation.RunOrScheduleAction',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder.AsyncStateMachineBox<TResult, TResult>.MoveNext',
'System.Threading!ExecutionContext.RunInternal',
'Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure!KestrelConnection.<ExecuteAsync>d__6<T>.MoveNext',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.DisposeAsync',
'System.Runtime.CompilerServices!AsyncMethodBuilderCore.Start<!<DisposeAsync>d__26>',
'Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal!SocketConnection.<DisposeAsync>d__26.MoveNext',
'System.Runtime.CompilerServices!AsyncTaskMethodBuilder<System.Threading.Tasks!VoidTaskResult>.GetStateMachineBox<!<DisposeAsync>d__26>',
'System.Threading!UnmanagedThreadPoolWorkItem.System.Threading.IThreadPoolWorkItem.Execute',
'System.Threading!TimerQueue.FireNextTimers',
'System.Threading!TimerQueueTimer.Fire',
'Microsoft.Extensions.FileProviders.Physical!PhysicalFilesWatcher.RaiseChangeEvents',
'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize',
'System.Threading!ThreadPoolWorkQueue.WorkStealingQueueList.Remove',
],
},
{
name: 'self',
values: [
0, 0, 0, 0, 0, 0, 12, 4, 0, 0, 0, 0, 8, 0, 0, 17, 3, 12, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1,
0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 3, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 15, 2, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 8, 7, 0, 1, 3, 2, 1, 0, 1,
3, 5, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 4, 0, 5, 12, 1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0,
0, 0, 0, 0, 0, 0, 0, 10, 0, 1, 0, 3, 2, 0, 0, 0, 7, 7, 1, 1, 0, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
0, 0, 0, 0, 0, 0, 15, 14, 14, 4, 0, 0, 0, 0, 0, 0, 3, 9, 3, 0, 0, 0, 0, 2, 0, 0, 1, 2, 0, 0, 0, 6, 0, 0, 2, 3,
1, 0, 0, 0, 0, 0, 3, 0, 0, 3, 0, 0, 2, 5, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 5, 0, 3,
2, 0, 1, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 1,
],
},
{
name: 'value',
values: [
378, 12, 12, 12, 12, 12, 12, 365, 230, 230, 230, 230, 8, 206, 206, 206, 15, 12, 26, 139, 139, 139, 136, 136,
136, 42, 42, 3, 3, 2, 0, 1, 1, 1, 1, 1, 0, 4, 4, 2, 0, 1, 1, 1, 1, 1, 1, 9, 9, 7, 5, 4, 4, 0, 0, 0, 3, 3, 1, 2,
2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 22, 22, 22, 22, 22, 7, 7, 7, 7, 7, 7, 15, 42, 32, 3, 3, 3, 3, 14, 13, 13,
13, 5, 5, 0, 5, 4, 1, 1, 8, 11, 4, 1, 3, 4, 2, 1, 1, 8, 5, 29, 26, 25, 25, 25, 12, 12, 2, 2, 2, 2, 0, 1, 4, 5,
5, 12, 1, 3, 3, 3, 0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 10, 10, 1, 1, 3, 3, 2,
0, 0, 7, 7, 7, 1, 1, 6, 6, 6, 1, 1, 131, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 117, 97, 97, 97, 97, 97, 73, 32, 18,
4, 26, 20, 20, 20, 17, 17, 3, 14, 5, 2, 2, 2, 2, 2, 1, 1, 1, 2, 6, 6, 6, 6, 6, 5, 5, 3, 1, 18, 18, 18, 18, 18,
18, 3, 3, 3, 7, 2, 2, 5, 3, 0, 0, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 1, 20, 20, 2, 2, 1, 0, 8, 3, 3, 10, 6, 6, 5, 2,
2, 0, 0, 0, 0, 0, 10, 10, 7, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1,
],
},
{
name: 'selfRight',
values: [
0, 0, 0, 0, 0, 0, 16, 5, 0, 0, 0, 0, 1, 0, 0, 19, 2, 8, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1,
1, 0, 0, 1, 1, 0, 2, 0, 0, 2, 1, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 11, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 7, 3, 0, 3, 2, 2, 0, 1, 1,
4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 0, 4, 13, 0, 0, 0, 0, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
0, 0, 0, 0, 0, 1, 1, 4, 0, 5, 0, 1, 0, 0, 1, 0, 7, 9, 3, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 4, 12, 12, 5, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 0, 0, 0, 1, 0, 0, 4, 3, 0, 0, 0, 6, 0, 0, 1, 2, 0,
0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 6, 8, 0, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 1, 0, 0, 1, 3,
0, 2, 1, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
},
{
name: 'valueRight',
values: [
316, 16, 16, 16, 16, 16, 16, 300, 190, 190, 190, 190, 1, 173, 173, 173, 10, 8, 28, 108, 108, 108, 107, 107, 107,
34, 34, 4, 4, 2, 1, 1, 1, 1, 1, 1, 1, 7, 7, 4, 1, 2, 2, 2, 2, 2, 1, 9, 9, 9, 8, 8, 8, 3, 2, 2, 5, 5, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13, 13, 13, 13, 13, 2, 2, 2, 2, 2, 2, 11, 31, 24, 2, 2, 2, 2, 10, 10, 10, 10, 3,
3, 1, 2, 2, 0, 0, 7, 8, 5, 3, 2, 4, 2, 2, 1, 6, 2, 28, 24, 24, 24, 24, 11, 11, 1, 1, 1, 1, 1, 2, 3, 4, 4, 13, 0,
1, 1, 1, 1, 3, 3, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 5, 4, 5, 5, 1, 1, 0, 1, 1, 7, 7, 9, 3, 0,
4, 4, 2, 0, 0, 105, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100, 87, 87, 87, 87, 87, 58, 29, 17, 5, 25, 19, 19, 19, 12,
12, 2, 10, 1, 1, 1, 1, 1, 1, 4, 4, 4, 3, 6, 6, 6, 6, 3, 3, 3, 2, 0, 26, 26, 26, 26, 26, 26, 1, 1, 1, 14, 6, 6,
8, 4, 2, 2, 1, 1, 1, 5, 5, 5, 5, 2, 2, 2, 3, 10, 10, 1, 1, 1, 1, 1, 1, 1, 8, 3, 3, 1, 0, 2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
},
],
};

View File

@@ -23,7 +23,7 @@ import { useMeasure } from 'react-use';
import { Icon, useStyles2 } from '@grafana/ui'; import { Icon, useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL } from '../../constants'; import { PIXELS_PER_LEVEL } from '../../constants';
import { ClickedItemData, ColorScheme, TextAlign } from '../types'; import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
import FlameGraphContextMenu from './FlameGraphContextMenu'; import FlameGraphContextMenu from './FlameGraphContextMenu';
import FlameGraphMetadata from './FlameGraphMetadata'; import FlameGraphMetadata from './FlameGraphMetadata';
@@ -46,7 +46,7 @@ type Props = {
onSandwich: (label: string) => void; onSandwich: (label: string) => void;
onFocusPillClick: () => void; onFocusPillClick: () => void;
onSandwichPillClick: () => void; onSandwichPillClick: () => void;
colorScheme: ColorScheme; colorScheme: ColorScheme | ColorSchemeDiff;
}; };
const FlameGraph = ({ const FlameGraph = ({
@@ -67,18 +67,21 @@ const FlameGraph = ({
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [levels, totalTicks, callersCount] = useMemo(() => { const [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount] = useMemo(() => {
let levels = data.getLevels(); let levels = data.getLevels();
let totalTicks = levels.length ? levels[0][0].value : 0; let totalProfileTicks = levels.length ? levels[0][0].value : 0;
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
let callersCount = 0; let callersCount = 0;
let totalViewTicks = totalProfileTicks;
if (sandwichItem) { if (sandwichItem) {
const [callers, callees] = data.getSandwichLevels(sandwichItem); const [callers, callees] = data.getSandwichLevels(sandwichItem);
levels = [...callers, [], ...callees]; levels = [...callers, [], ...callees];
totalTicks = callees.length ? callees[0][0].value : 0; // We need this separate as in case of diff profile we to compute diff colors based on the original ticks.
totalViewTicks = callees[0]?.[0]?.value ?? 0;
callersCount = callers.length; callersCount = callers.length;
} }
return [levels, totalTicks, callersCount]; return [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount];
}, [data, sandwichItem]); }, [data, sandwichItem]);
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>(); const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
@@ -87,29 +90,32 @@ const FlameGraph = ({
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>(); const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
useFlameRender( useFlameRender({
graphRef, canvasRef: graphRef,
colorScheme,
data, data,
focusedItemData,
levels, levels,
wrapperWidth,
rangeMin,
rangeMax, rangeMax,
rangeMin,
search, search,
textAlign, textAlign,
totalTicks, totalViewTicks,
colorScheme, // We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
focusedItemData totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks,
); totalTicksRight: totalProfileTicksRight,
wrapperWidth,
});
const onGraphClick = useCallback( const onGraphClick = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement>) => { (e: ReactMouseEvent<HTMLCanvasElement>) => {
setTooltipItem(undefined); setTooltipItem(undefined);
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin); const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates( const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY }, { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
levels, levels,
pixelsPerTick, pixelsPerTick,
totalTicks, totalViewTicks,
rangeMin rangeMin
); );
@@ -128,7 +134,7 @@ const FlameGraph = ({
setClickedItemData(undefined); setClickedItemData(undefined);
} }
}, },
[data, rangeMin, rangeMax, totalTicks, levels] [data, rangeMin, rangeMax, totalViewTicks, levels]
); );
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>(); const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>();
@@ -137,12 +143,12 @@ const FlameGraph = ({
if (clickedItemData === undefined) { if (clickedItemData === undefined) {
setTooltipItem(undefined); setTooltipItem(undefined);
setMousePosition(undefined); setMousePosition(undefined);
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin); const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates( const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY }, { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
levels, levels,
pixelsPerTick, pixelsPerTick,
totalTicks, totalViewTicks,
rangeMin rangeMin
); );
@@ -152,7 +158,7 @@ const FlameGraph = ({
} }
} }
}, },
[rangeMin, rangeMax, totalTicks, clickedItemData, levels, setMousePosition] [rangeMin, rangeMax, totalViewTicks, clickedItemData, levels, setMousePosition]
); );
const onGraphMouseLeave = useCallback(() => { const onGraphMouseLeave = useCallback(() => {
@@ -177,7 +183,7 @@ const FlameGraph = ({
data={data} data={data}
focusedItem={focusedItemData} focusedItem={focusedItemData}
sandwichedLabel={sandwichItem} sandwichedLabel={sandwichItem}
totalTicks={totalTicks} totalTicks={totalViewTicks}
onFocusPillClick={onFocusPillClick} onFocusPillClick={onFocusPillClick}
onSandwichPillClick={onSandwichPillClick} onSandwichPillClick={onSandwichPillClick}
/> />
@@ -207,7 +213,7 @@ const FlameGraph = ({
/> />
</div> </div>
</div> </div>
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalTicks} /> <FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalViewTicks} />
{clickedItemData && ( {clickedItemData && (
<FlameGraphContextMenu <FlameGraphContextMenu
itemData={clickedItemData} itemData={clickedItemData}
@@ -215,8 +221,8 @@ const FlameGraph = ({
setClickedItemData(undefined); setClickedItemData(undefined);
}} }}
onItemFocus={() => { onItemFocus={() => {
setRangeMin(clickedItemData.item.start / totalTicks); setRangeMin(clickedItemData.item.start / totalViewTicks);
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalTicks); setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks);
onItemFocused(clickedItemData); onItemFocused(clickedItemData);
}} }}
onSandwich={() => { onSandwich={() => {

View File

@@ -46,7 +46,9 @@ const FlameGraphMetadata = React.memo(
<Icon size={'sm'} name={'angle-right'} /> <Icon size={'sm'} name={'angle-right'} />
<div className={styles.metadataPill}> <div className={styles.metadataPill}>
<Icon size={'sm'} name={'gf-show-context'} />{' '} <Icon size={'sm'} name={'gf-show-context'} />{' '}
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)} <span className={styles.metadataPillName}>
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
</span>
<IconButton <IconButton
className={styles.pillCloseButton} className={styles.pillCloseButton}
name={'times'} name={'times'}
@@ -89,7 +91,8 @@ FlameGraphMetadata.displayName = 'FlameGraphMetadata';
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
metadataPill: css` metadataPill: css`
label: metadataPill; label: metadataPill;
display: inline-block; display: inline-flex;
align-items: center;
background: ${theme.colors.background.secondary}; background: ${theme.colors.background.secondary};
border-radius: ${theme.shape.borderRadius(8)}; border-radius: ${theme.shape.borderRadius(8)};
padding: ${theme.spacing(0.5, 1)}; padding: ${theme.spacing(0.5, 1)};
@@ -108,6 +111,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
margin: 8px 0; margin: 8px 0;
text-align: center; text-align: center;
`, `,
metadataPillName: css`
label: metadataPillName;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: ${theme.spacing(0.5)};
`,
}); });
export default FlameGraphMetadata; export default FlameGraphMetadata;

View File

@@ -1,6 +1,6 @@
import { Field, FieldType, createDataFrame } from '@grafana/data'; import { Field, FieldType, createDataFrame } from '@grafana/data';
import { getTooltipData } from './FlameGraphTooltip'; import { getDiffTooltipData, getTooltipData } from './FlameGraphTooltip';
import { FlameGraphDataContainer } from './dataTransform'; import { FlameGraphDataContainer } from './dataTransform';
function setupData(unit?: string) { function setupData(unit?: string) {
@@ -15,6 +15,20 @@ function setupData(unit?: string) {
return new FlameGraphDataContainer(flameGraphData); return new FlameGraphDataContainer(flameGraphData);
} }
function setupDiffData() {
const flameGraphData = createDataFrame({
fields: [
{ name: 'level', values: [0, 1] },
{ name: 'value', values: [200, 90] },
{ name: 'valueRight', values: [100, 40] },
{ name: 'self', values: [110, 90] },
{ name: 'selfRight', values: [60, 40] },
{ name: 'label', values: ['total', 'func1'] },
],
});
return new FlameGraphDataContainer(flameGraphData);
}
describe('FlameGraphTooltip', () => { describe('FlameGraphTooltip', () => {
it('for bytes', () => { it('for bytes', () => {
const tooltipData = getTooltipData( const tooltipData = getTooltipData(
@@ -23,7 +37,6 @@ describe('FlameGraphTooltip', () => {
8_624_078_250 8_624_078_250
); );
expect(tooltipData).toEqual({ expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01, percentSelf: 0.01,
percentValue: 100, percentValue: 100,
unitTitle: 'RAM', unitTitle: 'RAM',
@@ -40,7 +53,6 @@ describe('FlameGraphTooltip', () => {
8_624_078_250 8_624_078_250
); );
expect(tooltipData).toEqual({ expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01, percentSelf: 0.01,
percentValue: 100, percentValue: 100,
unitSelf: '978250', unitSelf: '978250',
@@ -57,7 +69,6 @@ describe('FlameGraphTooltip', () => {
8_624_078_250 8_624_078_250
); );
expect(tooltipData).toEqual({ expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01, percentSelf: 0.01,
percentValue: 100, percentValue: 100,
unitTitle: 'Count', unitTitle: 'Count',
@@ -74,7 +85,6 @@ describe('FlameGraphTooltip', () => {
8_624_078_250 8_624_078_250
); );
expect(tooltipData).toEqual({ expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01, percentSelf: 0.01,
percentValue: 100, percentValue: 100,
unitTitle: 'Count', unitTitle: 'Count',
@@ -91,7 +101,6 @@ describe('FlameGraphTooltip', () => {
8_624_078_250 8_624_078_250
); );
expect(tooltipData).toEqual({ expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01, percentSelf: 0.01,
percentValue: 100, percentValue: 100,
unitTitle: 'Time', unitTitle: 'Time',
@@ -102,6 +111,39 @@ describe('FlameGraphTooltip', () => {
}); });
}); });
describe('getDiffTooltipData', () => {
it('works with diff data', () => {
const tooltipData = getDiffTooltipData(
setupDiffData(),
{ start: 0, itemIndexes: [1], value: 90, valueRight: 40, children: [] },
200
);
expect(tooltipData).toEqual([
{
rowId: '1',
label: '% of total',
baseline: '50%',
comparison: '40%',
diff: '-20%',
},
{
rowId: '2',
label: 'Value',
baseline: '50',
comparison: '40',
diff: '-10',
},
{
rowId: '3',
label: 'Samples',
baseline: '50',
comparison: '40',
diff: '-10',
},
]);
});
});
function makeField(name: string, unit: string, values: number[]): Field { function makeField(name: string, unit: string, values: number[]): Field {
return { return {
name, name,

View File

@@ -1,8 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { DisplayValue, getValueFormat, GrafanaTheme2 } from '@grafana/data';
import { Portal, useStyles2, VizTooltipContainer } from '@grafana/ui'; import { InteractiveTable, Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
import { FlameGraphDataContainer, LevelItem } from './dataTransform'; import { FlameGraphDataContainer, LevelItem } from './dataTransform';
@@ -20,10 +20,26 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
return null; return null;
} }
const tooltipData = getTooltipData(data, item, totalTicks); let content;
const content = (
<div className={styles.tooltipContent}> if (data.isDiffFlamegraph()) {
<p>{data.getLabel(item.itemIndexes[0])}</p> const tableData = getDiffTooltipData(data, item, totalTicks);
content = (
<InteractiveTable
className={styles.tooltipTable}
columns={[
{ id: 'label', header: '' },
{ id: 'baseline', header: 'Baseline' },
{ id: 'comparison', header: 'Comparison' },
{ id: 'diff', header: 'Diff' },
]}
data={tableData}
getRowId={(originalRow) => originalRow.rowId}
/>
);
} else {
const tooltipData = getTooltipData(data, item, totalTicks);
content = (
<p className={styles.lastParagraph}> <p className={styles.lastParagraph}>
{tooltipData.unitTitle} {tooltipData.unitTitle}
<br /> <br />
@@ -33,20 +49,22 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
<br /> <br />
Samples: <b>{tooltipData.samples}</b> Samples: <b>{tooltipData.samples}</b>
</p> </p>
</div> );
); }
return ( return (
<Portal> <Portal>
<VizTooltipContainer position={position} offset={{ x: 15, y: 0 }}> <VizTooltipContainer className={styles.tooltipContainer} position={position} offset={{ x: 15, y: 0 }}>
{content} <div className={styles.tooltipContent}>
<p className={styles.tooltipName}>{data.getLabel(item.itemIndexes[0])}</p>
{content}
</div>
</VizTooltipContainer> </VizTooltipContainer>
</Portal> </Portal>
); );
}; };
type TooltipData = { type TooltipData = {
name: string;
percentValue: number; percentValue: number;
percentSelf: number; percentSelf: number;
unitTitle: string; unitTitle: string;
@@ -77,7 +95,6 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
} }
return { return {
name: data.getLabel(item.itemIndexes[0]),
percentValue, percentValue,
percentSelf, percentSelf,
unitTitle, unitTitle,
@@ -87,10 +104,85 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
}; };
}; };
type DiffTableData = {
rowId: string;
label: string;
baseline: string | number;
comparison: string | number;
diff: string | number;
};
export const getDiffTooltipData = (
data: FlameGraphDataContainer,
item: LevelItem,
totalTicks: number
): DiffTableData[] => {
const levels = data.getLevels();
const totalTicksRight = levels[0][0].valueRight!;
const totalTicksLeft = totalTicks - totalTicksRight;
const valueLeft = item.value - item.valueRight!;
const percentageLeft = Math.round((10000 * valueLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * item.valueRight!) / totalTicksRight) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
const displayValueLeft = getValueWithUnit(data, data.valueDisplayProcessor(valueLeft));
const displayValueRight = getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight!));
const shortValFormat = getValueFormat('short');
return [
{
rowId: '1',
label: '% of total',
baseline: percentageLeft + '%',
comparison: percentageRight + '%',
diff: shortValFormat(diff).text + '%',
},
{
rowId: '2',
label: 'Value',
baseline: displayValueLeft,
comparison: displayValueRight,
diff: getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight! - valueLeft)),
},
{
rowId: '3',
label: 'Samples',
baseline: shortValFormat(valueLeft).text,
comparison: shortValFormat(item.valueRight!).text,
diff: shortValFormat(item.valueRight! - valueLeft).text,
},
];
};
function getValueWithUnit(data: FlameGraphDataContainer, displayValue: DisplayValue) {
let unitValue = displayValue.text + displayValue.suffix;
const unitTitle = data.getUnitTitle();
if (unitTitle === 'Count') {
if (!displayValue.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
unitValue = displayValue.text;
}
}
return unitValue;
}
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
tooltipContainer: css`
title: tooltipContainer;
overflow: hidden;
`,
tooltipContent: css` tooltipContent: css`
title: tooltipContent; title: tooltipContent;
font-size: ${theme.typography.bodySmall.fontSize}; font-size: ${theme.typography.bodySmall.fontSize};
width: 100%;
`,
tooltipName: css`
title: tooltipName;
word-break: break-all;
`, `,
lastParagraph: css` lastParagraph: css`
title: lastParagraph; title: lastParagraph;
@@ -100,6 +192,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
title: name; title: name;
margin-bottom: 10px; margin-bottom: 10px;
`, `,
tooltipTable: css`
title: tooltipTable;
max-width: 300px;
`,
}); });
export default FlameGraphTooltip; export default FlameGraphTooltip;

View File

@@ -1,7 +1,10 @@
import { scaleLinear } from 'd3';
import color from 'tinycolor2'; import color from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { ColorSchemeDiff } from '../types';
import murmurhash3_32_gc from './murmur3'; import murmurhash3_32_gc from './murmur3';
// Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly. // Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly.
@@ -61,6 +64,41 @@ export function getBarColorByPackage(label: string, theme: GrafanaTheme2) {
return packageColor; return packageColor;
} }
// green to red
const diffDefaultColors = ['rgb(0, 170, 0)', 'rgb(148, 142, 142)', 'rgb(200, 0, 0)'];
export const diffDefaultGradient = `linear-gradient(90deg, ${diffDefaultColors[0]} 0%, ${diffDefaultColors[1]} 50%, ${diffDefaultColors[2]} 100%)`;
const diffColorBlindColors = ['rgb(26, 133, 255)', 'rgb(148, 142, 142)', 'rgb(220, 50, 32)'];
export const diffColorBlindGradient = `linear-gradient(90deg, ${diffColorBlindColors[0]} 0%, ${diffColorBlindColors[1]} 50%, ${diffColorBlindColors[2]} 100%)`;
export function getBarColorByDiff(
ticks: number,
ticksRight: number,
totalTicks: number,
totalTicksRight: number,
colorScheme: ColorSchemeDiff
) {
const ticksLeft = ticks - ticksRight;
const totalTicksLeft = totalTicks - totalTicksRight;
const percentageLeft = Math.round((10000 * ticksLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * ticksRight) / totalTicksRight) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
const range = colorScheme === ColorSchemeDiff.Default ? diffDefaultColors : diffColorBlindColors;
const colorScale = scaleLinear()
.domain([-100, 0, 100])
// TODO types from DefinitelyTyped seem to mismatch
// @ts-ignore
.range(range);
// TODO types from DefinitelyTyped seem to mismatch
// @ts-ignore
const rgbString: string = colorScale(diff);
return color(rgbString);
}
// const getColors = memoizeOne((theme) => getFilteredColors(colors, theme)); // const getColors = memoizeOne((theme) => getFilteredColors(colors, theme));
// Different regexes to get the package name and function name from the label. We may at some point get an info about // Different regexes to get the package name and function name from the label. We may at some point get an info about

View File

@@ -1,4 +1,4 @@
import { createDataFrame, FieldType } from '@grafana/data'; import { createDataFrame, DataFrameDTO, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform'; import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
@@ -68,4 +68,138 @@ describe('nestedSetToLevels', () => {
expect(levels[0]).toEqual([n1]); expect(levels[0]).toEqual([n1]);
expect(levels[1]).toEqual([n2, n3, n4]); expect(levels[1]).toEqual([n2, n3, n4]);
}); });
it('handles diff data', () => {
const frame = createDataFrame({
fields: [
{ name: 'level', values: [0, 1, 1, 1] },
{ name: 'value', values: [10, 5, 3, 1] },
{ name: 'valueRight', values: [10, 4, 2, 1] },
{ name: 'label', values: ['1', '2', '3', '4'], type: FieldType.string },
{ name: 'self', values: [10, 5, 3, 1] },
{ name: 'selfRight', values: [10, 4, 2, 1] },
],
});
const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
expect(levels[1][0]).toMatchObject({ itemIndexes: [1], value: 9, valueRight: 4 });
expect(levels[1][1]).toMatchObject({ itemIndexes: [2], value: 5, valueRight: 2 });
expect(levels[1][2]).toMatchObject({ itemIndexes: [3], value: 2, valueRight: 1 });
});
}); });
describe('diffFlamebearerToDataFrameDTO', () => {
it('works', function () {
// The main point of this test is to have some easy way to convert flamebearer data to data frame, so it can be used
// for example in test data source. Reason is in grafana we don't have a way to produce diff frames and so we have
// to use pyro app which gives you flamebearer format. So if you need to create a diff data frame to save somewhere
// just log the frame and copy the values.
const levels = [
[0, 378, 0, 0, 316, 0, 0],
[0, 12, 0, 0, 16, 0, 1],
];
const names = ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'];
const frame = diffFlamebearerToDataFrameDTO(levels, names);
// console.log(JSON.stringify(frame));
expect(frame).toMatchObject({
name: 'response',
meta: { preferredVisualisationType: 'flamegraph' },
fields: [
{ name: 'level', values: [0, 1] },
{ name: 'label', values: ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'] },
{ name: 'self', values: [0, 0] },
{ name: 'value', values: [378, 12] },
{ name: 'selfRight', values: [0, 0] },
{ name: 'valueRight', values: [316, 16] },
],
});
});
});
function getNodes(level: number[], names: string[]) {
const nodes = [];
for (let i = 0; i < level.length; i += 7) {
nodes.push({
level: 0,
label: names[level[i + 6]],
self: level[i + 2],
val: level[i + 1],
selfRight: level[i + 5],
valRight: level[i + 4],
valTotal: level[i + 1] + level[i + 4],
offset: level[i],
offsetRight: level[i + 3],
offsetTotal: level[i] + level[i + 3],
children: [],
});
}
return nodes;
}
function diffFlamebearerToDataFrameDTO(levels: number[][], names: string[]) {
const nodeLevels: any[][] = [];
for (let i = 0; i < levels.length; i++) {
nodeLevels[i] = [];
for (const node of getNodes(levels[i], names)) {
node.level = i;
nodeLevels[i].push(node);
if (i > 0) {
const prevNodesInLevel = nodeLevels[i].slice(0, -1);
const currentNodeStart =
prevNodesInLevel.reduce((acc: number, n: any) => n.offsetTotal + n.valTotal + acc, 0) + node.offsetTotal;
const prevLevel = nodeLevels[i - 1];
let prevLevelOffset = 0;
for (const prevLevelNode of prevLevel) {
const parentNodeStart = prevLevelOffset + prevLevelNode.offsetTotal;
const parentNodeEnd = parentNodeStart + prevLevelNode.valTotal;
if (parentNodeStart <= currentNodeStart && parentNodeEnd > currentNodeStart) {
prevLevelNode.children.push(node);
break;
} else {
prevLevelOffset += prevLevelNode.offsetTotal + prevLevelNode.valTotal;
}
}
}
}
}
const root = nodeLevels[0][0];
const stack = [root];
const labelValues = [];
const levelValues = [];
const selfValues = [];
const valueValues = [];
const selfRightValues = [];
const valueRightValues = [];
while (stack.length) {
const node = stack.shift();
labelValues.push(node.label);
levelValues.push(node.level);
selfValues.push(node.self);
valueValues.push(node.val);
selfRightValues.push(node.selfRight);
valueRightValues.push(node.valRight);
stack.unshift(...node.children);
}
const frame: DataFrameDTO = {
name: 'response',
meta: { preferredVisualisationType: 'flamegraph' },
fields: [
{ name: 'level', values: levelValues },
{ name: 'label', values: labelValues, type: FieldType.string },
{ name: 'self', values: selfValues },
{ name: 'value', values: valueValues },
{ name: 'selfRight', values: selfRightValues },
{ name: 'valueRight', values: valueRightValues },
],
};
return frame;
}

View File

@@ -17,7 +17,10 @@ export type LevelItem = {
start: number; start: number;
// Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have // Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have
// to trim the value to correspond only to the part used by the children in the subtree. // to trim the value to correspond only to the part used by the children in the subtree.
// In case of diff profile this is actually left + right value.
value: number; value: number;
// Only exists for diff profiles.
valueRight?: number;
// Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single // Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single
// node. // node.
itemIndexes: number[]; itemIndexes: number[];
@@ -46,7 +49,10 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
// We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level. // We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level.
// So we have to compute the correct offset based on the last sibling. // So we have to compute the correct offset based on the last sibling.
const lastSibling = levels[currentLevel][levels[currentLevel].length - 1]; const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
offset = lastSibling.start + container.getValue(lastSibling.itemIndexes[0]); offset =
lastSibling.start +
container.getValue(lastSibling.itemIndexes[0]) +
container.getValueRight(lastSibling.itemIndexes[0]);
// we assume there is always a single root node so lastSibling should always have a parent. // we assume there is always a single root node so lastSibling should always have a parent.
// Also it has to have the same parent because of how the items are ordered. // Also it has to have the same parent because of how the items are ordered.
parent = lastSibling.parents![0]; parent = lastSibling.parents![0];
@@ -54,7 +60,8 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
const newItem: LevelItem = { const newItem: LevelItem = {
itemIndexes: [i], itemIndexes: [i],
value: container.getValue(i), value: container.getValue(i) + container.getValueRight(i),
valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : undefined,
start: offset, start: offset,
parents: parent && [parent], parents: parent && [parent],
children: [], children: [],
@@ -135,6 +142,10 @@ export class FlameGraphDataContainer {
valueField: Field; valueField: Field;
selfField: Field; selfField: Field;
// Optional fields for diff view
valueRightField?: Field;
selfRightField?: Field;
labelDisplayProcessor: DisplayProcessor; labelDisplayProcessor: DisplayProcessor;
valueDisplayProcessor: DisplayProcessor; valueDisplayProcessor: DisplayProcessor;
uniqueLabels: string[]; uniqueLabels: string[];
@@ -155,6 +166,15 @@ export class FlameGraphDataContainer {
this.valueField = data.fields.find((f) => f.name === 'value')!; this.valueField = data.fields.find((f) => f.name === 'value')!;
this.selfField = data.fields.find((f) => f.name === 'self')!; this.selfField = data.fields.find((f) => f.name === 'self')!;
this.valueRightField = data.fields.find((f) => f.name === 'valueRight')!;
this.selfRightField = data.fields.find((f) => f.name === 'selfRight')!;
if ((this.valueField || this.selfField) && !(this.valueField && this.selfField)) {
throw new Error(
'Malformed dataFrame: both valueRight and selfRight has to be present if one of them is present.'
);
}
const enumConfig = this.labelField?.config?.type?.enum; const enumConfig = this.labelField?.config?.type?.enum;
// Label can actually be an enum field so depending on that we have to access it through display processor. This is // Label can actually be an enum field so depending on that we have to access it through display processor. This is
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow // both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
@@ -176,6 +196,10 @@ export class FlameGraphDataContainer {
}); });
} }
isDiffFlamegraph() {
return this.valueRightField && this.selfRightField;
}
getLabel(index: number) { getLabel(index: number) {
return this.labelDisplayProcessor(this.labelField.values[index]).text; return this.labelDisplayProcessor(this.labelField.values[index]).text;
} }
@@ -185,21 +209,19 @@ export class FlameGraphDataContainer {
} }
getValue(index: number | number[]) { getValue(index: number | number[]) {
let indexArray: number[] = typeof index === 'number' ? [index] : index; return fieldAccessor(this.valueField, index);
return indexArray.reduce((acc, index) => {
return acc + this.valueField.values[index];
}, 0);
} }
getValueDisplay(index: number | number[]) { getValueRight(index: number | number[]) {
return this.valueDisplayProcessor(this.getValue(index)); return fieldAccessor(this.valueRightField, index);
} }
getSelf(index: number | number[]) { getSelf(index: number | number[]) {
let indexArray: number[] = typeof index === 'number' ? [index] : index; return fieldAccessor(this.selfField, index);
return indexArray.reduce((acc, index) => { }
return acc + this.selfField.values[index];
}, 0); getSelfRight(index: number | number[]) {
return fieldAccessor(this.selfRightField, index);
} }
getSelfDisplay(index: number | number[]) { getSelfDisplay(index: number | number[]) {
@@ -226,11 +248,11 @@ export class FlameGraphDataContainer {
return this.levels!; return this.levels!;
} }
getSandwichLevels(label: string) { getSandwichLevels(label: string): [LevelItem[][], LevelItem[][]] {
const nodes = this.getNodesWithLabel(label); const nodes = this.getNodesWithLabel(label);
if (!nodes?.length) { if (!nodes?.length) {
return []; return [[], []];
} }
const callers = mergeParentSubtrees(nodes, this); const callers = mergeParentSubtrees(nodes, this);
@@ -252,3 +274,15 @@ export class FlameGraphDataContainer {
} }
} }
} }
// Access field value with either single index or array of indexes. This is needed as we sometimes merge multiple
// into one, and we want to access aggregated values.
function fieldAccessor(field: Field | undefined, index: number | number[]) {
if (!field) {
return 0;
}
let indexArray: number[] = typeof index === 'number' ? [index] : index;
return indexArray.reduce((acc, index) => {
return acc + field.values[index];
}, 0);
}

View File

@@ -12,26 +12,53 @@ import {
LABEL_THRESHOLD, LABEL_THRESHOLD,
PIXELS_PER_LEVEL, PIXELS_PER_LEVEL,
} from '../../constants'; } from '../../constants';
import { ClickedItemData, ColorScheme, TextAlign } from '../types'; import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
import { getBarColorByPackage, getBarColorByValue } from './colors'; import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './colors';
import { FlameGraphDataContainer, LevelItem } from './dataTransform'; import { FlameGraphDataContainer, LevelItem } from './dataTransform';
const ufuzzy = new uFuzzy(); const ufuzzy = new uFuzzy();
export function useFlameRender( type RenderOptions = {
canvasRef: RefObject<HTMLCanvasElement>, canvasRef: RefObject<HTMLCanvasElement>;
data: FlameGraphDataContainer, data: FlameGraphDataContainer;
levels: LevelItem[][], levels: LevelItem[][];
wrapperWidth: number, wrapperWidth: number;
rangeMin: number,
rangeMax: number, // If we are rendering only zoomed in part of the graph.
search: string, rangeMin: number;
textAlign: TextAlign, rangeMax: number;
totalTicks: number,
colorScheme: ColorScheme, search: string;
focusedItemData?: ClickedItemData textAlign: TextAlign;
) {
// Total ticks that will be used for sizing
totalViewTicks: number;
// Total ticks that will be used for computing colors as some color scheme (like in diff view) should not be affected
// by sandwich or focus view.
totalColorTicks: number;
// Total ticks used to compute the diff colors
totalTicksRight: number | undefined;
colorScheme: ColorScheme | ColorSchemeDiff;
focusedItemData?: ClickedItemData;
};
export function useFlameRender(options: RenderOptions) {
const {
canvasRef,
data,
levels,
wrapperWidth,
rangeMin,
rangeMax,
search,
textAlign,
totalViewTicks,
totalColorTicks,
totalTicksRight,
colorScheme,
focusedItemData,
} = options;
const foundLabels = useMemo(() => { const foundLabels = useMemo(() => {
if (search) { if (search) {
const foundLabels = new Set<string>(); const foundLabels = new Set<string>();
@@ -57,20 +84,21 @@ export function useFlameRender(
return; return;
} }
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalTicks / (rangeMax - rangeMin); const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalViewTicks / (rangeMax - rangeMin);
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) { for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
const level = levels[levelIndex]; const level = levels[levelIndex];
// Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because // Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because
// sometimes we collapse multiple bars into single rect. // sometimes we collapse multiple bars into single rect.
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick); const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalViewTicks, rangeMin, pixelsPerTick);
for (const rect of dimensions) { for (const rect of dimensions) {
const focusedLevel = focusedItemData ? focusedItemData.level : 0; const focusedLevel = focusedItemData ? focusedItemData.level : 0;
// Render each rectangle based on the computed dimensions // Render each rectangle based on the computed dimensions
renderRect( renderRect(
ctx, ctx,
rect, rect,
totalTicks, totalColorTicks,
totalTicksRight,
rangeMin, rangeMin,
rangeMax, rangeMax,
levelIndex, levelIndex,
@@ -93,7 +121,9 @@ export function useFlameRender(
focusedItemData, focusedItemData,
foundLabels, foundLabels,
textAlign, textAlign,
totalTicks, totalViewTicks,
totalColorTicks,
totalTicksRight,
colorScheme, colorScheme,
theme, theme,
]); ]);
@@ -129,6 +159,7 @@ type RectData = {
y: number; y: number;
collapsed: boolean; collapsed: boolean;
ticks: number; ticks: number;
ticksRight?: number;
label: string; label: string;
unitLabel: string; unitLabel: string;
itemIndex: number; itemIndex: number;
@@ -176,6 +207,8 @@ export function getRectDimensionsForLevel(
y: levelIndex * PIXELS_PER_LEVEL, y: levelIndex * PIXELS_PER_LEVEL,
collapsed, collapsed,
ticks: curBarTicks, ticks: curBarTicks,
// When collapsed this does not make that much sense but then we don't really use it anyway.
ticksRight: item.valueRight,
label: data.getLabel(item.itemIndexes[0]), label: data.getLabel(item.itemIndexes[0]),
unitLabel: unit, unitLabel: unit,
itemIndex: item.itemIndexes[0], itemIndex: item.itemIndexes[0],
@@ -188,13 +221,14 @@ export function renderRect(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
rect: RectData, rect: RectData,
totalTicks: number, totalTicks: number,
totalTicksRight: number | undefined,
rangeMin: number, rangeMin: number,
rangeMax: number, rangeMax: number,
levelIndex: number, levelIndex: number,
topLevelIndex: number, topLevelIndex: number,
foundNames: Set<string> | undefined, foundNames: Set<string> | undefined,
textAlign: TextAlign, textAlign: TextAlign,
colorScheme: ColorScheme, colorScheme: ColorScheme | ColorSchemeDiff,
theme: GrafanaTheme2 theme: GrafanaTheme2
) { ) {
if (rect.width < HIDE_THRESHOLD) { if (rect.width < HIDE_THRESHOLD) {
@@ -205,7 +239,10 @@ export function renderRect(
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height); ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
const color = const color =
colorScheme === ColorScheme.ValueBased rect.ticksRight !== undefined &&
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
? getBarColorByDiff(rect.ticks, rect.ticksRight, totalTicks, totalTicksRight!, colorScheme)
: colorScheme === ColorScheme.ValueBased
? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax) ? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax)
: getBarColorByPackage(rect.label, theme); : getBarColorByPackage(rect.label, theme);

View File

@@ -51,6 +51,7 @@ function getParentSubtrees(roots: LevelItem[]) {
if (args.child) { if (args.child) {
newNode.value = args.child.value; newNode.value = args.child.value;
newNode.valueRight = args.child.valueRight;
args.child.parents = [newNode]; args.child.parents = [newNode];
} }
@@ -86,6 +87,14 @@ export function mergeSubtrees(
const newItem: LevelItem = { const newItem: LevelItem = {
// We use the items value instead of value from the data frame, cause we could have changed it in the process // We use the items value instead of value from the data frame, cause we could have changed it in the process
value: args.items.reduce((acc, i) => acc + i.value, 0), value: args.items.reduce((acc, i) => acc + i.value, 0),
// valueRight may not exist at all if this is not a diff profile
valueRight: args.items.reduce<number | undefined>((acc, i) => {
if (i.valueRight !== undefined) {
return (acc ?? 0) + i.valueRight;
} else {
return acc;
}
}, undefined),
itemIndexes: indexes, itemIndexes: indexes,
// these will change later // these will change later
children: [], children: [],

View File

@@ -12,7 +12,7 @@ import FlameGraph from './FlameGraph/FlameGraph';
import { FlameGraphDataContainer } from './FlameGraph/dataTransform'; import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
import FlameGraphHeader from './FlameGraphHeader'; import FlameGraphHeader from './FlameGraphHeader';
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'; import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
import { ClickedItemData, ColorScheme, SelectedView, TextAlign } from './types'; import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
type Props = { type Props = {
data?: DataFrame; data?: DataFrame;
@@ -30,7 +30,6 @@ const FlameGraphContainer = (props: Props) => {
const [textAlign, setTextAlign] = useState<TextAlign>('left'); const [textAlign, setTextAlign] = useState<TextAlign>('left');
// This is a label of the item because in sandwich view we group all items by label and present a merged graph // This is a label of the item because in sandwich view we group all items by label and present a merged graph
const [sandwichItem, setSandwichItem] = useState<string>(); const [sandwichItem, setSandwichItem] = useState<string>();
const [colorScheme, setColorScheme] = useState<ColorScheme>(ColorScheme.ValueBased);
const theme = useTheme2(); const theme = useTheme2();
@@ -41,6 +40,8 @@ const FlameGraphContainer = (props: Props) => {
return new FlameGraphDataContainer(props.data, theme); return new FlameGraphDataContainer(props.data, theme);
}, [props.data, theme]); }, [props.data, theme]);
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
// If user resizes window with both as the selected view // If user resizes window with both as the selected view
@@ -105,6 +106,7 @@ const FlameGraphContainer = (props: Props) => {
showResetButton={Boolean(focusedItemData || sandwichItem)} showResetButton={Boolean(focusedItemData || sandwichItem)}
colorScheme={colorScheme} colorScheme={colorScheme}
onColorSchemeChange={setColorScheme} onColorSchemeChange={setColorScheme}
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
/> />
<div className={styles.body}> <div className={styles.body}>
@@ -149,6 +151,29 @@ const FlameGraphContainer = (props: Props) => {
); );
}; };
function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
const [colorScheme, setColorScheme] = useState<ColorScheme | ColorSchemeDiff>(
dataContainer?.isDiffFlamegraph() ? ColorSchemeDiff.Default : ColorScheme.ValueBased
);
useEffect(() => {
if (
dataContainer?.isDiffFlamegraph() &&
(colorScheme === ColorScheme.ValueBased || colorScheme === ColorScheme.PackageBased)
) {
setColorScheme(ColorSchemeDiff.Default);
}
if (
!dataContainer?.isDiffFlamegraph() &&
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
) {
setColorScheme(ColorScheme.ValueBased);
}
}, [dataContainer, colorScheme]);
return [colorScheme, setColorScheme] as const;
}
function getStyles(theme: GrafanaTheme2) { function getStyles(theme: GrafanaTheme2) {
return { return {
container: css({ container: css({

View File

@@ -29,6 +29,7 @@ describe('FlameGraphHeader', () => {
showResetButton={true} showResetButton={true}
colorScheme={ColorScheme.ValueBased} colorScheme={ColorScheme.ValueBased}
onColorSchemeChange={onSchemeChange} onColorSchemeChange={onSchemeChange}
isDiffMode={false}
{...props} {...props}
/> />
); );
@@ -72,4 +73,14 @@ describe('FlameGraphHeader', () => {
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1); expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
}); });
it('shows diff color scheme switch when diff', async () => {
setup({ isDiffMode: true });
const changeButton = screen.getByLabelText(/Change color scheme/);
expect(changeButton).toBeInTheDocument();
await userEvent.click(changeButton);
expect(screen.getByText(/Default/)).toBeInTheDocument();
expect(screen.getByText(/Color blind/)).toBeInTheDocument();
});
}); });

View File

@@ -9,8 +9,8 @@ import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@gr
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import { byPackageGradient, byValueGradient } from './FlameGraph/colors'; import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
import { ColorScheme, SelectedView, TextAlign } from './types'; import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
type Props = { type Props = {
app: CoreApp; app: CoreApp;
@@ -23,8 +23,9 @@ type Props = {
textAlign: TextAlign; textAlign: TextAlign;
onTextAlignChange: (align: TextAlign) => void; onTextAlignChange: (align: TextAlign) => void;
showResetButton: boolean; showResetButton: boolean;
colorScheme: ColorScheme; colorScheme: ColorScheme | ColorSchemeDiff;
onColorSchemeChange: (colorScheme: ColorScheme) => void; onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
isDiffMode: boolean;
}; };
const FlameGraphHeader = ({ const FlameGraphHeader = ({
@@ -40,6 +41,7 @@ const FlameGraphHeader = ({
showResetButton, showResetButton,
colorScheme, colorScheme,
onColorSchemeChange, onColorSchemeChange,
isDiffMode,
}: Props) => { }: Props) => {
const styles = useStyles2((theme) => getStyles(theme, app)); const styles = useStyles2((theme) => getStyles(theme, app));
function interaction(name: string, context: Record<string, string | number>) { function interaction(name: string, context: Record<string, string | number>) {
@@ -97,7 +99,7 @@ const FlameGraphHeader = ({
aria-label={'Reset focus and sandwich state'} aria-label={'Reset focus and sandwich state'}
/> />
)} )}
<ColorSchemeButton app={app} value={colorScheme} onChange={onColorSchemeChange} /> <ColorSchemeButton app={app} value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
<RadioButtonGroup<TextAlign> <RadioButtonGroup<TextAlign>
size="sm" size="sm"
disabled={selectedView === SelectedView.TopTable} disabled={selectedView === SelectedView.TopTable}
@@ -125,17 +127,38 @@ const FlameGraphHeader = ({
type ColorSchemeButtonProps = { type ColorSchemeButtonProps = {
app: CoreApp; app: CoreApp;
value: ColorScheme; value: ColorScheme | ColorSchemeDiff;
onChange: (colorScheme: ColorScheme) => void; onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
isDiffMode: boolean;
}; };
function ColorSchemeButton(props: ColorSchemeButtonProps) { function ColorSchemeButton(props: ColorSchemeButtonProps) {
const styles = useStyles2((theme) => getStyles(theme, props.app)); const styles = useStyles2((theme) => getStyles(theme, props.app));
const menu = (
let menu = (
<Menu> <Menu>
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} /> <Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} /> <Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
</Menu> </Menu>
); );
if (props.isDiffMode) {
menu = (
<Menu>
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
</Menu>
);
}
// Show a bit different gradient as a way to indicate selected value
const colorDotStyle =
{
[ColorScheme.ValueBased]: styles.colorDotByValue,
[ColorScheme.PackageBased]: styles.colorDotByPackage,
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
}[props.value] || styles.colorDotByValue;
return ( return (
<Dropdown overlay={menu}> <Dropdown overlay={menu}>
<Button <Button
@@ -147,13 +170,7 @@ function ColorSchemeButton(props: ColorSchemeButtonProps) {
className={styles.buttonSpacing} className={styles.buttonSpacing}
aria-label={'Change color scheme'} aria-label={'Change color scheme'}
> >
<span <span className={cx(styles.colorDot, colorDotStyle)} />
className={cx(
styles.colorDot,
// Show a bit different gradient as a way to indicate selected value
props.value === ColorScheme.ValueBased ? styles.colorDotByValue : styles.colorDotByPackage
)}
/>
</Button> </Button>
</Dropdown> </Dropdown>
); );
@@ -264,6 +281,15 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
label: colorDotByPackage; label: colorDotByPackage;
background: ${byPackageGradient}; background: ${byPackageGradient};
`, `,
colorDotDiffDefault: css`
label: colorDotDiffDefault;
background: ${diffDefaultGradient};
`,
colorDotDiffColorBlind: css`
label: colorDotDiffColorBlind;
background: ${diffColorBlindGradient};
`,
}); });
export default FlameGraphHeader; export default FlameGraphHeader;

View File

@@ -85,11 +85,13 @@ function buildTableDataFrame(
let table: { [key: string]: TableData } = {}; let table: { [key: string]: TableData } = {};
for (let i = 0; i < data.data.length; i++) { for (let i = 0; i < data.data.length; i++) {
const value = data.getValue(i); const value = data.getValue(i);
const valueRight = data.getValueRight(i);
const self = data.getSelf(i); const self = data.getSelf(i);
const label = data.getLabel(i); const label = data.getLabel(i);
table[label] = table[label] || {}; table[label] = table[label] || {};
table[label].self = table[label].self ? table[label].self + self : self; table[label].self = table[label].self ? table[label].self + self : self;
table[label].total = table[label].total ? table[label].total + value : value; table[label].total = table[label].total ? table[label].total + value : value;
table[label].totalRight = table[label].totalRight ? table[label].totalRight + valueRight : valueRight;
} }
const actionField: Field = createActionField(onSandwich, onSearch, search, sandwichItem); const actionField: Field = createActionField(onSandwich, onSearch, search, sandwichItem);
@@ -114,18 +116,58 @@ function buildTableDataFrame(
}, },
}; };
const selfField = createNumberField('Self', data.selfField.config.unit); let frame;
const totalField = createNumberField('Total', data.valueField.config.unit);
for (let key in table) { if (data.isDiffFlamegraph()) {
actionField.values.push(null); symbolField.config.custom.width = width - actionColumnWidth - TOP_TABLE_COLUMN_WIDTH * 3;
symbolField.values.push(key);
selfField.values.push(table[key].self); const baselineField = createNumberField('Baseline', 'percent');
totalField.values.push(table[key].total); const comparisonField = createNumberField('Comparison', 'percent');
const diffField = createNumberField('Diff', 'percent');
// For this we don't really consider sandwich view even though you can switch it on.
const levels = data.getLevels();
const totalTicks = levels.length ? levels[0][0].value : 0;
const totalTicksRight = levels.length ? levels[0][0].valueRight : undefined;
for (let key in table) {
actionField.values.push(null);
symbolField.values.push(key);
const ticksLeft = table[key].total;
const ticksRight = table[key].totalRight;
// We are iterating over table of the data so totalTicksRight needs to be defined
const totalTicksLeft = totalTicks - totalTicksRight!;
const percentageLeft = Math.round((10000 * ticksLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * ticksRight) / totalTicksRight!) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
baselineField.values.push(percentageLeft);
comparisonField.values.push(percentageRight);
diffField.values.push(diff);
}
frame = {
fields: [actionField, symbolField, baselineField, comparisonField, diffField],
length: symbolField.values.length,
};
} else {
const selfField = createNumberField('Self', data.selfField.config.unit);
const totalField = createNumberField('Total', data.valueField.config.unit);
for (let key in table) {
actionField.values.push(null);
symbolField.values.push(key);
selfField.values.push(table[key].self);
totalField.values.push(table[key].total);
}
frame = { fields: [actionField, symbolField, selfField, totalField], length: symbolField.values.length };
} }
const frame = { fields: [actionField, symbolField, selfField, totalField], length: symbolField.values.length };
const dataFrames = applyFieldOverrides({ const dataFrames = applyFieldOverrides({
data: [frame], data: [frame],
fieldConfig: { fieldConfig: {

View File

@@ -29,6 +29,8 @@ export enum SelectedView {
export interface TableData { export interface TableData {
self: number; self: number;
total: number; total: number;
// For diff view
totalRight: number;
} }
export interface TopTableData { export interface TopTableData {
@@ -47,4 +49,9 @@ export enum ColorScheme {
PackageBased = 'packageBased', PackageBased = 'packageBased',
} }
export enum ColorSchemeDiff {
Default = 'default',
DiffColorBlind = 'diffColorBlind',
}
export type TextAlign = 'left' | 'right'; export type TextAlign = 'left' | 'right';