diff --git a/server/channels/app/metrics.go b/server/channels/app/metrics.go index 9f7d961fdbb..59075755d9c 100644 --- a/server/channels/app/metrics.go +++ b/server/channels/app/metrics.go @@ -72,6 +72,10 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa a.Metrics().ObserveMobileClientChannelSwitchDuration(commonLabels["platform"], h.Value/1000) case model.MobileClientTeamSwitchDuration: 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: // we intentionally skip unknown metrics } diff --git a/server/einterfaces/metrics.go b/server/einterfaces/metrics.go index 54aca1fc58c..38e7ec0d7e6 100644 --- a/server/einterfaces/metrics.go +++ b/server/einterfaces/metrics.go @@ -120,4 +120,6 @@ type MetricsInterface interface { ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64) ClearMobileClientSessionMetadata() ObserveMobileClientSessionMetadata(version string, platform string, value float64, notificationDisabled string) + ObserveDesktopCpuUsage(platform, version, process string, usage float64) + ObserveDesktopMemoryUsage(platform, version, process string, usage float64) } diff --git a/server/einterfaces/mocks/MetricsInterface.go b/server/einterfaces/mocks/MetricsInterface.go index 76bb3ae1b2c..13ddb1a7ff2 100644 --- a/server/einterfaces/mocks/MetricsInterface.go +++ b/server/einterfaces/mocks/MetricsInterface.go @@ -353,6 +353,16 @@ func (_m *MetricsInterface) ObserveClusterRequestDuration(elapsed float64) { _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 func (_m *MetricsInterface) ObserveEnabledUsers(users int64) { _m.Called(users) diff --git a/server/enterprise/metrics/metrics.go b/server/enterprise/metrics/metrics.go index 389778b182b..688ef96a680 100644 --- a/server/enterprise/metrics/metrics.go +++ b/server/enterprise/metrics/metrics.go @@ -44,6 +44,7 @@ const ( MetricsSubsystemNotifications = "notifications" MetricsSubsystemClientsMobileApp = "mobileapp" MetricsSubsystemClientsWeb = "webapp" + MetricsSubsystemClientsDesktopApp = "desktopapp" MetricsCloudInstallationLabel = "installationId" MetricsCloudDatabaseClusterLabel = "databaseClusterName" MetricsCloudInstallationGroupLabel = "installationGroupId" @@ -216,6 +217,9 @@ type MetricsInterfaceImpl struct { MobileClientChannelSwitchDuration *prometheus.HistogramVec MobileClientTeamSwitchDuration *prometheus.HistogramVec MobileClientSessionMetadataGauge *prometheus.GaugeVec + + DesktopClientCPUUsage *prometheus.HistogramVec + DesktopClientMemoryUsage *prometheus.HistogramVec } func init() { @@ -1347,6 +1351,30 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf ) 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 } @@ -1850,6 +1878,14 @@ func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent 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) { mi.MobileClientLoadDuration.With(prometheus.Labels{"platform": platform}).Observe(elapsed) } diff --git a/server/public/model/metrics.go b/server/public/model/metrics.go index d3691b5c487..e8e47912dfb 100644 --- a/server/public/model/metrics.go +++ b/server/public/model/metrics.go @@ -30,6 +30,9 @@ const ( MobileClientChannelSwitchDuration MetricType = "mobile_channel_switch" MobileClientTeamSwitchDuration MetricType = "mobile_team_switch" + DesktopClientCPUUsage MetricType = "desktop_cpu" + DesktopClientMemoryUsage MetricType = "desktop_memory" + performanceReportTTLMilliseconds = 300 * 1000 // 300 seconds/5 minutes ) @@ -104,8 +107,9 @@ func (r *PerformanceReport) IsValid() error { func (r *PerformanceReport) ProcessLabels() map[string]string { return map[string]string{ - "platform": processLabel(r.Labels, "platform", acceptedPlatforms, "other"), - "agent": processLabel(r.Labels, "agent", acceptedAgents, "other"), + "platform": processLabel(r.Labels, "platform", acceptedPlatforms, "other"), + "agent": processLabel(r.Labels, "agent", acceptedAgents, "other"), + "desktop_app_version": r.Labels["desktop_app_version"], } } diff --git a/webapp/channels/package.json b/webapp/channels/package.json index c4b29017489..09f773e3cf3 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -13,7 +13,7 @@ "@mattermost/client": "*", "@mattermost/compass-components": "^0.2.12", "@mattermost/compass-icons": "0.1.39", - "@mattermost/desktop-api": "5.8.0-5", + "@mattermost/desktop-api": "5.10.0-2", "@mattermost/types": "*", "@mui/base": "5.0.0-alpha.127", "@mui/material": "5.11.16", diff --git a/webapp/channels/src/components/root/performance_reporter_controller.tsx b/webapp/channels/src/components/root/performance_reporter_controller.tsx index 14a780f2ce9..b8e102e0445 100644 --- a/webapp/channels/src/components/root/performance_reporter_controller.tsx +++ b/webapp/channels/src/components/root/performance_reporter_controller.tsx @@ -6,6 +6,7 @@ import {useStore} from 'react-redux'; import {Client4} from 'mattermost-redux/client'; +import DesktopAppAPI from 'utils/desktop_api'; import PerformanceReporter from 'utils/performance_telemetry/reporter'; export default function PerformanceReporterController() { @@ -14,7 +15,7 @@ export default function PerformanceReporterController() { const reporter = useRef(); useEffect(() => { - reporter.current = new PerformanceReporter(Client4, store); + reporter.current = new PerformanceReporter(Client4, store, DesktopAppAPI); reporter.current.observe(); // There's no way to clean up web-vitals, so continue to assume that this component won't ever be unmounted diff --git a/webapp/channels/src/utils/desktop_api.ts b/webapp/channels/src/utils/desktop_api.ts index 392740e3fd1..293a25d1821 100644 --- a/webapp/channels/src/utils/desktop_api.ts +++ b/webapp/channels/src/utils/desktop_api.ts @@ -13,9 +13,10 @@ declare global { } } -class DesktopAppAPI { +export class DesktopAppAPI { private name?: string; private version?: string | null; + private prereleaseVersion?: string; private dev?: boolean; /** @@ -32,6 +33,7 @@ class DesktopAppAPI { this.getDesktopAppInfo().then(({name, version}) => { this.name = name; this.version = semver.valid(semver.coerce(version)); + this.prereleaseVersion = version?.split('-')?.[1]; // Legacy Desktop App version, used by some plugins if (!window.desktop) { @@ -63,6 +65,10 @@ class DesktopAppAPI { return this.version; }; + getPrereleaseVersion = () => { + return this.prereleaseVersion; + }; + isDev = () => { return this.dev; }; @@ -152,6 +158,10 @@ class DesktopAppAPI { return () => this.removePostMessageListener('history-button-return', legacyListener); }; + onReceiveMetrics = (listener: (metricsMap: Map) => void) => { + return window.desktopAPI?.onSendMetrics?.(listener); + }; + /** * One-ways */ diff --git a/webapp/channels/src/utils/performance_telemetry/platform_detection.ts b/webapp/channels/src/utils/performance_telemetry/platform_detection.ts index b6a7f5cd198..4dde347bc09 100644 --- a/webapp/channels/src/utils/performance_telemetry/platform_detection.ts +++ b/webapp/channels/src/utils/performance_telemetry/platform_detection.ts @@ -37,3 +37,7 @@ export function getUserAgentLabel() { return 'other'; } + +export function getDesktopAppVersionLabel(appVersion?: string | null, prereleaseVersion?: string) { + return prereleaseVersion?.split('.')[0] ?? appVersion ?? 'unknown'; +} diff --git a/webapp/channels/src/utils/performance_telemetry/reporter.test.ts b/webapp/channels/src/utils/performance_telemetry/reporter.test.ts index 3d2b9c4510b..888386c8b65 100644 --- a/webapp/channels/src/utils/performance_telemetry/reporter.test.ts +++ b/webapp/channels/src/utils/performance_telemetry/reporter.test.ts @@ -10,6 +10,7 @@ import configureStore from 'store'; import {reset as resetUserAgent, setPlatform, set as setUserAgent} from 'tests/helpers/user_agent_mocks'; import {waitForObservations} from 'tests/performance_mock'; +import {DesktopAppAPI} from 'utils/desktop_api'; import PerformanceReporter from './reporter'; @@ -389,7 +390,7 @@ function newTestReporter(telemetryEnabled = true, loggedIn = true) { currentUserId: loggedIn ? 'currentUserId' : '', }, }, - })); + }), new DesktopAppAPI()); return { client, diff --git a/webapp/channels/src/utils/performance_telemetry/reporter.ts b/webapp/channels/src/utils/performance_telemetry/reporter.ts index e4cf85b610c..af2a9237e6b 100644 --- a/webapp/channels/src/utils/performance_telemetry/reporter.ts +++ b/webapp/channels/src/utils/performance_telemetry/reporter.ts @@ -10,12 +10,14 @@ import type {Client4} from '@mattermost/client'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {DesktopAppAPI} from 'utils/desktop_api'; + import type {GlobalState} from 'types/store'; import {identifyElementRegion} from './element_identification'; import type {PerformanceLongTaskTiming} from './long_task'; import type {PlatformLabel, UserAgentLabel} from './platform_detection'; -import {getPlatformLabel, getUserAgentLabel} from './platform_detection'; +import {getDesktopAppVersionLabel, getPlatformLabel, getUserAgentLabel} from './platform_detection'; import {Measure} from '.'; @@ -52,6 +54,7 @@ type PerformanceReport = { labels: { platform: PlatformLabel; agent: UserAgentLabel; + desktop_app_version?: string; }; start: number; @@ -65,8 +68,12 @@ export default class PerformanceReporter { private client: Client4; private store: Store; + private desktopAPI: DesktopAppAPI; + private desktopOffListener?: () => void; + private platformLabel: PlatformLabel; private userAgentLabel: UserAgentLabel; + private desktopAppVersion?: string; private counters: Map; private histogramMeasures: PerformanceReportMeasure[]; @@ -78,13 +85,17 @@ export default class PerformanceReporter { protected reportPeriodBase = 60 * 1000; protected reportPeriodJitter = 15 * 1000; - constructor(client: Client4, store: Store) { + constructor(client: Client4, store: Store, desktopAPI: DesktopAppAPI) { this.client = client; this.store = store; + this.desktopAPI = desktopAPI; this.platformLabel = getPlatformLabel(); 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.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 // what's recommended by various sites due to unload handlers being unreliable, particularly on mobile. addEventListener('visibilitychange', this.handleVisibilityChange); + + if (!this.desktopAPI.isDev()) { + this.desktopOffListener = this.desktopAPI.onReceiveMetrics((metrics) => this.collectDesktopAppMetrics(metrics)); + } } private measurePageLoad() { @@ -147,6 +162,8 @@ export default class PerformanceReporter { this.reportTimeout = undefined; this.observer.disconnect(); + + this.desktopOffListener?.(); } protected handleObservations(list: PerformanceObserverEntryList) { @@ -279,6 +296,7 @@ export default class PerformanceReporter { labels: { platform: this.platformLabel, agent: this.userAgentLabel, + desktop_app_version: this.desktopAppVersion, }, ...this.getReportStartEnd(now, histogramMeasures, counterMeasures), @@ -341,6 +359,35 @@ export default class PerformanceReporter { return navigator.sendBeacon(url, data); } + + protected collectDesktopAppMetrics(metricsMap: Map) { + 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 { diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 8de88b2907e..a2fd4074d5b 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -62,7 +62,7 @@ "@mattermost/client": "*", "@mattermost/compass-components": "^0.2.12", "@mattermost/compass-icons": "0.1.39", - "@mattermost/desktop-api": "5.8.0-5", + "@mattermost/desktop-api": "5.10.0-2", "@mattermost/types": "*", "@mui/base": "5.0.0-alpha.127", "@mui/material": "5.11.16", @@ -227,6 +227,19 @@ "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": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -4310,19 +4323,6 @@ "resolved": "platform/components", "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": { "resolved": "platform/eslint-plugin", "link": true