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
7 changes: 7 additions & 0 deletions .changeset/context-interning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@siteimprove/alfa-selector": patch
---

**Fixed:** `Context.hover()`/`.active()`/`.focus()`/`.visit()` (the static factories) now return the same `Context` instance when called repeatedly for the same element, instead of constructing a fresh one every time. Since `alfa-cascade`/`alfa-style`'s caches are keyed by `Context` object identity, repeated queries for the same element under the same single-state context previously missed those caches on every call, even when nothing had actually changed.

This only pays off across *repeated* evaluations of the same `Document`/device (the cache is scoped to the page, not to one rule run) — e.g. `sia-r65` run a second time against an already-loaded page drops from ~131.4s to ~0.4s on a 10k-node fixture, with identical pass/fail outcomes before and after. A single, fresh evaluation (the normal case: each rule runs exactly once per page) sees no measurable change, since there is no prior pass to reuse. Kept as a correctness-neutral cache fix rather than a performance one.
25 changes: 21 additions & 4 deletions packages/alfa-selector/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Cache } from "@siteimprove/alfa-cache";
import type { Element } from "@siteimprove/alfa-dom";
import { Map } from "@siteimprove/alfa-map";

Expand Down Expand Up @@ -49,8 +50,18 @@ export class Context {
return this.addState(element, Context.State.Hover);
}

private static _hovered = Cache.empty<Element, Context>();

/**
* @remarks
* Interned per element: repeated calls for the same element return the
* same instance, so downstream `Cache<Context, _>` lookups (e.g. in
* `Cascade`/`Style`) can actually hit across separate call sites querying
* the same single-element context instead of always missing on a fresh
* object.
*/
public static hover(element: Element): Context {
return this.empty().hover(element);
return this._hovered.get(element, () => this.empty().hover(element));
}

public isHovered(element: Element): boolean {
Expand All @@ -61,8 +72,10 @@ export class Context {
return this.addState(element, Context.State.Active);
}

private static _activated = Cache.empty<Element, Context>();

public static active(element: Element): Context {
return this.empty().active(element);
return this._activated.get(element, () => this.empty().active(element));
}

public isActive(element: Element): boolean {
Expand All @@ -73,8 +86,10 @@ export class Context {
return this.addState(element, Context.State.Focus);
}

private static _focused = Cache.empty<Element, Context>();

public static focus(element: Element): Context {
return this.empty().focus(element);
return this._focused.get(element, () => this.empty().focus(element));
}

public isFocused(element: Element): boolean {
Expand All @@ -85,8 +100,10 @@ export class Context {
return this.addState(element, Context.State.Visited);
}

private static _visited = Cache.empty<Element, Context>();

public static visit(element: Element): Context {
return this.empty().visit(element);
return this._visited.get(element, () => this.empty().visit(element));
}

public isVisited(element: Element): boolean {
Expand Down