mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Flamegraph: Diff profile support (#72383)
This commit is contained in:
parent
e0587dfb30
commit
91c7096eda
@ -4777,6 +4777,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[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": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -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 | null |
|
||||
| `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`. |
|
||||
| `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) |
|
||||
| `labels` | string | No | | |
|
||||
| `levelColumn` | boolean | No | | |
|
||||
|
@ -119,6 +119,7 @@ export interface TestDataDataQuery extends common.DataQuery {
|
||||
*/
|
||||
dropPercent?: number;
|
||||
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
||||
flamegraphDiff?: boolean;
|
||||
labels?: string;
|
||||
levelColumn?: boolean;
|
||||
lines?: number;
|
||||
|
@ -163,6 +163,7 @@ type TestDataDataQuery struct {
|
||||
// Drop percentage (the chance we will lose a point 0-100)
|
||||
DropPercent *float64 `json:"dropPercent,omitempty"`
|
||||
ErrorType *ErrorType `json:"errorType,omitempty"`
|
||||
FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"`
|
||||
Labels *string `json:"labels,omitempty"`
|
||||
LevelColumn *bool `json:"levelColumn,omitempty"`
|
||||
Lines *int64 `json:"lines,omitempty"`
|
||||
|
@ -344,6 +344,17 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
|
||||
</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 && (
|
||||
<PredictablePulseEditor onChange={onPulseWaveChange} query={query} ds={datasource} />
|
||||
)}
|
||||
|
@ -50,6 +50,8 @@ composableKinds: DataQuery: {
|
||||
// Drop percentage (the chance we will lose a point 0-100)
|
||||
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")
|
||||
|
||||
#StreamingQuery: {
|
||||
|
@ -116,6 +116,7 @@ export interface TestData extends common.DataQuery {
|
||||
*/
|
||||
dropPercent?: number;
|
||||
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
||||
flamegraphDiff?: boolean;
|
||||
labels?: string;
|
||||
levelColumn?: boolean;
|
||||
lines?: number;
|
||||
|
@ -24,7 +24,7 @@ import { Scenario, TestData, TestDataQueryType } from './dataquery.gen';
|
||||
import { queryMetricTree } from './metricTree';
|
||||
import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
|
||||
import { runStream } from './runStreams';
|
||||
import { flameGraphData } from './testData/flameGraphResponse';
|
||||
import { flameGraphData, flameGraphDataDiff } from './testData/flameGraphResponse';
|
||||
import { TestDataVariableSupport } from './variables';
|
||||
|
||||
export class TestDataDataSource extends DataSourceWithBackend<TestData> {
|
||||
@ -243,7 +243,8 @@ export class TestDataDataSource extends DataSourceWithBackend<TestData> {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@ -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,
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ import { useMeasure } from 'react-use';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||
import { ClickedItemData, ColorScheme, TextAlign } from '../types';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
|
||||
|
||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||
@ -46,7 +46,7 @@ type Props = {
|
||||
onSandwich: (label: string) => void;
|
||||
onFocusPillClick: () => void;
|
||||
onSandwichPillClick: () => void;
|
||||
colorScheme: ColorScheme;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
};
|
||||
|
||||
const FlameGraph = ({
|
||||
@ -67,18 +67,21 @@ const FlameGraph = ({
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [levels, totalTicks, callersCount] = useMemo(() => {
|
||||
const [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount] = useMemo(() => {
|
||||
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 totalViewTicks = totalProfileTicks;
|
||||
|
||||
if (sandwichItem) {
|
||||
const [callers, callees] = data.getSandwichLevels(sandwichItem);
|
||||
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;
|
||||
}
|
||||
return [levels, totalTicks, callersCount];
|
||||
return [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount];
|
||||
}, [data, sandwichItem]);
|
||||
|
||||
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
||||
@ -87,29 +90,32 @@ const FlameGraph = ({
|
||||
|
||||
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
|
||||
|
||||
useFlameRender(
|
||||
graphRef,
|
||||
useFlameRender({
|
||||
canvasRef: graphRef,
|
||||
colorScheme,
|
||||
data,
|
||||
focusedItemData,
|
||||
levels,
|
||||
wrapperWidth,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
rangeMin,
|
||||
search,
|
||||
textAlign,
|
||||
totalTicks,
|
||||
colorScheme,
|
||||
focusedItemData
|
||||
);
|
||||
totalViewTicks,
|
||||
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
|
||||
totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks,
|
||||
totalTicksRight: totalProfileTicksRight,
|
||||
wrapperWidth,
|
||||
});
|
||||
|
||||
const onGraphClick = useCallback(
|
||||
(e: ReactMouseEvent<HTMLCanvasElement>) => {
|
||||
setTooltipItem(undefined);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
||||
levels,
|
||||
pixelsPerTick,
|
||||
totalTicks,
|
||||
totalViewTicks,
|
||||
rangeMin
|
||||
);
|
||||
|
||||
@ -128,7 +134,7 @@ const FlameGraph = ({
|
||||
setClickedItemData(undefined);
|
||||
}
|
||||
},
|
||||
[data, rangeMin, rangeMax, totalTicks, levels]
|
||||
[data, rangeMin, rangeMax, totalViewTicks, levels]
|
||||
);
|
||||
|
||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>();
|
||||
@ -137,12 +143,12 @@ const FlameGraph = ({
|
||||
if (clickedItemData === undefined) {
|
||||
setTooltipItem(undefined);
|
||||
setMousePosition(undefined);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
||||
levels,
|
||||
pixelsPerTick,
|
||||
totalTicks,
|
||||
totalViewTicks,
|
||||
rangeMin
|
||||
);
|
||||
|
||||
@ -152,7 +158,7 @@ const FlameGraph = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[rangeMin, rangeMax, totalTicks, clickedItemData, levels, setMousePosition]
|
||||
[rangeMin, rangeMax, totalViewTicks, clickedItemData, levels, setMousePosition]
|
||||
);
|
||||
|
||||
const onGraphMouseLeave = useCallback(() => {
|
||||
@ -177,7 +183,7 @@ const FlameGraph = ({
|
||||
data={data}
|
||||
focusedItem={focusedItemData}
|
||||
sandwichedLabel={sandwichItem}
|
||||
totalTicks={totalTicks}
|
||||
totalTicks={totalViewTicks}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
/>
|
||||
@ -207,7 +213,7 @@ const FlameGraph = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalTicks} />
|
||||
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalViewTicks} />
|
||||
{clickedItemData && (
|
||||
<FlameGraphContextMenu
|
||||
itemData={clickedItemData}
|
||||
@ -215,8 +221,8 @@ const FlameGraph = ({
|
||||
setClickedItemData(undefined);
|
||||
}}
|
||||
onItemFocus={() => {
|
||||
setRangeMin(clickedItemData.item.start / totalTicks);
|
||||
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalTicks);
|
||||
setRangeMin(clickedItemData.item.start / totalViewTicks);
|
||||
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks);
|
||||
onItemFocused(clickedItemData);
|
||||
}}
|
||||
onSandwich={() => {
|
||||
|
@ -46,7 +46,9 @@ const FlameGraphMetadata = React.memo(
|
||||
<Icon size={'sm'} name={'angle-right'} />
|
||||
<div className={styles.metadataPill}>
|
||||
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
||||
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
||||
<span className={styles.metadataPillName}>
|
||||
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
||||
</span>
|
||||
<IconButton
|
||||
className={styles.pillCloseButton}
|
||||
name={'times'}
|
||||
@ -89,7 +91,8 @@ FlameGraphMetadata.displayName = 'FlameGraphMetadata';
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
metadataPill: css`
|
||||
label: metadataPill;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: ${theme.colors.background.secondary};
|
||||
border-radius: ${theme.shape.borderRadius(8)};
|
||||
padding: ${theme.spacing(0.5, 1)};
|
||||
@ -108,6 +111,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
margin: 8px 0;
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Field, FieldType, createDataFrame } from '@grafana/data';
|
||||
|
||||
import { getTooltipData } from './FlameGraphTooltip';
|
||||
import { getDiffTooltipData, getTooltipData } from './FlameGraphTooltip';
|
||||
import { FlameGraphDataContainer } from './dataTransform';
|
||||
|
||||
function setupData(unit?: string) {
|
||||
@ -15,6 +15,20 @@ function setupData(unit?: string) {
|
||||
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', () => {
|
||||
it('for bytes', () => {
|
||||
const tooltipData = getTooltipData(
|
||||
@ -23,7 +37,6 @@ describe('FlameGraphTooltip', () => {
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
percentValue: 100,
|
||||
unitTitle: 'RAM',
|
||||
@ -40,7 +53,6 @@ describe('FlameGraphTooltip', () => {
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
percentValue: 100,
|
||||
unitSelf: '978250',
|
||||
@ -57,7 +69,6 @@ describe('FlameGraphTooltip', () => {
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
percentValue: 100,
|
||||
unitTitle: 'Count',
|
||||
@ -74,7 +85,6 @@ describe('FlameGraphTooltip', () => {
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
percentValue: 100,
|
||||
unitTitle: 'Count',
|
||||
@ -91,7 +101,6 @@ describe('FlameGraphTooltip', () => {
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
percentValue: 100,
|
||||
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 {
|
||||
return {
|
||||
name,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
|
||||
import { DisplayValue, getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
||||
import { InteractiveTable, Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
|
||||
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
@ -20,10 +20,26 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipData = getTooltipData(data, item, totalTicks);
|
||||
const content = (
|
||||
<div className={styles.tooltipContent}>
|
||||
<p>{data.getLabel(item.itemIndexes[0])}</p>
|
||||
let content;
|
||||
|
||||
if (data.isDiffFlamegraph()) {
|
||||
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}>
|
||||
{tooltipData.unitTitle}
|
||||
<br />
|
||||
@ -33,20 +49,22 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
|
||||
<br />
|
||||
Samples: <b>{tooltipData.samples}</b>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<VizTooltipContainer position={position} offset={{ x: 15, y: 0 }}>
|
||||
{content}
|
||||
<VizTooltipContainer className={styles.tooltipContainer} position={position} offset={{ x: 15, y: 0 }}>
|
||||
<div className={styles.tooltipContent}>
|
||||
<p className={styles.tooltipName}>{data.getLabel(item.itemIndexes[0])}</p>
|
||||
{content}
|
||||
</div>
|
||||
</VizTooltipContainer>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type TooltipData = {
|
||||
name: string;
|
||||
percentValue: number;
|
||||
percentSelf: number;
|
||||
unitTitle: string;
|
||||
@ -77,7 +95,6 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
|
||||
}
|
||||
|
||||
return {
|
||||
name: data.getLabel(item.itemIndexes[0]),
|
||||
percentValue,
|
||||
percentSelf,
|
||||
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) => ({
|
||||
tooltipContainer: css`
|
||||
title: tooltipContainer;
|
||||
overflow: hidden;
|
||||
`,
|
||||
tooltipContent: css`
|
||||
title: tooltipContent;
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
width: 100%;
|
||||
`,
|
||||
tooltipName: css`
|
||||
title: tooltipName;
|
||||
word-break: break-all;
|
||||
`,
|
||||
lastParagraph: css`
|
||||
title: lastParagraph;
|
||||
@ -100,6 +192,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
title: name;
|
||||
margin-bottom: 10px;
|
||||
`,
|
||||
|
||||
tooltipTable: css`
|
||||
title: tooltipTable;
|
||||
max-width: 300px;
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphTooltip;
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { scaleLinear } from 'd3';
|
||||
import color from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { ColorSchemeDiff } from '../types';
|
||||
|
||||
import murmurhash3_32_gc from './murmur3';
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
// Different regexes to get the package name and function name from the label. We may at some point get an info about
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createDataFrame, FieldType } from '@grafana/data';
|
||||
import { createDataFrame, DataFrameDTO, FieldType } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
||||
|
||||
@ -68,4 +68,138 @@ describe('nestedSetToLevels', () => {
|
||||
expect(levels[0]).toEqual([n1]);
|
||||
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;
|
||||
}
|
||||
|
@ -17,7 +17,10 @@ export type LevelItem = {
|
||||
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
|
||||
// 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;
|
||||
// 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
|
||||
// node.
|
||||
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.
|
||||
// So we have to compute the correct offset based on the last sibling.
|
||||
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.
|
||||
// Also it has to have the same parent because of how the items are ordered.
|
||||
parent = lastSibling.parents![0];
|
||||
@ -54,7 +60,8 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
|
||||
|
||||
const newItem: LevelItem = {
|
||||
itemIndexes: [i],
|
||||
value: container.getValue(i),
|
||||
value: container.getValue(i) + container.getValueRight(i),
|
||||
valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : undefined,
|
||||
start: offset,
|
||||
parents: parent && [parent],
|
||||
children: [],
|
||||
@ -135,6 +142,10 @@ export class FlameGraphDataContainer {
|
||||
valueField: Field;
|
||||
selfField: Field;
|
||||
|
||||
// Optional fields for diff view
|
||||
valueRightField?: Field;
|
||||
selfRightField?: Field;
|
||||
|
||||
labelDisplayProcessor: DisplayProcessor;
|
||||
valueDisplayProcessor: DisplayProcessor;
|
||||
uniqueLabels: string[];
|
||||
@ -155,6 +166,15 @@ export class FlameGraphDataContainer {
|
||||
this.valueField = data.fields.find((f) => f.name === 'value')!;
|
||||
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;
|
||||
// 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
|
||||
@ -176,6 +196,10 @@ export class FlameGraphDataContainer {
|
||||
});
|
||||
}
|
||||
|
||||
isDiffFlamegraph() {
|
||||
return this.valueRightField && this.selfRightField;
|
||||
}
|
||||
|
||||
getLabel(index: number) {
|
||||
return this.labelDisplayProcessor(this.labelField.values[index]).text;
|
||||
}
|
||||
@ -185,21 +209,19 @@ export class FlameGraphDataContainer {
|
||||
}
|
||||
|
||||
getValue(index: number | number[]) {
|
||||
let indexArray: number[] = typeof index === 'number' ? [index] : index;
|
||||
return indexArray.reduce((acc, index) => {
|
||||
return acc + this.valueField.values[index];
|
||||
}, 0);
|
||||
return fieldAccessor(this.valueField, index);
|
||||
}
|
||||
|
||||
getValueDisplay(index: number | number[]) {
|
||||
return this.valueDisplayProcessor(this.getValue(index));
|
||||
getValueRight(index: number | number[]) {
|
||||
return fieldAccessor(this.valueRightField, index);
|
||||
}
|
||||
|
||||
getSelf(index: number | number[]) {
|
||||
let indexArray: number[] = typeof index === 'number' ? [index] : index;
|
||||
return indexArray.reduce((acc, index) => {
|
||||
return acc + this.selfField.values[index];
|
||||
}, 0);
|
||||
return fieldAccessor(this.selfField, index);
|
||||
}
|
||||
|
||||
getSelfRight(index: number | number[]) {
|
||||
return fieldAccessor(this.selfRightField, index);
|
||||
}
|
||||
|
||||
getSelfDisplay(index: number | number[]) {
|
||||
@ -226,11 +248,11 @@ export class FlameGraphDataContainer {
|
||||
return this.levels!;
|
||||
}
|
||||
|
||||
getSandwichLevels(label: string) {
|
||||
getSandwichLevels(label: string): [LevelItem[][], LevelItem[][]] {
|
||||
const nodes = this.getNodesWithLabel(label);
|
||||
|
||||
if (!nodes?.length) {
|
||||
return [];
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -12,26 +12,53 @@ import {
|
||||
LABEL_THRESHOLD,
|
||||
PIXELS_PER_LEVEL,
|
||||
} 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';
|
||||
|
||||
const ufuzzy = new uFuzzy();
|
||||
|
||||
export function useFlameRender(
|
||||
canvasRef: RefObject<HTMLCanvasElement>,
|
||||
data: FlameGraphDataContainer,
|
||||
levels: LevelItem[][],
|
||||
wrapperWidth: number,
|
||||
rangeMin: number,
|
||||
rangeMax: number,
|
||||
search: string,
|
||||
textAlign: TextAlign,
|
||||
totalTicks: number,
|
||||
colorScheme: ColorScheme,
|
||||
focusedItemData?: ClickedItemData
|
||||
) {
|
||||
type RenderOptions = {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
data: FlameGraphDataContainer;
|
||||
levels: LevelItem[][];
|
||||
wrapperWidth: number;
|
||||
|
||||
// If we are rendering only zoomed in part of the graph.
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
|
||||
search: string;
|
||||
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(() => {
|
||||
if (search) {
|
||||
const foundLabels = new Set<string>();
|
||||
@ -57,20 +84,21 @@ export function useFlameRender(
|
||||
return;
|
||||
}
|
||||
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++) {
|
||||
const level = levels[levelIndex];
|
||||
// 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.
|
||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalViewTicks, rangeMin, pixelsPerTick);
|
||||
for (const rect of dimensions) {
|
||||
const focusedLevel = focusedItemData ? focusedItemData.level : 0;
|
||||
// Render each rectangle based on the computed dimensions
|
||||
renderRect(
|
||||
ctx,
|
||||
rect,
|
||||
totalTicks,
|
||||
totalColorTicks,
|
||||
totalTicksRight,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
levelIndex,
|
||||
@ -93,7 +121,9 @@ export function useFlameRender(
|
||||
focusedItemData,
|
||||
foundLabels,
|
||||
textAlign,
|
||||
totalTicks,
|
||||
totalViewTicks,
|
||||
totalColorTicks,
|
||||
totalTicksRight,
|
||||
colorScheme,
|
||||
theme,
|
||||
]);
|
||||
@ -129,6 +159,7 @@ type RectData = {
|
||||
y: number;
|
||||
collapsed: boolean;
|
||||
ticks: number;
|
||||
ticksRight?: number;
|
||||
label: string;
|
||||
unitLabel: string;
|
||||
itemIndex: number;
|
||||
@ -176,6 +207,8 @@ export function getRectDimensionsForLevel(
|
||||
y: levelIndex * PIXELS_PER_LEVEL,
|
||||
collapsed,
|
||||
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]),
|
||||
unitLabel: unit,
|
||||
itemIndex: item.itemIndexes[0],
|
||||
@ -188,13 +221,14 @@ export function renderRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
rect: RectData,
|
||||
totalTicks: number,
|
||||
totalTicksRight: number | undefined,
|
||||
rangeMin: number,
|
||||
rangeMax: number,
|
||||
levelIndex: number,
|
||||
topLevelIndex: number,
|
||||
foundNames: Set<string> | undefined,
|
||||
textAlign: TextAlign,
|
||||
colorScheme: ColorScheme,
|
||||
colorScheme: ColorScheme | ColorSchemeDiff,
|
||||
theme: GrafanaTheme2
|
||||
) {
|
||||
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);
|
||||
|
||||
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)
|
||||
: getBarColorByPackage(rect.label, theme);
|
||||
|
||||
|
@ -51,6 +51,7 @@ function getParentSubtrees(roots: LevelItem[]) {
|
||||
|
||||
if (args.child) {
|
||||
newNode.value = args.child.value;
|
||||
newNode.valueRight = args.child.valueRight;
|
||||
args.child.parents = [newNode];
|
||||
}
|
||||
|
||||
@ -86,6 +87,14 @@ export function mergeSubtrees(
|
||||
const newItem: LevelItem = {
|
||||
// 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),
|
||||
// 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,
|
||||
// these will change later
|
||||
children: [],
|
||||
|
@ -12,7 +12,7 @@ import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { ClickedItemData, ColorScheme, SelectedView, TextAlign } from './types';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
|
||||
type Props = {
|
||||
data?: DataFrame;
|
||||
@ -30,7 +30,6 @@ const FlameGraphContainer = (props: Props) => {
|
||||
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
|
||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(ColorScheme.ValueBased);
|
||||
|
||||
const theme = useTheme2();
|
||||
|
||||
@ -41,6 +40,8 @@ const FlameGraphContainer = (props: Props) => {
|
||||
return new FlameGraphDataContainer(props.data, theme);
|
||||
}, [props.data, theme]);
|
||||
|
||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// If user resizes window with both as the selected view
|
||||
@ -105,6 +106,7 @@ const FlameGraphContainer = (props: Props) => {
|
||||
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
||||
colorScheme={colorScheme}
|
||||
onColorSchemeChange={setColorScheme}
|
||||
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
|
||||
/>
|
||||
|
||||
<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) {
|
||||
return {
|
||||
container: css({
|
||||
|
@ -29,6 +29,7 @@ describe('FlameGraphHeader', () => {
|
||||
showResetButton={true}
|
||||
colorScheme={ColorScheme.ValueBased}
|
||||
onColorSchemeChange={onSchemeChange}
|
||||
isDiffMode={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -72,4 +73,14 @@ describe('FlameGraphHeader', () => {
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -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 { byPackageGradient, byValueGradient } from './FlameGraph/colors';
|
||||
import { ColorScheme, SelectedView, TextAlign } from './types';
|
||||
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
||||
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
|
||||
type Props = {
|
||||
app: CoreApp;
|
||||
@ -23,8 +23,9 @@ type Props = {
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
showResetButton: boolean;
|
||||
colorScheme: ColorScheme;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme) => void;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
|
||||
const FlameGraphHeader = ({
|
||||
@ -40,6 +41,7 @@ const FlameGraphHeader = ({
|
||||
showResetButton,
|
||||
colorScheme,
|
||||
onColorSchemeChange,
|
||||
isDiffMode,
|
||||
}: Props) => {
|
||||
const styles = useStyles2((theme) => getStyles(theme, app));
|
||||
function interaction(name: string, context: Record<string, string | number>) {
|
||||
@ -97,7 +99,7 @@ const FlameGraphHeader = ({
|
||||
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>
|
||||
size="sm"
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
@ -125,17 +127,38 @@ const FlameGraphHeader = ({
|
||||
|
||||
type ColorSchemeButtonProps = {
|
||||
app: CoreApp;
|
||||
value: ColorScheme;
|
||||
onChange: (colorScheme: ColorScheme) => void;
|
||||
value: ColorScheme | ColorSchemeDiff;
|
||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
const styles = useStyles2((theme) => getStyles(theme, props.app));
|
||||
const menu = (
|
||||
|
||||
let menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||
</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 (
|
||||
<Dropdown overlay={menu}>
|
||||
<Button
|
||||
@ -147,13 +170,7 @@ function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
className={styles.buttonSpacing}
|
||||
aria-label={'Change color scheme'}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
styles.colorDot,
|
||||
// Show a bit different gradient as a way to indicate selected value
|
||||
props.value === ColorScheme.ValueBased ? styles.colorDotByValue : styles.colorDotByPackage
|
||||
)}
|
||||
/>
|
||||
<span className={cx(styles.colorDot, colorDotStyle)} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
@ -264,6 +281,15 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
|
||||
label: colorDotByPackage;
|
||||
background: ${byPackageGradient};
|
||||
`,
|
||||
colorDotDiffDefault: css`
|
||||
label: colorDotDiffDefault;
|
||||
background: ${diffDefaultGradient};
|
||||
`,
|
||||
|
||||
colorDotDiffColorBlind: css`
|
||||
label: colorDotDiffColorBlind;
|
||||
background: ${diffColorBlindGradient};
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphHeader;
|
||||
|
@ -85,11 +85,13 @@ function buildTableDataFrame(
|
||||
let table: { [key: string]: TableData } = {};
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const value = data.getValue(i);
|
||||
const valueRight = data.getValueRight(i);
|
||||
const self = data.getSelf(i);
|
||||
const label = data.getLabel(i);
|
||||
table[label] = table[label] || {};
|
||||
table[label].self = table[label].self ? table[label].self + self : self;
|
||||
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);
|
||||
@ -114,18 +116,58 @@ function buildTableDataFrame(
|
||||
},
|
||||
};
|
||||
|
||||
const selfField = createNumberField('Self', data.selfField.config.unit);
|
||||
const totalField = createNumberField('Total', data.valueField.config.unit);
|
||||
let frame;
|
||||
|
||||
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);
|
||||
if (data.isDiffFlamegraph()) {
|
||||
symbolField.config.custom.width = width - actionColumnWidth - TOP_TABLE_COLUMN_WIDTH * 3;
|
||||
|
||||
const baselineField = createNumberField('Baseline', 'percent');
|
||||
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({
|
||||
data: [frame],
|
||||
fieldConfig: {
|
||||
|
@ -29,6 +29,8 @@ export enum SelectedView {
|
||||
export interface TableData {
|
||||
self: number;
|
||||
total: number;
|
||||
// For diff view
|
||||
totalRight: number;
|
||||
}
|
||||
|
||||
export interface TopTableData {
|
||||
@ -47,4 +49,9 @@ export enum ColorScheme {
|
||||
PackageBased = 'packageBased',
|
||||
}
|
||||
|
||||
export enum ColorSchemeDiff {
|
||||
Default = 'default',
|
||||
DiffColorBlind = 'diffColorBlind',
|
||||
}
|
||||
|
||||
export type TextAlign = 'left' | 'right';
|
||||
|
Loading…
Reference in New Issue
Block a user