[Profiling] Adding normalize by time and scale factor on Diff TopN functions page (#159394)

Relevant changes:
- Adding **Normalize by** field on the Diff TopN functions page.
- Refactoring functions route/pages.


<img width="1469" alt="Screenshot 2023-06-09 at 2 34 48 PM"
src="b2c824bd-94ff-4f8b-a6b2-b9c30c51881e">


<img width="1353" alt="Screenshot 2023-06-09 at 2 34 58 PM"
src="9d1de932-8669-4ddf-968a-057199211931">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-06-14 09:19:39 +01:00 committed by GitHub
parent b85d92c343
commit c6bca36d96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 525 additions and 365 deletions

View file

@ -10,16 +10,6 @@ import { createFrameGroupID } from './frame_group';
import { fnv1a64 } from './hash';
import { createStackFrameMetadata, getCalleeLabel } from './profiling';
export enum FlameGraphComparisonMode {
Absolute = 'absolute',
Relative = 'relative',
}
export enum FlameGraphNormalizationMode {
Scale = 'scale',
Time = 'time',
}
export interface BaseFlameGraph {
Size: number;
Edges: number[][];

View file

@ -7,11 +7,11 @@
import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FlameGraphComparisonMode } from '../../../common/flamegraph';
import { ComparisonMode } from '../normalization_menu';
interface Props {
comparisonMode: FlameGraphComparisonMode;
onChange: (nextComparisonMode: FlameGraphComparisonMode) => void;
comparisonMode: ComparisonMode;
onChange: (nextComparisonMode: ComparisonMode) => void;
}
export function DifferentialComparisonMode({ comparisonMode, onChange }: Props) {
return (
@ -20,45 +20,37 @@ export function DifferentialComparisonMode({ comparisonMode, onChange }: Props)
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h3>
{i18n.translate(
'xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeTitle',
{ defaultMessage: 'Format' }
)}
{i18n.translate('xpack.profiling.differentialComparisonMode.title', {
defaultMessage: 'Format',
})}
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend={i18n.translate(
'xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeLegend',
{
defaultMessage:
'This switch allows you to switch between an absolute and relative comparison between both graphs',
}
)}
legend={i18n.translate('xpack.profiling.differentialComparisonMode.legend', {
defaultMessage:
'This switch allows you to switch between an absolute and relative comparison between both graphs',
})}
type="single"
buttonSize="s"
idSelected={comparisonMode}
onChange={(nextComparisonMode) => {
onChange(nextComparisonMode as FlameGraphComparisonMode);
onChange(nextComparisonMode as ComparisonMode);
}}
options={[
{
id: FlameGraphComparisonMode.Absolute,
id: ComparisonMode.Absolute,
label: i18n.translate(
'xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeAbsoluteButtonLabel',
{
defaultMessage: 'Abs',
}
'xpack.profiling.differentialComparisonMode.absoluteButtonLabel',
{ defaultMessage: 'Abs' }
),
},
{
id: FlameGraphComparisonMode.Relative,
id: ComparisonMode.Relative,
label: i18n.translate(
'xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeRelativeButtonLabel',
{
defaultMessage: 'Rel',
}
'xpack.profiling.differentialComparisonMode.relativeButtonLabel',
{ defaultMessage: 'Rel' }
),
},
]}

View file

@ -9,16 +9,17 @@ import { Chart, Datum, Flame, FlameLayerValue, PartialTheme, Settings } from '@e
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import React, { useEffect, useMemo, useState } from 'react';
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
import { ElasticFlameGraph } from '../../../common/flamegraph';
import { getFlamegraphModel } from '../../utils/get_flamegraph_model';
import { FlameGraphLegend } from './flame_graph_legend';
import { FrameInformationWindow } from '../frame_information_window';
import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip';
import { FlameGraphTooltip } from './flamegraph_tooltip';
import { ComparisonMode } from '../normalization_menu';
interface Props {
id: string;
comparisonMode: FlameGraphComparisonMode;
comparisonMode: ComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number;

View file

@ -25,35 +25,41 @@ import {
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { FlameGraphNormalizationMode } from '../../../common/flamegraph';
export interface FlameGraphNormalizationOptions {
export interface NormalizationOptions {
baselineScale: number;
baselineTime: number;
comparisonScale: number;
comparisonTime: number;
}
interface Props {
mode: FlameGraphNormalizationMode;
options: FlameGraphNormalizationOptions;
onChange: (mode: FlameGraphNormalizationMode, options: FlameGraphNormalizationOptions) => void;
export enum ComparisonMode {
Absolute = 'absolute',
Relative = 'relative',
}
const SCALE_LABEL = i18n.translate('xpack.profiling.flameGraphNormalizationMenu.scale', {
export enum NormalizationMode {
Scale = 'scale',
Time = 'time',
}
interface Props {
mode: NormalizationMode;
options: NormalizationOptions;
onChange: (mode: NormalizationMode, options: NormalizationOptions) => void;
}
const SCALE_LABEL = i18n.translate('xpack.profiling.normalizationMenu.scale', {
defaultMessage: 'Scale factor',
});
const TIME_LABEL = i18n.translate('xpack.profiling.flameGraphNormalizationMenu.time', {
const TIME_LABEL = i18n.translate('xpack.profiling.normalizationMenu.time', {
defaultMessage: 'Time',
});
const NORMALIZE_BY_LABEL = i18n.translate(
'xpack.profiling.flameGraphNormalizationMenu.normalizeBy',
{
defaultMessage: 'Normalize by',
}
);
const NORMALIZE_BY_LABEL = i18n.translate('xpack.profiling.normalizationMenu.normalizeBy', {
defaultMessage: 'Normalize by',
});
export function NormalizationMenu(props: Props) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@ -72,7 +78,7 @@ export function NormalizationMenu(props: Props) {
}, [props.mode, props.options]);
const { baseline, comparison } =
mode === FlameGraphNormalizationMode.Time
mode === NormalizationMode.Time
? { comparison: options.comparisonTime, baseline: options.baselineTime }
: { comparison: options.comparisonScale, baseline: options.baselineScale };
@ -110,7 +116,7 @@ export function NormalizationMenu(props: Props) {
padding: '0 16px',
}}
>
{props.mode === FlameGraphNormalizationMode.Scale ? SCALE_LABEL : TIME_LABEL}
{props.mode === NormalizationMode.Scale ? SCALE_LABEL : TIME_LABEL}
</EuiFlexItem>
</EuiFormControlLayout>
}
@ -129,24 +135,18 @@ export function NormalizationMenu(props: Props) {
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<span>
{i18n.translate(
'xpack.profiling.flameGraphNormalizationMenu.normalizeByTimeTooltip',
{
defaultMessage:
'Select Normalize by Scale factor and set your Baseline and Comparison scale factors to compare a set of machines of different sizes. For example, you can compare a deployment of 10% of machines to a deployment of 90% of machines.',
}
)}
{i18n.translate('xpack.profiling.normalizationMenu.normalizeByTimeTooltip', {
defaultMessage:
'Select Normalize by Scale factor and set your Baseline and Comparison scale factors to compare a set of machines of different sizes. For example, you can compare a deployment of 10% of machines to a deployment of 90% of machines.',
})}
</span>
</EuiFlexItem>
<EuiFlexItem>
<span>
{i18n.translate(
'xpack.profiling.flameGraphNormalizationMenu.normalizeByScaleTooltip',
{
defaultMessage:
'Select Normalize by Time to compare a set of machines across different time periods. For example, if you compare the last hour to the last 24 hours, the shorter timeframe (1 hour) is multiplied to match the longer timeframe (24 hours).',
}
)}
{i18n.translate('xpack.profiling.normalizationMenu.normalizeByScaleTooltip', {
defaultMessage:
'Select Normalize by Time to compare a set of machines across different time periods. For example, if you compare the last hour to the last 24 hours, the shorter timeframe (1 hour) is multiplied to match the longer timeframe (24 hours).',
})}
</span>
</EuiFlexItem>
</EuiFlexGroup>
@ -160,19 +160,19 @@ export function NormalizationMenu(props: Props) {
buttonSize="compressed"
isFullWidth
onChange={(id, value) => {
setMode(id as FlameGraphNormalizationMode);
setMode(id as NormalizationMode);
}}
legend={i18n.translate('xpack.profiling.flameGraphNormalizationMode.selectModeLegend', {
legend={i18n.translate('xpack.profiling.normalizationMode.selectModeLegend', {
defaultMessage: 'Select a normalization mode for the flamegraph',
})}
idSelected={mode}
options={[
{
id: FlameGraphNormalizationMode.Scale,
id: NormalizationMode.Scale,
label: SCALE_LABEL,
},
{
id: FlameGraphNormalizationMode.Time,
id: NormalizationMode.Time,
label: TIME_LABEL,
},
]}
@ -195,14 +195,14 @@ export function NormalizationMenu(props: Props) {
id={baselineScaleFactorInputId}
value={baseline}
onChange={(e) => {
if (mode === FlameGraphNormalizationMode.Scale) {
if (mode === NormalizationMode.Scale) {
setOptions((prevOptions) => ({
...prevOptions,
baselineScale: e.target.valueAsNumber,
}));
}
}}
disabled={mode === FlameGraphNormalizationMode.Time}
disabled={mode === NormalizationMode.Time}
/>
</EuiFormControlLayout>
<EuiSpacer size="m" />
@ -223,14 +223,14 @@ export function NormalizationMenu(props: Props) {
id={comparisonScaleFactorInputId}
value={comparison}
onChange={(e) => {
if (mode === FlameGraphNormalizationMode.Scale) {
if (mode === NormalizationMode.Scale) {
setOptions((prevOptions) => ({
...prevOptions,
comparisonScale: e.target.valueAsNumber,
}));
}
}}
disabled={mode === FlameGraphNormalizationMode.Time}
disabled={mode === NormalizationMode.Time}
/>
</EuiFormControlLayout>
<EuiSpacer size="m" />

View file

@ -92,7 +92,7 @@ function SampleStat({
diffSamples?: number;
totalSamples: number;
}) {
const samplesLabel = `${samples.toLocaleString()}`;
const samplesLabel = samples.toLocaleString();
if (diffSamples === undefined || diffSamples === 0 || totalSamples === 0) {
return <>{samplesLabel}</>;
@ -142,6 +142,12 @@ interface Props {
comparisonTopNFunctions?: TopNFunctions;
totalSeconds: number;
isDifferentialView: boolean;
baselineScaleFactor?: number;
comparisonScaleFactor?: number;
}
function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFactor?: number }) {
return value * scaleFactor;
}
export function TopNFunctionsTable({
@ -152,6 +158,8 @@ export function TopNFunctionsTable({
comparisonTopNFunctions,
totalSeconds,
isDifferentialView,
baselineScaleFactor,
comparisonScaleFactor,
}: Props) {
const [selectedRow, setSelectedRow] = useState<Row | undefined>();
@ -175,6 +183,11 @@ export function TopNFunctionsTable({
return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0).map((topN, i) => {
const comparisonRow = comparisonDataById?.[topN.Id];
const topNCountExclusiveScaled = scaleValue({
value: topN.CountExclusive,
scaleFactor: baselineScaleFactor,
});
const inclusiveCPU = (topN.CountInclusive / topNFunctions.TotalCount) * 100;
const exclusiveCPU = (topN.CountExclusive / topNFunctions.TotalCount) * 100;
const totalSamples = topN.CountExclusive;
@ -189,31 +202,43 @@ export function TopNFunctionsTable({
})
: undefined;
const diff =
comparisonTopNFunctions && comparisonRow
? {
rank: topN.Rank - comparisonRow.Rank,
samples: topN.CountExclusive - comparisonRow.CountExclusive,
exclusiveCPU:
exclusiveCPU -
(comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100,
inclusiveCPU:
inclusiveCPU -
(comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100,
}
: undefined;
function calculateDiff() {
if (comparisonTopNFunctions && comparisonRow) {
const comparisonCountExclusiveScaled = scaleValue({
value: comparisonRow.CountExclusive,
scaleFactor: comparisonScaleFactor,
});
return {
rank: topN.Rank - comparisonRow.Rank,
samples: topNCountExclusiveScaled - comparisonCountExclusiveScaled,
exclusiveCPU:
exclusiveCPU -
(comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100,
inclusiveCPU:
inclusiveCPU -
(comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100,
};
}
}
return {
rank: topN.Rank,
frame: topN.Frame,
samples: topN.CountExclusive,
samples: topNCountExclusiveScaled,
exclusiveCPU,
inclusiveCPU,
impactEstimates,
diff,
diff: calculateDiff(),
};
});
}, [topNFunctions, comparisonTopNFunctions, totalSeconds]);
}, [
topNFunctions,
comparisonTopNFunctions,
totalSeconds,
comparisonScaleFactor,
baselineScaleFactor,
]);
const theme = useEuiTheme();

View file

@ -9,12 +9,14 @@ import { toNumberRt } from '@kbn/io-ts-utils';
import { createRouter, Outlet } from '@kbn/typed-react-router-config';
import * as t from 'io-ts';
import React from 'react';
import { FlameGraphComparisonMode, FlameGraphNormalizationMode } from '../../common/flamegraph';
import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/functions';
import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces';
import { ComparisonMode, NormalizationMode } from '../components/normalization_menu';
import { RedirectTo } from '../components/redirect_to';
import { FlameGraphsView } from '../views/flame_graphs_view';
import { FunctionsView } from '../views/functions_view';
import { FunctionsView } from '../views/functions';
import { DifferentialTopNFunctionsView } from '../views/functions/differential_topn';
import { TopNFunctionsView } from '../views/functions/topn';
import { NoDataView } from '../views/no_data_view';
import { StackTracesView } from '../views/stack_traces_view';
import { RouteBreadcrumb } from './route_breadcrumb';
@ -117,14 +119,14 @@ const routes = {
comparisonRangeTo: t.string,
comparisonKuery: t.string,
comparisonMode: t.union([
t.literal(FlameGraphComparisonMode.Absolute),
t.literal(FlameGraphComparisonMode.Relative),
t.literal(ComparisonMode.Absolute),
t.literal(ComparisonMode.Relative),
]),
}),
t.partial({
normalizationMode: t.union([
t.literal(FlameGraphNormalizationMode.Scale),
t.literal(FlameGraphNormalizationMode.Time),
t.literal(NormalizationMode.Scale),
t.literal(NormalizationMode.Time),
]),
baseline: toNumberRt,
comparison: toNumberRt,
@ -133,8 +135,8 @@ const routes = {
}),
defaults: {
query: {
comparisonMode: FlameGraphComparisonMode.Absolute,
normalizationMode: FlameGraphNormalizationMode.Time,
comparisonMode: ComparisonMode.Absolute,
normalizationMode: NormalizationMode.Time,
},
},
},
@ -174,7 +176,7 @@ const routes = {
})}
href="/functions/topn"
>
<Outlet />
<TopNFunctionsView />
</RouteBreadcrumb>
),
},
@ -186,16 +188,34 @@ const routes = {
})}
href="/functions/differential"
>
<Outlet />
<DifferentialTopNFunctionsView />
</RouteBreadcrumb>
),
params: t.type({
query: t.type({
comparisonRangeFrom: t.string,
comparisonRangeTo: t.string,
comparisonKuery: t.string,
}),
query: t.intersection([
t.type({
comparisonRangeFrom: t.string,
comparisonRangeTo: t.string,
comparisonKuery: t.string,
normalizationMode: t.union([
t.literal(NormalizationMode.Scale),
t.literal(NormalizationMode.Time),
]),
}),
t.partial({
baseline: toNumberRt,
comparison: toNumberRt,
}),
]),
}),
defaults: {
query: {
comparisonRangeFrom: 'now-15m',
comparisonRangeTo: 'now',
comparisonKuery: '',
normalizationMode: NormalizationMode.Time,
},
},
},
},
},

View file

@ -9,9 +9,10 @@ import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { compact, range, sum, uniqueId } from 'lodash';
import { createColumnarViewModel } from '../../../common/columnar_view_model';
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
import { ElasticFlameGraph } from '../../../common/flamegraph';
import { FRAME_TYPE_COLOR_MAP, rgbToRGBA } from '../../../common/frame_type_colors';
import { describeFrameType, FrameType } from '../../../common/profiling';
import { ComparisonMode } from '../../components/normalization_menu';
import { getInterpolationValue } from './get_interpolation_value';
const nullColumnarViewModel = {
@ -39,7 +40,7 @@ export function getFlamegraphModel({
colorSuccess: string;
colorDanger: string;
colorNeutral: string;
comparisonMode: FlameGraphComparisonMode;
comparisonMode: ComparisonMode;
baseline?: number;
comparison?: number;
}): {
@ -135,23 +136,20 @@ export function getFlamegraphModel({
const comparisonTotalSamples = sum(comparisonFlamegraph.CountExclusive);
const weightComparisonSide =
comparisonMode === FlameGraphComparisonMode.Relative
? 1
: (comparison ?? 1) / (baseline ?? 1);
comparisonMode === ComparisonMode.Relative ? 1 : (comparison ?? 1) / (baseline ?? 1);
primaryFlamegraph.ID.forEach((nodeID, index) => {
const samples = primaryFlamegraph.CountInclusive[index];
const comparisonSamples = comparisonNodesById[nodeID]?.CountInclusive as number | undefined;
const foreground =
comparisonMode === FlameGraphComparisonMode.Absolute ? samples : samples / totalSamples;
comparisonMode === ComparisonMode.Absolute ? samples : samples / totalSamples;
const background =
comparisonMode === FlameGraphComparisonMode.Absolute
comparisonMode === ComparisonMode.Absolute
? comparisonSamples
: (comparisonSamples ?? 0) / comparisonTotalSamples;
const denominator =
comparisonMode === FlameGraphComparisonMode.Absolute ? totalSamples : foreground;
const denominator = comparisonMode === ComparisonMode.Absolute ? totalSamples : foreground;
const interpolationValue = getInterpolationValue(
foreground,

View file

@ -6,20 +6,24 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import React from 'react';
import { FlameGraphComparisonMode, FlameGraphNormalizationMode } from '../../../common/flamegraph';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
import { PrimaryAndComparisonSearchBar } from '../../components/primary_and_comparison_search_bar';
import { PrimaryProfilingSearchBar } from '../../components/profiling_app_page_template/primary_profiling_search_bar';
import { DifferentialComparisonMode } from './differential_comparison_mode';
import { FlameGraphNormalizationOptions, NormalizationMenu } from './normalization_menu';
import {
ComparisonMode,
NormalizationMode,
NormalizationOptions,
NormalizationMenu,
} from '../../components/normalization_menu';
import { DifferentialComparisonMode } from '../../components/differential_comparison_mode';
interface Props {
isDifferentialView: boolean;
comparisonMode: FlameGraphComparisonMode;
normalizationMode: FlameGraphNormalizationMode;
normalizationOptions: FlameGraphNormalizationOptions;
comparisonMode: ComparisonMode;
normalizationMode: NormalizationMode;
normalizationOptions: NormalizationOptions;
}
export function FlameGraphSearchPanel({
@ -32,7 +36,7 @@ export function FlameGraphSearchPanel({
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
function onChangeComparisonMode(nextComparisonMode: FlameGraphComparisonMode) {
function onChangeComparisonMode(nextComparisonMode: ComparisonMode) {
if (!('comparisonRangeFrom' in query)) {
return;
}
@ -41,24 +45,24 @@ export function FlameGraphSearchPanel({
path,
query: {
...query,
...(nextComparisonMode === FlameGraphComparisonMode.Absolute
...(nextComparisonMode === ComparisonMode.Absolute
? {
comparisonMode: FlameGraphComparisonMode.Absolute,
comparisonMode: ComparisonMode.Absolute,
normalizationMode,
}
: { comparisonMode: FlameGraphComparisonMode.Relative }),
: { comparisonMode: ComparisonMode.Relative }),
},
});
}
function onChangeNormalizationMode(
nextNormalizationMode: FlameGraphNormalizationMode,
options: FlameGraphNormalizationOptions
nextNormalizationMode: NormalizationMode,
options: NormalizationOptions
) {
profilingRouter.push(routePath, {
path: routePath,
query:
nextNormalizationMode === FlameGraphNormalizationMode.Scale
nextNormalizationMode === NormalizationMode.Scale
? {
...query,
baseline: options.baselineScale,
@ -82,7 +86,7 @@ export function FlameGraphSearchPanel({
comparisonMode={comparisonMode}
onChange={onChangeComparisonMode}
/>
{comparisonMode === FlameGraphComparisonMode.Absolute && (
{comparisonMode === ComparisonMode.Absolute && (
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>

View file

@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps } from '@elastic/e
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import React, { useState } from 'react';
import { FlameGraphComparisonMode, FlameGraphNormalizationMode } from '../../../common/flamegraph';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
@ -20,7 +19,11 @@ import { FlameGraph } from '../../components/flamegraph';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { RedirectTo } from '../../components/redirect_to';
import { FlameGraphSearchPanel } from './flame_graph_search_panel';
import { FlameGraphNormalizationOptions } from './normalization_menu';
import {
ComparisonMode,
NormalizationMode,
NormalizationOptions,
} from '../../components/normalization_menu';
export function FlameGraphsView({ children }: { children: React.ReactElement }) {
const {
@ -37,13 +40,12 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
);
const comparisonKuery = 'comparisonKuery' in query ? query.comparisonKuery : '';
const comparisonMode =
'comparisonMode' in query ? query.comparisonMode : FlameGraphComparisonMode.Absolute;
const comparisonMode = 'comparisonMode' in query ? query.comparisonMode : ComparisonMode.Absolute;
const normalizationMode: FlameGraphNormalizationMode = get(
const normalizationMode: NormalizationMode = get(
query,
'normalizationMode',
FlameGraphNormalizationMode.Time
NormalizationMode.Time
);
const baselineScale: number = get(query, 'baseline', 1);
@ -58,7 +60,7 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
const baselineTime = 1;
const comparisonTime = totalSeconds / totalComparisonSeconds;
const normalizationOptions: FlameGraphNormalizationOptions = {
const normalizationOptions: NormalizationOptions = {
baselineScale,
baselineTime,
comparisonScale,
@ -146,6 +148,8 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
return <RedirectTo pathname="/flamegraphs/flamegraph" />;
}
const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
return (
<ProfilingAppPageTemplate tabs={tabs} hideSearchBar={true}>
<EuiFlexGroup direction="column">
@ -164,16 +168,8 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}
baseline={
normalizationMode === FlameGraphNormalizationMode.Time
? baselineTime
: baselineScale
}
comparison={
normalizationMode === FlameGraphNormalizationMode.Time
? comparisonTime
: comparisonScale
}
baseline={isNormalizedByTime ? baselineTime : baselineScale}
comparison={isNormalizedByTime ? comparisonTime : comparisonScale}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
/>

View file

@ -0,0 +1,208 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { TypeOf } from '@kbn/typed-react-router-config';
import React from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import {
NormalizationMenu,
NormalizationMode,
NormalizationOptions,
} from '../../../components/normalization_menu';
import { PrimaryAndComparisonSearchBar } from '../../../components/primary_and_comparison_search_bar';
import { TopNFunctionsTable } from '../../../components/topn_functions';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
import { ProfilingRoutes } from '../../../routing';
export function DifferentialTopNFunctionsView() {
const {
path,
query,
query: {
rangeFrom,
rangeTo,
kuery,
sortDirection,
sortField,
comparisonKuery,
normalizationMode,
comparisonRangeFrom,
comparisonRangeTo,
baseline = 1,
comparison = 1,
},
} = useProfilingParams('/functions/differential');
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const comparisonTimeRange = useTimeRange({
rangeFrom: comparisonRangeFrom,
rangeTo: comparisonRangeTo,
optional: true,
});
const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start;
const totalComparisonSeconds =
(new Date(comparisonTimeRange.end!).getTime() -
new Date(comparisonTimeRange.start!).getTime()) /
1000;
const comparisonTime = totalSeconds / totalComparisonSeconds;
const baselineTime = 1;
const normalizationOptions: NormalizationOptions = {
baselineScale: baseline,
baselineTime,
comparisonScale: comparison,
comparisonTime,
};
const {
services: { fetchTopNFunctions },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return fetchTopNFunctions({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
startIndex: 0,
endIndex: 100000,
kuery,
});
},
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions]
);
const comparisonState = useTimeRangeAsync(
({ http }) => {
if (!comparisonTimeRange.inSeconds.start || !comparisonTimeRange.inSeconds.end) {
return undefined;
}
return fetchTopNFunctions({
http,
timeFrom: comparisonTimeRange.inSeconds.start,
timeTo: comparisonTimeRange.inSeconds.end,
startIndex: 0,
endIndex: 100000,
kuery: comparisonKuery,
});
},
[
comparisonTimeRange.inSeconds.start,
comparisonTimeRange.inSeconds.end,
comparisonKuery,
fetchTopNFunctions,
]
);
const routePath = useProfilingRoutePath() as
| '/functions'
| '/functions/topn'
| '/functions/differential';
const profilingRouter = useProfilingRouter();
function onChangeNormalizationMode(
nextNormalizationMode: NormalizationMode,
options: NormalizationOptions
) {
profilingRouter.push(routePath, {
path: routePath,
query:
nextNormalizationMode === NormalizationMode.Scale
? {
...query,
baseline: options.baselineScale,
comparison: options.comparisonScale,
normalizationMode: nextNormalizationMode,
}
: {
...query,
normalizationMode: nextNormalizationMode,
},
});
}
const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<PrimaryAndComparisonSearchBar />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<NormalizationMenu
mode={normalizationMode}
options={normalizationOptions}
onChange={onChangeNormalizationMode}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
<AsyncComponent {...state} size="xl" alignTop>
<TopNFunctionsTable
topNFunctions={state.data}
sortDirection={sortDirection}
sortField={sortField}
onSortChange={(nextSort) => {
profilingRouter.push(routePath, {
path,
query: {
...query,
sortField: nextSort.sortField,
sortDirection: nextSort.sortDirection,
},
});
}}
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? baselineTime : baseline}
/>
</AsyncComponent>
</EuiFlexItem>
{comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end ? (
<EuiFlexItem>
<AsyncComponent {...comparisonState} size="xl" alignTop>
<TopNFunctionsTable
sortDirection={sortDirection}
sortField={sortField}
onSortChange={(nextSort) => {
profilingRouter.push(routePath, {
path,
query: {
...(query as TypeOf<ProfilingRoutes, '/functions/differential'>['query']),
sortField: nextSort.sortField,
sortDirection: nextSort.sortDirection,
},
});
}}
topNFunctions={comparisonState.data}
comparisonTopNFunctions={state.data}
totalSeconds={
comparisonTimeRange.inSeconds.end - comparisonTimeRange.inSeconds.start
}
isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? comparisonTime : comparison}
comparisonScaleFactor={isNormalizedByTime ? baselineTime : baseline}
/>
</AsyncComponent>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -0,0 +1,64 @@
/*
* 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 { EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { NormalizationMode } from '../../components/normalization_menu';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { RedirectTo } from '../../components/redirect_to';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
export function FunctionsView({ children }: { children: React.ReactElement }) {
const { query } = useProfilingParams('/functions/*');
const routePath = useProfilingRoutePath() as
| '/functions'
| '/functions/topn'
| '/functions/differential';
const profilingRouter = useProfilingRouter();
if (routePath === '/functions') {
return <RedirectTo pathname="/functions/topn" />;
}
const isDifferentialView = routePath === '/functions/differential';
const tabs: Required<EuiPageHeaderContentProps>['tabs'] = [
{
label: i18n.translate('xpack.profiling.functionsView.functionsTabLabel', {
defaultMessage: 'TopN functions',
}),
isSelected: !isDifferentialView,
href: profilingRouter.link('/functions/topn', { query }),
},
{
label: i18n.translate('xpack.profiling.functionsView.differentialFunctionsTabLabel', {
defaultMessage: 'Differential TopN functions',
}),
isSelected: isDifferentialView,
href: profilingRouter.link('/functions/differential', {
query: {
...query,
comparisonRangeFrom: query.rangeFrom,
comparisonRangeTo: query.rangeTo,
comparisonKuery: query.kuery,
normalizationMode:
'normalizationMode' in query ? query.normalizationMode : NormalizationMode.Time,
},
}),
},
];
return (
<ProfilingAppPageTemplate tabs={tabs} hideSearchBar={isDifferentialView}>
{children}
</ProfilingAppPageTemplate>
);
}

View file

@ -0,0 +1,80 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { TopNFunctionsTable } from '../../../components/topn_functions';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
export function TopNFunctionsView() {
const {
path,
query,
query: { rangeFrom, rangeTo, kuery, sortDirection, sortField },
} = useProfilingParams('/functions/topn');
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const {
services: { fetchTopNFunctions },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return fetchTopNFunctions({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
startIndex: 0,
endIndex: 100000,
kuery,
});
},
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions]
);
const routePath = useProfilingRoutePath() as '/functions/topn';
const profilingRouter = useProfilingRouter();
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
<AsyncComponent {...state} size="xl" alignTop>
<TopNFunctionsTable
topNFunctions={state.data}
sortDirection={sortDirection}
sortField={sortField}
onSortChange={(nextSort) => {
profilingRouter.push(routePath, {
path,
query: {
...query,
sortField: nextSort.sortField,
sortDirection: nextSort.sortDirection,
},
});
}}
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={false}
/>
</AsyncComponent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -1,188 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TypeOf } from '@kbn/typed-react-router-config';
import React from 'react';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../hooks/use_time_range_async';
import { ProfilingRoutes } from '../../routing';
import { AsyncComponent } from '../../components/async_component';
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { PrimaryAndComparisonSearchBar } from '../../components/primary_and_comparison_search_bar';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { RedirectTo } from '../../components/redirect_to';
import { TopNFunctionsTable } from '../../components/topn_functions';
export function FunctionsView({ children }: { children: React.ReactElement }) {
const {
path,
query,
query: { rangeFrom, rangeTo, kuery, sortDirection, sortField },
} = useProfilingParams('/functions/*');
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const comparisonTimeRange = useTimeRange(
'comparisonRangeFrom' in query
? { rangeFrom: query.comparisonRangeFrom, rangeTo: query.comparisonRangeTo, optional: true }
: { rangeFrom: undefined, rangeTo: undefined, optional: true }
);
const comparisonKuery = 'comparisonKuery' in query ? query.comparisonKuery : '';
const {
services: { fetchTopNFunctions },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return fetchTopNFunctions({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
startIndex: 0,
endIndex: 100000,
kuery,
});
},
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions]
);
const comparisonState = useTimeRangeAsync(
({ http }) => {
if (!comparisonTimeRange.inSeconds.start || !comparisonTimeRange.inSeconds.end) {
return undefined;
}
return fetchTopNFunctions({
http,
timeFrom: comparisonTimeRange.inSeconds.start,
timeTo: comparisonTimeRange.inSeconds.end,
startIndex: 0,
endIndex: 100000,
kuery: comparisonKuery,
});
},
[
comparisonTimeRange.inSeconds.start,
comparisonTimeRange.inSeconds.end,
comparisonKuery,
fetchTopNFunctions,
]
);
const routePath = useProfilingRoutePath() as
| '/functions'
| '/functions/topn'
| '/functions/differential';
const profilingRouter = useProfilingRouter();
const isDifferentialView = routePath === '/functions/differential';
const tabs: Required<EuiPageHeaderContentProps>['tabs'] = [
{
label: i18n.translate('xpack.profiling.functionsView.functionsTabLabel', {
defaultMessage: 'TopN functions',
}),
isSelected: !isDifferentialView,
href: profilingRouter.link('/functions/topn', { query }),
},
{
label: i18n.translate('xpack.profiling.functionsView.differentialFunctionsTabLabel', {
defaultMessage: 'Differential TopN functions',
}),
isSelected: isDifferentialView,
href: profilingRouter.link('/functions/differential', {
query: {
...query,
comparisonRangeFrom: query.rangeFrom,
comparisonRangeTo: query.rangeTo,
comparisonKuery: query.kuery,
},
}),
},
];
if (routePath === '/functions') {
return <RedirectTo pathname="/functions/topn" />;
}
return (
<ProfilingAppPageTemplate tabs={tabs} hideSearchBar={isDifferentialView}>
<>
<EuiFlexGroup direction="column">
{isDifferentialView && (
<EuiFlexItem grow={false}>
<PrimaryAndComparisonSearchBar />
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem>
<AsyncComponent {...state} size="xl" alignTop>
<TopNFunctionsTable
topNFunctions={state.data}
sortDirection={sortDirection}
sortField={sortField}
onSortChange={(nextSort) => {
profilingRouter.push(routePath, {
path,
query: {
...query,
sortField: nextSort.sortField,
sortDirection: nextSort.sortDirection,
},
});
}}
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={isDifferentialView}
/>
</AsyncComponent>
</EuiFlexItem>
{isDifferentialView &&
comparisonTimeRange.inSeconds.start &&
comparisonTimeRange.inSeconds.end ? (
<EuiFlexItem>
<AsyncComponent {...comparisonState} size="xl" alignTop>
<TopNFunctionsTable
sortDirection={sortDirection}
sortField={sortField}
onSortChange={(nextSort) => {
profilingRouter.push(routePath, {
path,
query: {
...(query as TypeOf<
ProfilingRoutes,
'/functions/differential'
>['query']),
sortField: nextSort.sortField,
sortDirection: nextSort.sortDirection,
},
});
}}
topNFunctions={comparisonState.data}
comparisonTopNFunctions={state.data}
totalSeconds={
comparisonTimeRange.inSeconds.end - comparisonTimeRange.inSeconds.start
}
isDifferentialView={isDifferentialView}
/>
</AsyncComponent>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{children}
</>
</ProfilingAppPageTemplate>
);
}

View file

@ -27434,16 +27434,6 @@
"xpack.profiling.flameGraphLegend.improvement": "Amélioration",
"xpack.profiling.flameGraphLegend.regression": "Régression",
"xpack.profiling.flamegraphModel.noChange": "Aucune modification",
"xpack.profiling.flameGraphNormalizationMenu.normalizeBy": "Normaliser par",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByScaleTooltip": "Sélectionnez Normaliser par heure pour comparer un ensemble de machines sur différentes périodes. Par exemple, si vous comparez la dernière heure aux dernières 24 heures, la période la plus courte (1 heure) est multipliée pour égaler la période la plus longue (24 heures).",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByTimeTooltip": "Sélectionnez Normaliser par facteur d'échelle et définissez vos facteurs d'échelle de référence et de comparaison pour comparer un ensemble de machines de différentes tailles. Par exemple, vous pouvez comparer un déploiement de 10 % de machines à un déploiement de 90 % de machines.",
"xpack.profiling.flameGraphNormalizationMenu.scale": "Facteur de montée en charge",
"xpack.profiling.flameGraphNormalizationMenu.time": "Heure",
"xpack.profiling.flameGraphNormalizationMode.selectModeLegend": "Sélectionner un mode de normalisation pour le flame-graph",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeAbsoluteButtonLabel": "Abs",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeLegend": "Ce commutateur vous permet de basculer entre comparaison absolue et comparaison relative entre les deux graphes",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeRelativeButtonLabel": "Rel",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeTitle": "Format",
"xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel": "Flame-graph différentiel",
"xpack.profiling.flameGraphsView.flameGraphTabLabel": "Flame-graph",
"xpack.profiling.flameGraphTooltip.annualizedCo2": "CO2 annualisé",

View file

@ -27416,16 +27416,6 @@
"xpack.profiling.flameGraphLegend.improvement": "改善",
"xpack.profiling.flameGraphLegend.regression": "回帰",
"xpack.profiling.flamegraphModel.noChange": "変更なし",
"xpack.profiling.flameGraphNormalizationMenu.normalizeBy": "正規化",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByScaleTooltip": "時間で正規化を選択すると、異なる期間でコンピューターのセットを比較します。たとえば、過去1時間と過去24時間を比較する場合は、短い方の時間枠1時間が乗算され、長い方の時間枠24時間と照合されます。",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByTimeTooltip": "比率で正規化を選択し、ベースラインと比較比率を設定すると、異なるサイズのコンピューターのセットを比較します。たとえば、コンピューターの10%のデプロイをコンピューターの90%のデプロイと比較できます。",
"xpack.profiling.flameGraphNormalizationMenu.scale": "倍率",
"xpack.profiling.flameGraphNormalizationMenu.time": "時間",
"xpack.profiling.flameGraphNormalizationMode.selectModeLegend": "フレームグラフの正規化モードを選択",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeAbsoluteButtonLabel": "Abs",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeLegend": "このスイッチでは、両方のグラフの絶対比較と相対比較を切り替えることができます",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeRelativeButtonLabel": "Rel",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeTitle": "フォーマット",
"xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel": "差分flamegraph",
"xpack.profiling.flameGraphsView.flameGraphTabLabel": "Flamegraph",
"xpack.profiling.flameGraphTooltip.annualizedCo2": "年間換算CO2",

View file

@ -27414,16 +27414,6 @@
"xpack.profiling.flameGraphLegend.improvement": "提升",
"xpack.profiling.flameGraphLegend.regression": "回归",
"xpack.profiling.flamegraphModel.noChange": "无更改",
"xpack.profiling.flameGraphNormalizationMenu.normalizeBy": "标准化依据",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByScaleTooltip": "选择按时间标准化以跨不同时段比较一组机器。例如,如果您比较过去 1 小时与过去 24 小时的情况则会将较短时间范围1 小时加倍以匹配更长时间范围24 小时)。",
"xpack.profiling.flameGraphNormalizationMenu.normalizeByTimeTooltip": "选择按缩放因数标准化并设置基线和比较缩放因数,以比较不同大小的一组机器。例如,您可以将 10% 的机器部署与 90% 的机器部署进行比较。",
"xpack.profiling.flameGraphNormalizationMenu.scale": "缩放因数",
"xpack.profiling.flameGraphNormalizationMenu.time": "时间",
"xpack.profiling.flameGraphNormalizationMode.selectModeLegend": "为火焰图选择标准化模式",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeAbsoluteButtonLabel": "绝对",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeLegend": "此开关允许您在两个图表的绝对与相对比较之间进行切换",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeRelativeButtonLabel": "相对",
"xpack.profiling.flameGraphsView.differentialFlameGraphComparisonModeTitle": "格式",
"xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel": "差异火焰图",
"xpack.profiling.flameGraphsView.flameGraphTabLabel": "火焰图",
"xpack.profiling.flameGraphTooltip.annualizedCo2": "年化 CO2",