[Profiling] New settings to control CO2 calculation (#166637)

- Added new Profiling settings so users can customize the CO2 variables
- Fixed Embeddable components to also read the new settings
- Moved code from APM to obs-shared to create the custom settings page
in Profiling.
- New Settings Page was created in Profiling UI so users can easily find
the settings:
<img width="2053" alt="Screenshot 2023-09-22 at 11 18 35"
src="6969b079-745d-4302-8ff2-4f0f256c7f51">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-09-30 10:25:55 +01:00 committed by GitHub
parent 3550650a91
commit 12695646cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 816 additions and 192 deletions

View file

@ -557,6 +557,18 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingPerCoreWatt': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingCo2PerKWH': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingDatacenterPUE': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmEnableCriticalPath': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -155,4 +155,7 @@ export interface UsageStats {
'securitySolution:enableGroupedNav': boolean;
'securitySolution:showRelatedIntegrations': boolean;
'visualization:visualize:legacyGaugeChartsLibrary': boolean;
'observability:profilingPerCoreWatt': number;
'observability:profilingCo2PerKWH': number;
'observability:profilingDatacenterPUE': number;
}

View file

@ -10025,6 +10025,24 @@
"description": "Non-default value of setting."
}
},
"observability:profilingPerCoreWatt": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingCo2PerKWH": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingDatacenterPUE": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:apmEnableCriticalPath": {
"type": "boolean",
"_meta": {

View file

@ -23,8 +23,11 @@ import {
} from '@kbn/observability-plugin/common';
import { isEmpty } from 'lodash';
import React from 'react';
import {
useEditableSettings,
useUiTracker,
} from '@kbn/observability-shared-plugin/public';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmEditableSettings } from '../../../../hooks/use_apm_editable_settings';
import { BottomBarActions } from '../bottom_bar_actions';
const apmSettingsKeys = [
@ -42,6 +45,7 @@ const apmSettingsKeys = [
];
export function GeneralSettings() {
const trackApmEvent = useUiTracker({ app: 'apm' });
const { docLinks, notifications } = useApmPluginContext().core;
const {
handleFieldChange,
@ -50,14 +54,15 @@ export function GeneralSettings() {
saveAll,
isSaving,
cleanUnsavedChanges,
} = useApmEditableSettings(apmSettingsKeys);
} = useEditableSettings('apm', apmSettingsKeys);
async function handleSave() {
try {
const reloadPage = Object.keys(unsavedChanges).some((key) => {
return settingsEditableConfig[key].requiresPageReload;
});
await saveAll({ trackMetricName: 'general_settings_save' });
await saveAll();
trackApmEvent({ metric: 'general_settings_save' });
if (reloadPage) {
window.location.reload();
}

View file

@ -24,8 +24,11 @@ import {
import { LazyField } from '@kbn/advanced-settings-plugin/public';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
useEditableSettings,
useUiTracker,
} from '@kbn/observability-shared-plugin/public';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmEditableSettings } from '../../../../../hooks/use_apm_editable_settings';
import { useFetcher, isPending } from '../../../../../hooks/use_fetcher';
interface Props {
@ -33,6 +36,7 @@ interface Props {
}
export function LabsFlyout({ onClose }: Props) {
const trackApmEvent = useUiTracker({ app: 'apm' });
const { docLinks, notifications } = useApmPluginContext().core;
const { data, status } = useFetcher(
@ -48,7 +52,7 @@ export function LabsFlyout({ onClose }: Props) {
saveAll,
isSaving,
cleanUnsavedChanges,
} = useApmEditableSettings(labsItems);
} = useEditableSettings('apm', labsItems);
async function handleSave() {
try {
@ -56,7 +60,8 @@ export function LabsFlyout({ onClose }: Props) {
return settingsEditableConfig[key].requiresPageReload;
});
await saveAll({ trackMetricName: 'labs_save' });
await saveAll();
trackApmEvent({ metric: 'labs_save' });
if (reloadPage) {
window.location.reload();

View file

@ -41,6 +41,9 @@ export {
enableCriticalPath,
syntheticsThrottlingEnabled,
apmEnableProfilingIntegration,
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from './ui_settings_keys';
export {

View file

@ -27,3 +27,6 @@ export const apmEnableContinuousRollups = 'observability:apmEnableContinuousRoll
export const syntheticsThrottlingEnabled = 'observability:syntheticsThrottlingEnabled';
export const enableLegacyUptimeApp = 'observability:enableLegacyUptimeApp';
export const apmEnableProfilingIntegration = 'observability:apmEnableProfilingIntegration';
export const profilingPerCoreWatt = 'observability:profilingPerCoreWatt';
export const profilingCo2PerKWH = 'observability:profilingCo2PerKWH';
export const profilingDatacenterPUE = 'observability:profilingDatacenterPUE';

View file

@ -30,6 +30,9 @@ import {
syntheticsThrottlingEnabled,
enableLegacyUptimeApp,
apmEnableProfilingIntegration,
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '../common/ui_settings_keys';
const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', {
@ -374,6 +377,70 @@ export const uiSettings: Record<string, UiSettings> = {
schema: schema.boolean(),
requiresPageReload: false,
},
[profilingPerCoreWatt]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingName', {
defaultMessage: 'Per Core Watts',
}),
value: 7,
description: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingDescription', {
defaultMessage: `The average amortized per-core power consumption (based on 100% CPU utilization).`,
}),
schema: schema.number({ min: 0 }),
requiresPageReload: false,
},
[profilingDatacenterPUE]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingName', {
defaultMessage: 'Data Center PUE',
}),
value: 1.7,
description: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingDescription', {
defaultMessage: `Data center power usage effectiveness (PUE) measures how efficiently a data center uses energy. Defaults to 1.7, the average on-premise data center PUE according to the {uptimeLink} survey
</br></br>
You can also use the PUE that corresponds with your cloud provider:
<ul style="list-style-type: none;margin-left: 4px;">
<li><strong>AWS:</strong> 1.135</li>
<li><strong>GCP:</strong> 1.1</li>
<li><strong>Azure:</strong> 1.185</li>
</ul>
`,
values: {
uptimeLink:
'<a href="https://ela.st/uptimeinstitute" target="_blank" rel="noopener noreferrer">' +
i18n.translate(
'xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink',
{ defaultMessage: 'Uptime Institute' }
) +
'</a>',
},
}),
schema: schema.number({ min: 0 }),
requiresPageReload: false,
},
[profilingCo2PerKWH]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.profilingCo2PerKWHUiSettingName', {
defaultMessage: 'Regional Carbon Intensity (ton/kWh)',
}),
value: 0.000379069,
description: i18n.translate('xpack.observability.profilingCo2PerKWHUiSettingDescription', {
defaultMessage: `Carbon intensity measures how clean your data center electricity is.
Specifically, it measures the average amount of CO2 emitted per kilowatt-hour (kWh) of electricity consumed in a particular region.
Use the cloud carbon footprint {datasheetLink} to update this value according to your region. Defaults to US East (N. Virginia).`,
values: {
datasheetLink:
'<a href="https://ela.st/grid-datasheet" target="_blank" rel="noopener noreferrer">' +
i18n.translate(
'xpack.observability.profilingCo2PerKWHUiSettingDescription.datasheetLink',
{ defaultMessage: 'datasheet' }
) +
'</a>',
},
}),
schema: schema.number({ min: 0 }),
requiresPageReload: false,
},
};
function throttlingDocsLink({ href }: { href: string }) {

View file

@ -9,7 +9,7 @@
"configPath": ["xpack", "observability_shared"],
"requiredPlugins": ["cases", "guidedOnboarding", "uiActions", "embeddable", "share"],
"optionalPlugins": [],
"requiredBundles": ["data", "inspector", "kibanaReact", "kibanaUtils"],
"requiredBundles": ["data", "inspector", "kibanaReact", "kibanaUtils", "advancedSettings"],
"extraPublicDirs": ["common"]
}
}

View file

@ -10,7 +10,7 @@ import { FieldState } from '@kbn/advanced-settings-plugin/public';
import { toEditableConfig } from '@kbn/advanced-settings-plugin/public';
import { IUiSettingsClient } from '@kbn/core/public';
import { isEmpty } from 'lodash';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { ObservabilityApp } from '../../typings/common';
function getEditableConfig({
settingsKeys,
@ -41,15 +41,13 @@ function getEditableConfig({
return config;
}
export function useApmEditableSettings(settingsKeys: string[]) {
export function useEditableSettings(app: ObservabilityApp, settingsKeys: string[]) {
const { services } = useKibana();
const trackApmEvent = useUiTracker({ app: 'apm' });
const { uiSettings } = services;
const [isSaving, setIsSaving] = useState(false);
const [forceReloadSettings, setForceReloadSettings] = useState(0);
const [unsavedChanges, setUnsavedChanges] = useState<
Record<string, FieldState>
>({});
const [unsavedChanges, setUnsavedChanges] = useState<Record<string, FieldState>>({});
const settingsEditableConfig = useMemo(
() => {
@ -78,7 +76,7 @@ export function useApmEditableSettings(settingsKeys: string[]) {
setUnsavedChanges({});
}
async function saveAll({ trackMetricName }: { trackMetricName: string }) {
async function saveAll() {
if (uiSettings && !isEmpty(unsavedChanges)) {
try {
setIsSaving(true);
@ -87,7 +85,6 @@ export function useApmEditableSettings(settingsKeys: string[]) {
);
await Promise.all(arr);
trackApmEvent({ metric: trackMetricName });
setForceReloadSettings((state) => ++state);
cleanUnsavedChanges();
} finally {

View file

@ -43,6 +43,7 @@ export type { AddInspectorRequest } from './contexts/inspector/inspector_context
export { useInspectorContext } from './contexts/inspector/use_inspector_context';
export { useTheme } from './hooks/use_theme';
export { useEditableSettings } from './hooks/use_editable_settings';
export { useEsSearch, createEsParams } from './hooks/use_es_search';
export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher';
export type { FetcherResult } from './hooks/use_fetcher';

View file

@ -34,6 +34,7 @@
"@kbn/shared-ux-router",
"@kbn/embeddable-plugin",
"@kbn/profiling-utils",
"@kbn/advanced-settings-plugin",
"@kbn/utility-types",
"@kbn/share-plugin"
],

View file

@ -1,85 +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.
*/
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
// The assumed amortized per-core average power consumption (based on 100% CPU Utilization).
// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-i-energy-coefficients
const PER_CORE_WATT = 7;
// The assumed CO2 emissions in kg per kWh (the reference uses metric tons/kWh).
// This value represents "regional carbon intensity" and it defaults to AWS us-east-1.
// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-v-grid-emissions-factors
const CO2_PER_KWH = 0.379069;
// The assumed PUE of the datacenter (1.7 is likely to be an on-prem value).
const DATACENTER_PUE = 1.7;
// 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/)
const CORE_COST_PER_HOUR = 0.0425;
export function calculateImpactEstimates({
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
}: {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
}) {
return {
totalSamples: calculateImpact({
samples: totalSamples,
totalSamples,
totalSeconds,
}),
totalCPU: calculateImpact({
samples: countInclusive,
totalSamples,
totalSeconds,
}),
selfCPU: calculateImpact({
samples: countExclusive,
totalSamples,
totalSeconds,
}),
};
}
function calculateImpact({
samples,
totalSamples,
totalSeconds,
}: {
samples: number;
totalSamples: number;
totalSeconds: number;
}) {
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
const totalCoreSeconds = totalSamples / 20;
const percentage = samples / totalSamples;
const coreSeconds = totalCoreSeconds * percentage;
const annualizedCoreSeconds = coreSeconds * annualizedScaleUp;
const coreHours = coreSeconds / (60 * 60);
const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH * DATACENTER_PUE;
const annualizedCo2 = co2 * annualizedScaleUp;
const dollarCost = coreHours * CORE_COST_PER_HOUR;
const annualizedDollarCost = dollarCost * annualizedScaleUp;
return {
percentage,
coreSeconds,
annualizedCoreSeconds,
co2,
annualizedCo2,
dollarCost,
annualizedDollarCost,
};
}

View file

@ -172,6 +172,13 @@ describe('Differential Functions page', () => {
'have.length.gt',
1
);
cy.get(
'[data-test-subj="topNFunctionsGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
).contains('vmlinux');
cy.get(
'[data-test-subj="TopNFunctionsComparisonGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
).contains('vmlinux');
cy.addKqlFilter({
key: 'process.thread.name',
value: '108795321966692',
@ -183,38 +190,12 @@ describe('Differential Functions page', () => {
});
cy.wait('@getTopNFunctions');
cy.wait('@getTopNFunctions');
cy.get('[data-test-subj="topNFunctionsGrid"] .euiDataGridRow').should('have.length', 2);
cy.get('[data-test-subj="TopNFunctionsComparisonGrid"] .euiDataGridRow').should(
'have.length',
1
);
[
{ id: 'overallPerformance', value: '50.00%', icon: 'sortUp_success' },
{
id: 'annualizedCo2',
value: '0.13 lbs / 0.06 kg',
comparisonValue: '0.07 lbs / 0.03 kg (50.00%)',
icon: 'comparison_sortUp_success',
},
{
id: 'annualizedCost',
value: '$1.24',
comparisonValue: '$0.62 (50.00%)',
icon: 'comparison_sortUp_success',
},
{
id: 'totalNumberOfSamples',
value: '2',
comparisonValue: '1 (50.00%)',
icon: 'comparison_sortUp_success',
},
].forEach((item) => {
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
cy.get(`[data-test-subj="${item.id}_${item.icon}"]`).should('exist');
if (item.comparisonValue) {
cy.get(`[data-test-subj="${item.id}_comparison_value"]`).contains(item.comparisonValue);
}
});
cy.get(
'[data-test-subj="topNFunctionsGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
).contains('libsystemd-shared-237.so');
cy.get(
'[data-test-subj="TopNFunctionsComparisonGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
).contains('libjvm.so');
});
});
});

View file

@ -4,6 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '@kbn/observability-plugin/common';
describe('Functions page', () => {
const rangeFrom = '2023-04-18T00:00:00.000Z';
@ -80,11 +85,10 @@ describe('Functions page', () => {
cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions');
cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo });
cy.wait('@getTopNFunctions');
cy.get('.euiDataGridRow').should('have.length.gt', 1);
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
cy.get(firstRowSelector).eq(2).contains('vmlinux');
cy.addKqlFilter({ key: 'Stacktrace.id', value: '-7DvnP1mizQYw8mIIpgbMg' });
cy.wait('@getTopNFunctions');
cy.get('.euiDataGridRow').should('have.length', 1);
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
cy.get(firstRowSelector).eq(2).contains('libjvm.so');
});
@ -96,50 +100,50 @@ describe('Functions page', () => {
{
columnKey: 'rank',
columnIndex: 1,
highRank: 388,
highRank: 4481,
lowRank: 1,
highValue: 388,
highValue: 4481,
lowValue: 1,
},
{
columnKey: 'samples',
columnIndex: 7,
highRank: 1,
lowRank: 44,
lowRank: 389,
highValue: 28,
lowValue: 1,
lowValue: 0,
},
{
columnKey: 'selfCPU',
columnIndex: 3,
highRank: 1,
lowRank: 44,
lowRank: 389,
highValue: '5.46%',
lowValue: '0.19%',
lowValue: '0.00%',
},
{
columnKey: 'totalCPU',
columnIndex: 4,
highRank: 338,
highRank: 3623,
lowRank: 44,
highValue: '10.33%',
highValue: '60.43%',
lowValue: '0.19%',
},
{
columnKey: 'annualizedCo2',
columnIndex: 5,
highRank: 1,
lowRank: 44,
lowRank: 389,
highValue: '1.84 lbs / 0.84 kg',
lowValue: '0.07 lbs / 0.03 kg',
lowValue: undefined,
},
{
columnKey: 'annualizedDollarCost',
columnIndex: 6,
highRank: 1,
lowRank: 44,
lowRank: 389,
highValue: '$17.37',
lowValue: '$0.62',
lowValue: undefined,
},
].forEach(({ columnKey, columnIndex, highRank, highValue, lowRank, lowValue }) => {
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
@ -151,7 +155,11 @@ describe('Functions page', () => {
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
cy.contains('Sort Low-High').click();
cy.get(firstRowSelector).eq(1).contains(lowRank);
cy.get(firstRowSelector).eq(columnIndex).contains(lowValue);
if (lowValue !== undefined) {
cy.get(firstRowSelector).eq(columnIndex).contains(lowValue);
} else {
cy.get(firstRowSelector).eq(columnIndex).should('not.have.value');
}
});
cy.get(`[data-test-subj="dataGridHeaderCell-frame"]`).click();
@ -165,4 +173,58 @@ describe('Functions page', () => {
cy.get(firstRowSelector).eq(1).contains('371');
cy.get(firstRowSelector).eq(2).contains('/');
});
describe('Test changing CO2 settings', () => {
afterEach(() => {
cy.updateAdvancedSettings({
[profilingCo2PerKWH]: 0.000379069,
[profilingDatacenterPUE]: 1.7,
[profilingPerCoreWatt]: 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 });
cy.wait('@getTopNFunctions');
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
cy.get(firstRowSelector).eq(1).contains('1');
cy.get(firstRowSelector).eq(2).contains('vmlinux');
cy.get(firstRowSelector).eq(5).contains('1.84 lbs / 0.84 kg');
cy.contains('Settings').click();
cy.contains('Advanced Settings');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingCo2PerKWH}"]`)
.clear()
.type('0.12345');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
.clear()
.type('2.4');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
.clear()
.type('20');
cy.contains('Save changes').click();
cy.go('back');
cy.wait('@getTopNFunctions');
cy.get(firstRowSelector).eq(5).contains('24.22k lbs / 10.99k');
const firstRowSelectorActionButton =
'[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"] .euiButtonIcon';
cy.get(firstRowSelectorActionButton).click();
[
{ parentKey: 'impactEstimates', key: 'co2Emission', value: '0.02 lbs / 0.01 kg' },
{ parentKey: 'impactEstimates', key: 'selfCo2Emission', value: '0.02 lbs / 0.01 kg' },
{
parentKey: 'impactEstimates',
key: 'annualizedCo2Emission',
value: '24.22k lbs / 10.99k kg',
},
{
parentKey: 'impactEstimates',
key: 'annualizedSelfCo2Emission',
value: '24.22k lbs / 10.99k kg',
},
].forEach(({ parentKey, key, value }) => {
cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value);
});
});
});
});

View file

@ -0,0 +1,58 @@
/*
* 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.
*/
/*
* 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 {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '@kbn/observability-plugin/common';
describe('Settings page', () => {
beforeEach(() => {
cy.loginAsElastic();
});
afterEach(() => {
cy.updateAdvancedSettings({
[profilingCo2PerKWH]: 0.000379069,
[profilingDatacenterPUE]: 1.7,
[profilingPerCoreWatt]: 7,
});
});
it('opens setting page', () => {
cy.visitKibana('/app/profiling/settings');
cy.contains('Advanced Settings');
cy.contains('CO2');
cy.contains('Regional Carbon Intensity (ton/kWh)');
cy.contains('Data Center PUE');
cy.contains('Per Core Watts');
});
it('updates values', () => {
cy.visitKibana('/app/profiling/settings');
cy.contains('Advanced Settings');
cy.get('[data-test-subj="profilingBottomBarActions"]').should('not.exist');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingCo2PerKWH}"]`)
.clear()
.type('0.12345');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
.clear()
.type('2.4');
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
.clear()
.type('20');
cy.get('[data-test-subj="profilingBottomBarActions"]').should('exist');
cy.contains('Save changes').click();
cy.get('[data-test-subj="profilingBottomBarActions"]').should('not.exist');
});
});

View file

@ -71,3 +71,17 @@ Cypress.Commands.add(
cy.getByTestSubj(dataTestSubj).type('{enter}');
}
);
Cypress.Commands.add('updateAdvancedSettings', (settings: Record<string, unknown>) => {
const kibanaUrl = Cypress.env('KIBANA_URL');
cy.request({
log: false,
method: 'POST',
url: `${kibanaUrl}/internal/kibana/settings`,
body: { changes: settings },
headers: {
'kbn-xsrf': 'e2e_test',
},
auth: { user: 'elastic', pass: 'changeme' },
});
});

View file

@ -27,5 +27,6 @@ declare namespace Cypress {
dataTestSubj?: 'profilingUnifiedSearchBar' | 'profilingComparisonUnifiedSearchBar';
waitForSuggestion?: boolean;
}): void;
updateAdvancedSettings(settings: Record<string, unknown>): void;
}
}

View file

@ -22,5 +22,6 @@
"@kbn/test",
"@kbn/dev-utils",
"@kbn/cypress-config",
"@kbn/observability-plugin",
]
}

View file

@ -30,7 +30,8 @@
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"observabilityAIAssistant"
"observabilityAIAssistant",
"advancedSettings"
]
}
}

View file

@ -18,12 +18,12 @@ import {
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
import { calculateImpactEstimates } from '../../../common/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 { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
interface Props {
isRoot: boolean;
@ -57,6 +57,7 @@ export function FlameGraphTooltip({
onShowMoreClick,
}: Props) {
const theme = useEuiTheme();
const calculateImpactEstimates = useCalculateImpactEstimate();
const impactEstimates = calculateImpactEstimates({
countExclusive,

View file

@ -194,7 +194,6 @@ export function FlameGraph({
frame={selected}
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
totalSamples={totalSamples}
showAIAssistant={!isEmbedded}
showSymbolsStatus={!isEmbedded}
/>
)}

View file

@ -7,24 +7,26 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
import { asCost } from '../../utils/formatters/as_cost';
import { asDuration } from '../../utils/formatters/as_duration';
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';
export function getImpactRows({
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
calculateImpactEstimates,
}: {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
calculateImpactEstimates: CalculateImpactEstimates;
}) {
const { selfCPU, totalCPU } = calculateImpactEstimates({
countInclusive,

View file

@ -14,6 +14,7 @@ import { getImpactRows } from './get_impact_rows';
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';
export interface Frame {
fileID: string;
@ -39,9 +40,10 @@ export function FrameInformationWindow({
frame,
totalSamples,
totalSeconds,
showAIAssistant = true,
showSymbolsStatus = true,
}: Props) {
const calculateImpactEstimates = useCalculateImpactEstimate();
if (!frame) {
return (
<FrameInformationPanel>
@ -87,6 +89,7 @@ export function FrameInformationWindow({
countExclusive,
totalSamples,
totalSeconds,
calculateImpactEstimates,
});
return (
@ -95,11 +98,9 @@ export function FrameInformationWindow({
<EuiFlexItem>
<KeyValueList data-test-subj="informationRows" rows={informationRows} />
</EuiFlexItem>
{showAIAssistant ? (
<EuiFlexItem>
<FrameInformationAIAssistant frame={frame} />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<FrameInformationAIAssistant frame={frame} />
</EuiFlexItem>
{showSymbolsStatus && symbolStatus !== FrameSymbolStatus.SYMBOLIZED ? (
<EuiFlexItem>
<MissingSymbolsCallout frameType={frame.frameType} />

View file

@ -76,6 +76,11 @@ export function ProfilingHeaderActionMenu() {
</EuiFlexItem>
</EuiFlexGroup>
</EuiHeaderLink>
<EuiHeaderLink href={router.link('/settings')} color="text">
{i18n.translate('xpack.profiling.headerActionMenu.settings', {
defaultMessage: 'Settings',
})}
</EuiHeaderLink>
<ObservabilityAIAssistantActionMenuItem />
</EuiHeaderLinks>
);

View file

@ -26,6 +26,7 @@ import { FrameInformationTooltip } from '../frame_information_window/frame_infor
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;
@ -70,6 +71,7 @@ export const TopNFunctionsGrid = forwardRef(
) => {
const [selectedRow, setSelectedRow] = useState<IFunctionRow | undefined>();
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
const calculateImpactEstimates = useCalculateImpactEstimate();
function onSort(newSortingColumns: EuiDataGridSorting['columns']) {
const lastItem = last(newSortingColumns);
@ -93,9 +95,11 @@ export const TopNFunctionsGrid = forwardRef(
comparisonTopNFunctions,
topNFunctions,
totalSeconds,
calculateImpactEstimates,
});
}, [
baselineScaleFactor,
calculateImpactEstimates,
comparisonScaleFactor,
comparisonTopNFunctions,
topNFunctions,

View file

@ -6,7 +6,10 @@
*/
import { keyBy } from 'lodash';
import type { StackFrameMetadata, TopNFunctions } from '@kbn/profiling-utils';
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
import {
CalculateImpactEstimates,
ImpactEstimates,
} from '../../hooks/use_calculate_impact_estimates';
export function getColorLabel(percent: number) {
if (percent === 0) {
@ -37,7 +40,7 @@ export interface IFunctionRow {
totalCPU: number;
selfCPUPerc: number;
totalCPUPerc: number;
impactEstimates?: ReturnType<typeof calculateImpactEstimates>;
impactEstimates?: ImpactEstimates;
diff?: {
rank: number;
samples: number;
@ -45,7 +48,7 @@ export interface IFunctionRow {
totalCPU: number;
selfCPUPerc: number;
totalCPUPerc: number;
impactEstimates?: ReturnType<typeof calculateImpactEstimates>;
impactEstimates?: ImpactEstimates;
};
}
@ -55,12 +58,14 @@ export function getFunctionsRows({
comparisonTopNFunctions,
topNFunctions,
totalSeconds,
calculateImpactEstimates,
}: {
baselineScaleFactor?: number;
comparisonScaleFactor?: number;
comparisonTopNFunctions?: TopNFunctions;
topNFunctions?: TopNFunctions;
totalSeconds: number;
calculateImpactEstimates: CalculateImpactEstimates;
}): IFunctionRow[] {
if (!topNFunctions || !topNFunctions.TotalCount || topNFunctions.TotalCount === 0) {
return [];
@ -70,7 +75,7 @@ export function getFunctionsRows({
? keyBy(comparisonTopNFunctions.TopN, 'Id')
: {};
return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0).map((topN, i) => {
return topNFunctions.TopN.filter((topN) => topN.CountExclusive >= 0).map((topN, i) => {
const comparisonRow = comparisonDataById?.[topN.Id];
const scaledSelfCPU = scaleValue({

View file

@ -9,11 +9,11 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import type { TopNFunctions } from '@kbn/profiling-utils';
import { calculateImpactEstimates } from '../../../common/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 { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
interface Props {
baselineTopNFunctions?: TopNFunctions;
@ -38,6 +38,8 @@ export function TopNFunctionsSummary({
baselineDuration,
comparisonDuration,
}: Props) {
const calculateImpactEstimates = useCalculateImpactEstimate();
const baselineScaledTotalSamples = baselineTopNFunctions
? baselineTopNFunctions.TotalCount * baselineScaleFactor
: 0;
@ -87,6 +89,7 @@ export function TopNFunctionsSummary({
baselineDuration,
baselineScaledTotalSamples,
baselineTopNFunctions,
calculateImpactEstimates,
comparisonDuration,
comparisonScaledTotalSamples,
comparisonTopNFunctions,

View file

@ -4,14 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
import { Embeddable, EmbeddableOutput, IContainer } from '@kbn/embeddable-plugin/public';
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
import { createFlameGraph } from '@kbn/profiling-utils';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { createFlameGraph } from '@kbn/profiling-utils';
import { FlameGraph } from '../../components/flamegraph';
import { EmbeddableFlamegraphEmbeddableInput } from './embeddable_flamegraph_factory';
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
import {
ProfilingEmbeddableProvider,
ProfilingEmbeddablesDependencies,
} from '../profiling_embeddable_provider';
import { EmbeddableFlamegraphEmbeddableInput } from './embeddable_flamegraph_factory';
export class EmbeddableFlamegraph extends Embeddable<
EmbeddableFlamegraphEmbeddableInput,
@ -20,18 +24,29 @@ export class EmbeddableFlamegraph extends Embeddable<
readonly type = EMBEDDABLE_FLAMEGRAPH;
private _domNode?: HTMLElement;
constructor(
private deps: ProfilingEmbeddablesDependencies,
initialInput: EmbeddableFlamegraphEmbeddableInput,
parent?: IContainer
) {
super(initialInput, {}, parent);
}
render(domNode: HTMLElement) {
this._domNode = domNode;
const { data, isLoading } = this.input;
const flamegraph = !isLoading && data ? createFlameGraph(data) : undefined;
render(
<AsyncEmbeddableComponent isLoading={isLoading}>
<>
{flamegraph && (
<FlameGraph primaryFlamegraph={flamegraph} id="embddable_profiling" isEmbedded />
)}
</>
</AsyncEmbeddableComponent>,
<ProfilingEmbeddableProvider deps={this.deps}>
<AsyncEmbeddableComponent isLoading={isLoading}>
<>
{flamegraph && (
<FlameGraph primaryFlamegraph={flamegraph} id="embddable_profiling" isEmbedded />
)}
</>
</AsyncEmbeddableComponent>
</ProfilingEmbeddableProvider>,
domNode
);
}

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import {
IContainer,
EmbeddableInput,
EmbeddableFactoryDefinition,
EmbeddableInput,
IContainer,
} from '@kbn/embeddable-plugin/public';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import type { GetProfilingEmbeddableDependencies } from '../profiling_embeddable_provider';
interface EmbeddableFlamegraphInput {
data?: BaseFlameGraph;
@ -24,13 +25,16 @@ export class EmbeddableFlamegraphFactory
{
readonly type = EMBEDDABLE_FLAMEGRAPH;
constructor(private getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies) {}
async isEditable() {
return false;
}
async create(input: EmbeddableFlamegraphEmbeddableInput, parent?: IContainer) {
const { EmbeddableFlamegraph } = await import('./embeddable_flamegraph');
return new EmbeddableFlamegraph(input, {}, parent);
const deps = await this.getProfilingEmbeddableDependencies();
return new EmbeddableFlamegraph(deps, input, parent);
}
getDisplayName() {

View file

@ -4,13 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
import { Embeddable, EmbeddableOutput, IContainer } from '@kbn/embeddable-plugin/public';
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
import { EmbeddableFunctionsEmbeddableInput } from './embeddable_functions_factory';
import { EmbeddableFunctionsGrid } from './embeddable_functions_grid';
import {
ProfilingEmbeddableProvider,
ProfilingEmbeddablesDependencies,
} from '../profiling_embeddable_provider';
export class EmbeddableFunctions extends Embeddable<
EmbeddableFunctionsEmbeddableInput,
@ -19,16 +23,26 @@ export class EmbeddableFunctions extends Embeddable<
readonly type = EMBEDDABLE_FUNCTIONS;
private _domNode?: HTMLElement;
constructor(
private deps: ProfilingEmbeddablesDependencies,
initialInput: EmbeddableFunctionsEmbeddableInput,
parent?: IContainer
) {
super(initialInput, {}, parent);
}
render(domNode: HTMLElement) {
this._domNode = domNode;
const { data, isLoading, rangeFrom, rangeTo } = this.input;
const totalSeconds = (rangeTo - rangeFrom) / 1000;
render(
<AsyncEmbeddableComponent isLoading={isLoading}>
<div style={{ width: '100%' }}>
<EmbeddableFunctionsGrid data={data} totalSeconds={totalSeconds} />
</div>
</AsyncEmbeddableComponent>,
<ProfilingEmbeddableProvider deps={this.deps}>
<AsyncEmbeddableComponent isLoading={isLoading}>
<div style={{ width: '100%' }}>
<EmbeddableFunctionsGrid data={data} totalSeconds={totalSeconds} />
</div>
</AsyncEmbeddableComponent>
</ProfilingEmbeddableProvider>,
domNode
);
}

View file

@ -11,6 +11,7 @@ import {
} from '@kbn/embeddable-plugin/public';
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
import type { TopNFunctions } from '@kbn/profiling-utils';
import { GetProfilingEmbeddableDependencies } from '../profiling_embeddable_provider';
interface EmbeddableFunctionsInput {
data?: TopNFunctions;
@ -26,13 +27,16 @@ export class EmbeddableFunctionsFactory
{
readonly type = EMBEDDABLE_FUNCTIONS;
constructor(private getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies) {}
async isEditable() {
return false;
}
async create(input: EmbeddableFunctionsEmbeddableInput, parent?: IContainer) {
const { EmbeddableFunctions } = await import('./embeddable_functions');
return new EmbeddableFunctions(input, {}, parent);
const deps = await this.getProfilingEmbeddableDependencies();
return new EmbeddableFunctions(deps, input, parent);
}
getDisplayName() {

View file

@ -0,0 +1,56 @@
/*
* 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
import React, { ReactChild, useMemo } from 'react';
import { CoreSetup, CoreStart } from '@kbn/core/public';
import { ProfilingDependenciesContextProvider } from '../components/contexts/profiling_dependencies/profiling_dependencies_context';
import { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from '../types';
import { Services } from '../services';
export interface ProfilingEmbeddablesDependencies {
coreStart: CoreStart;
coreSetup: CoreSetup;
pluginsStart: ProfilingPluginPublicStartDeps;
pluginsSetup: ProfilingPluginPublicSetupDeps;
profilingFetchServices: Services;
}
export type GetProfilingEmbeddableDependencies = () => Promise<ProfilingEmbeddablesDependencies>;
interface Props {
deps: ProfilingEmbeddablesDependencies;
children: ReactChild;
}
export function ProfilingEmbeddableProvider({ deps, children }: Props) {
const profilingDependencies = useMemo(
() => ({
start: {
core: deps.coreStart,
...deps.pluginsStart,
},
setup: {
core: deps.coreSetup,
...deps.pluginsSetup,
},
services: deps.profilingFetchServices,
}),
[deps]
);
return (
<KibanaContextProvider services={{ ...deps.coreStart, ...deps.pluginsStart }}>
<ProfilingDependenciesContextProvider value={profilingDependencies}>
<ObservabilityAIAssistantProvider value={deps.pluginsStart.observabilityAIAssistant}>
{children}
</ObservabilityAIAssistantProvider>
</ProfilingDependenciesContextProvider>
</KibanaContextProvider>
);
}

View file

@ -12,8 +12,18 @@ import {
} from '@kbn/observability-shared-plugin/public';
import { EmbeddableFlamegraphFactory } from './flamegraph/embeddable_flamegraph_factory';
import { EmbeddableFunctionsFactory } from './functions/embeddable_functions_factory';
import { GetProfilingEmbeddableDependencies } from './profiling_embeddable_provider';
export function registerEmbeddables(embeddable: EmbeddableSetup) {
embeddable.registerEmbeddableFactory(EMBEDDABLE_FLAMEGRAPH, new EmbeddableFlamegraphFactory());
embeddable.registerEmbeddableFactory(EMBEDDABLE_FUNCTIONS, new EmbeddableFunctionsFactory());
export function registerEmbeddables(
embeddable: EmbeddableSetup,
getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies
) {
embeddable.registerEmbeddableFactory(
EMBEDDABLE_FLAMEGRAPH,
new EmbeddableFlamegraphFactory(getProfilingEmbeddableDependencies)
);
embeddable.registerEmbeddableFactory(
EMBEDDABLE_FUNCTIONS,
new EmbeddableFunctionsFactory(getProfilingEmbeddableDependencies)
);
}

View file

@ -4,10 +4,41 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { calculateImpactEstimates } from '.';
import { useCalculateImpactEstimate } from './use_calculate_impact_estimates';
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '@kbn/observability-plugin/common';
jest.mock('../components/contexts/profiling_dependencies/use_profiling_dependencies');
describe('useCalculateImpactEstimate', () => {
beforeAll(() => {
(useProfilingDependencies as jest.Mock).mockReturnValue({
start: {
core: {
uiSettings: {
get: (key: string) => {
if (key === profilingPerCoreWatt) {
return 7;
}
if (key === profilingCo2PerKWH) {
return 0.000379069;
}
if (key === profilingDatacenterPUE) {
return 1.7;
}
},
},
},
},
});
});
describe('calculateImpactEstimates', () => {
it('calculates impact when countExclusive is lower than countInclusive', () => {
const calculateImpactEstimates = useCalculateImpactEstimate();
const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({
countExclusive: 500,
countInclusive: 1000,
@ -47,6 +78,7 @@ describe('calculateImpactEstimates', () => {
});
it('calculates impact', () => {
const calculateImpactEstimates = useCalculateImpactEstimate();
const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({
countExclusive: 1000,
countInclusive: 1000,

View file

@ -0,0 +1,91 @@
/*
* 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 {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '@kbn/observability-plugin/common';
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
interface Params {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
}
export type CalculateImpactEstimates = ReturnType<typeof useCalculateImpactEstimate>;
export type ImpactEstimates = ReturnType<CalculateImpactEstimates>;
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/)
const CORE_COST_PER_HOUR = 0.0425;
export function useCalculateImpactEstimate() {
const {
start: { core },
} = useProfilingDependencies();
const perCoreWatts = core.uiSettings.get<number>(profilingPerCoreWatt);
const co2PerTonKWH = core.uiSettings.get<number>(profilingCo2PerKWH);
const datacenterPUE = core.uiSettings.get<number>(profilingDatacenterPUE);
function calculateImpact({
samples,
totalSamples,
totalSeconds,
}: {
samples: number;
totalSamples: number;
totalSeconds: number;
}) {
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
const totalCoreSeconds = totalSamples / 20;
const percentage = samples / totalSamples;
const coreSeconds = totalCoreSeconds * percentage;
const annualizedCoreSeconds = coreSeconds * annualizedScaleUp;
const coreHours = coreSeconds / (60 * 60);
const co2PerKWH = co2PerTonKWH * 1000;
const co2 = ((perCoreWatts * coreHours) / 1000.0) * co2PerKWH * datacenterPUE;
const annualizedCo2 = co2 * annualizedScaleUp;
const dollarCost = coreHours * CORE_COST_PER_HOUR;
const annualizedDollarCost = dollarCost * annualizedScaleUp;
return {
percentage,
coreSeconds,
annualizedCoreSeconds,
co2,
annualizedCo2,
dollarCost,
annualizedDollarCost,
};
}
return (params: Params) => {
return {
totalSamples: calculateImpact({
samples: params.totalSamples,
totalSamples: params.totalSamples,
totalSeconds: params.totalSeconds,
}),
totalCPU: calculateImpact({
samples: params.countInclusive,
totalSamples: params.totalSamples,
totalSeconds: params.totalSeconds,
}),
selfCPU: calculateImpact({
samples: params.countExclusive,
totalSamples: params.totalSamples,
totalSeconds: params.totalSeconds,
}),
};
};
}

View file

@ -19,6 +19,7 @@ import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
import { registerEmbeddables } from './embeddables/register_embeddables';
import { getServices } from './services';
import type { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from './types';
import { ProfilingEmbeddablesDependencies } from './embeddables/profiling_embeddable_provider';
export type ProfilingPluginSetup = void;
export type ProfilingPluginStart = void;
@ -81,6 +82,8 @@ export class ProfilingPlugin implements Plugin {
pluginsSetup.observabilityShared.navigation.registerSections(section$);
const profilingFetchServices = getServices();
coreSetup.application.register({
id: 'profiling',
title: 'Universal Profiling',
@ -95,7 +98,6 @@ export class ProfilingPlugin implements Plugin {
unknown
];
const profilingFetchServices = getServices();
const { renderApp } = await import('./app');
function pushKueryToSubject(location: Location) {
@ -128,7 +130,23 @@ export class ProfilingPlugin implements Plugin {
},
});
registerEmbeddables(pluginsSetup.embeddable);
const getProfilingEmbeddableDependencies =
async (): Promise<ProfilingEmbeddablesDependencies> => {
const [coreStart, pluginsStart] = (await coreSetup.getStartServices()) as [
CoreStart,
ProfilingPluginPublicStartDeps,
unknown
];
return {
coreStart,
coreSetup,
pluginsStart,
pluginsSetup,
profilingFetchServices,
};
};
registerEmbeddables(pluginsSetup.embeddable, getProfilingEmbeddableDependencies);
return {};
}

View file

@ -32,8 +32,21 @@ import { StackTracesView } from '../views/stack_traces_view';
import { StorageExplorerView } from '../views/storage_explorer';
import { RouteBreadcrumb } from './route_breadcrumb';
import { DeleteDataView } from '../views/delete_data_view';
import { Settings } from '../views/settings';
const routes = {
'/settings': {
element: (
<RouteBreadcrumb
title={i18n.translate('xpack.profiling.breadcrumb.settings', {
defaultMessage: 'Settings',
})}
href="/settings"
>
<Settings />
</RouteBreadcrumb>
),
},
'/': {
element: (
<RouteBreadcrumb

View file

@ -0,0 +1,83 @@
/*
* 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 {
EuiBottomBar,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
interface Props {
unsavedChangesCount: number;
isLoading: boolean;
onDiscardChanges: () => void;
onSave: () => void;
saveLabel: string;
}
export function BottomBarActions({
isLoading,
onDiscardChanges,
onSave,
unsavedChangesCount,
saveLabel,
}: Props) {
return (
<EuiBottomBar paddingSize="s" data-test-subj="profilingBottomBarActions">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem
grow={false}
style={{
flexDirection: 'row',
alignItems: 'center',
}}
>
<EuiHealth color="warning" />
<EuiText color="ghost">
{i18n.translate('xpack.profiling.bottomBarActions.unsavedChanges', {
defaultMessage:
'{unsavedChangesCount, plural, =0{0 unsaved changes} one {1 unsaved change} other {# unsaved changes}} ',
values: { unsavedChangesCount },
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="profilingBottomBarActionsDiscardChangesButton"
color="ghost"
onClick={onDiscardChanges}
>
{i18n.translate('xpack.profiling.bottomBarActions.discardChangesButton', {
defaultMessage: 'Discard changes',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="profilingBottomBarActionsButton"
onClick={onSave}
fill
isLoading={isLoading}
color="success"
iconType="check"
>
{saveLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
);
}

View file

@ -0,0 +1,115 @@
/*
* 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 { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { LazyField } from '@kbn/advanced-settings-plugin/public';
import { i18n } from '@kbn/i18n';
import {
profilingCo2PerKWH,
profilingDatacenterPUE,
profilingPerCoreWatt,
} from '@kbn/observability-plugin/common';
import { useEditableSettings, useUiTracker } from '@kbn/observability-shared-plugin/public';
import { isEmpty } from 'lodash';
import React from 'react';
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { BottomBarActions } from './bottom_bar_actions';
const settingKeys = [profilingCo2PerKWH, profilingDatacenterPUE, profilingPerCoreWatt];
export function Settings() {
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
const {
start: {
core: { docLinks, notifications },
},
} = useProfilingDependencies();
const {
handleFieldChange,
settingsEditableConfig,
unsavedChanges,
saveAll,
isSaving,
cleanUnsavedChanges,
} = useEditableSettings('profiling', settingKeys);
async function handleSave() {
try {
const reloadPage = Object.keys(unsavedChanges).some((key) => {
return settingsEditableConfig[key].requiresPageReload;
});
await saveAll();
trackProfilingEvent({ metric: 'general_settings_save' });
if (reloadPage) {
window.location.reload();
}
} catch (e) {
const error = e as Error;
notifications.toasts.addDanger({
title: i18n.translate('xpack.profiling.settings.save.error', {
defaultMessage: 'An error occurred while saving the settings',
}),
text: error.message,
});
}
}
return (
<ProfilingAppPageTemplate hideSearchBar>
<>
<EuiTitle>
<EuiText>
{i18n.translate('xpack.profiling.settings.title', {
defaultMessage: 'Advanced 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]}
/>
);
})}
</EuiPanel>
</EuiPanel>
{!isEmpty(unsavedChanges) && (
<BottomBarActions
isLoading={isSaving}
onDiscardChanges={cleanUnsavedChanges}
onSave={handleSave}
saveLabel={i18n.translate('xpack.profiling.settings.saveButton', {
defaultMessage: 'Save changes',
})}
unsavedChangesCount={Object.keys(unsavedChanges).length}
/>
)}
</>
</ProfilingAppPageTemplate>
);
}

View file

@ -49,7 +49,8 @@
"@kbn/observability-ai-assistant-plugin",
"@kbn/profiling-data-access-plugin",
"@kbn/embeddable-plugin",
"@kbn/profiling-utils"
"@kbn/profiling-utils",
"@kbn/advanced-settings-plugin"
// add references to other TypeScript projects the plugin depends on
// requiredPlugins from ./kibana.json