From ee20fdb57f68167903758807852e06531808df4d Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Thu, 3 Apr 2025 14:55:58 +0200 Subject: [PATCH] ui, tracker, backend: support long animation metrics --- backend/pkg/messages/messages.go | 32 + backend/pkg/messages/read-message.go | 26 + ee/connectors/msgcodec/messages.py | 12 + ee/connectors/msgcodec/messages.pyx | 19 + ee/connectors/msgcodec/msgcodec.py | 10 + ee/connectors/msgcodec/msgcodec.pyx | 10 + .../Player/ReplayPlayer/PlayerInst.tsx | 47 +- .../Session_/Player/Controls/Controls.tsx | 19 +- .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 1 - .../DevTools/LongTaskPanel/LongTaskPanel.tsx | 193 ++++++ .../shared/DevTools/LongTaskPanel/Script.tsx | 61 ++ .../DevTools/LongTaskPanel/TaskTimeline.tsx | 79 +++ .../shared/DevTools/LongTaskPanel/__mock.ts | 268 ++++++++ .../shared/DevTools/LongTaskPanel/type.ts | 21 + frontend/app/mstore/uiPlayerStore.ts | 3 + frontend/app/player/web/Lists.ts | 3 + frontend/app/player/web/TabManager.ts | 3 + .../web/messages/RawMessageReader.gen.ts | 40 +- .../app/player/web/messages/filters.gen.ts | 7 +- .../app/player/web/messages/message.gen.ts | 156 ++--- frontend/app/player/web/messages/raw.gen.ts | 625 +++++++++--------- .../player/web/messages/tracker-legacy.gen.ts | 5 +- .../app/player/web/messages/tracker.gen.ts | 481 ++++++++++---- frontend/app/player/web/types/longTask.ts | 41 ++ mobs/messages.rb | 11 +- tracker/tracker/src/common/messages.gen.ts | 13 +- tracker/tracker/src/main/app/messages.gen.ts | 19 + tracker/tracker/src/main/index.ts | 11 +- .../src/main/modules/longAnimationTask.ts | 61 ++ .../src/webworker/MessageEncoder.gen.ts | 4 + 30 files changed, 1720 insertions(+), 561 deletions(-) create mode 100644 frontend/app/components/shared/DevTools/LongTaskPanel/LongTaskPanel.tsx create mode 100644 frontend/app/components/shared/DevTools/LongTaskPanel/Script.tsx create mode 100644 frontend/app/components/shared/DevTools/LongTaskPanel/TaskTimeline.tsx create mode 100644 frontend/app/components/shared/DevTools/LongTaskPanel/__mock.ts create mode 100644 frontend/app/components/shared/DevTools/LongTaskPanel/type.ts create mode 100644 frontend/app/player/web/types/longTask.ts create mode 100644 tracker/tracker/src/main/modules/longAnimationTask.ts diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index d4f184d48..bccd0f210 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -84,6 +84,7 @@ const ( MsgPartitionedMessage = 82 MsgNetworkRequest = 83 MsgWSChannel = 84 + MsgLongAnimationTask = 89 MsgInputChange = 112 MsgSelectionChange = 113 MsgMouseThrashing = 114 @@ -2294,6 +2295,37 @@ func (msg *WSChannel) TypeID() int { return 84 } +type LongAnimationTask struct { + message + Name string + Duration int64 + BlockingDuration int64 + FirstUIEventTimestamp int64 + StartTime int64 + Scripts string +} + +func (msg *LongAnimationTask) Encode() []byte { + buf := make([]byte, 61+len(msg.Name)+len(msg.Scripts)) + buf[0] = 89 + p := 1 + p = WriteString(msg.Name, buf, p) + p = WriteInt(msg.Duration, buf, p) + p = WriteInt(msg.BlockingDuration, buf, p) + p = WriteInt(msg.FirstUIEventTimestamp, buf, p) + p = WriteInt(msg.StartTime, buf, p) + p = WriteString(msg.Scripts, buf, p) + return buf[:p] +} + +func (msg *LongAnimationTask) Decode() Message { + return msg +} + +func (msg *LongAnimationTask) TypeID() int { + return 89 +} + type InputChange struct { message ID uint64 diff --git a/backend/pkg/messages/read-message.go b/backend/pkg/messages/read-message.go index f0051a042..7f05c1500 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -1419,6 +1419,30 @@ func DecodeWSChannel(reader BytesReader) (Message, error) { return msg, err } +func DecodeLongAnimationTask(reader BytesReader) (Message, error) { + var err error = nil + msg := &LongAnimationTask{} + if msg.Name, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Duration, err = reader.ReadInt(); err != nil { + return nil, err + } + if msg.BlockingDuration, err = reader.ReadInt(); err != nil { + return nil, err + } + if msg.FirstUIEventTimestamp, err = reader.ReadInt(); err != nil { + return nil, err + } + if msg.StartTime, err = reader.ReadInt(); err != nil { + return nil, err + } + if msg.Scripts, err = reader.ReadString(); err != nil { + return nil, err + } + return msg, err +} + func DecodeInputChange(reader BytesReader) (Message, error) { var err error = nil msg := &InputChange{} @@ -2248,6 +2272,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { return DecodeNetworkRequest(reader) case 84: return DecodeWSChannel(reader) + case 89: + return DecodeLongAnimationTask(reader) case 112: return DecodeInputChange(reader) case 113: diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py index 53450583d..7792b06df 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -789,6 +789,18 @@ class WSChannel(Message): self.message_type = message_type +class LongAnimationTask(Message): + __id__ = 89 + + def __init__(self, name, duration, blocking_duration, first_ui_event_timestamp, start_time, scripts): + self.name = name + self.duration = duration + self.blocking_duration = blocking_duration + self.first_ui_event_timestamp = first_ui_event_timestamp + self.start_time = start_time + self.scripts = scripts + + class InputChange(Message): __id__ = 112 diff --git a/ee/connectors/msgcodec/messages.pyx b/ee/connectors/msgcodec/messages.pyx index b07658d51..f88163a8f 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -1176,6 +1176,25 @@ cdef class WSChannel(PyMessage): self.message_type = message_type +cdef class LongAnimationTask(PyMessage): + cdef public int __id__ + cdef public str name + cdef public long duration + cdef public long blocking_duration + cdef public long first_ui_event_timestamp + cdef public long start_time + cdef public str scripts + + def __init__(self, str name, long duration, long blocking_duration, long first_ui_event_timestamp, long start_time, str scripts): + self.__id__ = 89 + self.name = name + self.duration = duration + self.blocking_duration = blocking_duration + self.first_ui_event_timestamp = first_ui_event_timestamp + self.start_time = start_time + self.scripts = scripts + + cdef class InputChange(PyMessage): cdef public int __id__ cdef public unsigned long id diff --git a/ee/connectors/msgcodec/msgcodec.py b/ee/connectors/msgcodec/msgcodec.py index 6d771b559..7a8f0b25f 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -716,6 +716,16 @@ class MessageCodec(Codec): message_type=self.read_string(reader) ) + if message_id == 89: + return LongAnimationTask( + name=self.read_string(reader), + duration=self.read_int(reader), + blocking_duration=self.read_int(reader), + first_ui_event_timestamp=self.read_int(reader), + start_time=self.read_int(reader), + scripts=self.read_string(reader) + ) + if message_id == 112: return InputChange( id=self.read_uint(reader), diff --git a/ee/connectors/msgcodec/msgcodec.pyx b/ee/connectors/msgcodec/msgcodec.pyx index 9055ca0d7..07ae0c9c8 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -814,6 +814,16 @@ cdef class MessageCodec: message_type=self.read_string(reader) ) + if message_id == 89: + return LongAnimationTask( + name=self.read_string(reader), + duration=self.read_int(reader), + blocking_duration=self.read_int(reader), + first_ui_event_timestamp=self.read_int(reader), + start_time=self.read_int(reader), + scripts=self.read_string(reader) + ) + if message_id == 112: return InputChange( id=self.read_uint(reader), diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx index 4c0f3ab59..5112752a7 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx @@ -14,8 +14,8 @@ import { EXCEPTIONS, INSPECTOR, OVERVIEW, - BACKENDLOGS, -} from 'App/mstore/uiPlayerStore'; + BACKENDLOGS, LONG_TASK +} from "App/mstore/uiPlayerStore"; import { WebNetworkPanel } from 'Shared/DevTools/NetworkPanel'; import Storage from 'Components/Session_/Storage'; import { ConnectedPerformance } from 'Components/Session_/Performance'; @@ -31,6 +31,7 @@ import { PlayerContext } from 'App/components/Session/playerContext'; import { debounce } from 'App/utils'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; +import LongTaskPanel from "../../../shared/DevTools/LongTaskPanel/LongTaskPanel"; import BackendLogsPanel from '../SharedComponents/BackendLogs/BackendLogsPanel'; interface IProps { @@ -158,20 +159,7 @@ function Player(props: IProps) { onMouseDown={handleResize} className="w-full h-2 cursor-ns-resize absolute top-0 left-0 z-20" /> - {bottomBlock === OVERVIEW && } - {bottomBlock === CONSOLE && } - {bottomBlock === NETWORK && ( - - )} - {bottomBlock === STACKEVENTS && } - {bottomBlock === STORAGE && } - {bottomBlock === PROFILER && ( - - )} - {bottomBlock === PERFORMANCE && } - {bottomBlock === GRAPHQL && } - {bottomBlock === EXCEPTIONS && } - {bottomBlock === BACKENDLOGS && } + )} {!fullView ? ( @@ -189,4 +177,31 @@ function Player(props: IProps) { ); } +function BottomBlock({ panelHeight, block }: { panelHeight: number; block: number }) { + switch (block) { + case CONSOLE: + return ; + case NETWORK: + return ; + case STACKEVENTS: + return ; + case STORAGE: + return ; + case PROFILER: + return ; + case PERFORMANCE: + return ; + case GRAPHQL: + return ; + case EXCEPTIONS: + return ; + case BACKENDLOGS: + return ; + case LONG_TASK: + return ; + default: + return null; + } +} + export default observer(Player); diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index 1d53c626a..691ae6eb1 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -29,11 +29,12 @@ import { STACKEVENTS, STORAGE, BACKENDLOGS, -} from 'App/mstore/uiPlayerStore'; + LONG_TASK +} from "App/mstore/uiPlayerStore"; import { Icon } from 'UI'; import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton'; import { CodeOutlined, DashboardOutlined, ClusterOutlined } from '@ant-design/icons'; -import { ArrowDownUp, ListCollapse, Merge, Waypoints } from 'lucide-react' +import { ArrowDownUp, ListCollapse, Merge, Waypoints, Timer } from 'lucide-react' import ControlButton from './ControlButton'; import Timeline from './Timeline'; @@ -293,7 +294,11 @@ const DevtoolsButtons = observer( graphql: { icon: , label: 'Graphql', - } + }, + longTask: { + icon: , + label: t('Long Tasks'), + }, } // @ts-ignore const getLabel = (block: string) => labels[block][showIcons ? 'icon' : 'label'] @@ -359,6 +364,14 @@ const DevtoolsButtons = observer( label={getLabel('performance')} /> + toggleBottomTools(LONG_TASK)} + active={bottomBlock === LONG_TASK && !inspectorMode} + label={getLabel('longTask')} + /> + {showGraphql && ( (null); + const { player, store } = React.useContext(PlayerContext); + const [searchValue, setSearchValue] = React.useState(''); + + const { currentTab, tabStates } = store.get(); + const longTasks: LongAnimationTask[] = mockData; //tabStates[currentTab]?.longTasksList || []; + + const filteredList = useRegExListFilterMemo( + longTasks, + (task: LongAnimationTask) => [ + task.name, + task.scripts.map((script) => script.name).join(','), + task.scripts.map((script) => script.sourceURL).join(','), + ], + searchValue, + ) + + const onFilterChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchValue(value); + }; + + const onRowClick = (time: number) => { + player.jump(time); + }; + + const rows: Row[] = React.useMemo(() => { + const rowMap = filteredList.map((task) => ({ + ...task, + time: task.time ?? task.startTime, + })) + if (tab === 'blocking') { + return rowMap.filter((task) => task.blockingDuration > 0); + } + return rowMap; + }, [filteredList.length, tab]); + + const blockingTasks = rows.filter((task) => task.blockingDuration > 0); + + return ( + + +
+ + {t('Long Tasks')} + +
+
+ + {t('Blocking')} ({blockingTasks.length}) +
+ ), + value: 'blocking', + }, + ]} + /> + + +
+ + + + {t('No Data')} + + } + size="small" + show={filteredList.length === 0} + > + + {rows.map((task) => ( + + ))} + + + +
+ ); +} + +function LongTaskRow({ + task, + onJump, +}: { + task: Row; + onJump: (time: number) => void; +}) { + const [expanded, setExpanded] = React.useState(false); + + return ( +
setExpanded(!expanded)} + > + + +
+
+ +
+ {expanded ? ( + <> + +
+
First UI event timestamp:
+
{task.firstUIEventTimestamp.toFixed(2)} ms
+
+
Scripts:
+
+ {task.scripts.map((script, index) => ( +