mattermost-mobile/app/managers/performance_metrics_manager/index.ts

172 lines
5.9 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {AppState, type AppStateStatus} from 'react-native';
import performance from 'react-native-performance';
import {logDebug, logInfo, logWarning} from '@utils/log';
import Batcher from './performance_metrics_batcher';
import type {NetworkRequestMetrics} from './constant';
import type {MarkOptions} from 'react-native-performance/lib/typescript/performance';
interface PerformanceMarkWithDetail {
startTime: number;
detail?: string;
}
type Target = 'HOME' | 'CHANNEL' | 'THREAD' | undefined;
type MetricName = 'mobile_channel_switch' |
'mobile_team_switch';
const RETRY_TIME = 100;
const MAX_RETRIES = 3;
class PerformanceMetricsManagerSingleton {
private target: Target;
private batchers: {[serverUrl: string]: Batcher} = {};
private lastAppStateIsActive = AppState.currentState === 'active';
constructor() {
AppState.addEventListener('change', (appState: AppStateStatus) => this.onAppStateChange(appState));
}
private onAppStateChange(appState: AppStateStatus) {
const isAppStateActive = appState === 'active';
if (this.lastAppStateIsActive !== isAppStateActive && !isAppStateActive) {
for (const batcher of Object.values(this.batchers)) {
batcher.forceSend();
}
}
this.lastAppStateIsActive = isAppStateActive;
}
private ensureBatcher(serverUrl: string) {
if (this.batchers[serverUrl]) {
return this.batchers[serverUrl];
}
this.batchers[serverUrl] = new Batcher(serverUrl);
return this.batchers[serverUrl];
}
public setLoadTarget(target: Target) {
this.target = target;
}
public skipLoadMetric() {
RNUtils.setHasRegisteredLoad();
}
public finishLoad(location: Target, serverUrl: string) {
this.finishLoadWithRetries(location, serverUrl, 0);
}
private finishLoadWithRetries(location: Target, serverUrl: string, retries: number) {
if (this.target !== location || RNUtils.getHasRegisteredLoad().hasRegisteredLoad) {
return;
}
try {
const measure = performance.measure('mobile_load', 'nativeLaunchStart');
this.ensureBatcher(serverUrl).addToBatch({
metric: 'mobile_load',
value: measure.duration,
timestamp: Date.now(),
});
performance.clearMeasures('mobile_load');
} catch {
// There seems to be a race condition where in some scenarios, the mobile load
// mark does not exist. We add this to avoid crashes related to this.
if (retries < MAX_RETRIES) {
setTimeout(() => this.finishLoadWithRetries(location, serverUrl, retries + 1), RETRY_TIME);
return;
}
logWarning('We could not retrieve the mobile load metric');
}
RNUtils.setHasRegisteredLoad();
}
public startMetric(metricName: MetricName) {
performance.mark(metricName, {detail: 'startMetric'});
}
public mark(metricName: MetricName, options?: MarkOptions) {
performance.mark(metricName, options);
}
public endMetric(metricName: MetricName, serverUrl: string) {
const marks = performance.getEntriesByName(metricName, 'mark') as PerformanceMarkWithDetail[];
if (!marks.length) {
return;
}
let duration = 0;
if (marks.length === 1) {
const measureName = `measure_${metricName}`;
const measure = performance.measure(measureName, metricName);
duration = measure.duration;
performance.clearMeasures(measureName);
} else {
// get additional details when using mark()s
// measure() returns duration value that is not the same as the diff between the last and first mark
duration = marks[marks.length - 1].startTime - marks[0].startTime;
logInfo(`Performance marks for ${metricName}:`);
for (let i = 1; i < marks.length; i++) {
const markDuration = marks[i].startTime - marks[i - 1].startTime;
const timeStr = markDuration.toFixed(2).padStart(8, ' ');
const markRange = `Mark ${i - 1} => ${i}:`.padEnd(15, ' ');
const details = `[${marks[i - 1].detail || 'no detail'} => ${marks[i].detail || 'no detail'}]`;
logInfo(`${markRange}${timeStr}ms ${details}`);
}
logInfo(`${'Total:'.padEnd(15, ' ')}${duration.toFixed(2).padStart(8, ' ')}ms`);
}
this.ensureBatcher(serverUrl).addToBatch({
metric: metricName,
value: duration,
timestamp: Date.now(),
});
performance.clearMarks(metricName);
}
public startTimeToInteraction(options?: MarkOptions) {
performance.mark('tti', options);
}
public measureTimeToInteraction() {
try {
const result = performance.measure('TTI', 'tti');
performance.clearMarks('tti');
performance.clearMeasures('TTI');
logDebug('Time to Interaction', result.duration);
return result;
} catch {
return undefined;
}
}
public collectNetworkRequestData = (name: NetworkRequestMetrics, value: number, {serverUrl, groupLabel}: NetworkRequestDataOtherInfo) => {
this.ensureBatcher(serverUrl).addToBatch({
metric: name,
value,
timestamp: Date.now(),
label: {network_request_group: groupLabel},
});
};
}
export const testExports = {
PerformanceMetricsManagerSingleton,
RETRY_TIME,
MAX_RETRIES,
};
const PerformanceMetricsManager = new PerformanceMetricsManagerSingleton();
export default PerformanceMetricsManager;