|
1 | 1 | /** |
2 | | - * Which Bots' browsers to close, and when. |
| 2 | + * Which Bots' browsers to close, and when, and which of a Bot's tabs. |
3 | 3 | * |
4 | 4 | * Its own file, with no Playwright import, for the reason `authorisation.ts` and `bot-id.ts` are: |
5 | 5 | * `profiles.ts` imports Playwright at module scope, so anything living there needs a browser merely |
@@ -62,21 +62,96 @@ export function chooseEvictions( |
62 | 62 | const entries = [...running]; |
63 | 63 | if (entries.length <= max) return []; |
64 | 64 | // A cap of zero keeps nothing, the newest included; that is the mistake reading the setting guards. |
65 | | - const newest = |
66 | | - max > 0 |
67 | | - ? entries.reduce((latest, entry) => |
68 | | - entry[1].usedAt >= latest[1].usedAt ? entry : latest, |
69 | | - ) |
70 | | - : undefined; |
| 65 | + const newest = max > 0 ? newestOf(entries) : undefined; |
71 | 66 |
|
| 67 | + return inClosingOrder(entries, newest, goesLast) |
| 68 | + .slice(0, entries.length - max) |
| 69 | + .map(([botId]) => botId); |
| 70 | +} |
| 71 | + |
| 72 | +/** The most recently used, which is the one a launch has just added. Ties go to the later entry. */ |
| 73 | +function newestOf( |
| 74 | + entries: [string, Evictable][], |
| 75 | +): [string, Evictable] | undefined { |
| 76 | + if (entries.length === 0) return undefined; |
| 77 | + return entries.reduce((latest, entry) => |
| 78 | + entry[1].usedAt >= latest[1].usedAt ? entry : latest, |
| 79 | + ); |
| 80 | +} |
| 81 | + |
| 82 | +/** |
| 83 | + * The order browsers are closed in when some have to go: every Bot's before a person's native window |
| 84 | + * (`keptOpen`), and least recently used first within each. `spare` is left out, which is the newest. |
| 85 | + */ |
| 86 | +function inClosingOrder( |
| 87 | + entries: [string, Evictable][], |
| 88 | + spare: [string, Evictable] | undefined, |
| 89 | + goesLast: (botId: string) => boolean, |
| 90 | +): [string, Evictable][] { |
72 | 91 | return entries |
73 | | - .filter((entry) => entry !== newest) |
| 92 | + .filter((entry) => entry !== spare) |
74 | 93 | .sort( |
75 | 94 | ([aId, a], [bId, b]) => |
76 | 95 | Number(goesLast(aId)) - Number(goesLast(bId)) || a.usedAt - b.usedAt, |
| 96 | + ); |
| 97 | +} |
| 98 | + |
| 99 | +/** |
| 100 | + * How long after its last use a browser still counts as in use, and so is never closed for memory. |
| 101 | + * |
| 102 | + * A browser's use is recorded when something asks for its page: a Bot's step, a person's input, the |
| 103 | + * live screen. Between two steps a Bot asks for nothing while its model works out the next one, which |
| 104 | + * can take tens of seconds, and nothing here can tell that pause from a Bot that has finished. |
| 105 | + * Closing the browser then closes it in the middle of a run: the page the Bot was on is gone, and its |
| 106 | + * next step starts a cold browser, which is the very jump in memory the close was meant to head off. |
| 107 | + * A minute covers a model's turn between steps. It also covers a browser that has just launched, so |
| 108 | + * the next launch never closes the one before it, which is what would otherwise turn a full computer |
| 109 | + * into a loop of each Bot's launch closing the last Bot's browser. |
| 110 | + */ |
| 111 | +export const RECENTLY_USED_MS = 60_000; |
| 112 | + |
| 113 | +/** |
| 114 | + * Which to close next because the computer's memory runs high (`container-memory.ts`). |
| 115 | + * |
| 116 | + * One at a time, because the caller reads memory again after each close and stops as soon as it is |
| 117 | + * enough: a count says in advance how many have to go, and bytes do not. Least recently used first, |
| 118 | + * the cap's order, so the two never disagree about who goes first. |
| 119 | + * |
| 120 | + * Never a browser in use, which here means four things: |
| 121 | + * - The one being launched. The caller runs this before a launch and passes only the browsers |
| 122 | + * already running, so the Bot that asked is not among them. |
| 123 | + * - The most recently used of those, however long ago that was. So at least one browser is always |
| 124 | + * left, because one browser that is on its own too big for the computer is the limit's problem, |
| 125 | + * and closing it would only start it again on its next step. |
| 126 | + * - Any used within `recentlyUsedMs` ({@link RECENTLY_USED_MS}), which is a Bot in the middle of a |
| 127 | + * run or a browser that has just started. |
| 128 | + * - Any `spared` names, which is a Bot whose wheel a person holds or has been asked to take: closing |
| 129 | + * it closes it under somebody's hands, or under a page the Bot is waiting for somebody to work. |
| 130 | + * |
| 131 | + * And never a person's own browser, cloud or native. Their Recorder runs in it, and closing it ends |
| 132 | + * what they were recording without a word to them. It still counts toward the cap, which closes it |
| 133 | + * last; memory pressure is not a reason to end somebody's session. |
| 134 | + */ |
| 135 | +export function chooseForMemory( |
| 136 | + running: Iterable<[string, Evictable]>, |
| 137 | + now: number, |
| 138 | + { |
| 139 | + recentlyUsedMs = RECENTLY_USED_MS, |
| 140 | + spared = () => false, |
| 141 | + }: { |
| 142 | + recentlyUsedMs?: number; |
| 143 | + spared?: (botId: string) => boolean; |
| 144 | + } = {}, |
| 145 | +): string | undefined { |
| 146 | + const entries = [...running]; |
| 147 | + const cutoff = now - recentlyUsedMs; |
| 148 | + return inClosingOrder(entries, newestOf(entries), () => false) |
| 149 | + .filter( |
| 150 | + ([botId, entry]) => |
| 151 | + entry.usedAt <= cutoff && !isPersonBrowserId(botId) && !spared(botId), |
77 | 152 | ) |
78 | | - .slice(0, entries.length - max) |
79 | | - .map(([botId]) => botId); |
| 153 | + .map(([botId]) => botId) |
| 154 | + .at(0); |
80 | 155 | } |
81 | 156 |
|
82 | 157 | /** |
@@ -104,3 +179,40 @@ export function chooseIdle( |
104 | 179 | .filter(([botId, entry]) => entry.usedAt <= cutoff && !stays(botId)) |
105 | 180 | .map(([botId]) => botId); |
106 | 181 | } |
| 182 | + |
| 183 | +/** Enough of a tab for the choice. Keeps this file independent of what else is on a page. */ |
| 184 | +export type IdleTab = { |
| 185 | + /** When the Bot or a person last acted on it, it navigated, or it was the page in front. */ |
| 186 | + usedAt: number; |
| 187 | + /** The page the Bot is on, which is the one the live screen shows and its next action lands in. */ |
| 188 | + front: boolean; |
| 189 | + /** A page that is never the Bot's to lose: an extension's own (the Recorder's) or DevTools. */ |
| 190 | + kept?: boolean; |
| 191 | +}; |
| 192 | + |
| 193 | +/** |
| 194 | + * Which of a Bot's background tabs to close because nothing has touched them. |
| 195 | + * |
| 196 | + * A Bot is always on its newest page (`chooseLivePage`): a window a site opens is where its work |
| 197 | + * went, and it has no way back to an older tab except that newer ones close. So a tab left behind |
| 198 | + * (the page a sign-in popup was opened from, a link that opened in a new tab and was never closed) |
| 199 | + * holds a renderer and its memory for nothing, for as long as the browser stays up. Closed past a |
| 200 | + * timeout rather than at once, because an opener whose popup is still mid sign-in is waiting for |
| 201 | + * that popup to come back to it. |
| 202 | + * |
| 203 | + * Never the page in front, which is the one the Bot is on, so closing tabs never moves it: it stays |
| 204 | + * the newest open page, which is what the Bot follows. And never anything when which page is in |
| 205 | + * front is not exactly one, because then any tab could be the one the Bot is on. That also means the |
| 206 | + * last tab is never closed, since a single tab is the one in front. A timeout of zero or less |
| 207 | + * switches this off. |
| 208 | + */ |
| 209 | +export function chooseIdleTabs<T extends IdleTab>( |
| 210 | + tabs: readonly T[], |
| 211 | + idleTimeoutMs: number, |
| 212 | + now: number, |
| 213 | +): T[] { |
| 214 | + if (idleTimeoutMs <= 0) return []; |
| 215 | + if (tabs.filter((tab) => tab.front).length !== 1) return []; |
| 216 | + const cutoff = now - idleTimeoutMs; |
| 217 | + return tabs.filter((tab) => !tab.front && !tab.kept && tab.usedAt <= cutoff); |
| 218 | +} |
0 commit comments