(preferenceName: UserPreference): [T, (flag: T) => void] => {
+ const [flags, setFlags] = useLocalStorage<{[flag: string]: any}>(USER_PREFERENCES_KEY, {});
+
+ const value: T = flags[preferenceName] ?? USER_PREFERENCES[preferenceName].default;
+ const setFlag = (flag: T): void => {
+ setFlags({...flags, [preferenceName]: flag});
+ };
+
+ return [value, setFlag];
+};
+
+export default useUserPreference;
diff --git a/ui/packages/shared/functions/useUserPreference.ts b/ui/packages/shared/functions/useUserPreference.ts
new file mode 100644
index 00000000000..6bac5fb4dd4
--- /dev/null
+++ b/ui/packages/shared/functions/useUserPreference.ts
@@ -0,0 +1,15 @@
+// Copyright 2022 The Parca Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export {default} from './src/useUserPreference';
+export * from './src/useUserPreference';
diff --git a/ui/packages/shared/profile/src/IcicleGraph.tsx b/ui/packages/shared/profile/src/IcicleGraph.tsx
index 5c7670edf00..2a0097de48b 100644
--- a/ui/packages/shared/profile/src/IcicleGraph.tsx
+++ b/ui/packages/shared/profile/src/IcicleGraph.tsx
@@ -383,7 +383,7 @@ export default function IcicleGraph({
if (ref.current != null) {
setHeight(ref?.current.getBoundingClientRect().height);
}
- }, [width]);
+ }, [width, graph]);
const total = useMemo(() => parseFloat(graph.total), [graph.total]);
const xScale = useMemo(() => {
diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx
index 9abdd82891f..e8662b88343 100644
--- a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx
+++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx
@@ -17,6 +17,11 @@ import {useContainerDimensions} from '@parca/dynamicsize';
import DiffLegend from '../components/DiffLegend';
import IcicleGraph from '../IcicleGraph';
+import {useEffect, useMemo} from 'react';
+
+const numberFormatter = new Intl.NumberFormat('en-US');
+
+export type ResizeHandler = (width: number, height: number) => void;
interface ProfileIcicleGraphProps {
width?: number;
@@ -24,6 +29,7 @@ interface ProfileIcicleGraphProps {
sampleUnit: string;
curPath: string[] | [];
setNewCurPath: (path: string[]) => void;
+ onContainerResize?: ResizeHandler;
}
const ProfileIcicleGraph = ({
@@ -31,10 +37,38 @@ const ProfileIcicleGraph = ({
curPath,
setNewCurPath,
sampleUnit,
+ onContainerResize,
}: ProfileIcicleGraphProps): JSX.Element => {
const compareMode = useAppSelector(selectCompareMode);
const {ref, dimensions} = useContainerDimensions();
+ useEffect(() => {
+ if (dimensions === undefined) return;
+ if (onContainerResize === undefined) return;
+
+ onContainerResize(dimensions.width, dimensions.height);
+ }, [dimensions, onContainerResize]);
+
+ const [trimDifference, trimmedPercentage, formattedTotal, formattedUntrimmedTotal] =
+ useMemo(() => {
+ if (graph === undefined || graph.untrimmedTotal === '0') {
+ return [BigInt(0), '0'];
+ }
+
+ const untrimmedTotal = BigInt(graph.untrimmedTotal);
+ const total = BigInt(graph.total);
+
+ const trimDifference = untrimmedTotal - total;
+ const trimmedPercentage = (total * BigInt(100)) / untrimmedTotal;
+
+ return [
+ trimDifference,
+ trimmedPercentage.toString(),
+ numberFormatter.format(total),
+ numberFormatter.format(untrimmedTotal),
+ ];
+ }, [graph]);
+
if (graph === undefined) return no data...
;
const total = graph.total;
if (parseFloat(total) === 0) return <>Profile has no samples>;
@@ -42,6 +76,11 @@ const ProfileIcicleGraph = ({
return (
<>
{compareMode && }
+ {trimDifference > BigInt(0) ? (
+
+ Showing {formattedTotal}({trimmedPercentage}%) out of {formattedUntrimmedTotal} samples
+
+ ) : null}
void;
@@ -76,6 +77,7 @@ export interface ProfileViewProps {
navigateTo?: NavigateFunction;
compare?: boolean;
onDownloadPProf: () => void;
+ onFlamegraphContainerResize?: ResizeHandler;
}
function arrayEquals(a: T[], b: T[]): boolean {
@@ -113,6 +115,7 @@ export const ProfileView = ({
navigateTo,
profileVisState,
onDownloadPProf,
+ onFlamegraphContainerResize,
}: ProfileViewProps): JSX.Element => {
const dispatch = useAppDispatch();
const {ref, dimensions} = useContainerDimensions();
@@ -123,7 +126,9 @@ export const ProfileView = ({
const filterByFunctionString = useAppSelector(selectFilterByFunction);
const [callgraphEnabled] = useUIFeatureFlag('callgraph');
- const [highlightAfterFilteringEnabled] = useUIFeatureFlag('highlightAfterFiltering');
+ const [highlightAfterFilteringEnabled] = useUserPreference(
+ USER_PREFERENCES.HIGHTLIGHT_AFTER_FILTERING.key
+ );
const {loader, perf} = useParcaContext();
@@ -305,6 +310,7 @@ export const ProfileView = ({
setNewCurPath={setNewCurPath}
graph={flamegraphData.data}
sampleUnit={sampleUnit}
+ onContainerResize={onFlamegraphContainerResize}
/>
diff --git a/ui/packages/shared/profile/src/ProfileViewWithData.tsx b/ui/packages/shared/profile/src/ProfileViewWithData.tsx
index 4f8737bf130..64250989b46 100644
--- a/ui/packages/shared/profile/src/ProfileViewWithData.tsx
+++ b/ui/packages/shared/profile/src/ProfileViewWithData.tsx
@@ -19,7 +19,8 @@ import {ProfileSource} from './ProfileSource';
import {downloadPprof} from './utils';
import {useGrpcMetadata, useParcaContext} from '@parca/components';
import {saveAsBlob} from '@parca/functions';
-import {useEffect} from 'react';
+import {useEffect, useState} from 'react';
+import useUserPreference, {USER_PREFERENCES} from '@parca/functions/useUserPreference';
export type NavigateFunction = (path: string, queryParams: any) => void;
@@ -38,12 +39,33 @@ export const ProfileViewWithData = ({
const profileVisState = useProfileVisState();
const metadata = useGrpcMetadata();
const {currentView} = profileVisState;
+ const [nodeTrimThreshold, setNodeTrimThreshold] = useState(0);
+ const [disableTrimming] = useUserPreference(USER_PREFERENCES.DISABLE_GRAPH_TRIMMING.key);
+
+ useEffect(() => {
+ if (disableTrimming) {
+ setNodeTrimThreshold(0);
+ }
+ }, [disableTrimming]);
+
+ const onFlamegraphContainerResize = (width: number): void => {
+ if (disableTrimming || width === 0) {
+ return;
+ }
+ const threshold = (1 / width) * 100;
+ if (threshold === nodeTrimThreshold) {
+ return;
+ }
+ setNodeTrimThreshold(threshold);
+ };
+
const {
isLoading: flamegraphLoading,
response: flamegraphResponse,
error: flamegraphError,
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_TABLE, {
skip: currentView !== 'icicle' && currentView !== 'both',
+ nodeTrimThreshold,
});
const {perf} = useParcaContext();
@@ -120,6 +142,7 @@ export const ProfileViewWithData = ({
queryClient={queryClient}
navigateTo={navigateTo}
onDownloadPProf={() => void downloadPProfClick()}
+ onFlamegraphContainerResize={onFlamegraphContainerResize}
/>
);
};
diff --git a/ui/packages/shared/profile/src/useQuery.tsx b/ui/packages/shared/profile/src/useQuery.tsx
index 78f1772ad73..42bab645a79 100644
--- a/ui/packages/shared/profile/src/useQuery.tsx
+++ b/ui/packages/shared/profile/src/useQuery.tsx
@@ -26,6 +26,7 @@ export interface IQueryResult {
interface UseQueryOptions {
skip?: boolean;
+ nodeTrimThreshold?: number;
}
export const useQuery = (
@@ -37,10 +38,11 @@ export const useQuery = (
const {skip = false} = options ?? {};
const metadata = useGrpcMetadata();
const {data, isLoading, error} = useGrpcQuery({
- key: ['query', profileSource, reportType],
+ key: ['query', profileSource, reportType, options?.nodeTrimThreshold],
queryFn: async () => {
const req = profileSource.QueryRequest();
req.reportType = reportType;
+ req.nodeTrimThreshold = options?.nodeTrimThreshold;
const {response} = await client.query(req, {meta: metadata});
return response;