Skip to content

Commit

Permalink
[#noissue] feat: add search and link func into timeline UI
Browse files Browse the repository at this point in the history
  • Loading branch information
BillionaireDY authored and binDongKim committed Jun 20, 2024
1 parent 00ffaf0 commit 62fba42
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 89 deletions.
2 changes: 2 additions & 0 deletions web-frontend/src/main/v3/packages/atoms/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ import { HeatmapDrag, TransactionInfo } from '@pinpoint-fe/constants';

export const transactionListDatasAtom = atom<HeatmapDrag.Response | undefined>(undefined);
export const transactionInfoDatasAtom = atom<TransactionInfo.Response | undefined>(undefined);
export const transactionInfoCurrentTabId = atom<string>('');
export const transactionInfoCallTreeFocusId = atom<string>('');
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function VirtualizedDataTable<TData, TValue>({
onChangeRowSelection?.([...selectedRowData]);
}, [rowSelection]);

useUpdateEffect(() => {
React.useEffect(() => {
if (focusRowIndex) {
rowVirtualizer.scrollToIndex(focusRowIndex, { align: 'center' });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import React from 'react';
import { throttle } from 'lodash';
import cloneDeep from 'lodash.clonedeep';
import { FlameNode, FlameNodeClickHandler, FlameNodeType } from './FlameNode';
import { FlameNode, FlameNodeType, FlameNodeProps } from './FlameNode';
import { FlameAxis } from './FlameAxis';
import { FlameGraphConfigContext, flameGraphDefaultConfig } from './FlameGraphConfigContext';
import { FlameTimeline } from './FlameTimeline';

export interface FlameGraphProps<T> {
data: FlameNodeType<T>[];
export interface FlameGraphProps<T>
extends Pick<FlameNodeProps<T>, 'customNodeStyle' | 'customTextStyle' | 'onClickNode'> {
data: FlameNodeType<T>[][];
start?: number;
end?: number;
onClickNode?: FlameNodeClickHandler<T>;
}

export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameGraphProps<T>) => {
export const FlameGraph = <T,>({
data,
start = 0,
end = 0,
onClickNode,
customNodeStyle,
customTextStyle,
}: FlameGraphProps<T>) => {
const widthOffset = end - start || 1;
const [config] = React.useState(flameGraphDefaultConfig);

Expand Down Expand Up @@ -60,8 +67,12 @@ export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameG

function getContainerHeight() {
const { height, padding } = config;
return data.reduce((acc, curr) => {
return acc + (getMaxDepth(curr) + 1) * height.node + padding.group;

return data.reduce((acc, group) => {
const groupHeights = group.map(
(node) => (getMaxDepth(node) + 1) * height.node + padding.group,
);
return acc + Math.max(...groupHeights);
}, 2 * padding.bottom);
}

Expand All @@ -88,29 +99,42 @@ export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameG
<FlameAxis width={containerWidth} />
{containerWidth &&
// group별 렌더링
data.map((node, i) => {
data.map((group, i) => {
if (i === 0) prevDepth.current = 0;

const { height, padding, color } = config;
const newNode = cloneDeep(node);
const currentNodeDepth = getMaxDepth(newNode);
const yOffset = prevDepth.current * height.node + padding.group * i;

styleNode(newNode, 0, 0, yOffset);
prevDepth.current = currentNodeDepth + prevDepth.current + 1;

return (
<React.Fragment key={node.id}>
<FlameNode node={newNode} svgRef={svgRef} onClickNode={onClickNode} />
<line
x1={0}
y1={yOffset - padding.group / 2 + padding.top}
x2={containerWidth}
y2={yOffset - padding.group / 2 + padding.top}
stroke={color.axis}
/>
</React.Fragment>
);
else {
const prevGroupMaxDepth = Math.max(
...data[i - 1].map((node) => getMaxDepth(node)),
);
prevDepth.current = prevGroupMaxDepth + prevDepth.current + 1;
}

// node별 렌더링
return group.map((node) => {
const { height, padding, color } = config;
const newNode = cloneDeep(node);
const yOffset = prevDepth.current * height.node + padding.group * i;

styleNode(newNode, 0, 0, yOffset);

return (
<React.Fragment key={node.id}>
<FlameNode
node={newNode}
svgRef={svgRef}
customNodeStyle={customNodeStyle}
customTextStyle={customTextStyle}
onClickNode={onClickNode}
/>
<line
x1={0}
y1={yOffset - padding.group / 2 + padding.top}
x2={containerWidth}
y2={yOffset - padding.group / 2 + padding.top}
stroke={color.axis}
/>
</React.Fragment>
);
});
})}
</svg>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,52 @@ export interface FlameNodeType<T> {
children: FlameNodeType<T>[];
}

export type FlameNodeClickHandler<T> = (node: FlameNodeType<T | unknown>) => void;
export type FlameNodeColorType = {
color: string;
hoverColor: string;
};

export type FlameNodeClickHandler<T> = (node: FlameNodeType<T>) => void;

export interface FlameNodeProps<T> {
node: FlameNodeType<T>;
svgRef?: React.RefObject<SVGSVGElement>;
onClickNode?: FlameNodeClickHandler<T>;
onClickNode: FlameNodeClickHandler<T>;
customNodeStyle?: (node: FlameNodeType<T>, color: FlameNodeColorType) => React.CSSProperties;
customTextStyle?: (node: FlameNodeType<T>, color: string) => React.CSSProperties;
// renderText?: (
// text: string,
// elementAttributes: React.SVGTextElementAttributes<SVGTextElement>,
// ) => ReactElement;
}

export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNodeProps<T>) => {
const { x = 0, y = 0, width = 1, height = 1, name, nodeStyle, textStyle } = node;
const colorMap = React.useRef<{ [key: string]: { color: string; hoverColor: string } }>({});
const FlameNodeComponent = <T,>({
node,
svgRef,
onClickNode,
customNodeStyle,
customTextStyle,
}: FlameNodeProps<T>) => {
const { x = 0, y = 0, width = 1, height = 1, name } = node;
const colorMap = React.useRef<{ [key: string]: FlameNodeColorType }>({});
const color = colorMap.current[name]?.color || getRandomColor();
const hoverColor = colorMap.current[name]?.hoverColor || getDarkenHexColor(color);
const ellipsizedText = React.useMemo(
() => getEllipsizedText(name, width, svgRef),
[name, width, svgRef],
);
const [isHover, setHover] = React.useState(false);

const [isHover, setHover] = React.useState(false);
if (!colorMap.current[name]) colorMap.current[name] = { color, hoverColor };
const contrastringTextColor = getContrastingTextColor(color);
const nodeStyle = {
...node.nodeStyle,
...customNodeStyle?.(node, colorMap.current[name]),
};
const textStyle = {
...node.textStyle,
...customTextStyle?.(node, contrastringTextColor),
};

return (
<>
Expand All @@ -52,8 +78,10 @@ export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNod
y={y}
width={width}
height={height}
fill={isHover ? hoverColor : color}
style={nodeStyle}
style={{
...nodeStyle,
fill: isHover ? hoverColor : color,
}}
/>
<text
x={x + width / 2}
Expand All @@ -62,24 +90,32 @@ export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNod
fontSize="0.75rem"
letterSpacing={-0.5}
textAnchor="middle"
fill={getContrastingTextColor(color)}
fill={contrastringTextColor}
style={textStyle}
>
{ellipsizedText}
</text>
</g>
{node.children &&
node.children.map((childNode, i) => (
<FlameNode
key={i}
node={childNode as FlameNodeType<T>}
svgRef={svgRef}
onClickNode={onClickNode}
/>
))}
node.children.map((childNode, i) => {
return (
<FlameNode<T>
key={i}
node={childNode}
svgRef={svgRef}
onClickNode={onClickNode}
customNodeStyle={customNodeStyle}
customTextStyle={customTextStyle}
/>
);
})}
</>
);
});
};

export const FlameNode = React.memo(FlameNodeComponent) as <T>(
props: FlameNodeProps<T>,
) => JSX.Element;

const getEllipsizedText = (text: string, maxWidth = 1, svgRef?: React.RefObject<SVGSVGElement>) => {
if (!svgRef?.current) return text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
import { TransactionInfo } from '@pinpoint-fe/constants';
import { RxMagnifyingGlass } from 'react-icons/rx';
import { HighLightCode } from '../../HighLightCode';
import { useAtomValue } from 'jotai';
import { transactionInfoCallTreeFocusId } from '@pinpoint-fe/atoms';

export interface CallTreeProps {
data: TransactionInfo.CallStackKeyValueMap[];
Expand Down Expand Up @@ -59,6 +61,15 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
});
},
});
const focusIdFromTimeline = useAtomValue(transactionInfoCallTreeFocusId);

React.useEffect(() => {
setFocusRowId(undefined);
}, [data]);

React.useEffect(() => {
setFocusRowId(focusIdFromTimeline);
}, [focusIdFromTimeline]);

useUpdateEffect(() => {
if (filter === 'hasException') {
Expand Down Expand Up @@ -116,7 +127,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {

return (
<div className="relative h-full">
<div className="absolute flex gap-1 rounded -top-11 right-4 h-7">
<div className="absolute flex gap-1 rounded -top-10 right-4 h-7">
<Select value={filter} onValueChange={(value) => setFilter(value)}>
<SelectTrigger className="w-24 h-full text-xs">
<SelectValue placeholder="Theme" />
Expand All @@ -131,7 +142,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
</Select>
<div className="border flex rounded pr-0.5 w-64">
<Input
className="h-full text-xs border-none shadow-none focus-visible:ring-0"
className="h-full text-xs border-none shadow-none focus-visible:ring-0 placeholder:text-xs"
placeholder="Filter call tree..."
value={input}
onChange={(e) => setInput(e.currentTarget.value)}
Expand Down Expand Up @@ -190,6 +201,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
<CallTreeTable
data={data}
metaData={metaData}
// scrollToIndex={(row) => row.findIndex((r) => r.original.id === callTreeFocusId)}
focusRowIndex={Number(focusRowId) - 1}
filteredRowIds={filteredListIds}
onDoubleClickCell={(cell) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,50 @@ import { IoMdClose } from 'react-icons/io';
import { TraceViewerData } from '@pinpoint-fe/constants';
import { Separator } from '@radix-ui/react-dropdown-menu';
import { Button } from '../../../components/ui';
import { FlameNodeType } from '../../FlameGraph/FlameNode';
import { useSetAtom } from 'jotai';
import { transactionInfoCallTreeFocusId, transactionInfoCurrentTabId } from '@pinpoint-fe/atoms';

export interface TimelineDetailProps {
start: number;
node: FlameNodeType<TraceViewerData.TraceEvent>;
data: TraceViewerData.TraceEvent;
onClose?: () => void;
}

export const TimelineDetail = ({ start, node, onClose }: TimelineDetailProps) => {
export const TimelineDetail = ({ start, data, onClose }: TimelineDetailProps) => {
const setCurrentTab = useSetAtom(transactionInfoCurrentTabId);
const setCallTreeFocusId = useSetAtom(transactionInfoCallTreeFocusId);

return (
<div className="w-2/5 border-l min-w-96">
<div className="flex items-center h-12 p-2 text-sm font-semibold border-b relativ bg-secondary/50">
Timeline detail
<Button className="ml-auto" variant={'ghost'} size={'icon'} onClick={() => onClose?.()}>
<IoMdClose className="w-5 h-5" />
</Button>
<div className="flex items-center ml-auto">
<Button
className="text-xs"
variant="link"
onClick={() => {
setCurrentTab('callTree');
setCallTreeFocusId(data.args.id);
}}
>
View in Call Tree
</Button>
<Button variant={'ghost'} size={'icon'} onClick={() => onClose?.()}>
<IoMdClose className="w-5 h-5" />
</Button>
</div>
</div>
<Separator />
<div className="overflow-auto h-[calc(100%-3.2rem)]">
<div className="p-2 pl-3 pb-4 text-xs [&>*:nth-child(2n-1)]:font-semibold grid grid-cols-[10rem_auto] [&>*:nth-child(2n)]:break-all gap-1">
<div>Name </div>
<div>{node.name}</div>
<div>Category </div>
<div>{node.detail.cat}</div>
<div>{data.cat}</div>
<div>Start time </div>
<div>{(node.detail.ts - start * 1000) / 1000}ms</div>
<div>{(data.ts - start * 1000) / 1000}ms</div>
<div>Duration </div>
<div>{node.duration}ms</div>
{node.detail?.args &&
Object.entries(node.detail.args).map(([key, value]) => {
<div>{data.dur}ms</div>
{data?.args &&
Object.entries(data.args).map(([key, value]) => {
return (
<React.Fragment key={key}>
<div>{key}</div>
Expand All @@ -41,7 +55,7 @@ export const TimelineDetail = ({ start, node, onClose }: TimelineDetailProps) =>
);
})}
<div>track_id</div>
<div>{node.detail.tid}</div>
<div>{data.tid}</div>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 62fba42

Please sign in to comment.