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..94d62e1d1 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -339,6 +339,23 @@ class PageEvent(Message): self.web_vitals = web_vitals +class StringDictGlobal(Message): + __id__ = 34 + + def __init__(self, key, value): + self.key = key + self.value = value + + +class SetNodeAttributeDictGlobal(Message): + __id__ = 35 + + def __init__(self, id, name, value): + self.id = id + self.name = name + self.value = value + + class CSSInsertRule(Message): __id__ = 37 @@ -789,6 +806,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..468d19059 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -511,6 +511,30 @@ cdef class PageEvent(PyMessage): self.web_vitals = web_vitals +cdef class StringDictGlobal(PyMessage): + cdef public int __id__ + cdef public unsigned long key + cdef public str value + + def __init__(self, unsigned long key, str value): + self.__id__ = 34 + self.key = key + self.value = value + + +cdef class SetNodeAttributeDictGlobal(PyMessage): + cdef public int __id__ + cdef public unsigned long id + cdef public unsigned long name + cdef public unsigned long value + + def __init__(self, unsigned long id, unsigned long name, unsigned long value): + self.__id__ = 35 + self.id = id + self.name = name + self.value = value + + cdef class CSSInsertRule(PyMessage): cdef public int __id__ cdef public unsigned long id @@ -1176,6 +1200,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..9c4018587 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -360,6 +360,19 @@ class MessageCodec(Codec): web_vitals=self.read_string(reader) ) + if message_id == 34: + return StringDictGlobal( + key=self.read_uint(reader), + value=self.read_string(reader) + ) + + if message_id == 35: + return SetNodeAttributeDictGlobal( + id=self.read_uint(reader), + name=self.read_uint(reader), + value=self.read_uint(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -716,6 +729,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..f1426fe17 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -458,6 +458,19 @@ cdef class MessageCodec: web_vitals=self.read_string(reader) ) + if message_id == 34: + return StringDictGlobal( + key=self.read_uint(reader), + value=self.read_string(reader) + ) + + if message_id == 35: + return SetNodeAttributeDictGlobal( + id=self.read_uint(reader), + name=self.read_uint(reader), + value=self.read_uint(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -814,6 +827,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 && ( + {props.time ? ( +
+ {shortDurationFromMs(props.time)} +
+ ) : null} - {props.time ? ( -
- {shortDurationFromMs(props.time)} -
- ) : null}
); diff --git a/frontend/app/components/shared/DevTools/LongTaskPanel/LongTaskPanel.tsx b/frontend/app/components/shared/DevTools/LongTaskPanel/LongTaskPanel.tsx new file mode 100644 index 000000000..c021afdef --- /dev/null +++ b/frontend/app/components/shared/DevTools/LongTaskPanel/LongTaskPanel.tsx @@ -0,0 +1,265 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; +import { Input } from 'antd'; +import { VList, VListHandle } from 'virtua'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import JumpButton from '../JumpButton'; +import { useRegExListFilterMemo } from '../useListFilter'; +import BottomBlock from '../BottomBlock'; +import { NoContent, Icon } from 'UI'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Segmented, Select, Tag } from 'antd'; +import { LongAnimationTask } from './type'; +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(TABS.all); + const [sortBy, setSortBy] = React.useState(SORT_BY.timeAsc); + const _list = React.useRef(null); + const { player, store } = React.useContext(PlayerContext); + const [searchValue, setSearchValue] = React.useState(''); + + const { currentTab, tabStates } = store.get(); + const longTasks = tabStates[currentTab]?.longTaskList || []; + + 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(() => { + let rowMap = filteredList.map((task) => ({ + ...task, + time: task.time ?? task.startTime, + })); + if (tab === 'blocking') { + 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, sortBy]); + + const blockingTasks = React.useMemo(() => { + let blockingAmount = 0; + for (const task of longTasks) { + if (task.blockingDuration > 0) { + blockingAmount++; + } + } + return blockingAmount; + }, [longTasks.length]); + + return ( + + +
+ + {t('Long Tasks')} + +
+
+ + {t('Blocking')} ({blockingTasks}) +
+ ), + value: 'blocking', + }, + ]} + /> +