mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 22:14:51 +02:00
[eric] canvas: browsers and apps share one 10-renderer ceiling, so the two caps can no longer stack to 14
This commit is contained in:
@@ -11,11 +11,18 @@ import { getActivity, isAnyBrowserBusy } from '@/shared/browserCommandHandler';
|
||||
import { isKeepAliveBrowser } from '@/shared/browserFocus';
|
||||
import { captureTabCapsule } from '@/shared/browserStateCapsule';
|
||||
import { getMinimizedShot } from '../../desktop/minimizedShots';
|
||||
import { guestBudgetHasRoom, wireBrowserLiveCounter } from '@/shared/appWebviewBudget';
|
||||
import { useAppHidden } from './useAppHidden';
|
||||
import { cardIntersectsViewport, distFromCenter, type Viewport } from './suspendGeometry';
|
||||
|
||||
const isElectron = typeof navigator !== 'undefined' && navigator.userAgent.includes('Electron');
|
||||
|
||||
// Feed the global guest budget the live-browser count, so apps and browsers share ONE ceiling.
|
||||
wireBrowserLiveCounter(() => {
|
||||
const dl = store.getState().dashboardLayout;
|
||||
return Object.keys(dl.browserCards).filter((id) => !dl.suspendedBrowserCards[id]).length;
|
||||
});
|
||||
|
||||
const SETTLE_MS = 800;
|
||||
// Hysteresis: suspend only well past the edge, resume just past it, so a card sitting on the boundary never flaps between webview and snapshot.
|
||||
const SUSPEND_MARGIN_PX = 320;
|
||||
@@ -184,7 +191,9 @@ export function useWebviewSuspend(
|
||||
continue;
|
||||
}
|
||||
const bigEnough = card.width * zoom >= RESUME_MIN_CARD_PX;
|
||||
if (bigEnough && cardIntersectsViewport(card, vpRef.current, RESUME_MARGIN_PX)) {
|
||||
// Passive wake also asks the GLOBAL budget: a free browser slot means nothing if apps already
|
||||
// hold the machine at its ceiling. Explicit restores and working agents above never ask.
|
||||
if (bigEnough && cardIntersectsViewport(card, vpRef.current, RESUME_MARGIN_PX) && guestBudgetHasRoom()) {
|
||||
dispatch(resumeBrowserCard(id));
|
||||
budget--;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
requestAppSlot,
|
||||
releaseAppSlot,
|
||||
wireBrowserLiveCounter,
|
||||
totalLiveGuests,
|
||||
MAX_LIVE_APP_WEBVIEWS as MAX,
|
||||
} from './appWebviewBudget.ts';
|
||||
|
||||
@@ -65,3 +67,25 @@ test('re-requesting an already-live card just updates it, no extra slot', () =>
|
||||
assert.equal(requestAppSlot('f-far', 9999, false), false, 'still full after a re-request');
|
||||
release([...keys, 'f-far']);
|
||||
});
|
||||
|
||||
test('the global ceiling counts browsers and apps together', () => {
|
||||
// 8 live browsers reported: only 2 of the 6 app slots may actually go live.
|
||||
wireBrowserLiveCounter(() => 8);
|
||||
const keys: string[] = [];
|
||||
let granted = 0;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const k = `g${i}`;
|
||||
if (requestAppSlot(k, i, false)) { granted++; keys.push(k); }
|
||||
}
|
||||
assert.equal(granted, 2, `apps must stop at the global ceiling, granted ${granted}`);
|
||||
assert.equal(totalLiveGuests(), 10);
|
||||
wireBrowserLiveCounter(() => 0);
|
||||
keys.forEach(releaseAppSlot);
|
||||
});
|
||||
|
||||
test('pinned cards ignore the global ceiling, a working agent is never throttled', () => {
|
||||
wireBrowserLiveCounter(() => 99);
|
||||
assert.equal(requestAppSlot('pinned-work', 0, true), true);
|
||||
wireBrowserLiveCounter(() => 0);
|
||||
releaseAppSlot('pinned-work');
|
||||
});
|
||||
|
||||
@@ -10,6 +10,29 @@
|
||||
*/
|
||||
export const MAX_LIVE_APP_WEBVIEWS = 6;
|
||||
|
||||
// The GLOBAL ceiling across every guest renderer, browsers and apps together. Each side already had
|
||||
// its own cap (browsers 8, apps 6) but neither knew the other existed, so a busy canvas could still
|
||||
// stack 14 live renderers, and renderer memory pressure is exactly what evicts the wash (the
|
||||
// background band) and, at the limit, what OOMs the app. Pinned/working cards stay exempt: a
|
||||
// throttle must never blind an agent mid-task.
|
||||
export const MAX_LIVE_GUESTS = 10;
|
||||
|
||||
// Browsers live in redux, not in this map; the suspend hook wires in a counter so this module stays
|
||||
// store-free (and its tests stay pure).
|
||||
let p_browserLiveCount: () => number = () => 0;
|
||||
|
||||
export function wireBrowserLiveCounter(fn: () => number): void {
|
||||
p_browserLiveCount = fn;
|
||||
}
|
||||
|
||||
export function totalLiveGuests(): number {
|
||||
return live.size + p_browserLiveCount();
|
||||
}
|
||||
|
||||
export function guestBudgetHasRoom(): boolean {
|
||||
return totalLiveGuests() < MAX_LIVE_GUESTS;
|
||||
}
|
||||
|
||||
interface Slot {
|
||||
priority: number; // squared distance from viewport center; smaller = closer = kept when slots are scarce
|
||||
pinned: boolean; // actively used: never counted against the cap, never evicted
|
||||
@@ -43,7 +66,7 @@ export function requestAppSlot(key: string, priority: number, pinned: boolean):
|
||||
existing.pinned = pinned;
|
||||
return true;
|
||||
}
|
||||
if (pinned || evictableLiveCount() < MAX_LIVE_APP_WEBVIEWS) {
|
||||
if (pinned || (evictableLiveCount() < MAX_LIVE_APP_WEBVIEWS && guestBudgetHasRoom())) {
|
||||
live.set(key, { priority, pinned });
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user