172 lines
5.9 KiB
TypeScript
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;
|