diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index a554812fe..763f2ad72 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -75,6 +75,7 @@ const ( MsgPartitionedMessage = 82 MsgNetworkRequest = 83 MsgWSChannel = 84 + MsgLongAnimationTask = 89 MsgInputChange = 112 MsgSelectionChange = 113 MsgMouseThrashing = 114 @@ -2042,6 +2043,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 380e0b6b2..575beaf51 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -1257,6 +1257,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{} @@ -2068,6 +2092,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 dfd38a369..92fbafb4b 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -716,6 +716,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 d84f4a1c5..2f849f1ef 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -1065,6 +1065,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 7bc5a8174..1bc42380d 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -657,6 +657,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 e53fd9224..fcb9e372a 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -755,6 +755,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) => ( +