feat(desktop): add hot view retain/release lifecycle and process metrics
Replace single-shot acquireHotView with retainHotView/releaseHotView plus a minute-level reaper that closes views idle beyond 5 minutes and never evicts a view while it still has live retainers. Wrap adapter calls in try/finally so a failing publish/query still releases the view. Sample process metrics every minute and expose hot-view policy and process snapshot via the runtime snapshot for diagnostics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,15 +5,54 @@ export interface HotViewHandle {
|
||||
accountId: string;
|
||||
view: ElectronWebContentsView;
|
||||
activatedAt: number;
|
||||
lastReleasedAt: number | null;
|
||||
retainCount: number;
|
||||
}
|
||||
|
||||
const hotViews = new Map<string, HotViewHandle>();
|
||||
const HOT_VIEW_IDLE_TTL_MS = 5 * 60_000;
|
||||
const HOT_VIEW_REAPER_INTERVAL_MS = 60_000;
|
||||
let reaperHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function closeView(handle: HotViewHandle): void {
|
||||
if (!handle.view.webContents.isDestroyed()) {
|
||||
handle.view.webContents.close();
|
||||
}
|
||||
}
|
||||
|
||||
function touchHandle(handle: HotViewHandle): HotViewHandle {
|
||||
handle.activatedAt = Date.now();
|
||||
return handle;
|
||||
}
|
||||
|
||||
function demoteViewInternal(accountId: string): boolean {
|
||||
const handle = hotViews.get(accountId);
|
||||
if (!handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
closeView(handle);
|
||||
hotViews.delete(accountId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function startHotViewReaper(): void {
|
||||
if (reaperHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaperHandle = setInterval(() => {
|
||||
reapIdleHotViews();
|
||||
}, HOT_VIEW_REAPER_INTERVAL_MS);
|
||||
reaperHandle.unref?.();
|
||||
}
|
||||
|
||||
export function acquireHotView(accountId: string): HotViewHandle {
|
||||
startHotViewReaper();
|
||||
|
||||
const existing = hotViews.get(accountId);
|
||||
if (existing) {
|
||||
existing.activatedAt = Date.now();
|
||||
return existing;
|
||||
return touchHandle(existing);
|
||||
}
|
||||
|
||||
const handle: HotViewHandle = {
|
||||
@@ -26,26 +65,80 @@ export function acquireHotView(accountId: string): HotViewHandle {
|
||||
},
|
||||
}),
|
||||
activatedAt: Date.now(),
|
||||
lastReleasedAt: Date.now(),
|
||||
retainCount: 0,
|
||||
};
|
||||
hotViews.set(accountId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function demoteView(accountId: string): void {
|
||||
export function retainHotView(accountId: string): HotViewHandle {
|
||||
const handle = touchHandle(acquireHotView(accountId));
|
||||
handle.retainCount += 1;
|
||||
handle.lastReleasedAt = null;
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function releaseHotView(accountId: string): void {
|
||||
const handle = hotViews.get(accountId);
|
||||
if (!handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handle.view.webContents.isDestroyed()) {
|
||||
handle.view.webContents.close();
|
||||
if (handle.retainCount > 0) {
|
||||
handle.retainCount -= 1;
|
||||
}
|
||||
|
||||
if (handle.retainCount === 0) {
|
||||
handle.lastReleasedAt = Date.now();
|
||||
}
|
||||
hotViews.delete(accountId);
|
||||
}
|
||||
|
||||
export function listHotViews(): Array<Pick<HotViewHandle, "accountId" | "activatedAt">> {
|
||||
return [...hotViews.values()].map(({ accountId, activatedAt }) => ({
|
||||
export function demoteView(accountId: string): boolean {
|
||||
const handle = hotViews.get(accountId);
|
||||
if (!handle || handle.retainCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return demoteViewInternal(accountId);
|
||||
}
|
||||
|
||||
export function reapIdleHotViews(
|
||||
now = Date.now(),
|
||||
idleTTLms = HOT_VIEW_IDLE_TTL_MS,
|
||||
): string[] {
|
||||
const reclaimed: string[] = [];
|
||||
|
||||
for (const [accountId, handle] of hotViews.entries()) {
|
||||
if (handle.retainCount > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const idleSince = handle.lastReleasedAt ?? handle.activatedAt;
|
||||
if (now - idleSince < idleTTLms) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (demoteViewInternal(accountId)) {
|
||||
reclaimed.push(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
return reclaimed;
|
||||
}
|
||||
|
||||
export function listHotViews(): Array<Pick<HotViewHandle, "accountId" | "activatedAt" | "lastReleasedAt" | "retainCount">> {
|
||||
return [...hotViews.values()].map(({ accountId, activatedAt, lastReleasedAt, retainCount }) => ({
|
||||
accountId,
|
||||
activatedAt,
|
||||
lastReleasedAt,
|
||||
retainCount,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getHotViewPoolPolicy() {
|
||||
return {
|
||||
idleTTLms: HOT_VIEW_IDLE_TTL_MS,
|
||||
reapIntervalMs: HOT_VIEW_REAPER_INTERVAL_MS,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user