diff --git a/CHANGELOG.md b/CHANGELOG.md index bce2cd6..2806f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +### v1.1.1 (GC Deprecation Fix) + +- **Fix Node DEP0152 deprecation warning in GC monitoring** - `SystemMetricsCollector._startGCMonitoring()` now reads GC kind from `entry.detail.kind` (with fallback to `entry.kind` for older Node versions) instead of the deprecated `entry.kind` accessor. Also fixes GC pause-time accounting: kinds are numeric constants (`NODE_PERFORMANCE_GC_MAJOR`, etc.), not string labels like `'major'` / `'minor'`. + ### v1.1.0 (Mergebox RAM Residency Collector) - **New `MergeboxCollector`** - Measures Meteor's MERGEBOX RAM residency (the per-session, server-side cache of published documents) and posts per-(publication, collection) rollups to `POST /api/v1/metrics/mergebox`. The collector walks `Meteor.server.sessions` read-only, estimates the resident bytes each session's mergebox holds per published collection (sizing each `SessionDocumentView.dataByKey` field value directly), reads the publication strategy via `Meteor.server.getPublicationStrategy()` (reverse-mapped to all four Meteor strategies — `SERVER_MERGE` / `NO_MERGE` / `NO_MERGE_NO_HISTORY` / `NO_MERGE_MULTI`; `unknown` only when the strategy genuinely can't be read), and attributes residency to subscriptions via a pure even-split across `existsIn`. The even-split is sum-preserving: the rows for a collection sum back to that collection's true residency. `connectionCount` is a count of distinct DDP sessions (never a list of connection ids). diff --git a/lib/collectors/SystemMetricsCollector.js b/lib/collectors/SystemMetricsCollector.js index 8167993..7f327ec 100644 --- a/lib/collectors/SystemMetricsCollector.js +++ b/lib/collectors/SystemMetricsCollector.js @@ -3,14 +3,20 @@ import os from "os"; import fs from "fs/promises"; import { exec } from "child_process"; import { promisify } from "util"; -import { PerformanceObserver, performance } from "perf_hooks"; +import { PerformanceObserver, performance, constants } from "perf_hooks"; import v8 from "v8"; // Promisified exec for non-blocking command execution const execAsync = promisify(exec); +const { + NODE_PERFORMANCE_GC_MAJOR, + NODE_PERFORMANCE_GC_MINOR, + NODE_PERFORMANCE_GC_INCREMENTAL, +} = constants; + // Agent version - must be updated alongside package.js on each release -const AGENT_VERSION = '1.1.0'; +const AGENT_VERSION = '1.1.1'; // cgroup v1 "unlimited" sentinel: values >= 2^62 mean no limit is set const CGROUP_V1_UNLIMITED = 2 ** 62; @@ -937,6 +943,36 @@ export default class SystemMetricsCollector { return null; } + /** + * Resolve GC kind from a performance entry (Node DEP0152: use detail.kind). + * @param {PerformanceEntry} entry + * @returns {number|undefined} + * @private + */ + _getGcKind(entry) { + return entry.detail?.kind ?? entry.kind; + } + + /** + * Accumulate GC stats from a single performance entry. + * @param {PerformanceEntry} entry + * @private + */ + _processGcEntry(entry) { + const kind = this._getGcKind(entry); + + this.gcStats.count++; + this.gcStats.totalDuration += entry.duration; + + if (kind === NODE_PERFORMANCE_GC_MAJOR || kind === NODE_PERFORMANCE_GC_MINOR) { + this.gcStats.totalPauseTime += entry.duration; + } else if (kind === NODE_PERFORMANCE_GC_INCREMENTAL) { + this.gcStats.totalPauseTime += entry.duration * 0.15; + } else { + this.gcStats.totalPauseTime += entry.duration * 0.5; + } + } + /** * Start monitoring garbage collection events * Uses Node.js PerformanceObserver to track GC activity @@ -947,26 +983,7 @@ export default class SystemMetricsCollector { // Create a PerformanceObserver to watch for GC events this.gcObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); - - entries.forEach((entry) => { - // entry.kind: 'major', 'minor', 'incremental', 'weakcb', etc. - // entry.duration: Duration in milliseconds - - this.gcStats.count++; - this.gcStats.totalDuration += entry.duration; - - // For pause time, we use the duration since GC pauses the main thread - // Some GC types are incremental and have lower pause times - if (entry.kind === 'major' || entry.kind === 'minor') { - this.gcStats.totalPauseTime += entry.duration; - } else if (entry.kind === 'incremental') { - // Incremental GC has lower pause times (typically 10-20% of duration) - this.gcStats.totalPauseTime += entry.duration * 0.15; - } else { - // For other types, use half the duration as an estimate - this.gcStats.totalPauseTime += entry.duration * 0.5; - } - }); + entries.forEach((entry) => this._processGcEntry(entry)); }); // Start observing GC events diff --git a/package.js b/package.js index 3ea8a14..cf6476b 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "skysignal:agent", - version: "1.1.0", + version: "1.1.1", summary: "SkySignal APM agent for Meteor applications - monitors performance, errors, and system metrics", git: "https://github.com/skysignalapm/agent.git", diff --git a/tests/unit/collectors/SystemMetricsCollector.test.js b/tests/unit/collectors/SystemMetricsCollector.test.js index 663e331..8cc6770 100644 --- a/tests/unit/collectors/SystemMetricsCollector.test.js +++ b/tests/unit/collectors/SystemMetricsCollector.test.js @@ -8,8 +8,15 @@ import { expect } from 'chai'; import sinon from 'sinon'; +import { constants } from 'perf_hooks'; import SystemMetricsCollector from '../../../lib/collectors/SystemMetricsCollector.js'; +const { + NODE_PERFORMANCE_GC_MAJOR, + NODE_PERFORMANCE_GC_MINOR, + NODE_PERFORMANCE_GC_INCREMENTAL, +} = constants; + describe('SystemMetricsCollector', function () { let collector; @@ -177,6 +184,41 @@ describe('SystemMetricsCollector', function () { }); }); + // ========================================== + // _getGcKind + // ========================================== + describe('_getGcKind', function () { + + it('reads kind from entry.detail when present', function () { + const entry = { detail: { kind: NODE_PERFORMANCE_GC_MAJOR } }; + expect(collector._getGcKind(entry)).to.equal(NODE_PERFORMANCE_GC_MAJOR); + }); + + it('falls back to entry.kind for older Node entries', function () { + const entry = { kind: NODE_PERFORMANCE_GC_MINOR }; + expect(collector._getGcKind(entry)).to.equal(NODE_PERFORMANCE_GC_MINOR); + }); + }); + + // ========================================== + // _processGcEntry + // ========================================== + describe('_processGcEntry', function () { + + it('accumulates pause time using numeric GC kinds from entry.detail', function () { + collector.gcStats = { count: 0, totalDuration: 0, totalPauseTime: 0 }; + + collector._processGcEntry({ duration: 10, detail: { kind: NODE_PERFORMANCE_GC_MAJOR } }); + collector._processGcEntry({ duration: 8, detail: { kind: NODE_PERFORMANCE_GC_MINOR } }); + collector._processGcEntry({ duration: 6, detail: { kind: NODE_PERFORMANCE_GC_INCREMENTAL } }); + collector._processGcEntry({ duration: 4, detail: { kind: 99 } }); + + expect(collector.gcStats.count).to.equal(4); + expect(collector.gcStats.totalDuration).to.equal(28); + expect(collector.gcStats.totalPauseTime).to.equal(10 + 8 + (6 * 0.15) + (4 * 0.5)); + }); + }); + // ========================================== // start / stop // ==========================================