[MM-60086][MM-60610] Implement performanceMonitor, collect CPU/memory usage data and send via API (#3165)
* [MM-60086][MM-60610] Implement performanceMonitor, collect CPU/memory usage data and send via API * Translations * PR feedback * Update api-types package
This commit is contained in:
@@ -79,6 +79,8 @@ export type DesktopAPI = {
|
||||
openCallsUserSettings: () => void;
|
||||
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||
|
||||
onSendMetrics: (listener: (metricsMap: Map<string, {cpu?: number; memory?: number}>) => void) => () => void;
|
||||
|
||||
// Utility
|
||||
unregister: (channel: string) => void;
|
||||
}
|
||||
|
4
api-types/lib/index.d.ts
vendored
4
api-types/lib/index.d.ts
vendored
@@ -65,5 +65,9 @@ export type DesktopAPI = {
|
||||
onOpenStopRecordingModal: (listener: (channelID: string) => void) => () => void;
|
||||
openCallsUserSettings: () => void;
|
||||
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||
onSendMetrics: (listener: (metricsMap: Map<string, {
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
}>) => void) => () => void;
|
||||
unregister: (channel: string) => void;
|
||||
};
|
||||
|
4
api-types/package-lock.json
generated
4
api-types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@mattermost/desktop-api",
|
||||
"version": "5.10.0-1",
|
||||
"version": "5.10.0-2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@mattermost/desktop-api",
|
||||
"version": "5.10.0-1",
|
||||
"version": "5.10.0-2",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^4.3.0 || ^5.0.0"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mattermost/desktop-api",
|
||||
"version": "5.10.0-1",
|
||||
"version": "5.10.0-2",
|
||||
"description": "Shared types for the Desktop App API provided to the Web App",
|
||||
"keywords": [
|
||||
"mattermost"
|
||||
|
@@ -218,6 +218,8 @@
|
||||
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
||||
"renderer.components.settingsPage.enableHardwareAcceleration": "Use GPU hardware acceleration",
|
||||
"renderer.components.settingsPage.enableHardwareAcceleration.description": "If enabled, {appName} UI is rendered more efficiently but can lead to decreased stability for some systems.",
|
||||
"renderer.components.settingsPage.enableMetrics": "Send anonymous usage data to your configured servers",
|
||||
"renderer.components.settingsPage.enableMetrics.description": "Sends usage data about the application and its performance to your configured servers that accept it.",
|
||||
"renderer.components.settingsPage.flashWindow": "Flash taskbar icon when a new message is received",
|
||||
"renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.",
|
||||
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.",
|
||||
|
@@ -150,6 +150,7 @@ const configDataSchemaV3 = Joi.object<ConfigV3>({
|
||||
alwaysClose: Joi.boolean(),
|
||||
logLevel: Joi.string().default('info'),
|
||||
appLanguage: Joi.string().allow(''),
|
||||
enableMetrics: Joi.boolean(),
|
||||
});
|
||||
|
||||
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
|
||||
|
@@ -197,3 +197,7 @@ export const GET_NONCE = 'get-nonce';
|
||||
export const DEVELOPER_MODE_UPDATED = 'developer-mode-updated';
|
||||
export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
||||
export const GET_DEVELOPER_MODE_SETTING = 'get-developer-mode-setting';
|
||||
|
||||
export const METRICS_SEND = 'metrics-send';
|
||||
export const METRICS_RECEIVE = 'metrics-receive';
|
||||
export const METRICS_REQUEST = 'metrics-request';
|
||||
|
@@ -51,6 +51,7 @@ const defaultPreferences: ConfigV3 = {
|
||||
downloadLocation: getDefaultDownloadLocation(),
|
||||
startInFullscreen: false,
|
||||
logLevel: 'info',
|
||||
enableMetrics: true,
|
||||
};
|
||||
|
||||
export default defaultPreferences;
|
||||
|
@@ -241,6 +241,10 @@ export class Config extends EventEmitter {
|
||||
return this.combinedData?.appLanguage;
|
||||
}
|
||||
|
||||
get enableMetrics() {
|
||||
return this.combinedData?.enableMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the servers from registry into the config object and reload
|
||||
*
|
||||
|
@@ -27,5 +27,11 @@ export default function migrateConfigItems(config: Config) {
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
if (!migrationPrefs.getValue('enableMetrics')) {
|
||||
config.enableMetrics = true;
|
||||
migrationPrefs.setValue('enableMetrics', true);
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
return didMigrate;
|
||||
}
|
||||
|
@@ -77,7 +77,9 @@ jest.mock('electron', () => ({
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
init: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
setLocale: jest.fn(),
|
||||
|
@@ -50,6 +50,7 @@ import i18nManager from 'main/i18nManager';
|
||||
import NonceManager from 'main/nonceManager';
|
||||
import {getDoNotDisturb} from 'main/notifications';
|
||||
import parseArgs from 'main/ParseArgs';
|
||||
import PerformanceMonitor from 'main/performanceMonitor';
|
||||
import PermissionsManager from 'main/permissionsManager';
|
||||
import Tray from 'main/tray/tray';
|
||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||
@@ -448,6 +449,10 @@ async function initializeAfterAppReady() {
|
||||
AppVersionManager.lastAppVersion = app.getVersion();
|
||||
|
||||
handleMainWindowIsShown();
|
||||
|
||||
// The metrics won't start collecting for another minute
|
||||
// so we can assume if we start now everything should be loaded by the time we're done
|
||||
PerformanceMonitor.init();
|
||||
}
|
||||
|
||||
function onUserActivityStatus(status: {
|
||||
|
255
src/main/performanceMonitor.test.js
Normal file
255
src/main/performanceMonitor.test.js
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {app, ipcMain, powerMonitor} from 'electron';
|
||||
|
||||
import {EMIT_CONFIGURATION, METRICS_RECEIVE, METRICS_REQUEST, METRICS_SEND} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
|
||||
import {PerformanceMonitor} from './performanceMonitor';
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
app: {
|
||||
getAppMetrics: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
powerMonitor: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('common/config', () => ({
|
||||
enableMetrics: true,
|
||||
}));
|
||||
|
||||
describe('main/performanceMonitor', () => {
|
||||
let makeWebContents;
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(global, 'setInterval');
|
||||
jest.spyOn(global, 'clearInterval');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
app.getAppMetrics.mockReturnValue([]);
|
||||
|
||||
let cb;
|
||||
ipcMain.on.mockImplementation((channel, listener) => {
|
||||
if (channel === METRICS_RECEIVE) {
|
||||
cb = listener;
|
||||
}
|
||||
});
|
||||
|
||||
makeWebContents = (id, resolve) => ({
|
||||
send: jest.fn().mockImplementation((channel, arg1, arg2) => {
|
||||
if (channel === METRICS_REQUEST) {
|
||||
cb({sender: {id}}, arg1, {serverId: arg2, cpu: id, memory: id * 100});
|
||||
}
|
||||
if (channel === METRICS_SEND) {
|
||||
resolve(arg1);
|
||||
}
|
||||
}),
|
||||
on: (_, listener) => listener(),
|
||||
id,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Config.enableMetrics = true;
|
||||
});
|
||||
|
||||
it('should start and stop with config changes', () => {
|
||||
let emitConfigCb;
|
||||
ipcMain.on.mockImplementation((channel, listener) => {
|
||||
if (channel === EMIT_CONFIGURATION) {
|
||||
emitConfigCb = listener;
|
||||
}
|
||||
});
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
|
||||
Config.enableMetrics = false;
|
||||
emitConfigCb();
|
||||
expect(clearInterval).toHaveBeenCalled();
|
||||
|
||||
Config.enableMetrics = true;
|
||||
emitConfigCb();
|
||||
expect(setInterval).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should start and stop with power monitor changes', () => {
|
||||
const listeners = new Map();
|
||||
powerMonitor.on.mockImplementation((channel, listener) => {
|
||||
listeners.set(channel, listener);
|
||||
});
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
|
||||
listeners.get('suspend')();
|
||||
expect(clearInterval).toHaveBeenCalled();
|
||||
|
||||
setInterval.mockClear();
|
||||
clearInterval.mockClear();
|
||||
|
||||
listeners.get('resume')();
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
|
||||
listeners.get('lock-screen')();
|
||||
expect(clearInterval).toHaveBeenCalled();
|
||||
|
||||
setInterval.mockClear();
|
||||
clearInterval.mockClear();
|
||||
|
||||
listeners.get('unlock-screen')();
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
|
||||
listeners.get('speed-limit-change')(50);
|
||||
expect(clearInterval).toHaveBeenCalled();
|
||||
|
||||
setInterval.mockClear();
|
||||
clearInterval.mockClear();
|
||||
|
||||
listeners.get('speed-limit-change')(100);
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should not start until init', () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
expect(setInterval).not.toHaveBeenCalled();
|
||||
|
||||
performanceMonitor.init();
|
||||
expect(setInterval).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run app metrics for node on init', () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
expect(app.getAppMetrics).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not start if disabled by config', () => {
|
||||
Config.enableMetrics = false;
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
expect(setInterval).not.toHaveBeenCalled();
|
||||
|
||||
performanceMonitor.init();
|
||||
expect(setInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerView', () => {
|
||||
it('should send metrics to registered server views', async () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(await sendValue).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}]]));
|
||||
});
|
||||
|
||||
it('should send metrics for other tabs to registered server views', async () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
performanceMonitor.registerView('view-2', makeWebContents(2, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(await sendValue).toEqual(new Map([['view-2', {cpu: 2, memory: 200, serverId: 'server-1'}], ['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}]]));
|
||||
});
|
||||
|
||||
it('should not send metrics for tabs of other servers to registered server views', async () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
performanceMonitor.registerView('view-2', makeWebContents(2, resolve), 'server-2');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(await sendValue).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}]]));
|
||||
});
|
||||
|
||||
it('should always include node metrics', async () => {
|
||||
app.getAppMetrics.mockReturnValue([{
|
||||
name: 'main',
|
||||
type: 'Browser',
|
||||
cpu: {percentCPUUsage: 50},
|
||||
memory: {privateBytes: 1000},
|
||||
}]);
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(await sendValue).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}], ['main', {cpu: 50, memory: 1000}]]));
|
||||
});
|
||||
|
||||
it('should never include tabs from getAppMetrics', async () => {
|
||||
app.getAppMetrics.mockReturnValue([{
|
||||
name: 'other-server',
|
||||
type: 'Tab',
|
||||
cpu: {percentCPUUsage: 50},
|
||||
memory: {privateBytes: 1000},
|
||||
}]);
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(await sendValue).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}]]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterView', () => {
|
||||
it('should not send after the view is removed', async () => {
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
performanceMonitor.init();
|
||||
|
||||
const sendValue = new Promise((resolve) => {
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
performanceMonitor.registerServerView('view-2', makeWebContents(2, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(await sendValue).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}], ['view-2', {cpu: 2, memory: 200, serverId: 'server-1'}]]));
|
||||
|
||||
// Have to re-register to make sure the promise resolves
|
||||
const sendValue2 = new Promise((resolve) => {
|
||||
performanceMonitor.unregisterView(2);
|
||||
performanceMonitor.registerServerView('view-1', makeWebContents(1, resolve), 'server-1');
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(await sendValue2).toEqual(new Map([['view-1', {cpu: 1, memory: 100, serverId: 'server-1'}]]));
|
||||
});
|
||||
});
|
||||
});
|
177
src/main/performanceMonitor.ts
Normal file
177
src/main/performanceMonitor.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {app, ipcMain, type IpcMainEvent, powerMonitor, type WebContents} from 'electron';
|
||||
|
||||
import {EMIT_CONFIGURATION, METRICS_RECEIVE, METRICS_REQUEST, METRICS_SEND} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
import {Logger} from 'common/log';
|
||||
|
||||
const METRIC_SEND_INTERVAL = 60000;
|
||||
const log = new Logger('PerformanceMonitor');
|
||||
|
||||
type MetricsView = {
|
||||
name: string;
|
||||
webContents: WebContents;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
type Metrics = {
|
||||
serverId?: string;
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
}
|
||||
|
||||
export class PerformanceMonitor {
|
||||
private updateInterval?: NodeJS.Timeout;
|
||||
private views: Map<number, MetricsView>;
|
||||
private serverViews: Map<number, MetricsView>;
|
||||
private isInitted: boolean;
|
||||
|
||||
constructor() {
|
||||
this.views = new Map();
|
||||
this.serverViews = new Map();
|
||||
this.isInitted = false;
|
||||
|
||||
powerMonitor.on('suspend', this.stop);
|
||||
powerMonitor.on('resume', this.start);
|
||||
powerMonitor.on('lock-screen', this.stop);
|
||||
powerMonitor.on('unlock-screen', this.start);
|
||||
powerMonitor.on('speed-limit-change', this.handleSpeedLimitChange);
|
||||
|
||||
ipcMain.on(EMIT_CONFIGURATION, this.handleConfigUpdate);
|
||||
}
|
||||
|
||||
init = () => {
|
||||
// Set that it's initted so that the powerMonitor functions correctly
|
||||
this.isInitted = true;
|
||||
|
||||
// Run once because the first CPU value is always 0
|
||||
this.runMetrics();
|
||||
|
||||
if (Config.enableMetrics) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
registerView = (name: string, webContents: WebContents, serverId?: string) => {
|
||||
log.debug('registerView', webContents.id, name);
|
||||
|
||||
webContents.on('did-finish-load', () => {
|
||||
this.views.set(webContents.id, {name, webContents, serverId});
|
||||
});
|
||||
};
|
||||
|
||||
registerServerView = (name: string, webContents: WebContents, serverId: string) => {
|
||||
log.debug('registerServerView', webContents.id, serverId);
|
||||
|
||||
webContents.on('did-finish-load', () => {
|
||||
this.serverViews.set(webContents.id, {name, webContents, serverId});
|
||||
});
|
||||
};
|
||||
|
||||
unregisterView = (webContentsId: number) => {
|
||||
log.debug('unregisterView', webContentsId);
|
||||
|
||||
this.views.delete(webContentsId);
|
||||
this.serverViews.delete(webContentsId);
|
||||
};
|
||||
|
||||
private start = () => {
|
||||
if (!this.isInitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Config.enableMetrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.verbose('start');
|
||||
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
this.updateInterval = setInterval(this.sendMetrics, METRIC_SEND_INTERVAL);
|
||||
};
|
||||
|
||||
private stop = () => {
|
||||
log.verbose('stop');
|
||||
|
||||
clearInterval(this.updateInterval);
|
||||
delete this.updateInterval;
|
||||
};
|
||||
|
||||
private runMetrics = async () => {
|
||||
const metricsMap: Map<string, Metrics> = new Map();
|
||||
|
||||
// Collect metrics for all of the Node processes
|
||||
app.getAppMetrics().
|
||||
filter((metric) => metric.type !== 'Tab').
|
||||
forEach((metric) => {
|
||||
metricsMap.set(metric.name ?? metric.type, {
|
||||
cpu: metric.cpu.percentCPUUsage,
|
||||
memory: metric.memory.privateBytes ?? metric.memory.workingSetSize,
|
||||
});
|
||||
});
|
||||
|
||||
const viewResolves: Map<number, () => void> = new Map();
|
||||
const listener = (event: IpcMainEvent, name: string, metrics: Metrics) => {
|
||||
metricsMap.set(name, metrics);
|
||||
viewResolves.get(event.sender.id)?.();
|
||||
};
|
||||
ipcMain.on(METRICS_RECEIVE, listener);
|
||||
const viewPromises = [...this.views.values(), ...this.serverViews.values()].map((view) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
viewResolves.set(view.webContents.id, resolve);
|
||||
view.webContents.send(METRICS_REQUEST, view.name, view.serverId);
|
||||
});
|
||||
});
|
||||
|
||||
// After 5 seconds, if all the promises are not resolved, resolve them so we don't block the send
|
||||
// This can happen if a view doesn't send back metrics information
|
||||
setTimeout(() => {
|
||||
[...viewResolves.values()].forEach((value) => value());
|
||||
}, 5000);
|
||||
await Promise.allSettled(viewPromises);
|
||||
ipcMain.off(METRICS_RECEIVE, listener);
|
||||
return metricsMap;
|
||||
};
|
||||
|
||||
private sendMetrics = async () => {
|
||||
const metricsMap = await this.runMetrics();
|
||||
for (const view of this.serverViews.values()) {
|
||||
const serverId = view.serverId;
|
||||
if (!serverId) {
|
||||
log.error(`Cannot send metrics for ${view.name} - missing server id`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!view.webContents) {
|
||||
log.error(`Cannot send metrics for ${view.name} - missing web contents`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const serverMetricsMap = new Map([...metricsMap].filter((value) => !value[1].serverId || value[1].serverId === view.serverId));
|
||||
view.webContents.send(METRICS_SEND, serverMetricsMap);
|
||||
}
|
||||
};
|
||||
|
||||
private handleConfigUpdate = () => {
|
||||
if (!Config.enableMetrics && this.updateInterval) {
|
||||
this.stop();
|
||||
} else if (!this.updateInterval) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
private handleSpeedLimitChange = (limit: number) => {
|
||||
if (limit < 100) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
export default performanceMonitor;
|
@@ -44,6 +44,9 @@ import {
|
||||
LEGACY_OFF,
|
||||
TAB_LOGIN_CHANGED,
|
||||
GET_DEVELOPER_MODE_SETTING,
|
||||
METRICS_SEND,
|
||||
METRICS_REQUEST,
|
||||
METRICS_RECEIVE,
|
||||
} from 'common/communication';
|
||||
|
||||
import type {ExternalAPI} from 'types/externalAPI';
|
||||
@@ -131,12 +134,22 @@ ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceLegacyAPI').then((force) =>
|
||||
openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS),
|
||||
onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener),
|
||||
|
||||
onSendMetrics: (listener) => createListener(METRICS_SEND, listener),
|
||||
|
||||
// Utility
|
||||
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||
};
|
||||
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
||||
});
|
||||
|
||||
ipcRenderer.on(METRICS_REQUEST, async (_, name, serverId) => {
|
||||
const memory = await process.getProcessMemoryInfo();
|
||||
ipcRenderer.send(METRICS_RECEIVE, name, {serverId, cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private});
|
||||
});
|
||||
|
||||
// Call this once to unset it to 0
|
||||
process.getCPUUsage();
|
||||
|
||||
// Specific info for the testing environment
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
contextBridge.exposeInMainWorld('testHelper', {
|
||||
|
@@ -93,6 +93,8 @@ import {
|
||||
VIEW_FINISHED_RESIZING,
|
||||
GET_NONCE,
|
||||
IS_DEVELOPER_MODE_ENABLED,
|
||||
METRICS_REQUEST,
|
||||
METRICS_RECEIVE,
|
||||
} from 'common/communication';
|
||||
|
||||
console.log('Preload initialized');
|
||||
@@ -258,3 +260,10 @@ const createKeyDownListener = () => {
|
||||
};
|
||||
createKeyDownListener();
|
||||
|
||||
ipcRenderer.on(METRICS_REQUEST, async (_, name) => {
|
||||
const memory = await process.getProcessMemoryInfo();
|
||||
ipcRenderer.send(METRICS_RECEIVE, name, {cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private});
|
||||
});
|
||||
|
||||
// Call this once to unset it to 0
|
||||
process.getCPUUsage();
|
||||
|
@@ -59,6 +59,9 @@ jest.mock('main/AutoLauncher', () => ({
|
||||
jest.mock('main/badge', () => ({
|
||||
setUnreadBadgeSetting: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/windows/mainWindow', () => ({
|
||||
sendToRenderer: jest.fn(),
|
||||
on: jest.fn(),
|
||||
|
@@ -62,6 +62,11 @@ jest.mock('../utils', () => ({
|
||||
jest.mock('main/developerMode', () => ({
|
||||
get: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
registerServerView: jest.fn(),
|
||||
unregisterView: jest.fn(),
|
||||
}));
|
||||
|
||||
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
||||
const view = new MessagingView(server, true);
|
||||
|
@@ -23,8 +23,9 @@ import type {Logger} from 'common/log';
|
||||
import ServerManager from 'common/servers/serverManager';
|
||||
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
||||
import {isInternalURL, parseURL} from 'common/utils/url';
|
||||
import type {MattermostView} from 'common/views/View';
|
||||
import {TAB_MESSAGING, type MattermostView} from 'common/views/View';
|
||||
import DeveloperMode from 'main/developerMode';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import {getServerAPI} from 'main/server/serverAPI';
|
||||
import MainWindow from 'main/windows/mainWindow';
|
||||
|
||||
@@ -196,6 +197,11 @@ export class MattermostBrowserView extends EventEmitter {
|
||||
loadURL = this.view.url.toString();
|
||||
}
|
||||
this.log.verbose(`Loading ${loadURL}`);
|
||||
if (this.view.type === TAB_MESSAGING) {
|
||||
performanceMonitor.registerServerView(`Server ${this.browserView.webContents.id}`, this.browserView.webContents, this.view.server.id);
|
||||
} else {
|
||||
performanceMonitor.registerView(`Server ${this.browserView.webContents.id}`, this.browserView.webContents, this.view.server.id);
|
||||
}
|
||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
if (err.code && err.code.startsWith('ERR_CERT')) {
|
||||
@@ -262,6 +268,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
|
||||
AppState.clear(this.id);
|
||||
MainWindow.get()?.removeBrowserView(this.browserView);
|
||||
performanceMonitor.unregisterView(this.browserView.webContents.id);
|
||||
this.browserView.webContents.close();
|
||||
|
||||
this.isVisible = false;
|
||||
|
@@ -52,6 +52,9 @@ jest.mock('macos-notification-state', () => ({
|
||||
getDoNotDisturb: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/downloadsManager', () => ({}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/windows/mainWindow', () => ({
|
||||
on: jest.fn(),
|
||||
get: jest.fn(),
|
||||
|
@@ -27,6 +27,7 @@ import {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from 'common/utils/constants';
|
||||
import downloadsManager from 'main/downloadsManager';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import {getLocalPreload} from 'main/utils';
|
||||
import MainWindow from 'main/windows/mainWindow';
|
||||
|
||||
@@ -75,6 +76,7 @@ export class DownloadsDropdownMenuView {
|
||||
// @ts-ignore
|
||||
transparent: true,
|
||||
}});
|
||||
performanceMonitor.registerView('DownloadsDropdownMenuView', this.view.webContents);
|
||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html');
|
||||
MainWindow.get()?.addBrowserView(this.view);
|
||||
};
|
||||
|
@@ -65,6 +65,9 @@ jest.mock('main/downloadsManager', () => ({
|
||||
onOpen: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/windows/mainWindow', () => ({
|
||||
on: jest.fn(),
|
||||
get: jest.fn(),
|
||||
|
@@ -21,6 +21,7 @@ import Config from 'common/config';
|
||||
import {Logger} from 'common/log';
|
||||
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
|
||||
import downloadsManager from 'main/downloadsManager';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import {getLocalPreload} from 'main/utils';
|
||||
import MainWindow from 'main/windows/mainWindow';
|
||||
|
||||
@@ -65,6 +66,7 @@ export class DownloadsDropdownView {
|
||||
transparent: true,
|
||||
}});
|
||||
|
||||
performanceMonitor.registerView('DownloadsDropdownView', this.view.webContents);
|
||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
||||
MainWindow.get()?.addBrowserView(this.view);
|
||||
};
|
||||
|
@@ -10,7 +10,9 @@ jest.mock('electron', () => ({
|
||||
on: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/windows/mainWindow', () => ({
|
||||
get: jest.fn(),
|
||||
on: jest.fn(),
|
||||
|
@@ -5,6 +5,7 @@ import {BrowserView, app, ipcMain} from 'electron';
|
||||
|
||||
import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
|
||||
import {Logger} from 'common/log';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import {getLocalPreload, getWindowBoundaries} from 'main/utils';
|
||||
import MainWindow from 'main/windows/mainWindow';
|
||||
|
||||
@@ -86,6 +87,8 @@ export class LoadingScreen {
|
||||
transparent: true,
|
||||
}});
|
||||
const localURL = 'mattermost-desktop://renderer/loadingScreen.html';
|
||||
|
||||
performanceMonitor.registerView('LoadingScreen', this.view.webContents);
|
||||
this.view.webContents.loadURL(localURL);
|
||||
};
|
||||
|
||||
|
@@ -27,6 +27,10 @@ jest.mock('../contextMenu', () => jest.fn());
|
||||
jest.mock('../utils', () => ({
|
||||
getWindowBoundaries: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
unregisterView: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/views/modalView', () => {
|
||||
describe('show', () => {
|
||||
|
@@ -5,6 +5,7 @@ import type {BrowserWindow} from 'electron';
|
||||
import {BrowserView} from 'electron';
|
||||
|
||||
import {Logger} from 'common/log';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries} from '../utils';
|
||||
@@ -50,6 +51,7 @@ export class ModalView<T, T2> {
|
||||
|
||||
this.status = Status.ACTIVE;
|
||||
try {
|
||||
performanceMonitor.registerView(`Modal-${key}`, this.view.webContents);
|
||||
this.view.webContents.loadURL(this.html);
|
||||
} catch (e) {
|
||||
this.log.error('there was an error loading the modal:');
|
||||
@@ -99,6 +101,7 @@ export class ModalView<T, T2> {
|
||||
this.view.webContents.closeDevTools();
|
||||
}
|
||||
this.windowAttached.removeBrowserView(this.view);
|
||||
performanceMonitor.unregisterView(this.view.webContents.id);
|
||||
this.view.webContents.close();
|
||||
|
||||
delete this.windowAttached;
|
||||
|
@@ -29,6 +29,9 @@ jest.mock('electron', () => ({
|
||||
getPath: jest.fn(() => '/valid/downloads/path'),
|
||||
},
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/windows/mainWindow', () => ({
|
||||
on: jest.fn(),
|
||||
get: jest.fn(),
|
||||
|
@@ -22,6 +22,7 @@ import Config from 'common/config';
|
||||
import {Logger} from 'common/log';
|
||||
import ServerManager from 'common/servers/serverManager';
|
||||
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import {getLocalPreload} from 'main/utils';
|
||||
|
||||
import type {UniqueServer} from 'types/config';
|
||||
@@ -83,6 +84,7 @@ export class ServerDropdownView {
|
||||
// @ts-ignore
|
||||
transparent: true,
|
||||
}});
|
||||
performanceMonitor.registerView('ServerDropdownView', this.view.webContents);
|
||||
this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html');
|
||||
|
||||
this.setOrderedServers();
|
||||
|
@@ -82,6 +82,9 @@ jest.mock('main/windows/mainWindow', () => ({
|
||||
get: jest.fn(),
|
||||
on: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
jest.mock('common/servers/serverManager', () => ({
|
||||
getOrderedTabsForServer: jest.fn(),
|
||||
getAllServers: jest.fn(),
|
||||
|
@@ -48,6 +48,7 @@ import {TAB_MESSAGING} from 'common/views/View';
|
||||
import {flushCookiesStore} from 'main/app/utils';
|
||||
import DeveloperMode from 'main/developerMode';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import PermissionsManager from 'main/permissionsManager';
|
||||
import MainWindow from 'main/windows/mainWindow';
|
||||
|
||||
@@ -373,6 +374,7 @@ export class ViewManager {
|
||||
transparent: true,
|
||||
}});
|
||||
const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`;
|
||||
performanceMonitor.registerView('URLView', urlView.webContents);
|
||||
urlView.webContents.loadURL(localURL);
|
||||
MainWindow.get()?.addBrowserView(urlView);
|
||||
const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds();
|
||||
@@ -385,6 +387,7 @@ export class ViewManager {
|
||||
log.error('Failed to remove URL view', e);
|
||||
}
|
||||
|
||||
performanceMonitor.unregisterView(urlView.webContents.id);
|
||||
urlView.webContents.close();
|
||||
};
|
||||
|
||||
|
@@ -67,6 +67,10 @@ jest.mock('main/windows/mainWindow', () => ({
|
||||
jest.mock('app/serverViewState', () => ({
|
||||
switchServer: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
unregisterView: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/views/viewManager', () => ({
|
||||
getView: jest.fn(),
|
||||
getViewByWebContentsId: jest.fn(),
|
||||
@@ -156,6 +160,9 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||
on: jest.fn(),
|
||||
close: jest.fn(),
|
||||
isDestroyed: jest.fn(),
|
||||
webContents: {
|
||||
id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@@ -28,6 +28,7 @@ import {Logger} from 'common/log';
|
||||
import {CALLS_PLUGIN_ID, MINIMUM_CALLS_WIDGET_HEIGHT, MINIMUM_CALLS_WIDGET_WIDTH} from 'common/utils/constants';
|
||||
import {getFormattedPathName, isCallsPopOutURL, parseURL} from 'common/utils/url';
|
||||
import Utils from 'common/utils/util';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
import PermissionsManager from 'main/permissionsManager';
|
||||
import {
|
||||
composeUserAgent,
|
||||
@@ -173,6 +174,7 @@ export class CallsWidgetWindow {
|
||||
if (!widgetURL) {
|
||||
return;
|
||||
}
|
||||
performanceMonitor.registerView('CallsWidgetWindow', this.win.webContents);
|
||||
this.win?.loadURL(widgetURL, {
|
||||
userAgent: composeUserAgent(),
|
||||
}).catch((reason) => {
|
||||
@@ -195,6 +197,7 @@ export class CallsWidgetWindow {
|
||||
return;
|
||||
}
|
||||
this.win?.on('closed', resolve);
|
||||
performanceMonitor.unregisterView(this.win.webContents.id);
|
||||
this.win?.close();
|
||||
});
|
||||
};
|
||||
|
@@ -73,6 +73,9 @@ jest.mock('../utils', () => ({
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/performanceMonitor', () => ({
|
||||
registerView: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/windows/mainWindow', () => {
|
||||
describe('init', () => {
|
||||
|
@@ -34,6 +34,7 @@ import Utils from 'common/utils/util';
|
||||
import * as Validator from 'common/Validator';
|
||||
import {boundsInfoPath} from 'main/constants';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import performanceMonitor from 'main/performanceMonitor';
|
||||
|
||||
import type {SavedWindowState} from 'types/mainWindow';
|
||||
|
||||
@@ -152,6 +153,7 @@ export class MainWindow extends EventEmitter {
|
||||
contextMenu.reload();
|
||||
|
||||
const localURL = 'mattermost-desktop://renderer/index.html';
|
||||
performanceMonitor.registerView('MainWindow', this.win.webContents);
|
||||
this.win.loadURL(localURL).catch(
|
||||
(reason) => {
|
||||
log.error('failed to load', reason);
|
||||
|
@@ -67,6 +67,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
|
||||
logLevelRef: React.RefObject<HTMLSelectElement>;
|
||||
appLanguageRef: React.RefObject<HTMLSelectElement>;
|
||||
enableMetricsRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
saveQueue: SaveQueueItem[];
|
||||
|
||||
@@ -106,6 +107,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||
this.autoCheckForUpdatesRef = React.createRef();
|
||||
this.logLevelRef = React.createRef();
|
||||
this.appLanguageRef = React.createRef();
|
||||
this.enableMetricsRef = React.createRef();
|
||||
|
||||
this.saveQueue = [];
|
||||
this.selectedSpellCheckerLocales = [];
|
||||
@@ -218,6 +220,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
handleEnableMetrics = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableMetrics', data: this.enableMetricsRef.current?.checked});
|
||||
this.setState({
|
||||
enableMetrics: this.enableMetricsRef.current?.checked,
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeShowTrayIcon = () => {
|
||||
const shouldShowTrayIcon = this.showTrayIconRef.current?.checked;
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
|
||||
@@ -709,6 +718,30 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||
</FormCheck>);
|
||||
}
|
||||
|
||||
options.push(
|
||||
<FormCheck
|
||||
key='enableMetrics'
|
||||
>
|
||||
<FormCheck.Input
|
||||
type='checkbox'
|
||||
key='inputEnableMetrics'
|
||||
id='inputEnableMetrics'
|
||||
ref={this.enableMetricsRef}
|
||||
checked={this.state.enableMetrics}
|
||||
onChange={this.handleEnableMetrics}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics'
|
||||
defaultMessage='Send anonymous usage data to your configured servers'
|
||||
/>
|
||||
<FormText>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics.description'
|
||||
defaultMessage='Sends usage data about the application and its performance to your configured servers that accept it.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
|
||||
if (window.process.platform === 'win32' || window.process.platform === 'linux') {
|
||||
options.push(
|
||||
<FormCheck>
|
||||
|
@@ -59,6 +59,7 @@ export type ConfigV3 = {
|
||||
alwaysClose?: boolean;
|
||||
logLevel?: string;
|
||||
appLanguage?: string;
|
||||
enableMetrics?: boolean;
|
||||
}
|
||||
|
||||
export type ConfigV2 =
|
||||
@@ -131,4 +132,5 @@ export type MigrationInfo = {
|
||||
updateTrayIconWin32: boolean;
|
||||
masConfigs: boolean;
|
||||
closeExtraTabs: boolean;
|
||||
enableMetrics: boolean;
|
||||
}
|
||||
|
@@ -25,4 +25,5 @@ export interface ExternalAPI {
|
||||
createListener(event: 'calls-widget-open-thread', listener: (threadID: string) => void): () => void;
|
||||
createListener(event: 'calls-widget-open-stop-recording-modal', listener: (channelID: string) => void): () => void;
|
||||
createListener(event: 'calls-widget-open-user-settings', listener: () => void): () => void;
|
||||
createListener(event: 'metrics-send', listener: (metricsMap: Map<string, {cpu?: number; memory?: number}>) => void): () => void;
|
||||
}
|
||||
|
Reference in New Issue
Block a user