ui: refine LAT design

This commit is contained in:
nick-delirium 2025-04-04 10:31:03 +02:00
parent 58fdd7fbf5
commit 212a92f735
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
3 changed files with 146 additions and 64 deletions

View file

@ -9,19 +9,31 @@ import { useRegExListFilterMemo } from '../useListFilter';
import BottomBlock from '../BottomBlock';
import { NoContent, Icon } from 'UI';
import { InfoCircleOutlined } from '@ant-design/icons';
import { mockData } from './__mock';
import { Segmented } from 'antd';
import { Segmented, Select, Tag } from 'antd';
import { LongAnimationTask } from './type';
import Script from './Script'
import TaskTimeline from "./TaskTimeline";
import Script from './Script';
import TaskTimeline from './TaskTimeline';
import { Hourglass } from 'lucide-react';
interface Row extends LongAnimationTask {
time: number;
}
const TABS = {
all: 'all',
blocking: 'blocking',
};
const SORT_BY = {
timeAsc: 'timeAsc',
blocking: 'blockingDesc',
duration: 'durationDesc',
};
function LongTaskPanel() {
const { t } = useTranslation();
const [tab, setTab] = React.useState('all');
const [tab, setTab] = React.useState(TABS.all);
const [sortBy, setSortBy] = React.useState(SORT_BY.timeAsc);
const _list = React.useRef<VListHandle>(null);
const { player, store } = React.useContext(PlayerContext);
const [searchValue, setSearchValue] = React.useState('');
@ -29,7 +41,6 @@ function LongTaskPanel() {
const { currentTab, tabStates } = store.get();
const longTasks = tabStates[currentTab]?.longTaskList || [];
console.log('list', longTasks)
const filteredList = useRegExListFilterMemo(
longTasks,
(task: LongAnimationTask) => [
@ -38,7 +49,7 @@ function LongTaskPanel() {
task.scripts.map((script) => script.sourceURL).join(','),
],
searchValue,
)
);
const onFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
@ -50,17 +61,35 @@ function LongTaskPanel() {
};
const rows: Row[] = React.useMemo(() => {
const rowMap = filteredList.map((task) => ({
let rowMap = filteredList.map((task) => ({
...task,
time: task.time ?? task.startTime,
}))
}));
if (tab === 'blocking') {
return rowMap.filter((task) => task.blockingDuration > 0);
rowMap = rowMap.filter((task) => task.blockingDuration > 0);
}
switch (sortBy) {
case SORT_BY.blocking:
rowMap = rowMap.sort((a, b) => b.blockingDuration - a.blockingDuration);
break;
case SORT_BY.duration:
rowMap = rowMap.sort((a, b) => b.duration - a.duration);
break;
default:
rowMap = rowMap.sort((a, b) => a.time - b.time);
}
return rowMap;
}, [filteredList.length, tab]);
}, [filteredList.length, tab, sortBy]);
const blockingTasks = rows.filter((task) => task.blockingDuration > 0);
const blockingTasks = React.useMemo(() => {
let blockingAmount = 0;
for (const task of longTasks) {
if (task.blockingDuration > 0) {
blockingAmount++;
}
}
return blockingAmount;
}, [longTasks.length]);
return (
<BottomBlock style={{ height: '100%' }}>
@ -80,13 +109,26 @@ function LongTaskPanel() {
{
label: (
<div>
{t('Blocking')} ({blockingTasks.length})
{t('Blocking')} ({blockingTasks})
</div>
),
value: 'blocking',
},
]}
/>
<Select
size="small"
className="rounded-lg"
value={sortBy}
onChange={setSortBy}
popupMatchSelectWidth={150}
dropdownStyle={{ minWidth: '150px' }}
options={[
{ label: t('Default Order'), value: 'timeAsc' },
{ label: t('Blocking Duration'), value: 'blockingDesc' },
{ label: t('Task Duration'), value: 'durationDesc' },
]}
/>
<Input.Search
className="rounded-lg"
placeholder={t('Filter by name or source URL')}
@ -131,28 +173,24 @@ function LongTaskRow({
return (
<div
className={
'relative border-b border-neutral-950/5 group hover:bg-active-blue cursor-pointer flex items-start gap-2 py-1 px-4 pe-8'
'relative border-b border-neutral-950/5 group hover:bg-active-blue py-1 px-4 pe-8'
}
onClick={() => setExpanded(!expanded)}
>
<Icon
name={expanded ? 'caret-down-fill' : 'caret-right-fill'}
className={'mt-1'}
/>
<div className="flex flex-col">
<div className="flex flex-col">
<TaskTitle entry={task} />
</div>
<div className="flex flex-col w-full">
<TaskTitle expanded={expanded} entry={task} toggleExpand={() => setExpanded(!expanded)} />
{expanded ? (
<>
<TaskTimeline task={task} />
<div className={'flex items-center gap-1'}>
<div className={'text-gray-dark'}>First UI event timestamp:</div>
<div>{task.firstUIEventTimestamp.toFixed(2)} ms</div>
<div className={'flex items-center gap-1 mb-2'}>
<div className={'text-neutral-900 font-medium'}>
First UI event timestamp:
</div>
<div className="text-neutral-600 font-mono block">
{Math.round(task.firstUIEventTimestamp)} ms
</div>
</div>
<div className={'text-gray-dark'}>Scripts:</div>
<div className="flex flex-col gap-1 pl-2">
<div className={'text-neutral-900 font-medium'}>Scripts:</div>
<div className="flex flex-col gap-1">
{task.scripts.map((script, index) => (
<Script script={script} key={index} />
))}
@ -167,28 +205,61 @@ function LongTaskRow({
function TaskTitle({
entry,
toggleExpand,
expanded,
}: {
entry: {
name: string;
duration: number;
blockingDuration?: number;
scripts: LongAnimationTask['scripts'];
};
expanded: boolean;
toggleExpand: () => void;
}) {
const isBlocking =
entry.blockingDuration !== undefined && entry.blockingDuration > 0;
const scriptTitles = entry.scripts.map((script) =>
script.invokerType ? script.invokerType : script.name,
);
const { title, plusMore } = getFirstTwoScripts(scriptTitles);
return (
<div className={'flex items-center gap-1'}>
<span>Long Animation Frame</span>
<span className={'text-disabled-text'}>
({entry.duration.toFixed(2)} ms)
<div className={'flex items-center gap-1 text-sm cursor-pointer'} onClick={toggleExpand}>
<Icon
name={expanded ? 'caret-down-fill' : 'caret-right-fill'}
/>
<span className="font-mono font-bold">{title}</span>
<Tag color="default" bordered={false}>
{plusMore}
</Tag>
<span className={'text-neutral-600 font-mono'}>
{Math.round(entry.duration)} ms
</span>
{isBlocking ? (
<span className={'text-red'}>
{entry.blockingDuration!.toFixed(2)} ms blocking
</span>
<Tag
bordered={false}
color="red"
className="font-mono rounded-lg text-xs flex gap-1 items-center text-red-600"
>
<Hourglass size={11} /> {Math.round(entry.blockingDuration!)} ms
blocking
</Tag>
) : null}
</div>
);
}
function getFirstTwoScripts(titles: string[]) {
if (titles.length === 0) {
return { title: 'Long Animation Task', plusMore: null };
}
const additional = titles.length - 2;
const additionalStr = additional > 0 ? `+ ${additional} more` : null;
return {
title: `${titles[0]}${titles[1] ? `, ${titles[1]}` : ''}`,
plusMore: additionalStr,
};
}
export default observer(LongTaskPanel);

View file

@ -1,5 +1,7 @@
import React from 'react'
import { LongAnimationTask } from './type'
import React from 'react';
import { LongAnimationTask } from './type';
import { Tag } from 'antd';
import { Code } from 'lucide-react';
function getAddress(script: LongAnimationTask['scripts'][number]) {
return `${script.sourceURL}${script.sourceFunctionName ? ':' + script.sourceFunctionName : ''}${script.sourceCharPosition && script.sourceCharPosition >= 0 ? ':' + script.sourceCharPosition : ''}`;
@ -7,13 +9,13 @@ function getAddress(script: LongAnimationTask['scripts'][number]) {
function ScriptTitle({
script,
}: {
script: LongAnimationTask['scripts'][number]
script: LongAnimationTask['scripts'][number];
}) {
return script.invokerType ? (
<span>{script.invokerType}</span>
) : (
<span>{script.name}</span>
)
<span>{script.name}</span>
);
}
function ScriptInfo({
@ -28,8 +30,14 @@ function ScriptInfo({
<InfoEntry title={'invoker:'} value={script.invoker} />
) : null}
<InfoEntry title={'address:'} value={getAddress(script)} />
<InfoEntry title={'script execution:'} value={`${script.duration} ms`} />
<InfoEntry title={'pause duration:'} value={`${script.pauseDuration} ms`} />
<InfoEntry
title={'script execution:'}
value={`${Math.round(script.duration)} ms`}
/>
<InfoEntry
title={'pause duration:'}
value={`${Math.round(script.pauseDuration)} ms`}
/>
</div>
);
}
@ -42,20 +50,23 @@ function InfoEntry({
value: string | number;
}) {
return (
<div className={'flex items-center gap-1'}>
<div className={'flex items-center gap-1 text-sm'}>
<div className={'text-disabled-text'}>{title}</div>
<div>{value}</div>
<div className='font-mono text-neutral-600'>{value}</div>
</div>
);
}
function Script({ script }: { script: LongAnimationTask['scripts'][number] }) {
return (
<div className="flex flex-col">
<ScriptTitle script={script} />
<div className="flex flex-col mb-4">
<Tag className='w-fit font-mono text-sm font-bold flex gap-1 items-center rounded-lg'>
<Code size={12} />
<ScriptTitle script={script} />
</Tag>
<ScriptInfo script={script} />
</div>
)
);
}
export default Script
export default Script;

View file

@ -3,6 +3,12 @@ import { Tooltip } from 'antd'
import { LongAnimationTask } from "./type";
import cn from "classnames";
const getSeverityClass = (duration: number) => {
if (duration > 200) return 'bg-[#CC0000]';
if (duration > 100) return 'bg-[#EFB100]';
return 'bg-[#66a299]';
};
function TaskTimeline({ task }: { task: LongAnimationTask }) {
const totalDuration = task.duration;
const scriptDuration = task.scripts.reduce((sum, script) => sum + script.duration, 0);
@ -16,41 +22,35 @@ function TaskTimeline({ task }: { task: LongAnimationTask }) {
const layoutWidth = (layoutDuration / totalDuration) * 100;
const idleWidth = (idleDuration / totalDuration) * 100;
const getSeverityClass = (duration) => {
if (duration > 200) return 'bg-[#e7000b]';
if (duration > 100) return 'bg-[#efb100]';
return 'bg-[#51a2ff]';
};
return (
<div className="w-full mb-2">
<div className="w-full mb-2 mt-1">
<div className="text-gray-dark mb-1">Timeline:</div>
<div className="flex h-4 w-full rounded-sm overflow-hidden">
<div className="flex h-2 w-full rounded overflow-hidden">
{scriptDuration > 0 && (
<TimelineSegment
classes={`${getSeverityClass(scriptDuration)} h-full`}
name={`Script: ${scriptDuration.toFixed(2)}ms`}
name={`Script: ${Math.round(scriptDuration)}ms`}
width={scriptWidth}
/>
)}
{idleDuration > 0 && (
<TimelineSegment
classes="bg-gray-light h-full"
classes="bg-gray-light h-full bg-[repeating-linear-gradient(45deg,#ccc_0px,#ccc_5px,#f2f2f2_5px,#f2f2f2_10px)]"
width={idleWidth}
name={`Idle: ${idleDuration.toFixed(2)}ms`}
name={`Idle: ${Math.round(idleDuration)}ms`}
/>
)}
{layoutDuration > 0 && (
<TimelineSegment
classes="bg-[#8200db] h-full"
width={layoutWidth}
name={`Layout & Style: ${layoutDuration.toFixed(2)}ms`}
name={`Layout & Style: ${Math.round(layoutDuration)}ms`}
/>
)}
</div>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>start: {task.startTime.toFixed(2)}ms</span>
<span>finish: {(task.startTime + task.duration).toFixed(2)}ms</span>
<span>start: {Math.round(task.startTime)}ms</span>
<span>finish: {Math.round(task.startTime + task.duration)}ms</span>
</div>
</div>
);
@ -76,4 +76,4 @@ function TimelineSegment({
);
}
export default TaskTimeline;
export default TaskTimeline;