Skip to content

Commit

Permalink
feat(core): reorganizing all stats into a StatsManager
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed May 25, 2024
1 parent 16b21bf commit 265a561
Show file tree
Hide file tree
Showing 30 changed files with 276 additions and 80 deletions.
4 changes: 2 additions & 2 deletions core/components/DiscordBot/interactionCreateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default async (txAdmin: TxAdmin, interaction: Interaction) => {
// console.error(`No handler available for button interaction ${interaction.customId}`);
// return;
// }
// txAdmin.statisticsManager.botCommands.count(???);
// txAdmin.statsManager.txRuntime.botCommands.count(???);
// //Executes interaction
// try {
// return await handler.execute(interaction, args, txChungus);
Expand All @@ -64,7 +64,7 @@ export default async (txAdmin: TxAdmin, interaction: Interaction) => {
noHandlerResponse(interaction).catch((e) => {});
return;
}
txAdmin.statisticsManager.botCommands.count(interaction.commandName);
txAdmin.statsManager.txRuntime.botCommands.count(interaction.commandName);

//Executes interaction
try {
Expand Down
2 changes: 1 addition & 1 deletion core/components/FxRunner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export default class FXRunner {
}
globals.resourcesManager.handleServerStop();
globals.playerlistManager.handleServerStop(this.currentMutex);
globals.performanceCollector.logServerClose(reasonString);
globals.statsManager.svRuntime.logServerClose(reasonString);
return null;
} catch (error) {
const msg = "Couldn't kill the server. Perhaps What Is Dead May Never Die.";
Expand Down
2 changes: 1 addition & 1 deletion core/components/FxRunner/outputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default class OutputHandler {
} else if (data.payload.type === 'txAdminLogData') {
this.#txAdmin.logger.server.write(data.payload.logs, mutex);
} else if (data.payload.type === 'txAdminLogNodeHeap') {
this.#txAdmin.performanceCollector.logServerNodeMemory(data.payload);
this.#txAdmin.statsManager.svRuntime.logServerNodeMemory(data.payload);
} else if (data.payload.type === 'txAdminResourceEvent') {
this.#txAdmin.resourcesManager.handleServerEvents(data.payload, mutex);
} else if (data.payload.type === 'txAdminPlayerlistEvent') {
Expand Down
14 changes: 7 additions & 7 deletions core/components/HealthMonitor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ export default class HealthMonitor {
this.setCurrentStatus('ONLINE');
if (this.hasServerStartedYet == false) {
this.hasServerStartedYet = true;
globals.statisticsManager.registerFxserverBoot(processUptime);
globals.performanceCollector.logServerBoot(processUptime);
globals.statsManager.txRuntime.registerFxserverBoot(processUptime);
globals.statsManager.svRuntime.logServerBoot(processUptime);
}
return;
}
Expand All @@ -230,7 +230,7 @@ export default class HealthMonitor {
//Check if fxChild is closed, in this case no need to wait the failure count
const processStatus = globals.fxRunner.getStatus();
if (processStatus == 'closed') {
globals.statisticsManager.registerFxserverRestart('close');
globals.statsManager.txRuntime.registerFxserverRestart('close');
this.restartFXServer(
'server close detected',
globals.translator.t('restarter.crash_detected'),
Expand Down Expand Up @@ -322,13 +322,13 @@ export default class HealthMonitor {
}
} else if (elapsedHealthCheck > this.hardConfigs.healthCheck.failLimit) {
//FIXME: se der hang tanto HB quanto HC, ele ainda sim cai nesse caso
globals.statisticsManager.registerFxserverRestart('healthCheck');
globals.statsManager.txRuntime.registerFxserverRestart('healthCheck');
this.restartFXServer(
'server partial hang detected',
globals.translator.t('restarter.hang_detected'),
);
} else {
globals.statisticsManager.registerFxserverRestart('heartBeat');
globals.statsManager.txRuntime.registerFxserverRestart('heartBeat');
this.restartFXServer(
'server hang detected',
globals.translator.t('restarter.hang_detected'),
Expand All @@ -347,7 +347,7 @@ export default class HealthMonitor {
&& tsNow - this.lastSuccessfulHTTPHeartBeat > 15
&& tsNow - this.lastSuccessfulFD3HeartBeat < 5
) {
globals.statisticsManager.registerFxserverRestart('http');
globals.statsManager.txRuntime.registerFxserverRestart('http');
}
this.lastSuccessfulFD3HeartBeat = tsNow;
} else if (source === 'http') {
Expand All @@ -356,7 +356,7 @@ export default class HealthMonitor {
&& tsNow - this.lastSuccessfulFD3HeartBeat > 15
&& tsNow - this.lastSuccessfulHTTPHeartBeat < 5
) {
globals.statisticsManager.registerFxserverRestart('fd3');
globals.statsManager.txRuntime.registerFxserverRestart('fd3');
}
this.lastSuccessfulHTTPHeartBeat = tsNow;
}
Expand Down
2 changes: 1 addition & 1 deletion core/components/Logger/handlers/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export default class ServerLogger extends LoggerBase {
: 'Debug Message: unknown';

} else if (eventData.type === 'MenuEvent') {
globals?.statisticsManager.menuCommands.count(eventData.data?.action ?? 'unknown');
globals?.statsManager.txRuntime.menuCommands.count(eventData.data?.action ?? 'unknown');
eventMessage = (typeof eventData.data.message === 'string')
? `${eventData.data.message}`
: 'did unknown action';
Expand Down
2 changes: 1 addition & 1 deletion core/components/PlayerDatabase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DatabaseActionType, DatabaseDataType, DatabasePlayerType, DatabaseWhite
import { cloneDeep } from 'lodash-es';
import { now } from '@core/extras/helpers';
import consoleFactory from '@extras/console';
import { MultipleCounter } from '../StatisticsManager/statsUtils';
import { MultipleCounter } from '@core/components/StatsManager/statsUtils';
const console = consoleFactory(modulename);


Expand Down
1 change: 0 additions & 1 deletion core/components/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ export default class Scheduler {
string: scheduledString,
minuteFloorTs: scheduledMinuteFloorTs,
};
console.dir(this.nextTempSchedule);

//This is needed to refresh this.calculatedNextRestartMinuteFloorTs
this.checkSchedule();
Expand Down
25 changes: 25 additions & 0 deletions core/components/StatsManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const modulename = 'StatsManager';
import consoleFactory from '@extras/console';
import type TxAdmin from '@core/txAdmin.js';
import SvRuntimeStatsManager from './svRuntime';
import TxRuntimeStatsManager from './txRuntime';
import PlayerDropStatsManager from './playerDrop';
const console = consoleFactory(modulename);


/**
* Module responsible to collect statistics and data.
* It is broken down into sub-modules for each specific area.
* NOTE: yes, this could just have been an object, but who cares.
*/
export default class StatsManager {
public readonly svRuntime: SvRuntimeStatsManager;
public readonly txRuntime: TxRuntimeStatsManager;
public readonly playerDrop: PlayerDropStatsManager;

constructor(txAdmin: TxAdmin) {
this.svRuntime = new SvRuntimeStatsManager(txAdmin);
this.txRuntime = new TxRuntimeStatsManager(txAdmin);
this.playerDrop = new PlayerDropStatsManager(txAdmin);
}
};
76 changes: 76 additions & 0 deletions core/components/StatsManager/playerDrop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const modulename = 'PlayerDropStatsManager';
import consoleFactory from '@extras/console';
import type TxAdmin from '@core/txAdmin.js';
const console = consoleFactory(modulename);


//Consts
const STATS_DATA_FILE_VERSION = 1;
const STATS_DATA_FILE_NAME = 'stats_playerDrop.json';


/**
* FIXME:
*/
export default class PlayerDropStatsManager {
readonly #txAdmin: TxAdmin;
private readonly statsDataPath: string;

constructor(txAdmin: TxAdmin) {
this.#txAdmin = txAdmin;
this.statsDataPath = `${txAdmin.info.serverProfilePath}/data/${STATS_DATA_FILE_NAME}`;
this.loadStatsHistory();
}


/**
* Loads the stats database/cache/history
*/
async loadStatsHistory() {
// try {
// const rawFileData = await fsp.readFile(this.statsDataPath, 'utf8');
// const fileData = JSON.parse(rawFileData);
// if (fileData?.version !== STATS_DATA_FILE_VERSION) throw new Error('invalid version');
// const statsData = SSFileSchema.parse(fileData);
// this.lastPerfBoundaries = statsData.lastPerfBoundaries;
// this.statsLog = statsData.log;
// this.resetPerfState();
// console.verbose.debug(`Loaded ${this.statsLog.length} performance snapshots from cache`);
// await optimizeStatsLog(this.statsLog);
// } catch (error) {
// if (error instanceof ZodError) {
// console.warn(`Failed to load ${STATS_DATA_FILE_NAME} due to invalid data.`);
// console.warn('Since this is not a critical file, it will be reset.');
// } else {
// console.warn(`Failed to load ${STATS_DATA_FILE_NAME} with message: ${(error as Error).message}`);
// console.warn('Since this is not a critical file, it will be reset.');
// }
// }
}


/**
* Saves the stats database/cache/history
*/
async saveStatsHistory() {
try {
// await optimizeStatsLog(this.statsLog);
// const savePerfData: SSFileType = {
// version: STATS_DATA_FILE_VERSION,
// lastPerfBoundaries: this.lastPerfBoundaries,
// log: this.statsLog,
// };
// await fsp.writeFile(this.statsDataPath, JSON.stringify(savePerfData));
} catch (error) {
console.warn(`Failed to save ${STATS_DATA_FILE_NAME} with message: ${(error as Error).message}`);
}
}


/**
* FIXME:
*/
getServerPerfSummary() {
//FIXME:
}
};
89 changes: 89 additions & 0 deletions core/components/StatsManager/statsUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { test, expect, suite, it } from 'vitest';
import { MultipleCounter, QuantileArray, TimeCounter } from './statsUtils';


suite('MultipleCounter', () => {
it('should instantiate empty correctly', () => {
const counter = new MultipleCounter();
expect(counter).toBeInstanceOf(MultipleCounter);
expect(counter.toArray()).toEqual([]);
expect(counter.toJSON()).toEqual({});
});

it('should instantiate locked if specified', () => {
const lockedCounter = new MultipleCounter(undefined, true);
expect(() => lockedCounter.count('a')).toThrowError('is locked');
expect(() => lockedCounter.clear()).toThrowError('is locked');
});

it('handle instantiation data error', () => {
expect(() => new MultipleCounter(null as any)).toThrowError('must be an iterable');
expect(() => new MultipleCounter({ a: 'b' as any })).toThrowError('only integer');
});

const counterWithData = new MultipleCounter({ a: 1, b: 2 });
it('should instantiate with data correctly', () => {
expect(counterWithData.toArray()).toEqual([['a', 1], ['b', 2]]);
expect(counterWithData.toJSON()).toEqual({ a: 1, b: 2 });
});
it('should count and clear', () => {
counterWithData.count('a');
expect(counterWithData.toJSON()).toEqual({ a: 2, b: 2 });
counterWithData.count('b');
counterWithData.count('c', 5);
expect(counterWithData.toJSON()).toEqual({ a: 2, b: 3, c: 5});
counterWithData.clear();
expect(counterWithData.toJSON()).toEqual({});
});
});


test('QuantileArray', () => {
const array = new QuantileArray(4, 2);
array.count(0);
expect(array.result()).toEqual({ notEnoughData: true });
array.count(0);
array.count(0);
array.count(0);
expect(array.result()).toEqual({
count: 4,
q5: 0,
q25: 0,
q50: 0,
q75: 0,
q95: 0,
});

array.count(1);
array.count(1);
expect(array.result()).toEqual({
count: 4,
q5: 0,
q25: 0,
q50: 0.5,
q75: 1,
q95: 1,
});

array.clear();
expect(array.result()).toEqual({ notEnoughData: true });
});


test('TimeCounter', async () => {
const counter = new TimeCounter();
await new Promise((resolve) => setTimeout(resolve, 150));
const duration = counter.stop();

// Check if the duration is a valid object
expect(duration.seconds).toBeTypeOf('number');
expect(duration.milliseconds).toBeTypeOf('number');
expect(duration.nanoseconds).toBeTypeOf('number');
expect(counter.toJSON()).toEqual(duration);

// Check if the duration is within the expected range
const isCloseTo50ms = (x: number) => (x > 150 && x < 175);
expect(duration.seconds * 1000).toSatisfy(isCloseTo50ms);
expect(duration.milliseconds).toSatisfy(isCloseTo50ms);
expect(duration.nanoseconds / 1_000_000).toSatisfy(isCloseTo50ms);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inspect } from 'node:util';
import CircularBuffer from 'mnemonist/circular-buffer';
import * as d3array from 'd3-array';


//Output types
export type MultipleCounterOutput = Record<string, number>;
export type QuantileArrayOutput = {
Expand All @@ -15,6 +16,7 @@ export type QuantileArrayOutput = {
notEnoughData: true;
}; //if less than min size


/**
* Helper class to count different options
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
const modulename = 'PerformanceCollector';
const modulename = 'SvRuntimeStatsManager';
import fsp from 'node:fs/promises';
import * as d3array from 'd3-array';
import consoleFactory from '@extras/console';
import type TxAdmin from '@core/txAdmin.js';
import { LogNodeHeapEventSchema, SSFileSchema, isSSLogDataType } from './perfSchemas';
import type { LogNodeHeapEventType, SSFileType, SSLogDataType, SSLogType, SSPerfBoundariesType, SSPerfCountsType } from './perfSchemas';
import { diffPerfs, fetchFxsMemory, fetchRawPerfData, perfCountsToHist } from './perfUtils';
import { optimizeStatsLog } from './statsLogOptimizer';
import { optimizeSvRuntimeLog } from './logOptimizer';
import { convars } from '@core/globalData';
import { ZodError } from 'zod';
import { PERF_DATA_BUCKET_COUNT, PERF_DATA_INITIAL_RESOLUTION, PERF_DATA_MIN_TICKS } from './statsConfigs';
import { PERF_DATA_BUCKET_COUNT, PERF_DATA_INITIAL_RESOLUTION, PERF_DATA_MIN_TICKS } from './config';
const console = consoleFactory(modulename);


//Consts
const megabyte = 1024 * 1024;
const STATS_DATA_FILE_VERSION = 1;
const STATS_DATA_FILE_NAME = 'statsData.json';
const STATS_DATA_FILE_NAME = 'stats_svRuntime.json';


/**
* This module is reponsiple to collect many statistics from the server.
* This module is reponsiple to collect many statistics from the server runtime
* Most of those will be displayed on the Dashboard.
*/
export default class PerformanceCollector {
export default class SvRuntimeStatsManager {
readonly #txAdmin: TxAdmin;
private readonly statsDataPath: string;
private statsLog: SSLogType = [];
Expand Down Expand Up @@ -254,7 +254,7 @@ export default class PerformanceCollector {
this.statsLog = statsData.log;
this.resetPerfState();
console.verbose.debug(`Loaded ${this.statsLog.length} performance snapshots from cache`);
await optimizeStatsLog(this.statsLog);
await optimizeSvRuntimeLog(this.statsLog);
} catch (error) {
if (error instanceof ZodError) {
console.warn(`Failed to load ${STATS_DATA_FILE_NAME} due to invalid data.`);
Expand All @@ -272,7 +272,7 @@ export default class PerformanceCollector {
*/
async saveStatsHistory() {
try {
await optimizeStatsLog(this.statsLog);
await optimizeSvRuntimeLog(this.statsLog);
const savePerfData: SSFileType = {
version: STATS_DATA_FILE_VERSION,
lastPerfBoundaries: this.lastPerfBoundaries,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { STATS_LOG_SIZE_LIMIT, STATS_RESOLUTION_TABLE } from "./statsConfigs";
import { STATS_LOG_SIZE_LIMIT, STATS_RESOLUTION_TABLE } from "./config";
import type { SSLogType } from "./perfSchemas";

//Consts
Expand All @@ -8,7 +8,7 @@ const YIELD_INTERVAL = 100;
/**
* Optimizes (in place) the stats log by removing old data and combining snaps to match the resolution
*/
export const optimizeStatsLog = async (statsLog: SSLogType) => {
export const optimizeSvRuntimeLog = async (statsLog: SSLogType) => {
statsLog.splice(0, statsLog.length - STATS_LOG_SIZE_LIMIT);
for (let i = 0; i < statsLog.length; i++) {
//FIXME: write code
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PERF_DATA_BUCKET_COUNT } from "./statsConfigs";
import { PERF_DATA_BUCKET_COUNT } from "./config";
import { isValidPerfThreadName, type SSPerfBoundariesType, type SSPerfCountsType } from "./perfSchemas";


Expand Down
Loading

0 comments on commit 265a561

Please sign in to comment.