[MM-60609][MM-60612] Include Desktop App metrics in PerformanceReporter, add metrics in Prometheus for CPU/Memory usage (#28825)

* [MM-60609][MM-60612] Include Desktop App metrics in PerformanceReporter, add metrics in Prometheus for CPU/Memory usage

* Fix mocks

* PR feedback
Этот коммит содержится в:
Devin Binnie
2024-10-22 12:16:20 -04:00
коммит произвёл GitHub
родитель 2d96053012
Коммит 0c90b0363b
12 изменённых файлов: 141 добавлений и 22 удалений

Просмотреть файл

@@ -72,6 +72,10 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa
a.Metrics().ObserveMobileClientChannelSwitchDuration(commonLabels["platform"], h.Value/1000) a.Metrics().ObserveMobileClientChannelSwitchDuration(commonLabels["platform"], h.Value/1000)
case model.MobileClientTeamSwitchDuration: case model.MobileClientTeamSwitchDuration:
a.Metrics().ObserveMobileClientTeamSwitchDuration(commonLabels["platform"], h.Value/1000) a.Metrics().ObserveMobileClientTeamSwitchDuration(commonLabels["platform"], h.Value/1000)
case model.DesktopClientCPUUsage:
a.Metrics().ObserveDesktopCpuUsage(commonLabels["platform"], commonLabels["desktop_app_version"], h.Labels["process"], h.Value)
case model.DesktopClientMemoryUsage:
a.Metrics().ObserveDesktopMemoryUsage(commonLabels["platform"], commonLabels["desktop_app_version"], h.Labels["process"], h.Value/1000)
default: default:
// we intentionally skip unknown metrics // we intentionally skip unknown metrics
} }

Просмотреть файл

@@ -120,4 +120,6 @@ type MetricsInterface interface {
ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64) ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64)
ClearMobileClientSessionMetadata() ClearMobileClientSessionMetadata()
ObserveMobileClientSessionMetadata(version string, platform string, value float64, notificationDisabled string) ObserveMobileClientSessionMetadata(version string, platform string, value float64, notificationDisabled string)
ObserveDesktopCpuUsage(platform, version, process string, usage float64)
ObserveDesktopMemoryUsage(platform, version, process string, usage float64)
} }

Просмотреть файл

@@ -353,6 +353,16 @@ func (_m *MetricsInterface) ObserveClusterRequestDuration(elapsed float64) {
_m.Called(elapsed) _m.Called(elapsed)
} }
// ObserveDesktopCpuUsage provides a mock function with given fields: platform, version, process, usage
func (_m *MetricsInterface) ObserveDesktopCpuUsage(platform string, version string, process string, usage float64) {
_m.Called(platform, version, process, usage)
}
// ObserveDesktopMemoryUsage provides a mock function with given fields: platform, version, process, usage
func (_m *MetricsInterface) ObserveDesktopMemoryUsage(platform string, version string, process string, usage float64) {
_m.Called(platform, version, process, usage)
}
// ObserveEnabledUsers provides a mock function with given fields: users // ObserveEnabledUsers provides a mock function with given fields: users
func (_m *MetricsInterface) ObserveEnabledUsers(users int64) { func (_m *MetricsInterface) ObserveEnabledUsers(users int64) {
_m.Called(users) _m.Called(users)

Просмотреть файл

@@ -44,6 +44,7 @@ const (
MetricsSubsystemNotifications = "notifications" MetricsSubsystemNotifications = "notifications"
MetricsSubsystemClientsMobileApp = "mobileapp" MetricsSubsystemClientsMobileApp = "mobileapp"
MetricsSubsystemClientsWeb = "webapp" MetricsSubsystemClientsWeb = "webapp"
MetricsSubsystemClientsDesktopApp = "desktopapp"
MetricsCloudInstallationLabel = "installationId" MetricsCloudInstallationLabel = "installationId"
MetricsCloudDatabaseClusterLabel = "databaseClusterName" MetricsCloudDatabaseClusterLabel = "databaseClusterName"
MetricsCloudInstallationGroupLabel = "installationGroupId" MetricsCloudInstallationGroupLabel = "installationGroupId"
@@ -216,6 +217,9 @@ type MetricsInterfaceImpl struct {
MobileClientChannelSwitchDuration *prometheus.HistogramVec MobileClientChannelSwitchDuration *prometheus.HistogramVec
MobileClientTeamSwitchDuration *prometheus.HistogramVec MobileClientTeamSwitchDuration *prometheus.HistogramVec
MobileClientSessionMetadataGauge *prometheus.GaugeVec MobileClientSessionMetadataGauge *prometheus.GaugeVec
DesktopClientCPUUsage *prometheus.HistogramVec
DesktopClientMemoryUsage *prometheus.HistogramVec
} }
func init() { func init() {
@@ -1347,6 +1351,30 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
) )
m.Registry.MustRegister(m.MobileClientSessionMetadataGauge) m.Registry.MustRegister(m.MobileClientSessionMetadataGauge)
m.DesktopClientCPUUsage = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsDesktopApp,
Name: "cpu_usage",
Help: "Average CPU usage of a specific process over an interval",
Buckets: []float64{0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 80, 100},
},
[]string{"platform", "version", "processName"},
)
m.Registry.MustRegister(m.DesktopClientCPUUsage)
m.DesktopClientMemoryUsage = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsDesktopApp,
Name: "memory_usage",
Help: "Memory usage in MB of a specific process",
Buckets: []float64{0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 3000, 5000},
},
[]string{"platform", "version", "processName"},
)
m.Registry.MustRegister(m.DesktopClientMemoryUsage)
return m return m
} }
@@ -1850,6 +1878,14 @@ func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent
mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
} }
func (mi *MetricsInterfaceImpl) ObserveDesktopCpuUsage(platform, version, process string, usage float64) {
mi.DesktopClientCPUUsage.With(prometheus.Labels{"platform": platform, "version": version, "processName": process}).Observe(usage)
}
func (mi *MetricsInterfaceImpl) ObserveDesktopMemoryUsage(platform, version, process string, usage float64) {
mi.DesktopClientMemoryUsage.With(prometheus.Labels{"platform": platform, "version": version, "processName": process}).Observe(usage)
}
func (mi *MetricsInterfaceImpl) ObserveMobileClientLoadDuration(platform string, elapsed float64) { func (mi *MetricsInterfaceImpl) ObserveMobileClientLoadDuration(platform string, elapsed float64) {
mi.MobileClientLoadDuration.With(prometheus.Labels{"platform": platform}).Observe(elapsed) mi.MobileClientLoadDuration.With(prometheus.Labels{"platform": platform}).Observe(elapsed)
} }

Просмотреть файл

@@ -30,6 +30,9 @@ const (
MobileClientChannelSwitchDuration MetricType = "mobile_channel_switch" MobileClientChannelSwitchDuration MetricType = "mobile_channel_switch"
MobileClientTeamSwitchDuration MetricType = "mobile_team_switch" MobileClientTeamSwitchDuration MetricType = "mobile_team_switch"
DesktopClientCPUUsage MetricType = "desktop_cpu"
DesktopClientMemoryUsage MetricType = "desktop_memory"
performanceReportTTLMilliseconds = 300 * 1000 // 300 seconds/5 minutes performanceReportTTLMilliseconds = 300 * 1000 // 300 seconds/5 minutes
) )
@@ -104,8 +107,9 @@ func (r *PerformanceReport) IsValid() error {
func (r *PerformanceReport) ProcessLabels() map[string]string { func (r *PerformanceReport) ProcessLabels() map[string]string {
return map[string]string{ return map[string]string{
"platform": processLabel(r.Labels, "platform", acceptedPlatforms, "other"), "platform": processLabel(r.Labels, "platform", acceptedPlatforms, "other"),
"agent": processLabel(r.Labels, "agent", acceptedAgents, "other"), "agent": processLabel(r.Labels, "agent", acceptedAgents, "other"),
"desktop_app_version": r.Labels["desktop_app_version"],
} }
} }

Просмотреть файл

@@ -13,7 +13,7 @@
"@mattermost/client": "*", "@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12", "@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39", "@mattermost/compass-icons": "0.1.39",
"@mattermost/desktop-api": "5.8.0-5", "@mattermost/desktop-api": "5.10.0-2",
"@mattermost/types": "*", "@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127", "@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16", "@mui/material": "5.11.16",

Просмотреть файл

@@ -6,6 +6,7 @@ import {useStore} from 'react-redux';
import {Client4} from 'mattermost-redux/client'; import {Client4} from 'mattermost-redux/client';
import DesktopAppAPI from 'utils/desktop_api';
import PerformanceReporter from 'utils/performance_telemetry/reporter'; import PerformanceReporter from 'utils/performance_telemetry/reporter';
export default function PerformanceReporterController() { export default function PerformanceReporterController() {
@@ -14,7 +15,7 @@ export default function PerformanceReporterController() {
const reporter = useRef<PerformanceReporter>(); const reporter = useRef<PerformanceReporter>();
useEffect(() => { useEffect(() => {
reporter.current = new PerformanceReporter(Client4, store); reporter.current = new PerformanceReporter(Client4, store, DesktopAppAPI);
reporter.current.observe(); reporter.current.observe();
// There's no way to clean up web-vitals, so continue to assume that this component won't ever be unmounted // There's no way to clean up web-vitals, so continue to assume that this component won't ever be unmounted

Просмотреть файл

@@ -13,9 +13,10 @@ declare global {
} }
} }
class DesktopAppAPI { export class DesktopAppAPI {
private name?: string; private name?: string;
private version?: string | null; private version?: string | null;
private prereleaseVersion?: string;
private dev?: boolean; private dev?: boolean;
/** /**
@@ -32,6 +33,7 @@ class DesktopAppAPI {
this.getDesktopAppInfo().then(({name, version}) => { this.getDesktopAppInfo().then(({name, version}) => {
this.name = name; this.name = name;
this.version = semver.valid(semver.coerce(version)); this.version = semver.valid(semver.coerce(version));
this.prereleaseVersion = version?.split('-')?.[1];
// Legacy Desktop App version, used by some plugins // Legacy Desktop App version, used by some plugins
if (!window.desktop) { if (!window.desktop) {
@@ -63,6 +65,10 @@ class DesktopAppAPI {
return this.version; return this.version;
}; };
getPrereleaseVersion = () => {
return this.prereleaseVersion;
};
isDev = () => { isDev = () => {
return this.dev; return this.dev;
}; };
@@ -152,6 +158,10 @@ class DesktopAppAPI {
return () => this.removePostMessageListener('history-button-return', legacyListener); return () => this.removePostMessageListener('history-button-return', legacyListener);
}; };
onReceiveMetrics = (listener: (metricsMap: Map<string, {cpu?: number; memory?: number}>) => void) => {
return window.desktopAPI?.onSendMetrics?.(listener);
};
/** /**
* One-ways * One-ways
*/ */

Просмотреть файл

@@ -37,3 +37,7 @@ export function getUserAgentLabel() {
return 'other'; return 'other';
} }
export function getDesktopAppVersionLabel(appVersion?: string | null, prereleaseVersion?: string) {
return prereleaseVersion?.split('.')[0] ?? appVersion ?? 'unknown';
}

Просмотреть файл

@@ -10,6 +10,7 @@ import configureStore from 'store';
import {reset as resetUserAgent, setPlatform, set as setUserAgent} from 'tests/helpers/user_agent_mocks'; import {reset as resetUserAgent, setPlatform, set as setUserAgent} from 'tests/helpers/user_agent_mocks';
import {waitForObservations} from 'tests/performance_mock'; import {waitForObservations} from 'tests/performance_mock';
import {DesktopAppAPI} from 'utils/desktop_api';
import PerformanceReporter from './reporter'; import PerformanceReporter from './reporter';
@@ -389,7 +390,7 @@ function newTestReporter(telemetryEnabled = true, loggedIn = true) {
currentUserId: loggedIn ? 'currentUserId' : '', currentUserId: loggedIn ? 'currentUserId' : '',
}, },
}, },
})); }), new DesktopAppAPI());
return { return {
client, client,

Просмотреть файл

@@ -10,12 +10,14 @@ import type {Client4} from '@mattermost/client';
import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {DesktopAppAPI} from 'utils/desktop_api';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
import {identifyElementRegion} from './element_identification'; import {identifyElementRegion} from './element_identification';
import type {PerformanceLongTaskTiming} from './long_task'; import type {PerformanceLongTaskTiming} from './long_task';
import type {PlatformLabel, UserAgentLabel} from './platform_detection'; import type {PlatformLabel, UserAgentLabel} from './platform_detection';
import {getPlatformLabel, getUserAgentLabel} from './platform_detection'; import {getDesktopAppVersionLabel, getPlatformLabel, getUserAgentLabel} from './platform_detection';
import {Measure} from '.'; import {Measure} from '.';
@@ -52,6 +54,7 @@ type PerformanceReport = {
labels: { labels: {
platform: PlatformLabel; platform: PlatformLabel;
agent: UserAgentLabel; agent: UserAgentLabel;
desktop_app_version?: string;
}; };
start: number; start: number;
@@ -65,8 +68,12 @@ export default class PerformanceReporter {
private client: Client4; private client: Client4;
private store: Store<GlobalState>; private store: Store<GlobalState>;
private desktopAPI: DesktopAppAPI;
private desktopOffListener?: () => void;
private platformLabel: PlatformLabel; private platformLabel: PlatformLabel;
private userAgentLabel: UserAgentLabel; private userAgentLabel: UserAgentLabel;
private desktopAppVersion?: string;
private counters: Map<string, number>; private counters: Map<string, number>;
private histogramMeasures: PerformanceReportMeasure[]; private histogramMeasures: PerformanceReportMeasure[];
@@ -78,13 +85,17 @@ export default class PerformanceReporter {
protected reportPeriodBase = 60 * 1000; protected reportPeriodBase = 60 * 1000;
protected reportPeriodJitter = 15 * 1000; protected reportPeriodJitter = 15 * 1000;
constructor(client: Client4, store: Store<GlobalState>) { constructor(client: Client4, store: Store<GlobalState>, desktopAPI: DesktopAppAPI) {
this.client = client; this.client = client;
this.store = store; this.store = store;
this.desktopAPI = desktopAPI;
this.platformLabel = getPlatformLabel(); this.platformLabel = getPlatformLabel();
this.userAgentLabel = getUserAgentLabel(); this.userAgentLabel = getUserAgentLabel();
// We want to submit by prerelease version if it exists, so we don't muddy up the metrics for the release builds
this.desktopAppVersion = getDesktopAppVersionLabel(desktopAPI.getAppVersion(), desktopAPI.getPrereleaseVersion());
this.counters = new Map(); this.counters = new Map();
this.histogramMeasures = []; this.histogramMeasures = [];
@@ -121,6 +132,10 @@ export default class PerformanceReporter {
// Send any remaining metrics when the page becomes hidden rather than when it's unloaded because that's // Send any remaining metrics when the page becomes hidden rather than when it's unloaded because that's
// what's recommended by various sites due to unload handlers being unreliable, particularly on mobile. // what's recommended by various sites due to unload handlers being unreliable, particularly on mobile.
addEventListener('visibilitychange', this.handleVisibilityChange); addEventListener('visibilitychange', this.handleVisibilityChange);
if (!this.desktopAPI.isDev()) {
this.desktopOffListener = this.desktopAPI.onReceiveMetrics((metrics) => this.collectDesktopAppMetrics(metrics));
}
} }
private measurePageLoad() { private measurePageLoad() {
@@ -147,6 +162,8 @@ export default class PerformanceReporter {
this.reportTimeout = undefined; this.reportTimeout = undefined;
this.observer.disconnect(); this.observer.disconnect();
this.desktopOffListener?.();
} }
protected handleObservations(list: PerformanceObserverEntryList) { protected handleObservations(list: PerformanceObserverEntryList) {
@@ -279,6 +296,7 @@ export default class PerformanceReporter {
labels: { labels: {
platform: this.platformLabel, platform: this.platformLabel,
agent: this.userAgentLabel, agent: this.userAgentLabel,
desktop_app_version: this.desktopAppVersion,
}, },
...this.getReportStartEnd(now, histogramMeasures, counterMeasures), ...this.getReportStartEnd(now, histogramMeasures, counterMeasures),
@@ -341,6 +359,35 @@ export default class PerformanceReporter {
return navigator.sendBeacon(url, data); return navigator.sendBeacon(url, data);
} }
protected collectDesktopAppMetrics(metricsMap: Map<string, {cpu?: number; memory?: number}>) {
const now = Date.now();
for (const [processName, metrics] of metricsMap.entries()) {
let process = processName;
if (process.startsWith('Server ')) {
process = 'Server';
}
if (metrics.cpu) {
this.histogramMeasures.push({
metric: 'desktop_cpu',
timestamp: now,
labels: {process},
value: metrics.cpu,
});
}
if (metrics.memory) {
this.histogramMeasures.push({
metric: 'desktop_memory',
timestamp: now,
labels: {process},
value: metrics.memory,
});
}
}
}
} }
function isPerformanceLongTask(entry: PerformanceEntry): entry is PerformanceLongTaskTiming { function isPerformanceLongTask(entry: PerformanceEntry): entry is PerformanceLongTaskTiming {

28
webapp/package-lock.json сгенерированный
Просмотреть файл

@@ -62,7 +62,7 @@
"@mattermost/client": "*", "@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12", "@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39", "@mattermost/compass-icons": "0.1.39",
"@mattermost/desktop-api": "5.8.0-5", "@mattermost/desktop-api": "5.10.0-2",
"@mattermost/types": "*", "@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127", "@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16", "@mui/material": "5.11.16",
@@ -227,6 +227,19 @@
"yargs": "16.2.0" "yargs": "16.2.0"
} }
}, },
"channels/node_modules/@mattermost/desktop-api": {
"version": "5.10.0-2",
"resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.10.0-2.tgz",
"integrity": "sha512-Okb+VP6gdwEBnSthzxiU3+oO5XLTocDvWlzoGYjBYrT4DN2/xxAzRYgp1VB6skQxEwVhbP/zaQvWbRtZrp8org==",
"peerDependencies": {
"typescript": "^4.3.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"channels/node_modules/semver": { "channels/node_modules/semver": {
"version": "7.3.8", "version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -4310,19 +4323,6 @@
"resolved": "platform/components", "resolved": "platform/components",
"link": true "link": true
}, },
"node_modules/@mattermost/desktop-api": {
"version": "5.8.0-5",
"resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.8.0-5.tgz",
"integrity": "sha512-YPtFRnduVFOXyK25GedJA+PkAKmFpLDKqfxvV/IIS+SMypv6BD17LJ8AkdBsguFFa6ZWLlZU40M5grKYBbjOuA==",
"peerDependencies": {
"typescript": "^4.3.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@mattermost/eslint-plugin": { "node_modules/@mattermost/eslint-plugin": {
"resolved": "platform/eslint-plugin", "resolved": "platform/eslint-plugin",
"link": true "link": true