From 12695646cfa9f35392f5338133319a16c88d7a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Sat, 30 Sep 2023 10:25:55 +0100 Subject: [PATCH] [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: Screenshot 2023-09-22 at 11 18 35 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/collectors/management/schema.ts | 12 ++ .../server/collectors/management/types.ts | 3 + src/plugins/telemetry/schema/oss_plugins.json | 18 +++ .../app/settings/general_settings/index.tsx | 11 +- .../labs/labs_flyout.tsx | 11 +- x-pack/plugins/observability/common/index.ts | 3 + .../observability/common/ui_settings_keys.ts | 3 + .../observability/server/ui_settings.ts | 67 ++++++++++ .../plugins/observability_shared/kibana.jsonc | 2 +- .../public/hooks/use_editable_settings.tsx} | 13 +- .../observability_shared/public/index.ts | 1 + .../observability_shared/tsconfig.json | 1 + .../calculate_impact_estimates/index.ts | 85 ------------- .../differential_functions.cy.ts | 45 ++----- .../e2e/profiling_views/functions.cy.ts | 94 +++++++++++--- .../e2e/profiling_views/settings.cy.ts | 58 +++++++++ .../profiling/e2e/cypress/support/commands.ts | 14 +++ .../profiling/e2e/cypress/support/types.d.ts | 1 + x-pack/plugins/profiling/e2e/tsconfig.json | 1 + x-pack/plugins/profiling/kibana.jsonc | 3 +- .../flamegraph/flamegraph_tooltip.tsx | 3 +- .../public/components/flamegraph/index.tsx | 1 - .../get_impact_rows.tsx | 4 +- .../frame_information_window/index.tsx | 13 +- .../profiling_header_action_menu.tsx | 5 + .../components/topn_functions/index.tsx | 4 + .../public/components/topn_functions/utils.ts | 13 +- .../topn_functions_summary/index.tsx | 5 +- .../flamegraph/embeddable_flamegraph.tsx | 35 ++++-- .../embeddable_flamegraph_factory.ts | 12 +- .../functions/embeddable_functions.tsx | 26 +++- .../functions/embeddable_functions_factory.ts | 6 +- .../profiling_embeddable_provider.tsx | 56 +++++++++ .../embeddables/register_embeddables.ts | 16 ++- .../use_calculate_impact_estimates.test.ts} | 36 +++++- .../hooks/use_calculate_impact_estimates.ts | 91 ++++++++++++++ x-pack/plugins/profiling/public/plugin.tsx | 22 +++- .../profiling/public/routing/index.tsx | 13 ++ .../views/settings/bottom_bar_actions.tsx | 83 +++++++++++++ .../profiling/public/views/settings/index.tsx | 115 ++++++++++++++++++ x-pack/plugins/profiling/tsconfig.json | 3 +- 41 files changed, 816 insertions(+), 192 deletions(-) rename x-pack/plugins/{apm/public/hooks/use_apm_editable_settings.tsx => observability_shared/public/hooks/use_editable_settings.tsx} (87%) delete mode 100644 x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts create mode 100644 x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/settings.cy.ts create mode 100644 x-pack/plugins/profiling/public/embeddables/profiling_embeddable_provider.tsx rename x-pack/plugins/profiling/{common/calculate_impact_estimates/calculate_impact_estimates.test.ts => public/hooks/use_calculate_impact_estimates.test.ts} (69%) create mode 100644 x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.ts create mode 100644 x-pack/plugins/profiling/public/views/settings/bottom_bar_actions.tsx create mode 100644 x-pack/plugins/profiling/public/views/settings/index.tsx diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 0f3db5fc2bba..47309833c7b1 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -557,6 +557,18 @@ export const stackManagementSchema: MakeSchemaFrom = { 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.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index fb3c31bf44d8..b48fa13280e4 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -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; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index e2013ff091c7..da1ed4d1e657 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -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": { diff --git a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx index 52ede89eefba..8926e2155592 100644 --- a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx @@ -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(); } diff --git a/x-pack/plugins/apm/public/components/routing/app_root/apm_header_action_menu/labs/labs_flyout.tsx b/x-pack/plugins/apm/public/components/routing/app_root/apm_header_action_menu/labs/labs_flyout.tsx index d5c151d30518..6ccac9340486 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root/apm_header_action_menu/labs/labs_flyout.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root/apm_header_action_menu/labs/labs_flyout.tsx @@ -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(); diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index a90240d95e05..51a92af4b47b 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -41,6 +41,9 @@ export { enableCriticalPath, syntheticsThrottlingEnabled, apmEnableProfilingIntegration, + profilingCo2PerKWH, + profilingDatacenterPUE, + profilingPerCoreWatt, } from './ui_settings_keys'; export { diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 183e9f41030c..46f83d7e9c95 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -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'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 39678758bd7e..4bc87fba7949 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -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 = { 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 +

+ You can also use the PUE that corresponds with your cloud provider: + + `, + values: { + uptimeLink: + '' + + i18n.translate( + 'xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink', + { defaultMessage: 'Uptime Institute' } + ) + + '', + }, + }), + 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: + '' + + i18n.translate( + 'xpack.observability.profilingCo2PerKWHUiSettingDescription.datasheetLink', + { defaultMessage: 'datasheet' } + ) + + '', + }, + }), + schema: schema.number({ min: 0 }), + requiresPageReload: false, + }, }; function throttlingDocsLink({ href }: { href: string }) { diff --git a/x-pack/plugins/observability_shared/kibana.jsonc b/x-pack/plugins/observability_shared/kibana.jsonc index e1d5b14d2393..1948fca972d2 100644 --- a/x-pack/plugins/observability_shared/kibana.jsonc +++ b/x-pack/plugins/observability_shared/kibana.jsonc @@ -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"] } } diff --git a/x-pack/plugins/apm/public/hooks/use_apm_editable_settings.tsx b/x-pack/plugins/observability_shared/public/hooks/use_editable_settings.tsx similarity index 87% rename from x-pack/plugins/apm/public/hooks/use_apm_editable_settings.tsx rename to x-pack/plugins/observability_shared/public/hooks/use_editable_settings.tsx index 3ee7447eb741..460bb50f57bc 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_editable_settings.tsx +++ b/x-pack/plugins/observability_shared/public/hooks/use_editable_settings.tsx @@ -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 - >({}); + const [unsavedChanges, setUnsavedChanges] = useState>({}); 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 { diff --git a/x-pack/plugins/observability_shared/public/index.ts b/x-pack/plugins/observability_shared/public/index.ts index a35cdce8f85e..66492328e4b8 100644 --- a/x-pack/plugins/observability_shared/public/index.ts +++ b/x-pack/plugins/observability_shared/public/index.ts @@ -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'; diff --git a/x-pack/plugins/observability_shared/tsconfig.json b/x-pack/plugins/observability_shared/tsconfig.json index ee7efc921b6a..cecfb33c9386 100644 --- a/x-pack/plugins/observability_shared/tsconfig.json +++ b/x-pack/plugins/observability_shared/tsconfig.json @@ -34,6 +34,7 @@ "@kbn/shared-ux-router", "@kbn/embeddable-plugin", "@kbn/profiling-utils", + "@kbn/advanced-settings-plugin", "@kbn/utility-types", "@kbn/share-plugin" ], diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts b/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts deleted file mode 100644 index 6ebbe26a9fb4..000000000000 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts +++ /dev/null @@ -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, - }; -} diff --git a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/differential_functions.cy.ts b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/differential_functions.cy.ts index b1a2df79e3dc..bbbcb6c8abdc 100644 --- a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/differential_functions.cy.ts +++ b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/differential_functions.cy.ts @@ -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'); }); }); }); diff --git a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts index f928db33e2f1..8b0a66180c7a 100644 --- a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts +++ b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts @@ -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); + }); + }); + }); }); diff --git a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/settings.cy.ts b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/settings.cy.ts new file mode 100644 index 000000000000..4863afbca437 --- /dev/null +++ b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/settings.cy.ts @@ -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'); + }); +}); diff --git a/x-pack/plugins/profiling/e2e/cypress/support/commands.ts b/x-pack/plugins/profiling/e2e/cypress/support/commands.ts index 9a01caf46a8f..ada8681d308d 100644 --- a/x-pack/plugins/profiling/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/profiling/e2e/cypress/support/commands.ts @@ -71,3 +71,17 @@ Cypress.Commands.add( cy.getByTestSubj(dataTestSubj).type('{enter}'); } ); + +Cypress.Commands.add('updateAdvancedSettings', (settings: Record) => { + 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' }, + }); +}); diff --git a/x-pack/plugins/profiling/e2e/cypress/support/types.d.ts b/x-pack/plugins/profiling/e2e/cypress/support/types.d.ts index 67bbc4509dc2..d83cce31db87 100644 --- a/x-pack/plugins/profiling/e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/profiling/e2e/cypress/support/types.d.ts @@ -27,5 +27,6 @@ declare namespace Cypress { dataTestSubj?: 'profilingUnifiedSearchBar' | 'profilingComparisonUnifiedSearchBar'; waitForSuggestion?: boolean; }): void; + updateAdvancedSettings(settings: Record): void; } } diff --git a/x-pack/plugins/profiling/e2e/tsconfig.json b/x-pack/plugins/profiling/e2e/tsconfig.json index c4de3fff85cf..ed7aaac66702 100644 --- a/x-pack/plugins/profiling/e2e/tsconfig.json +++ b/x-pack/plugins/profiling/e2e/tsconfig.json @@ -22,5 +22,6 @@ "@kbn/test", "@kbn/dev-utils", "@kbn/cypress-config", + "@kbn/observability-plugin", ] } diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc index 6ebaf5e99832..1eae495f8c85 100644 --- a/x-pack/plugins/profiling/kibana.jsonc +++ b/x-pack/plugins/profiling/kibana.jsonc @@ -30,7 +30,8 @@ "requiredBundles": [ "kibanaReact", "kibanaUtils", - "observabilityAIAssistant" + "observabilityAIAssistant", + "advancedSettings" ] } } diff --git a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx index 7a3b661cdac6..9d00900c1e19 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx @@ -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, diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx index 76105907f8d5..6da7062b9b38 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx @@ -194,7 +194,6 @@ export function FlameGraph({ frame={selected} totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0} totalSamples={totalSamples} - showAIAssistant={!isEmbedded} showSymbolsStatus={!isEmbedded} /> )} diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx index 59c5eb0086c7..f0510b2d07b4 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx @@ -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, diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx index 856e30001bec..c5333d147787 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx @@ -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 ( @@ -87,6 +89,7 @@ export function FrameInformationWindow({ countExclusive, totalSamples, totalSeconds, + calculateImpactEstimates, }); return ( @@ -95,11 +98,9 @@ export function FrameInformationWindow({ - {showAIAssistant ? ( - - - - ) : null} + + + {showSymbolsStatus && symbolStatus !== FrameSymbolStatus.SYMBOLIZED ? ( diff --git a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx index eb50e1b3ea94..2a730a7ffce7 100644 --- a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx +++ b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx @@ -76,6 +76,11 @@ export function ProfilingHeaderActionMenu() { + + {i18n.translate('xpack.profiling.headerActionMenu.settings', { + defaultMessage: 'Settings', + })} + ); diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index e1318471913d..90fd422e8d46 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -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(); 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, diff --git a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts index 5257b41b5ade..788c7397fa79 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts +++ b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts @@ -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; + impactEstimates?: ImpactEstimates; diff?: { rank: number; samples: number; @@ -45,7 +48,7 @@ export interface IFunctionRow { totalCPU: number; selfCPUPerc: number; totalCPUPerc: number; - impactEstimates?: ReturnType; + 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({ diff --git a/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx index bd1387c2576a..e3994ab442b7 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx @@ -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, diff --git a/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph.tsx b/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph.tsx index 8e491af0afe6..b88881ea04cb 100644 --- a/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph.tsx +++ b/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph.tsx @@ -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( - - <> - {flamegraph && ( - - )} - - , + + + <> + {flamegraph && ( + + )} + + + , domNode ); } diff --git a/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph_factory.ts b/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph_factory.ts index 568a4d20acc7..259c61df525e 100644 --- a/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph_factory.ts +++ b/x-pack/plugins/profiling/public/embeddables/flamegraph/embeddable_flamegraph_factory.ts @@ -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() { diff --git a/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions.tsx b/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions.tsx index fca243f81f56..4cfbe7ceddbb 100644 --- a/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions.tsx +++ b/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions.tsx @@ -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( - -
- -
-
, + + +
+ +
+
+
, domNode ); } diff --git a/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions_factory.ts b/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions_factory.ts index 8f7397b087c3..99e0d4baa485 100644 --- a/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions_factory.ts +++ b/x-pack/plugins/profiling/public/embeddables/functions/embeddable_functions_factory.ts @@ -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() { diff --git a/x-pack/plugins/profiling/public/embeddables/profiling_embeddable_provider.tsx b/x-pack/plugins/profiling/public/embeddables/profiling_embeddable_provider.tsx new file mode 100644 index 000000000000..d4db1e2d9fb7 --- /dev/null +++ b/x-pack/plugins/profiling/public/embeddables/profiling_embeddable_provider.tsx @@ -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; + +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 ( + + + + {children} + + + + ); +} diff --git a/x-pack/plugins/profiling/public/embeddables/register_embeddables.ts b/x-pack/plugins/profiling/public/embeddables/register_embeddables.ts index 93d57a4e721a..d7b2e947144b 100644 --- a/x-pack/plugins/profiling/public/embeddables/register_embeddables.ts +++ b/x-pack/plugins/profiling/public/embeddables/register_embeddables.ts @@ -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) + ); } diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts b/x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.test.ts similarity index 69% rename from x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts rename to x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.test.ts index a1abe24a79fa..65666b95b7ab 100644 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts +++ b/x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.test.ts @@ -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, diff --git a/x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.ts b/x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.ts new file mode 100644 index 000000000000..1a32cf541f54 --- /dev/null +++ b/x-pack/plugins/profiling/public/hooks/use_calculate_impact_estimates.ts @@ -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; +export type ImpactEstimates = ReturnType; + +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(profilingPerCoreWatt); + const co2PerTonKWH = core.uiSettings.get(profilingCo2PerKWH); + const datacenterPUE = core.uiSettings.get(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, + }), + }; + }; +} diff --git a/x-pack/plugins/profiling/public/plugin.tsx b/x-pack/plugins/profiling/public/plugin.tsx index d888ba6ce978..bcbdf3db8e24 100644 --- a/x-pack/plugins/profiling/public/plugin.tsx +++ b/x-pack/plugins/profiling/public/plugin.tsx @@ -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 => { + const [coreStart, pluginsStart] = (await coreSetup.getStartServices()) as [ + CoreStart, + ProfilingPluginPublicStartDeps, + unknown + ]; + return { + coreStart, + coreSetup, + pluginsStart, + pluginsSetup, + profilingFetchServices, + }; + }; + + registerEmbeddables(pluginsSetup.embeddable, getProfilingEmbeddableDependencies); return {}; } diff --git a/x-pack/plugins/profiling/public/routing/index.tsx b/x-pack/plugins/profiling/public/routing/index.tsx index 9e40cdcb6dc0..cb92422a9407 100644 --- a/x-pack/plugins/profiling/public/routing/index.tsx +++ b/x-pack/plugins/profiling/public/routing/index.tsx @@ -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: ( + + + + ), + }, '/': { element: ( void; + onSave: () => void; + saveLabel: string; +} + +export function BottomBarActions({ + isLoading, + onDiscardChanges, + onSave, + unsavedChangesCount, + saveLabel, +}: Props) { + return ( + + + + + + {i18n.translate('xpack.profiling.bottomBarActions.unsavedChanges', { + defaultMessage: + '{unsavedChangesCount, plural, =0{0 unsaved changes} one {1 unsaved change} other {# unsaved changes}} ', + values: { unsavedChangesCount }, + })} + + + + + + + {i18n.translate('xpack.profiling.bottomBarActions.discardChangesButton', { + defaultMessage: 'Discard changes', + })} + + + + + {saveLabel} + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/settings/index.tsx b/x-pack/plugins/profiling/public/views/settings/index.tsx new file mode 100644 index 000000000000..afc32d42e6f3 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/settings/index.tsx @@ -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 ( + + <> + + + {i18n.translate('xpack.profiling.settings.title', { + defaultMessage: 'Advanced Settings', + })} + + + + + + + + {i18n.translate('xpack.profiling.settings.co2Sections', { + defaultMessage: 'CO2', + })} + + + + + {settingKeys.map((settingKey) => { + const editableConfig = settingsEditableConfig[settingKey]; + return ( + + ); + })} + + + {!isEmpty(unsavedChanges) && ( + + )} + + + ); +} diff --git a/x-pack/plugins/profiling/tsconfig.json b/x-pack/plugins/profiling/tsconfig.json index 60c06119bfa6..af7971b5115d 100644 --- a/x-pack/plugins/profiling/tsconfig.json +++ b/x-pack/plugins/profiling/tsconfig.json @@ -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