Skip to content

Commit

Permalink
wip(perf): new perf parser
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed May 22, 2024
1 parent 5e3c858 commit 56be6d6
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 140 deletions.
95 changes: 95 additions & 0 deletions core/components/PerformanceCollector/perfParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { test, expect, it, suite } from 'vitest';
import { arePerfBucketBoundariesValid, parsePerf } from './perfParser';

test('arePerfBucketBoundariesValid', () => {
const fnc = arePerfBucketBoundariesValid;
expect(fnc([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, '+Inf'])).toBe(true);
expect(fnc([])).toBe(false); //length
expect(fnc([1, 2, 3])).toBe(false); //length
expect(fnc([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])).toBe(false); //last item
expect(fnc([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'xx', 12, 13, 14, '+Inf'])).toBe(false); //always number, except last
expect(fnc([1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 12, 13, 14, '+Inf'])).toBe(false); //always increasing
expect(fnc([0.1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 11, 12, 13, 14, '+Inf'])).toBe(false); //always increasing
});

const perfValidExample = `# HELP tickTime Time spent on server ticks
# TYPE tickTime histogram
tickTime_count{name="svNetwork"} 1840805
tickTime_sum{name="svNetwork"} 76.39499999999963
tickTime_bucket{name="svNetwork",le="0.005"} 1840798
tickTime_bucket{name="svNetwork",le="0.01"} 1840804
tickTime_bucket{name="svNetwork",le="0.025"} 1840805
tickTime_bucket{name="svNetwork",le="0.05"} 1840805
tickTime_bucket{name="svNetwork",le="0.075"} 1840805
tickTime_bucket{name="svNetwork",le="0.1"} 1840805
tickTime_bucket{name="svNetwork",le="0.25"} 1840805
tickTime_bucket{name="svNetwork",le="0.5"} 1840805
tickTime_bucket{name="svNetwork",le="0.75"} 1840805
tickTime_bucket{name="svNetwork",le="1"} 1840805
tickTime_bucket{name="svNetwork",le="2.5"} 1840805
tickTime_bucket{name="svNetwork",le="5"} 1840805
tickTime_bucket{name="svNetwork",le="7.5"} 1840805
tickTime_bucket{name="svNetwork",le="10"} 1840805
tickTime_bucket{name="svNetwork",le="+Inf"} 1840805
tickTime_count{name="svSync"} 2268704
tickTime_sum{name="svSync"} 1091.617999988212
tickTime_bucket{name="svSync",le="0.005"} 2267516
tickTime_bucket{name="svSync",le="0.01"} 2268532
tickTime_bucket{name="svSync",le="0.025"} 2268664
tickTime_bucket{name="svSync",le="0.05"} 2268685
tickTime_bucket{name="svSync",le="0.075"} 2268686
tickTime_bucket{name="svSync",le="0.1"} 2268688
tickTime_bucket{name="svSync",le="0.25"} 2268703
tickTime_bucket{name="svSync",le="0.5"} 2268704
tickTime_bucket{name="svSync",le="0.75"} 2268704
tickTime_bucket{name="svSync",le="1"} 2268704
tickTime_bucket{name="svSync",le="2.5"} 2268704
tickTime_bucket{name="svSync",le="5"} 2268704
tickTime_bucket{name="svSync",le="7.5"} 2268704
tickTime_bucket{name="svSync",le="10"} 2268704
tickTime_bucket{name="svSync",le="+Inf"} 2268704
tickTime_count{name="svMain"} 355594
tickTime_sum{name="svMain"} 1330.458999996208
tickTime_bucket{name="svMain",le="0.005"} 299261
tickTime_bucket{name="svMain",le="0.01"} 327819
tickTime_bucket{name="svMain",le="0.025"} 352052
tickTime_bucket{name="svMain",le="0.05"} 354360
tickTime_bucket{name="svMain",le="0.075"} 354808
tickTime_bucket{name="svMain",le="0.1"} 355262
tickTime_bucket{name="svMain",le="0.25"} 355577
tickTime_bucket{name="svMain",le="0.5"} 355591
tickTime_bucket{name="svMain",le="0.75"} 355591
tickTime_bucket{name="svMain",le="1"} 355592
tickTime_bucket{name="svMain",le="2.5"} 355593
tickTime_bucket{name="svMain",le="5"} 355593
tickTime_bucket{name="svMain",le="7.5"} 355593
tickTime_bucket{name="svMain",le="10"} 355593
tickTime_bucket{name="svMain",le="+Inf"} 355594`;

suite('parsePerf', () => {
it('should parse the perf data correctly', () => {
const result = parsePerf(perfValidExample);
expect(result.bucketBoundaries).toEqual([0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, '+Inf']);
expect(result.metrics.svNetwork.count).toBe(1840805);
expect(result.metrics.svSync.count).toBe(2268704);
expect(result.metrics.svMain.count).toBe(355594);
expect(result.metrics.svSync.sum).toBe(1091.617999988212);
expect(result.metrics.svMain.buckets).toEqual([299261, 327819, 352052, 354360, 354808, 355262, 355577, 355591, 355591, 355592, 355593, 355593, 355593, 355593, 355594]);
});

it('should handle bad data', () => {
expect(() => parsePerf(123 as any)).toThrow('string expected');

let targetLine = 'tickTime_bucket{name="svMain",le="10"} 355593';
let perfModifiedExample = perfValidExample.replace(targetLine, '');
expect(() => parsePerf(perfModifiedExample)).toThrow('invalid bucket boundaries');

targetLine = 'tickTime_bucket{name="svNetwork",le="+Inf"} 1840805';
perfModifiedExample = perfValidExample.replace(targetLine, '');
expect(() => parsePerf(perfModifiedExample)).toThrow('invalid threads');

targetLine = 'tickTime_sum{name="svNetwork"} 76.39499999999963';
perfModifiedExample = perfValidExample.replace(targetLine, 'tickTime_sum{name="svNetwork"} ????');
expect(() => parsePerf(perfModifiedExample)).toThrow('invalid threads');
});
});
140 changes: 140 additions & 0 deletions core/components/PerformanceCollector/perfParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { PERF_DATA_BUCKET_COUNT, isValidPerfThreadName, type PerfDataBucketBoundariesType, type PerfDataThreadNamesType } from "./perfSchemas";


//Consts
const REGEX_BUCKET_BOUNDARIE = /le="(\d+(\.\d+)?|\+Inf)"/;
const REGEX_PERF_LINE = /tickTime_(count|sum|bucket)\{name="(svSync|svNetwork|svMain)"(,le="(\d+(\.\d+)?|\+Inf)")?\}\s(\S+)/;

//Types
type ParsedRawPerfType = {
bucketBoundaries: PerfDataBucketBoundariesType,
metrics: Record<PerfDataThreadNamesType, {
count: number,
sum: number,
buckets: number[]
}>
};


/**
* Returns if the given thread name is a valid PerfDataThreadNamesType
*/
export const arePerfBucketBoundariesValid = (boundaries: (number | string)[]): boundaries is PerfDataBucketBoundariesType => {
// Check if the length is correct
if (boundaries.length !== PERF_DATA_BUCKET_COUNT) {
return false;
}

// Check if the last item is +Inf
if (boundaries[boundaries.length - 1] !== '+Inf') {
return false;
}

//Check any value is non-numeric except the last one
if (boundaries.slice(0, -1).some((val) => typeof val === 'string')) {
return false;
}

// Check if the values only increase
for (let i = 1; i < boundaries.length - 1; i++) {
if (boundaries[i] <= boundaries[i - 1]) {
return false;
}
}

return true;
}


/**
* Parses the output of FXServer /perf/ in the proteus format
*/
export const parsePerf = (rawData: string): ParsedRawPerfType => {
if (typeof rawData !== 'string') throw new Error('string expected');
const lines = rawData.trim().split('\n');
const metrics: ParsedRawPerfType['metrics'] = {
svSync: {
count: 0,
sum: 0,
buckets: [],
},
svNetwork: {
count: 0,
sum: 0,
buckets: [],
},
svMain: {
count: 0,
sum: 0,
buckets: [],
},
};

//Extract bucket boundaries
const bucketBoundaries = lines
.filter((line) => line.startsWith('tickTime_bucket{name="svMain"'))
.map((line) => {
const parsed = line.match(REGEX_BUCKET_BOUNDARIE);
if (parsed === null) {
return undefined;
} else if (parsed[1] === '+Inf') {
return '+Inf';
} else {
return parseFloat(parsed[1]);
};
})
.filter((val): val is number | '+Inf' => {
return val !== undefined && (val === '+Inf' || isFinite(val))
}) as PerfDataBucketBoundariesType; //it's alright, will check later
if (!arePerfBucketBoundariesValid(bucketBoundaries)) {
throw new Error('invalid bucket boundaries');
}

//Parse lines
for (const line of lines) {
const parsed = line.match(REGEX_PERF_LINE);
if (parsed === null) continue;
const regType = parsed[1];
const thread = parsed[2];
const bucket = parsed[4];
const value = parsed[6];
if (!isValidPerfThreadName(thread)) continue;

if (regType == 'count') {
const count = parseInt(value);
if (!isNaN(count)) metrics[thread].count = count;
} else if (regType == 'sum') {
const sum = parseFloat(value);
if (!isNaN(sum)) metrics[thread].sum = sum;
} else if (regType == 'bucket') {
//Check if the bucket is correct
const currBucketIndex = metrics[thread].buckets.length;
const lastBucketIndex = PERF_DATA_BUCKET_COUNT - 1;
if (currBucketIndex === lastBucketIndex) {
if (bucket !== '+Inf') {
throw new Error(`unexpected last bucket to be +Inf and got ${bucket}`);
}
} else if (parseFloat(bucket) !== bucketBoundaries[currBucketIndex]) {
throw new Error(`unexpected bucket ${bucket} at position ${currBucketIndex}`);
}
//Add the bucket
metrics[thread].buckets.push(parseInt(value));
}
}

//Check perf validity
const invalid = Object.values(metrics).filter((thread) => {
return (
!Number.isInteger(thread.count)
|| thread.count === 0
|| !Number.isFinite(thread.sum)
|| thread.sum === 0
|| thread.buckets.length !== PERF_DATA_BUCKET_COUNT
);
});
if (invalid.length) {
throw new Error(`${invalid.length} invalid threads in /perf/`);
}

return { bucketBoundaries, metrics };
};
83 changes: 83 additions & 0 deletions core/components/PerformanceCollector/perfSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ValuesType } from 'utility-types';
import * as z from 'zod';


/**
* Consts
*/
export const PERF_DATA_BUCKET_COUNT = 15;
export const PERF_DATA_THREAD_NAMES = ['svNetwork', 'svSync', 'svMain'] as const;
export type PerfDataThreadNamesType = ValuesType<typeof PERF_DATA_THREAD_NAMES>;


/**
* Returns if the given thread name is a valid PerfDataThreadNamesType
*/
export const isValidPerfThreadName = (threadName: string): threadName is PerfDataThreadNamesType => {
return PERF_DATA_THREAD_NAMES.includes(threadName as PerfDataThreadNamesType);
}


/**
* Schemas
*/
export const PerfDataBucketBoundariesSchema = z.array(z.union([
z.number().nonnegative(),
z.literal('+Inf'),
]));


//Last snapshot stuff - only the necessary data to calculate the histogram
export const PerfDataRawThreadDataSchema = z.object({
sum: z.number().positive(), //FIXME: required???
count: z.number().int().positive(), //FIXME: required???
buckets: z.array(z.number().int().nonnegative()).length(PERF_DATA_BUCKET_COUNT),
});

export const PerfDataRawThreadsSchema = z.object({
svSync: PerfDataRawThreadDataSchema,
svNetwork: PerfDataRawThreadDataSchema,
svMain: PerfDataRawThreadDataSchema,
})

export const PerfDataPreviousSchema = z.object({
ts: z.number().int().positive(), //FIXME: required???
mainTickCounter: z.number().int().positive(),
perf: PerfDataRawThreadsSchema,
});


//Snapshot stuff - only the necessary data for the chart
export const PerfDataBucketFreqsSchema = z.array(z.number().nonnegative());

export const PerfDataSnapshotSchema = z.object({
ts: z.number().int().positive(),
skipped: z.boolean(),
players: z.number().int().positive(),
// fxsMemoryUsedMb: z.number().nonnegative(),
// nodeHeapTotalMb: z.number().nonnegative(),
perf: z.object({
svSync: PerfDataBucketFreqsSchema,
svNetwork: PerfDataBucketFreqsSchema,
svMain: PerfDataBucketFreqsSchema,
}),
});


//File schema
export const PerfDataFileSchema = z.object({
version: z.literal(1),
bucketBoundaries: PerfDataBucketBoundariesSchema,
previous: PerfDataPreviousSchema,
log: z.array(PerfDataSnapshotSchema),
});


//Exporting types
export type PerfDataRawThreadDataType = z.infer<typeof PerfDataRawThreadDataSchema>;
export type PerfDataRawThreadsType = z.infer<typeof PerfDataRawThreadsSchema>;
export type PerfDataPreviousType = z.infer<typeof PerfDataPreviousSchema>;
export type PerfDataBucketFreqsType = z.infer<typeof PerfDataBucketFreqsSchema>;
export type PerfDataSnapshotType = z.infer<typeof PerfDataSnapshotSchema>;
export type PerfDataBucketBoundariesType = z.infer<typeof PerfDataBucketBoundariesSchema>;
export type PerfDataFileType = z.infer<typeof PerfDataFileSchema>;
Loading

0 comments on commit 56be6d6

Please sign in to comment.