mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-26 17:14:39 +02:00
* docs: design backend diagnostics memory noise handling * chore(gh): tweak workflow * docs: clarify shared heap snapshot reporting * docs: plan backend diagnostics memory noise handling * chore: ignore local worktrees * feat(diagnostics): add independent delta summary * feat(diagnostics): make heap snapshot deltas noise-aware * docs: correct heap snapshot threshold plan * fix(diagnostics): suppress backend memory noise * ci: refine backend diagnostics sampling * test(diagnostics): address final review * wip * Update heap-snapshot-sampling.ts * wip * wip * docs: design frontend metric noise reporting * docs: plan frontend metric noise reporting * fix(diagnostics): suppress frontend metric noise * docs: remove temporary frontend metric plans * test(diagnostics): cover frontend metric thresholds * docs: design shared diagnostics metric table * docs: plan shared diagnostics metric table * refactor(diagnostics): simplify independent delta statistics * feat(diagnostics): add shared metric table renderer * refactor(diagnostics): use shared heap snapshot table * refactor(diagnostics): use shared backend metric table * refactor(diagnostics): use shared frontend metric table * docs: remove temporary metric table plans * tweak * docs: design metric table no-data output * fix(diagnostics): show marker for empty metric tables * chore: remove temporary diagnostics design
195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
finiteMedian,
|
|
independentDeltaSummary,
|
|
isOutsideObservedNoise,
|
|
mad,
|
|
median,
|
|
pairedDeltaSummary,
|
|
sampleSpread,
|
|
type IndependentDeltaSummary,
|
|
} from '../src/stats';
|
|
|
|
describe('median', () => {
|
|
test('takes the middle value of an odd-length sample', () => {
|
|
expect(median([3, 1, 2])).toBe(2);
|
|
});
|
|
|
|
// 偶数長では平均を整数に丸める。KiB単位の整数値を扱う前提のため
|
|
test('rounds the average of an even-length sample', () => {
|
|
expect(median([1, 2])).toBe(2);
|
|
expect(median([1, 4])).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('mad', () => {
|
|
test('measures the median absolute deviation', () => {
|
|
expect(mad([1, 1, 1])).toBe(0);
|
|
expect(mad([1, 2, 3])).toBe(1);
|
|
});
|
|
|
|
test('refuses to compute from a single sample', () => {
|
|
expect(() => mad([1])).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('finiteMedian', () => {
|
|
test('ignores non-finite entries', () => {
|
|
expect(finiteMedian([1, null, undefined, Number.NaN, 3])).toBe(2);
|
|
});
|
|
|
|
test('returns null by default when nothing is finite', () => {
|
|
expect(finiteMedian([null, undefined])).toBeNull();
|
|
});
|
|
|
|
test('returns the supplied default when nothing is finite', () => {
|
|
expect(finiteMedian([null, undefined], 0)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('sampleSpread', () => {
|
|
test('needs at least two finite samples', () => {
|
|
expect(sampleSpread([1])).toBeNull();
|
|
expect(sampleSpread([1, null])).toBeNull();
|
|
expect(sampleSpread([1, 3])).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('pairedDeltaSummary', () => {
|
|
const base = [
|
|
{ round: 1, value: 100 },
|
|
{ round: 2, value: 200 },
|
|
{ round: 3, value: 300 },
|
|
];
|
|
|
|
test('compares base and head within the same round', () => {
|
|
const head = [
|
|
{ round: 1, value: 110 },
|
|
{ round: 2, value: 230 },
|
|
{ round: 3, value: 320 },
|
|
];
|
|
|
|
expect(pairedDeltaSummary(base, head, sample => sample.value)).toStrictEqual({
|
|
median: 20,
|
|
mad: 10,
|
|
min: 10,
|
|
max: 30,
|
|
samples: 3,
|
|
});
|
|
});
|
|
|
|
test('drops rounds that only one side has', () => {
|
|
const head = [
|
|
{ round: 1, value: 110 },
|
|
{ round: 2, value: 230 },
|
|
{ round: 9, value: 999 },
|
|
];
|
|
|
|
expect(pairedDeltaSummary(base, head, sample => sample.value).samples).toBe(2);
|
|
});
|
|
|
|
// 1サンプルでも中央値・最小・最大は定まる (偏差は常に0)
|
|
test('summarizes a single paired round without treating MAD as an error', () => {
|
|
expect(pairedDeltaSummary([base[0]], [{ round: 1, value: 130 }], sample => sample.value)).toStrictEqual({
|
|
median: 30,
|
|
mad: 0,
|
|
min: 30,
|
|
max: 30,
|
|
samples: 1,
|
|
});
|
|
});
|
|
|
|
test('fails loudly when no round is shared', () => {
|
|
expect(() => pairedDeltaSummary(base, [{ round: 9, value: 1 }], sample => sample.value)).toThrow(/no rounds in common/);
|
|
});
|
|
|
|
// 負のroundはwarmupを表すので集計に混ぜない
|
|
test('ignores warmup rounds', () => {
|
|
const warmupBase = [{ round: -1, value: 0 }, ...base];
|
|
const warmupHead = [{ round: -1, value: 9999 }, { round: 1, value: 110 }, { round: 2, value: 230 }, { round: 3, value: 320 }];
|
|
|
|
expect(pairedDeltaSummary(warmupBase, warmupHead, sample => sample.value).samples).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('independentDeltaSummary', () => {
|
|
const base = [290_000, 292_900, 295_800, 298_700, 301_600]
|
|
.map((value, index) => ({ round: index + 1, value }));
|
|
const head = [292_900, 296_300, 298_700, 301_600, 290_000]
|
|
.map((value, index) => ({ round: index + 1, value }));
|
|
|
|
test('uses the difference of independent medians instead of the paired median', () => {
|
|
expect(pairedDeltaSummary(base, head, sample => sample.value).median).toBe(2_900);
|
|
|
|
const summary = independentDeltaSummary(base, head, sample => sample.value);
|
|
expect(summary).toMatchObject({
|
|
baseMedian: 295_800,
|
|
headMedian: 296_300,
|
|
delta: 500,
|
|
baseMad: 2_900,
|
|
headMad: 3_400,
|
|
baseSamples: 5,
|
|
headSamples: 5,
|
|
});
|
|
expect(summary.combinedMad).toBeCloseTo(Math.hypot(2_900, 3_400));
|
|
});
|
|
|
|
test('allows unequal sample counts', () => {
|
|
const summary = independentDeltaSummary(
|
|
[{ value: 10 }, { value: 20 }],
|
|
[{ value: 20 }, { value: 30 }, { value: 40 }],
|
|
sample => sample.value,
|
|
);
|
|
|
|
expect(summary).toStrictEqual({
|
|
baseMedian: 15,
|
|
headMedian: 30,
|
|
delta: 15,
|
|
baseMad: 5,
|
|
headMad: 10,
|
|
combinedMad: Math.hypot(5, 10),
|
|
baseSamples: 2,
|
|
headSamples: 3,
|
|
});
|
|
});
|
|
|
|
test('throws when either side has fewer than two samples', () => {
|
|
expect(() => independentDeltaSummary(
|
|
[{ value: 10 }],
|
|
[{ value: 20 }, { value: 20 }],
|
|
sample => sample.value,
|
|
)).toThrow('At least two samples per side are required');
|
|
|
|
expect(() => independentDeltaSummary(
|
|
[{ value: 10 }, { value: 10 }],
|
|
[{ value: 20 }],
|
|
sample => sample.value,
|
|
)).toThrow('At least two samples per side are required');
|
|
});
|
|
|
|
test('treats exactly three combined MADs as noise and only larger deltas as outside noise', () => {
|
|
function summary(delta: number, combinedMad: number): IndependentDeltaSummary {
|
|
return {
|
|
baseMedian: 100,
|
|
headMedian: 100 + delta,
|
|
delta,
|
|
baseMad: 0,
|
|
headMad: combinedMad,
|
|
combinedMad,
|
|
baseSamples: 3,
|
|
headSamples: 3,
|
|
};
|
|
}
|
|
|
|
expect(isOutsideObservedNoise(summary(29, 10))).toBe(false);
|
|
expect(isOutsideObservedNoise(summary(30, 10))).toBe(false);
|
|
expect(isOutsideObservedNoise(summary(31, 10))).toBe(true);
|
|
expect(isOutsideObservedNoise(summary(-31, 10))).toBe(true);
|
|
});
|
|
});
|