diff --git a/src/extra/KVDataMemory.md b/src/extra/KVDataMemory.md index 3f8a7bb..a17f613 100644 --- a/src/extra/KVDataMemory.md +++ b/src/extra/KVDataMemory.md @@ -7,6 +7,7 @@ - При старте делает первый замер и отправляет результат. - Далее повторяет замер через равные интервалы (по умолчанию 5 минут). - Каждый замер отправляется как KV-событие с ключом `MemoryUsedBytes` (значение в байтах). +- Дополнительная информация (API, источник замера и исходный breakdown) отправляется в `meta` этого события. - На сервере данные привязаны к `sessionId` и имеют timestamp — этого достаточно для построения графика потребления памяти по времени и обнаружения утечек. ## Источник данных @@ -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`). diff --git a/src/extra/KVDataMemory.spec.ts b/src/extra/KVDataMemory.spec.ts index a5e6f62..da174e0 100644 --- a/src/extra/KVDataMemory.spec.ts +++ b/src/extra/KVDataMemory.spec.ts @@ -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); @@ -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(); @@ -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(); }); diff --git a/src/extra/KVDataMemory.ts b/src/extra/KVDataMemory.ts index 079747c..2c492ff 100644 --- a/src/extra/KVDataMemory.ts +++ b/src/extra/KVDataMemory.ts @@ -3,24 +3,43 @@ import type { KVDataItem, WebTelemetryKVData } from '../types.js'; interface PerformanceWithMemoryAPIs extends Performance { memory?: { usedJSHeapSize: number }; - measureUserAgentSpecificMemory?: () => Promise<{ bytes: number }>; + measureUserAgentSpecificMemory?: () => Promise; } 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 | 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 { + private async measure(): Promise { const perf = performance as PerformanceWithMemoryAPIs; if ( @@ -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; @@ -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 { @@ -81,7 +110,7 @@ export class KVDataMemory implements WebTelemetryKVData { } KVdata(): KVDataItem[] { - if (this.lastUsedBytes === null) { + if (this.lastMeasurement === null) { return []; } @@ -89,7 +118,12 @@ export class KVDataMemory implements WebTelemetryKVData { { payload: { key: 'MemoryUsedBytes', - value: this.lastUsedBytes, + value: this.lastMeasurement.bytes, + }, + meta: { + api: this.lastMeasurement.api, + source: this.lastMeasurement.source, + rawBreakdown: this.lastMeasurement.rawBreakdown, }, }, ];