@cocalc/project
Version:
CoCalc: project daemon
195 lines (194 loc) • 6.8 kB
JavaScript
"use strict";
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageInfoServer = void 0;
/*
Usage Info Server
This derives usage information (cpu, mem, etc.)
for a specific "path" (e.g. the corresponding jupyter process for a notebook)
from the ProjectInfoServer (which collects data about everything)
*/
const debug_1 = __importDefault(require("debug"));
const L = (0, debug_1.default)("project:usage-info:server");
const events_1 = require("events");
const awaiting_1 = require("awaiting");
const project_info_1 = require("../project-info");
function is_diff(prev, next, key) {
// we assume a,b >= 0, hence we leave out Math.abs operations
const a = prev[key] ?? 0;
const b = next[key] ?? 0;
if (a === 0 && b === 0)
return false;
return Math.abs(b - a) / Math.min(a, b) > 0.05;
}
class UsageInfoServer extends events_1.EventEmitter {
constructor(path, testing = false) {
super();
this.running = false;
this.testing = testing;
this.path = path;
this.dbg = L;
this.project_info = (0, project_info_1.get_ProjectInfoServer)();
this.dbg("starting");
}
async init() {
this.project_info.start();
this.project_info.on("info", (info) => {
//this.dbg(`got info timestamp=${info.timestamp}`);
this.info = info;
this.update();
});
}
// get the process at the given path – for now, that only works for jupyter notebooks
path_process() {
if (this.info?.processes == null)
return;
for (const p of Object.values(this.info.processes)) {
const cocalc = p.cocalc;
if (cocalc == null || cocalc.type != "jupyter")
continue;
if (cocalc.path == this.path)
return p;
}
}
// we compute the total cpu and memory usage sum for the given PID
// this is a quick recursive traverse, with "stats" as the accumulator
proces_tree_stats(ppid, stats) {
const procs = this.info?.processes;
if (procs == null)
return;
for (const proc of Object.values(procs)) {
if (proc.ppid != ppid)
continue;
this.proces_tree_stats(proc.pid, stats);
stats.mem += proc.stat.mem.rss;
stats.cpu += proc.cpu.pct;
}
}
// cpu usage sum of all children
usage_children(pid) {
const stats = { mem: 0, cpu: 0 };
this.proces_tree_stats(pid, stats);
return stats;
}
// we silently treat non-existing information as zero usage
path_usage_info() {
const proc = this.path_process();
if (proc == null) {
return { cpu: 0, mem: 0, cpu_chld: 0, mem_chld: 0 };
}
else {
// we send whole numbers. saves bandwidth and won't be displayed anyways
const children = this.usage_children(proc.pid);
return {
cpu: Math.round(proc.cpu.pct),
cpu_chld: Math.round(children.cpu),
mem: Math.round(proc.stat.mem.rss),
mem_chld: Math.round(children.mem),
};
}
}
// this function takes the "info" we have (+ more maybe?)
// and derives specific information for the notebook (future: also other file types)
// at the given path.
update() {
if (this.info == null) {
L("was told to update, but there is no ProjectInfo");
return;
}
const cg = this.info.cgroup;
const du = this.info.disk_usage;
if (cg == null || du == null) {
this.dbg("info incomplete, can't send usage data");
return;
}
const mem_rss = cg.mem_stat.total_rss + (du.tmp?.usage ?? 0);
const mem_tot = cg.mem_stat.hierarchical_memory_limit;
const usage = {
time: Date.now(),
...this.path_usage_info(),
mem_limit: mem_tot,
cpu_limit: cg.cpu_cores_limit,
mem_free: Math.max(0, mem_tot - mem_rss),
};
// this.dbg("usage", usage);
if (this.should_update(usage)) {
this.usage = usage;
this.emit("usage", this.usage);
this.last = this.usage;
}
}
// only cause to emit a change if it changed significantly (more than x%),
// or if it changes close to zero (in particular, if cpu usage is low again)
should_update(usage) {
if (this.last == null)
return true;
if (usage == null)
return false;
const keys = ["cpu", "mem", "cpu_chld", "mem_chld"];
for (const key of keys) {
// we want everyone to know if essentially dropped to zero
if ((this.last[key] ?? 0) >= 1 && (usage[key] ?? 0) < 1)
return true;
// … or of one of the values is significantly different
if (is_diff(usage, this.last, key))
return true;
}
// … or if the remaining memory changed
// i.e. if another process uses up a portion, there's less for the current notebook
if (is_diff(usage, this.last, "mem_free"))
return true;
return false;
}
async get_usage() {
this.update();
return this.usage;
}
stop() {
this.running = false;
}
async start() {
if (this.running) {
this.dbg("UsageInfoServer already running, cannot be started twice");
}
else {
await this._start();
}
}
async _start() {
this.dbg("start");
if (this.running) {
throw Error("Cannot start UsageInfoServer twice");
}
this.running = true;
await this.init();
// emit once after startup
const usage = await this.get_usage();
this.emit("usage", usage);
while (this.testing) {
await (0, awaiting_1.delay)(5000);
const usage = await this.get_usage();
this.emit("usage", usage);
}
}
}
exports.UsageInfoServer = UsageInfoServer;
// testing: $ ts-node server.ts
if (require.main === module) {
const uis = new UsageInfoServer("testing.ipynb", true);
uis.start();
let cnt = 0;
uis.on("usage", (usage) => {
console.log(JSON.stringify(usage, null, 2));
cnt += 1;
if (cnt >= 2)
process.exit();
});
}
//# sourceMappingURL=server.js.map