# The semantics hold `DuskPlugin.install()` takes a semantics handle for the whole process, because every snapshot, `q` handle and actionability check reads the semantics tree. The cost is that every frame of a dusk-driven app also builds that tree, which an app without dusk (and without a screen reader) does not. `ext.dusk.semantics_hold` lets `dusk:perf_run --semantics-pass` measure the app without it, for one timed window. --- ## ext.dusk.semantics_hold | Param | Meaning | |---|---| | `action=release` | Drops dusk's handle. Refused unless a perf session is open, so the tree can only go away inside a timed window. | | `action=acquire` | Takes the handle again, then awaits one frame (bounded, 200 ms) so the tree exists again when it answers. | ```json {"action": "release", "released": true, "semanticsEnabled": false, "heldByPlatform": false} {"action": "acquire", "acquired": true, "semanticsEnabled": true, "treeReady": true} ``` `semanticsEnabled` stays `true` after a release when something else holds a handle: the tree only goes away when the last handle does. `heldByPlatform` is `platformDispatcher.semanticsEnabled`, true when the platform holds a handle of its own. On Flutter web that is every real app once it has sent a semantics tree: the engine turns semantics on at the first update and nothing turns it off, so a release there answers `semanticsEnabled: true, heldByPlatform: true`. On a native device it is true while an accessibility service runs. `heldByPlatform: false` with `semanticsEnabled: true` is another `SemanticsHandle` in the app (or the test harness). `DuskPlugin.semanticsReleased` reads the state in-app. The caller owns the pairing: acquire after `perf_end`, so the acquire's full tree rebuild is not a frame of the session, and acquire even when a step or `perf_end` failed. `perf_end` also re-takes a handle left released, as a safety net for a caller that died in between. `dusk:perf_run` does all of it. --- ## Driving while the tree is released Anything that resolves a target reads the tree, so while it is released nothing may: `ext.dusk.find`, a `q` action and a snapshot would all see no nodes, and a transient handle taken to look would rebuild the tree inside the window being measured. Resolve first, with the tree on, then dispatch by coordinates: | Extension | Records the point (tree on) | Dispatches by coordinates | |---|---|---| | `ext.dusk.tap` | `reportPoint: true` adds `point: {x, y}` | `{x, y}` with no `ref` | | `ext.dusk.drag` | `reportPoint: true` adds `from` and `to`; `startRef` plus `dx`/`dy` drags by an offset | `{x, y, toX, toY}` with no ref | | `ext.dusk.hover` | `reportPoint: true` adds `point` | (a wheel then goes through CDP) | A coordinate dispatch has no element, so none of the actionability gate's checks can run. Its response always carries: ```json "checks": {"gate": "skipped", "semantics": "released", "why": "..."} ``` `semantics` is `held` or `released`; the enabled check is the one that reads the tree. Nothing about a coordinate dispatch proves what it landed on: it is a replay of a point that was valid when the tree was on, for a screen that is expected to be in the same state. --- ## See also - [dusk:perf_run](../commands/dusk-perf-run.md#the-semantics-pass), which drives the pass. - [The actionability gate](actionability-gate.md).