UNPKG

@cocalc/project

Version:
195 lines (194 loc) 6.8 kB
"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