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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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, "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"]
],

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 |
| `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 | | |

View File

@ -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;

View File

@ -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"`

View File

@ -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} />
)}

View File

@ -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: {

View File

@ -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;

View File

@ -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> {

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 { 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={() => {

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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

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';
@ -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;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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: [],

View File

@ -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({

View File

@ -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();
});
});

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 { 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;

View File

@ -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: {

View File

@ -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';