diff --git a/webapp/channels/jest.config.js b/webapp/channels/jest.config.js index c7972e9f3d..a6bcd7ab00 100644 --- a/webapp/channels/jest.config.js +++ b/webapp/channels/jest.config.js @@ -19,6 +19,9 @@ const config = { 'utils/src/**/*.{js,jsx,ts,tsx}', ], coverageReporters: ['lcov', 'text-summary'], + fakeTimers: { + doNotFake: ['performance'], + }, moduleNameMapper: { '^@mattermost/(components)$': '/../platform/$1/src', '^@mattermost/(client)$': '/../platform/$1/src', diff --git a/webapp/channels/src/actions/views/rhs.ts b/webapp/channels/src/actions/views/rhs.ts index a127c8c99a..ebc34c5044 100644 --- a/webapp/channels/src/actions/views/rhs.ts +++ b/webapp/channels/src/actions/views/rhs.ts @@ -31,23 +31,17 @@ import {getSearchTerms, getRhsState, getPluggableId, getFilesSearchExtFilter, ge import {SidebarSize} from 'components/resizable_sidebar/constants'; import {ActionTypes, RHSStates, Constants} from 'utils/constants'; +import {Mark, Measure, measureAndReport} from 'utils/performance_telemetry'; import {getBrowserUtcOffset, getUtcOffsetForTimeZone} from 'utils/timezone'; import type {GlobalState} from 'types/store'; import type {RhsState} from 'types/store/rhs'; -function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState): ActionFuncAsync { - return async (dispatch, getState) => { - const postRootId = post.root_id || post.id; +function selectPostWithPreviousState(post: Post, previousRhsState?: RhsState): ActionFunc { + return (dispatch, getState) => { const state = getState(); - dispatch({ - type: ActionTypes.SELECT_POST, - postId: postRootId, - channelId: post.channel_id, - previousRhsState: previousRhsState || getRhsState(state), - timestamp: Date.now(), - }); + dispatch(selectPost(post, previousRhsState || getRhsState(state))); return {data: true}; }; @@ -118,7 +112,7 @@ export function goBack(): ActionFuncAsync { } export function selectPostFromRightHandSideSearch(post: Post) { - return selectPostFromRightHandSideSearchWithPreviousState(post); + return selectPostWithPreviousState(post); } export function selectPostFromRightHandSideSearchByPostId(postId: string): ActionFuncAsync { @@ -520,11 +514,14 @@ export function toggleRhsExpanded() { }; } -export function selectPost(post: Post) { +export function selectPost(post: Post, previousRhsState?: RhsState) { + performance.mark(Mark.PostSelected); + return { type: ActionTypes.SELECT_POST, postId: post.root_id || post.id, channelId: post.channel_id, + previousRhsState, timestamp: Date.now(), }; } @@ -614,7 +611,7 @@ export function openAtPrevious(previous: any): ThunkActionFunc { + measureAndReport(Measure.RhsLoad, Mark.PostSelected, undefined, true); + + performance.clearMarks(Mark.PostSelected); + }; +} diff --git a/webapp/channels/src/components/post_view/post_list/post_list.tsx b/webapp/channels/src/components/post_view/post_list/post_list.tsx index 424f91278e..240acfb79d 100644 --- a/webapp/channels/src/components/post_view/post_list/post_list.tsx +++ b/webapp/channels/src/components/post_view/post_list/post_list.tsx @@ -13,7 +13,7 @@ import LoadingScreen from 'components/loading_screen'; import VirtPostList from 'components/post_view/post_list_virtualized/post_list_virtualized'; import {PostRequestTypes} from 'utils/constants'; -import {measureAndReport} from 'utils/performance_telemetry'; +import {Mark, Measure, measureAndReport} from 'utils/performance_telemetry'; import {getOldestPostId, getLatestPostId} from 'utils/post_utils'; const MAX_NUMBER_OF_AUTO_RETRIES = 3; @@ -22,18 +22,18 @@ export const MAX_EXTRA_PAGES_LOADED = 10; // Measures the time between channel or team switch started and the post list component rendering posts. // Set "fresh" to true when the posts have not been loaded before. function markAndMeasureChannelSwitchEnd(fresh = false) { - mark('PostList#component'); + mark(Mark.PostListLoaded); // Send new performance metrics to server - const channelSwitch = measureAndReport('channel_switch', 'SidebarChannelLink#click', 'PostList#component', true); - const teamSwitch = measureAndReport('team_switch', 'TeamLink#click', 'PostList#component', true); + const channelSwitch = measureAndReport(Measure.ChannelSwitch, Mark.ChannelLinkClicked, Mark.PostListLoaded, true); + const teamSwitch = measureAndReport(Measure.TeamSwitch, Mark.TeamLinkClicked, Mark.PostListLoaded, true); // Send old performance metrics to Rudder if (shouldTrackPerformance()) { if (channelSwitch) { - const requestCount1 = countRequestsBetween('SidebarChannelLink#click', 'PostList#component'); + const requestCount1 = countRequestsBetween(Mark.ChannelLinkClicked, Mark.PostListLoaded); - trackEvent('performance', 'channel_switch', { + trackEvent('performance', Measure.ChannelSwitch, { duration: Math.round(channelSwitch.duration), fresh, requestCount: requestCount1, @@ -41,9 +41,9 @@ function markAndMeasureChannelSwitchEnd(fresh = false) { } if (teamSwitch) { - const requestCount2 = countRequestsBetween('TeamLink#click', 'PostList#component'); + const requestCount2 = countRequestsBetween(Mark.TeamLinkClicked, Mark.PostListLoaded); - trackEvent('performance', 'team_switch', { + trackEvent('performance', Measure.TeamSwitch, { duration: Math.round(teamSwitch.duration), fresh, requestCount: requestCount2, @@ -53,9 +53,9 @@ function markAndMeasureChannelSwitchEnd(fresh = false) { // Clear all the metrics so that we can differentiate between a channel and team switch next time this is called clearMarks([ - 'SidebarChannelLink#click', - 'TeamLink#click', - 'PostList#component', + Mark.ChannelLinkClicked, + Mark.TeamLinkClicked, + Mark.PostListLoaded, ]); } diff --git a/webapp/channels/src/components/rhs_thread/index.ts b/webapp/channels/src/components/rhs_thread/index.ts index 5b311fa765..9d092d4566 100644 --- a/webapp/channels/src/components/rhs_thread/index.ts +++ b/webapp/channels/src/components/rhs_thread/index.ts @@ -3,9 +3,6 @@ import {connect} from 'react-redux'; -import type {Post} from '@mattermost/types/posts'; - -import {makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; import {getSelectedChannel, getSelectedPost} from 'selectors/rhs'; @@ -14,24 +11,15 @@ import type {GlobalState} from 'types/store'; import RhsThread from './rhs_thread'; -function makeMapStateToProps() { - const getPostsForThread = makeGetPostsForThread(); +function mapStateToProps(state: GlobalState) { + const selected = getSelectedPost(state); + const channel = getSelectedChannel(state); + const currentTeam = getCurrentTeam(state); - return function mapStateToProps(state: GlobalState) { - const selected = getSelectedPost(state); - const channel = getSelectedChannel(state); - const currentTeam = getCurrentTeam(state); - let posts: Post[] = []; - if (selected) { - posts = getPostsForThread(state, selected.id); - } - - return { - selected, - channel, - posts, - currentTeam, - }; + return { + selected, + channel, + currentTeam, }; } -export default connect(makeMapStateToProps)(RhsThread); +export default connect(mapStateToProps)(RhsThread); diff --git a/webapp/channels/src/components/rhs_thread/rhs_thread.tsx b/webapp/channels/src/components/rhs_thread/rhs_thread.tsx index 4dad101b21..d022bb6cd2 100644 --- a/webapp/channels/src/components/rhs_thread/rhs_thread.tsx +++ b/webapp/channels/src/components/rhs_thread/rhs_thread.tsx @@ -17,7 +17,6 @@ import type {FakePost, RhsState} from 'types/store/rhs'; type Props = { currentTeam?: Team; - posts: Post[]; channel?: Channel; selected: Post | FakePost; previousRhsState?: RhsState; @@ -26,7 +25,6 @@ type Props = { const RhsThread = ({ currentTeam, channel, - posts, selected, previousRhsState, }: Props) => { @@ -39,7 +37,7 @@ const RhsThread = ({ } }, [currentTeam, channel]); - if (posts == null || selected == null || !channel) { + if (selected == null || !channel) { return (
); diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx index 42a3efea85..e5e71adc50 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx @@ -18,6 +18,7 @@ import Pluggable from 'plugins/pluggable'; import Constants, {RHSStates} from 'utils/constants'; import {wrapEmojis} from 'utils/emoji_utils'; import {cmdOrCtrlPressed} from 'utils/keyboard'; +import {Mark} from 'utils/performance_telemetry'; import {localizeMessage} from 'utils/utils'; import type {RhsState} from 'types/store/rhs'; @@ -135,7 +136,7 @@ export default class SidebarChannelLink extends React.PureComponent this.gmItemRef.current?.removeAttribute?.('aria-describedby'); handleChannelClick = (event: React.MouseEvent): void => { - mark('SidebarChannelLink#click'); + mark(Mark.ChannelLinkClicked); this.handleSelectChannel(event); if (this.props.rhsOpen && this.props.rhsState === RHSStates.EDIT_HISTORY) { diff --git a/webapp/channels/src/components/team_sidebar/components/team_button.tsx b/webapp/channels/src/components/team_sidebar/components/team_button.tsx index 704f64f9ac..e204a3168b 100644 --- a/webapp/channels/src/components/team_sidebar/components/team_button.tsx +++ b/webapp/channels/src/components/team_sidebar/components/team_button.tsx @@ -13,6 +13,8 @@ import TeamIcon from 'components/widgets/team_icon/team_icon'; import WithTooltip from 'components/with_tooltip'; import {ShortcutKeys} from 'components/with_tooltip/shortcut'; +import {Mark} from 'utils/performance_telemetry'; + const messages = defineMessages({ nameUndefined: { id: 'team.button.name_undefined', @@ -59,7 +61,7 @@ export default function TeamButton({ const {formatMessage} = useIntl(); const handleSwitch = useCallback((e: React.MouseEvent) => { - mark('TeamLink#click'); + mark(Mark.TeamLinkClicked); e.preventDefault(); switchTeam(url); diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts b/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts index cc3a7caa85..1acc7616ed 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts @@ -10,6 +10,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {measureRhsOpened} from 'actions/views/rhs'; import {getIsMobileView} from 'selectors/views/browser'; import {makePrepareReplyIdsForThreadViewer, makeGetThreadLastViewedAt} from 'selectors/views/threads'; @@ -58,4 +59,8 @@ function makeMapStateToProps() { }; } -export default connect(makeMapStateToProps)(ThreadViewerVirtualized); +const mapDispatchToProps = { + measureRhsOpened, +}; + +export default connect(makeMapStateToProps, mapDispatchToProps)(ThreadViewerVirtualized); diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx index 1e3b16ed9f..c1b7ad2671 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx @@ -39,6 +39,7 @@ function getBasePropsAndState(): [Props, DeepPartial] { isMobileView: false, isThreadView: false, newMessagesSeparatorActions: [], + measureRhsOpened: jest.fn(), }; const state: DeepPartial = { diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx index fb951014c2..babcfd4d6b 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx @@ -41,6 +41,7 @@ type Props = { isThreadView: boolean; newMessagesSeparatorActions: PluginComponent[]; inputPlaceholder?: string; + measureRhsOpened: () => void; } type State = { @@ -122,6 +123,8 @@ class ThreadViewerVirtualized extends PureComponent { componentDidMount() { this.mounted = true; + + this.props.measureRhsOpened(); } componentWillUnmount() { diff --git a/webapp/channels/src/utils/performance_telemetry/index.ts b/webapp/channels/src/utils/performance_telemetry/index.ts index 909b7ee0d7..552c1eedb1 100644 --- a/webapp/channels/src/utils/performance_telemetry/index.ts +++ b/webapp/channels/src/utils/performance_telemetry/index.ts @@ -1,6 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +export const enum Mark { + ChannelLinkClicked = 'SidebarChannelLink#click', + PostListLoaded = 'PostList#component', + PostSelected = 'PostList#postSelected', + TeamLinkClicked = 'TeamLink#click', +} + +export const enum Measure { + ChannelSwitch = 'channel_switch', + RhsLoad = 'rhs_load', + TeamSwitch = 'team_switch', +} + export function markAndReport(name: string): PerformanceMark { return performance.mark(name, { detail: { @@ -11,12 +24,12 @@ export function markAndReport(name: string): PerformanceMark { /** * Measures the duration between two performance marks, schedules it to be reported to the server, and returns the - * PerformanceMeasure created by doing this. + * PerformanceMeasure created by doing this. If endMark is omitted, the measure will measure the duration until now. * * If either the start or end mark does not exist, undefined will be returned and, if canFail is false, an error * will be logged. */ -export function measureAndReport(measureName: string, startMark: string, endMark: string, canFail = false): PerformanceMeasure | undefined { +export function measureAndReport(measureName: string, startMark: string, endMark: string | undefined, canFail = false): PerformanceMeasure | undefined { const options: PerformanceMeasureOptions = { start: startMark, end: endMark,