Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/extra/KVDataMemory.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- При старте делает первый замер и отправляет результат.
- Далее повторяет замер через равные интервалы (по умолчанию 5 минут).
- Каждый замер отправляется как KV-событие с ключом `MemoryUsedBytes` (значение в байтах).
- Дополнительная информация (API, источник замера и исходный breakdown) отправляется в `meta` этого события.
- На сервере данные привязаны к `sessionId` и имеют timestamp — этого достаточно для построения графика потребления памяти по времени и обнаружения утечек.

## Источник данных
Expand Down Expand Up @@ -43,6 +44,12 @@ memory.endMonitoring();

## Отправляемые данные

| Ключ | Тип | Описание |
| ----------------- | -------- | --------------------------- |
| `MemoryUsedBytes` | `number` | Потребление памяти в байтах |
| Поле | Тип | Описание |
| ----------------------- | -------- | ---------------------------------------------------------------------- |
| `payload.key` | `string` | Всегда `MemoryUsedBytes` |
| `payload.value` | `number` | Потребление памяти в байтах |
| `metadata.api` | `string` | Короткий идентификатор API: `uasm` / `jsHeap` |
| `metadata.source` | `string` | Источник замера: `MeasureUserAgentSpecificMemory` / `JSHeapUsed` |
| `metadata.rawBreakdown` | `array` | Исходный `breakdown` из `measureUserAgentSpecificMemory` без агрегации |

`metadata` приходит как JSON-строка (стандартный формат транспорта `WebTelemetryKV`).
28 changes: 27 additions & 1 deletion src/extra/KVDataMemory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,26 @@ describe('KVDataMemory', () => {
expect(calls.length).toBe(1);
const data = JSON.parse(calls[0]);
const item = data.find((x: any) => x.key === 'MemoryUsedBytes');
expect(data.length).toBe(1);
expect(item.valueNum).toBe(50_000_000);
const metadata = JSON.parse(item.metadata);
expect(metadata.api).toBe('jsHeap');
expect(metadata.source).toBe('JSHeapUsed');
expect(metadata.rawBreakdown).toEqual([]);

memory.endMonitoring();
});

it('should measure via measureUserAgentSpecificMemory when cross-origin isolated', async () => {
(globalThis as any).crossOriginIsolated = true;
(performance as any).measureUserAgentSpecificMemory = vi.fn().mockResolvedValue({ bytes: 80_000_000 }) as any;
(performance as any).measureUserAgentSpecificMemory = vi.fn().mockResolvedValue({
bytes: 80_000_000,
breakdown: [
{ bytes: 50_000_000, types: ['JavaScript'] },
{ bytes: 10_000_000, types: ['DOM'] },
{ bytes: 20_000_000, types: ['Shared'] },
],
}) as any;

const { transport, calls } = createFakeTransport();
const kv = createKV(transport);
Expand All @@ -65,7 +77,16 @@ describe('KVDataMemory', () => {
expect(calls.length).toBe(1);
const data = JSON.parse(calls[0]);
const item = data.find((x: any) => x.key === 'MemoryUsedBytes');
expect(data.length).toBe(1);
expect(item.valueNum).toBe(80_000_000);
const metadata = JSON.parse(item.metadata);
expect(metadata.api).toBe('uasm');
expect(metadata.source).toBe('MeasureUserAgentSpecificMemory');
expect(metadata.rawBreakdown).toEqual([
{ bytes: 50_000_000, types: ['JavaScript'] },
{ bytes: 10_000_000, types: ['DOM'] },
{ bytes: 20_000_000, types: ['Shared'] },
]);
expect((performance as any).measureUserAgentSpecificMemory).toHaveBeenCalled();

memory.endMonitoring();
Expand All @@ -87,7 +108,12 @@ describe('KVDataMemory', () => {
expect(calls.length).toBe(1);
const data = JSON.parse(calls[0]);
const item = data.find((x: any) => x.key === 'MemoryUsedBytes');
expect(data.length).toBe(1);
expect(item.valueNum).toBe(30_000_000);
const metadata = JSON.parse(item.metadata);
expect(metadata.api).toBe('jsHeap');
expect(metadata.source).toBe('JSHeapUsed');
expect(metadata.rawBreakdown).toEqual([]);

memory.endMonitoring();
});
Expand Down
54 changes: 44 additions & 10 deletions src/extra/KVDataMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,43 @@ import type { KVDataItem, WebTelemetryKVData } from '../types.js';

interface PerformanceWithMemoryAPIs extends Performance {
memory?: { usedJSHeapSize: number };
measureUserAgentSpecificMemory?: () => Promise<{ bytes: number }>;
measureUserAgentSpecificMemory?: () => Promise<MemoryMeasurementResult>;
}

const DEFAULT_INTERVAL_MIN = 5;
type MeasurementSource = 'MeasureUserAgentSpecificMemory' | 'JSHeapUsed';
type MeasurementAPI = 'uasm' | 'jsHeap';

interface MemoryMeasurementBreakdownItem {
bytes: number;
types?: string[];
}

interface MemoryMeasurementResult {
bytes: number;
breakdown?: MemoryMeasurementBreakdownItem[];
}

interface MemoryMeasurementSnapshot {
bytes: number;
api: MeasurementAPI;
source: MeasurementSource;
rawBreakdown: MemoryMeasurementBreakdownItem[];
}

export class KVDataMemory implements WebTelemetryKVData {
private KV: WebTelemetryKV;
private intervalMs: number;
private timerId: ReturnType<typeof setTimeout> | null = null;
private measuring = false;
private lastUsedBytes: number | null = null;
private lastMeasurement: MemoryMeasurementSnapshot | null = null;

constructor(KV: WebTelemetryKV, intervalMinutes = DEFAULT_INTERVAL_MIN) {
this.KV = KV;
this.intervalMs = intervalMinutes * 60 * 1000;
}

private async measure(): Promise<number | null> {
private async measure(): Promise<MemoryMeasurementSnapshot | null> {
const perf = performance as PerformanceWithMemoryAPIs;

if (
Expand All @@ -30,13 +49,23 @@ export class KVDataMemory implements WebTelemetryKVData {
) {
try {
const result = await perf.measureUserAgentSpecificMemory();
return result.bytes;
return {
bytes: result.bytes,
api: 'uasm',
source: 'MeasureUserAgentSpecificMemory',
rawBreakdown: result.breakdown ?? [],
};
// eslint-disable-next-line no-empty
} catch (_e) {}
}

if (perf.memory) {
return perf.memory.usedJSHeapSize;
return {
bytes: perf.memory.usedJSHeapSize,
api: 'jsHeap',
source: 'JSHeapUsed',
rawBreakdown: [],
};
}

return null;
Expand All @@ -50,10 +79,10 @@ export class KVDataMemory implements WebTelemetryKVData {
this.measuring = true;

try {
const bytes = await this.measure();
const measurement = await this.measure();

if (bytes !== null) {
this.lastUsedBytes = bytes;
if (measurement !== null) {
this.lastMeasurement = measurement;
await this.KV.pushListAndSend(this.KVdata());
}
} finally {
Expand Down Expand Up @@ -81,15 +110,20 @@ export class KVDataMemory implements WebTelemetryKVData {
}

KVdata(): KVDataItem[] {
if (this.lastUsedBytes === null) {
if (this.lastMeasurement === null) {
return [];
}

return [
{
payload: {
key: 'MemoryUsedBytes',
value: this.lastUsedBytes,
value: this.lastMeasurement.bytes,
},
meta: {
api: this.lastMeasurement.api,
source: this.lastMeasurement.source,
rawBreakdown: this.lastMeasurement.rawBreakdown,
},
},
];
Expand Down
Loading