93 lines · 3.9 KB · TypeScript
RawDownload
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());