608 lines · 20.7 KB · TypeScript
RawDownload
import type {  DesktopElectronProcessMetric,  DesktopHostTelemetrySnapshot,  ResourceMonitorProcessSample,  ResourceMonitorSnapshotEvent,  ResourceTelemetryAggregate,  ResourceTelemetryProcess,  ResourceTelemetryProcessCategory,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Option from "effect/Option"; const MAX_DELTA_INTERVAL_MS = 30_000;const ELECTRON_IDENTITY_TOLERANCE_MS = 2_000; export interface ProcessState {  readonly process: ResourceTelemetryProcess;  readonly sampledAtMs: number;} export interface GroupCounters {  readonly cpuTimeMs: number;  readonly ioReadBytes: number;  readonly ioWriteBytes: number;  readonly processStarts: number;  readonly processExits: number;} export interface TelemetryCounters {  readonly backend: GroupCounters;  readonly electron: GroupCounters;  readonly monitor: GroupCounters;  readonly allT3: GroupCounters;} export interface ProcessDelta {  readonly identityKey: string;  readonly category: ResourceTelemetryProcessCategory;  readonly cpuTimeMs: number;  readonly ioReadBytes: number;  readonly ioWriteBytes: number;} export interface MergeProcessesInput {  readonly serverPid: number;  readonly sidecarPid: Option.Option<number>;  readonly fallbackSampledAtMs: number;  readonly nativeSnapshot: Option.Option<ResourceMonitorSnapshotEvent>;  readonly desktopSnapshot: Option.Option<DesktopHostTelemetrySnapshot>;  readonly electronRootPids?: ReadonlySet<number>;  readonly electronRootStartTimes?: ReadonlyMap<number, number>;  readonly previous: ReadonlyMap<string, ProcessState>;  readonly counters: TelemetryCounters;  readonly updatePrevious: boolean;} export interface MergeProcessesResult {  readonly sampledAtMs: number;  readonly processes: ReadonlyArray<ResourceTelemetryProcess>;  readonly previous: ReadonlyMap<string, ProcessState>;  readonly counters: TelemetryCounters;  readonly groups: {    readonly backend: ResourceTelemetryAggregate;    readonly electron: ResourceTelemetryAggregate;    readonly monitor: ResourceTelemetryAggregate;    readonly allT3: ResourceTelemetryAggregate;  };  readonly deltas: ReadonlyArray<ProcessDelta>;} const emptyGroupCounters = (): GroupCounters => ({  cpuTimeMs: 0,  ioReadBytes: 0,  ioWriteBytes: 0,  processStarts: 0,  processExits: 0,}); export const emptyTelemetryCounters = (): TelemetryCounters => ({  backend: emptyGroupCounters(),  electron: emptyGroupCounters(),  monitor: emptyGroupCounters(),  allT3: emptyGroupCounters(),}); export function processIdentityKey(pid: number, startTimeMs: number): string {  return `${pid}:${startTimeMs}`;} function finiteNonNegative(value: number): number {  return Number.isFinite(value) ? Math.max(0, value) : 0;} function categoryGroup(  category: ResourceTelemetryProcessCategory,): "backend" | "electron" | "monitor" {  if (category === "resource-monitor") return "monitor";  if (category.startsWith("electron-")) return "electron";  return "backend";} function electronCategory(metric: DesktopElectronProcessMetric): ResourceTelemetryProcessCategory {  switch (metric.type) {    case "Browser":      return "electron-main";    case "Tab":      return "electron-renderer";    case "GPU":      return "electron-gpu";    default:      return "electron-utility";  }} function inferredElectronCategory(  process: ResourceMonitorProcessSample,): ResourceTelemetryProcessCategory {  const command = process.command.toLowerCase();  if (command.includes("--type=renderer")) return "electron-renderer";  if (command.includes("--type=gpu-process")) return "electron-gpu";  return "electron-utility";} function matchElectronMetric(  process: ResourceMonitorProcessSample,  metricsByPid: ReadonlyMap<number, DesktopElectronProcessMetric>,): DesktopElectronProcessMetric | undefined {  const metric = metricsByPid.get(process.pid);  if (!metric) return undefined;  return Math.abs(metric.creationTimeMs - process.startTimeMs) <= ELECTRON_IDENTITY_TOLERANCE_MS    ? metric    : undefined;} function syntheticNativeSample(  metric: DesktopElectronProcessMetric,  sampledAtMs: number,  previous: ProcessState | undefined,): ResourceMonitorProcessSample {  const cpuTimeMs =    metric.cumulativeCpuSeconds !== undefined      ? Math.max(0, Math.round(metric.cumulativeCpuSeconds * 1_000))      : previous        ? previous.process.cpuTimeMs +          Math.max(0, ((sampledAtMs - previous.sampledAtMs) * metric.cpuPercent) / 100)        : 0;  return {    pid: metric.pid,    ppid: 0,    startTimeMs: metric.creationTimeMs,    runTimeMs: Math.max(0, sampledAtMs - metric.creationTimeMs),    name: metric.name ?? metric.serviceName ?? metric.type,    command: metric.name ?? metric.serviceName ?? metric.type,    status: "Running",    cpuPercent: metric.cpuPercent,    cpuTimeMs,    residentBytes: metric.workingSetBytes,    virtualBytes: 0,    ioReadBytes: 0,    ioWriteBytes: 0,    ioSemantics: "storage",  };} function processDepths(  processes: ReadonlyArray<ResourceMonitorProcessSample>,  roots: ReadonlySet<number>,): ReadonlyMap<number, number> {  const childrenByParent = new Map<number, number[]>();  for (const process of processes) {    const children = childrenByParent.get(process.ppid) ?? [];    children.push(process.pid);    childrenByParent.set(process.ppid, children);  }   const depths = new Map<number, number>();  const queue = [...roots].map((pid) => ({ pid, depth: 0 }));  while (queue.length > 0) {    const current = queue.shift();    if (!current || depths.has(current.pid)) continue;    depths.set(current.pid, current.depth);    for (const childPid of childrenByParent.get(current.pid) ?? []) {      queue.push({ pid: childPid, depth: current.depth + 1 });    }  }  return depths;} function isElectronDescendant(  pid: number,  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,  electronPids: ReadonlySet<number>,): boolean {  const visited = new Set<number>();  let currentPid = pid;  while (!visited.has(currentPid)) {    visited.add(currentPid);    if (electronPids.has(currentPid)) return true;    const current = processesByPid.get(currentPid);    if (!current || current.ppid <= 0 || current.ppid === currentPid) return false;    currentPid = current.ppid;  }  return false;} function hasElectronAncestor(  process: ResourceMonitorProcessSample,  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,  electronPids: ReadonlySet<number>,): boolean {  const visited = new Set<number>();  let currentPid = process.ppid;  while (currentPid > 0 && !visited.has(currentPid)) {    visited.add(currentPid);    if (electronPids.has(currentPid)) return true;    const current = processesByPid.get(currentPid);    if (!current || current.ppid === currentPid) return false;    currentPid = current.ppid;  }  return false;} function orderProcessTree(  processes: ReadonlyArray<ResourceTelemetryProcess>,  rootPids: ReadonlyArray<number>,): ReadonlyArray<ResourceTelemetryProcess> {  const processesByPid = new Map(processes.map((process) => [process.identity.pid, process]));  const childrenByParent = new Map<number, ResourceTelemetryProcess[]>();  for (const process of processes) {    const children = childrenByParent.get(process.ppid) ?? [];    children.push(process);    childrenByParent.set(process.ppid, children);  }  for (const children of childrenByParent.values()) {    children.sort((left, right) => left.identity.pid - right.identity.pid);  }   const ordered: ResourceTelemetryProcess[] = [];  const visited = new Set<number>();  const visit = (process: ResourceTelemetryProcess): void => {    if (visited.has(process.identity.pid)) return;    visited.add(process.identity.pid);    ordered.push(process);    for (const child of childrenByParent.get(process.identity.pid) ?? []) {      visit(child);    }  };   for (const rootPid of rootPids) {    const root = processesByPid.get(rootPid);    if (root) visit(root);  }  for (const process of processes.toSorted(    (left, right) => left.depth - right.depth || left.identity.pid - right.identity.pid,  )) {    visit(process);  }  return ordered;} function delta(input: {  readonly current: number;  readonly previous: number;  readonly elapsedMs: number;}): number {  if (    input.elapsedMs <= 0 ||    input.elapsedMs > MAX_DELTA_INTERVAL_MS ||    input.current < input.previous  ) {    return 0;  }  return input.current - input.previous;} function incrementCounters(counters: GroupCounters, update: Partial<GroupCounters>): GroupCounters {  return {    cpuTimeMs: counters.cpuTimeMs + (update.cpuTimeMs ?? 0),    ioReadBytes: counters.ioReadBytes + (update.ioReadBytes ?? 0),    ioWriteBytes: counters.ioWriteBytes + (update.ioWriteBytes ?? 0),    processStarts: counters.processStarts + (update.processStarts ?? 0),    processExits: counters.processExits + (update.processExits ?? 0),  };} function applyLifecycleCounters(input: {  readonly counters: TelemetryCounters;  readonly deltas: ReadonlyArray<ProcessDelta>;  readonly current: ReadonlyMap<string, ProcessState>;  readonly previous: ReadonlyMap<string, ProcessState>;}): TelemetryCounters {  let backend = input.counters.backend;  let electron = input.counters.electron;  let monitor = input.counters.monitor;  let allT3 = input.counters.allT3;  for (const processDelta of input.deltas) {    const group = categoryGroup(processDelta.category);    switch (group) {      case "backend":        backend = incrementCounters(backend, processDelta);        break;      case "electron":        electron = incrementCounters(electron, processDelta);        break;      case "monitor":        monitor = incrementCounters(monitor, processDelta);        break;    }    allT3 = incrementCounters(allT3, processDelta);  }   for (const [identityKey, current] of input.current) {    if (input.previous.has(identityKey)) continue;    const group = categoryGroup(current.process.category);    switch (group) {      case "backend":        backend = incrementCounters(backend, { processStarts: 1 });        break;      case "electron":        electron = incrementCounters(electron, { processStarts: 1 });        break;      case "monitor":        monitor = incrementCounters(monitor, { processStarts: 1 });        break;    }    allT3 = incrementCounters(allT3, { processStarts: 1 });  }   for (const [identityKey, previous] of input.previous) {    if (input.current.has(identityKey)) continue;    const group = categoryGroup(previous.process.category);    switch (group) {      case "backend":        backend = incrementCounters(backend, { processExits: 1 });        break;      case "electron":        electron = incrementCounters(electron, { processExits: 1 });        break;      case "monitor":        monitor = incrementCounters(monitor, { processExits: 1 });        break;    }    allT3 = incrementCounters(allT3, { processExits: 1 });  }   return { backend, electron, monitor, allT3 };} function aggregate(  processes: ReadonlyArray<ResourceTelemetryProcess>,  counters: GroupCounters,): ResourceTelemetryAggregate {  return {    processCount: processes.length,    currentCpuPercent: processes.reduce((total, process) => total + process.cpuPercent, 0),    cpuTimeMs: counters.cpuTimeMs,    currentRssBytes: processes.reduce((total, process) => total + process.residentBytes, 0),    peakRssBytes: processes.reduce((total, process) => total + process.peakResidentBytes, 0),    ioReadBytes: counters.ioReadBytes,    ioWriteBytes: counters.ioWriteBytes,    ioReadBytesPerSecond: processes.reduce(      (total, process) => total + process.ioReadBytesPerSecond,      0,    ),    ioWriteBytesPerSecond: processes.reduce(      (total, process) => total + process.ioWriteBytesPerSecond,      0,    ),    processStarts: counters.processStarts,    processExits: counters.processExits,  };} export function mergeProcesses(input: MergeProcessesInput): MergeProcessesResult {  const nativeProcesses = Option.match(input.nativeSnapshot, {    onNone: () => [] as ReadonlyArray<ResourceMonitorProcessSample>,    onSome: (snapshot) => snapshot.processes,  });  const electronMetrics = Option.match(input.desktopSnapshot, {    onNone: () => [] as ReadonlyArray<DesktopElectronProcessMetric>,    onSome: (snapshot) => snapshot.electronProcesses,  });  const sampledAtMs = Option.match(input.nativeSnapshot, {    onNone: () =>      Option.match(input.desktopSnapshot, {        onNone: () => input.fallbackSampledAtMs,        onSome: (snapshot) => snapshot.sampledAtUnixMs,      }),    onSome: (native) =>      Option.match(input.desktopSnapshot, {        onNone: () => native.sampledAtUnixMs,        onSome: (desktop) => Math.max(native.sampledAtUnixMs, desktop.sampledAtUnixMs),      }),  });  const nativeSampledAtMs = Option.map(    input.nativeSnapshot,    (snapshot) => snapshot.sampledAtUnixMs,  );  const desktopSampledAtMs = Option.map(    input.desktopSnapshot,    (snapshot) => snapshot.sampledAtUnixMs,  );  const nativeProcessPids = new Set(nativeProcesses.map((process) => process.pid));  const nativeByPid = new Map(nativeProcesses.map((process) => [process.pid, process]));  const metricsByPid = new Map<number, DesktopElectronProcessMetric>();  for (const metric of electronMetrics) {    const nativeProcess = nativeByPid.get(metric.pid);    if (!nativeProcess) {      nativeByPid.set(        metric.pid,        syntheticNativeSample(          metric,          Option.getOrElse(desktopSampledAtMs, () => sampledAtMs),          input.previous.get(processIdentityKey(metric.pid, metric.creationTimeMs)),        ),      );      metricsByPid.set(metric.pid, metric);      continue;    }    if (      Math.abs(metric.creationTimeMs - nativeProcess.startTimeMs) <= ELECTRON_IDENTITY_TOLERANCE_MS    ) {      metricsByPid.set(metric.pid, metric);    }  }  const processes = [...nativeByPid.values()];  const processesByPid = new Map(processes.map((process) => [process.pid, process]));  const requestedElectronRootPids = input.electronRootPids ?? new Set<number>();  const explicitElectronRootPids = new Set(    [...requestedElectronRootPids].filter((pid) => {      const process = processesByPid.get(pid);      if (!process) return false;      const expectedStartTime = input.electronRootStartTimes?.get(pid);      if (expectedStartTime !== undefined) {        return Math.abs(process.startTimeMs - expectedStartTime) <= ELECTRON_IDENTITY_TOLERANCE_MS;      }      if (metricsByPid.has(pid)) return true;      return [...input.previous.values()].some(        (previous) =>          previous.process.category === "electron-main" &&          previous.process.identity.pid === pid &&          previous.process.identity.startTimeMs === process.startTimeMs,      );    }),  );  const electronPids = new Set([...metricsByPid.keys(), ...explicitElectronRootPids]);  const electronRootPids = [    ...explicitElectronRootPids,    ...[...electronPids]      .filter((pid) => {        if (explicitElectronRootPids.has(pid)) return false;        const process = processesByPid.get(pid);        return process === undefined          ? true          : !hasElectronAncestor(process, processesByPid, electronPids);      })      .toSorted((left, right) => left - right),  ].filter((pid, index, values) => values.indexOf(pid) === index);  const rootPids = [input.serverPid, ...electronRootPids];  const roots = new Set(rootPids);  const depths = processDepths(processes, roots);  const childrenByParent = new Map<number, number[]>();  for (const process of processes) {    const children = childrenByParent.get(process.ppid) ?? [];    children.push(process.pid);    childrenByParent.set(process.ppid, children);  }   const nextPrevious = new Map<string, ProcessState>();  const processDeltas: ProcessDelta[] = [];  const normalized = processes.map((process): ResourceTelemetryProcess => {    const identityKey = processIdentityKey(process.pid, process.startTimeMs);    const previous = input.previous.get(identityKey);    const counterSampledAtMs = nativeProcessPids.has(process.pid)      ? Option.getOrElse(nativeSampledAtMs, () => sampledAtMs)      : Option.getOrElse(desktopSampledAtMs, () => sampledAtMs);    const elapsedMs = previous ? counterSampledAtMs - previous.sampledAtMs : 0;    const cpuTimeDelta = previous      ? delta({          current: process.cpuTimeMs,          previous: previous.process.cpuTimeMs,          elapsedMs,        })      : 0;    const ioReadDelta = previous      ? delta({          current: process.ioReadBytes,          previous: previous.process.ioReadBytes,          elapsedMs,        })      : 0;    const ioWriteDelta = previous      ? delta({          current: process.ioWriteBytes,          previous: previous.process.ioWriteBytes,          elapsedMs,        })      : 0;    const electronMetric = matchElectronMetric(process, metricsByPid);    const category: ResourceTelemetryProcessCategory =

Lines 1–500 of 608.