import * as NodeOS from "node:os";import type { HostResourcesSnapshot } from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Cache from "effect/Cache";import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; export class HostResources extends Context.Service< HostResources, { readonly read: Effect.Effect<HostResourcesSnapshot> }>()("t3/resourceTelemetry/HostResources") {} function readCpu() { const cpus = NodeOS.cpus(); const cpu = cpus.reduce( (sum, { times }) => ({ idle: sum.idle + times.idle, total: sum.total + times.user + times.nice + times.sys + times.idle + times.irq, }), { idle: 0, total: 0 }, ); return { ...cpu, count: cpus.length };} function darwinAvailableMemory(output: string): number | null { const pageSize = /page size of (\d+) bytes/.exec(output)?.[1]; const free = /^Pages free:\s+(\d+)\./m.exec(output)?.[1]; const inactive = /^Pages inactive:\s+(\d+)\./m.exec(output)?.[1]; const speculative = /^Pages speculative:\s+(\d+)\./m.exec(output)?.[1]; if (!pageSize || !free || !inactive || !speculative) return null; // vm_stat subtracts speculative pages from its printed "Pages free" count. // Adding them here counts each reclaimable page once; purgeable pages overlap. const available = (Number(free) + Number(inactive) + Number(speculative)) * Number(pageSize); return Number.isSafeInteger(available) && Number(pageSize) > 0 ? available : null;} export const make = Effect.fn("makeHostResources")(function* () { const fs = yield* FileSystem.FileSystem; const platform = yield* HostProcessPlatform; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const sample = Effect.fn("HostResources.sample")(function* () { const previousCpu = readCpu(); // CPU counters need two readings; idle servers do no polling or process scans. yield* Effect.sleep("200 millis"); const cpu = readCpu(); const totalDelta = cpu.total - previousCpu.total; const idleDelta = cpu.idle - previousCpu.idle; const cpuUtilization = previousCpu.count === cpu.count && totalDelta > 0 && idleDelta >= 0 ? Math.min(1, Math.max(0, 1 - idleDelta / totalDelta)) : null; const totalMemoryBytes = NodeOS.totalmem(); // On Windows libuv returns GlobalMemoryStatusEx.ullAvailPhys, including standby memory. let availableMemoryBytes = NodeOS.freemem(); if (platform === "linux") { const meminfo = yield* fs .readFileString("/proc/meminfo") .pipe(Effect.catch(() => Effect.succeed(""))); const available = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(meminfo)?.[1]; if (available) availableMemoryBytes = Number(available) * 1024; } else if (platform === "darwin") { const output = yield* spawner .string(ChildProcess.make("/usr/bin/vm_stat", [], { stdin: "ignore", stderr: "ignore" })) .pipe( Effect.timeout("1 second"), Effect.catch(() => Effect.succeed("")), ); availableMemoryBytes = darwinAvailableMemory(output) ?? availableMemoryBytes; } return { sampledAt: DateTime.toEpochMillis(yield* DateTime.now), cpuUtilization, cpuCount: cpu.count, availableMemoryBytes: Math.min(totalMemoryBytes, Math.max(0, availableMemoryBytes)), totalMemoryBytes, }; }); // One server-lifetime cache deduplicates simultaneous requests from all sockets. const cache = yield* Cache.make({ capacity: 1, lookup: (_key: "host") => sample(), timeToLive: "5 seconds", }); return HostResources.of({ read: Cache.get(cache, "host") });}); export const layer = Layer.effect(HostResources, make());
import * as NodeOS from "node:os";
import type { HostResourcesSnapshot } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Cache from "effect/Cache";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
export class HostResources extends Context.Service<
HostResources,
{ readonly read: Effect.Effect<HostResourcesSnapshot> }
>()("t3/resourceTelemetry/HostResources") {}
function readCpu() {
const cpus = NodeOS.cpus();
const cpu = cpus.reduce(
(sum, { times }) => ({
idle: sum.idle + times.idle,
total: sum.total + times.user + times.nice + times.sys + times.idle + times.irq,
}),
{ idle: 0, total: 0 },
);
return { ...cpu, count: cpus.length };
}
function darwinAvailableMemory(output: string): number | null {
const pageSize = /page size of (\d+) bytes/.exec(output)?.[1];
const free = /^Pages free:\s+(\d+)\./m.exec(output)?.[1];
const inactive = /^Pages inactive:\s+(\d+)\./m.exec(output)?.[1];
const speculative = /^Pages speculative:\s+(\d+)\./m.exec(output)?.[1];
if (!pageSize || !free || !inactive || !speculative) return null;
// vm_stat subtracts speculative pages from its printed "Pages free" count.
// Adding them here counts each reclaimable page once; purgeable pages overlap.
const available = (Number(free) + Number(inactive) + Number(speculative)) * Number(pageSize);
return Number.isSafeInteger(available) && Number(pageSize) > 0 ? available : null;
}
export const make = Effect.fn("makeHostResources")(function* () {
const fs = yield* FileSystem.FileSystem;
const platform = yield* HostProcessPlatform;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const sample = Effect.fn("HostResources.sample")(function* () {
const previousCpu = readCpu();
// CPU counters need two readings; idle servers do no polling or process scans.
yield* Effect.sleep("200 millis");
const cpu = readCpu();
const totalDelta = cpu.total - previousCpu.total;
const idleDelta = cpu.idle - previousCpu.idle;
const cpuUtilization =
previousCpu.count === cpu.count && totalDelta > 0 && idleDelta >= 0
? Math.min(1, Math.max(0, 1 - idleDelta / totalDelta))
: null;
const totalMemoryBytes = NodeOS.totalmem();
// On Windows libuv returns GlobalMemoryStatusEx.ullAvailPhys, including standby memory.
let availableMemoryBytes = NodeOS.freemem();
if (platform === "linux") {
const meminfo = yield* fs
.readFileString("/proc/meminfo")
.pipe(Effect.catch(() => Effect.succeed("")));
const available = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(meminfo)?.[1];
if (available) availableMemoryBytes = Number(available) * 1024;
} else if (platform === "darwin") {
const output = yield* spawner
.string(ChildProcess.make("/usr/bin/vm_stat", [], { stdin: "ignore", stderr: "ignore" }))
.pipe(
Effect.timeout("1 second"),
Effect.catch(() => Effect.succeed("")),
);
availableMemoryBytes = darwinAvailableMemory(output) ?? availableMemoryBytes;
}
return {
sampledAt: DateTime.toEpochMillis(yield* DateTime.now),
cpuUtilization,
cpuCount: cpu.count,
availableMemoryBytes: Math.min(totalMemoryBytes, Math.max(0, availableMemoryBytes)),
totalMemoryBytes,
};
});
// One server-lifetime cache deduplicates simultaneous requests from all sockets.
const cache = yield* Cache.make({
capacity: 1,
lookup: (_key: "host") => sample(),
timeToLive: "5 seconds",
});
return HostResources.of({ read: Cache.get(cache, "host") });
});
export const layer = Layer.effect(HostResources, make());