Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: free memory native module #114

Merged
merged 18 commits into from
Mar 20, 2024
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
87 changes: 86 additions & 1 deletion lib/models/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { KadiraModel } from './0model';
import { EventLoopMonitor } from '../event_loop_monitor.js';
import { Ntp } from '../ntp';
import os from 'os';
import fs from 'fs';
import cp from 'child_process';

export const MEMORY_ROUNDING_FACTOR = 100 * 1024; // 100kb
const EVENT_LOOP_ROUNDING_FACTOR = 500; // microseconds
Expand Down Expand Up @@ -39,14 +41,95 @@ export function SystemModel () {
this.previousCpuUsage = process.cpuUsage();
this.cpuHistory = [];
this.currentCpuUsage = 0;
this.freeMemory = os.freemem();

setInterval(() => {
this.cpuUsage();
}, 2000);

setInterval(async () => {
try {
await this.getFreeMemory();
} catch (e) {
console.log('Monti APM: failed to get memory info', e);
}
}, 2000);
}

_.extend(SystemModel.prototype, KadiraModel.prototype);

async function meminfo () {
let info = {};
const content = await new Promise((resolve, reject) => {
fs.readFile('/proc/meminfo', (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
let data = content.toString();
data.split(/\n/g).forEach(function (line) {
line = line.split(':');

// Ignore invalid lines, if any
if (line.length < 2) {
return;
}

// Remove parseInt call to make all values strings
info[line[0].trim()] = parseInt(line[1].trim(), 10);
});

return info;
}

SystemModel.prototype.getFreeMemory = async function () {
const isLinux = process.platform === 'linux';
const isMac = process.platform === 'darwin';
try {
if (isLinux) {
const info = await meminfo();
// in kb to bytes
this.freeMemory = 1024 * (info.MemFree + info.Buffers + info.Cached);
return true;
}
if (isMac) {
const output = await new Promise((resolve, reject) => {
cp.exec(
'vm_stat',
(error, stdout) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
});
const pageSizeArray = /page size of (\d*)/g.exec(output);
const pageSize = parseInt(pageSizeArray[1], 10);
const freePagesMatches = /Pages free:\s*(\d*)/g.exec(output);
const freePages = parseInt(freePagesMatches[1], 10);
const inactivePagesMatches = /Pages inactive:\s*(\d*)/g.exec(output);
const inactivePages = parseInt(inactivePagesMatches[1],10);
[pageSize, freePages, inactivePages].forEach(o => {
if (Number.isNaN(o)) {
throw new Error('Monti APM: failed to parse vm_stat');
}
});
const totalFreeMemory = pageSize * (freePages + inactivePages);
this.freeMemory = totalFreeMemory;
return true;
}
} catch (e) {
console.error('Monti APM: failed to get native memory info, falling back to default option', e);
}

this.freeMemory = os.freemem();
return false;
};

SystemModel.prototype.buildPayload = function () {
let metrics = {};
let now = Ntp._now();
Expand All @@ -60,7 +143,9 @@ SystemModel.prototype.buildPayload = function () {
metrics.memoryExternal = roundUsingFactor(memoryUsage.external, MEMORY_ROUNDING_FACTOR) / (1024 * 1024);
metrics.memoryHeapUsed = roundUsingFactor(memoryUsage.heapUsed, MEMORY_ROUNDING_FACTOR) / (1024 * 1024);
metrics.memoryHeapTotal = roundUsingFactor(memoryUsage.heapTotal, MEMORY_ROUNDING_FACTOR) / (1024 * 1024);
metrics.freeMemory = roundUsingFactor(os.freemem(), MEMORY_ROUNDING_FACTOR) / (1024 * 1024);

const freeMemory = this.freeMemory;
metrics.freeMemory = roundUsingFactor(freeMemory, MEMORY_ROUNDING_FACTOR) / (1024 * 1024);
metrics.totalMemory = roundUsingFactor(os.totalmem(), MEMORY_ROUNDING_FACTOR) / (1024 * 1024);

metrics.newSessions = this.newSessions;
Expand Down
3 changes: 2 additions & 1 deletion package.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ let npmModules = {
parseurl: '1.3.3',
};

Npm.depends(npmModules);

Package.onUse(function (api) {
Npm.depends(npmModules);
configurePackage(api, false);
api.export(['Kadira', 'Monti']);
});

Package.onTest(function (api) {
Npm.depends(Object.assign({}, npmModules, {sinon: '6.3.5'}));
configurePackage(api, true);
api.use([
'peerlibrary:reactive-publish',
Expand Down
168 changes: 166 additions & 2 deletions tests/models/system.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import cp from 'child_process';
import fs from 'fs';
import { Meteor } from 'meteor/meteor';
import sinon from 'sinon';
import { MEMORY_ROUNDING_FACTOR, SystemModel } from '../../lib/models/system';
import { Wait } from '../_helpers/helpers';

import { Wait, releaseParts } from '../_helpers/helpers';
/**
* @flaky
*/
Expand All @@ -15,12 +17,174 @@ Tinytest.add(
let payload = model.buildPayload().systemMetrics[0];
test.isTrue(payload.memory > 0, `memory: ${payload.memory}`);
test.isTrue((payload.memory * 1024 * 1024 /* in bytes */) % MEMORY_ROUNDING_FACTOR === 0, 'memory is rounded');
test.isTrue((payload.freeMemory * 1024 * 1024 /* in bytes */) % MEMORY_ROUNDING_FACTOR === 0, 'memory is rounded');
test.isTrue(payload.freeMemory > 0, 'free memory is > 0');
test.isTrue(payload.freeMemory > 0, 'free memory is > 0');
test.isTrue(payload.pcpu >= 0, `pcpu: ${payload.pcpu}`);
test.isTrue(payload.sessions >= 0, `sessions: ${payload.sessions}`);
test.isTrue(payload.endTime >= payload.startTime + 500, `time: ${payload.endTime} - ${payload.startTime}`);
}
);

// sinon cant stub fs/cp on older node versions
if (releaseParts[0] > 1 || (releaseParts[0] === 1 && releaseParts[1] > 8) ) {
Tinytest.addAsync(
'Models - System - freeMemory',
async function (test) {
let model = new SystemModel();
/**
* MAC OS
*/
sinon.stub(process, 'platform').value('darwin');

sinon.replace(cp, 'exec', (a, callback) => {
callback(null,
`Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free: 3293.
Pages active: 231224.
Pages inactive: 238682.
Pages speculative: 534.
Pages throttled: 0.
Pages wired down: 212821.
Pages purgeable: 15075.
"Translation faults": 3619403884.
Pages copy-on-write: 205742534.
Pages zero filled: 1133125308.
Pages reactivated: 862283057.
Pages purged: 221979774.
File-backed pages: 115729.
Anonymous pages: 354711.
Pages stored in compressor: 2284038.
Pages occupied by compressor: 452497.
Decompressions: 992611451.
Compressions: 1067810568.
Pageins: 58750181.
Pageouts: 2172799.
Swapins: 6971267.
Swapouts: 8892364.`);
});
await model.getFreeMemory();
test.isTrue(model.freeMemory === 3964518400, 'should use the file format on mac');
sinon.restore();

/**
* LINUX
*/
sinon.stub(process, 'platform').value('linux');
model = new SystemModel();
sinon.replace(fs, 'readFile', (_,callback) => {
callback(null,
{ toString: () => `MemTotal: 2097152 kB
MemFree: 2085696 kB
MemAvailable: 2085828 kB
Buffers: 0 kB
Cached: 132 kB
SwapCached: 0 kB
Active: 0 kB
Inactive: 4116 kB
Active(anon): 0 kB
Inactive(anon): 4116 kB
Active(file): 0 kB
Inactive(file): 0 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 4116 kB
Mapped: 0 kB
Shmem: 0 kB
KReclaimable: 6807616 kB
Slab: 0 kB
SReclaimable: 0 kB
SUnreclaim: 0 kB
KernelStack: 36496 kB
PageTables: 48024 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 325948112 kB
Committed_AS: 18755168 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 163092 kB
VmallocChunk: 0 kB
Percpu: 525312 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 8192
HugePages_Free: 8189
HugePages_Rsvd: 61
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 16777216 kB
DirectMap4k: 14918780 kB
DirectMap2M: 116037632 kB
DirectMap1G: 5242880 kB`});
});

await model.getFreeMemory();
console.log(model.freeMemory);
test.isTrue(model.freeMemory === 2135887872, 'should use the file format on linux');
sinon.restore();
}
);
Tinytest.addAsync(
'Models - System - freeMemory silent error',
async function (test) {
let model = new SystemModel();
/**
* MAC OS
*/
sinon.stub(process, 'platform').value('darwin');
sinon.replace(cp, 'exec', (a, callback) => {
callback(/* error */ true,null);
});
await model.getFreeMemory();
test.isTrue(model.freeMemory > 0, 'should use fallback on mac');
sinon.restore();

/**
* LINUX
*/
sinon.stub(process, 'platform').value('linux');
model = new SystemModel();
sinon.replace(fs, 'readFile', (_,callback) => {
callback(/* error */ true, null);
});

await model.getFreeMemory();
test.isTrue(model.freeMemory > 0 , 'should use fallback linux');
sinon.restore();
}
);
} else {
Tinytest.addAsync(
'Models - System - freeMemory silent error',
async function (test, done) {
const model = new SystemModel();
await model.getFreeMemory();
test.isTrue(model.freeMemory > 0 , 'should use fallback linux');
done();
});
}

if (process.platform !== 'win32') {
Tinytest.addAsync(
'Models - System - freeMemory succeed on osx/linux',
async function (test, done) {
const model = new SystemModel();
let success = await model.getFreeMemory();
test.isTrue(success);
done();
}
);
}

Tinytest.add(
'Models - System - new Sessions - count new session',
function (test) {
Expand Down
Loading