Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("resolveNativeSampleIntervalMs", () => {
expect(resolveNativeSampleIntervalMs({ ...basePower, onBattery: "true" }, 1)).toBe(5_000);
});

it("keeps unknown background telemetry cheap but serves live diagnostics at 1Hz", () => {
it("uses a 5-second background cadence but serves live diagnostics at 1Hz", () => {
const unknown: HostPowerSnapshot = {
...basePower,
source: "unknown",
Expand All @@ -58,7 +58,8 @@ describe("resolveNativeSampleIntervalMs", () => {
0,
),
).toBe(5_000);
expect(resolveNativeSampleIntervalMs(basePower, 0)).toBe(1_000);
expect(resolveNativeSampleIntervalMs(basePower, 0)).toBe(5_000);
expect(resolveNativeSampleIntervalMs(basePower, 1)).toBe(1_000);
});
});

Expand Down
10 changes: 5 additions & 5 deletions apps/server/src/resourceTelemetry/NativeTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { ServerConfig } from "../config.ts";
import { subscribeBeforeSnapshotWithoutMutex } from "../utils/subscribeBeforeSnapshot.ts";

const SAMPLE_INTERVAL_MS = 1_000;
const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;
const BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;
const BATTERY_SAMPLE_INTERVAL_MS = 5_000;
const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;
const HANDSHAKE_TIMEOUT = Duration.seconds(5);
Expand Down Expand Up @@ -257,7 +257,7 @@ export function resolveNativeSampleIntervalMs(
liveSubscriberCount: number,
): number {
if (snapshot.stale || snapshot.source === "unknown") {
return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;
return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : BACKGROUND_SAMPLE_INTERVAL_MS;
}
if (
snapshot.suspended ||
Expand All @@ -268,7 +268,7 @@ export function resolveNativeSampleIntervalMs(
return CONSTRAINED_SAMPLE_INTERVAL_MS;
}
if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS;
return SAMPLE_INTERVAL_MS;
return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : BACKGROUND_SAMPLE_INTERVAL_MS;
}

export function commitCollectionControlUpdate<E, R>(
Expand Down Expand Up @@ -379,7 +379,7 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu
updatedAt: initializedAt,
},
liveSubscriberCount: 0,
sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,
sampleIntervalMs: BACKGROUND_SAMPLE_INTERVAL_MS,
});
const appliedCollectionControl = yield* Ref.make(yield* Ref.get(collectionControl));
const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]);
Expand Down Expand Up @@ -980,7 +980,7 @@ export const layerTest = (
lastSampleAt: Option.none<DateTime.Utc>(),
lastError: Option.some("Resource monitor test implementation is unavailable."),
restartCount: 0,
sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,
sampleIntervalMs: BACKGROUND_SAMPLE_INTERVAL_MS,
});
return Layer.succeed(
NativeTelemetryClient,
Expand Down
78 changes: 70 additions & 8 deletions apps/web/src/components/settings/settingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
useSyncExternalStore,
} from "react";

import { cn } from "../../lib/utils";
Expand Down Expand Up @@ -78,14 +77,77 @@ function useSettingsSearchTarget<T extends HTMLElement>(id: string | undefined)
return targetRef;
}

interface RelativeTimeTicker {
nowMs: number;
timerId: number | null;
readonly listeners: Set<() => void>;
}

const relativeTimeTickers = new Map<number, RelativeTimeTicker>();

function getRelativeTimeTicker(intervalMs: number): RelativeTimeTicker {
const existing = relativeTimeTickers.get(intervalMs);
if (existing !== undefined) {
return existing;
}
const ticker: RelativeTimeTicker = {
nowMs: Date.now(),
timerId: null,
listeners: new Set(),
};
relativeTimeTickers.set(intervalMs, ticker);
return ticker;
}

function tickRelativeTime(ticker: RelativeTimeTicker): void {
const nextNowMs = Date.now();
if (nextNowMs === ticker.nowMs) {
return;
}
ticker.nowMs = nextNowMs;
for (const listener of ticker.listeners) {
listener();
}
}

function subscribeToRelativeTime(
ticker: RelativeTimeTicker,
intervalMs: number,
listener: () => void,
): () => void {
ticker.listeners.add(listener);
if (ticker.listeners.size === 1) {
ticker.timerId = window.setInterval(() => tickRelativeTime(ticker), intervalMs);
}

return () => {
ticker.listeners.delete(listener);
if (ticker.listeners.size === 0 && ticker.timerId !== null) {
window.clearInterval(ticker.timerId);
ticker.timerId = null;
if (relativeTimeTickers.get(intervalMs) === ticker) {
relativeTimeTickers.delete(intervalMs);
}
}
};
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.

function getRelativeTimeSnapshot(ticker: RelativeTimeTicker): number {
if (ticker.timerId === null) {
ticker.nowMs = Date.now();
}
return ticker.nowMs;
}

/** Re-render every `intervalMs`; return a stable timestamp snapshot for render-time relative labels. */
export function useRelativeTimeTick(intervalMs = 1_000) {
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNowMs(Date.now()), intervalMs);
return () => clearInterval(id);
}, [intervalMs]);
return nowMs;
const ticker = getRelativeTimeTicker(intervalMs);
const subscribe = useCallback(
(listener: () => void) => subscribeToRelativeTime(ticker, intervalMs, listener),
[intervalMs, ticker],
);
const getSnapshot = useCallback(() => getRelativeTimeSnapshot(ticker), [ticker]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

export function SettingsSection({
Expand Down
78 changes: 65 additions & 13 deletions apps/web/src/connection/useDesktopLocalBootstraps.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
import type { DesktopEnvironmentBootstrap } from "@t3tools/contracts";
import { useEffect, useState } from "react";
import { useSyncExternalStore } from "react";

import { readDesktopSecondaryBootstraps } from "./desktopLocal";

const DESKTOP_LOCAL_BOOTSTRAP_POLL_MS = 2_000;
const EMPTY_BOOTSTRAPS: ReadonlyArray<DesktopEnvironmentBootstrap> = [];

let bootstraps: ReadonlyArray<DesktopEnvironmentBootstrap> = EMPTY_BOOTSTRAPS;
let pollId: number | null = null;
const listeners = new Set<() => void>();

function sameBootstrap(
left: DesktopEnvironmentBootstrap,
right: DesktopEnvironmentBootstrap,
): boolean {
return (
left.id === right.id &&
left.label === right.label &&
left.runningDistro === right.runningDistro &&
left.httpBaseUrl === right.httpBaseUrl &&
left.wsBaseUrl === right.wsBaseUrl &&
left.bootstrapToken === right.bootstrapToken
);
}

function sameBootstraps(
left: ReadonlyArray<DesktopEnvironmentBootstrap>,
right: ReadonlyArray<DesktopEnvironmentBootstrap>,
): boolean {
return (
left.length === right.length &&
left.every((entry, index) => sameBootstrap(entry, right[index]!))
);
}

function read(): void {
const next = readDesktopSecondaryBootstraps();
if (sameBootstraps(bootstraps, next)) {
return;
}
bootstraps = next;
for (const listener of listeners) {
listener();
}
}

function subscribe(listener: () => void): () => void {
listeners.add(listener);
if (listeners.size === 1) {
read();
pollId = window.setInterval(read, DESKTOP_LOCAL_BOOTSTRAP_POLL_MS);
}

return () => {
listeners.delete(listener);
if (listeners.size === 0 && pollId !== null) {
window.clearInterval(pollId);
pollId = null;
}
};
}

function getSnapshot(): ReadonlyArray<DesktopEnvironmentBootstrap> {
if (pollId === null && typeof window !== "undefined") {
read();
}
return bootstraps;
}

/**
* Reactively track the desktop's secondary local backends (e.g. a parallel WSL
Expand All @@ -13,16 +76,5 @@ const DESKTOP_LOCAL_BOOTSTRAP_POLL_MS = 2_000;
* renderer consumer reads the same topology.
*/
export function useDesktopLocalBootstraps(): ReadonlyArray<DesktopEnvironmentBootstrap> {
const [bootstraps, setBootstraps] = useState<ReadonlyArray<DesktopEnvironmentBootstrap>>(
readDesktopSecondaryBootstraps,
);

useEffect(() => {
const read = () => setBootstraps(readDesktopSecondaryBootstraps());
read();
const interval = setInterval(read, DESKTOP_LOCAL_BOOTSTRAP_POLL_MS);
return () => clearInterval(interval);
}, []);

return bootstraps;
return useSyncExternalStore(subscribe, getSnapshot, () => EMPTY_BOOTSTRAPS);
}
65 changes: 49 additions & 16 deletions native/resource-monitor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,17 @@ impl Collector {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
process_refresh_kind(),
process_tracking_refresh_kind(),
);
self.cpu_baseline_refreshed_at = Some(Instant::now());
}

fn sample(&mut self, config: &CollectorConfig, request_id: Option<String>) -> SnapshotEvent {
fn sample(
&mut self,
config: &CollectorConfig,
request_id: Option<String>,
refresh_commands: bool,
) -> SnapshotEvent {
if let Some(delay) =
remaining_cpu_measurement_delay(self.cpu_baseline_refreshed_at.take(), Instant::now())
{
Expand All @@ -352,7 +357,7 @@ impl Collector {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
process_refresh_kind(),
process_tracking_refresh_kind(),
);
self.cpu_baseline_refreshed_at = Some(Instant::now());

Expand Down Expand Up @@ -388,6 +393,16 @@ impl Collector {
roots.insert(config.root_pid);
let tracked = select_tracked_pids(&rows, &roots);
let tracked_process_count = tracked.len();
let tracked_pids = tracked
.iter()
.copied()
.map(Pid::from_u32)
.collect::<Vec<_>>();
self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&tracked_pids),
true,
process_details_refresh_kind(refresh_commands),
);
let mut processes = tracked
.into_iter()
.filter_map(|pid| {
Expand Down Expand Up @@ -450,12 +465,21 @@ impl Collector {
}
}

fn process_refresh_kind() -> ProcessRefreshKind {
fn process_tracking_refresh_kind() -> ProcessRefreshKind {
ProcessRefreshKind::nothing().with_cpu().without_tasks()
}

fn process_details_refresh_kind(refresh_commands: bool) -> ProcessRefreshKind {
let command_update = if refresh_commands {
UpdateKind::Always
} else {
UpdateKind::OnlyIfNotSet
};

ProcessRefreshKind::nothing()
.with_memory()
.with_cpu()
.with_disk_usage()
.with_cmd(UpdateKind::Always)
.with_cmd(command_update)
.without_tasks()
}

Expand Down Expand Up @@ -696,7 +720,7 @@ fn main() -> io::Result<()> {
if next_sample_at.is_some_and(|deadline| deadline <= Instant::now()) {
if let Some(current) = config.as_ref() {
if let Some(interval) = current.sample_interval {
let event = collector.sample(current, None);
let event = collector.sample(current, None, streaming_enabled);
history.record(&event);
if streaming_enabled {
write_event(&mut writer, &event)?;
Expand Down Expand Up @@ -789,7 +813,7 @@ fn main() -> io::Result<()> {
}
Command::SampleNow { request_id, .. } => {
if let Some(current) = config.as_ref() {
let event = collector.sample(current, Some(request_id));
let event = collector.sample(current, Some(request_id), true);
history.record(&event);
write_event(&mut writer, &event)?;
next_sample_at = sample_now_deadline(
Expand Down Expand Up @@ -1133,14 +1157,23 @@ mod tests {
}

#[test]
fn refreshes_commands_without_enumerating_linux_tasks() {
let refresh_kind = process_refresh_kind();

assert_eq!(refresh_kind.cmd(), UpdateKind::Always);
assert!(!refresh_kind.tasks());
assert!(refresh_kind.cpu());
assert!(refresh_kind.memory());
assert!(refresh_kind.disk_usage());
fn limits_expensive_refreshes_to_tracked_processes() {
let tracking_kind = process_tracking_refresh_kind();
assert!(!tracking_kind.tasks());
assert!(tracking_kind.cpu());
assert!(!tracking_kind.memory());
assert!(!tracking_kind.disk_usage());
assert_eq!(tracking_kind.cmd(), UpdateKind::Never);

let background_details = process_details_refresh_kind(false);
assert!(!background_details.tasks());
assert!(!background_details.cpu());
assert!(background_details.memory());
assert!(background_details.disk_usage());
assert_eq!(background_details.cmd(), UpdateKind::OnlyIfNotSet);

let live_details = process_details_refresh_kind(true);
assert_eq!(live_details.cmd(), UpdateKind::Always);
}

#[test]
Expand Down
Loading