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:
@@ -4777,6 +4777,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||||
],
|
],
|
||||||
|
"public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts:5381": [
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
|
],
|
||||||
"public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx:5381": [
|
"public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 |
|
| `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) |
|
| `dropPercent` | number | No | | Drop percentage (the chance we will lose a point 0-100) |
|
||||||
| `errorType` | string | No | | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. |
|
| `errorType` | string | No | | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. |
|
||||||
|
| `flamegraphDiff` | boolean | No | | |
|
||||||
| `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)<br/>Note this does not always imply that the query should not be executed since<br/>the results from a hidden query may be used as the input to other queries (SSE etc) |
|
| `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)<br/>Note this does not always imply that the query should not be executed since<br/>the results from a hidden query may be used as the input to other queries (SSE etc) |
|
||||||
| `labels` | string | No | | |
|
| `labels` | string | No | | |
|
||||||
| `levelColumn` | boolean | No | | |
|
| `levelColumn` | boolean | No | | |
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export interface TestDataDataQuery extends common.DataQuery {
|
|||||||
*/
|
*/
|
||||||
dropPercent?: number;
|
dropPercent?: number;
|
||||||
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
||||||
|
flamegraphDiff?: boolean;
|
||||||
labels?: string;
|
labels?: string;
|
||||||
levelColumn?: boolean;
|
levelColumn?: boolean;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ type TestDataDataQuery struct {
|
|||||||
// Drop percentage (the chance we will lose a point 0-100)
|
// Drop percentage (the chance we will lose a point 0-100)
|
||||||
DropPercent *float64 `json:"dropPercent,omitempty"`
|
DropPercent *float64 `json:"dropPercent,omitempty"`
|
||||||
ErrorType *ErrorType `json:"errorType,omitempty"`
|
ErrorType *ErrorType `json:"errorType,omitempty"`
|
||||||
|
FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"`
|
||||||
Labels *string `json:"labels,omitempty"`
|
Labels *string `json:"labels,omitempty"`
|
||||||
LevelColumn *bool `json:"levelColumn,omitempty"`
|
LevelColumn *bool `json:"levelColumn,omitempty"`
|
||||||
Lines *int64 `json:"lines,omitempty"`
|
Lines *int64 `json:"lines,omitempty"`
|
||||||
|
|||||||
@@ -344,6 +344,17 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{scenarioId === TestDataQueryType.FlameGraph && (
|
||||||
|
<InlineField label={'Diff profile'} grow>
|
||||||
|
<InlineSwitch
|
||||||
|
value={Boolean(query.flamegraphDiff)}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdate({ ...query, flamegraphDiff: e.currentTarget.checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
)}
|
||||||
|
|
||||||
{scenarioId === TestDataQueryType.PredictablePulse && (
|
{scenarioId === TestDataQueryType.PredictablePulse && (
|
||||||
<PredictablePulseEditor onChange={onPulseWaveChange} query={query} ds={datasource} />
|
<PredictablePulseEditor onChange={onPulseWaveChange} query={query} ds={datasource} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ composableKinds: DataQuery: {
|
|||||||
// Drop percentage (the chance we will lose a point 0-100)
|
// Drop percentage (the chance we will lose a point 0-100)
|
||||||
dropPercent?: float64
|
dropPercent?: float64
|
||||||
|
|
||||||
|
flamegraphDiff?: bool
|
||||||
|
|
||||||
#TestDataQueryType: "random_walk" | "slow_query" | "random_walk_with_error" | "random_walk_table" | "exponential_heatmap_bucket_data" | "linear_heatmap_bucket_data" | "no_data_points" | "datapoints_outside_range" | "csv_metric_values" | "predictable_pulse" | "predictable_csv_wave" | "streaming_client" | "simulation" | "usa" | "live" | "grafana_api" | "arrow" | "annotations" | "table_static" | "server_error_500" | "logs" | "node_graph" | "flame_graph" | "raw_frame" | "csv_file" | "csv_content" | "trace" | "manual_entry" | "variables-query" @cuetsy(kind="enum", memberNames="RandomWalk|SlowQuery|RandomWalkWithError|RandomWalkTable|ExponentialHeatmapBucketData|LinearHeatmapBucketData|NoDataPoints|DataPointsOutsideRange|CSVMetricValues|PredictablePulse|PredictableCSVWave|StreamingClient|Simulation|USA|Live|GrafanaAPI|Arrow|Annotations|TableStatic|ServerError500|Logs|NodeGraph|FlameGraph|RawFrame|CSVFile|CSVContent|Trace|ManualEntry|VariablesQuery")
|
#TestDataQueryType: "random_walk" | "slow_query" | "random_walk_with_error" | "random_walk_table" | "exponential_heatmap_bucket_data" | "linear_heatmap_bucket_data" | "no_data_points" | "datapoints_outside_range" | "csv_metric_values" | "predictable_pulse" | "predictable_csv_wave" | "streaming_client" | "simulation" | "usa" | "live" | "grafana_api" | "arrow" | "annotations" | "table_static" | "server_error_500" | "logs" | "node_graph" | "flame_graph" | "raw_frame" | "csv_file" | "csv_content" | "trace" | "manual_entry" | "variables-query" @cuetsy(kind="enum", memberNames="RandomWalk|SlowQuery|RandomWalkWithError|RandomWalkTable|ExponentialHeatmapBucketData|LinearHeatmapBucketData|NoDataPoints|DataPointsOutsideRange|CSVMetricValues|PredictablePulse|PredictableCSVWave|StreamingClient|Simulation|USA|Live|GrafanaAPI|Arrow|Annotations|TableStatic|ServerError500|Logs|NodeGraph|FlameGraph|RawFrame|CSVFile|CSVContent|Trace|ManualEntry|VariablesQuery")
|
||||||
|
|
||||||
#StreamingQuery: {
|
#StreamingQuery: {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export interface TestData extends common.DataQuery {
|
|||||||
*/
|
*/
|
||||||
dropPercent?: number;
|
dropPercent?: number;
|
||||||
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
errorType?: ('server_panic' | 'frontend_exception' | 'frontend_observable');
|
||||||
|
flamegraphDiff?: boolean;
|
||||||
labels?: string;
|
labels?: string;
|
||||||
levelColumn?: boolean;
|
levelColumn?: boolean;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { Scenario, TestData, TestDataQueryType } from './dataquery.gen';
|
|||||||
import { queryMetricTree } from './metricTree';
|
import { queryMetricTree } from './metricTree';
|
||||||
import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
|
import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
|
||||||
import { runStream } from './runStreams';
|
import { runStream } from './runStreams';
|
||||||
import { flameGraphData } from './testData/flameGraphResponse';
|
import { flameGraphData, flameGraphDataDiff } from './testData/flameGraphResponse';
|
||||||
import { TestDataVariableSupport } from './variables';
|
import { TestDataVariableSupport } from './variables';
|
||||||
|
|
||||||
export class TestDataDataSource extends DataSourceWithBackend<TestData> {
|
export class TestDataDataSource extends DataSourceWithBackend<TestData> {
|
||||||
@@ -243,7 +243,8 @@ export class TestDataDataSource extends DataSourceWithBackend<TestData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flameGraphQuery(target: TestData): Observable<DataQueryResponse> {
|
flameGraphQuery(target: TestData): Observable<DataQueryResponse> {
|
||||||
return of({ data: [{ ...flameGraphData, refId: target.refId }] }).pipe(delay(100));
|
const data = target.flamegraphDiff ? flameGraphDataDiff : flameGraphData;
|
||||||
|
return of({ data: [{ ...data, refId: target.refId }] }).pipe(delay(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
trace(target: TestData, options: DataQueryRequest<TestData>): Observable<DataQueryResponse> {
|
trace(target: TestData, options: DataQueryRequest<TestData>): Observable<DataQueryResponse> {
|
||||||
|
|||||||
@@ -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 { Icon, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||||
import { ClickedItemData, ColorScheme, TextAlign } from '../types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
|
||||||
|
|
||||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||||
@@ -46,7 +46,7 @@ type Props = {
|
|||||||
onSandwich: (label: string) => void;
|
onSandwich: (label: string) => void;
|
||||||
onFocusPillClick: () => void;
|
onFocusPillClick: () => void;
|
||||||
onSandwichPillClick: () => void;
|
onSandwichPillClick: () => void;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraph = ({
|
const FlameGraph = ({
|
||||||
@@ -67,18 +67,21 @@ const FlameGraph = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const [levels, totalTicks, callersCount] = useMemo(() => {
|
const [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount] = useMemo(() => {
|
||||||
let levels = data.getLevels();
|
let levels = data.getLevels();
|
||||||
let totalTicks = levels.length ? levels[0][0].value : 0;
|
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
|
||||||
|
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
|
||||||
let callersCount = 0;
|
let callersCount = 0;
|
||||||
|
let totalViewTicks = totalProfileTicks;
|
||||||
|
|
||||||
if (sandwichItem) {
|
if (sandwichItem) {
|
||||||
const [callers, callees] = data.getSandwichLevels(sandwichItem);
|
const [callers, callees] = data.getSandwichLevels(sandwichItem);
|
||||||
levels = [...callers, [], ...callees];
|
levels = [...callers, [], ...callees];
|
||||||
totalTicks = callees.length ? callees[0][0].value : 0;
|
// We need this separate as in case of diff profile we to compute diff colors based on the original ticks.
|
||||||
|
totalViewTicks = callees[0]?.[0]?.value ?? 0;
|
||||||
callersCount = callers.length;
|
callersCount = callers.length;
|
||||||
}
|
}
|
||||||
return [levels, totalTicks, callersCount];
|
return [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount];
|
||||||
}, [data, sandwichItem]);
|
}, [data, sandwichItem]);
|
||||||
|
|
||||||
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
||||||
@@ -87,29 +90,32 @@ const FlameGraph = ({
|
|||||||
|
|
||||||
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
|
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
|
||||||
|
|
||||||
useFlameRender(
|
useFlameRender({
|
||||||
graphRef,
|
canvasRef: graphRef,
|
||||||
|
colorScheme,
|
||||||
data,
|
data,
|
||||||
|
focusedItemData,
|
||||||
levels,
|
levels,
|
||||||
wrapperWidth,
|
|
||||||
rangeMin,
|
|
||||||
rangeMax,
|
rangeMax,
|
||||||
|
rangeMin,
|
||||||
search,
|
search,
|
||||||
textAlign,
|
textAlign,
|
||||||
totalTicks,
|
totalViewTicks,
|
||||||
colorScheme,
|
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
|
||||||
focusedItemData
|
totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks,
|
||||||
);
|
totalTicksRight: totalProfileTicksRight,
|
||||||
|
wrapperWidth,
|
||||||
|
});
|
||||||
|
|
||||||
const onGraphClick = useCallback(
|
const onGraphClick = useCallback(
|
||||||
(e: ReactMouseEvent<HTMLCanvasElement>) => {
|
(e: ReactMouseEvent<HTMLCanvasElement>) => {
|
||||||
setTooltipItem(undefined);
|
setTooltipItem(undefined);
|
||||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
|
||||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||||
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
||||||
levels,
|
levels,
|
||||||
pixelsPerTick,
|
pixelsPerTick,
|
||||||
totalTicks,
|
totalViewTicks,
|
||||||
rangeMin
|
rangeMin
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -128,7 +134,7 @@ const FlameGraph = ({
|
|||||||
setClickedItemData(undefined);
|
setClickedItemData(undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data, rangeMin, rangeMax, totalTicks, levels]
|
[data, rangeMin, rangeMax, totalViewTicks, levels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>();
|
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>();
|
||||||
@@ -137,12 +143,12 @@ const FlameGraph = ({
|
|||||||
if (clickedItemData === undefined) {
|
if (clickedItemData === undefined) {
|
||||||
setTooltipItem(undefined);
|
setTooltipItem(undefined);
|
||||||
setMousePosition(undefined);
|
setMousePosition(undefined);
|
||||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
|
||||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||||
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
|
||||||
levels,
|
levels,
|
||||||
pixelsPerTick,
|
pixelsPerTick,
|
||||||
totalTicks,
|
totalViewTicks,
|
||||||
rangeMin
|
rangeMin
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -152,7 +158,7 @@ const FlameGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rangeMin, rangeMax, totalTicks, clickedItemData, levels, setMousePosition]
|
[rangeMin, rangeMax, totalViewTicks, clickedItemData, levels, setMousePosition]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onGraphMouseLeave = useCallback(() => {
|
const onGraphMouseLeave = useCallback(() => {
|
||||||
@@ -177,7 +183,7 @@ const FlameGraph = ({
|
|||||||
data={data}
|
data={data}
|
||||||
focusedItem={focusedItemData}
|
focusedItem={focusedItemData}
|
||||||
sandwichedLabel={sandwichItem}
|
sandwichedLabel={sandwichItem}
|
||||||
totalTicks={totalTicks}
|
totalTicks={totalViewTicks}
|
||||||
onFocusPillClick={onFocusPillClick}
|
onFocusPillClick={onFocusPillClick}
|
||||||
onSandwichPillClick={onSandwichPillClick}
|
onSandwichPillClick={onSandwichPillClick}
|
||||||
/>
|
/>
|
||||||
@@ -207,7 +213,7 @@ const FlameGraph = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalTicks} />
|
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalViewTicks} />
|
||||||
{clickedItemData && (
|
{clickedItemData && (
|
||||||
<FlameGraphContextMenu
|
<FlameGraphContextMenu
|
||||||
itemData={clickedItemData}
|
itemData={clickedItemData}
|
||||||
@@ -215,8 +221,8 @@ const FlameGraph = ({
|
|||||||
setClickedItemData(undefined);
|
setClickedItemData(undefined);
|
||||||
}}
|
}}
|
||||||
onItemFocus={() => {
|
onItemFocus={() => {
|
||||||
setRangeMin(clickedItemData.item.start / totalTicks);
|
setRangeMin(clickedItemData.item.start / totalViewTicks);
|
||||||
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalTicks);
|
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks);
|
||||||
onItemFocused(clickedItemData);
|
onItemFocused(clickedItemData);
|
||||||
}}
|
}}
|
||||||
onSandwich={() => {
|
onSandwich={() => {
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ const FlameGraphMetadata = React.memo(
|
|||||||
<Icon size={'sm'} name={'angle-right'} />
|
<Icon size={'sm'} name={'angle-right'} />
|
||||||
<div className={styles.metadataPill}>
|
<div className={styles.metadataPill}>
|
||||||
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
<Icon size={'sm'} name={'gf-show-context'} />{' '}
|
||||||
|
<span className={styles.metadataPillName}>
|
||||||
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
|
||||||
|
</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.pillCloseButton}
|
className={styles.pillCloseButton}
|
||||||
name={'times'}
|
name={'times'}
|
||||||
@@ -89,7 +91,8 @@ FlameGraphMetadata.displayName = 'FlameGraphMetadata';
|
|||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
metadataPill: css`
|
metadataPill: css`
|
||||||
label: metadataPill;
|
label: metadataPill;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
background: ${theme.colors.background.secondary};
|
background: ${theme.colors.background.secondary};
|
||||||
border-radius: ${theme.shape.borderRadius(8)};
|
border-radius: ${theme.shape.borderRadius(8)};
|
||||||
padding: ${theme.spacing(0.5, 1)};
|
padding: ${theme.spacing(0.5, 1)};
|
||||||
@@ -108,6 +111,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`,
|
`,
|
||||||
|
metadataPillName: css`
|
||||||
|
label: metadataPillName;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: ${theme.spacing(0.5)};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default FlameGraphMetadata;
|
export default FlameGraphMetadata;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field, FieldType, createDataFrame } from '@grafana/data';
|
import { Field, FieldType, createDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { getTooltipData } from './FlameGraphTooltip';
|
import { getDiffTooltipData, getTooltipData } from './FlameGraphTooltip';
|
||||||
import { FlameGraphDataContainer } from './dataTransform';
|
import { FlameGraphDataContainer } from './dataTransform';
|
||||||
|
|
||||||
function setupData(unit?: string) {
|
function setupData(unit?: string) {
|
||||||
@@ -15,6 +15,20 @@ function setupData(unit?: string) {
|
|||||||
return new FlameGraphDataContainer(flameGraphData);
|
return new FlameGraphDataContainer(flameGraphData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupDiffData() {
|
||||||
|
const flameGraphData = createDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'level', values: [0, 1] },
|
||||||
|
{ name: 'value', values: [200, 90] },
|
||||||
|
{ name: 'valueRight', values: [100, 40] },
|
||||||
|
{ name: 'self', values: [110, 90] },
|
||||||
|
{ name: 'selfRight', values: [60, 40] },
|
||||||
|
{ name: 'label', values: ['total', 'func1'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return new FlameGraphDataContainer(flameGraphData);
|
||||||
|
}
|
||||||
|
|
||||||
describe('FlameGraphTooltip', () => {
|
describe('FlameGraphTooltip', () => {
|
||||||
it('for bytes', () => {
|
it('for bytes', () => {
|
||||||
const tooltipData = getTooltipData(
|
const tooltipData = getTooltipData(
|
||||||
@@ -23,7 +37,6 @@ describe('FlameGraphTooltip', () => {
|
|||||||
8_624_078_250
|
8_624_078_250
|
||||||
);
|
);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
percentValue: 100,
|
percentValue: 100,
|
||||||
unitTitle: 'RAM',
|
unitTitle: 'RAM',
|
||||||
@@ -40,7 +53,6 @@ describe('FlameGraphTooltip', () => {
|
|||||||
8_624_078_250
|
8_624_078_250
|
||||||
);
|
);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
percentValue: 100,
|
percentValue: 100,
|
||||||
unitSelf: '978250',
|
unitSelf: '978250',
|
||||||
@@ -57,7 +69,6 @@ describe('FlameGraphTooltip', () => {
|
|||||||
8_624_078_250
|
8_624_078_250
|
||||||
);
|
);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
percentValue: 100,
|
percentValue: 100,
|
||||||
unitTitle: 'Count',
|
unitTitle: 'Count',
|
||||||
@@ -74,7 +85,6 @@ describe('FlameGraphTooltip', () => {
|
|||||||
8_624_078_250
|
8_624_078_250
|
||||||
);
|
);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
percentValue: 100,
|
percentValue: 100,
|
||||||
unitTitle: 'Count',
|
unitTitle: 'Count',
|
||||||
@@ -91,7 +101,6 @@ describe('FlameGraphTooltip', () => {
|
|||||||
8_624_078_250
|
8_624_078_250
|
||||||
);
|
);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
percentValue: 100,
|
percentValue: 100,
|
||||||
unitTitle: 'Time',
|
unitTitle: 'Time',
|
||||||
@@ -102,6 +111,39 @@ describe('FlameGraphTooltip', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDiffTooltipData', () => {
|
||||||
|
it('works with diff data', () => {
|
||||||
|
const tooltipData = getDiffTooltipData(
|
||||||
|
setupDiffData(),
|
||||||
|
{ start: 0, itemIndexes: [1], value: 90, valueRight: 40, children: [] },
|
||||||
|
200
|
||||||
|
);
|
||||||
|
expect(tooltipData).toEqual([
|
||||||
|
{
|
||||||
|
rowId: '1',
|
||||||
|
label: '% of total',
|
||||||
|
baseline: '50%',
|
||||||
|
comparison: '40%',
|
||||||
|
diff: '-20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowId: '2',
|
||||||
|
label: 'Value',
|
||||||
|
baseline: '50',
|
||||||
|
comparison: '40',
|
||||||
|
diff: '-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowId: '3',
|
||||||
|
label: 'Samples',
|
||||||
|
baseline: '50',
|
||||||
|
comparison: '40',
|
||||||
|
diff: '-10',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function makeField(name: string, unit: string, values: number[]): Field {
|
function makeField(name: string, unit: string, values: number[]): Field {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { DisplayValue, getValueFormat, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
|
import { InteractiveTable, Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
|
||||||
|
|
||||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
@@ -20,10 +20,26 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
const tooltipData = getTooltipData(data, item, totalTicks);
|
||||||
const content = (
|
content = (
|
||||||
<div className={styles.tooltipContent}>
|
|
||||||
<p>{data.getLabel(item.itemIndexes[0])}</p>
|
|
||||||
<p className={styles.lastParagraph}>
|
<p className={styles.lastParagraph}>
|
||||||
{tooltipData.unitTitle}
|
{tooltipData.unitTitle}
|
||||||
<br />
|
<br />
|
||||||
@@ -33,20 +49,22 @@ const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
|
|||||||
<br />
|
<br />
|
||||||
Samples: <b>{tooltipData.samples}</b>
|
Samples: <b>{tooltipData.samples}</b>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<VizTooltipContainer position={position} offset={{ x: 15, y: 0 }}>
|
<VizTooltipContainer className={styles.tooltipContainer} position={position} offset={{ x: 15, y: 0 }}>
|
||||||
|
<div className={styles.tooltipContent}>
|
||||||
|
<p className={styles.tooltipName}>{data.getLabel(item.itemIndexes[0])}</p>
|
||||||
{content}
|
{content}
|
||||||
|
</div>
|
||||||
</VizTooltipContainer>
|
</VizTooltipContainer>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type TooltipData = {
|
type TooltipData = {
|
||||||
name: string;
|
|
||||||
percentValue: number;
|
percentValue: number;
|
||||||
percentSelf: number;
|
percentSelf: number;
|
||||||
unitTitle: string;
|
unitTitle: string;
|
||||||
@@ -77,7 +95,6 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: data.getLabel(item.itemIndexes[0]),
|
|
||||||
percentValue,
|
percentValue,
|
||||||
percentSelf,
|
percentSelf,
|
||||||
unitTitle,
|
unitTitle,
|
||||||
@@ -87,10 +104,85 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DiffTableData = {
|
||||||
|
rowId: string;
|
||||||
|
label: string;
|
||||||
|
baseline: string | number;
|
||||||
|
comparison: string | number;
|
||||||
|
diff: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDiffTooltipData = (
|
||||||
|
data: FlameGraphDataContainer,
|
||||||
|
item: LevelItem,
|
||||||
|
totalTicks: number
|
||||||
|
): DiffTableData[] => {
|
||||||
|
const levels = data.getLevels();
|
||||||
|
const totalTicksRight = levels[0][0].valueRight!;
|
||||||
|
const totalTicksLeft = totalTicks - totalTicksRight;
|
||||||
|
const valueLeft = item.value - item.valueRight!;
|
||||||
|
|
||||||
|
const percentageLeft = Math.round((10000 * valueLeft) / totalTicksLeft) / 100;
|
||||||
|
const percentageRight = Math.round((10000 * item.valueRight!) / totalTicksRight) / 100;
|
||||||
|
|
||||||
|
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
|
||||||
|
|
||||||
|
const displayValueLeft = getValueWithUnit(data, data.valueDisplayProcessor(valueLeft));
|
||||||
|
const displayValueRight = getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight!));
|
||||||
|
|
||||||
|
const shortValFormat = getValueFormat('short');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rowId: '1',
|
||||||
|
label: '% of total',
|
||||||
|
baseline: percentageLeft + '%',
|
||||||
|
comparison: percentageRight + '%',
|
||||||
|
diff: shortValFormat(diff).text + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowId: '2',
|
||||||
|
label: 'Value',
|
||||||
|
baseline: displayValueLeft,
|
||||||
|
comparison: displayValueRight,
|
||||||
|
diff: getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight! - valueLeft)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowId: '3',
|
||||||
|
label: 'Samples',
|
||||||
|
baseline: shortValFormat(valueLeft).text,
|
||||||
|
comparison: shortValFormat(item.valueRight!).text,
|
||||||
|
diff: shortValFormat(item.valueRight! - valueLeft).text,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getValueWithUnit(data: FlameGraphDataContainer, displayValue: DisplayValue) {
|
||||||
|
let unitValue = displayValue.text + displayValue.suffix;
|
||||||
|
|
||||||
|
const unitTitle = data.getUnitTitle();
|
||||||
|
if (unitTitle === 'Count') {
|
||||||
|
if (!displayValue.suffix) {
|
||||||
|
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||||
|
unitValue = displayValue.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unitValue;
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
tooltipContainer: css`
|
||||||
|
title: tooltipContainer;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
tooltipContent: css`
|
tooltipContent: css`
|
||||||
title: tooltipContent;
|
title: tooltipContent;
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
tooltipName: css`
|
||||||
|
title: tooltipName;
|
||||||
|
word-break: break-all;
|
||||||
`,
|
`,
|
||||||
lastParagraph: css`
|
lastParagraph: css`
|
||||||
title: lastParagraph;
|
title: lastParagraph;
|
||||||
@@ -100,6 +192,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
title: name;
|
title: name;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
tooltipTable: css`
|
||||||
|
title: tooltipTable;
|
||||||
|
max-width: 300px;
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default FlameGraphTooltip;
|
export default FlameGraphTooltip;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { scaleLinear } from 'd3';
|
||||||
import color from 'tinycolor2';
|
import color from 'tinycolor2';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
import { ColorSchemeDiff } from '../types';
|
||||||
|
|
||||||
import murmurhash3_32_gc from './murmur3';
|
import murmurhash3_32_gc from './murmur3';
|
||||||
|
|
||||||
// Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly.
|
// Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly.
|
||||||
@@ -61,6 +64,41 @@ export function getBarColorByPackage(label: string, theme: GrafanaTheme2) {
|
|||||||
return packageColor;
|
return packageColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// green to red
|
||||||
|
const diffDefaultColors = ['rgb(0, 170, 0)', 'rgb(148, 142, 142)', 'rgb(200, 0, 0)'];
|
||||||
|
export const diffDefaultGradient = `linear-gradient(90deg, ${diffDefaultColors[0]} 0%, ${diffDefaultColors[1]} 50%, ${diffDefaultColors[2]} 100%)`;
|
||||||
|
const diffColorBlindColors = ['rgb(26, 133, 255)', 'rgb(148, 142, 142)', 'rgb(220, 50, 32)'];
|
||||||
|
export const diffColorBlindGradient = `linear-gradient(90deg, ${diffColorBlindColors[0]} 0%, ${diffColorBlindColors[1]} 50%, ${diffColorBlindColors[2]} 100%)`;
|
||||||
|
|
||||||
|
export function getBarColorByDiff(
|
||||||
|
ticks: number,
|
||||||
|
ticksRight: number,
|
||||||
|
totalTicks: number,
|
||||||
|
totalTicksRight: number,
|
||||||
|
colorScheme: ColorSchemeDiff
|
||||||
|
) {
|
||||||
|
const ticksLeft = ticks - ticksRight;
|
||||||
|
const totalTicksLeft = totalTicks - totalTicksRight;
|
||||||
|
|
||||||
|
const percentageLeft = Math.round((10000 * ticksLeft) / totalTicksLeft) / 100;
|
||||||
|
const percentageRight = Math.round((10000 * ticksRight) / totalTicksRight) / 100;
|
||||||
|
|
||||||
|
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
|
||||||
|
|
||||||
|
const range = colorScheme === ColorSchemeDiff.Default ? diffDefaultColors : diffColorBlindColors;
|
||||||
|
|
||||||
|
const colorScale = scaleLinear()
|
||||||
|
.domain([-100, 0, 100])
|
||||||
|
// TODO types from DefinitelyTyped seem to mismatch
|
||||||
|
// @ts-ignore
|
||||||
|
.range(range);
|
||||||
|
|
||||||
|
// TODO types from DefinitelyTyped seem to mismatch
|
||||||
|
// @ts-ignore
|
||||||
|
const rgbString: string = colorScale(diff);
|
||||||
|
return color(rgbString);
|
||||||
|
}
|
||||||
|
|
||||||
// const getColors = memoizeOne((theme) => getFilteredColors(colors, theme));
|
// const getColors = memoizeOne((theme) => getFilteredColors(colors, theme));
|
||||||
|
|
||||||
// Different regexes to get the package name and function name from the label. We may at some point get an info about
|
// Different regexes to get the package name and function name from the label. We may at some point get an info about
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createDataFrame, FieldType } from '@grafana/data';
|
import { createDataFrame, DataFrameDTO, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
||||||
|
|
||||||
@@ -68,4 +68,138 @@ describe('nestedSetToLevels', () => {
|
|||||||
expect(levels[0]).toEqual([n1]);
|
expect(levels[0]).toEqual([n1]);
|
||||||
expect(levels[1]).toEqual([n2, n3, n4]);
|
expect(levels[1]).toEqual([n2, n3, n4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles diff data', () => {
|
||||||
|
const frame = createDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'level', values: [0, 1, 1, 1] },
|
||||||
|
{ name: 'value', values: [10, 5, 3, 1] },
|
||||||
|
{ name: 'valueRight', values: [10, 4, 2, 1] },
|
||||||
|
{ name: 'label', values: ['1', '2', '3', '4'], type: FieldType.string },
|
||||||
|
{ name: 'self', values: [10, 5, 3, 1] },
|
||||||
|
{ name: 'selfRight', values: [10, 4, 2, 1] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
|
||||||
|
|
||||||
|
expect(levels[1][0]).toMatchObject({ itemIndexes: [1], value: 9, valueRight: 4 });
|
||||||
|
expect(levels[1][1]).toMatchObject({ itemIndexes: [2], value: 5, valueRight: 2 });
|
||||||
|
expect(levels[1][2]).toMatchObject({ itemIndexes: [3], value: 2, valueRight: 1 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('diffFlamebearerToDataFrameDTO', () => {
|
||||||
|
it('works', function () {
|
||||||
|
// The main point of this test is to have some easy way to convert flamebearer data to data frame, so it can be used
|
||||||
|
// for example in test data source. Reason is in grafana we don't have a way to produce diff frames and so we have
|
||||||
|
// to use pyro app which gives you flamebearer format. So if you need to create a diff data frame to save somewhere
|
||||||
|
// just log the frame and copy the values.
|
||||||
|
|
||||||
|
const levels = [
|
||||||
|
[0, 378, 0, 0, 316, 0, 0],
|
||||||
|
[0, 12, 0, 0, 16, 0, 1],
|
||||||
|
];
|
||||||
|
const names = ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'];
|
||||||
|
const frame = diffFlamebearerToDataFrameDTO(levels, names);
|
||||||
|
|
||||||
|
// console.log(JSON.stringify(frame));
|
||||||
|
expect(frame).toMatchObject({
|
||||||
|
name: 'response',
|
||||||
|
meta: { preferredVisualisationType: 'flamegraph' },
|
||||||
|
fields: [
|
||||||
|
{ name: 'level', values: [0, 1] },
|
||||||
|
{ name: 'label', values: ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'] },
|
||||||
|
{ name: 'self', values: [0, 0] },
|
||||||
|
{ name: 'value', values: [378, 12] },
|
||||||
|
{ name: 'selfRight', values: [0, 0] },
|
||||||
|
{ name: 'valueRight', values: [316, 16] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNodes(level: number[], names: string[]) {
|
||||||
|
const nodes = [];
|
||||||
|
for (let i = 0; i < level.length; i += 7) {
|
||||||
|
nodes.push({
|
||||||
|
level: 0,
|
||||||
|
label: names[level[i + 6]],
|
||||||
|
self: level[i + 2],
|
||||||
|
val: level[i + 1],
|
||||||
|
selfRight: level[i + 5],
|
||||||
|
valRight: level[i + 4],
|
||||||
|
valTotal: level[i + 1] + level[i + 4],
|
||||||
|
offset: level[i],
|
||||||
|
offsetRight: level[i + 3],
|
||||||
|
offsetTotal: level[i] + level[i + 3],
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffFlamebearerToDataFrameDTO(levels: number[][], names: string[]) {
|
||||||
|
const nodeLevels: any[][] = [];
|
||||||
|
for (let i = 0; i < levels.length; i++) {
|
||||||
|
nodeLevels[i] = [];
|
||||||
|
for (const node of getNodes(levels[i], names)) {
|
||||||
|
node.level = i;
|
||||||
|
nodeLevels[i].push(node);
|
||||||
|
if (i > 0) {
|
||||||
|
const prevNodesInLevel = nodeLevels[i].slice(0, -1);
|
||||||
|
const currentNodeStart =
|
||||||
|
prevNodesInLevel.reduce((acc: number, n: any) => n.offsetTotal + n.valTotal + acc, 0) + node.offsetTotal;
|
||||||
|
|
||||||
|
const prevLevel = nodeLevels[i - 1];
|
||||||
|
let prevLevelOffset = 0;
|
||||||
|
for (const prevLevelNode of prevLevel) {
|
||||||
|
const parentNodeStart = prevLevelOffset + prevLevelNode.offsetTotal;
|
||||||
|
const parentNodeEnd = parentNodeStart + prevLevelNode.valTotal;
|
||||||
|
|
||||||
|
if (parentNodeStart <= currentNodeStart && parentNodeEnd > currentNodeStart) {
|
||||||
|
prevLevelNode.children.push(node);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
prevLevelOffset += prevLevelNode.offsetTotal + prevLevelNode.valTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeLevels[0][0];
|
||||||
|
const stack = [root];
|
||||||
|
|
||||||
|
const labelValues = [];
|
||||||
|
const levelValues = [];
|
||||||
|
const selfValues = [];
|
||||||
|
const valueValues = [];
|
||||||
|
const selfRightValues = [];
|
||||||
|
const valueRightValues = [];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const node = stack.shift();
|
||||||
|
labelValues.push(node.label);
|
||||||
|
levelValues.push(node.level);
|
||||||
|
selfValues.push(node.self);
|
||||||
|
valueValues.push(node.val);
|
||||||
|
selfRightValues.push(node.selfRight);
|
||||||
|
valueRightValues.push(node.valRight);
|
||||||
|
stack.unshift(...node.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame: DataFrameDTO = {
|
||||||
|
name: 'response',
|
||||||
|
meta: { preferredVisualisationType: 'flamegraph' },
|
||||||
|
fields: [
|
||||||
|
{ name: 'level', values: levelValues },
|
||||||
|
{ name: 'label', values: labelValues, type: FieldType.string },
|
||||||
|
{ name: 'self', values: selfValues },
|
||||||
|
{ name: 'value', values: valueValues },
|
||||||
|
{ name: 'selfRight', values: selfRightValues },
|
||||||
|
{ name: 'valueRight', values: valueRightValues },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export type LevelItem = {
|
|||||||
start: number;
|
start: number;
|
||||||
// Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have
|
// Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have
|
||||||
// to trim the value to correspond only to the part used by the children in the subtree.
|
// to trim the value to correspond only to the part used by the children in the subtree.
|
||||||
|
// In case of diff profile this is actually left + right value.
|
||||||
value: number;
|
value: number;
|
||||||
|
// Only exists for diff profiles.
|
||||||
|
valueRight?: number;
|
||||||
// Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single
|
// Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single
|
||||||
// node.
|
// node.
|
||||||
itemIndexes: number[];
|
itemIndexes: number[];
|
||||||
@@ -46,7 +49,10 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
|
|||||||
// We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level.
|
// We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level.
|
||||||
// So we have to compute the correct offset based on the last sibling.
|
// So we have to compute the correct offset based on the last sibling.
|
||||||
const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
|
const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
|
||||||
offset = lastSibling.start + container.getValue(lastSibling.itemIndexes[0]);
|
offset =
|
||||||
|
lastSibling.start +
|
||||||
|
container.getValue(lastSibling.itemIndexes[0]) +
|
||||||
|
container.getValueRight(lastSibling.itemIndexes[0]);
|
||||||
// we assume there is always a single root node so lastSibling should always have a parent.
|
// we assume there is always a single root node so lastSibling should always have a parent.
|
||||||
// Also it has to have the same parent because of how the items are ordered.
|
// Also it has to have the same parent because of how the items are ordered.
|
||||||
parent = lastSibling.parents![0];
|
parent = lastSibling.parents![0];
|
||||||
@@ -54,7 +60,8 @@ export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelIte
|
|||||||
|
|
||||||
const newItem: LevelItem = {
|
const newItem: LevelItem = {
|
||||||
itemIndexes: [i],
|
itemIndexes: [i],
|
||||||
value: container.getValue(i),
|
value: container.getValue(i) + container.getValueRight(i),
|
||||||
|
valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : undefined,
|
||||||
start: offset,
|
start: offset,
|
||||||
parents: parent && [parent],
|
parents: parent && [parent],
|
||||||
children: [],
|
children: [],
|
||||||
@@ -135,6 +142,10 @@ export class FlameGraphDataContainer {
|
|||||||
valueField: Field;
|
valueField: Field;
|
||||||
selfField: Field;
|
selfField: Field;
|
||||||
|
|
||||||
|
// Optional fields for diff view
|
||||||
|
valueRightField?: Field;
|
||||||
|
selfRightField?: Field;
|
||||||
|
|
||||||
labelDisplayProcessor: DisplayProcessor;
|
labelDisplayProcessor: DisplayProcessor;
|
||||||
valueDisplayProcessor: DisplayProcessor;
|
valueDisplayProcessor: DisplayProcessor;
|
||||||
uniqueLabels: string[];
|
uniqueLabels: string[];
|
||||||
@@ -155,6 +166,15 @@ export class FlameGraphDataContainer {
|
|||||||
this.valueField = data.fields.find((f) => f.name === 'value')!;
|
this.valueField = data.fields.find((f) => f.name === 'value')!;
|
||||||
this.selfField = data.fields.find((f) => f.name === 'self')!;
|
this.selfField = data.fields.find((f) => f.name === 'self')!;
|
||||||
|
|
||||||
|
this.valueRightField = data.fields.find((f) => f.name === 'valueRight')!;
|
||||||
|
this.selfRightField = data.fields.find((f) => f.name === 'selfRight')!;
|
||||||
|
|
||||||
|
if ((this.valueField || this.selfField) && !(this.valueField && this.selfField)) {
|
||||||
|
throw new Error(
|
||||||
|
'Malformed dataFrame: both valueRight and selfRight has to be present if one of them is present.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const enumConfig = this.labelField?.config?.type?.enum;
|
const enumConfig = this.labelField?.config?.type?.enum;
|
||||||
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
|
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
|
||||||
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
||||||
@@ -176,6 +196,10 @@ export class FlameGraphDataContainer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDiffFlamegraph() {
|
||||||
|
return this.valueRightField && this.selfRightField;
|
||||||
|
}
|
||||||
|
|
||||||
getLabel(index: number) {
|
getLabel(index: number) {
|
||||||
return this.labelDisplayProcessor(this.labelField.values[index]).text;
|
return this.labelDisplayProcessor(this.labelField.values[index]).text;
|
||||||
}
|
}
|
||||||
@@ -185,21 +209,19 @@ export class FlameGraphDataContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getValue(index: number | number[]) {
|
getValue(index: number | number[]) {
|
||||||
let indexArray: number[] = typeof index === 'number' ? [index] : index;
|
return fieldAccessor(this.valueField, index);
|
||||||
return indexArray.reduce((acc, index) => {
|
|
||||||
return acc + this.valueField.values[index];
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getValueDisplay(index: number | number[]) {
|
getValueRight(index: number | number[]) {
|
||||||
return this.valueDisplayProcessor(this.getValue(index));
|
return fieldAccessor(this.valueRightField, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelf(index: number | number[]) {
|
getSelf(index: number | number[]) {
|
||||||
let indexArray: number[] = typeof index === 'number' ? [index] : index;
|
return fieldAccessor(this.selfField, index);
|
||||||
return indexArray.reduce((acc, index) => {
|
}
|
||||||
return acc + this.selfField.values[index];
|
|
||||||
}, 0);
|
getSelfRight(index: number | number[]) {
|
||||||
|
return fieldAccessor(this.selfRightField, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelfDisplay(index: number | number[]) {
|
getSelfDisplay(index: number | number[]) {
|
||||||
@@ -226,11 +248,11 @@ export class FlameGraphDataContainer {
|
|||||||
return this.levels!;
|
return this.levels!;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSandwichLevels(label: string) {
|
getSandwichLevels(label: string): [LevelItem[][], LevelItem[][]] {
|
||||||
const nodes = this.getNodesWithLabel(label);
|
const nodes = this.getNodesWithLabel(label);
|
||||||
|
|
||||||
if (!nodes?.length) {
|
if (!nodes?.length) {
|
||||||
return [];
|
return [[], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const callers = mergeParentSubtrees(nodes, this);
|
const callers = mergeParentSubtrees(nodes, this);
|
||||||
@@ -252,3 +274,15 @@ export class FlameGraphDataContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access field value with either single index or array of indexes. This is needed as we sometimes merge multiple
|
||||||
|
// into one, and we want to access aggregated values.
|
||||||
|
function fieldAccessor(field: Field | undefined, index: number | number[]) {
|
||||||
|
if (!field) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let indexArray: number[] = typeof index === 'number' ? [index] : index;
|
||||||
|
return indexArray.reduce((acc, index) => {
|
||||||
|
return acc + field.values[index];
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,26 +12,53 @@ import {
|
|||||||
LABEL_THRESHOLD,
|
LABEL_THRESHOLD,
|
||||||
PIXELS_PER_LEVEL,
|
PIXELS_PER_LEVEL,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { ClickedItemData, ColorScheme, TextAlign } from '../types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
|
||||||
|
|
||||||
import { getBarColorByPackage, getBarColorByValue } from './colors';
|
import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './colors';
|
||||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
const ufuzzy = new uFuzzy();
|
const ufuzzy = new uFuzzy();
|
||||||
|
|
||||||
export function useFlameRender(
|
type RenderOptions = {
|
||||||
canvasRef: RefObject<HTMLCanvasElement>,
|
canvasRef: RefObject<HTMLCanvasElement>;
|
||||||
data: FlameGraphDataContainer,
|
data: FlameGraphDataContainer;
|
||||||
levels: LevelItem[][],
|
levels: LevelItem[][];
|
||||||
wrapperWidth: number,
|
wrapperWidth: number;
|
||||||
rangeMin: number,
|
|
||||||
rangeMax: number,
|
// If we are rendering only zoomed in part of the graph.
|
||||||
search: string,
|
rangeMin: number;
|
||||||
textAlign: TextAlign,
|
rangeMax: number;
|
||||||
totalTicks: number,
|
|
||||||
colorScheme: ColorScheme,
|
search: string;
|
||||||
focusedItemData?: ClickedItemData
|
textAlign: TextAlign;
|
||||||
) {
|
|
||||||
|
// Total ticks that will be used for sizing
|
||||||
|
totalViewTicks: number;
|
||||||
|
// Total ticks that will be used for computing colors as some color scheme (like in diff view) should not be affected
|
||||||
|
// by sandwich or focus view.
|
||||||
|
totalColorTicks: number;
|
||||||
|
// Total ticks used to compute the diff colors
|
||||||
|
totalTicksRight: number | undefined;
|
||||||
|
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||||
|
focusedItemData?: ClickedItemData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useFlameRender(options: RenderOptions) {
|
||||||
|
const {
|
||||||
|
canvasRef,
|
||||||
|
data,
|
||||||
|
levels,
|
||||||
|
wrapperWidth,
|
||||||
|
rangeMin,
|
||||||
|
rangeMax,
|
||||||
|
search,
|
||||||
|
textAlign,
|
||||||
|
totalViewTicks,
|
||||||
|
totalColorTicks,
|
||||||
|
totalTicksRight,
|
||||||
|
colorScheme,
|
||||||
|
focusedItemData,
|
||||||
|
} = options;
|
||||||
const foundLabels = useMemo(() => {
|
const foundLabels = useMemo(() => {
|
||||||
if (search) {
|
if (search) {
|
||||||
const foundLabels = new Set<string>();
|
const foundLabels = new Set<string>();
|
||||||
@@ -57,20 +84,21 @@ export function useFlameRender(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalTicks / (rangeMax - rangeMin);
|
const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalViewTicks / (rangeMax - rangeMin);
|
||||||
|
|
||||||
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
||||||
const level = levels[levelIndex];
|
const level = levels[levelIndex];
|
||||||
// Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because
|
// Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because
|
||||||
// sometimes we collapse multiple bars into single rect.
|
// sometimes we collapse multiple bars into single rect.
|
||||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalViewTicks, rangeMin, pixelsPerTick);
|
||||||
for (const rect of dimensions) {
|
for (const rect of dimensions) {
|
||||||
const focusedLevel = focusedItemData ? focusedItemData.level : 0;
|
const focusedLevel = focusedItemData ? focusedItemData.level : 0;
|
||||||
// Render each rectangle based on the computed dimensions
|
// Render each rectangle based on the computed dimensions
|
||||||
renderRect(
|
renderRect(
|
||||||
ctx,
|
ctx,
|
||||||
rect,
|
rect,
|
||||||
totalTicks,
|
totalColorTicks,
|
||||||
|
totalTicksRight,
|
||||||
rangeMin,
|
rangeMin,
|
||||||
rangeMax,
|
rangeMax,
|
||||||
levelIndex,
|
levelIndex,
|
||||||
@@ -93,7 +121,9 @@ export function useFlameRender(
|
|||||||
focusedItemData,
|
focusedItemData,
|
||||||
foundLabels,
|
foundLabels,
|
||||||
textAlign,
|
textAlign,
|
||||||
totalTicks,
|
totalViewTicks,
|
||||||
|
totalColorTicks,
|
||||||
|
totalTicksRight,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
theme,
|
theme,
|
||||||
]);
|
]);
|
||||||
@@ -129,6 +159,7 @@ type RectData = {
|
|||||||
y: number;
|
y: number;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
ticks: number;
|
ticks: number;
|
||||||
|
ticksRight?: number;
|
||||||
label: string;
|
label: string;
|
||||||
unitLabel: string;
|
unitLabel: string;
|
||||||
itemIndex: number;
|
itemIndex: number;
|
||||||
@@ -176,6 +207,8 @@ export function getRectDimensionsForLevel(
|
|||||||
y: levelIndex * PIXELS_PER_LEVEL,
|
y: levelIndex * PIXELS_PER_LEVEL,
|
||||||
collapsed,
|
collapsed,
|
||||||
ticks: curBarTicks,
|
ticks: curBarTicks,
|
||||||
|
// When collapsed this does not make that much sense but then we don't really use it anyway.
|
||||||
|
ticksRight: item.valueRight,
|
||||||
label: data.getLabel(item.itemIndexes[0]),
|
label: data.getLabel(item.itemIndexes[0]),
|
||||||
unitLabel: unit,
|
unitLabel: unit,
|
||||||
itemIndex: item.itemIndexes[0],
|
itemIndex: item.itemIndexes[0],
|
||||||
@@ -188,13 +221,14 @@ export function renderRect(
|
|||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
rect: RectData,
|
rect: RectData,
|
||||||
totalTicks: number,
|
totalTicks: number,
|
||||||
|
totalTicksRight: number | undefined,
|
||||||
rangeMin: number,
|
rangeMin: number,
|
||||||
rangeMax: number,
|
rangeMax: number,
|
||||||
levelIndex: number,
|
levelIndex: number,
|
||||||
topLevelIndex: number,
|
topLevelIndex: number,
|
||||||
foundNames: Set<string> | undefined,
|
foundNames: Set<string> | undefined,
|
||||||
textAlign: TextAlign,
|
textAlign: TextAlign,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme | ColorSchemeDiff,
|
||||||
theme: GrafanaTheme2
|
theme: GrafanaTheme2
|
||||||
) {
|
) {
|
||||||
if (rect.width < HIDE_THRESHOLD) {
|
if (rect.width < HIDE_THRESHOLD) {
|
||||||
@@ -205,7 +239,10 @@ export function renderRect(
|
|||||||
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
|
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
colorScheme === ColorScheme.ValueBased
|
rect.ticksRight !== undefined &&
|
||||||
|
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
|
||||||
|
? getBarColorByDiff(rect.ticks, rect.ticksRight, totalTicks, totalTicksRight!, colorScheme)
|
||||||
|
: colorScheme === ColorScheme.ValueBased
|
||||||
? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax)
|
? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax)
|
||||||
: getBarColorByPackage(rect.label, theme);
|
: getBarColorByPackage(rect.label, theme);
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function getParentSubtrees(roots: LevelItem[]) {
|
|||||||
|
|
||||||
if (args.child) {
|
if (args.child) {
|
||||||
newNode.value = args.child.value;
|
newNode.value = args.child.value;
|
||||||
|
newNode.valueRight = args.child.valueRight;
|
||||||
args.child.parents = [newNode];
|
args.child.parents = [newNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +87,14 @@ export function mergeSubtrees(
|
|||||||
const newItem: LevelItem = {
|
const newItem: LevelItem = {
|
||||||
// We use the items value instead of value from the data frame, cause we could have changed it in the process
|
// We use the items value instead of value from the data frame, cause we could have changed it in the process
|
||||||
value: args.items.reduce((acc, i) => acc + i.value, 0),
|
value: args.items.reduce((acc, i) => acc + i.value, 0),
|
||||||
|
// valueRight may not exist at all if this is not a diff profile
|
||||||
|
valueRight: args.items.reduce<number | undefined>((acc, i) => {
|
||||||
|
if (i.valueRight !== undefined) {
|
||||||
|
return (acc ?? 0) + i.valueRight;
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, undefined),
|
||||||
itemIndexes: indexes,
|
itemIndexes: indexes,
|
||||||
// these will change later
|
// these will change later
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import FlameGraph from './FlameGraph/FlameGraph';
|
|||||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||||
import { ClickedItemData, ColorScheme, SelectedView, TextAlign } from './types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data?: DataFrame;
|
data?: DataFrame;
|
||||||
@@ -30,7 +30,6 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
||||||
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
||||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
const [sandwichItem, setSandwichItem] = useState<string>();
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(ColorScheme.ValueBased);
|
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
|
||||||
@@ -41,6 +40,8 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
return new FlameGraphDataContainer(props.data, theme);
|
return new FlameGraphDataContainer(props.data, theme);
|
||||||
}, [props.data, theme]);
|
}, [props.data, theme]);
|
||||||
|
|
||||||
|
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
// If user resizes window with both as the selected view
|
// If user resizes window with both as the selected view
|
||||||
@@ -105,6 +106,7 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
onColorSchemeChange={setColorScheme}
|
onColorSchemeChange={setColorScheme}
|
||||||
|
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
@@ -149,6 +151,29 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||||
|
const [colorScheme, setColorScheme] = useState<ColorScheme | ColorSchemeDiff>(
|
||||||
|
dataContainer?.isDiffFlamegraph() ? ColorSchemeDiff.Default : ColorScheme.ValueBased
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
dataContainer?.isDiffFlamegraph() &&
|
||||||
|
(colorScheme === ColorScheme.ValueBased || colorScheme === ColorScheme.PackageBased)
|
||||||
|
) {
|
||||||
|
setColorScheme(ColorSchemeDiff.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!dataContainer?.isDiffFlamegraph() &&
|
||||||
|
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
|
||||||
|
) {
|
||||||
|
setColorScheme(ColorScheme.ValueBased);
|
||||||
|
}
|
||||||
|
}, [dataContainer, colorScheme]);
|
||||||
|
|
||||||
|
return [colorScheme, setColorScheme] as const;
|
||||||
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
container: css({
|
container: css({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('FlameGraphHeader', () => {
|
|||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
colorScheme={ColorScheme.ValueBased}
|
colorScheme={ColorScheme.ValueBased}
|
||||||
onColorSchemeChange={onSchemeChange}
|
onColorSchemeChange={onSchemeChange}
|
||||||
|
isDiffMode={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -72,4 +73,14 @@ describe('FlameGraphHeader', () => {
|
|||||||
|
|
||||||
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
|
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows diff color scheme switch when diff', async () => {
|
||||||
|
setup({ isDiffMode: true });
|
||||||
|
const changeButton = screen.getByLabelText(/Change color scheme/);
|
||||||
|
expect(changeButton).toBeInTheDocument();
|
||||||
|
await userEvent.click(changeButton);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Default/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Color blind/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@gr
|
|||||||
|
|
||||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||||
|
|
||||||
import { byPackageGradient, byValueGradient } from './FlameGraph/colors';
|
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
||||||
import { ColorScheme, SelectedView, TextAlign } from './types';
|
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
app: CoreApp;
|
app: CoreApp;
|
||||||
@@ -23,8 +23,9 @@ type Props = {
|
|||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
onTextAlignChange: (align: TextAlign) => void;
|
onTextAlignChange: (align: TextAlign) => void;
|
||||||
showResetButton: boolean;
|
showResetButton: boolean;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||||
onColorSchemeChange: (colorScheme: ColorScheme) => void;
|
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||||
|
isDiffMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphHeader = ({
|
const FlameGraphHeader = ({
|
||||||
@@ -40,6 +41,7 @@ const FlameGraphHeader = ({
|
|||||||
showResetButton,
|
showResetButton,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
onColorSchemeChange,
|
onColorSchemeChange,
|
||||||
|
isDiffMode,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2((theme) => getStyles(theme, app));
|
const styles = useStyles2((theme) => getStyles(theme, app));
|
||||||
function interaction(name: string, context: Record<string, string | number>) {
|
function interaction(name: string, context: Record<string, string | number>) {
|
||||||
@@ -97,7 +99,7 @@ const FlameGraphHeader = ({
|
|||||||
aria-label={'Reset focus and sandwich state'}
|
aria-label={'Reset focus and sandwich state'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ColorSchemeButton app={app} value={colorScheme} onChange={onColorSchemeChange} />
|
<ColorSchemeButton app={app} value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
||||||
<RadioButtonGroup<TextAlign>
|
<RadioButtonGroup<TextAlign>
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={selectedView === SelectedView.TopTable}
|
disabled={selectedView === SelectedView.TopTable}
|
||||||
@@ -125,17 +127,38 @@ const FlameGraphHeader = ({
|
|||||||
|
|
||||||
type ColorSchemeButtonProps = {
|
type ColorSchemeButtonProps = {
|
||||||
app: CoreApp;
|
app: CoreApp;
|
||||||
value: ColorScheme;
|
value: ColorScheme | ColorSchemeDiff;
|
||||||
onChange: (colorScheme: ColorScheme) => void;
|
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||||
|
isDiffMode: boolean;
|
||||||
};
|
};
|
||||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||||
const styles = useStyles2((theme) => getStyles(theme, props.app));
|
const styles = useStyles2((theme) => getStyles(theme, props.app));
|
||||||
const menu = (
|
|
||||||
|
let menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (props.isDiffMode) {
|
||||||
|
menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
|
||||||
|
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a bit different gradient as a way to indicate selected value
|
||||||
|
const colorDotStyle =
|
||||||
|
{
|
||||||
|
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
||||||
|
[ColorScheme.PackageBased]: styles.colorDotByPackage,
|
||||||
|
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
|
||||||
|
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
|
||||||
|
}[props.value] || styles.colorDotByValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={menu}>
|
||||||
<Button
|
<Button
|
||||||
@@ -147,13 +170,7 @@ function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
|||||||
className={styles.buttonSpacing}
|
className={styles.buttonSpacing}
|
||||||
aria-label={'Change color scheme'}
|
aria-label={'Change color scheme'}
|
||||||
>
|
>
|
||||||
<span
|
<span className={cx(styles.colorDot, colorDotStyle)} />
|
||||||
className={cx(
|
|
||||||
styles.colorDot,
|
|
||||||
// Show a bit different gradient as a way to indicate selected value
|
|
||||||
props.value === ColorScheme.ValueBased ? styles.colorDotByValue : styles.colorDotByPackage
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
@@ -264,6 +281,15 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
|
|||||||
label: colorDotByPackage;
|
label: colorDotByPackage;
|
||||||
background: ${byPackageGradient};
|
background: ${byPackageGradient};
|
||||||
`,
|
`,
|
||||||
|
colorDotDiffDefault: css`
|
||||||
|
label: colorDotDiffDefault;
|
||||||
|
background: ${diffDefaultGradient};
|
||||||
|
`,
|
||||||
|
|
||||||
|
colorDotDiffColorBlind: css`
|
||||||
|
label: colorDotDiffColorBlind;
|
||||||
|
background: ${diffColorBlindGradient};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default FlameGraphHeader;
|
export default FlameGraphHeader;
|
||||||
|
|||||||
@@ -85,11 +85,13 @@ function buildTableDataFrame(
|
|||||||
let table: { [key: string]: TableData } = {};
|
let table: { [key: string]: TableData } = {};
|
||||||
for (let i = 0; i < data.data.length; i++) {
|
for (let i = 0; i < data.data.length; i++) {
|
||||||
const value = data.getValue(i);
|
const value = data.getValue(i);
|
||||||
|
const valueRight = data.getValueRight(i);
|
||||||
const self = data.getSelf(i);
|
const self = data.getSelf(i);
|
||||||
const label = data.getLabel(i);
|
const label = data.getLabel(i);
|
||||||
table[label] = table[label] || {};
|
table[label] = table[label] || {};
|
||||||
table[label].self = table[label].self ? table[label].self + self : self;
|
table[label].self = table[label].self ? table[label].self + self : self;
|
||||||
table[label].total = table[label].total ? table[label].total + value : value;
|
table[label].total = table[label].total ? table[label].total + value : value;
|
||||||
|
table[label].totalRight = table[label].totalRight ? table[label].totalRight + valueRight : valueRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionField: Field = createActionField(onSandwich, onSearch, search, sandwichItem);
|
const actionField: Field = createActionField(onSandwich, onSearch, search, sandwichItem);
|
||||||
@@ -114,6 +116,45 @@ function buildTableDataFrame(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let frame;
|
||||||
|
|
||||||
|
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 selfField = createNumberField('Self', data.selfField.config.unit);
|
||||||
const totalField = createNumberField('Total', data.valueField.config.unit);
|
const totalField = createNumberField('Total', data.valueField.config.unit);
|
||||||
|
|
||||||
@@ -124,7 +165,8 @@ function buildTableDataFrame(
|
|||||||
totalField.values.push(table[key].total);
|
totalField.values.push(table[key].total);
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = { fields: [actionField, symbolField, selfField, totalField], length: symbolField.values.length };
|
frame = { fields: [actionField, symbolField, selfField, totalField], length: symbolField.values.length };
|
||||||
|
}
|
||||||
|
|
||||||
const dataFrames = applyFieldOverrides({
|
const dataFrames = applyFieldOverrides({
|
||||||
data: [frame],
|
data: [frame],
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export enum SelectedView {
|
|||||||
export interface TableData {
|
export interface TableData {
|
||||||
self: number;
|
self: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
// For diff view
|
||||||
|
totalRight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopTableData {
|
export interface TopTableData {
|
||||||
@@ -47,4 +49,9 @@ export enum ColorScheme {
|
|||||||
PackageBased = 'packageBased',
|
PackageBased = 'packageBased',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ColorSchemeDiff {
|
||||||
|
Default = 'default',
|
||||||
|
DiffColorBlind = 'diffColorBlind',
|
||||||
|
}
|
||||||
|
|
||||||
export type TextAlign = 'left' | 'right';
|
export type TextAlign = 'left' | 'right';
|
||||||
|
|||||||
Reference in New Issue
Block a user