[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;
|
openCallsUserSettings: () => void;
|
||||||
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||||
|
|
||||||
|
onSendMetrics: (listener: (metricsMap: Map<string, {cpu?: number; memory?: number}>) => void) => () => void;
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
unregister: (channel: string) => void;
|
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;
|
onOpenStopRecordingModal: (listener: (channelID: string) => void) => () => void;
|
||||||
openCallsUserSettings: () => void;
|
openCallsUserSettings: () => void;
|
||||||
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
onOpenCallsUserSettings: (listener: () => void) => () => void;
|
||||||
|
onSendMetrics: (listener: (metricsMap: Map<string, {
|
||||||
|
cpu?: number;
|
||||||
|
memory?: number;
|
||||||
|
}>) => void) => () => void;
|
||||||
unregister: (channel: string) => 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",
|
"name": "@mattermost/desktop-api",
|
||||||
"version": "5.10.0-1",
|
"version": "5.10.0-2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@mattermost/desktop-api",
|
"name": "@mattermost/desktop-api",
|
||||||
"version": "5.10.0-1",
|
"version": "5.10.0-2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^4.3.0 || ^5.0.0"
|
"typescript": "^4.3.0 || ^5.0.0"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mattermost/desktop-api",
|
"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",
|
"description": "Shared types for the Desktop App API provided to the Web App",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mattermost"
|
"mattermost"
|
||||||
|
@@ -218,6 +218,8 @@
|
|||||||
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
||||||
"renderer.components.settingsPage.enableHardwareAcceleration": "Use GPU hardware acceleration",
|
"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.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": "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": "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.",
|
"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(),
|
alwaysClose: Joi.boolean(),
|
||||||
logLevel: Joi.string().default('info'),
|
logLevel: Joi.string().default('info'),
|
||||||
appLanguage: Joi.string().allow(''),
|
appLanguage: Joi.string().allow(''),
|
||||||
|
enableMetrics: Joi.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
|
// 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 DEVELOPER_MODE_UPDATED = 'developer-mode-updated';
|
||||||
export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
||||||
export const GET_DEVELOPER_MODE_SETTING = 'get-developer-mode-setting';
|
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(),
|
downloadLocation: getDefaultDownloadLocation(),
|
||||||
startInFullscreen: false,
|
startInFullscreen: false,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
enableMetrics: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defaultPreferences;
|
export default defaultPreferences;
|
||||||
|
@@ -241,6 +241,10 @@ export class Config extends EventEmitter {
|
|||||||
return this.combinedData?.appLanguage;
|
return this.combinedData?.appLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get enableMetrics() {
|
||||||
|
return this.combinedData?.enableMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the servers from registry into the config object and reload
|
* Gets the servers from registry into the config object and reload
|
||||||
*
|
*
|
||||||
|
@@ -27,5 +27,11 @@ export default function migrateConfigItems(config: Config) {
|
|||||||
didMigrate = true;
|
didMigrate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!migrationPrefs.getValue('enableMetrics')) {
|
||||||
|
config.enableMetrics = true;
|
||||||
|
migrationPrefs.setValue('enableMetrics', true);
|
||||||
|
didMigrate = true;
|
||||||
|
}
|
||||||
|
|
||||||
return didMigrate;
|
return didMigrate;
|
||||||
}
|
}
|
||||||
|
@@ -77,7 +77,9 @@ jest.mock('electron', () => ({
|
|||||||
handle: jest.fn(),
|
handle: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
init: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
setLocale: jest.fn(),
|
setLocale: jest.fn(),
|
||||||
|
@@ -50,6 +50,7 @@ import i18nManager from 'main/i18nManager';
|
|||||||
import NonceManager from 'main/nonceManager';
|
import NonceManager from 'main/nonceManager';
|
||||||
import {getDoNotDisturb} from 'main/notifications';
|
import {getDoNotDisturb} from 'main/notifications';
|
||||||
import parseArgs from 'main/ParseArgs';
|
import parseArgs from 'main/ParseArgs';
|
||||||
|
import PerformanceMonitor from 'main/performanceMonitor';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
import Tray from 'main/tray/tray';
|
import Tray from 'main/tray/tray';
|
||||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||||
@@ -448,6 +449,10 @@ async function initializeAfterAppReady() {
|
|||||||
AppVersionManager.lastAppVersion = app.getVersion();
|
AppVersionManager.lastAppVersion = app.getVersion();
|
||||||
|
|
||||||
handleMainWindowIsShown();
|
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: {
|
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,
|
LEGACY_OFF,
|
||||||
TAB_LOGIN_CHANGED,
|
TAB_LOGIN_CHANGED,
|
||||||
GET_DEVELOPER_MODE_SETTING,
|
GET_DEVELOPER_MODE_SETTING,
|
||||||
|
METRICS_SEND,
|
||||||
|
METRICS_REQUEST,
|
||||||
|
METRICS_RECEIVE,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
import type {ExternalAPI} from 'types/externalAPI';
|
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),
|
openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS),
|
||||||
onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener),
|
onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener),
|
||||||
|
|
||||||
|
onSendMetrics: (listener) => createListener(METRICS_SEND, listener),
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
};
|
};
|
||||||
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
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
|
// Specific info for the testing environment
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
contextBridge.exposeInMainWorld('testHelper', {
|
contextBridge.exposeInMainWorld('testHelper', {
|
||||||
|
@@ -93,6 +93,8 @@ import {
|
|||||||
VIEW_FINISHED_RESIZING,
|
VIEW_FINISHED_RESIZING,
|
||||||
GET_NONCE,
|
GET_NONCE,
|
||||||
IS_DEVELOPER_MODE_ENABLED,
|
IS_DEVELOPER_MODE_ENABLED,
|
||||||
|
METRICS_REQUEST,
|
||||||
|
METRICS_RECEIVE,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
@@ -258,3 +260,10 @@ const createKeyDownListener = () => {
|
|||||||
};
|
};
|
||||||
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', () => ({
|
jest.mock('main/badge', () => ({
|
||||||
setUnreadBadgeSetting: jest.fn(),
|
setUnreadBadgeSetting: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/mainWindow', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
@@ -62,6 +62,11 @@ jest.mock('../utils', () => ({
|
|||||||
jest.mock('main/developerMode', () => ({
|
jest.mock('main/developerMode', () => ({
|
||||||
get: jest.fn(),
|
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 server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
||||||
const view = new MessagingView(server, true);
|
const view = new MessagingView(server, true);
|
||||||
|
@@ -23,8 +23,9 @@ import type {Logger} from 'common/log';
|
|||||||
import ServerManager from 'common/servers/serverManager';
|
import ServerManager from 'common/servers/serverManager';
|
||||||
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
||||||
import {isInternalURL, parseURL} from 'common/utils/url';
|
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 DeveloperMode from 'main/developerMode';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import {getServerAPI} from 'main/server/serverAPI';
|
import {getServerAPI} from 'main/server/serverAPI';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -196,6 +197,11 @@ export class MattermostBrowserView extends EventEmitter {
|
|||||||
loadURL = this.view.url.toString();
|
loadURL = this.view.url.toString();
|
||||||
}
|
}
|
||||||
this.log.verbose(`Loading ${loadURL}`);
|
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'))});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||||
if (err.code && err.code.startsWith('ERR_CERT')) {
|
if (err.code && err.code.startsWith('ERR_CERT')) {
|
||||||
@@ -262,6 +268,7 @@ export class MattermostBrowserView extends EventEmitter {
|
|||||||
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
|
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
|
||||||
AppState.clear(this.id);
|
AppState.clear(this.id);
|
||||||
MainWindow.get()?.removeBrowserView(this.browserView);
|
MainWindow.get()?.removeBrowserView(this.browserView);
|
||||||
|
performanceMonitor.unregisterView(this.browserView.webContents.id);
|
||||||
this.browserView.webContents.close();
|
this.browserView.webContents.close();
|
||||||
|
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
@@ -52,6 +52,9 @@ jest.mock('macos-notification-state', () => ({
|
|||||||
getDoNotDisturb: jest.fn(),
|
getDoNotDisturb: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('main/downloadsManager', () => ({}));
|
jest.mock('main/downloadsManager', () => ({}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/mainWindow', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
|
@@ -27,6 +27,7 @@ import {
|
|||||||
TAB_BAR_HEIGHT,
|
TAB_BAR_HEIGHT,
|
||||||
} from 'common/utils/constants';
|
} from 'common/utils/constants';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import {getLocalPreload} from 'main/utils';
|
import {getLocalPreload} from 'main/utils';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ export class DownloadsDropdownMenuView {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
transparent: true,
|
transparent: true,
|
||||||
}});
|
}});
|
||||||
|
performanceMonitor.registerView('DownloadsDropdownMenuView', this.view.webContents);
|
||||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html');
|
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html');
|
||||||
MainWindow.get()?.addBrowserView(this.view);
|
MainWindow.get()?.addBrowserView(this.view);
|
||||||
};
|
};
|
||||||
|
@@ -65,6 +65,9 @@ jest.mock('main/downloadsManager', () => ({
|
|||||||
onOpen: jest.fn(),
|
onOpen: jest.fn(),
|
||||||
onClose: jest.fn(),
|
onClose: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/mainWindow', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
|
@@ -21,6 +21,7 @@ import Config from 'common/config';
|
|||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
|
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import {getLocalPreload} from 'main/utils';
|
import {getLocalPreload} from 'main/utils';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ export class DownloadsDropdownView {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
performanceMonitor.registerView('DownloadsDropdownView', this.view.webContents);
|
||||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
||||||
MainWindow.get()?.addBrowserView(this.view);
|
MainWindow.get()?.addBrowserView(this.view);
|
||||||
};
|
};
|
||||||
|
@@ -10,7 +10,9 @@ jest.mock('electron', () => ({
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/mainWindow', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
on: 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 {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import {getLocalPreload, getWindowBoundaries} from 'main/utils';
|
import {getLocalPreload, getWindowBoundaries} from 'main/utils';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -86,6 +87,8 @@ export class LoadingScreen {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
}});
|
}});
|
||||||
const localURL = 'mattermost-desktop://renderer/loadingScreen.html';
|
const localURL = 'mattermost-desktop://renderer/loadingScreen.html';
|
||||||
|
|
||||||
|
performanceMonitor.registerView('LoadingScreen', this.view.webContents);
|
||||||
this.view.webContents.loadURL(localURL);
|
this.view.webContents.loadURL(localURL);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -27,6 +27,10 @@ jest.mock('../contextMenu', () => jest.fn());
|
|||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
getWindowBoundaries: jest.fn(),
|
getWindowBoundaries: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
unregisterView: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('main/views/modalView', () => {
|
describe('main/views/modalView', () => {
|
||||||
describe('show', () => {
|
describe('show', () => {
|
||||||
|
@@ -5,6 +5,7 @@ import type {BrowserWindow} from 'electron';
|
|||||||
import {BrowserView} from 'electron';
|
import {BrowserView} from 'electron';
|
||||||
|
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getWindowBoundaries} from '../utils';
|
import {getWindowBoundaries} from '../utils';
|
||||||
@@ -50,6 +51,7 @@ export class ModalView<T, T2> {
|
|||||||
|
|
||||||
this.status = Status.ACTIVE;
|
this.status = Status.ACTIVE;
|
||||||
try {
|
try {
|
||||||
|
performanceMonitor.registerView(`Modal-${key}`, this.view.webContents);
|
||||||
this.view.webContents.loadURL(this.html);
|
this.view.webContents.loadURL(this.html);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log.error('there was an error loading the modal:');
|
this.log.error('there was an error loading the modal:');
|
||||||
@@ -99,6 +101,7 @@ export class ModalView<T, T2> {
|
|||||||
this.view.webContents.closeDevTools();
|
this.view.webContents.closeDevTools();
|
||||||
}
|
}
|
||||||
this.windowAttached.removeBrowserView(this.view);
|
this.windowAttached.removeBrowserView(this.view);
|
||||||
|
performanceMonitor.unregisterView(this.view.webContents.id);
|
||||||
this.view.webContents.close();
|
this.view.webContents.close();
|
||||||
|
|
||||||
delete this.windowAttached;
|
delete this.windowAttached;
|
||||||
|
@@ -29,6 +29,9 @@ jest.mock('electron', () => ({
|
|||||||
getPath: jest.fn(() => '/valid/downloads/path'),
|
getPath: jest.fn(() => '/valid/downloads/path'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/mainWindow', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
|
@@ -22,6 +22,7 @@ import Config from 'common/config';
|
|||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import ServerManager from 'common/servers/serverManager';
|
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 {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 {getLocalPreload} from 'main/utils';
|
||||||
|
|
||||||
import type {UniqueServer} from 'types/config';
|
import type {UniqueServer} from 'types/config';
|
||||||
@@ -83,6 +84,7 @@ export class ServerDropdownView {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
transparent: true,
|
transparent: true,
|
||||||
}});
|
}});
|
||||||
|
performanceMonitor.registerView('ServerDropdownView', this.view.webContents);
|
||||||
this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html');
|
this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html');
|
||||||
|
|
||||||
this.setOrderedServers();
|
this.setOrderedServers();
|
||||||
|
@@ -82,6 +82,9 @@ jest.mock('main/windows/mainWindow', () => ({
|
|||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('common/servers/serverManager', () => ({
|
jest.mock('common/servers/serverManager', () => ({
|
||||||
getOrderedTabsForServer: jest.fn(),
|
getOrderedTabsForServer: jest.fn(),
|
||||||
getAllServers: jest.fn(),
|
getAllServers: jest.fn(),
|
||||||
|
@@ -48,6 +48,7 @@ import {TAB_MESSAGING} from 'common/views/View';
|
|||||||
import {flushCookiesStore} from 'main/app/utils';
|
import {flushCookiesStore} from 'main/app/utils';
|
||||||
import DeveloperMode from 'main/developerMode';
|
import DeveloperMode from 'main/developerMode';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -373,6 +374,7 @@ export class ViewManager {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
}});
|
}});
|
||||||
const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`;
|
const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`;
|
||||||
|
performanceMonitor.registerView('URLView', urlView.webContents);
|
||||||
urlView.webContents.loadURL(localURL);
|
urlView.webContents.loadURL(localURL);
|
||||||
MainWindow.get()?.addBrowserView(urlView);
|
MainWindow.get()?.addBrowserView(urlView);
|
||||||
const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds();
|
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);
|
log.error('Failed to remove URL view', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
performanceMonitor.unregisterView(urlView.webContents.id);
|
||||||
urlView.webContents.close();
|
urlView.webContents.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -67,6 +67,10 @@ jest.mock('main/windows/mainWindow', () => ({
|
|||||||
jest.mock('app/serverViewState', () => ({
|
jest.mock('app/serverViewState', () => ({
|
||||||
switchServer: jest.fn(),
|
switchServer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
unregisterView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/views/viewManager', () => ({
|
jest.mock('main/views/viewManager', () => ({
|
||||||
getView: jest.fn(),
|
getView: jest.fn(),
|
||||||
getViewByWebContentsId: jest.fn(),
|
getViewByWebContentsId: jest.fn(),
|
||||||
@@ -156,6 +160,9 @@ describe('main/windows/callsWidgetWindow', () => {
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
close: jest.fn(),
|
close: jest.fn(),
|
||||||
isDestroyed: jest.fn(),
|
isDestroyed: jest.fn(),
|
||||||
|
webContents: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
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 {CALLS_PLUGIN_ID, MINIMUM_CALLS_WIDGET_HEIGHT, MINIMUM_CALLS_WIDGET_WIDTH} from 'common/utils/constants';
|
||||||
import {getFormattedPathName, isCallsPopOutURL, parseURL} from 'common/utils/url';
|
import {getFormattedPathName, isCallsPopOutURL, parseURL} from 'common/utils/url';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
import {
|
import {
|
||||||
composeUserAgent,
|
composeUserAgent,
|
||||||
@@ -173,6 +174,7 @@ export class CallsWidgetWindow {
|
|||||||
if (!widgetURL) {
|
if (!widgetURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
performanceMonitor.registerView('CallsWidgetWindow', this.win.webContents);
|
||||||
this.win?.loadURL(widgetURL, {
|
this.win?.loadURL(widgetURL, {
|
||||||
userAgent: composeUserAgent(),
|
userAgent: composeUserAgent(),
|
||||||
}).catch((reason) => {
|
}).catch((reason) => {
|
||||||
@@ -195,6 +197,7 @@ export class CallsWidgetWindow {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.win?.on('closed', resolve);
|
this.win?.on('closed', resolve);
|
||||||
|
performanceMonitor.unregisterView(this.win.webContents.id);
|
||||||
this.win?.close();
|
this.win?.close();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -73,6 +73,9 @@ jest.mock('../utils', () => ({
|
|||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/performanceMonitor', () => ({
|
||||||
|
registerView: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('main/windows/mainWindow', () => {
|
describe('main/windows/mainWindow', () => {
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
|
@@ -34,6 +34,7 @@ import Utils from 'common/utils/util';
|
|||||||
import * as Validator from 'common/Validator';
|
import * as Validator from 'common/Validator';
|
||||||
import {boundsInfoPath} from 'main/constants';
|
import {boundsInfoPath} from 'main/constants';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
import performanceMonitor from 'main/performanceMonitor';
|
||||||
|
|
||||||
import type {SavedWindowState} from 'types/mainWindow';
|
import type {SavedWindowState} from 'types/mainWindow';
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ export class MainWindow extends EventEmitter {
|
|||||||
contextMenu.reload();
|
contextMenu.reload();
|
||||||
|
|
||||||
const localURL = 'mattermost-desktop://renderer/index.html';
|
const localURL = 'mattermost-desktop://renderer/index.html';
|
||||||
|
performanceMonitor.registerView('MainWindow', this.win.webContents);
|
||||||
this.win.loadURL(localURL).catch(
|
this.win.loadURL(localURL).catch(
|
||||||
(reason) => {
|
(reason) => {
|
||||||
log.error('failed to load', reason);
|
log.error('failed to load', reason);
|
||||||
|
@@ -67,6 +67,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||||||
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
|
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
|
||||||
logLevelRef: React.RefObject<HTMLSelectElement>;
|
logLevelRef: React.RefObject<HTMLSelectElement>;
|
||||||
appLanguageRef: React.RefObject<HTMLSelectElement>;
|
appLanguageRef: React.RefObject<HTMLSelectElement>;
|
||||||
|
enableMetricsRef: React.RefObject<HTMLInputElement>;
|
||||||
|
|
||||||
saveQueue: SaveQueueItem[];
|
saveQueue: SaveQueueItem[];
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||||||
this.autoCheckForUpdatesRef = React.createRef();
|
this.autoCheckForUpdatesRef = React.createRef();
|
||||||
this.logLevelRef = React.createRef();
|
this.logLevelRef = React.createRef();
|
||||||
this.appLanguageRef = React.createRef();
|
this.appLanguageRef = React.createRef();
|
||||||
|
this.enableMetricsRef = React.createRef();
|
||||||
|
|
||||||
this.saveQueue = [];
|
this.saveQueue = [];
|
||||||
this.selectedSpellCheckerLocales = [];
|
this.selectedSpellCheckerLocales = [];
|
||||||
@@ -218,6 +220,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||||||
}, 2000);
|
}, 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 = () => {
|
handleChangeShowTrayIcon = () => {
|
||||||
const shouldShowTrayIcon = this.showTrayIconRef.current?.checked;
|
const shouldShowTrayIcon = this.showTrayIconRef.current?.checked;
|
||||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
|
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>);
|
</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') {
|
if (window.process.platform === 'win32' || window.process.platform === 'linux') {
|
||||||
options.push(
|
options.push(
|
||||||
<FormCheck>
|
<FormCheck>
|
||||||
|
@@ -59,6 +59,7 @@ export type ConfigV3 = {
|
|||||||
alwaysClose?: boolean;
|
alwaysClose?: boolean;
|
||||||
logLevel?: string;
|
logLevel?: string;
|
||||||
appLanguage?: string;
|
appLanguage?: string;
|
||||||
|
enableMetrics?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigV2 =
|
export type ConfigV2 =
|
||||||
@@ -131,4 +132,5 @@ export type MigrationInfo = {
|
|||||||
updateTrayIconWin32: boolean;
|
updateTrayIconWin32: boolean;
|
||||||
masConfigs: boolean;
|
masConfigs: boolean;
|
||||||
closeExtraTabs: 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-thread', listener: (threadID: string) => void): () => void;
|
||||||
createListener(event: 'calls-widget-open-stop-recording-modal', listener: (channelID: 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: '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