mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-14 10:56:33 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db3a9e88e2 | ||
|
|
65435c06f5 |
No files matched your search
@@ -89,6 +89,15 @@ test("a visible pane past the cap releases the least recently seen hidden pane",
|
||||
assert.equal(budget.getActiveCount(), 3);
|
||||
});
|
||||
|
||||
test("new visible panes are refused when visible holders fill the cap", () => {
|
||||
const { budget } = setup(2);
|
||||
const members = ["a", "b", "c", "d"].map((name) => new FakeMember(name));
|
||||
assert.deepEqual(members.map((member) => budget.tryAcquire(member)), [true, true, false, false]);
|
||||
assert.equal(budget.tryAcquire(members[0]), true, "an existing holder keeps its slot");
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
assert.deepEqual(members.map((member) => member.releases), [0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
test("recently hidden panes are released only once they stay hidden long enough", () => {
|
||||
const { clock, budget } = setup(2);
|
||||
const [a, b] = ["a", "b"].map((name) => new FakeMember(name));
|
||||
@@ -101,6 +110,8 @@ test("recently hidden panes are released only once they stay hidden long enough"
|
||||
budget.tryAcquire(new FakeMember("shown"));
|
||||
assert.equal(a.releases, 0, "a quick tab flip must not lose the renderer");
|
||||
assert.equal(budget.getActiveCount(), 3);
|
||||
assert.equal(budget.tryAcquire(new FakeMember("overflow")), false);
|
||||
assert.equal(budget.getActiveCount(), 3, "one hidden holder allows only one extra context");
|
||||
|
||||
clock.advance(2_999);
|
||||
assert.equal(a.releases, 0);
|
||||
@@ -110,26 +121,33 @@ test("recently hidden panes are released only once they stay hidden long enough"
|
||||
assert.equal(clock.pendingTimers, 0);
|
||||
});
|
||||
|
||||
test("a pane hidden after the budget went over is released once eligible", () => {
|
||||
test("a pane hidden after a grace overshoot is released once eligible", () => {
|
||||
const { clock, budget } = setup(2);
|
||||
const [a, b, c] = ["a", "b", "c"].map((name) => new FakeMember(name));
|
||||
budget.tryAcquire(a);
|
||||
budget.tryAcquire(b);
|
||||
budget.tryAcquire(c);
|
||||
assert.equal(budget.getActiveCount(), 3, "three visible panes all keep WebGL");
|
||||
assert.equal(budget.tryAcquire(c), false);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
assert.equal(clock.pendingTimers, 0);
|
||||
|
||||
a.hiddenSince = clock.now();
|
||||
budget.noteHidden(a);
|
||||
assert.equal(budget.tryAcquire(c), true);
|
||||
assert.equal(budget.getActiveCount(), 3, "recently hidden holders allow a grace overshoot");
|
||||
a.hiddenSince = null;
|
||||
b.hiddenSince = clock.now();
|
||||
budget.noteHidden(b);
|
||||
clock.advance(3_000);
|
||||
|
||||
assert.equal(b.releases, 1);
|
||||
assert.equal(a.releases, 0, "a holder shown again keeps its renderer");
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
});
|
||||
|
||||
test("a hidden pane cannot take a context once the budget is full", () => {
|
||||
const { budget } = setup(2);
|
||||
budget.tryAcquire(new FakeMember("a"));
|
||||
const { clock, budget } = setup(2);
|
||||
const a = new FakeMember("a");
|
||||
budget.tryAcquire(a);
|
||||
budget.tryAcquire(new FakeMember("b"));
|
||||
|
||||
const hidden = new FakeMember("hidden");
|
||||
@@ -138,7 +156,13 @@ test("a hidden pane cannot take a context once the budget is full", () => {
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
|
||||
hidden.hiddenSince = null;
|
||||
assert.equal(budget.tryAcquire(hidden), true, "it gets one as soon as it is shown");
|
||||
assert.equal(budget.tryAcquire(hidden), false, "visible holders still fill the cap");
|
||||
a.hiddenSince = clock.now();
|
||||
budget.noteHidden(a);
|
||||
clock.advance(3_000);
|
||||
assert.equal(budget.tryAcquire(hidden), true, "a shown pane can replace an eligible hidden holder");
|
||||
assert.equal(a.releases, 1);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
});
|
||||
|
||||
test("released and forgotten panes free their slot", () => {
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
*
|
||||
* The budget stays below the browser limit by releasing the renderers of panes
|
||||
* that have been hidden for a while, least recently seen first. Visible panes
|
||||
* are never released and always get a context. A released pane builds a new
|
||||
* renderer when it is shown again. Below the cap nothing changes.
|
||||
* are never released, but new ones stay on DOM when visible holders fill the
|
||||
* cap. Recently hidden holders may temporarily exceed the cap during tab flips.
|
||||
* A released pane can rebuild when shown if the budget permits.
|
||||
*/
|
||||
|
||||
export const MAX_ACTIVE_WEBGL_CONTEXTS = 12;
|
||||
@@ -31,8 +32,8 @@ export interface WebglContextBudgetMember {
|
||||
|
||||
export interface WebglContextBudget {
|
||||
/**
|
||||
* Reserve a context for a member about to create its renderer. Visible
|
||||
* members always get one; hidden members only while the budget has room.
|
||||
* Reserve a context if the budget has room. Visible members may replace
|
||||
* hidden holders, allowing a temporary overshoot until their grace expires.
|
||||
*/
|
||||
tryAcquire(member: WebglContextBudgetMember): boolean;
|
||||
/** Give the slot back once the member's renderer is gone. Idempotent. */
|
||||
@@ -102,7 +103,11 @@ export function createWebglContextBudget(
|
||||
return {
|
||||
tryAcquire(member) {
|
||||
if (active.has(member)) return true;
|
||||
if (member.getHiddenSince() !== null && active.size >= maxActive) return false;
|
||||
if (active.size >= maxActive) {
|
||||
if (member.getHiddenSince() !== null) return false;
|
||||
const visibleCount = [...active].filter((holder) => holder.getHiddenSince() === null).length;
|
||||
if (visibleCount >= maxActive) return false;
|
||||
}
|
||||
active.add(member);
|
||||
releaseOverflow();
|
||||
return true;
|
||||
|
||||
@@ -186,20 +186,93 @@ test("panes past the budget give up WebGL after staying hidden, and get it back
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
});
|
||||
|
||||
test("simultaneous visible ensures stay within the context budget", () => {
|
||||
const { budget, createPane } = setup();
|
||||
const panes = Array.from({ length: 13 }, () => createPane());
|
||||
for (const pane of panes) pane.controller.ensure();
|
||||
|
||||
assert.deepEqual(panes.map((pane) => pane.loaded), [...Array(12).fill(true), false]);
|
||||
assert.equal(panes[12].addons.length, 0);
|
||||
assert.equal(budget.getActiveCount(), 12);
|
||||
});
|
||||
|
||||
test("visible context loss recovers at the cap, including with a refused visible pane", () => {
|
||||
for (const withOverflow of [false, true]) {
|
||||
const { clock, budget, createPane } = setup(2);
|
||||
const panes = [createPane(), createPane()];
|
||||
for (const pane of panes) pane.controller.ensure();
|
||||
const overflow = withOverflow ? createPane() : null;
|
||||
overflow?.controller.ensure();
|
||||
assert.equal(overflow?.addons.length ?? 0, 0);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
|
||||
panes[0].addons[0].loseContext();
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
clock.advance(49);
|
||||
assert.equal(panes[0].loaded, false);
|
||||
clock.advance(1);
|
||||
assert.equal(panes[0].loaded, true);
|
||||
assert.equal(panes[0].addons.length, 2);
|
||||
assert.equal(panes[1].addons.length, 1);
|
||||
overflow?.controller.ensure();
|
||||
assert.equal(overflow?.addons.length ?? 0, 0);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
assert.equal(clock.pendingTimers, 0);
|
||||
assert.ok(panes.every((pane) => !pane.warnings.some((warning) => warning.includes("staying on DOM renderer"))));
|
||||
}
|
||||
});
|
||||
|
||||
test("visible loss stays on DOM if another visible pane takes the freed slot before recovery", () => {
|
||||
const { clock, budget, createPane } = setup(2);
|
||||
const panes = [createPane(), createPane(), createPane()];
|
||||
for (const pane of panes) pane.controller.ensure();
|
||||
assert.equal(panes[2].loaded, false);
|
||||
|
||||
panes[0].addons[0].loseContext();
|
||||
panes[2].controller.ensure();
|
||||
assert.equal(panes[2].loaded, true);
|
||||
clock.advance(50);
|
||||
|
||||
assert.equal(panes[0].loaded, false);
|
||||
assert.deepEqual(panes.map((pane) => pane.addons.length), [1, 1, 1]);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
assert.equal(clock.pendingTimers, 0);
|
||||
assert.ok(panes.every((pane) => !pane.warnings.some((warning) => warning.includes("staying on DOM renderer"))));
|
||||
});
|
||||
|
||||
test("a hidden pane mounted past the budget keeps the DOM renderer until shown", () => {
|
||||
const { budget, createPane } = setup(1);
|
||||
createPane().controller.ensure();
|
||||
const { clock, budget, createPane } = setup(1);
|
||||
const occupant = createPane();
|
||||
occupant.controller.ensure();
|
||||
|
||||
const hidden = createPane(false);
|
||||
hidden.controller.ensure();
|
||||
assert.equal(hidden.addons.length, 0);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
|
||||
occupant.setVisible(false);
|
||||
clock.advance(3_000);
|
||||
hidden.reveal();
|
||||
assert.equal(hidden.loaded, true);
|
||||
assert.equal(occupant.loaded, false);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
});
|
||||
|
||||
test("a pane on screen is never refused or released, even if a sync was missed", () => {
|
||||
test("revealing a hidden pane is refused while visible holders fill the budget", () => {
|
||||
const { budget, createPane } = setup(1);
|
||||
const occupant = createPane();
|
||||
occupant.controller.ensure();
|
||||
const hidden = createPane(false);
|
||||
hidden.controller.ensure();
|
||||
hidden.reveal();
|
||||
|
||||
assert.equal(hidden.loaded, false);
|
||||
assert.equal(hidden.addons.length, 0);
|
||||
assert.equal(occupant.loaded, true);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
});
|
||||
|
||||
test("a holder on screen is never released, even if a sync was missed", () => {
|
||||
const { clock, budget, createPane } = setup(1);
|
||||
const visible = createPane();
|
||||
visible.controller.ensure();
|
||||
@@ -210,12 +283,37 @@ test("a pane on screen is never refused or released, even if a sync was missed",
|
||||
createPane().controller.ensure();
|
||||
clock.advance(10_000);
|
||||
assert.equal(visible.loaded, true);
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
|
||||
const late = createPane(false);
|
||||
late.setVisibleWithoutSync(true);
|
||||
late.controller.ensure();
|
||||
assert.equal(late.loaded, true);
|
||||
assert.equal(late.loaded, false);
|
||||
assert.equal(late.addons.length, 0);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
});
|
||||
|
||||
test("grace overflow never releases a holder shown again without a visibility sync", () => {
|
||||
const { clock, budget, createPane } = setup(1);
|
||||
const holder = createPane();
|
||||
holder.controller.ensure();
|
||||
holder.setVisible(false);
|
||||
const front = createPane();
|
||||
front.controller.ensure();
|
||||
assert.equal(budget.getActiveCount(), 2);
|
||||
|
||||
holder.setVisibleWithoutSync(true);
|
||||
clock.advance(3_000);
|
||||
assert.equal(holder.loaded, true);
|
||||
assert.equal(front.loaded, true);
|
||||
assert.equal(holder.addons[0].disposeCalls, 0);
|
||||
|
||||
front.addons[0].loseContext();
|
||||
clock.advance(50);
|
||||
assert.equal(front.loaded, false, "recovery cannot exceed a cap already filled by visible holders");
|
||||
assert.equal(front.addons.length, 1);
|
||||
assert.equal(budget.getActiveCount(), 1);
|
||||
assert.equal(clock.pendingTimers, 0);
|
||||
});
|
||||
|
||||
test("suspend, dispose and failed loads return the budget slot", () => {
|
||||
|
||||
@@ -65,7 +65,7 @@ export function createWebglRendererController<TAddon extends WebglRendererAddon>
|
||||
|
||||
// isVisible() is the source of truth; hiddenSince only remembers when the
|
||||
// pane was last seen. Re-reading it at every decision keeps a pane that is
|
||||
// on screen from being refused or released even if a sync was missed.
|
||||
// on screen from being released even if a sync was missed.
|
||||
const readVisibility = () => {
|
||||
if (!options.isVisible) return;
|
||||
if (options.isVisible()) hiddenSince = null;
|
||||
@@ -94,7 +94,7 @@ export function createWebglRendererController<TAddon extends WebglRendererAddon>
|
||||
|
||||
const ensure = () => {
|
||||
if (loaded || circuitBroken || disposed || !options.enabled) return;
|
||||
// A hidden pane past the budget keeps the DOM renderer until it is shown.
|
||||
// Panes past the budget keep the DOM renderer until a slot is available.
|
||||
if (budget && !budget.tryAcquire(budgetMember)) return;
|
||||
let nextAddon: TAddon | null = null;
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user