MM-57881 Add metric for the amount of time it takes for the RHS to load a thread (#26970)
* Assure most code dispatches selectPost to open RHS to a thread * Add measurement of the time it takes to open the RHS * Reduce unnecessary re-rendering of RhsThread * Never use Jest's fake timers for the performance API * Make constants for performance marks and measures * Add missed constants
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
de3c7ad544
Коммит
099f704d4f
@@ -19,6 +19,9 @@ const config = {
|
||||
'utils/src/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
coverageReporters: ['lcov', 'text-summary'],
|
||||
fakeTimers: {
|
||||
doNotFake: ['performance'],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@mattermost/(components)$': '<rootDir>/../platform/$1/src',
|
||||
'^@mattermost/(client)$': '<rootDir>/../platform/$1/src',
|
||||
|
||||
@@ -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<boolean, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const postRootId = post.root_id || post.id;
|
||||
function selectPostWithPreviousState(post: Post, previousRhsState?: RhsState): ActionFunc<boolean, GlobalState> {
|
||||
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<boolean, GlobalState> {
|
||||
}
|
||||
|
||||
export function selectPostFromRightHandSideSearch(post: Post) {
|
||||
return selectPostFromRightHandSideSearchWithPreviousState(post);
|
||||
return selectPostWithPreviousState(post);
|
||||
}
|
||||
|
||||
export function selectPostFromRightHandSideSearchByPostId(postId: string): ActionFuncAsync<boolean, GlobalState> {
|
||||
@@ -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<unknown, GlobalSt
|
||||
}
|
||||
if (previous.selectedPostId) {
|
||||
const post = getPost(getState(), previous.selectedPostId);
|
||||
return post ? dispatch(selectPostFromRightHandSideSearchWithPreviousState(post, previous.previousRhsState)) : dispatch(openRHSSearch());
|
||||
return post ? dispatch(selectPostWithPreviousState(post, previous.previousRhsState)) : dispatch(openRHSSearch());
|
||||
}
|
||||
if (previous.selectedPostCardId) {
|
||||
const post = getPost(getState(), previous.selectedPostCardId);
|
||||
@@ -648,3 +645,11 @@ export function setEditChannelMembers(active: boolean) {
|
||||
active,
|
||||
};
|
||||
}
|
||||
|
||||
export function measureRhsOpened() {
|
||||
return () => {
|
||||
measureAndReport(Measure.RhsLoad, Mark.PostSelected, undefined, true);
|
||||
|
||||
performance.clearMarks(Mark.PostSelected);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div/>
|
||||
);
|
||||
|
||||
@@ -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<Props, State
|
||||
removeTooltipLink = (): void => this.gmItemRef.current?.removeAttribute?.('aria-describedby');
|
||||
|
||||
handleChannelClick = (event: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
mark('SidebarChannelLink#click');
|
||||
mark(Mark.ChannelLinkClicked);
|
||||
this.handleSelectChannel(event);
|
||||
|
||||
if (this.props.rhsOpen && this.props.rhsState === RHSStates.EDIT_HISTORY) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -39,6 +39,7 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
|
||||
isMobileView: false,
|
||||
isThreadView: false,
|
||||
newMessagesSeparatorActions: [],
|
||||
measureRhsOpened: jest.fn(),
|
||||
};
|
||||
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
|
||||
@@ -41,6 +41,7 @@ type Props = {
|
||||
isThreadView: boolean;
|
||||
newMessagesSeparatorActions: PluginComponent[];
|
||||
inputPlaceholder?: string;
|
||||
measureRhsOpened: () => void;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -122,6 +123,8 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
|
||||
this.props.measureRhsOpened();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Ссылка в новой задаче
Block a user