Skip to content

Commit

Permalink
wip(perf): playerDrop stats with the new format
Browse files Browse the repository at this point in the history
This new format doesn't save all drop reasons, just a MultipleCounter of the categories and crashes
as well as the last 200 unknown crashes, for future analysis if necessary.
  • Loading branch information
tabarra committed May 27, 2024
1 parent 0ae9b08 commit 74e0f23
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 70 deletions.
2 changes: 1 addition & 1 deletion core/components/StatsManager/playerDrop/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
const daysMs = 24 * 60 * 60 * 1000;
export const PDL_CRASH_REASON_CHAR_LIMIT = 512;
export const PDL_UNKNOWN_REASON_CHAR_LIMIT = 320;
export const PDL_UNKNOWN_LIST_SIZE_LIMIT = 1000;
export const PDL_UNKNOWN_LIST_SIZE_LIMIT = 200;
export const PDL_RETENTION = 14 * daysMs;
84 changes: 66 additions & 18 deletions core/components/StatsManager/playerDrop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ const modulename = 'PlayerDropStatsManager';
import fsp from 'node:fs/promises';
import consoleFactory from '@extras/console';
import type TxAdmin from '@core/txAdmin.js';
import { PDLFileSchema, PDLFileType, PDLLogType, PDLServerBootDataSchema } from './playerDropSchemas';
import { PDLFileSchema, PDLFileType, PDLHourlyRawType, PDLHourlyType, PDLServerBootDataSchema } from './playerDropSchemas';
import { classifyDropReason } from './classifyDropReason';
import { parseFxserverVersion } from '@extras/helpers';
import { PDL_RETENTION, PDL_UNKNOWN_LIST_SIZE_LIMIT } from './config';
import { ZodError } from 'zod';
import { getDateHourEnc, parseDateHourEnc } from './playerDropUtils';
import { MultipleCounter } from '../statsUtils';
const console = consoleFactory(modulename);


Expand All @@ -24,10 +26,11 @@ const LOG_DATA_FILE_NAME = 'stats_playerDrop.json';
export default class PlayerDropStatsManager {
readonly #txAdmin: TxAdmin;
private readonly logFilePath: string;
private eventLog: PDLLogType = [];
private eventLog: PDLHourlyType[] = [];
private lastGameVersion: string | undefined;
private lastServerVersion: string | undefined;
private lastResourceList: string[] | undefined;
private lastUnknownReasons: string[] = [];

constructor(txAdmin: TxAdmin) {
this.#txAdmin = txAdmin;
Expand All @@ -36,10 +39,34 @@ export default class PlayerDropStatsManager {
}


/**
* Returns the object of the current hour object in log.
* Creates one if doesn't exist one for the current hour.
*/
private getCurrentLogHourRef() {
const { dateHourTs, dateHourStr } = getDateHourEnc();
const currentHourLog = this.eventLog.find((entry) => entry.hour.dateHourStr === dateHourStr);
if (currentHourLog) return currentHourLog;
const newHourLog: PDLHourlyType = {
hour: {
dateHourTs: dateHourTs,
dateHourStr: dateHourStr,
},
changes: [],
crashes: [],
dropCounts: new MultipleCounter(),
};
this.eventLog.push(newHourLog);
return newHourLog;
}


/**
* Handles receiving the data sent to the logger as soon as the server boots
*/
public handleServerBootData(rawPayload: any) {
const logRef = this.getCurrentLogHourRef();

//Parsing data
const validation = PDLServerBootDataSchema.safeParse(rawPayload);
if (!validation.success) {
Expand All @@ -54,7 +81,7 @@ export default class PlayerDropStatsManager {
if (gameString !== this.lastGameVersion) {
shouldSave = true;
this.lastGameVersion = gameString;
this.eventLog.push({
logRef.changes.push({
ts: Date.now(),
type: 'gameChanged',
newVersion: gameString,
Expand All @@ -67,7 +94,7 @@ export default class PlayerDropStatsManager {
if (fxsVersionString !== this.lastServerVersion) {
shouldSave = true;
this.lastServerVersion = fxsVersionString;
this.eventLog.push({
logRef.changes.push({
ts: Date.now(),
type: 'fxsChanged',
newVersion: fxsVersionString,
Expand All @@ -78,7 +105,7 @@ export default class PlayerDropStatsManager {
if (resources.length) {
if (!this.lastResourceList || !this.lastResourceList.length) {
shouldSave = true;
this.eventLog.push({
logRef.changes.push({
ts: Date.now(),
type: 'resourcesChanged',
resAdded: resources,
Expand All @@ -89,7 +116,7 @@ export default class PlayerDropStatsManager {
const resRemoved = this.lastResourceList.filter(r => !resources.includes(r));
if (resAdded.length || resRemoved.length) {
shouldSave = true;
this.eventLog.push({
logRef.changes.push({
ts: Date.now(),
type: 'resourcesChanged',
resAdded,
Expand All @@ -110,13 +137,16 @@ export default class PlayerDropStatsManager {
* Handles receiving the player drop event
*/
public handlePlayerDrop(reason: string) {
const logRef = this.getCurrentLogHourRef();
const { category, cleanReason } = classifyDropReason(reason);
this.eventLog.push({
ts: Date.now(),
type: 'playerDrop',
category,
reason: cleanReason ?? 'unknown',
});
logRef.dropTypes.count(category);
if (cleanReason) {
if (category === 'crash') {
logRef.crashTypes.count(cleanReason);
} else if (category === 'unknown') {
this.lastUnknownReasons.push(cleanReason);
}
}
this.saveEventLog();
}

Expand All @@ -130,6 +160,7 @@ export default class PlayerDropStatsManager {
this.lastGameVersion = undefined;
this.lastServerVersion = undefined;
this.lastResourceList = undefined;
this.lastUnknownReasons = [];
this.saveEventLog(reason);
}

Expand All @@ -146,8 +177,16 @@ export default class PlayerDropStatsManager {
this.lastGameVersion = statsData.lastGameVersion;
this.lastServerVersion = statsData.lastServerVersion;
this.lastResourceList = statsData.lastResourceList;
this.eventLog = statsData.log;
console.verbose.debug(`Loaded ${this.eventLog.length} player drop events from cache`);
this.lastUnknownReasons = statsData.lastUnknownReasons;
this.eventLog = statsData.log.map((entry): PDLHourlyType => {
return {
hour: parseDateHourEnc(entry.hour),
changes: entry.changes,
dropTypes: new MultipleCounter(entry.dropTypes),
crashTypes: new MultipleCounter(entry.crashTypes),
}
});
console.verbose.debug(`Loaded ${this.eventLog.length} log entries from cache`);
this.optimizeStatsLog();
} catch (error) {
if (error instanceof ZodError) {
Expand All @@ -164,11 +203,12 @@ export default class PlayerDropStatsManager {
* Optimizes the event log by removing old entries
*/
private optimizeStatsLog() {
if (this.eventLog.length > PDL_UNKNOWN_LIST_SIZE_LIMIT) {
this.eventLog = this.eventLog.slice(-PDL_UNKNOWN_LIST_SIZE_LIMIT);
if (this.lastUnknownReasons.length > PDL_UNKNOWN_LIST_SIZE_LIMIT) {
this.lastUnknownReasons = this.lastUnknownReasons.slice(-PDL_UNKNOWN_LIST_SIZE_LIMIT);
}

const maxAge = Date.now() - PDL_RETENTION;
const cutoffIdx = this.eventLog.findIndex((entry) => entry.ts > maxAge);
const cutoffIdx = this.eventLog.findIndex((entry) => entry.hour.dateHourTs > maxAge);
if (cutoffIdx > 0) {
this.eventLog = this.eventLog.slice(cutoffIdx);
}
Expand Down Expand Up @@ -196,7 +236,15 @@ export default class PlayerDropStatsManager {
lastGameVersion: this.lastGameVersion ?? 'unknown',
lastServerVersion: this.lastServerVersion ?? 'unknown',
lastResourceList: this.lastResourceList ?? [],
log: this.eventLog,
lastUnknownReasons: this.lastUnknownReasons,
log: this.eventLog.map((entry): PDLHourlyRawType => {
return {
hour: entry.hour.dateHourStr,
changes: entry.changes,
crashTypes: entry.crashTypes.toArray(),
dropTypes: entry.dropTypes.toArray(),
}
}),
};
await fsp.writeFile(this.logFilePath, JSON.stringify(savePerfData));
} catch (error) {
Expand Down
48 changes: 25 additions & 23 deletions core/components/StatsManager/playerDrop/playerDropSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as z from 'zod';
import type { MultipleCounter } from '../statsUtils';
import { parseDateHourEnc } from './playerDropUtils';
import { DeepReadonly } from 'utility-types';


//Generic schemas
Expand All @@ -15,13 +18,6 @@ export const PDLServerBootDataSchema = z.object({


//Log stuff
export const PDLPlayerDropEventSchema = z.object({
ts: zIntNonNegative,
type: z.literal('playerDrop'),
category: z.string(),
reason: z.string(),
});

export const PDLFxsChangedEventSchema = z.object({
ts: zIntNonNegative,
type: z.literal('fxsChanged'),
Expand All @@ -44,35 +40,41 @@ export const PDLResourcesChangedEventSchema = z.object({
// newVersion: z.string(),
// });

export const PDLHourlyRawSchema = z.object({
hour: z.string(),
changes: z.array(z.union([
PDLFxsChangedEventSchema,
PDLGameChangedEventSchema,
PDLResourcesChangedEventSchema,
// PDLClientChangedEventSchema
])),
dropTypes: z.array(z.tuple([z.string(), z.number()])),
crashTypes: z.array(z.tuple([z.string(), z.number()])),
});

export const PDLFileSchema = z.object({
version: z.literal(1),
emptyReason: z.string().optional(), //If the log is empty, this will be the reason
lastGameVersion: z.string(),
lastServerVersion: z.string(),
lastResourceList: z.array(z.string()),
log: z.array(z.union([
PDLPlayerDropEventSchema,
PDLFxsChangedEventSchema,
PDLGameChangedEventSchema,
PDLResourcesChangedEventSchema,
// PDLClientChangedEventSchema
])),
lastUnknownReasons: z.array(z.string()), //store the last few for potential analysis
log: z.array(PDLHourlyRawSchema),
});



//Exporting types
export type PDLFileType = z.infer<typeof PDLFileSchema>;
export type PDLPlayerDropEventType = z.infer<typeof PDLPlayerDropEventSchema>;
export type PDLHourlyRawType = z.infer<typeof PDLHourlyRawSchema>;
export type PDLFxsChangedEventType = z.infer<typeof PDLFxsChangedEventSchema>;
export type PDLGameChangedEventType = z.infer<typeof PDLGameChangedEventSchema>;
export type PDLResourcesChangedEventType = z.infer<typeof PDLResourcesChangedEventSchema>;
// export type PDLClientChangedEventType = z.infer<typeof PDLClientChangedEventSchema>;
export type PDLLogType = (
PDLPlayerDropEventType
| PDLFxsChangedEventType
| PDLGameChangedEventType
| PDLResourcesChangedEventType
// | PDLClientChangedEventType
)[];
export type PDLHourlyChanges = PDLHourlyRawType['changes'];

export type PDLHourlyType = {
hour: DeepReadonly<ReturnType<typeof parseDateHourEnc>>;
changes: PDLHourlyChanges;
dropTypes: MultipleCounter;
crashTypes: MultipleCounter;
};
17 changes: 17 additions & 0 deletions core/components/StatsManager/playerDrop/playerDropUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const getDateHourEnc = () => {
const now = new Date();
const dateHourTs = now.setUTCMinutes(0, 0, 0);
return {
dateHourTs,
dateHourStr: now.toISOString(),
}
}

export const parseDateHourEnc = (dateHourStr: string) => {
const date = new Date(dateHourStr);
const dateHourTs = date.setUTCMinutes(0, 0, 0);
return {
dateHourTs,
dateHourStr,
}
}
20 changes: 16 additions & 4 deletions core/components/StatsManager/statsUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ suite('MultipleCounter', () => {
});

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', () => {
it('should instantiate with object correctly', () => {
expect(counterWithData.toArray()).toEqual([['a', 1], ['b', 2]]);
expect(counterWithData.toJSON()).toEqual({ a: 1, b: 2 });
});
Expand All @@ -31,10 +30,23 @@ suite('MultipleCounter', () => {
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});
expect(counterWithData.toJSON()).toEqual({ a: 2, b: 3, c: 5 });
counterWithData.clear();
expect(counterWithData.toJSON()).toEqual({});
});

it('should sort the data', () => {
const counter = new MultipleCounter({ a: 3, z: 1, c: 2 });
expect(counter.toSortedKeyObject()).toEqual({ a: 3, c: 2, z: 1 });
expect(counter.toSortedKeyObject(true)).toEqual({ z: 1, c: 2, a: 3 });
expect(counter.toSortedValuesObject()).toEqual({ a: 3, c: 2, z: 1 });
expect(counter.toSortedValuesObject(true)).toEqual({ z: 1, c: 2, a: 3 });
expect(counter.toSortedKeysArray()).toEqual([['a', 3], ['c', 2], ['z', 1]]);
expect(counter.toSortedKeysArray(true)).toEqual([['z', 1], ['c', 2], ['a', 3]]);
expect(counter.toSortedValuesArray()).toEqual([['z', 1], ['c', 2], ['a', 3]]);
expect(counter.toSortedValuesArray(true)).toEqual([['a', 3], ['c', 2], ['z', 1]]);

});
});


Expand Down Expand Up @@ -74,7 +86,7 @@ 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');
Expand Down
Loading

0 comments on commit 74e0f23

Please sign in to comment.