-
Notifications
You must be signed in to change notification settings - Fork 564
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
367 additions
and
140 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
Oops, something went wrong.