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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
61 changes: 39 additions & 22 deletions lib/collectors/SystemMetricsCollector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/collectors/SystemMetricsCollector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
// ==========================================
Expand Down
Loading