@parca/profile 0.16.302 → 0.16.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.304](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.303...@parca/profile@0.16.304) (2023-11-09)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## 0.16.303 (2023-11-09)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.302](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.301...@parca/profile@0.16.302) (2023-11-07)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -17,7 +17,7 @@ import { pointer } from 'd3-selection';
17
17
  import throttle from 'lodash.throttle';
18
18
  import { useContextMenu } from 'react-contexify';
19
19
  import { DateTimeRange } from '@parca/components';
20
- import { formatDate, formatForTimespan, sanitizeHighlightedValues, valueFormatter, } from '@parca/utilities';
20
+ import { formatDate, formatForTimespan, getPrecision, sanitizeHighlightedValues, valueFormatter, } from '@parca/utilities';
21
21
  import MetricsCircle from '../MetricsCircle';
22
22
  import MetricsSeries from '../MetricsSeries';
23
23
  import MetricsContextMenu from './MetricsContextMenu';
@@ -248,9 +248,17 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
248
248
  }
249
249
  return (_jsxs(_Fragment, { children: [_jsx(MetricsContextMenu, { onAddLabelMatcher: addLabelMatcher, menuId: MENU_ID, highlighted: highlighted, trackVisibility: trackVisibility }), highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (_jsx("div", { onMouseMove: onMouseMove, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false), children: !isContextMenuOpen && (_jsx(MetricsTooltip, { x: pos[0] + margin, y: pos[1] + margin, highlighted: highlighted, contextElement: graph.current, sampleUnit: sampleUnit, delta: isDeltaType })) })), _jsx("div", { ref: graph, onMouseEnter: function () {
250
250
  setHovering(true);
251
- }, onMouseLeave: () => setHovering(false), onContextMenu: displayMenu, children: _jsxs("svg", { width: `${width}px`, height: `${height + margin}px`, onMouseDown: onMouseDown, onMouseUp: onMouseUp, onMouseMove: onMouseMove, children: [_jsx("g", { transform: `translate(${margin}, 0)`, children: dragging && (_jsx("g", { className: "zoom-time-rect", children: _jsx("rect", { className: "bar", x: pos[0] - relPos < 0 ? pos[0] : relPos, y: 0, height: height, width: Math.abs(pos[0] - relPos), fill: 'rgba(0, 0, 0, 0.125)' }) })) }), _jsxs("g", { transform: `translate(${margin * 1.5}, ${margin / 1.5})`, children: [_jsxs("g", { className: "y axis", textAnchor: "end", fontSize: "10", fill: "none", children: [yScale.ticks(5).map((d, i) => (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
252
- /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
253
- transform: `translate(0, ${yScale(d)})`, children: [_jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x2: -6 }), _jsx("text", { fill: "currentColor", x: -9, dy: '0.32em', children: valueFormatter(d, sampleUnit, 1) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(from), x2: xScale(to), y1: yScale(d), y2: yScale(d) }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`))), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: 0, y1: 0, y2: height - margin }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(to), x2: xScale(to), y1: 0, y2: height - margin }), _jsx("g", { transform: `translate(${-margin}, ${(height - margin) / 2}) rotate(270)`, children: _jsx("text", { fill: "currentColor", dy: "-0.7em", className: "text-sm capitalize", textAnchor: "middle", children: yAxisLabel }) })] }), _jsxs("g", { className: "x axis", fill: "none", fontSize: "10", textAnchor: "middle", transform: `translate(0,${height - margin})`, children: [xScale.ticks(5).map((d, i) => (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
251
+ }, onMouseLeave: () => setHovering(false), onContextMenu: displayMenu, children: _jsxs("svg", { width: `${width}px`, height: `${height + margin}px`, onMouseDown: onMouseDown, onMouseUp: onMouseUp, onMouseMove: onMouseMove, children: [_jsx("g", { transform: `translate(${margin}, 0)`, children: dragging && (_jsx("g", { className: "zoom-time-rect", children: _jsx("rect", { className: "bar", x: pos[0] - relPos < 0 ? pos[0] : relPos, y: 0, height: height, width: Math.abs(pos[0] - relPos), fill: 'rgba(0, 0, 0, 0.125)' }) })) }), _jsxs("g", { transform: `translate(${margin * 1.5}, ${margin / 1.5})`, children: [_jsxs("g", { className: "y axis", textAnchor: "end", fontSize: "10", fill: "none", children: [yScale.ticks(5).map((d, i, allTicks) => {
252
+ let decimals = 2;
253
+ const intervalBetweenTicks = allTicks[1] - allTicks[0];
254
+ if (intervalBetweenTicks < 1) {
255
+ const precision = getPrecision(intervalBetweenTicks);
256
+ decimals = precision;
257
+ }
258
+ return (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
259
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
260
+ transform: `translate(0, ${yScale(d)})`, children: [_jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x2: -6 }), _jsx("text", { fill: "currentColor", x: -9, dy: '0.32em', children: valueFormatter(d, sampleUnit, decimals) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(from), x2: xScale(to), y1: yScale(d), y2: yScale(d) }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`));
261
+ }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: 0, y1: 0, y2: height - margin }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(to), x2: xScale(to), y1: 0, y2: height - margin }), _jsx("g", { transform: `translate(${-margin}, ${(height - margin) / 2}) rotate(270)`, children: _jsx("text", { fill: "currentColor", dy: "-0.7em", className: "text-sm capitalize", textAnchor: "middle", children: yAxisLabel }) })] }), _jsxs("g", { className: "x axis", fill: "none", fontSize: "10", textAnchor: "middle", transform: `translate(0,${height - margin})`, children: [xScale.ticks(5).map((d, i) => (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
254
262
  /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
255
263
  transform: `translate(${xScale(d)}, 0)`, children: [_jsx("line", { y2: 6, className: "stroke-gray-300 dark:stroke-gray-500" }), _jsx("text", { fill: "currentColor", dy: ".71em", y: 9, children: formatDate(d, formatForTimespan(from, to)) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(d), x2: xScale(d), y1: 0, y2: -height + margin }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`))), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(from), x2: xScale(to), y1: 0, y2: 0 }), _jsx("g", { transform: `translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`, children: _jsx("text", { fill: "currentColor", dy: ".71em", y: 5, className: "text-sm", children: "Time" }) })] }), _jsx("g", { className: "lines fill-transparent", children: series.map((s, i) => (_jsx("g", { className: "line", children: _jsx(MetricsSeries, { data: s, line: l, color: color(i.toString()), strokeWidth: hovering && highlighted != null && i === highlighted.seriesIndex
256
264
  ? lineStrokeHover
@@ -15,6 +15,7 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react';
15
15
  import { setHoveringNode, useAppDispatch } from '@parca/store';
16
16
  import { scaleLinear, selectQueryParam } from '@parca/utilities';
17
17
  import GraphTooltip from '../../GraphTooltip';
18
+ import { useProfileViewContext } from '../../ProfileView/ProfileViewContext';
18
19
  import ColorStackLegend from './ColorStackLegend';
19
20
  import { IcicleNode, RowHeight } from './IcicleGraphNodes';
20
21
  import useColoredGraph from './useColoredGraph';
@@ -25,7 +26,7 @@ export const IcicleGraph = memo(function IcicleGraph({ graph, total, filtered, w
25
26
  const ref = useRef(null);
26
27
  const coloredGraph = useColoredGraph(graph);
27
28
  const currentSearchString = selectQueryParam('search_string') ?? '';
28
- const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
29
+ const { compareMode } = useProfileViewContext();
29
30
  const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
30
31
  useEffect(() => {
31
32
  if (ref.current != null) {
@@ -20,6 +20,7 @@ import { getLastItem, scaleLinear, selectQueryParam, } from '@parca/utilities';
20
20
  import GraphTooltipArrow from '../../GraphTooltipArrow';
21
21
  import GraphTooltipArrowContent from '../../GraphTooltipArrow/Content';
22
22
  import { DockedGraphTooltip } from '../../GraphTooltipArrow/DockedGraphTooltip';
23
+ import { useProfileViewContext } from '../../ProfileView/ProfileViewContext';
23
24
  import ColorStackLegend from './ColorStackLegend';
24
25
  import ContextMenu from './ContextMenu';
25
26
  import { IcicleNode, RowHeight } from './IcicleGraphNodes';
@@ -53,7 +54,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, f
53
54
  const svg = useRef(null);
54
55
  const ref = useRef(null);
55
56
  const currentSearchString = selectQueryParam('search_string') ?? '';
56
- const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
57
+ const { compareMode } = useProfileViewContext();
57
58
  const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
58
59
  const mappings = useMemo(() => {
59
60
  // Read the mappings from the dictionary that contains all mapping strings.
@@ -16,7 +16,8 @@ import { Menu, Transition } from '@headlessui/react';
16
16
  import { Icon } from '@iconify/react';
17
17
  import { Button, Select, useParcaContext, useURLState } from '@parca/components';
18
18
  import { USER_PREFERENCES, useUserPreference } from '@parca/hooks';
19
- import { capitalizeOnlyFirstLetter, divide, selectQueryParam, } from '@parca/utilities';
19
+ import { capitalizeOnlyFirstLetter, divide } from '@parca/utilities';
20
+ import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
20
21
  import DiffLegend from '../components/DiffLegend';
21
22
  import IcicleGraph from './IcicleGraph';
22
23
  import IcicleGraphArrow, { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_FILE_NAME, FIELD_FUNCTION_NAME, FIELD_LABELS, FIELD_LOCATION_ADDRESS, FIELD_MAPPING_FILE, } from './IcicleGraphArrow';
@@ -29,7 +30,7 @@ const ShowHideLegendButton = ({ navigateTo }) => {
29
30
  param: 'color_stack_legend',
30
31
  navigateTo,
31
32
  });
32
- const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
33
+ const { compareMode } = useProfileViewContext();
33
34
  const isColorStackLegendEnabled = colorStackLegend === 'true';
34
35
  const [colorProfileName] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
35
36
  const setColorStackLegend = useCallback((value) => {
@@ -42,7 +43,7 @@ const GroupAndSortActionButtons = ({ navigateTo }) => {
42
43
  param: 'sort_by',
43
44
  navigateTo,
44
45
  });
45
- const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
46
+ const { compareMode } = useProfileViewContext();
46
47
  const [storeGroupBy = [FIELD_FUNCTION_NAME], setStoreGroupBy] = useURLState({
47
48
  param: 'group_by',
48
49
  navigateTo,
@@ -86,7 +87,7 @@ const RuntimeFilterDropdown = ({ showRuntimeRuby, toggleShowRuntimeRuby, showRun
86
87
  };
87
88
  const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, total, filtered, curPath, setNewCurPath, sampleUnit, navigateTo, loading, setActionButtons, error, width, }) {
88
89
  const { loader, onError, authenticationErrorMessage } = useParcaContext();
89
- const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
90
+ const { compareMode } = useProfileViewContext();
90
91
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
91
92
  param: 'sort_by',
92
93
  navigateTo,
@@ -132,7 +133,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
132
133
  if (isTrimmed) {
133
134
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
134
135
  }
135
- return (_jsxs("div", { className: "relative", children: [compareMode && _jsx(DiffLegend, {}), _jsxs("div", { className: "min-h-48", children: [graph !== undefined && (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), arrow !== undefined && (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }));
136
+ return (_jsxs("div", { className: "relative", children: [compareMode ? _jsx(DiffLegend, {}) : null, _jsxs("div", { className: "min-h-48", children: [graph !== undefined && (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), arrow !== undefined && (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }));
136
137
  };
137
138
  const groupByOptions = [
138
139
  {
@@ -3,6 +3,7 @@ import { ProfileSource } from '../ProfileSource';
3
3
  interface Props {
4
4
  profileSource?: ProfileSource;
5
5
  sampleUnit: string;
6
+ compareMode: boolean;
6
7
  }
7
8
  export declare const defaultValue: Props;
8
9
  declare const ProfileViewContext: import("react").Context<Props>;
@@ -15,6 +15,7 @@ import { createContext, useContext } from 'react';
15
15
  export const defaultValue = {
16
16
  profileSource: undefined,
17
17
  sampleUnit: 'bytes',
18
+ compareMode: false,
18
19
  };
19
20
  const ProfileViewContext = createContext(defaultValue);
20
21
  export const ProfileViewContextProvider = ({ children, value, }) => {
@@ -47,5 +47,5 @@ export interface ProfileViewProps {
47
47
  onDownloadPProf: () => void;
48
48
  pprofDownloading?: boolean;
49
49
  }
50
- export declare const ProfileView: ({ total, filtered, flamegraphData, topTableData, callgraphData, sourceData, sampleUnit, profileSource, queryClient, navigateTo, onDownloadPProf, pprofDownloading, }: ProfileViewProps) => JSX.Element;
50
+ export declare const ProfileView: ({ total, filtered, flamegraphData, topTableData, callgraphData, sourceData, sampleUnit, profileSource, queryClient, navigateTo, onDownloadPProf, pprofDownloading, compare, }: ProfileViewProps) => JSX.Element;
51
51
  export {};
@@ -21,7 +21,7 @@ import { DragDropContext, Draggable, Droppable, } from 'react-beautiful-dnd';
21
21
  import { Button, ConditionalWrapper, KeyDownProvider, UserPreferences, useParcaContext, useURLState, } from '@parca/components';
22
22
  import { useContainerDimensions } from '@parca/hooks';
23
23
  import { selectDarkMode, useAppSelector } from '@parca/store';
24
- import { getNewSpanColor } from '@parca/utilities';
24
+ import { getNewSpanColor, selectQueryParam } from '@parca/utilities';
25
25
  import { Callgraph } from '../';
26
26
  import { jsonToDot } from '../Callgraph/utils';
27
27
  import ProfileIcicleGraph from '../ProfileIcicleGraph';
@@ -39,7 +39,7 @@ function arrayEquals(a, b) {
39
39
  a.length === b.length &&
40
40
  a.every((val, index) => val === b[index]));
41
41
  }
42
- export const ProfileView = ({ total, filtered, flamegraphData, topTableData, callgraphData, sourceData, sampleUnit, profileSource, queryClient, navigateTo, onDownloadPProf, pprofDownloading, }) => {
42
+ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, callgraphData, sourceData, sampleUnit, profileSource, queryClient, navigateTo, onDownloadPProf, pprofDownloading, compare, }) => {
43
43
  const { ref, dimensions } = useContainerDimensions();
44
44
  const [curPath, setCurPath] = useState([]);
45
45
  const [rawDashboardItems = ['icicle'], setDashboardItems] = useURLState({
@@ -171,9 +171,11 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
171
171
  const profileSourceString = profileSource?.toString();
172
172
  const hasProfileSource = profileSource !== undefined && profileSourceString !== '';
173
173
  const headerParts = profileSourceString?.split('"') ?? [];
174
- return (_jsx(KeyDownProvider, { children: _jsxs(ProfileViewContextProvider, { value: { profileSource, sampleUnit }, children: [_jsxs("div", { className: cx('mb-4 flex w-full items-center', hasProfileSource ? 'justify-between' : 'justify-end'), children: [hasProfileSource && (_jsxs("div", { className: "max-w-[300px]", children: [_jsx("div", { className: "text-sm font-medium capitalize", children: headerParts.length > 0 ? headerParts[0].replace(/"/g, '') : '' }), _jsx("div", { className: "text-xs", children: headerParts.length > 1
174
+ const compareMode = compare === true ||
175
+ (selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true');
176
+ return (_jsx(KeyDownProvider, { children: _jsxs(ProfileViewContextProvider, { value: { profileSource, sampleUnit, compareMode }, children: [_jsxs("div", { className: cx('mb-4 flex w-full items-center', hasProfileSource ? 'justify-between' : 'justify-end'), children: [hasProfileSource && (_jsxs("div", { className: "max-w-[300px]", children: [_jsx("div", { className: "text-sm font-medium capitalize", children: headerParts.length > 0 ? headerParts[0].replace(/"/g, '') : '' }), _jsx("div", { className: "text-xs", children: headerParts.length > 1
175
177
  ? headerParts[headerParts.length - 1].replace(/"/g, '')
176
- : '' })] })), _jsxs("div", { className: "flex items-center md:justify-end gap-2 flex-wrap", children: [_jsx(FilterByFunctionButton, { navigateTo: navigateTo }), _jsx(UserPreferences, { customButton: _jsxs(Button, { className: "gap-2", variant: "neutral", children: ["Preferences", _jsx(Icon, { icon: "pajamas:preferences", width: 20 })] }) }), profileSource !== undefined && queryClient !== undefined ? (_jsx(ProfileShareButton, { queryRequest: profileSource.QueryRequest(), queryClient: queryClient })) : null, _jsxs(Button, { className: "gap-2", variant: "neutral", onClick: e => {
178
+ : '' })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2 md:justify-end", children: [_jsx(FilterByFunctionButton, { navigateTo: navigateTo }), _jsx(UserPreferences, { customButton: _jsxs(Button, { className: "gap-2", variant: "neutral", children: ["Preferences", _jsx(Icon, { icon: "pajamas:preferences", width: 20 })] }) }), profileSource !== undefined && queryClient !== undefined ? (_jsx(ProfileShareButton, { queryRequest: profileSource.QueryRequest(), queryClient: queryClient })) : null, _jsxs(Button, { className: "gap-2", variant: "neutral", onClick: e => {
177
179
  e.preventDefault();
178
180
  onDownloadPProf();
179
181
  }, disabled: pprofDownloading, children: [pprofDownloading != null && pprofDownloading ? 'Downloading...' : 'Download pprof', _jsx(Icon, { icon: "material-symbols:download", width: 20 })] }), _jsx(ViewSelector, { defaultValue: "", navigateTo: navigateTo, position: -1, placeholderText: "Add panel", icon: _jsx(Icon, { icon: "material-symbols:add", width: 20 }), addView: true, disabled: isMultiPanelView || dashboardItems.length < 1 })] })] }), _jsx("div", { className: "w-full", ref: ref, children: isLoaderVisible ? (_jsx(_Fragment, { children: loader })) : (_jsx(DragDropContext, { onDragEnd: onDragEnd, children: _jsx(Droppable, { droppableId: "droppable", direction: "horizontal", children: provided => (_jsx("div", { ref: provided.innerRef, className: cx('grid w-full gap-2', isMultiPanelView ? 'grid-cols-2' : 'grid-cols-1'), ...provided.droppableProps, children: dashboardItems.map((dashboardItem, index) => {
@@ -17,6 +17,7 @@ import { Icon } from '@iconify/react';
17
17
  import { tableFromIPC } from 'apache-arrow';
18
18
  import { Button, Table as TableComponent, useURLState } from '@parca/components';
19
19
  import { getLastItem, isSearchMatch, parseParams, valueFormatter, } from '@parca/utilities';
20
+ import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
20
21
  import { hexifyAddress } from '../utils';
21
22
  const FIELD_MAPPING_FILE = 'mapping_file';
22
23
  const FIELD_LOCATION_ADDRESS = 'location_address';
@@ -30,9 +31,8 @@ const FIELD_CUMULATIVE_DIFF = 'cumulative_diff';
30
31
  export const Table = React.memo(function Table({ data, total, filtered, sampleUnit: unit, navigateTo, loading, currentSearchString, setActionButtons, }) {
31
32
  const router = parseParams(window?.location.search);
32
33
  const [rawDashboardItems] = useURLState({ param: 'dashboard_items' });
33
- const [rawcompareMode] = useURLState({ param: 'compare_a' });
34
34
  const [filterByFunctionInput] = useURLState({ param: 'filter_by_function' });
35
- const compareMode = rawcompareMode === undefined ? false : rawcompareMode === 'true';
35
+ const { compareMode } = useProfileViewContext();
36
36
  const dashboardItems = useMemo(() => {
37
37
  if (rawDashboardItems !== undefined) {
38
38
  return rawDashboardItems;
@@ -15,6 +15,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
15
15
  import { createColumnHelper } from '@tanstack/react-table';
16
16
  import { Button, Table, useURLState } from '@parca/components';
17
17
  import { getLastItem, isSearchMatch, parseParams, valueFormatter, } from '@parca/utilities';
18
+ import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
18
19
  import { hexifyAddress } from '../utils';
19
20
  export const RowLabel = (meta) => {
20
21
  if (meta === undefined)
@@ -38,8 +39,7 @@ const addPlusSign = (num) => {
38
39
  export const TopTable = React.memo(function TopTable({ data: top, sampleUnit: unit, navigateTo, loading, currentSearchString, setActionButtons, }) {
39
40
  const router = parseParams(window?.location.search);
40
41
  const [rawDashboardItems] = useURLState({ param: 'dashboard_items' });
41
- const [rawcompareMode] = useURLState({ param: 'compare_a' });
42
- const compareMode = rawcompareMode === undefined ? false : rawcompareMode === 'true';
42
+ const { compareMode } = useProfileViewContext();
43
43
  const dashboardItems = useMemo(() => {
44
44
  if (rawDashboardItems !== undefined) {
45
45
  return rawDashboardItems;
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.302",
3
+ "version": "0.16.304",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@parca/client": "^0.16.98",
7
- "@parca/components": "^0.16.225",
7
+ "@parca/components": "^0.16.226",
8
8
  "@parca/dynamicsize": "^0.16.60",
9
9
  "@parca/hooks": "^0.0.35",
10
10
  "@parca/parser": "^0.16.66",
11
- "@parca/store": "^0.16.115",
12
- "@parca/utilities": "^0.0.43",
11
+ "@parca/store": "^0.16.116",
12
+ "@parca/utilities": "^0.0.44",
13
13
  "@tanstack/react-query": "^4.0.5",
14
14
  "@types/react-beautiful-dnd": "^13.1.3",
15
15
  "apache-arrow": "^12.0.0",
@@ -50,5 +50,5 @@
50
50
  "access": "public",
51
51
  "registry": "https://registry.npmjs.org/"
52
52
  },
53
- "gitHead": "c21b3a846496d053c2a2dd8f0eda1930f4d8d26f"
53
+ "gitHead": "a342b32b3ed89bee6659d1f4fd073c6bcd52cb16"
54
54
  }
@@ -23,6 +23,7 @@ import {DateTimeRange} from '@parca/components';
23
23
  import {
24
24
  formatDate,
25
25
  formatForTimespan,
26
+ getPrecision,
26
27
  sanitizeHighlightedValues,
27
28
  valueFormatter,
28
29
  } from '@parca/utilities';
@@ -450,30 +451,40 @@ export const RawMetricsGraph = ({
450
451
  </g>
451
452
  <g transform={`translate(${margin * 1.5}, ${margin / 1.5})`}>
452
453
  <g className="y axis" textAnchor="end" fontSize="10" fill="none">
453
- {yScale.ticks(5).map((d, i) => (
454
- <Fragment key={`${i.toString()}-${d.toString()}`}>
455
- <g
456
- key={`tick-${i}`}
457
- className="tick"
458
- /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
459
- transform={`translate(0, ${yScale(d)})`}
460
- >
461
- <line className="stroke-gray-300 dark:stroke-gray-500" x2={-6} />
462
- <text fill="currentColor" x={-9} dy={'0.32em'}>
463
- {valueFormatter(d, sampleUnit, 1)}
464
- </text>
465
- </g>
466
- <g key={`grid-${i}`}>
467
- <line
468
- className="stroke-gray-300 dark:stroke-gray-500"
469
- x1={xScale(from)}
470
- x2={xScale(to)}
471
- y1={yScale(d)}
472
- y2={yScale(d)}
473
- />
474
- </g>
475
- </Fragment>
476
- ))}
454
+ {yScale.ticks(5).map((d, i, allTicks) => {
455
+ let decimals = 2;
456
+ const intervalBetweenTicks = allTicks[1] - allTicks[0];
457
+
458
+ if (intervalBetweenTicks < 1) {
459
+ const precision = getPrecision(intervalBetweenTicks);
460
+ decimals = precision;
461
+ }
462
+
463
+ return (
464
+ <Fragment key={`${i.toString()}-${d.toString()}`}>
465
+ <g
466
+ key={`tick-${i}`}
467
+ className="tick"
468
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
469
+ transform={`translate(0, ${yScale(d)})`}
470
+ >
471
+ <line className="stroke-gray-300 dark:stroke-gray-500" x2={-6} />
472
+ <text fill="currentColor" x={-9} dy={'0.32em'}>
473
+ {valueFormatter(d, sampleUnit, decimals)}
474
+ </text>
475
+ </g>
476
+ <g key={`grid-${i}`}>
477
+ <line
478
+ className="stroke-gray-300 dark:stroke-gray-500"
479
+ x1={xScale(from)}
480
+ x2={xScale(to)}
481
+ y1={yScale(d)}
482
+ y2={yScale(d)}
483
+ />
484
+ </g>
485
+ </Fragment>
486
+ );
487
+ })}
477
488
  <line
478
489
  className="stroke-gray-300 dark:stroke-gray-500"
479
490
  x1={0}
@@ -18,6 +18,7 @@ import {setHoveringNode, useAppDispatch} from '@parca/store';
18
18
  import {scaleLinear, selectQueryParam, type NavigateFunction} from '@parca/utilities';
19
19
 
20
20
  import GraphTooltip from '../../GraphTooltip';
21
+ import {useProfileViewContext} from '../../ProfileView/ProfileViewContext';
21
22
  import ColorStackLegend from './ColorStackLegend';
22
23
  import {IcicleNode, RowHeight} from './IcicleGraphNodes';
23
24
  import useColoredGraph from './useColoredGraph';
@@ -50,8 +51,7 @@ export const IcicleGraph = memo(function IcicleGraph({
50
51
 
51
52
  const coloredGraph = useColoredGraph(graph);
52
53
  const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
53
- const compareMode: boolean =
54
- selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
54
+ const {compareMode} = useProfileViewContext();
55
55
  const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
56
56
 
57
57
  useEffect(() => {
@@ -36,6 +36,7 @@ import {
36
36
  import GraphTooltipArrow from '../../GraphTooltipArrow';
37
37
  import GraphTooltipArrowContent from '../../GraphTooltipArrow/Content';
38
38
  import {DockedGraphTooltip} from '../../GraphTooltipArrow/DockedGraphTooltip';
39
+ import {useProfileViewContext} from '../../ProfileView/ProfileViewContext';
39
40
  import ColorStackLegend from './ColorStackLegend';
40
41
  import ContextMenu from './ContextMenu';
41
42
  import {IcicleNode, RowHeight, mappingColors} from './IcicleGraphNodes';
@@ -98,8 +99,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
98
99
  const ref = useRef<SVGGElement>(null);
99
100
 
100
101
  const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
101
- const compareMode: boolean =
102
- selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
102
+ const {compareMode} = useProfileViewContext();
103
103
  const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
104
104
 
105
105
  const mappings = useMemo(() => {
@@ -19,13 +19,9 @@ import {Icon} from '@iconify/react';
19
19
  import {Flamegraph, FlamegraphArrow} from '@parca/client';
20
20
  import {Button, Select, useParcaContext, useURLState} from '@parca/components';
21
21
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
22
- import {
23
- capitalizeOnlyFirstLetter,
24
- divide,
25
- selectQueryParam,
26
- type NavigateFunction,
27
- } from '@parca/utilities';
22
+ import {capitalizeOnlyFirstLetter, divide, type NavigateFunction} from '@parca/utilities';
28
23
 
24
+ import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
29
25
  import DiffLegend from '../components/DiffLegend';
30
26
  import IcicleGraph from './IcicleGraph';
31
27
  import IcicleGraphArrow, {
@@ -67,8 +63,7 @@ const ShowHideLegendButton = ({navigateTo}: {navigateTo?: NavigateFunction}): JS
67
63
  navigateTo,
68
64
  });
69
65
 
70
- const compareMode: boolean =
71
- selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
66
+ const {compareMode} = useProfileViewContext();
72
67
 
73
68
  const isColorStackLegendEnabled = colorStackLegend === 'true';
74
69
 
@@ -104,8 +99,7 @@ const GroupAndSortActionButtons = ({navigateTo}: {navigateTo?: NavigateFunction}
104
99
  param: 'sort_by',
105
100
  navigateTo,
106
101
  });
107
- const compareMode: boolean =
108
- selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
102
+ const {compareMode} = useProfileViewContext();
109
103
 
110
104
  const [storeGroupBy = [FIELD_FUNCTION_NAME], setStoreGroupBy] = useURLState({
111
105
  param: 'group_by',
@@ -298,8 +292,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
298
292
  width,
299
293
  }: ProfileIcicleGraphProps): JSX.Element {
300
294
  const {loader, onError, authenticationErrorMessage} = useParcaContext();
301
- const compareMode: boolean =
302
- selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
295
+ const {compareMode} = useProfileViewContext();
303
296
 
304
297
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
305
298
  param: 'sort_by',
@@ -383,7 +376,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
383
376
 
384
377
  return (
385
378
  <div className="relative">
386
- {compareMode && <DiffLegend />}
379
+ {compareMode ? <DiffLegend /> : null}
387
380
  <div className="min-h-48">
388
381
  {graph !== undefined && (
389
382
  <IcicleGraph
@@ -18,11 +18,13 @@ import {ProfileSource} from '../ProfileSource';
18
18
  interface Props {
19
19
  profileSource?: ProfileSource;
20
20
  sampleUnit: string;
21
+ compareMode: boolean;
21
22
  }
22
23
 
23
24
  export const defaultValue: Props = {
24
25
  profileSource: undefined,
25
26
  sampleUnit: 'bytes',
27
+ compareMode: false,
26
28
  };
27
29
 
28
30
  const ProfileViewContext = createContext<Props>(defaultValue);
@@ -44,7 +44,7 @@ import {
44
44
  } from '@parca/components';
45
45
  import {useContainerDimensions} from '@parca/hooks';
46
46
  import {selectDarkMode, useAppSelector} from '@parca/store';
47
- import {getNewSpanColor} from '@parca/utilities';
47
+ import {getNewSpanColor, selectQueryParam} from '@parca/utilities';
48
48
 
49
49
  import {Callgraph} from '../';
50
50
  import {jsonToDot} from '../Callgraph/utils';
@@ -131,6 +131,7 @@ export const ProfileView = ({
131
131
  navigateTo,
132
132
  onDownloadPProf,
133
133
  pprofDownloading,
134
+ compare,
134
135
  }: ProfileViewProps): JSX.Element => {
135
136
  const {ref, dimensions} = useContainerDimensions();
136
137
  const [curPath, setCurPath] = useState<string[]>([]);
@@ -352,9 +353,13 @@ export const ProfileView = ({
352
353
  const hasProfileSource = profileSource !== undefined && profileSourceString !== '';
353
354
  const headerParts = profileSourceString?.split('"') ?? [];
354
355
 
356
+ const compareMode =
357
+ compare === true ||
358
+ (selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true');
359
+
355
360
  return (
356
361
  <KeyDownProvider>
357
- <ProfileViewContextProvider value={{profileSource, sampleUnit}}>
362
+ <ProfileViewContextProvider value={{profileSource, sampleUnit, compareMode}}>
358
363
  <div
359
364
  className={cx(
360
365
  'mb-4 flex w-full items-center',
@@ -374,7 +379,7 @@ export const ProfileView = ({
374
379
  </div>
375
380
  )}
376
381
 
377
- <div className="flex items-center md:justify-end gap-2 flex-wrap">
382
+ <div className="flex flex-wrap items-center gap-2 md:justify-end">
378
383
  <FilterByFunctionButton navigateTo={navigateTo} />
379
384
  <UserPreferences
380
385
  customButton={
@@ -27,6 +27,7 @@ import {
27
27
  type NavigateFunction,
28
28
  } from '@parca/utilities';
29
29
 
30
+ import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
30
31
  import {hexifyAddress} from '../utils';
31
32
 
32
33
  const FIELD_MAPPING_FILE = 'mapping_file';
@@ -84,10 +85,9 @@ export const Table = React.memo(function Table({
84
85
  }: TableProps): React.JSX.Element {
85
86
  const router = parseParams(window?.location.search);
86
87
  const [rawDashboardItems] = useURLState({param: 'dashboard_items'});
87
- const [rawcompareMode] = useURLState({param: 'compare_a'});
88
88
  const [filterByFunctionInput] = useURLState({param: 'filter_by_function'});
89
89
 
90
- const compareMode: boolean = rawcompareMode === undefined ? false : rawcompareMode === 'true';
90
+ const {compareMode} = useProfileViewContext();
91
91
 
92
92
  const dashboardItems = useMemo(() => {
93
93
  if (rawDashboardItems !== undefined) {
@@ -25,6 +25,7 @@ import {
25
25
  type NavigateFunction,
26
26
  } from '@parca/utilities';
27
27
 
28
+ import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
28
29
  import {hexifyAddress} from '../utils';
29
30
 
30
31
  interface TopTableProps {
@@ -72,9 +73,8 @@ export const TopTable = React.memo(function TopTable({
72
73
  }: TopTableProps): JSX.Element {
73
74
  const router = parseParams(window?.location.search);
74
75
  const [rawDashboardItems] = useURLState({param: 'dashboard_items'});
75
- const [rawcompareMode] = useURLState({param: 'compare_a'});
76
76
 
77
- const compareMode: boolean = rawcompareMode === undefined ? false : rawcompareMode === 'true';
77
+ const {compareMode} = useProfileViewContext();
78
78
 
79
79
  const dashboardItems = useMemo(() => {
80
80
  if (rawDashboardItems !== undefined) {