[Profiling] Use CO2 and Cost values provided by the ES Profiling APIs (#170612)

- Removes old flamegraph code replacing it with the ES Flamegraph API
- Creates new user settings
- Adds a feature flag to use the kibana CO2/Cost calculations instead of
the new version
- Reads CO2 and Cost from /Stacktraces and /Flamegraph APIs

Where do we show the CO2 and Cost values?
- Flamegraph toolip
- Flamegraph Frame information flyout
- Diff Flamegraph Summary
- Functions table
- Function information flyout
- Diff Functions Summary

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-12-04 04:59:30 +00:00 committed by GitHub
parent 3aac9bd56d
commit 7470d2136d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1829 additions and 795 deletions

View file

@ -1,17 +0,0 @@
The stacktrace fixtures in this directory are originally from Elasticsearch's
`POST /_profiling/stacktraces` endpoint. They were subsequently filtered
through the `shrink_stacktrace_response.js` command in `x-pack/plugins/profiling/scripts/`
to reduce the size without losing sampling fidelity (see the script for further
details).
The naming convention for each stacktrace fixture follows this pattern:
```
stacktraces_{seconds}s_{upsampling rate}x.json
```
where `seconds` is the time span of the original query and `upsampling rate` is
the reciprocal of the sampling rate returned from the original query.
To add a new stacktrace fixture to the test suite, update `stacktraces.ts`
appropriately.

View file

@ -0,0 +1,298 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BaseFlameGraph } from '../flamegraph';
export const baseFlamegraph: BaseFlameGraph = {
Edges: [
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9],
[10],
[11],
[12],
[13],
[14],
[15],
[16],
[17],
[18],
[19],
[20],
[21],
[22],
[23],
[24],
[25],
[26],
[27],
[28],
[29],
[30],
[31],
[32],
[33],
[34],
[],
],
FileID: [
'',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
],
FrameType: [
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4,
],
Inline: [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
],
ExeFilename: [
'',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
],
AddressOrLine: [
0, 43443520, 67880745, 67881145, 53704110, 53704665, 53696841, 53697537, 53700683, 53696841,
52492674, 67626923, 67629380, 67630226, 51515812, 51512445, 51522994, 44606453, 43747101,
43699300, 43538916, 43547623, 42994898, 42994925, 14680216, 14356875, 3732840, 3732678, 3721714,
3719260, 3936007, 3897721, 4081162, 4458225, 1712873,
],
FunctionName: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'entry_SYSCALL_64_after_hwframe',
'do_syscall_64',
'__x64_sys_read',
'ksys_read',
'vfs_read',
'new_sync_read',
'seq_read_iter',
'm_show',
'show_mountinfo',
'kernfs_sop_show_path',
'cgroup_show_path',
],
FunctionOffset: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
],
SourceFilename: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
SourceLine: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
],
CountInclusive: [
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7,
],
CountExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 7,
],
AnnualCO2TonsInclusive: [
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
],
AnnualCO2TonsExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0.0013627551116480942,
],
AnnualCostsUSDInclusive: [
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
],
AnnualCostsUSDExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 61.30240940376492,
],
Size: 35,
SamplingRate: 1,
SelfCPU: 7,
SelfAnnualCO2Tons: 0.0013627551116480942,
TotalAnnualCO2Tons: 0.04769642890768329,
SelfAnnualCostsUSD: 61.30240940376492,
TotalAnnualCostsUSD: 2145.5843291317715,
TotalCPU: 245,
TotalSamples: 7,
TotalSeconds: 4.980000019073486,
};

View file

@ -8,18 +8,166 @@
import { StackTraceResponse } from '../stack_traces';
import stackTraces1x from './stacktraces_60s_1x.json';
import stackTraces5x from './stacktraces_3600s_5x.json';
import stackTraces125x from './stacktraces_86400s_125x.json';
import stackTraces625x from './stacktraces_604800s_625x.json';
export const stackTraceFixtures: Array<{
response: StackTraceResponse;
seconds: number;
upsampledBy: number;
}> = [
{ response: stackTraces1x, seconds: 60, upsampledBy: 1 },
{ response: stackTraces5x, seconds: 3600, upsampledBy: 5 },
{ response: stackTraces125x, seconds: 86400, upsampledBy: 125 },
{ response: stackTraces625x, seconds: 604800, upsampledBy: 625 },
];
export const stacktraces: StackTraceResponse = {
stack_traces: {
['c2TovSbgCECd_RtKHxMtyQ']: {
address_or_lines: [
43443520, 67880745, 67881145, 53704110, 53704665, 53696841, 53697537, 53700683, 53696841,
52492674, 67626923, 67629380, 67630226, 51515812, 51512445, 51522994, 44606453, 43747101,
43699300, 43538916, 43547623, 42994898, 42994925, 14680216, 14356875, 3732840, 3732678,
3721714, 3719260, 3936007, 3897721, 4081162, 4458225, 1712873,
],
file_ids: [
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
],
frame_ids: [
'fwIcP8qXDOl7k0VhWU8z9QAAAAACluVA',
'fwIcP8qXDOl7k0VhWU8z9QAAAAAEC8cp',
'fwIcP8qXDOl7k0VhWU8z9QAAAAAEC8i5',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM3Wu',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM3fZ',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM1lJ',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM1wB',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM2hL',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADM1lJ',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADIPmC',
'fwIcP8qXDOl7k0VhWU8z9QAAAAAEB-er',
'fwIcP8qXDOl7k0VhWU8z9QAAAAAEB_FE',
'fwIcP8qXDOl7k0VhWU8z9QAAAAAEB_SS',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADEhGk',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADEgR9',
'fwIcP8qXDOl7k0VhWU8z9QAAAAADEi2y',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACqKP1',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACm4cd',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACmsxk',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACmFnk',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACmHvn',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACkAzS',
'fwIcP8qXDOl7k0VhWU8z9QAAAAACkAzt',
'5JfXt00O17Yra2Rwh8HT8QAAAAAA4ACY',
'5JfXt00O17Yra2Rwh8HT8QAAAAAA2xGL',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOPVo',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOPTG',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOMny',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOMBc',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAPA8H',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAO3l5',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAPkYK',
'5JfXt00O17Yra2Rwh8HT8QAAAAAARAbx',
'5JfXt00O17Yra2Rwh8HT8QAAAAAAGiLp',
],
type_ids: [
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4,
],
annual_co2_tons: 0.0013627551116480942,
annual_costs_usd: 61.30240940376492,
count: 7,
},
},
stack_frames: {
'5JfXt00O17Yra2Rwh8HT8QAAAAAAO3l5': {
file_name: [],
function_name: ['m_show'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAA4ACY': {
file_name: [],
function_name: ['entry_SYSCALL_64_after_hwframe'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAARAbx': {
file_name: [],
function_name: ['kernfs_sop_show_path'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOMBc': {
file_name: [],
function_name: ['new_sync_read'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAPA8H': {
file_name: [],
function_name: ['seq_read_iter'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOPVo': {
file_name: [],
function_name: ['__x64_sys_read'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAPkYK': {
file_name: [],
function_name: ['show_mountinfo'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAGiLp': {
file_name: [],
function_name: ['cgroup_show_path'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOMny': {
file_name: [],
function_name: ['vfs_read'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAAOPTG': {
file_name: [],
function_name: ['ksys_read'],
function_offset: [],
line_number: [],
},
'5JfXt00O17Yra2Rwh8HT8QAAAAAA2xGL': {
file_name: [],
function_name: ['do_syscall_64'],
function_offset: [],
line_number: [],
},
},
executables: { '5JfXt00O17Yra2Rwh8HT8Q': 'vmlinux', fwIcP8qXDOl7k0VhWU8z9Q: 'metricbeat' },
stack_trace_events: { ['c2TovSbgCECd_RtKHxMtyQ']: 7 },
total_frames: 34,
sampling_rate: 1,
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { sum } from 'lodash';
import { createCalleeTree } from './callee';
import { decodeStackTraceResponse } from './stack_traces';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
describe('Callee operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {
const { events, stackTraces, stackFrames, executables, totalFrames, samplingRate } =
decodeStackTraceResponse(response);
const tree = createCalleeTree(
events,
stackTraces,
stackFrames,
executables,
totalFrames,
samplingRate
);
describe(`stacktraces from ${seconds} seconds and upsampled by ${upsampledBy}`, () => {
test('inclusive count of root to be less than or equal total sampled stacktraces', () => {
const totalAdjustedSamples = Math.ceil(sum([...events.values()]) / samplingRate);
expect(tree.CountInclusive[0]).toBeLessThanOrEqual(totalAdjustedSamples);
});
test('inclusive count for each node should be greater than or equal to its children', () => {
const allGreaterThanOrEqual = tree.Edges.map(
(children, i) =>
tree.CountInclusive[i] >= sum([...children.values()].map((j) => tree.CountInclusive[j]))
);
expect(allGreaterThanOrEqual).toBeTruthy();
});
test('exclusive count of root is zero', () => {
expect(tree.CountExclusive[0]).toEqual(0);
});
test('tree de-duplicates sibling nodes', () => {
expect(tree.Size).toBeLessThan(totalFrames);
});
});
});
});

View file

@ -1,201 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { sum } from 'lodash';
import { createFrameGroupID, FrameGroupID } from './frame_group';
import {
emptyExecutable,
emptyStackFrame,
emptyStackTrace,
Executable,
FileID,
StackFrame,
StackFrameID,
StackTrace,
StackTraceID,
} from './profiling';
type NodeID = number;
/**
* Callee tree
*/
export interface CalleeTree {
/** size */
Size: number;
/** edges */
Edges: Array<Map<FrameGroupID, NodeID>>;
/** file ids */
FileID: string[];
/** frame types */
FrameType: number[];
/** inlines */
Inline: boolean[];
/** executable file names */
ExeFilename: string[];
/** address or lines */
AddressOrLine: number[];
/** function names */
FunctionName: string[];
/** function offsets */
FunctionOffset: number[];
/** source file names */
SourceFilename: string[];
/** source lines */
SourceLine: number[];
/** total cpu */
CountInclusive: number[];
/** self cpu */
CountExclusive: number[];
TotalSamples: number;
TotalCPU: number;
SelfCPU: number;
}
/**
* Create a callee tree
* @param events Map<StackTraceID, number>
* @param stackTraces Map<StackTraceID, StackTrace>
* @param stackFrames Map<StackFrameID, StackFrame>
* @param executables Map<FileID, Executable>
* @param totalFrames number
* @param samplingRate number
* @returns
*/
export function createCalleeTree(
events: Map<StackTraceID, number>,
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>,
totalFrames: number,
samplingRate: number
): CalleeTree {
const tree: CalleeTree = {
Size: 1,
Edges: new Array(totalFrames),
FileID: new Array(totalFrames),
FrameType: new Array(totalFrames),
Inline: new Array(totalFrames),
ExeFilename: new Array(totalFrames),
AddressOrLine: new Array(totalFrames),
FunctionName: new Array(totalFrames),
FunctionOffset: new Array(totalFrames),
SourceFilename: new Array(totalFrames),
SourceLine: new Array(totalFrames),
CountInclusive: new Array(totalFrames),
CountExclusive: new Array(totalFrames),
TotalSamples: 0,
SelfCPU: 0,
TotalCPU: 0,
};
// The inverse of the sampling rate is the number with which to multiply the number of
// samples to get an estimate of the actual number of samples the backend received.
const scalingFactor = 1.0 / samplingRate;
let totalSamples = 0;
tree.Edges[0] = new Map<FrameGroupID, NodeID>();
tree.FileID[0] = '';
tree.FrameType[0] = 0;
tree.Inline[0] = false;
tree.ExeFilename[0] = '';
tree.AddressOrLine[0] = 0;
tree.FunctionName[0] = '';
tree.FunctionOffset[0] = 0;
tree.SourceFilename[0] = '';
tree.SourceLine[0] = 0;
tree.CountInclusive[0] = 0;
tree.CountExclusive[0] = 0;
const sortedStackTraceIDs = new Array<StackTraceID>();
for (const trace of stackTraces.keys()) {
sortedStackTraceIDs.push(trace);
}
sortedStackTraceIDs.sort((t1, t2) => {
return t1.localeCompare(t2);
});
// Walk through all traces. Increment the count of the root by the count of
// that trace. Walk "down" the trace (through the callees) and add the count
// of the trace to each callee.
for (const stackTraceID of sortedStackTraceIDs) {
// The slice of frames is ordered so that the leaf function is at the
// highest index.
// It is possible that we do not have a stacktrace for an event,
// e.g. when stopping the host agent or on network errors.
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
const lenStackTrace = stackTrace.FrameIDs.length;
const samples = Math.floor((events.get(stackTraceID) ?? 0) * scalingFactor);
totalSamples += samples;
let currentNode = 0;
// Increment the count by the number of samples observed, multiplied with the inverse of the
// samplingrate (this essentially means scaling up the total samples). It would incur
tree.CountInclusive[currentNode] += samples;
tree.CountExclusive[currentNode] = 0;
for (let i = 0; i < lenStackTrace; i++) {
const frameID = stackTrace.FrameIDs[i];
const fileID = stackTrace.FileIDs[i];
const addressOrLine = stackTrace.AddressOrLines[i];
const frame = stackFrames.get(frameID) ?? emptyStackFrame;
const executable = executables.get(fileID) ?? emptyExecutable;
const frameGroupID = createFrameGroupID(
fileID,
addressOrLine,
executable.FileName,
frame.FileName,
frame.FunctionName
);
let node = tree.Edges[currentNode].get(frameGroupID);
if (node === undefined) {
node = tree.Size;
tree.FileID[node] = fileID;
tree.FrameType[node] = stackTrace.Types[i];
tree.ExeFilename[node] = executable.FileName;
tree.AddressOrLine[node] = addressOrLine;
tree.FunctionName[node] = frame.FunctionName;
tree.FunctionOffset[node] = frame.FunctionOffset;
tree.SourceLine[node] = frame.LineNumber;
tree.SourceFilename[node] = frame.FileName;
tree.Inline[node] = frame.Inline;
tree.CountInclusive[node] = samples;
tree.CountExclusive[node] = 0;
tree.Edges[currentNode].set(frameGroupID, node);
tree.Edges[node] = new Map<FrameGroupID, NodeID>();
tree.Size++;
} else {
tree.CountInclusive[node] += samples;
}
if (i === lenStackTrace - 1) {
// Leaf frame: sum up counts for exclusive CPU.
tree.CountExclusive[node] += samples;
}
currentNode = node;
}
}
const sumSelfCPU = sum(tree.CountExclusive);
const sumTotalCPU = sum(tree.CountInclusive);
return {
...tree,
TotalSamples: totalSamples,
SelfCPU: sumSelfCPU,
TotalCPU: sumTotalCPU,
};
}

View file

@ -6,49 +6,32 @@
* Side Public License, v 1.
*/
import { createCalleeTree } from './callee';
import { createBaseFlameGraph, createFlameGraph } from './flamegraph';
import { decodeStackTraceResponse } from './stack_traces';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
import { createFlameGraph } from './flamegraph';
import { baseFlamegraph } from './__fixtures__/base_flamegraph';
describe('Flamegraph operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {
const { events, stackTraces, stackFrames, executables, totalFrames, samplingRate } =
decodeStackTraceResponse(response);
const tree = createCalleeTree(
events,
stackTraces,
stackFrames,
executables,
totalFrames,
samplingRate
);
const baseFlamegraph = createBaseFlameGraph(tree, samplingRate, seconds);
const flamegraph = createFlameGraph(baseFlamegraph);
describe('Flamegraph', () => {
const flamegraph = createFlameGraph(baseFlamegraph);
describe(`stacktraces from ${seconds} seconds and upsampled by ${upsampledBy}`, () => {
test('base flamegraph has non-zero total seconds', () => {
expect(baseFlamegraph.TotalSeconds).toEqual(seconds);
});
it('base flamegraph has non-zero total seconds', () => {
expect(baseFlamegraph.TotalSeconds).toEqual(4.980000019073486);
});
test('base flamegraph has one more node than the number of edges', () => {
const numEdges = baseFlamegraph.Edges.flatMap((edge) => edge).length;
it('base flamegraph has one more node than the number of edges', () => {
const numEdges = baseFlamegraph.Edges.flatMap((edge) => edge).length;
expect(numEdges).toEqual(baseFlamegraph.Size - 1);
});
expect(numEdges).toEqual(baseFlamegraph.Size - 1);
});
test('all flamegraph IDs are the same non-zero length', () => {
// 16 is the length of a 64-bit FNV-1a hash encoded to a hex string
const allSameLengthIDs = flamegraph.ID.every((id) => id.length === 16);
it('all flamegraph IDs are the same non-zero length', () => {
// 16 is the length of a 64-bit FNV-1a hash encoded to a hex string
const allSameLengthIDs = flamegraph.ID.every((id) => id.length === 16);
expect(allSameLengthIDs).toBeTruthy();
});
expect(allSameLengthIDs).toBeTruthy();
});
test('all flamegraph labels are non-empty', () => {
const allNonEmptyLabels = flamegraph.Label.every((id) => id.length > 0);
it('all flamegraph labels are non-empty', () => {
const allNonEmptyLabels = flamegraph.Label.every((id) => id.length > 0);
expect(allNonEmptyLabels).toBeTruthy();
});
});
expect(allNonEmptyLabels).toBeTruthy();
});
});

View file

@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
import { CalleeTree } from './callee';
import { createFrameGroupID } from './frame_group';
import { fnv1a64 } from './hash';
import { createStackFrameMetadata, getCalleeLabel } from './profiling';
import { convertTonsToKgs } from './utils';
/**
* Base Flamegraph
@ -48,63 +48,37 @@ export interface BaseFlameGraph {
TotalSamples: number;
TotalCPU: number;
SelfCPU: number;
}
/**
* createBaseFlameGraph encapsulates the tree representation into a serialized form.
* @param tree CalleeTree
* @param samplingRate number
* @param totalSeconds number
* @returns BaseFlameGraph
*/
export function createBaseFlameGraph(
tree: CalleeTree,
samplingRate: number,
totalSeconds: number
): BaseFlameGraph {
const graph: BaseFlameGraph = {
Size: tree.Size,
SamplingRate: samplingRate,
Edges: new Array<number[]>(tree.Size),
FileID: tree.FileID.slice(0, tree.Size),
FrameType: tree.FrameType.slice(0, tree.Size),
Inline: tree.Inline.slice(0, tree.Size),
ExeFilename: tree.ExeFilename.slice(0, tree.Size),
AddressOrLine: tree.AddressOrLine.slice(0, tree.Size),
FunctionName: tree.FunctionName.slice(0, tree.Size),
FunctionOffset: tree.FunctionOffset.slice(0, tree.Size),
SourceFilename: tree.SourceFilename.slice(0, tree.Size),
SourceLine: tree.SourceLine.slice(0, tree.Size),
CountInclusive: tree.CountInclusive.slice(0, tree.Size),
CountExclusive: tree.CountExclusive.slice(0, tree.Size),
TotalSeconds: totalSeconds,
TotalSamples: tree.TotalSamples,
SelfCPU: tree.SelfCPU,
TotalCPU: tree.TotalCPU,
};
for (let i = 0; i < tree.Size; i++) {
let j = 0;
const nodes = new Array<number>(tree.Edges[i].size);
for (const [, n] of tree.Edges[i]) {
nodes[j] = n;
j++;
}
graph.Edges[i] = nodes;
}
return graph;
AnnualCO2TonsExclusive: number[];
AnnualCO2TonsInclusive: number[];
AnnualCostsUSDInclusive: number[];
AnnualCostsUSDExclusive: number[];
SelfAnnualCO2Tons: number;
TotalAnnualCO2Tons: number;
SelfAnnualCostsUSD: number;
TotalAnnualCostsUSD: number;
}
/** Elasticsearch flamegraph */
export interface ElasticFlameGraph extends BaseFlameGraph {
export interface ElasticFlameGraph
extends Omit<
BaseFlameGraph,
| 'AnnualCO2TonsExclusive'
| 'AnnualCO2TonsInclusive'
| 'SelfAnnualCO2Tons'
| 'TotalAnnualCO2Tons'
| 'AnnualCostsUSDInclusive'
| 'AnnualCostsUSDExclusive'
> {
/** ID */
ID: string[];
/** Label */
Label: string[];
SelfAnnualCO2KgsItems: number[];
TotalAnnualCO2KgsItems: number[];
SelfAnnualCostsUSDItems: number[];
TotalAnnualCostsUSDItems: number[];
SelfAnnualCO2Kgs: number;
TotalAnnualCO2Kgs: number;
}
/**
@ -141,6 +115,14 @@ export function createFlameGraph(base: BaseFlameGraph): ElasticFlameGraph {
TotalSamples: base.TotalSamples,
SelfCPU: base.SelfCPU,
TotalCPU: base.TotalCPU,
SelfAnnualCO2KgsItems: base.AnnualCO2TonsExclusive.map(convertTonsToKgs),
TotalAnnualCO2KgsItems: base.AnnualCO2TonsInclusive.map(convertTonsToKgs),
SelfAnnualCostsUSDItems: base.AnnualCostsUSDExclusive,
TotalAnnualCostsUSDItems: base.AnnualCostsUSDInclusive,
SelfAnnualCO2Kgs: convertTonsToKgs(base.SelfAnnualCO2Tons),
TotalAnnualCO2Kgs: convertTonsToKgs(base.TotalAnnualCO2Tons),
SelfAnnualCostsUSD: base.SelfAnnualCostsUSD,
TotalAnnualCostsUSD: base.TotalAnnualCostsUSD,
};
const rootFrameGroupID = createFrameGroupID(

View file

@ -9,49 +9,44 @@
import { sum } from 'lodash';
import { createTopNFunctions } from './functions';
import { decodeStackTraceResponse } from '..';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
import { stacktraces } from './__fixtures__/stacktraces';
describe('TopN function operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {
const { events, stackTraces, stackFrames, executables, samplingRate } =
decodeStackTraceResponse(response);
const { events, stackTraces, stackFrames, executables, samplingRate } =
decodeStackTraceResponse(stacktraces);
const maxTopN = 5;
const topNFunctions = createTopNFunctions({
events,
stackTraces,
stackFrames,
executables,
startIndex: 0,
endIndex: maxTopN,
samplingRate,
});
const exclusiveCounts = topNFunctions.TopN.map((value) => value.CountExclusive);
describe(`stacktraces upsampled by ${upsampledBy}`, () => {
const maxTopN = 5;
const topNFunctions = createTopNFunctions({
events,
stackTraces,
stackFrames,
executables,
startIndex: 0,
endIndex: maxTopN,
samplingRate,
});
const exclusiveCounts = topNFunctions.TopN.map((value) => value.CountExclusive);
it('samples are less than or equal to original upsampled samples', () => {
const totalUpsampledSamples = Math.ceil(sum([...events.values()]) / samplingRate);
expect(topNFunctions.TotalCount).toBeLessThanOrEqual(totalUpsampledSamples);
});
test('samples are less than or equal to original upsampled samples', () => {
const totalUpsampledSamples = Math.ceil(sum([...events.values()]) / samplingRate);
expect(topNFunctions.TotalCount).toBeLessThanOrEqual(totalUpsampledSamples);
});
it('number of functions is equal to maximum', () => {
expect(topNFunctions.TopN.length).toEqual(maxTopN);
});
test('number of functions is equal to maximum', () => {
expect(topNFunctions.TopN.length).toEqual(maxTopN);
});
it('all exclusive counts are numeric', () => {
expect(typeof exclusiveCounts[0]).toBe('number');
expect(typeof exclusiveCounts[1]).toBe('number');
expect(typeof exclusiveCounts[2]).toBe('number');
expect(typeof exclusiveCounts[3]).toBe('number');
expect(typeof exclusiveCounts[4]).toBe('number');
});
test('all exclusive counts are numeric', () => {
expect(typeof exclusiveCounts[0]).toBe('number');
expect(typeof exclusiveCounts[1]).toBe('number');
expect(typeof exclusiveCounts[2]).toBe('number');
expect(typeof exclusiveCounts[3]).toBe('number');
expect(typeof exclusiveCounts[4]).toBe('number');
});
test('exclusive counts are sorted from highest to lowest', () => {
expect(exclusiveCounts[0]).toBeGreaterThanOrEqual(exclusiveCounts[1]);
expect(exclusiveCounts[1]).toBeGreaterThanOrEqual(exclusiveCounts[2]);
expect(exclusiveCounts[2]).toBeGreaterThanOrEqual(exclusiveCounts[3]);
expect(exclusiveCounts[3]).toBeGreaterThanOrEqual(exclusiveCounts[4]);
});
});
it('exclusive counts are sorted from highest to lowest', () => {
expect(exclusiveCounts[0]).toBeGreaterThanOrEqual(exclusiveCounts[1]);
expect(exclusiveCounts[1]).toBeGreaterThanOrEqual(exclusiveCounts[2]);
expect(exclusiveCounts[2]).toBeGreaterThanOrEqual(exclusiveCounts[3]);
expect(exclusiveCounts[3]).toBeGreaterThanOrEqual(exclusiveCounts[4]);
});
});

View file

@ -30,11 +30,21 @@ interface TopNFunctionAndFrameGroup {
FrameGroupID: FrameGroupID;
CountExclusive: number;
CountInclusive: number;
selfAnnualCO2kgs: number;
selfAnnualCostUSD: number;
totalAnnualCO2kgs: number;
totalAnnualCostUSD: number;
}
type TopNFunction = Pick<
TopNFunctionAndFrameGroup,
'Frame' | 'CountExclusive' | 'CountInclusive'
| 'Frame'
| 'CountExclusive'
| 'CountInclusive'
| 'selfAnnualCO2kgs'
| 'selfAnnualCostUSD'
| 'totalAnnualCO2kgs'
| 'totalAnnualCostUSD'
> & {
Id: string;
Rank: number;
@ -46,6 +56,8 @@ export interface TopNFunctions {
SamplingRate: number;
selfCPU: number;
totalCPU: number;
totalAnnualCO2Kgs: number;
totalAnnualCostUSD: number;
}
export function createTopNFunctions({
@ -84,6 +96,9 @@ export function createTopNFunctions({
// It is possible that we do not have a stacktrace for an event,
// e.g. when stopping the host agent or on network errors.
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
const selfAnnualCO2kgs = stackTrace.selfAnnualCO2Kgs;
const selfAnnualCostUSD = stackTrace.selfAnnualCostUSD;
const lenStackTrace = stackTrace.FrameIDs.length;
for (let i = 0; i < lenStackTrace; i++) {
@ -122,6 +137,10 @@ export function createTopNFunctions({
FrameGroupID: frameGroupID,
CountExclusive: 0,
CountInclusive: 0,
selfAnnualCO2kgs: 0,
totalAnnualCO2kgs: 0,
selfAnnualCostUSD: 0,
totalAnnualCostUSD: 0,
};
topNFunctions.set(frameGroupID, topNFunction);
@ -130,11 +149,15 @@ export function createTopNFunctions({
if (!uniqueFrameGroupsPerEvent.has(frameGroupID)) {
uniqueFrameGroupsPerEvent.add(frameGroupID);
topNFunction.CountInclusive += scaledCount;
topNFunction.totalAnnualCO2kgs += selfAnnualCO2kgs;
topNFunction.totalAnnualCostUSD += selfAnnualCostUSD;
}
if (i === lenStackTrace - 1) {
// Leaf frame: sum up counts for exclusive CPU.
topNFunction.CountExclusive += scaledCount;
topNFunction.selfAnnualCO2kgs += selfAnnualCO2kgs;
topNFunction.selfAnnualCostUSD += selfAnnualCostUSD;
}
}
}
@ -161,21 +184,29 @@ export function createTopNFunctions({
endIndex = topN.length;
}
const framesAndCountsAndIds = topN.slice(startIndex, endIndex).map((frameAndCount, i) => {
const countExclusive = frameAndCount.CountExclusive;
const countInclusive = frameAndCount.CountInclusive;
const framesAndCountsAndIds = topN
.slice(startIndex, endIndex)
.map((frameAndCount, i): TopNFunction => {
const countExclusive = frameAndCount.CountExclusive;
const countInclusive = frameAndCount.CountInclusive;
return {
Rank: i + 1,
Frame: frameAndCount.Frame,
CountExclusive: countExclusive,
CountInclusive: countInclusive,
Id: frameAndCount.FrameGroupID,
};
});
return {
Rank: i + 1,
Frame: frameAndCount.Frame,
CountExclusive: countExclusive,
CountInclusive: countInclusive,
Id: frameAndCount.FrameGroupID,
selfAnnualCO2kgs: frameAndCount.selfAnnualCO2kgs,
selfAnnualCostUSD: frameAndCount.selfAnnualCostUSD,
totalAnnualCO2kgs: frameAndCount.totalAnnualCO2kgs,
totalAnnualCostUSD: frameAndCount.totalAnnualCostUSD,
};
});
const sumSelfCPU = sumBy(framesAndCountsAndIds, 'CountExclusive');
const sumTotalCPU = sumBy(framesAndCountsAndIds, 'CountInclusive');
const totalAnnualCO2Kgs = sumBy(framesAndCountsAndIds, 'totalAnnualCO2kgs');
const totalAnnualCostUSD = sumBy(framesAndCountsAndIds, 'totalAnnualCostUSD');
return {
TotalCount: totalCount,
@ -183,6 +214,8 @@ export function createTopNFunctions({
SamplingRate: samplingRate,
selfCPU: sumSelfCPU,
totalCPU: sumTotalCPU,
totalAnnualCO2Kgs,
totalAnnualCostUSD,
};
}

View file

@ -74,6 +74,9 @@ export interface StackTrace {
AddressOrLines: number[];
/** types */
Types: number[];
selfAnnualCO2Kgs: number;
selfAnnualCostUSD: number;
Count: number;
}
/**
* Empty stack trace
@ -87,6 +90,9 @@ export const emptyStackTrace: StackTrace = {
AddressOrLines: [],
/** Types */
Types: [],
selfAnnualCO2Kgs: 0,
selfAnnualCostUSD: 0,
Count: 0,
};
/** Stack frame */

View file

@ -58,6 +58,9 @@ describe('Stack trace response operations', () => {
frame_ids: ['abc123', 'def456'],
address_or_lines: [123, 456],
type_ids: [0, 1],
count: 3,
annual_co2_tons: 1,
annual_costs_usd: 1,
},
},
stack_frames: {
@ -92,6 +95,9 @@ describe('Stack trace response operations', () => {
FrameIDs: ['abc123', makeFrameID('def456', 0), makeFrameID('def456', 1)],
AddressOrLines: [123, 456, 456],
Types: [0, 1, 1],
Count: 3,
selfAnnualCO2Kgs: 1,
selfAnnualCostUSD: 1,
},
],
]),
@ -165,6 +171,9 @@ describe('Stack trace response operations', () => {
frame_ids: ['abc123'],
address_or_lines: [123],
type_ids: [0],
count: 3,
annual_co2_tons: 1,
annual_costs_usd: 1,
},
},
stack_frames: {
@ -191,6 +200,9 @@ describe('Stack trace response operations', () => {
FrameIDs: ['abc123'],
AddressOrLines: [123],
Types: [0],
Count: 3,
selfAnnualCO2Kgs: 1,
selfAnnualCostUSD: 1,
},
],
]),

View file

@ -15,6 +15,7 @@ import {
StackTrace,
StackTraceID,
} from './profiling';
import { convertTonsToKgs } from './utils';
/** Profiling status response */
export interface ProfilingStatusResponse {
@ -42,6 +43,9 @@ export interface ProfilingStackTrace {
['frame_ids']: string[];
['address_or_lines']: number[];
['type_ids']: number[];
['annual_co2_tons']: number;
['annual_costs_usd']: number;
count: number;
}
interface ProfilingStackTraces {
@ -140,7 +144,10 @@ const createInlineTrace = (
FileIDs: fileIDs,
AddressOrLines: addressOrLines,
Types: typeIDs,
} as StackTrace;
selfAnnualCO2Kgs: convertTonsToKgs(trace.annual_co2_tons),
selfAnnualCostUSD: trace.annual_costs_usd,
Count: trace.count,
};
};
/**

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const convertTonsToKgs = (value: number) => value * 1000;

View file

@ -7,8 +7,7 @@
*/
export { decodeStackTraceResponse } from './common/stack_traces';
export { createBaseFlameGraph, createFlameGraph } from './common/flamegraph';
export { createCalleeTree } from './common/callee';
export { createFlameGraph } from './common/flamegraph';
export { ProfilingESField } from './common/elasticsearch';
export {
groupStackFrameMetadataByStackTrace,
@ -33,8 +32,8 @@ export {
TopNComparisonFunctionSortField,
topNComparisonFunctionSortFieldRt,
} from './common/functions';
export { convertTonsToKgs } from './common/utils';
export type { CalleeTree } from './common/callee';
export type {
ProfilingStatusResponse,
StackTraceResponse,

View file

@ -553,7 +553,11 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingPerCoreWatt': {
'observability:profilingPerVCPUWattX86': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingPervCPUWattArm64': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
@ -589,8 +593,16 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingUseLegacyFlamegraphAPI': {
'observability:profilingUseLegacyCo2Calculation': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingCostPervCPUPerHour': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingAWSCostDiscountRate': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
};

View file

@ -154,8 +154,11 @@ export interface UsageStats {
'securitySolution:enableGroupedNav': boolean;
'securitySolution:showRelatedIntegrations': boolean;
'visualization:visualize:legacyGaugeChartsLibrary': boolean;
'observability:profilingUseLegacyFlamegraphAPI': boolean;
'observability:profilingPerCoreWatt': number;
'observability:profilingPerVCPUWattX86': number;
'observability:profilingPervCPUWattArm64': number;
'observability:profilingCo2PerKWH': number;
'observability:profilingDatacenterPUE': number;
'observability:profilingUseLegacyCo2Calculation': boolean;
'observability:profilingCostPervCPUPerHour': number;
'observability:profilingAWSCostDiscountRate': number;
}

View file

@ -10019,7 +10019,13 @@
"description": "Non-default value of setting."
}
},
"observability:profilingPerCoreWatt": {
"observability:profilingPerVCPUWattX86": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingPervCPUWattArm64": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
@ -10073,11 +10079,23 @@
"description": "Non-default value of setting."
}
},
"observability:profilingUseLegacyFlamegraphAPI": {
"observability:profilingUseLegacyCo2Calculation": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingCostPervCPUPerHour": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingAWSCostDiscountRate": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
}
}
},

View file

@ -8,7 +8,6 @@
import { toNumberRt } from '@kbn/io-ts-utils';
import type { BaseFlameGraph, TopNFunctions } from '@kbn/profiling-utils';
import * as t from 'io-ts';
import { profilingUseLegacyFlamegraphAPI } from '@kbn/observability-plugin/common';
import { HOST_NAME } from '../../../common/es_fields/apm';
import {
mergeKueries,
@ -42,13 +41,10 @@ const profilingFlamegraphRoute = createApmServerRoute({
{ flamegraph: BaseFlameGraph; hostNames: string[] } | undefined
> => {
const { context, plugins, params } = resources;
const useLegacyFlamegraphAPI = await (
await context.core
).uiSettings.client.get<boolean>(profilingUseLegacyFlamegraphAPI);
const core = await context.core;
const [esClient, apmEventClient, profilingDataAccessStart] =
await Promise.all([
(await context.core).elasticsearch.client,
core.elasticsearch.client,
await getApmEventClient(resources),
await plugins.profilingDataAccess?.start(),
]);
@ -73,6 +69,7 @@ const profilingFlamegraphRoute = createApmServerRoute({
const flamegraph =
await profilingDataAccessStart?.services.fetchFlamechartData({
core,
esClient: esClient.asCurrentUser,
rangeFromMs: start,
rangeToMs: end,
@ -80,7 +77,6 @@ const profilingFlamegraphRoute = createApmServerRoute({
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
kuery,
]),
useLegacyFlamegraphAPI,
});
return { flamegraph, hostNames: serviceHostNames };
@ -107,9 +103,10 @@ const profilingFunctionsRoute = createApmServerRoute({
resources
): Promise<{ functions: TopNFunctions; hostNames: string[] } | undefined> => {
const { context, plugins, params } = resources;
const core = await context.core;
const [esClient, apmEventClient, profilingDataAccessStart] =
await Promise.all([
(await context.core).elasticsearch.client,
core.elasticsearch.client,
await getApmEventClient(resources),
await plugins.profilingDataAccess?.start(),
]);
@ -141,6 +138,7 @@ const profilingFunctionsRoute = createApmServerRoute({
}
const functions = await profilingDataAccessStart?.services.fetchFunction({
core,
esClient: esClient.asCurrentUser,
rangeFromMs: start,
rangeToMs: end,

View file

@ -8,24 +8,19 @@
import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { ProfilingDataAccessPluginStart } from '@kbn/profiling-data-access-plugin/server';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { profilingUseLegacyFlamegraphAPI } from '@kbn/observability-plugin/common';
import type { InfraProfilingFlamegraphRequestParams } from '../../../../common/http_api/profiling_api';
import { HOST_FIELD } from '../../../../common/constants';
import type { InfraProfilingFlamegraphRequestParams } from '../../../../common/http_api/profiling_api';
export async function fetchProfilingFlamegraph(
{ hostname, from, to }: InfraProfilingFlamegraphRequestParams,
profilingDataAccess: ProfilingDataAccessPluginStart,
coreRequestContext: CoreRequestHandlerContext
): Promise<BaseFlameGraph> {
const useLegacyFlamegraphAPI = await coreRequestContext.uiSettings.client.get<boolean>(
profilingUseLegacyFlamegraphAPI
);
return await profilingDataAccess.services.fetchFlamechartData({
core: coreRequestContext,
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
rangeFromMs: from,
rangeToMs: to,
kuery: `${HOST_FIELD} : "${hostname}"`,
useLegacyFlamegraphAPI,
});
}

View file

@ -19,6 +19,7 @@ export async function fetchProfilingFunctions(
const { hostname, from, to, startIndex, endIndex } = params;
return await profilingDataAccess.services.fetchFunction({
core: coreRequestContext,
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
rangeFromMs: from,
rangeToMs: to,

View file

@ -41,10 +41,13 @@ export {
enableCriticalPath,
syntheticsThrottlingEnabled,
apmEnableProfilingIntegration,
profilingUseLegacyFlamegraphAPI,
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
profilingUseLegacyCo2Calculation,
profilingPervCPUWattArm64,
profilingAWSCostDiscountRate,
profilingCostPervCPUPerHour,
} from './ui_settings_keys';
export {

View file

@ -27,7 +27,10 @@ export const apmEnableContinuousRollups = 'observability:apmEnableContinuousRoll
export const syntheticsThrottlingEnabled = 'observability:syntheticsThrottlingEnabled';
export const enableLegacyUptimeApp = 'observability:enableLegacyUptimeApp';
export const apmEnableProfilingIntegration = 'observability:apmEnableProfilingIntegration';
export const profilingUseLegacyFlamegraphAPI = 'observability:profilingUseLegacyFlamegraphAPI';
export const profilingPerCoreWatt = 'observability:profilingPerCoreWatt';
export const profilingPervCPUWattX86 = 'observability:profilingPerVCPUWattX86';
export const profilingPervCPUWattArm64 = 'observability:profilingPervCPUWattArm64';
export const profilingCo2PerKWH = 'observability:profilingCo2PerKWH';
export const profilingDatacenterPUE = 'observability:profilingDatacenterPUE';
export const profilingUseLegacyCo2Calculation = 'observability:profilingUseLegacyCo2Calculation';
export const profilingAWSCostDiscountRate = 'observability:profilingAWSCostDiscountRate';
export const profilingCostPervCPUPerHour = 'observability:profilingCostPervCPUPerHour';

View file

@ -30,10 +30,13 @@ import {
syntheticsThrottlingEnabled,
enableLegacyUptimeApp,
apmEnableProfilingIntegration,
profilingUseLegacyFlamegraphAPI,
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
profilingUseLegacyCo2Calculation,
profilingPervCPUWattArm64,
profilingAWSCostDiscountRate,
profilingCostPervCPUPerHour,
} from '../common/ui_settings_keys';
const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', {
@ -378,26 +381,33 @@ export const uiSettings: Record<string, UiSettings> = {
schema: schema.boolean(),
requiresPageReload: false,
},
[profilingUseLegacyFlamegraphAPI]: {
[profilingPervCPUWattX86]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingUseLegacyFlamegraphAPI', {
defaultMessage: 'Use legacy Flamegraph API in Universal Profiling',
}),
value: false,
schema: schema.boolean(),
},
[profilingPerCoreWatt]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingName', {
defaultMessage: 'Per Core Watts',
name: i18n.translate('xpack.observability.profilingPervCPUWattX86UiSettingName', {
defaultMessage: 'Per vCPU Watts - x86',
}),
value: 7,
description: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingDescription', {
defaultMessage: `The average amortized per-core power consumption (based on 100% CPU utilization).`,
description: i18n.translate('xpack.observability.profilingPervCPUWattX86UiSettingDescription', {
defaultMessage: `The average amortized per-core power consumption (based on 100% CPU utilization) for x86 architecture.`,
}),
schema: schema.number({ min: 0 }),
requiresPageReload: true,
},
[profilingPervCPUWattArm64]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingPervCPUWattArm64UiSettingName', {
defaultMessage: 'Per vCPU Watts - arm64',
}),
value: 2.8,
description: i18n.translate(
'xpack.observability.profilingPervCPUWattArm64UiSettingDescription',
{
defaultMessage: `The average amortized per-core power consumption (based on 100% CPU utilization) for arm64 architecture.`,
}
),
schema: schema.number({ min: 0 }),
requiresPageReload: true,
},
[profilingDatacenterPUE]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingName', {
@ -450,6 +460,45 @@ export const uiSettings: Record<string, UiSettings> = {
schema: schema.number({ min: 0 }),
requiresPageReload: true,
},
[profilingUseLegacyCo2Calculation]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingUseLegacyCo2Calculation', {
defaultMessage: 'Use legacy CO2 and Dollar cost calculations in Universal Profiling',
}),
value: false,
schema: schema.boolean(),
},
[profilingAWSCostDiscountRate]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingAWSCostDiscountRateUiSettingName', {
defaultMessage: 'AWS EDP discount rate (%)',
}),
value: 6,
schema: schema.number({ min: 0, max: 100 }),
requiresPageReload: true,
description: i18n.translate(
'xpack.observability.profilingAWSCostDiscountRateUiSettingDescription',
{
defaultMessage:
"If you're enrolled in the AWS Enterprise Discount Program (EDP), enter your discount rate to update the profiling cost calculation.",
}
),
},
[profilingCostPervCPUPerHour]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingCostPervCPUPerHourUiSettingName', {
defaultMessage: 'Cost per vCPU per hour ($)',
}),
value: 0.0425,
description: i18n.translate(
'xpack.observability.profilingCostPervCPUPerHourUiSettingNameDescription',
{
defaultMessage: `Default average cost per CPU core per hour (Non-AWS instances only)`,
}
),
schema: schema.number({ min: 0, max: 100 }),
requiresPageReload: true,
},
};
function throttlingDocsLink({ href }: { href: string }) {

View file

@ -1,17 +0,0 @@
The stacktrace fixtures in this directory are originally from Elasticsearch's
`POST /_profiling/stacktraces` endpoint. They were subsequently filtered
through the `shrink_stacktrace_response.js` command in `x-pack/plugins/profiling/scripts/`
to reduce the size without losing sampling fidelity (see the script for further
details).
The naming convention for each stacktrace fixture follows this pattern:
```
stacktraces_{seconds}s_{upsampling rate}x.json
```
where `seconds` is the time span of the original query and `upsampling rate` is
the reciprocal of the sampling rate returned from the original query.
To add a new stacktrace fixture to the test suite, update `stacktraces.ts`
appropriately.

View file

@ -0,0 +1,297 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BaseFlameGraph } from '@kbn/profiling-utils';
export const baseFlamegraph: BaseFlameGraph = {
Edges: [
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9],
[10],
[11],
[12],
[13],
[14],
[15],
[16],
[17],
[18],
[19],
[20],
[21],
[22],
[23],
[24],
[25],
[26],
[27],
[28],
[29],
[30],
[31],
[32],
[33],
[34],
[],
],
FileID: [
'',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'fwIcP8qXDOl7k0VhWU8z9Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
'5JfXt00O17Yra2Rwh8HT8Q',
],
FrameType: [
0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4,
],
Inline: [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
],
ExeFilename: [
'',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'metricbeat',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
'vmlinux',
],
AddressOrLine: [
0, 43443520, 67880745, 67881145, 53704110, 53704665, 53696841, 53697537, 53700683, 53696841,
52492674, 67626923, 67629380, 67630226, 51515812, 51512445, 51522994, 44606453, 43747101,
43699300, 43538916, 43547623, 42994898, 42994925, 14680216, 14356875, 3732840, 3732678, 3721714,
3719260, 3936007, 3897721, 4081162, 4458225, 1712873,
],
FunctionName: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'entry_SYSCALL_64_after_hwframe',
'do_syscall_64',
'__x64_sys_read',
'ksys_read',
'vfs_read',
'new_sync_read',
'seq_read_iter',
'm_show',
'show_mountinfo',
'kernfs_sop_show_path',
'cgroup_show_path',
],
FunctionOffset: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
],
SourceFilename: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
SourceLine: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
],
CountInclusive: [
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7,
],
CountExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 7,
],
AnnualCO2TonsInclusive: [
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
0.0013627551116480942, 0.0013627551116480942, 0.0013627551116480942,
],
AnnualCO2TonsExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0.0013627551116480942,
],
AnnualCostsUSDInclusive: [
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492, 61.30240940376492,
],
AnnualCostsUSDExclusive: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 61.30240940376492,
],
Size: 35,
SamplingRate: 1,
SelfCPU: 7,
TotalCPU: 245,
SelfAnnualCO2Tons: 0.0013627551116480942,
TotalAnnualCO2Tons: 0.04769642890768329,
SelfAnnualCostsUSD: 61.30240940376492,
TotalAnnualCostsUSD: 2145.5843291317715,
TotalSamples: 7,
TotalSeconds: 4.980000019073486,
};

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { StackTraceResponse } from '@kbn/profiling-utils';
import stackTraces1x from './stacktraces_60s_1x.json';
import stackTraces5x from './stacktraces_3600s_5x.json';
import stackTraces125x from './stacktraces_86400s_125x.json';
import stackTraces625x from './stacktraces_604800s_625x.json';
export const stackTraceFixtures: Array<{
response: StackTraceResponse;
seconds: number;
upsampledBy: number;
}> = [
{ response: stackTraces1x, seconds: 60, upsampledBy: 1 },
{ response: stackTraces5x, seconds: 3600, upsampledBy: 5 },
{ response: stackTraces125x, seconds: 86400, upsampledBy: 125 },
{ response: stackTraces625x, seconds: 604800, upsampledBy: 625 },
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,110 +5,91 @@
* 2.0.
*/
import {
createBaseFlameGraph,
createCalleeTree,
createFlameGraph,
decodeStackTraceResponse,
} from '@kbn/profiling-utils';
import { createFlameGraph } from '@kbn/profiling-utils';
import { sum } from 'lodash';
import { createColumnarViewModel } from './columnar_view_model';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
import { baseFlamegraph } from './__fixtures__/base_flamegraph';
describe('Columnar view model operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {
const { events, stackTraces, stackFrames, executables, totalFrames, samplingRate } =
decodeStackTraceResponse(response);
const tree = createCalleeTree(
events,
stackTraces,
stackFrames,
executables,
totalFrames,
samplingRate
);
const graph = createFlameGraph(createBaseFlameGraph(tree, samplingRate, seconds));
const graph = createFlameGraph(baseFlamegraph);
describe(`stacktraces from ${seconds} seconds and upsampled by ${upsampledBy}`, () => {
describe('color values are generated by default', () => {
const viewModel = createColumnarViewModel(graph);
describe('color values are generated by default', () => {
const viewModel = createColumnarViewModel(graph);
test('length of colors is equal to length of labels multipled by 4', () => {
expect(viewModel.color.length).toEqual(viewModel.label.length * 4);
});
it('length of colors is equal to length of labels multipled by 4', () => {
expect(viewModel.color.length).toEqual(viewModel.label.length * 4);
});
test('length of position0 is equal to length of labels multipled by 2', () => {
expect(viewModel.position0.length).toEqual(viewModel.label.length * 2);
});
it('length of position0 is equal to length of labels multipled by 2', () => {
expect(viewModel.position0.length).toEqual(viewModel.label.length * 2);
});
test('length of position1 is equal to length of labels multipled by 2', () => {
expect(viewModel.position1.length).toEqual(viewModel.label.length * 2);
});
it('length of position1 is equal to length of labels multipled by 2', () => {
expect(viewModel.position1.length).toEqual(viewModel.label.length * 2);
});
test('length of size0 is equal to length of labels', () => {
expect(viewModel.size0.length).toEqual(viewModel.label.length);
});
it('length of size0 is equal to length of labels', () => {
expect(viewModel.size0.length).toEqual(viewModel.label.length);
});
test('length of size1 is equal to length of labels', () => {
expect(viewModel.size1.length).toEqual(viewModel.label.length);
});
it('length of size1 is equal to length of labels', () => {
expect(viewModel.size1.length).toEqual(viewModel.label.length);
});
test('length of values is equal to length of labels', () => {
expect(viewModel.value.length).toEqual(viewModel.label.length);
});
it('length of values is equal to length of labels', () => {
expect(viewModel.value.length).toEqual(viewModel.label.length);
});
test('both position arrays are equal', () => {
expect(viewModel.position0).toEqual(viewModel.position1);
});
it('both position arrays are equal', () => {
expect(viewModel.position0).toEqual(viewModel.position1);
});
test('both size arrays are equal', () => {
expect(viewModel.size0).toEqual(viewModel.size1);
});
it('both size arrays are equal', () => {
expect(viewModel.size0).toEqual(viewModel.size1);
});
test('sum of colors is greater than zero', () => {
expect(sum(viewModel.color)).toBeGreaterThan(0);
});
});
it('sum of colors is greater than zero', () => {
expect(sum(viewModel.color)).toBeGreaterThan(0);
});
});
describe('color values are not generated when disabled', () => {
const viewModel = createColumnarViewModel(graph, false);
describe('color values are not generated when disabled', () => {
const viewModel = createColumnarViewModel(graph, false);
test('length of colors is equal to length of labels multipled by 4', () => {
expect(viewModel.color.length).toEqual(viewModel.label.length * 4);
});
it('length of colors is equal to length of labels multipled by 4', () => {
expect(viewModel.color.length).toEqual(viewModel.label.length * 4);
});
test('length of position0 is equal to length of labels multipled by 2', () => {
expect(viewModel.position0.length).toEqual(viewModel.label.length * 2);
});
it('length of position0 is equal to length of labels multipled by 2', () => {
expect(viewModel.position0.length).toEqual(viewModel.label.length * 2);
});
test('length of position1 is equal to length of labels multipled by 2', () => {
expect(viewModel.position1.length).toEqual(viewModel.label.length * 2);
});
it('length of position1 is equal to length of labels multipled by 2', () => {
expect(viewModel.position1.length).toEqual(viewModel.label.length * 2);
});
test('length of size0 is equal to length of labels', () => {
expect(viewModel.size0.length).toEqual(viewModel.label.length);
});
it('length of size0 is equal to length of labels', () => {
expect(viewModel.size0.length).toEqual(viewModel.label.length);
});
test('length of size1 is equal to length of labels', () => {
expect(viewModel.size1.length).toEqual(viewModel.label.length);
});
it('length of size1 is equal to length of labels', () => {
expect(viewModel.size1.length).toEqual(viewModel.label.length);
});
test('length of values is equal to length of labels', () => {
expect(viewModel.value.length).toEqual(viewModel.label.length);
});
it('length of values is equal to length of labels', () => {
expect(viewModel.value.length).toEqual(viewModel.label.length);
});
test('both position arrays are equal', () => {
expect(viewModel.position0).toEqual(viewModel.position1);
});
it('both position arrays are equal', () => {
expect(viewModel.position0).toEqual(viewModel.position1);
});
test('both size arrays are equal', () => {
expect(viewModel.size0).toEqual(viewModel.size1);
});
it('both size arrays are equal', () => {
expect(viewModel.size0).toEqual(viewModel.size1);
});
test('sum of colors is equal to zero', () => {
expect(sum(viewModel.color)).toEqual(0);
});
});
it('sum of colors is equal to zero', () => {
expect(sum(viewModel.color)).toEqual(0);
});
});
});

View file

@ -31,8 +31,8 @@ describe('Differential Functions page', () => {
cy.wait('@getTopNFunctions');
[
{ id: 'overallPerformance', value: '0%' },
{ id: 'annualizedCo2', value: '33.79 lbs / 15.33 kg' },
{ id: 'annualizedCost', value: '$318.32' },
{ id: 'annualizedCo2', value: '2.5k lbs / 1.13k kg' },
{ id: 'annualizedCost', value: '$10.66k' },
{ id: 'totalNumberOfSamples', value: '513' },
].forEach((item) => {
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
@ -50,8 +50,8 @@ describe('Differential Functions page', () => {
cy.wait('@getTopNFunctions');
[
{ id: 'overallPerformance', value: '0%' },
{ id: 'annualizedCo2', value: '0 lbs / 0 kg', comparisonValue: '33.79 lbs / 15.33 kg' },
{ id: 'annualizedCost', value: '$0', comparisonValue: '$318.32' },
{ id: 'annualizedCo2', value: '0 lbs / 0 kg', comparisonValue: '2.5k lbs / 1.13k kg' },
{ id: 'annualizedCost', value: '$0', comparisonValue: '$10.66k' },
{ id: 'totalNumberOfSamples', value: '0', comparisonValue: '15,390' },
].forEach((item) => {
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
@ -76,14 +76,14 @@ describe('Differential Functions page', () => {
{ id: 'overallPerformance', value: '65.89%', icon: 'sortUp_success' },
{
id: 'annualizedCo2',
value: '33.79 lbs / 15.33 kg',
comparisonValue: '11.53 lbs / 5.23 kg (65.89%)',
value: '2.5k lbs / 1.13k kg',
comparisonValue: '548.84 lbs / 248.95 kg (78.01%',
icon: 'comparison_sortUp_success',
},
{
id: 'annualizedCost',
value: '$318.32',
comparisonValue: '$108.59 (65.89%)',
value: '$10.66k',
comparisonValue: '$2.35k (78.01%)',
icon: 'comparison_sortUp_success',
},
{
@ -116,14 +116,14 @@ describe('Differential Functions page', () => {
{ id: 'overallPerformance', value: '193.14%', icon: 'sortDown_danger' },
{
id: 'annualizedCo2',
value: '11.53 lbs / 5.23 kg',
comparisonValue: '33.79 lbs / 15.33 kg (193.14%)',
value: '548.84 lbs / 248.95 kg',
comparisonValue: '2.5k lbs / 1.13k kg (354.66%)',
icon: 'comparison_sortDown_danger',
},
{
id: 'annualizedCost',
value: '$108.59',
comparisonValue: '$318.32 (193.14%)',
value: '$2.35k',
comparisonValue: '$10.66k (354.66%)',
icon: 'comparison_sortDown_danger',
},
{

View file

@ -7,7 +7,7 @@
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
describe('Functions page', () => {
@ -38,7 +38,7 @@ describe('Functions page', () => {
cy.get(firstRowSelector).eq(2).contains('vmlinux');
cy.get(firstRowSelector).eq(3).contains('5.46%');
cy.get(firstRowSelector).eq(4).contains('5.46%');
cy.get(firstRowSelector).eq(5).contains('1.84 lbs / 0.84 kg');
cy.get(firstRowSelector).eq(5).contains('4.07 lbs / 1.84 kg');
cy.get(firstRowSelector).eq(6).contains('$17.37');
cy.get(firstRowSelector).eq(7).contains('28');
});
@ -66,11 +66,11 @@ describe('Functions page', () => {
{ parentKey: 'impactEstimates', key: 'annualizedSelfCoreSeconds', value: '17.03 days' },
{ parentKey: 'impactEstimates', key: 'co2Emission', value: '~0.00 lbs / ~0.00 kg' },
{ parentKey: 'impactEstimates', key: 'selfCo2Emission', value: '~0.00 lbs / ~0.00 kg' },
{ parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '1.84 lbs / 0.84 kg' },
{ parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '4.07 lbs / 1.84 kg' },
{
parentKey: 'impactEstimates',
key: 'annualizedSelfCo2Emission',
value: '1.84 lbs / 0.84 kg',
value: '4.07 lbs / 1.84 kg',
},
{ parentKey: 'impactEstimates', key: 'dollarCost', value: '$~0.00' },
{ parentKey: 'impactEstimates', key: 'selfDollarCost', value: '$~0.00' },
@ -133,17 +133,17 @@ describe('Functions page', () => {
columnKey: 'annualizedCo2',
columnIndex: 5,
highRank: 1,
lowRank: 389,
highValue: '1.84 lbs / 0.84 kg',
lowValue: undefined,
lowRank: 44,
highValue: '45.01 lbs / 20.42 kg',
lowValue: '0.15 lbs / 0.07 kg',
},
{
columnKey: 'annualizedDollarCost',
columnIndex: 6,
highRank: 1,
lowRank: 389,
highValue: '$17.37',
lowValue: undefined,
lowRank: 44,
highValue: '$192.36',
lowValue: '$0.62',
},
].forEach(({ columnKey, columnIndex, highRank, highValue, lowRank, lowValue }) => {
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
@ -174,15 +174,15 @@ describe('Functions page', () => {
cy.get(firstRowSelector).eq(2).contains('/');
});
describe('Test changing CO2 settings', () => {
// skipping this for now until the values are passed to the ES plugin
describe.skip('Test changing CO2 settings', () => {
afterEach(() => {
cy.updateAdvancedSettings({
[profilingCo2PerKWH]: 0.000379069,
[profilingDatacenterPUE]: 1.7,
[profilingPerCoreWatt]: 7,
[profilingPervCPUWattX86]: 7,
});
});
it('changes CO2 settings and validate values in the table', () => {
cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions');
cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo });
@ -199,7 +199,7 @@ describe('Functions page', () => {
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
.clear()
.type('2.4');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPervCPUWattX86}"]`)
.clear()
.type('20');
cy.contains('Save changes').click();

View file

@ -13,7 +13,7 @@
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
describe('Settings page', () => {
@ -25,7 +25,7 @@ describe('Settings page', () => {
cy.updateAdvancedSettings({
[profilingCo2PerKWH]: 0.000379069,
[profilingDatacenterPUE]: 1.7,
[profilingPerCoreWatt]: 7,
[profilingPervCPUWattX86]: 7,
});
});
@ -35,7 +35,10 @@ describe('Settings page', () => {
cy.contains('CO2');
cy.contains('Regional Carbon Intensity (ton/kWh)');
cy.contains('Data Center PUE');
cy.contains('Per Core Watts');
cy.contains('Per vCPU Watts - x86');
cy.contains('Per vCPU Watts - arm64');
cy.contains('AWS EDP discount rate (%)');
cy.contains('Cost per vCPU per hour ($)');
});
it('updates values', () => {
@ -48,7 +51,7 @@ describe('Settings page', () => {
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
.clear()
.type('2.4');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPervCPUWattX86}"]`)
.clear()
.type('20');
cy.get('[data-test-subj="profilingBottomBarActions"]').should('exist');

View file

@ -21,48 +21,65 @@ import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
import { profilingUseLegacyCo2Calculation } from '@kbn/observability-plugin/common';
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
import { asCost } from '../../utils/formatters/as_cost';
import { asPercentage } from '../../utils/formatters/as_percentage';
import { asWeight } from '../../utils/formatters/as_weight';
import { CPULabelWithHint } from '../cpu_label_with_hint';
import { TooltipRow } from './tooltip_row';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
interface Props {
isRoot: boolean;
label: string;
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
annualCO2KgsInclusive: number;
annualCostsUSDInclusive: number;
baselineScaleFactor?: number;
comparisonScaleFactor?: number;
comparisonCountInclusive?: number;
comparisonAnnualCO2KgsInclusive?: number;
comparisonAnnualCostsUSDInclusive?: number;
comparisonCountExclusive?: number;
comparisonCountInclusive?: number;
comparisonScaleFactor?: number;
comparisonTotalSamples?: number;
comparisonTotalSeconds?: number;
onShowMoreClick?: () => void;
countExclusive: number;
countInclusive: number;
inline: boolean;
isRoot: boolean;
label: string;
onShowMoreClick?: () => void;
parentLabel?: string;
totalSamples: number;
totalSeconds: number;
}
export function FlameGraphTooltip({
isRoot,
label,
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
annualCO2KgsInclusive,
annualCostsUSDInclusive,
baselineScaleFactor,
comparisonScaleFactor,
comparisonCountInclusive,
comparisonAnnualCO2KgsInclusive,
comparisonAnnualCostsUSDInclusive,
comparisonCountExclusive,
comparisonCountInclusive,
comparisonScaleFactor,
comparisonTotalSamples,
comparisonTotalSeconds,
onShowMoreClick,
countExclusive,
countInclusive,
inline,
isRoot,
label,
onShowMoreClick,
parentLabel,
totalSamples,
totalSeconds,
}: Props) {
const {
start: { core },
} = useProfilingDependencies();
const shouldUseLegacyCo2Calculation = core.uiSettings.get<boolean>(
profilingUseLegacyCo2Calculation
);
const theme = useEuiTheme();
const calculateImpactEstimates = useCalculateImpactEstimate();
@ -170,9 +187,17 @@ export function FlameGraphTooltip({
label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedCo2', {
defaultMessage: `Annualized CO2`,
})}
value={impactEstimates.totalCPU.annualizedCo2}
comparison={comparisonImpactEstimates?.totalCPU.annualizedCo2}
formatValue={asWeight}
value={
shouldUseLegacyCo2Calculation
? impactEstimates.totalCPU.annualizedCo2
: annualCO2KgsInclusive
}
comparison={
shouldUseLegacyCo2Calculation
? comparisonImpactEstimates?.totalCPU.annualizedCo2
: comparisonAnnualCO2KgsInclusive
}
formatValue={(value) => asWeight(value, 'kgs')}
showDifference
formatDifferenceAsPercentage={false}
/>
@ -180,8 +205,16 @@ export function FlameGraphTooltip({
label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedDollarCost', {
defaultMessage: `Annualized dollar cost`,
})}
value={impactEstimates.totalCPU.annualizedDollarCost}
comparison={comparisonImpactEstimates?.totalCPU.annualizedDollarCost}
value={
shouldUseLegacyCo2Calculation
? impactEstimates.totalCPU.annualizedDollarCost
: annualCostsUSDInclusive
}
comparison={
shouldUseLegacyCo2Calculation
? comparisonImpactEstimates?.totalCPU.annualizedDollarCost
: comparisonAnnualCostsUSDInclusive
}
formatValue={asCost}
showDifference
formatDifferenceAsPercentage={false}

View file

@ -10,23 +10,23 @@ import {
Datum,
Flame,
FlameLayerValue,
FlameSpec,
PartialTheme,
Settings,
Tooltip,
FlameSpec,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import React, { useEffect, useMemo, useState } from 'react';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import type { ElasticFlameGraph } from '@kbn/profiling-utils';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useMemo, useState } from 'react';
import { getFlamegraphModel } from '../../utils/get_flamegraph_model';
import { FlameGraphLegend } from './flame_graph_legend';
import { FrameInformationWindow } from '../frame_information_window';
import { Frame } from '../frame_information_window';
import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip';
import { FlameGraphTooltip } from './flamegraph_tooltip';
import { ComparisonMode } from '../normalization_menu';
import { FlameGraphTooltip } from './flamegraph_tooltip';
import { FlameGraphLegend } from './flame_graph_legend';
interface Props {
id: string;
@ -90,7 +90,7 @@ export function FlameGraph({
const [highlightedVmIndex, setHighlightedVmIndex] = useState<number | undefined>(undefined);
const selected: undefined | React.ComponentProps<typeof FrameInformationWindow>['frame'] =
const selected: Frame | undefined =
primaryFlamegraph && highlightedVmIndex !== undefined
? {
fileID: primaryFlamegraph.FileID[highlightedVmIndex],
@ -102,6 +102,10 @@ export function FlameGraph({
sourceLine: primaryFlamegraph.SourceLine[highlightedVmIndex],
countInclusive: primaryFlamegraph.CountInclusive[highlightedVmIndex],
countExclusive: primaryFlamegraph.CountExclusive[highlightedVmIndex],
selfAnnualCO2Kgs: primaryFlamegraph.SelfAnnualCO2KgsItems[highlightedVmIndex],
totalAnnualCO2Kgs: primaryFlamegraph.TotalAnnualCO2KgsItems[highlightedVmIndex],
selfAnnualCostUSD: primaryFlamegraph.SelfAnnualCostsUSDItems[highlightedVmIndex],
totalAnnualCostUSD: primaryFlamegraph.TotalAnnualCostsUSDItems[highlightedVmIndex],
}
: undefined;
@ -154,23 +158,35 @@ export function FlameGraph({
return (
<FlameGraphTooltip
isRoot={valueIndex === 0}
label={label}
countInclusive={countInclusive}
countExclusive={countExclusive}
totalSamples={totalSamples}
totalSeconds={totalSeconds}
comparisonCountInclusive={comparisonNode?.CountInclusive}
annualCO2KgsInclusive={
primaryFlamegraph.TotalAnnualCO2KgsItems[valueIndex]
}
annualCostsUSDInclusive={
primaryFlamegraph.TotalAnnualCostsUSDItems[valueIndex]
}
baselineScaleFactor={baseline}
comparisonAnnualCO2KgsInclusive={
comparisonFlamegraph?.TotalAnnualCO2KgsItems[valueIndex]
}
comparisonAnnualCostsUSDInclusive={
comparisonFlamegraph?.TotalAnnualCostsUSDItems[valueIndex]
}
comparisonCountExclusive={comparisonNode?.CountExclusive}
comparisonCountInclusive={comparisonNode?.CountInclusive}
comparisonScaleFactor={comparison}
comparisonTotalSamples={comparisonFlamegraph?.CountInclusive[0]}
comparisonTotalSeconds={comparisonFlamegraph?.TotalSeconds}
baselineScaleFactor={baseline}
comparisonScaleFactor={comparison}
countExclusive={countExclusive}
countInclusive={countInclusive}
isRoot={valueIndex === 0}
label={label}
onShowMoreClick={() => {
trackProfilingEvent({ metric: 'flamegraph_node_details_click' });
toggleShowInformationWindow();
setHighlightedVmIndex(valueIndex);
}}
totalSamples={totalSamples}
totalSeconds={totalSeconds}
inline={inline}
parentLabel={parentLabel}
/>

View file

@ -13,7 +13,23 @@ import { asNumber } from '../../utils/formatters/as_number';
import { asPercentage } from '../../utils/formatters/as_percentage';
import { asWeight } from '../../utils/formatters/as_weight';
import { CPULabelWithHint } from '../cpu_label_with_hint';
import { CalculateImpactEstimates } from '../../hooks/use_calculate_impact_estimates';
import {
ANNUAL_SECONDS,
CalculateImpactEstimates,
} from '../../hooks/use_calculate_impact_estimates';
interface Params {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
calculateImpactEstimates: CalculateImpactEstimates;
shouldUseLegacyCo2Calculation: boolean;
selfAnnualCO2Kgs: number;
totalAnnualCO2Kgs: number;
selfAnnualCostUSD: number;
totalAnnualCostUSD: number;
}
export function getImpactRows({
countInclusive,
@ -21,13 +37,12 @@ export function getImpactRows({
totalSamples,
totalSeconds,
calculateImpactEstimates,
}: {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
calculateImpactEstimates: CalculateImpactEstimates;
}) {
shouldUseLegacyCo2Calculation,
selfAnnualCO2Kgs,
totalAnnualCO2Kgs,
selfAnnualCostUSD,
totalAnnualCostUSD,
}: Params) {
const { selfCPU, totalCPU } = calculateImpactEstimates({
countInclusive,
countExclusive,
@ -35,6 +50,8 @@ export function getImpactRows({
totalSeconds,
});
const annualSecondsRatio = ANNUAL_SECONDS / totalSeconds;
return [
{
'data-test-subj': 'totalCPU',
@ -100,7 +117,10 @@ export function getImpactRows({
defaultMessage: 'CO2 emission',
}
),
value: asWeight(totalCPU.co2),
value: asWeight(
shouldUseLegacyCo2Calculation ? totalCPU.co2 : totalAnnualCO2Kgs / annualSecondsRatio,
'kgs'
),
},
{
'data-test-subj': 'selfCo2Emission',
@ -108,7 +128,10 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.co2EmissionExclusiveLabel',
{ defaultMessage: 'CO2 emission (excl. children)' }
),
value: asWeight(selfCPU.co2),
value: asWeight(
shouldUseLegacyCo2Calculation ? selfCPU.co2 : selfAnnualCO2Kgs / annualSecondsRatio,
'kgs'
),
},
{
'data-test-subj': 'annualizedCo2Emission',
@ -116,7 +139,10 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel',
{ defaultMessage: 'Annualized CO2' }
),
value: asWeight(totalCPU.annualizedCo2),
value: asWeight(
shouldUseLegacyCo2Calculation ? totalCPU.annualizedCo2 : totalAnnualCO2Kgs,
'kgs'
),
},
{
'data-test-subj': 'annualizedSelfCo2Emission',
@ -124,7 +150,10 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel',
{ defaultMessage: 'Annualized CO2 (excl. children)' }
),
value: asWeight(selfCPU.annualizedCo2),
value: asWeight(
shouldUseLegacyCo2Calculation ? selfCPU.annualizedCo2 : selfAnnualCO2Kgs,
'kgs'
),
},
{
'data-test-subj': 'dollarCost',
@ -132,7 +161,11 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.dollarCostInclusiveLabel',
{ defaultMessage: 'Dollar cost' }
),
value: asCost(totalCPU.dollarCost),
value: asCost(
shouldUseLegacyCo2Calculation
? totalCPU.dollarCost
: totalAnnualCostUSD / annualSecondsRatio
),
},
{
'data-test-subj': 'selfDollarCost',
@ -140,7 +173,9 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.dollarCostExclusiveLabel',
{ defaultMessage: 'Dollar cost (excl. children)' }
),
value: asCost(selfCPU.dollarCost),
value: asCost(
shouldUseLegacyCo2Calculation ? selfCPU.dollarCost : selfAnnualCostUSD / annualSecondsRatio
),
},
{
'data-test-subj': 'annualizedDollarCost',
@ -148,7 +183,9 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostInclusiveLabel',
{ defaultMessage: 'Annualized dollar cost' }
),
value: asCost(totalCPU.annualizedDollarCost),
value: asCost(
shouldUseLegacyCo2Calculation ? totalCPU.annualizedDollarCost : totalAnnualCostUSD
),
},
{
'data-test-subj': 'annualizedSelfDollarCost',
@ -156,7 +193,9 @@ export function getImpactRows({
'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostExclusiveLabel',
{ defaultMessage: 'Annualized dollar cost (excl. children)' }
),
value: asCost(selfCPU.annualizedDollarCost),
value: asCost(
shouldUseLegacyCo2Calculation ? selfCPU.annualizedDollarCost : selfAnnualCostUSD
),
},
];
}

View file

@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FrameSymbolStatus, getFrameSymbolStatus } from '@kbn/profiling-utils';
import React from 'react';
import { profilingUseLegacyCo2Calculation } from '@kbn/observability-plugin/common';
import { FrameInformationAIAssistant } from './frame_information_ai_assistant';
import { FrameInformationPanel } from './frame_information_panel';
import { getImpactRows } from './get_impact_rows';
@ -15,6 +16,7 @@ import { getInformationRows } from './get_information_rows';
import { KeyValueList } from './key_value_list';
import { MissingSymbolsCallout } from './missing_symbols_callout';
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
export interface Frame {
fileID: string;
@ -26,6 +28,10 @@ export interface Frame {
sourceLine: number;
countInclusive: number;
countExclusive: number;
selfAnnualCO2Kgs: number;
totalAnnualCO2Kgs: number;
selfAnnualCostUSD: number;
totalAnnualCostUSD: number;
}
export interface Props {
@ -43,6 +49,12 @@ export function FrameInformationWindow({
showSymbolsStatus = true,
}: Props) {
const calculateImpactEstimates = useCalculateImpactEstimate();
const {
start: { core },
} = useProfilingDependencies();
const shouldUseLegacyCo2Calculation = core.uiSettings.get<boolean>(
profilingUseLegacyCo2Calculation
);
if (!frame) {
return (
@ -72,6 +84,10 @@ export function FrameInformationWindow({
sourceLine,
countInclusive,
countExclusive,
selfAnnualCO2Kgs,
totalAnnualCO2Kgs,
selfAnnualCostUSD,
totalAnnualCostUSD,
} = frame;
const informationRows = getInformationRows({
@ -90,6 +106,11 @@ export function FrameInformationWindow({
totalSamples,
totalSeconds,
calculateImpactEstimates,
shouldUseLegacyCo2Calculation,
selfAnnualCO2Kgs,
totalAnnualCO2Kgs,
selfAnnualCostUSD,
totalAnnualCostUSD,
});
return (

View file

@ -16,11 +16,13 @@ import {
} from '@elastic/eui';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { profilingUseLegacyCo2Calculation } from '@kbn/observability-plugin/common';
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
import { asCost } from '../../utils/formatters/as_cost';
import { asWeight } from '../../utils/formatters/as_weight';
import { calculateBaseComparisonDiff } from '../topn_functions/utils';
import { SummaryItem } from './summary_item';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
interface FrameValue {
selfCPU: number;
@ -28,6 +30,8 @@ interface FrameValue {
totalCount: number;
duration: number;
scaleFactor?: number;
totalAnnualCO2Kgs: number;
totalAnnualCostUSD: number;
}
interface Props {
@ -45,6 +49,12 @@ function getScaleFactor(scaleFactor: number = 1) {
}
export function FramesSummary({ baseValue, comparisonValue, isLoading }: Props) {
const {
start: { core },
} = useProfilingDependencies();
const shouldUseLegacyCo2Calculation = core.uiSettings.get<boolean>(
profilingUseLegacyCo2Calculation
);
const calculateImpactEstimates = useCalculateImpactEstimate();
const baselineScaledTotalSamples = baseValue
@ -82,13 +92,25 @@ export function FramesSummary({ baseValue, comparisonValue, isLoading }: Props)
comparisonValue: comparisonScaledTotalSamples || 0,
}),
co2EmissionDiff: calculateBaseComparisonDiff({
baselineValue: baseImpactEstimates?.totalSamples?.annualizedCo2 || 0,
comparisonValue: comparisonImpactEstimates?.totalSamples.annualizedCo2 || 0,
formatValue: asWeight,
baselineValue:
(shouldUseLegacyCo2Calculation
? baseImpactEstimates?.totalSamples?.annualizedCo2
: baseValue?.totalAnnualCO2Kgs) || 0,
comparisonValue:
(shouldUseLegacyCo2Calculation
? comparisonImpactEstimates?.totalSamples.annualizedCo2
: comparisonValue?.totalAnnualCO2Kgs) || 0,
formatValue: (value) => asWeight(value, 'kgs'),
}),
costImpactDiff: calculateBaseComparisonDiff({
baselineValue: baseImpactEstimates?.totalSamples.annualizedDollarCost || 0,
comparisonValue: comparisonImpactEstimates?.totalSamples.annualizedDollarCost || 0,
baselineValue:
(shouldUseLegacyCo2Calculation
? baseImpactEstimates?.totalSamples.annualizedDollarCost
: baseValue?.totalAnnualCostUSD) || 0,
comparisonValue:
(shouldUseLegacyCo2Calculation
? comparisonImpactEstimates?.totalSamples.annualizedDollarCost
: comparisonValue?.totalAnnualCostUSD) || 0,
formatValue: asCost,
}),
};
@ -98,6 +120,7 @@ export function FramesSummary({ baseValue, comparisonValue, isLoading }: Props)
calculateImpactEstimates,
comparisonScaledTotalSamples,
comparisonValue,
shouldUseLegacyCo2Calculation,
]);
const data = [

View file

@ -17,8 +17,10 @@ import {
import { i18n } from '@kbn/i18n';
import { TopNFunctionSortField } from '@kbn/profiling-utils';
import React, { useEffect } from 'react';
import { profilingUseLegacyCo2Calculation } from '@kbn/observability-plugin/common';
import { asCost } from '../../utils/formatters/as_cost';
import { asWeight } from '../../utils/formatters/as_weight';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
import { StackFrameSummary } from '../stack_frame_summary';
import { CPUStat } from './cpu_stat';
import { SampleStat } from './sample_stat';
@ -39,6 +41,14 @@ export function FunctionRow({
onFrameClick,
setCellProps,
}: Props) {
const {
start: { core },
} = useProfilingDependencies();
const shouldUseLegacyCo2Calculation = core.uiSettings.get<boolean>(
profilingUseLegacyCo2Calculation
);
if (columnId === TopNFunctionSortField.Diff) {
return <DiffColumn diff={functionRow.diff} setCellProps={setCellProps} />;
}
@ -72,16 +82,33 @@ export function FunctionRow({
if (
columnId === TopNFunctionSortField.AnnualizedCo2 &&
functionRow.impactEstimates?.selfCPU?.annualizedCo2
functionRow.impactEstimates?.totalCPU?.annualizedCo2
) {
return <div>{asWeight(functionRow.impactEstimates.selfCPU.annualizedCo2)}</div>;
return (
<div>
{asWeight(
shouldUseLegacyCo2Calculation
? functionRow.impactEstimates.totalCPU.annualizedCo2
: functionRow.totalAnnualCO2kgs,
'kgs'
)}
</div>
);
}
if (
columnId === TopNFunctionSortField.AnnualizedDollarCost &&
functionRow.impactEstimates?.selfCPU?.annualizedDollarCost
functionRow.impactEstimates?.totalCPU?.annualizedDollarCost
) {
return <div>{asCost(functionRow.impactEstimates.selfCPU.annualizedDollarCost)}</div>;
return (
<div>
{asCost(
shouldUseLegacyCo2Calculation
? functionRow.impactEstimates.totalCPU.annualizedDollarCost
: functionRow.totalAnnualCostUSD
)}
</div>
);
}
return null;

View file

@ -21,12 +21,14 @@ import { getCalleeFunction, TopNFunctions, TopNFunctionSortField } from '@kbn/pr
import { last, orderBy } from 'lodash';
import React, { forwardRef, Ref, useMemo, useState } from 'react';
import { GridOnScrollProps } from 'react-window';
import { profilingUseLegacyCo2Calculation } from '@kbn/observability-plugin/common';
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
import { CPULabelWithHint } from '../cpu_label_with_hint';
import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip';
import { LabelWithHint } from '../label_with_hint';
import { FunctionRow } from './function_row';
import { getFunctionsRows, IFunctionRow } from './utils';
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
interface Props {
topNFunctions?: TopNFunctions;
@ -69,6 +71,12 @@ export const TopNFunctionsGrid = forwardRef(
}: Props,
ref: Ref<EuiDataGridRefProps> | undefined
) => {
const {
start: { core },
} = useProfilingDependencies();
const shouldUseLegacyCo2Calculation = core.uiSettings.get<boolean>(
profilingUseLegacyCo2Calculation
);
const [selectedRow, setSelectedRow] = useState<IFunctionRow | undefined>();
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
const calculateImpactEstimates = useCalculateImpactEstimate();
@ -115,17 +123,27 @@ export const TopNFunctionsGrid = forwardRef(
case TopNFunctionSortField.TotalCPU:
return orderBy(rows, (row) => row.totalCPUPerc, sortDirection);
case TopNFunctionSortField.AnnualizedCo2:
return orderBy(rows, (row) => row.impactEstimates?.selfCPU.annualizedCo2, sortDirection);
return orderBy(
rows,
(row) =>
shouldUseLegacyCo2Calculation
? row.impactEstimates?.totalCPU.annualizedCo2
: row.totalAnnualCO2kgs,
sortDirection
);
case TopNFunctionSortField.AnnualizedDollarCost:
return orderBy(
rows,
(row) => row.impactEstimates?.selfCPU.annualizedDollarCost,
(row) =>
shouldUseLegacyCo2Calculation
? row.impactEstimates?.totalCPU.annualizedDollarCost
: row.totalAnnualCostUSD,
sortDirection
);
default:
return orderBy(rows, sortField, sortDirection);
}
}, [rows, sortDirection, sortField]);
}, [rows, shouldUseLegacyCo2Calculation, sortDirection, sortField]);
const { columns, leadingControlColumns } = useMemo(() => {
const gridColumns: EuiDataGridColumn[] = [
@ -255,7 +273,11 @@ export const TopNFunctionsGrid = forwardRef(
headerCellRender() {
return (
<EuiScreenReaderOnly>
<span>Controls</span>
<span>
{i18n.translate('xpack.profiling.topNFunctionsGrid.span.controlsLabel', {
defaultMessage: 'Controls',
})}
</span>
</EuiScreenReaderOnly>
);
},
@ -267,7 +289,10 @@ export const TopNFunctionsGrid = forwardRef(
return (
<EuiButtonIcon
data-test-subj="profilingTopNFunctionsGridButton"
aria-label="Show actions"
aria-label={i18n.translate(
'xpack.profiling.topNFunctionsGrid.euiButtonIcon.showActionsLabel',
{ defaultMessage: 'Show actions' }
)}
iconType="expand"
color="text"
onClick={handleOnClick}
@ -306,7 +331,10 @@ export const TopNFunctionsGrid = forwardRef(
<EuiDataGrid
data-test-subj={dataTestSubj}
ref={ref}
aria-label="TopN functions"
aria-label={i18n.translate(
'xpack.profiling.topNFunctionsGrid.euiDataGrid.topNFunctionsLabel',
{ defaultMessage: 'TopN functions' }
)}
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
rowCount={sortedRows.length > 100 ? 100 : sortedRows.length}
@ -348,6 +376,10 @@ export const TopNFunctionsGrid = forwardRef(
functionName: selectedRow.frame.FunctionName,
sourceFileName: selectedRow.frame.SourceFilename,
sourceLine: selectedRow.frame.SourceLine,
selfAnnualCO2Kgs: selectedRow.selfAnnualCO2kgs,
totalAnnualCO2Kgs: selectedRow.totalAnnualCO2kgs,
selfAnnualCostUSD: selectedRow.selfAnnualCostUSD,
totalAnnualCostUSD: selectedRow.totalAnnualCostUSD,
}}
totalSeconds={totalSeconds}
totalSamples={totalCount}

View file

@ -42,6 +42,10 @@ export interface IFunctionRow {
selfCPUPerc: number;
totalCPUPerc: number;
impactEstimates?: ImpactEstimates;
selfAnnualCO2kgs: number;
selfAnnualCostUSD: number;
totalAnnualCO2kgs: number;
totalAnnualCostUSD: number;
diff?: {
rank: number;
samples: number;
@ -50,6 +54,10 @@ export interface IFunctionRow {
selfCPUPerc: number;
totalCPUPerc: number;
impactEstimates?: ImpactEstimates;
selfAnnualCO2kgs: number;
selfAnnualCostUSD: number;
totalAnnualCO2kgs: number;
totalAnnualCostUSD: number;
};
}
@ -117,6 +125,10 @@ export function getFunctionsRows({
totalCPUPerc:
totalCPUPerc -
(comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100,
selfAnnualCO2kgs: comparisonRow.selfAnnualCO2kgs,
selfAnnualCostUSD: comparisonRow.selfAnnualCostUSD,
totalAnnualCO2kgs: comparisonRow.totalAnnualCO2kgs,
totalAnnualCostUSD: comparisonRow.totalAnnualCostUSD,
};
}
}
@ -131,6 +143,10 @@ export function getFunctionsRows({
selfCPU: topN.CountExclusive,
totalCPU: topN.CountInclusive,
impactEstimates,
selfAnnualCO2kgs: topN.selfAnnualCO2kgs,
selfAnnualCostUSD: topN.selfAnnualCostUSD,
totalAnnualCO2kgs: topN.totalAnnualCO2kgs,
totalAnnualCostUSD: topN.totalAnnualCostUSD,
diff: calculateDiff(),
};
});

View file

@ -9,7 +9,7 @@ import { useProfilingDependencies } from '../components/contexts/profiling_depen
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
jest.mock('../components/contexts/profiling_dependencies/use_profiling_dependencies');
@ -21,7 +21,7 @@ describe('useCalculateImpactEstimate', () => {
core: {
uiSettings: {
get: (key: string) => {
if (key === profilingPerCoreWatt) {
if (key === profilingPervCPUWattX86) {
return 7;
}
if (key === profilingCo2PerKWH) {

View file

@ -8,7 +8,7 @@
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
@ -22,7 +22,7 @@ interface Params {
export type CalculateImpactEstimates = ReturnType<typeof useCalculateImpactEstimate>;
export type ImpactEstimates = ReturnType<CalculateImpactEstimates>;
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
export const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
// The cost of an x86 CPU core per hour, in US$.
// (ARM is 60% less based graviton 3 data, see https://aws.amazon.com/ec2/graviton/)
@ -33,7 +33,7 @@ export function useCalculateImpactEstimate() {
start: { core },
} = useProfilingDependencies();
const perCoreWatts = core.uiSettings.get<number>(profilingPerCoreWatt);
const perCoreWatts = core.uiSettings.get<number>(profilingPervCPUWattX86);
const co2PerTonKWH = core.uiSettings.get<number>(profilingCo2PerKWH);
const datacenterPUE = core.uiSettings.get<number>(profilingDatacenterPUE);

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { asWeight } from './as_weight';
describe('asWeight', () => {
it('should correctly convert and format weight in pounds to kilograms', () => {
const valueInPounds = 150;
expect(asWeight(valueInPounds, 'lbs')).toBe('150 lbs / 68.04 kg');
});
it('should correctly convert and format weight in kilograms to pounds', () => {
const valueInKilograms = 75;
expect(asWeight(valueInKilograms, 'kgs')).toBe('165.35 lbs / 75 kg');
});
it('should handle NaN input', () => {
expect(asWeight(NaN, 'lbs')).toBe('N/A lbs / N/A kg');
});
it('should handle zero input', () => {
expect(asWeight(0, 'kgs')).toBe('0 lbs / 0 kg');
});
it('should format very small values in pounds as "~0.00"', () => {
expect(asWeight(0.0001, 'lbs')).toBe('~0.00 lbs / ~0.00 kg');
});
});

View file

@ -8,10 +8,16 @@
import { asNumber } from './as_number';
const ONE_POUND_TO_A_KILO = 0.45359237;
const ONE_KILO_TO_A_POUND = 2.20462;
export function asWeight(valueInPounds: number): string {
const lbs = asNumber(valueInPounds);
const kgs = asNumber(Number(valueInPounds * ONE_POUND_TO_A_KILO));
export function asWeight(value: number, unit: 'kgs' | 'lbs'): string {
const formattedValue = asNumber(value);
return `${lbs} lbs / ${kgs} kg`;
if (unit === 'lbs') {
const kgs = asNumber(Number(value * ONE_POUND_TO_A_KILO));
return `${formattedValue} lbs / ${kgs} kg`;
}
const lbs = asNumber(Number(value * ONE_KILO_TO_A_POUND));
return `${lbs} lbs / ${formattedValue} kg`;
}

View file

@ -131,6 +131,8 @@ export function DifferentialFlameGraphsView() {
totalCPU: state.data.primaryFlamegraph.TotalCPU,
totalCount: state.data.primaryFlamegraph.TotalSamples,
scaleFactor: isNormalizedByTime ? baselineTime : baseline,
totalAnnualCO2Kgs: state.data.primaryFlamegraph.TotalAnnualCO2Kgs,
totalAnnualCostUSD: state.data.primaryFlamegraph.TotalAnnualCostsUSD,
}
: undefined
}
@ -142,6 +144,8 @@ export function DifferentialFlameGraphsView() {
totalCPU: state.data.comparisonFlamegraph.TotalCPU,
totalCount: state.data.comparisonFlamegraph.TotalSamples,
scaleFactor: isNormalizedByTime ? comparisonTime : comparison,
totalAnnualCO2Kgs: state.data.comparisonFlamegraph.TotalAnnualCO2Kgs,
totalAnnualCostUSD: state.data.comparisonFlamegraph.TotalAnnualCostsUSD,
}
: undefined
}

View file

@ -180,6 +180,8 @@ export function DifferentialTopNFunctionsView() {
totalCount: state.data.TotalCount,
totalCPU: state.data.totalCPU,
scaleFactor: isNormalizedByTime ? baselineTime : baseline,
totalAnnualCO2Kgs: state.data.totalAnnualCO2Kgs,
totalAnnualCostUSD: state.data.totalAnnualCostUSD,
}
: undefined
}
@ -191,6 +193,8 @@ export function DifferentialTopNFunctionsView() {
totalCount: comparisonState.data.TotalCount,
totalCPU: comparisonState.data.totalCPU,
scaleFactor: isNormalizedByTime ? comparisonTime : comparison,
totalAnnualCO2Kgs: comparisonState.data.totalAnnualCO2Kgs,
totalAnnualCostUSD: comparisonState.data.totalAnnualCostUSD,
}
: undefined
}

View file

@ -5,13 +5,26 @@
* 2.0.
*/
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { LazyField } from '@kbn/advanced-settings-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
profilingPervCPUWattX86,
profilingPervCPUWattArm64,
profilingAWSCostDiscountRate,
profilingCostPervCPUPerHour,
} from '@kbn/observability-plugin/common';
import { useEditableSettings, useUiTracker } from '@kbn/observability-shared-plugin/public';
import { isEmpty } from 'lodash';
@ -20,7 +33,13 @@ import { useProfilingDependencies } from '../../components/contexts/profiling_de
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { BottomBarActions } from './bottom_bar_actions';
const settingKeys = [profilingCo2PerKWH, profilingDatacenterPUE, profilingPerCoreWatt];
const co2Settings = [
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPervCPUWattX86,
profilingPervCPUWattArm64,
];
const costSettings = [profilingAWSCostDiscountRate, profilingCostPervCPUPerHour];
export function Settings() {
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
@ -37,7 +56,7 @@ export function Settings() {
saveAll,
isSaving,
cleanUnsavedChanges,
} = useEditableSettings('profiling', settingKeys);
} = useEditableSettings('profiling', [...co2Settings, ...costSettings]);
async function handleSave() {
try {
@ -71,33 +90,136 @@ export function Settings() {
</EuiText>
</EuiTitle>
<EuiSpacer />
<EuiPanel grow={false} hasShadow={false} hasBorder paddingSize="none">
<EuiPanel color="subdued" hasShadow={false}>
<EuiTitle size="s">
<EuiText>
{i18n.translate('xpack.profiling.settings.co2Sections', {
defaultMessage: 'CO2',
})}
</EuiText>
</EuiTitle>
</EuiPanel>
<EuiPanel hasShadow={false}>
{settingKeys.map((settingKey) => {
const editableConfig = settingsEditableConfig[settingKey];
return (
<LazyField
key={settingKey}
setting={editableConfig}
handleChange={handleFieldChange}
enableSaving
docLinks={docLinks.links}
toasts={notifications.toasts}
unsavedChanges={unsavedChanges[settingKey]}
{[
{
label: i18n.translate('xpack.profiling.settings.co2Section', {
defaultMessage: 'Custom CO2 settings',
}),
description: {
title: i18n.translate('xpack.profiling.settings.co2.title', {
defaultMessage:
'The Universal Profiling host agent can detect if your machine is running on AWS, Azure, or Google Cloud Platform.',
}),
subtitle: (
<FormattedMessage
id="xpack.profiling.settings.co2.subtitle"
defaultMessage="For machines running on AWS, Universal Profiling applies the appropriate {regionalCarbonIntensityLink} for your instance's AWS region and the current AWS data center {pue}."
values={{
regionalCarbonIntensityLink: (
<EuiLink
data-test-subj="profilingSettingsLink"
href="https://ela.st/grid-datasheet"
target="_blank"
>
{i18n.translate('xpack.profiling.settings.co2.subtitle.link', {
defaultMessage: 'regional carbon intensity',
})}
</EuiLink>
),
pue: (
<strong>
{i18n.translate('xpack.profiling.settings.co2.subtitle.pue', {
defaultMessage: 'PUE',
})}
</strong>
),
}}
/>
);
})}
</EuiPanel>
</EuiPanel>
),
text: i18n.translate('xpack.profiling.settings.co2.text', {
defaultMessage:
'For all other configurations, Universal Profiling uses the following default configurations. You can update these configurations as needed.',
}),
},
settings: co2Settings,
},
{
label: i18n.translate('xpack.profiling.settings.costSection', {
defaultMessage: 'Custom cost settings',
}),
description: {
title: (
<FormattedMessage
id="xpack.profiling.settings.cost.title"
defaultMessage="Universal Profiling sets the cost for AWS configurations using the {awsPriceList} for your EC2 instance type."
values={{
awsPriceList: (
<EuiLink
data-test-subj="profilingSettingsLink"
href="https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/Welcome.html#Welcome_AWS_Price_List_Service"
target="_blank"
>
{i18n.translate('xpack.profiling.settings.cost.subtitle.link', {
defaultMessage: 'AWS price list',
})}
</EuiLink>
),
}}
/>
),
},
settings: costSettings,
},
].map((item) => (
<>
<EuiPanel key={item.label} grow={false} hasShadow={false} hasBorder paddingSize="none">
<EuiPanel color="subdued" hasShadow={false}>
<EuiTitle size="s">
<EuiText>{item.label}</EuiText>
</EuiTitle>
</EuiPanel>
<EuiPanel hasShadow={false}>
{item.description ? (
<>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="xs">
{item.description.title && (
<EuiFlexItem>
<EuiText size="s">{item.description.title}</EuiText>
</EuiFlexItem>
)}
{item.description.subtitle && (
<EuiFlexItem>
<EuiText size="s">{item.description.subtitle}</EuiText>
</EuiFlexItem>
)}
{item.description.text && (
<EuiFlexItem>
<EuiText size="s">
<strong>{item.description.text}</strong>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
) : null}
{item.settings.map((settingKey) => {
const editableConfig = settingsEditableConfig[settingKey];
return (
<LazyField
key={settingKey}
setting={editableConfig}
handleChange={handleFieldChange}
enableSaving
docLinks={docLinks.links}
toasts={notifications.toasts}
unsavedChanges={unsavedChanges[settingKey]}
/>
);
})}
</EuiPanel>
</EuiPanel>
<EuiSpacer />
</>
))}
{!isEmpty(unsavedChanges) && (
<BottomBarActions
isLoading={isSaving}

View file

@ -6,7 +6,6 @@
*/
import { schema } from '@kbn/config-schema';
import { profilingUseLegacyFlamegraphAPI } from '@kbn/observability-plugin/common';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
@ -34,18 +33,17 @@ export function registerFlameChartSearchRoute({
},
async (context, request, response) => {
const { timeFrom, timeTo, kuery } = request.query;
const useLegacyFlamegraphAPI = await (
await context.core
).uiSettings.client.get<boolean>(profilingUseLegacyFlamegraphAPI);
const core = await context.core;
try {
const esClient = await getClient(context);
const flamegraph = await profilingDataAccess.services.fetchFlamechartData({
core,
esClient,
rangeFromMs: timeFrom,
rangeToMs: timeTo,
kuery,
useLegacyFlamegraphAPI,
});
return response.ok({ body: flamegraph });

View file

@ -37,9 +37,12 @@ export function registerTopNFunctionsSearchRoute({
},
async (context, request, response) => {
try {
const core = await context.core;
const { timeFrom, timeTo, startIndex, endIndex, kuery }: QuerySchemaType = request.query;
const esClient = await getClient(context);
const topNFunctions = await profilingDataAccess.services.fetchFunction({
core,
esClient,
rangeFromMs: timeFrom,
rangeToMs: timeTo,

View file

@ -12,12 +12,18 @@ export async function searchStackTraces({
client,
filter,
sampleSize,
durationSeconds,
}: {
client: ProfilingESClient;
filter: ProjectTimeQuery;
sampleSize: number;
durationSeconds: number;
}) {
const response = await client.profilingStacktraces({ query: filter, sampleSize });
const response = await client.profilingStacktraces({
query: filter,
sampleSize,
durationSeconds,
});
return decodeStackTraceResponse(response);
}

View file

@ -129,10 +129,13 @@ export async function topNElasticSearchQuery({
kuery: stackTraceKuery,
});
const totalSeconds = timeTo - timeFrom;
return searchStackTraces({
client,
filter: stackTraceFilter,
sampleSize: targetSampleSize,
durationSeconds: totalSeconds,
});
}
);

View file

@ -37,6 +37,7 @@ export interface ProfilingESClient {
profilingStacktraces({}: {
query: QueryDslQueryContainer;
sampleSize: number;
durationSeconds: number;
}): Promise<StackTraceResponse>;
profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise<ProfilingStatusResponse>;
getEsClient(): ElasticsearchClient;
@ -75,7 +76,7 @@ export function createProfilingEsClient({
return unwrapEsResponse(promise);
},
profilingStacktraces({ query, sampleSize }) {
profilingStacktraces({ query, sampleSize, durationSeconds }) {
const controller = new AbortController();
const promise = withProfilingSpan('_profiling/stacktraces', () => {
@ -87,6 +88,7 @@ export function createProfilingEsClient({
body: {
query,
sample_size: sampleSize,
requested_duration: durationSeconds,
},
},
{

View file

@ -22,11 +22,25 @@ export interface ProfilingESClient {
profilingStacktraces({}: {
query: QueryDslQueryContainer;
sampleSize: number;
durationSeconds: number;
co2PerKWH?: number;
datacenterPUE?: number;
pervCPUWattX86?: number;
pervCPUWattArm64?: number;
awsCostDiscountRate?: number;
costPervCPUPerHour?: number;
}): Promise<StackTraceResponse>;
profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise<ProfilingStatusResponse>;
getEsClient(): ElasticsearchClient;
profilingFlamegraph({}: {
query: QueryDslQueryContainer;
sampleSize: number;
durationSeconds: number;
co2PerKWH?: number;
datacenterPUE?: number;
pervCPUWattX86?: number;
pervCPUWattArm64?: number;
awsCostDiscountRate?: number;
costPervCPUPerHour?: number;
}): Promise<BaseFlameGraph>;
}

View file

@ -5,62 +5,52 @@
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import { createBaseFlameGraph, createCalleeTree } from '@kbn/profiling-utils';
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
import {
profilingAWSCostDiscountRate,
profilingCo2PerKWH,
profilingCostPervCPUPerHour,
profilingDatacenterPUE,
profilingPervCPUWattArm64,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
import { percentToFactor } from '../../utils/percent_to_factor';
import { kqlQuery } from '../../utils/query';
import { withProfilingSpan } from '../../utils/with_profiling_span';
import { RegisterServicesParams } from '../register_services';
import { searchStackTraces } from '../search_stack_traces';
export interface FetchFlamechartParams {
esClient: ElasticsearchClient;
core: CoreRequestHandlerContext;
rangeFromMs: number;
rangeToMs: number;
kuery: string;
useLegacyFlamegraphAPI?: boolean;
}
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
export function createFetchFlamechart({ createProfilingEsClient }: RegisterServicesParams) {
return async ({
esClient,
rangeFromMs,
rangeToMs,
kuery,
useLegacyFlamegraphAPI = false,
}: FetchFlamechartParams) => {
return async ({ core, esClient, rangeFromMs, rangeToMs, kuery }: FetchFlamechartParams) => {
const rangeFromSecs = rangeFromMs / 1000;
const rangeToSecs = rangeToMs / 1000;
const [
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
] = await Promise.all([
core.uiSettings.client.get<number>(profilingCo2PerKWH),
core.uiSettings.client.get<number>(profilingDatacenterPUE),
core.uiSettings.client.get<number>(profilingPervCPUWattX86),
core.uiSettings.client.get<number>(profilingPervCPUWattArm64),
core.uiSettings.client.get<number>(profilingAWSCostDiscountRate),
core.uiSettings.client.get<number>(profilingCostPervCPUPerHour),
]);
const profilingEsClient = createProfilingEsClient({ esClient });
const totalSeconds = rangeToSecs - rangeFromSecs;
// Use legacy stack traces API to fetch the flamegraph
if (useLegacyFlamegraphAPI) {
const { events, stackTraces, executables, stackFrames, totalFrames, samplingRate } =
await searchStackTraces({
client: profilingEsClient,
rangeFrom: rangeFromSecs,
rangeTo: rangeToSecs,
kuery,
sampleSize: targetSampleSize,
});
return await withProfilingSpan('create_flamegraph', async () => {
const tree = createCalleeTree(
events,
stackTraces,
stackFrames,
executables,
totalFrames,
samplingRate
);
return createBaseFlameGraph(tree, samplingRate, totalSeconds);
});
}
const flamegraph = await profilingEsClient.profilingFlamegraph({
query: {
bool: {
@ -79,6 +69,13 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
},
},
sampleSize: targetSampleSize,
durationSeconds: totalSeconds,
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
costPervCPUPerHour,
});
return { ...flamegraph, TotalSeconds: totalSeconds };
};

View file

@ -4,14 +4,23 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import {
profilingAWSCostDiscountRate,
profilingCo2PerKWH,
profilingCostPervCPUPerHour,
profilingDatacenterPUE,
profilingPervCPUWattArm64,
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
import { createTopNFunctions } from '@kbn/profiling-utils';
import { percentToFactor } from '../../utils/percent_to_factor';
import { withProfilingSpan } from '../../utils/with_profiling_span';
import { RegisterServicesParams } from '../register_services';
import { searchStackTraces } from '../search_stack_traces';
export interface FetchFunctionsParams {
core: CoreRequestHandlerContext;
esClient: ElasticsearchClient;
rangeFromMs: number;
rangeToMs: number;
@ -24,6 +33,7 @@ const targetSampleSize = 20000; // minimum number of samples to get statisticall
export function createFetchFunctions({ createProfilingEsClient }: RegisterServicesParams) {
return async ({
core,
esClient,
rangeFromMs,
rangeToMs,
@ -33,6 +43,23 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
}: FetchFunctionsParams) => {
const rangeFromSecs = rangeFromMs / 1000;
const rangeToSecs = rangeToMs / 1000;
const totalSeconds = rangeToSecs - rangeFromSecs;
const [
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
] = await Promise.all([
core.uiSettings.client.get<number>(profilingCo2PerKWH),
core.uiSettings.client.get<number>(profilingDatacenterPUE),
core.uiSettings.client.get<number>(profilingPervCPUWattX86),
core.uiSettings.client.get<number>(profilingPervCPUWattArm64),
core.uiSettings.client.get<number>(profilingAWSCostDiscountRate),
core.uiSettings.client.get<number>(profilingCostPervCPUPerHour),
]);
const profilingEsClient = createProfilingEsClient({ esClient });
@ -43,6 +70,13 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
rangeTo: rangeToSecs,
kuery,
sampleSize: targetSampleSize,
durationSeconds: totalSeconds,
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
costPervCPUPerHour,
}
);

View file

@ -15,12 +15,26 @@ export async function searchStackTraces({
rangeFrom,
rangeTo,
kuery,
durationSeconds,
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
}: {
client: ProfilingESClient;
sampleSize: number;
rangeFrom: number;
rangeTo: number;
kuery: string;
durationSeconds: number;
co2PerKWH: number;
datacenterPUE: number;
pervCPUWattX86: number;
pervCPUWattArm64: number;
awsCostDiscountRate: number;
costPervCPUPerHour: number;
}) {
const response = await client.profilingStacktraces({
query: {
@ -41,6 +55,13 @@ export async function searchStackTraces({
},
},
sampleSize,
durationSeconds,
co2PerKWH,
datacenterPUE,
pervCPUWattX86,
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
});
return decodeStackTraceResponse(response);

View file

@ -39,7 +39,17 @@ export function createProfilingEsClient({
return unwrapEsResponse(promise);
},
profilingStacktraces({ query, sampleSize }) {
profilingStacktraces({
query,
sampleSize,
durationSeconds,
co2PerKWH,
datacenterPUE,
awsCostDiscountRate,
costPervCPUPerHour,
pervCPUWattArm64,
pervCPUWattX86,
}) {
const controller = new AbortController();
const promise = withProfilingSpan('_profiling/stacktraces', () => {
return esClient.transport.request(
@ -49,6 +59,13 @@ export function createProfilingEsClient({
body: {
query,
sample_size: sampleSize,
requested_duration: durationSeconds,
co2_per_kwh: co2PerKWH,
per_core_watt_x86: pervCPUWattX86,
per_core_watt_arm64: pervCPUWattArm64,
datacenter_pue: datacenterPUE,
aws_cost_factor: awsCostDiscountRate,
cost_per_core_hour: costPervCPUPerHour,
},
},
{
@ -83,7 +100,17 @@ export function createProfilingEsClient({
getEsClient() {
return esClient;
},
profilingFlamegraph({ query, sampleSize }) {
profilingFlamegraph({
query,
sampleSize,
durationSeconds,
co2PerKWH,
datacenterPUE,
awsCostDiscountRate,
costPervCPUPerHour,
pervCPUWattArm64,
pervCPUWattX86,
}) {
const controller = new AbortController();
const promise = withProfilingSpan('_profiling/flamegraph', () => {
@ -94,6 +121,13 @@ export function createProfilingEsClient({
body: {
query,
sample_size: sampleSize,
requested_duration: durationSeconds,
co2_per_kwh: co2PerKWH,
per_core_watt_x86: pervCPUWattX86,
per_core_watt_arm64: pervCPUWattArm64,
datacenter_pue: datacenterPUE,
aws_cost_factor: awsCostDiscountRate,
cost_per_core_hour: costPervCPUPerHour,
},
},
{

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { percentToFactor } from './percent_to_factor'; // Replace 'yourFile' with the actual file path
describe('percentToFactor function', () => {
it('should convert 6% to factor 0.94', () => {
expect(percentToFactor(6)).toBe(0.94);
});
it('should convert 0% to factor 1', () => {
expect(percentToFactor(0)).toBe(1);
});
it('should convert 100% to factor 0', () => {
expect(percentToFactor(100)).toBe(0);
});
it('should handle negative input, convert -10% to factor 1.1', () => {
expect(percentToFactor(-10)).toBe(1.1);
});
it('should handle decimal input, convert 3.5% to factor 0.965', () => {
expect(percentToFactor(3.5)).toBe(0.965);
});
it('should handle large input, convert 1000% to factor -9', () => {
expect(percentToFactor(1000)).toBe(-9);
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const percentToFactor = (percent: number) => 1 - percent / 100;

View file

@ -21,5 +21,6 @@
"@kbn/fleet-plugin",
"@kbn/cloud-plugin",
"@kbn/spaces-plugin",
"@kbn/observability-plugin",
]
}

View file

@ -29313,9 +29313,6 @@
"xpack.observability.profilingDatacenterPUEUiSettingDescription": "L'indicateur d'efficacité énergétique (PUE) des centres de données mesure l'efficacité de l'utilisation de l'énergie par les centres de données. La valeur par défaut de 1,7 correspond au PUE moyen sur site d'un centre de données selon l'enquête {uptimeLink} \n </br></br>\n Vous pouvez également utiliser le PUE qui correspond à votre fournisseur cloud : \n <ul style=\"list-style-type: none;margin-left: 4px;\">\n <li><strong>AWS :</strong> 1,135</li>\n <li><strong>Google Cloud :</strong> 1,1</li>\n <li><strong>Azure :</strong> 1,185</li>\n </ul>\n ",
"xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink": "Uptime Institute",
"xpack.observability.profilingDatacenterPUEUiSettingName": "PUE des centres de données",
"xpack.observability.profilingPerCoreWattUiSettingDescription": "Consommation d'énergie moyenne amortie par cœur (avec une utilisation du CPU à 100 %).",
"xpack.observability.profilingPerCoreWattUiSettingName": "Watts par cœur",
"xpack.observability.profilingUseLegacyFlamegraphAPI": "Utiliser l'API héritée flame-graph dans Universal Profiling",
"xpack.observability.resources.documentation": "Documentation",
"xpack.observability.resources.forum": "Forum de discussion",
"xpack.observability.resources.quick_start": "Vidéos de démarrage rapide",
@ -30236,7 +30233,6 @@
"xpack.profiling.notAvailableLabel": "S. O.",
"xpack.profiling.privilegesWarningDescription": "Des problèmes de privilèges empêchent la vérification du statut d'Universal Profiling. Si vous rencontrez des problèmes ou si les données ne chargent pas, n'hésitez pas à faire appel à votre administrateur.",
"xpack.profiling.privilegesWarningTitle": "Limitation des privilèges utilisateur",
"xpack.profiling.settings.co2Sections": "CO2",
"xpack.profiling.settings.save.error": "Une erreur s'est produite lors de l'enregistrement des paramètres",
"xpack.profiling.settings.saveButton": "Enregistrer les modifications",
"xpack.profiling.settings.title": "Paramètres avancés",

View file

@ -29313,9 +29313,6 @@
"xpack.observability.profilingDatacenterPUEUiSettingDescription": "データセンターの電力使用効率PUEは、データセンターがいかに効率的にエネルギーを使用しているかを測定します。デフォルトは、{uptimeLink}調査によるオンプレミスデータセンターの平均PUEである1.7です \n </br></br>\n また、クラウドプロバイダーに対応するPUEを使用することもできます。 \n <ul style=\"list-style-type: none;margin-left: 4px;\">\n <li><strong>AWS:</strong>1.135</li>\n <li><strong>GCP:</strong>1.1</li>\n <li><strong>Azure:</strong>1.185</li>\n </ul>\n ",
"xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink": "Uptime Institute",
"xpack.observability.profilingDatacenterPUEUiSettingName": "データセンターPUE",
"xpack.observability.profilingPerCoreWattUiSettingDescription": "コア単位の平均償却消費電力CPU使用率100%に基づく)。",
"xpack.observability.profilingPerCoreWattUiSettingName": "コア単位のワット数",
"xpack.observability.profilingUseLegacyFlamegraphAPI": "ユニバーサルプロファイリングでレガシーFlamegraph APIを使用",
"xpack.observability.resources.documentation": "ドキュメント",
"xpack.observability.resources.forum": "ディスカッションフォーラム",
"xpack.observability.resources.quick_start": "クイックスタートビデオ",
@ -30236,7 +30233,6 @@
"xpack.profiling.notAvailableLabel": "N/A",
"xpack.profiling.privilegesWarningDescription": "特権の問題により、ユニバーサルプロファイリングのステータスを確認できませんでした。問題が発生した場合、またはデータの読み込みに失敗した場合は、管理者にお問い合わせください。",
"xpack.profiling.privilegesWarningTitle": "ユーザー特権の制限",
"xpack.profiling.settings.co2Sections": "CO2",
"xpack.profiling.settings.save.error": "設定の保存中にエラーが発生しました",
"xpack.profiling.settings.saveButton": "変更を保存",
"xpack.profiling.settings.title": "高度な設定",

View file

@ -29310,9 +29310,6 @@
"xpack.observability.profilingDatacenterPUEUiSettingDescription": "数据中心电源使用效率 (PUE) 衡量数据中心的能源利用效率。根据 {uptimeLink} 调查,本地数据中心的平均 PUE 默认为 1.7 \n </br></br>\n 您还可以使用与您的云服务提供商对应的 PUE \n <ul style=\"list-style-type: none;margin-left: 4px;\">\n <li><strong>AWS</strong>1.135</li>\n <li><strong>GCP</strong>1.1</li>\n <li><strong>Azure</strong>1.185</li>\n </ul>\n ",
"xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink": "正常运行时间协会",
"xpack.observability.profilingDatacenterPUEUiSettingName": "数据中心 PUE",
"xpack.observability.profilingPerCoreWattUiSettingDescription": "平均分摊每核功耗(基于 100% 的 CPU 利用率)。",
"xpack.observability.profilingPerCoreWattUiSettingName": "每核功耗(瓦)",
"xpack.observability.profilingUseLegacyFlamegraphAPI": "在 Universal Profiling 中使用旧版 Flamegraph API",
"xpack.observability.resources.documentation": "文档",
"xpack.observability.resources.forum": "讨论论坛",
"xpack.observability.resources.quick_start": "快速入门视频",
@ -30233,7 +30230,6 @@
"xpack.profiling.notAvailableLabel": "不可用",
"xpack.profiling.privilegesWarningDescription": "由于权限问题,无法查看 Universal Profiling 状态。如果遇到任何问题或无法加载数据,请联系管理员获取帮助。",
"xpack.profiling.privilegesWarningTitle": "用户权限限制",
"xpack.profiling.settings.co2Sections": "CO2",
"xpack.profiling.settings.save.error": "保存设置时出错",
"xpack.profiling.settings.saveButton": "保存更改",
"xpack.profiling.settings.title": "高级设置",