Repository navigation
0.0.17
Fixed
-
dusk:perf_campaignkeeps the credentials it reads out of the processes it starts. Hooks got the whole inherited environment spread underDUSK_PERF_*, and the preparation processes inherited it too, so a serverhooks.before_campaignbackgrounded kept the password the campaign reads through${env.*}for as long as it lived. The loader now names every variable a${env.NAME}read (PerfLoadResult.envNames,PerfSetupLoadResult.envNames,PerfCampaign.secretEnvNames), and hooks and preparation processes run withincludeParentEnvironment: falseon the invoking environment minus those names. The filter is by name, not by value:CI=1survives a secret that reads1. artisanstartandstoprun in-process with no environment seam, so theflutter run, the Chrome and the Androidadb force-stopthey spawn still inherit the dispatcher's environment; the campaign doc says so. (lib/src/perf/scenario.dart,lib/src/perf/scenario_loader.dart,lib/src/perf/campaign.dart,lib/src/commands/dusk_perf_campaign_command.dart,doc/commands/dusk-perf-campaign.md) -
perf_run's restart wait names the app's last answer. Since the boot wait became
pollDuskBoot, a read could start after the pause that spent the budget, with no time left, and itsTimeoutExceptionreplaced the app's own last error in the failure; no read starts once the budget is spent now. The campaign also survives a stale.errit cannot delete (reported, like a write it cannot make) and aflutter-dev.logwith malformed UTF-8 (decoded leniently), both of which escaped the per-scenario catch and skippedafter_campaignand the envelope. (lib/src/perf/perf_run_driver.dart,lib/src/commands/dusk_perf_campaign_command.dart) -
dusk:perf_campaign --jsonprints its envelope however the campaign ends. A stop in the preparation (a hook,flutter pub get, an Android step) returned before the envelope, so a--jsoncaller read nothing; abefore_scenariostop listed only the scenarios tried. The envelope now lists every selected scenario, those never tried asstatus: "not_run"withattempts: 0, carries the stop sentence asstopped, and names anything that failed after the scenarios inerrors. An artisan stop that fails after the campaign now exits 1 (it kept exit 0), and an.errthat cannot be written inside the per-scenario catch is reported through the output instead of escaping it and ending the campaign before the app stop. (lib/src/commands/dusk_perf_campaign_command.dart,doc/commands/dusk-perf-campaign.md) -
dusk:perf_campaign'safter_startwaits for the Router as long as its guards wait. The Router wait beforeafter_startgave up at perf_run's 10 s, so a cold start that mounted its login screen at 15 s failed before awhenguard written for 60 s ever polled.PerfSetupRunner.awaitRoutertakes an optionalbudget(perf_run keeps its 10 s,kPerfRouterBudget), and the campaign passes the largesttimeout_msamong theafter_startguards, the 60 s guard default when there is none, never less than 10 s. (lib/src/perf/perf_setup_runner.dart,lib/src/commands/dusk_perf_campaign_command.dart,doc/commands/dusk-perf-campaign.md) -
A secret shorter than 4 characters is a load problem. Every output is masked for every secret wherever its text appears, so a secret
1or80masked that number in every log line, run file and envelope.loadPerfScenariosandloadPerfSetupnow refuse a non-empty${env.*}value orsecret: trueparam under 4 characters (kPerfMinSecretLength), naming the variable or the param and its length, never the value. (lib/src/perf/scenario.dart,lib/src/perf/scenario_loader.dart,doc/commands/dusk-perf-run.md) -
ext.dusk.navigate_backleaves a hidden shell branch alone. It popped the first poppable Navigator in tree order, offstage ones included, so in a go_routerStatefulShellRoute(every branch kept alive, the inactive ones underOffstageand a disabledTickerMode) a page stacked on a hidden branch was popped while the visible branch stayed, and the answer saidpopped: true. A subtree underOffstage(offstage: true),TickerMode(enabled: false)orVisibility(visible: false)is no longer walked. (lib/src/extensions/ext_navigation.dart) -
A
perf_runsetup navigate lands on its own path, and a late landing is on the record. The late-landing poll accepted a path under the route, so a dropped navigate to/monitorswhile a web hot restart had left the app on/monitors/<id>passed as landed; it now needs the exact path, query ignored. A unit whose setup relied on a late landing carriessetupLandedLate: truein itsrepeats[]entry. (lib/src/commands/dusk_perf_run_command.dart) -
perf_run's post-idle route check compares paths. It compared whole URIs, so a page that normalises its query after the first fetch (/monitorsto/monitors?page=1) failed setup as moved. (lib/src/commands/dusk_perf_run_command.dart) -
perf_tracecuts its frames byperf_end's rule. Under a clock mismatch (frames carry a timestamp and none lands in the window)perf_endkeeps every frame while the trace dropped them all, so the two artifacts disagreed silently. The trace now sharesperf_end's window cut, keeps the frames, and says so withotherData.sessionClockMismatch: true.buildPerfReportforwardssessionClockMismatchthe wayanalysePerftakes it. (lib/src/extensions/ext_perf.dart,lib/src/extensions/ext_perf_trace.dart,lib/src/utils/perf_insights.dart) -
The CDP emulation tools say how long their override lasts.
dusk_resize_viewportanddusk_device_profilepromised a resize, and neither can passhold: each call opens its own CDP session and closes it on return, and Chrome drops theEmulation.*override with it. Both descriptors, both CLI descriptions anddusk:device's output now say the override lasts only as long as the call's CDP session (onlydusk:device's window size stays), anddusk:resizereports "override sent" rather than "Viewport set to", pointing at--hold. (lib/src/dusk_artisan_provider.dart,lib/src/commands/dusk_resize_command.dart,lib/src/commands/dusk_device_command.dart) -
ext.dusk.navigate_backpops a page stacked inside a shell. It popped the first Navigator in the tree, which in a go_router app is the root one, and that holds the shell alone: for a detail page pushed inside aShellRoute(MagicRouter layouts,.stacked()) the pop did nothing, the answer was stillnavigatedBack: true, andget_routes'surikept naming the detail page because it was still mounted. It pops the outermost Navigator that can pop instead, so a page pushed on the root is left before a shell's.uriitself was not stale: it follows the Router's report within the frames the handler awaits. The answer gains an additivepopped,falsewhen no Navigator could pop, so a no-op no longer reads as a pop. (lib/src/extensions/ext_navigation.dart,lib/src/dusk_artisan_provider.dart,doc/mcp/tool-reference.md,skills/fluttersdk-dusk/references/mcp-tools.md) -
The semantics pass re-acquires after
perf_end, not inside the window.perf_runcalledext.dusk.semantics_hold action=acquirebeforeperf_end, so the acquire's frame, a rebuild of the whole semantics tree, was a frame of the session, andperf_endreadenv.semanticsEnabledafter it: everysemanticsOffrepeat reportedtrue. The session now closes first and the acquire follows, and it still runs when a step orperf_endthrows. (lib/src/commands/dusk_perf_run_command.dart,lib/src/extensions/ext_semantics_hold.dart,doc/commands/dusk-perf-run.md,doc/reference/semantics-hold.md) -
The semantics pass no longer reports a release that left semantics on as measured. On Flutter web the engine turns semantics on at the first semantics tree a real app sends and never turns it off, so once dusk had snapshotted the app, releasing dusk's handle left the tree on: a measured uptizm list scroll showed the
SEMANTICSblock on 58 of 60semanticsOffframes, as many as the attribution series, undersemanticsPass: "measured". The release answer gainsheldByPlatform(platformDispatcher.semanticsEnabled), and a release that answerssemanticsEnabled: trueends the pass:semanticsPass: "unsupported", nosemanticsOffseries, and asemanticsPassReasonnaming the platform and what holds semantics on (the web engine on chrome, an accessibility service on a device, or anotherSemanticsHandle). (lib/src/extensions/ext_semantics_hold.dart,lib/src/commands/dusk_perf_run_command.dart,lib/src/dusk_artisan_provider.dart,doc/commands/dusk-perf-run.md,doc/reference/semantics-hold.md,doc/mcp/tool-reference.md) -
perf_runnavigates once the app can route.ext.dusk.boot_idanswers as soon asDuskPlugin.install()has run, which a host that installs dusk before its own boot (magic_devtools' documented order) reaches beforerunAppmounts its Router, so the first setupnavigateafter every hot restart answerednavigated: falseand every Chrome scenario that navigates failed at its first unit. A setup navigate now waits, up to 10 s, forget_routesto report a mounted Router, and holds anavigated: falseagainst the Router for up to 3 s before failing: a Router that has only just mounted is still applying its first location when the navigate reads it (measured:falseas it mounted,true300 ms later). The route is never dispatched twice. (lib/src/commands/dusk_perf_run_command.dart,doc/commands/dusk-perf-run.md) -
perf_run's post-idle route check can see a redirect. It comparedget_routes'slocationbefore and after network idle, which a Router-based app answers as""on both sides; it comparesurinow, and the setup diagnostics quote it (route none (no Router mounted)when there is none). (lib/src/commands/dusk_perf_run_command.dart) -
perf_enddescribes its own window. A list scroll on the Pixel 8 emulator drew 24 frames and the summary held 31: timings the engine had parked beforeperf_beginarrive after the buffer is cleared, and the idle frameperf_enddraws to flush the tail is a frame of its own. Frames are now kept byvsyncStartUsinside the session window (a record without one is kept), the denominator stays the liveness advance read before the flush, andcoverage.framesOutsideSessioncounts what was read and left out.vsyncStartUsagainstFlutterTimeline.nowis unverified off the web; a disagreement shows as a largeframesOutsideSession. (lib/src/extensions/ext_perf.dart,lib/src/utils/perf_insights.dart,doc/commands/dusk-perf-end.md) -
perf_tracecounts the frames it leaves out. A frame outside the session window was dropped silently;otherData.framesOutsideWindownow says how many. (lib/src/extensions/ext_perf_trace.dart,doc/reference/perf-trace.md) -
ext.dusk.find_by_labelfinds labels in a running app. It walkedrootPipelineOwneralone, so a view mounted under a child pipeline owner (the test harness, a multi-view app) matched nothing; it now walks every owner asobserveandsnapdo. (lib/src/extensions/ext_wait_find.dart) -
dusk:resizesays what it can keep. Chrome dropsEmulation.setDeviceMetricsOverridewhen the DevTools session that sent it detaches, whatever its parameters (measured against Chrome: 390 px inside the session, the page's own width right after), so the command set a viewport and exited, and the success line said it was set. A new--holdkeeps the session open until Ctrl-C or until Chrome exits; without it the command now warns that the override ends with it. (lib/src/commands/dusk_resize_command.dart,doc/commands/index.md,doc/commands/dusk-screenshot.md) -
perf_runsetup failures say which screen the app was on. Await_for_textthat timed out, or a setup gesture that matched nothing, named only the text, so a navigate the router had dropped read the same as a slow page. Every setup failure but a restart's now ends with aDiagnostics:line: the routeext.dusk.get_routesanswers, the last setup navigate's payload and the three newestext.dusk.exceptionsentries. A setupnavigateansweringnavigated: falsefails at once with its payload instead of being ignored, and after each one the runner waits for network idle and reads the route again, failing when the app has moved (an auth redirect after the first fetch). (lib/src/commands/dusk_perf_run_command.dart,doc/commands/dusk-perf-run.md) -
perf_runwaits up to 3 s for a target. A target the screen had not built yet failed the run as "matched nothing" on the first lookup; it is now looked up every 100 ms for up to 3 s (30 lookups at most) first. (lib/src/commands/dusk_perf_run_command.dart) -
perf_runresolves outside the window what it can. Every target was resolved inside the measured window, so each lookup (and a wheel's hover) was session cost. A target onlywaitsteps precede is now resolved beforeperf_begin, a wheel's hover point with it (PerfStepVerb.movesTargets); a target an earlier step can create or move, such as an option in the overlay a tap opens, still resolves just before its step. Each repeat in the run file gainsresolves: [{step, verb, phase: beforeBegin|inWindow, resolveMs}], so the in-window cost is visible. (lib/src/commands/dusk_perf_run_command.dart,lib/src/perf/scenario.dart,doc/commands/dusk-perf-run.md) -
The run file keeps the renderer the app reported.
env.rendererwas always overwritten with the run log scrape, which readsunknownon web, so thecanvaskitorskwasmanswer fromrendererReadernever reached the file. The app's answer now wins, and the scrape is used only when the app answersunknown. (lib/src/commands/dusk_perf_run_command.dart,doc/commands/dusk-perf-run.md) -
perf_runsetup waits survive a slow web restart.wait_for_textnow waits in 5 s slices until its budget is spent: DWDS abandons any service extension call at 10 s, so one 15 s in-app wait failed as a -32603 whenever the app took longer than 10 s to show the text. -
The actionability gate no longer refuses every ref once the soft keyboard has opened. The stable check measured the live rect against the rect the snapshot minted, so any reflow since the snapshot read as motion: on Android, the keyboard that
fillitself opens failed the nextfillandtapwithnot stable (rect changed by 21.0px)and kept failing until a re-snap, however still the page was. Both samples are now live, one frame apart, as the check was documented to do. The off-viewport check now measures against the view minus itsviewInsets, and scrolls a target in when its center is outside that area: a submit button laid out under the keyboard used to pass as on-screen and then fail withobscured by other widget (top=_RenderInkFeatures), the Scaffold's own Material in the inset. With noScrollableto bring it up, such a target is refused asoff-viewport. Check order and reason substrings are unchanged. (lib/src/utils/actionability_gate.dart,doc/reference/actionability-gate.md) -
A scenario
wheelcan scroll like a wheel.ticks: Nsends N events ofdx/dy, one frame apart, at the one point the step resolved. A single 1200 px event jumped the uptizm monitor list in one frame and the session measured five. -
perf_endno longer loses the frames a session drew last. The web engine hands frame timings over only from inside a later frame, 100 ms after the previous hand-over, so the tail of a session stayed parked and a list scroll reported 1 of 5 frames.perf_endnow waits past that interval and draws one idle frame (after the liveness verdict, so a hidden page still refuses) before it reads.
Added
-
hooks.after_campaign: a campaign's teardown.before_campaigncould start services and nothing stopped them (a server bound to0.0.0.0outlived every campaign).after_campaignruns once after the final app stop, however the campaign ended, a failedbefore_campaignincluded (it may have started half of what the teardown stops), and not when the command refused its input or selected nothing. It gets the hook environment plusDUSK_PERF_STATUS=ok|failed; a failure exits 1, is listed in the envelope'serrorsand appended tocampaign-<label>.err, and never hides the scenario results. (lib/src/perf/campaign.dart,lib/src/commands/dusk_perf_campaign_command.dart,doc/commands/dusk-perf-campaign.md) -
ext.dusk.navigateanswersexactPath. Its verdict stays a prefix match, the documented intent (a navigate to/monitorsthat shows/monitors/7, a default child, navigated), and the payload now says whether the Router's path is the route's own, query aside:exactPath: truefor the route itself,falsefor a page under it and besidenavigated: false. Additive;dusk:perf_run's setup keeps its own exact check. (lib/src/extensions/ext_navigation.dart,lib/src/dusk_artisan_provider.dart,doc/mcp/tool-reference.md,skills/fluttersdk-dusk/) -
The run file's setup echoes its
whenguards.PerfSetupStep.toJsonwrote a guarded step as if it always ran, so nothing in a run file said a guard stood in front of it. Each guard is now echoed once, marking where its group begins, aswhen: {text, unless_text, timeout_ms}, on the first step it governs (PerfSetupStep.toJson(opens:),PerfSetupGuard.toJson); an enclosing guard entered on the same step is itsparent, and a bare verb there becomes{verb: null, when}. Unguarded steps are unchanged, anddusk:perf_comparereads no setup. (lib/src/perf/scenario.dart,doc/commands/dusk-perf-run.md) -
dusk:perf_campaign: a whole perf campaign in one command.dusk:perf_campaign <campaign.yaml> --platform=<chrome|android|ios>filters the campaign's scenarios by platform and--only(nothing selected exits 1 before any hook or process runs), runshooks.before_campaignthrough/bin/sh -cwithDUSK_PERF_PLATFORM,DUSK_PERF_LABELandDUSK_PERF_OUT,flutter pub get, and on Android bootsandroid.avd, waits forsys.boot_completed,adb reverses each port and installs and grants a profile APK. Each scenario then gets up toretries + 1attempts, each from a cold start:hooks.before_scenario(plusDUSK_PERF_SCENARIO), artisan stop, a wait until the old pid is gone and its ports are free, artisan start, a poll ofext.dusk.boot_id,after_startonce a Router is mounted, anddusk:perf_runin-process. A failed attempt, whatever it threw, is recorded in<out>/<scenario>-<label>.err(withflutter-dev.logwhen the start failed) and the campaign goes on; the app is stopped at the end; one line per scenario (or a--jsonenvelope) and exit 1 when any failed. Every line,.err, run file and envelope is masked for the campaign's secrets: the run file perf_run wrote is masked again for theafter_startones, a diagnostic exception message is masked before it is cut to 200 characters, and the--jsonenvelopes of both commands are tree-masked and printed once, so a numeric secret cannot break their JSON. Aflutter,adbor hook shell that cannot start stops the campaign with<exe> could not start, a--cdp-portthat is not a port exits 1 before anything runs, anandroid.grantentry or GradleapplicationIdthat is not[A-Za-z0-9_.]+is refused before it reaches the device shell, and a scenario that passed on a retry printsok (attempt N, see <err>).dusk_perf_runtakesvariant. CLI only, no MCP tool. (lib/src/commands/dusk_perf_campaign_command.dart,lib/src/dusk_artisan_provider.dart,doc/commands/dusk-perf-campaign.md) -
dusk:perf_runloads fragments and variants, runswhenguards and masks secrets. Both the scenario and--againstload throughloadPerfScenarios, soinclude,${...}andvariantswork in a run.--variant=<key>picks one variant (required when the file declaresvariants, the error listing the keys; refused when it does not; applied to--againsttoo), and the run file carries a top-levelvariantwhen one was picked. An include'swhenguard pollsext.dusk.find --textevery 250 ms up totimeout_ms,unless_textfirst:textruns the group,unless_textskips it, a timeout skips it withoutunless_textand fails the run naming both texts with one. Every line the command prints and every string in the run files it writes is masked for the loaded secrets, raw and JSON-encoded (PerfRedactor,RedactingOutput). The setup and step execution moved into the publicPerfSetupRunner(run,awaitRouter,diagnose) andPerfActions, which the campaign command reuses; existing scenarios run exactly as before. (lib/src/commands/dusk_perf_run_command.dart,lib/src/perf/perf_actions.dart,lib/src/perf/perf_setup_runner.dart,lib/src/perf/perf_redaction.dart,doc/commands/dusk-perf-run.md) -
Scenario fragments, interpolation, secrets and variants.
loadPerfScenarios(path, env:)loads a scenario file and answers(scenarios, secrets): a setup entry- include: <path>withwith:andwhen:flattens a fragment (params,when,steps, nested up to 8 deep, cycles refused) intosetup, each flattened entry keeping its origin (fragments/login.yaml steps[1]) and thewhenguard as aPerfSetupGuard; every scalar is interpolated once (${param},${env.NAME},$$); a value from the environment or asecret: trueparam may only be the text of afillortype, is markedPerfStep.secret, written as***bytoJsonand masked in every parse problem;variants: {<key>: {viewport, platforms, repeat, steps}}yields<name>-<key>scenarios validated one by one.loadPerfSetupreads a bare entry list (a campaign'safter_start:) the same way.PerfScenario.parserefusesincludeandvariants, and reads$$as$. (lib/src/perf/scenario.dart,lib/src/perf/scenario_loader.dart,doc/commands/dusk-perf-run.md) -
rendererReader, a seventh cross-package pointer, fillsenv.rendererin theperf_endreport. Its default is dusk's own answer:skwasmorcanvaskiton web,unknownelsewhere, where the launch log scrape ofdusk:perf_runcovers native. A host may reassign it. Exported from the barrel. (lib/src/utils/perf_readers.dart,lib/src/extensions/ext_perf.dart,lib/dusk.dart) -
perfInteractionAt(int us), exported. The interaction whose window[startUs, closedAtUs ?? now]holds a recorded time, the newest when windows overlap; for a host that recorded a time without recording the gesture behind it. (lib/src/utils/perf_interaction.dart,lib/dusk.dart) -
dusk:perf_insight/dusk_perf_insight/ext.dusk.perf_insight: drill into one insight of the lastperf_endreport. Takesid(and an optionaltokennaming the report's session) and returns Title / Summary / Detail / EstimatedSavings / NextStep, wheredetailis the raw rows behind the insight: the worst frames with their self-time blocks, the frame-number gaps, the frames where one block weighed most, or what a coverage gap left out. Ids are assigned before the report cuts its list, so an insight counted inomitted.insightsis still drillable. An unknown id, a stale token or a refused session answers an error naming what to read instead; an unknown id points atperf_end'sinsights[]. (lib/src/extensions/ext_perf.dart,lib/src/commands/dusk_perf_insight_command.dart,lib/src/dusk_artisan_provider.dart,doc/commands/dusk-perf-insight.md) -
perf_begingainsmode: attribution|timing.timingtouches nodebugProfile*flag and no collection flag, and its report carries frame timings only: the profiling that makes attribution possible inflates every duration it wraps, so milliseconds are only comparable between timing sessions.phaseswithtimingis rejected, as is an unknown mode, before anything is touched. (lib/src/extensions/ext_perf.dart,lib/src/commands/dusk_perf_begin_command.dart) -
perfInsightContributors, a fifth cross-package pointer, and a purebuildPerfReport, both exported from the barrel. The host (magic_devtools) appends rules that need to know what a wind or magic counter means; a contributor that throws or returns a malformed insight becomes onewarninsight (contributorErrors) and never costs the report.buildPerfReport(framePerf, extras, wind, env: ...)is the function the extension calls, so a host's conformance test builds the same report from the same maps. (lib/src/utils/perf_insights.dart,lib/src/utils/perf_readers.dart,lib/dusk.dart) -
A closable interaction per dusk gesture, carried in the zone. While a perf session is open, every verb that dispatches into the app (
tap,dblclick,triple_click,right_click,hover,drag,type,clear,fill,press_key,focus,blur,scroll,select_option,set_checkbox,navigate,navigate_back,dismiss_modals,reset_overlays) runs its dispatch insiderunZoned(zoneValues: {#fluttersdk_interaction: handle}), so the Timers, Futures and subscriptions its callbacks start can name the gesture that caused them.PerfInteraction {id, verb, target, startUs, closedAtUs}closes at settle (no frame scheduled for 300 ms, or 5 s, or the session closing), and a closed handle reads as absent: a socket opened during a tap keeps its zone forever and would otherwise be attributed to it for good. Frame-zone work (builds,initStaterefetches, post-frame callbacks), which the zone cannot reach, joins throughactiveInteraction(). A verb dispatched inside another joins its interaction, sofillis one. Outside a session nothing changes: no handle, no zone, no timer; the actionability gate and gesture dispatch are untouched.PerfInteractionandactiveInteractionare exported from the barrel. (lib/src/utils/perf_interaction.dart, the gesture extensions,lib/dusk.dart,doc/reference/perf-trace.md) -
ext.dusk.perf_trace: the closed session's timeline as Chrome Trace Event JSON, which ui.perfetto.dev andchrome://tracingopen as is. Interactions and frames (placed atvsyncStartUs) areXslices; host rows from the newperfTimelineReaderpointer map by kind, a span with anidto an asyncb/epair, one without toX, an instant toi, a counter toC. AnXslice that would straddle another on its track moves to an overflow lane (frames (2)) rather than being trimmed. Only what started inside the session window is exported; a malformed host row is counted inotherData.skippedRows. Takes an optionaltoken; answers an error before any session closed, for a stale token, while a newer session is open (itsperf_begincleared the buffers the trace reads), and when a host reader throws.dusk:perf_trace/dusk_perf_tracewrite it to a file. (lib/src/extensions/ext_perf_trace.dart,lib/src/utils/perf_readers.dart,lib/src/extensions/register_dusk_extensions.dart,doc/reference/perf-trace.md) -
ext.dusk.perf_endacceptsfull: 'true', which lifts every cut: block rankings, counter breakdowns, route transitions and insights carry every row andomittedreads all zeros. For a runner that writes the report to a file; the default stays bounded to about 6 KB.buildPerfReportgains the samefullflag. (lib/src/extensions/ext_perf.dart,lib/src/utils/perf_insights.dart,doc/commands/dusk-perf-end.md) -
dusk:perf_run/dusk_perf_run: a perf scenario, repeated from a clean start, to a file. A scenario YAML namessetup(hot_restart,navigate,wait_for_text,wait_for_network_idle, plus the gesturestap,fill,type,press_key,wheel,dragandwaitwith the steps' grammar and validation, run before every repeat and beforeperf_begin, so the path to the measured screen is not measured),steps(tap,fill,type,press_key,scroll,wheel,drag,navigate,resize,wait, each optionallyonly: [...]),platforms,viewport,repeatandthresholds. A target is exactly one of{text},{label},{role, name},{key}plus an optionalindex(not on a key or a label), resolved on the live screen right before its step;{role, name}is the nodedusk:snapprints as that role and name, listed throughext.dusk.observewith snap's roles (button,textbox,checkbox,link,heading,image), sinceext.dusk.find_by_labelwalks only the root pipeline owner and finds nothing in a running app; a literal ref, awheelorresizethat could run off Chrome, and a name or label outside[a-z0-9_-]are rejected, every problem at once.hot_restartis a hot restart on debug and a relaunch throughartisan restarton a build that cannot (env.restartMode); either way the runner waits, 90 s at most after a hot restart, forext.dusk.boot_idto answer a new id, not for a new isolate, which Flutter web never gets (DWDS keeps isolate"1"). Each repeat brings Chrome to front, thenperf_begin, the steps, a 300 ms settle andperf_end full=true. Writes<out>/<scenario>-<label>.json:env(plushostand the run log'srenderer),summary(medians of counts per painted frame and ms, with aspreadblock), the median repeat'sinsights, every repeat and the scenario;--jsonprints it minusrepeats[].--timinginterleaves timing-mode repeats,--against <baseline.yaml>runs a second scenario in the same rounds, the order alternating each round. A refused repeat is recorded and left out of the medians; the command exits 1 only when every repeat refused. (lib/src/commands/dusk_perf_run_command.dart,lib/src/perf/scenario.dart,doc/commands/dusk-perf-run.md) -
dusk:perf_run --semantics-passandext.dusk.semantics_hold. The pass records where eachtap,dragandwheeldispatched with the tree on, then replays them by coordinates with dusk's process-wide semantics handle released for the timed window only (action=releaseafterperf_begin, refused outside a session;action=acquireafterperf_end, awaiting one frame so the tree exists again, run even when a step failed). Reported as asemanticsOffseries; a scenario with afill,typeorscroll, or a replay that fails, recordssemanticsPass: "unsupported"with the reason instead of failing the run.DuskPlugingainsreleaseSemantics,acquireSemanticsandsemanticsReleased. (lib/src/extensions/ext_semantics_hold.dart,lib/src/dusk_plugin.dart,doc/reference/semantics-hold.md) -
Coordinate dispatch and reported points on
ext.dusk.tap,dragandhover.tap {x, y}anddrag {x, y, toX, toY}dispatch with no ref and read nothing from the semantics tree; theirchecksblock always saysgate: skippedand whether the handle isheldorreleased.reportPoint: trueadds the dispatchedpoint(tap, hover) orfrom/to(drag), anddragacceptsstartRefplusdx/dyin place ofendRef. Ref dispatch is otherwise unchanged. (lib/src/extensions/ext_pointer.dart) -
dusk:perf_compare/dusk_perf_compare: judge one run file against another. Gates on counts per painted frame, never raw counts, so a run that drew 10% fewer frames with the same per-frame counts isunchanged, notimproved; milliseconds only from timing-mode medians; emulator raster ms as info. A change inside either run's repeat-to-repeat range isunchanged. Default thresholds warn +10% and error +25%, overridden by the scenario. Prints a compact table, or the JSON with--json; exits 1 on an error-level regression. (lib/src/commands/dusk_perf_compare_command.dart,doc/commands/dusk-perf-compare.md) -
dusk:perf_trace/dusk_perf_trace: writeext.dusk.perf_traceto a Chrome Trace JSON file and print only its path. (lib/src/commands/dusk_perf_trace_command.dart,doc/commands/dusk-perf-trace.md) -
ext.dusk.boot_id, internal: the idDuskPlugin.install()mints for each run ofmain()(DuskPlugin.bootId). Registered last, so an answer means every other extension is registered too. No CLI command or MCP tool wraps it;dusk:perf_runreads it to tell a restarted app from the one before. (lib/src/extensions/ext_boot.dart,lib/src/dusk_plugin.dart,lib/src/extensions/register_dusk_extensions.dart)
Changed
-
The artisan and contracts floors move to
fluttersdk_artisan ^0.0.17andfluttersdk_wind_diagnostics_contracts ^1.2.0.dusk:perf_campaignreadsStartCommand.browserDevices, public from artisan 0.0.17, instead of a local copy, and its Android runs rely on the profile launch and force-stop artisan 0.0.17 ships; contracts 1.2.0 documents thewidgetBuilds,wrapperEmissionsandinheritedReadsstats keys the perf snapshot reads. (pubspec.yaml,lib/src/commands/dusk_perf_campaign_command.dart,doc/getting-started/,CLAUDE.md) -
dusk:perf_campaignopens one driver per attempt.after_startanddusk:perf_rundescribed the host (asysctlorunamespawn) and read the run log twice per attempt, each through its ownconnectArtisanPerfRun; the attempt now opens the driver once and hands it toDuskPerfRunCommand.connected, which runs on it and leaves it to its owner to close. The cold start's boot wait and perf_run's restart wait share one loop (pollDuskBoot), andPerfRunDriver,PerfRunEnvironmentandPerfRunExceptionmoved tolib/src/perf/perf_run_driver.dart, so the perf library no longer imports a command. (lib/src/commands/dusk_perf_campaign_command.dart,lib/src/commands/dusk_perf_run_command.dart,lib/src/perf/perf_run_driver.dart,lib/src/perf/perf_support.dart) -
dusk:installgates the install under!kReleaseMode, and every doc says the same.dusk:perf_runrelaunches the app as a profile build, and theif (kDebugMode)block the installer wrote registered noext.dusk.*there, so the firstboot_idorperf_begincall against an app set up by the installer failed. The injected block, theMagicDuskIntegrationblock and the import (show kReleaseMode) now match theDuskPlugindocblock; release builds still tree-shake the branch. An app wired underkDebugModebefore this is left alone: the installer now checks for theDuskPlugin.install()andMagicDuskIntegration.install()calls rather than for its own snippet, so a re-run adds neither a second block nor an unused import, and such an app keeps working in debug until its guard is changed by hand. Thedusk:installpage also stops claiming the installer wiresWind.installDebugResolver(); it never did. (lib/src/commands/dusk_install_command.dart,install.yaml,README.md,ARCHITECTURE.md,CLAUDE.md,doc/getting-started/,doc/commands/dusk-install.md,doc/plugins/,doc/mcp/tool-reference.md,skills/fluttersdk-dusk/,example/lib/main.dart) -
ext.dusk.get_routesanswers the Router's location asuri.locationreads the root Navigator's top page name, which a Router-based app (go_router, MagicRouter) leaves empty on every screen, so a caller had no way to see where the app was. The newurifield is the first mounted Router's location, the valueext.dusk.navigatealready verifies against, and null while no Router is mounted;locationandtitleare unchanged. (lib/src/extensions/ext_navigation.dart,lib/src/dusk_artisan_provider.dart,doc/mcp/tool-reference.md) -
yamlmoves from dev_dependencies to dependencies, for the scenario filesdusk:perf_runreads. No consumer graph gains a package:fluttersdk_artisan, which dusk already requires, depends onyaml ^3.1.3itself. The only importer islib/src/perf/scenario.dart, reached from the host-sidedusk:perf_runanddusk:perf_comparecommands; in an app it sits behind the same!kReleaseModebranch as the rest of dusk and is tree-shaken from release. (pubspec.yaml,CLAUDE.md) -
Breaking:
perfSessionBeginHookreceives the session'sPerfMode(void Function(PerfMode mode)), so a host can leave wind's counting off in atimingsession. Counting sits onWindParser.parse, the hottest path in the framework, and switched on regardless it inflated exactly the milliseconds timing mode exists to report. A host assigning() {}no longer compiles; assign(PerfMode mode) {}. (lib/src/utils/perf_readers.dart,lib/src/extensions/ext_perf.dart,doc/commands/dusk-perf-begin.md) -
Breaking:
ext.dusk.perf_endanswers an LLM-first report instead of a raw dump, with no alias of the old keys. A real session's payload was 7 to 13 KB of mixed micros and millis, top-N lists that did not say what they cut, and an overhead caveat buried in prose. The payload is now{sessionToken, refused, mode, env, coverage, summary, counters, insights, omitted}, bounded to about 6 KB for a 3600-frame session over 500 block names. Every duration is in ms against a statedbudgetMsof 16.7;summary.framesgivescount,painted,dropped(frame-number gaps), over-budget counts split by thread, and p50/p90/p99/worst build and raster; blocks are ranked by SELF time (selfMicros, never the nestedmicros, which blames a parent for its child's work) and by count per painted frame; every counter is given raw and per painted frame;omittedcounts what each ranked list cut.envstatesplatform,isWeb,buildMode(fromkProfileMode/kDebugMode),semanticsEnabledandphases.coverage.missingnames sources never read, socounters.wind: nullplusmissing: ['wind']is no longer confusable with a wind that counted nothing. Built-in insight rules (over budget, dropped frames, a dominant self-time block, a count-per-frame outlier, incomplete coverage) each state their threshold in the evidence. Removed top-level keys:frameSummary,blockAttribution,note,wind,magic,liveness,phases(nowenv.phases); the refusal carries the liveness numbers undercoverage. The stalled-engine refusal (1 frame or fewer) and the flag receipt are unchanged. (lib/src/extensions/ext_perf.dart,lib/src/utils/frame_summary.dart,lib/src/utils/perf_insights.dart,lib/src/commands/dusk_perf_end_command.dart,doc/commands/dusk-perf-end.md,doc/mcp/tool-reference.md) -
perfExtrasReaderdocuments the full magic key set (controllerNotifies,notifyCauses,queryReloads,actions,events,casts,timerTicks,broadcasts,routeTransitions), and its default returns an empty structure for each. Hosts that still return only the first and last keep working: unknown or absent keys cost nothing. (lib/src/utils/perf_readers.dart) -
ext.dusk.snapregisters in profile builds too (!kReleaseModeinstead ofkDebugMode), so a profile-mode perf session can still be driven.ext.dusk.evaluatestays debug-only. (lib/src/extensions/ext_snapshot.dart)