Driving Issie from outside it
scripts/inspect-canvas.js reads what the draw block is showing. This is the other half: sending
Issie messages, asking what state it is in, and knowing when it has finished responding.
|
Issie must be running a debug build — npm run app -- -d, or npm run dev, since the harness
is published only when JSHelpers.debugLevel > 0. It can start simulations and open projects,
which is not something a shipped build should offer a page.
Why it exists
The alternative is synthesising DOM events and reading rendered text back, and that fails in three ways that cost real time:
-
Waiting. With nothing to wait on, every step becomes a guessed
sleep. Too short and it races; too long and a five-step sequence takes a minute. -
Clicking. A click has to find the right element, and "the element whose text is X" is wrong
often enough to matter — a tab that needs the event on its
<a>and not its<li>, a menu left open over the canvas, a button whose text isSee Problemsbecause the design has an error. - Reading. Text scraped from the DOM can be a frame out of date while looking authoritative. This is the one that causes wrong conclusions rather than slow ones.
A script
One <name> [arg] per line; # comments and blank lines ignored. Each line waits for the render
its message caused before the next is sent, so no sleeps are needed anywhere.
openProject C:
openSheet main5
rightTab Simulation
simSubTab StepSim
startSimulation
What refs is for
|
A FastSimulation is reachable from several places and only one of them is the model, so "is it
still in memory" cannot be answered from the model alone. It also cannot reliably be answered from
the heap: usedJSHeapSize counts typed arrays, does not shrink until a collection runs, and says
nothing about which reference is keeping something alive. Ending a simulation shows
inModel and stepCache drop to 0 immediately, while usedHeapMB stays where it was until the
next GC — the counts are the truthful signal.
This was added because finding one retained simulation took a heap snapshot with a retainer path
and an A/B build. refs answers the same question in one call.
Measuring simulation speed: benchmark and rerun
|
|
Simulation speed must be measured in the app rather than under .NET (simulatorStructure.md), and these are how. Four things they are shaped around, each of which caused a wrong answer before it was:
-
The build is not the run. On a 480,000-component design the build is ~33s and a run is
~1.9s, so a profiler wrapped around
benchmarkprofiles the build. Wrap it aroundrerun, which reuses whatbenchmarkbuilt. -
*
seriesMsis every repetition, in order.* The median alone cannot show whether the warm-up was long enough. A first repetition slower than the rest means the JIT is still tiering up; on a small design that is the first 1–2ms and nothing after, and on a large one it is invisible. -
The second argument is the step array size, overriding
SimulationView.Constants.maxArraySize. It is the only thing that changes the distance between the words a clock cycle touches without changing the work, which is what makes "is this design memory-bound?" a question with an answer. -
A benchmark retains its simulation so that
reruncan use it, and a heap left near its limit slows down everything measured afterwards.endSimulationdrops it. Compare only measurements taken in comparable heap states, and readusedHeapMBin the reply to know which one you were in.
Run-to-run variance on a laptop with performance and efficiency cores is up to 2x, so repeat, and do not believe a single number.
Adding a command
send takes a name from a fixed table, not a serialised Msg. A Msg is an F# union carrying
models, canvases and functions; nothing useful survives a round trip through JSON, and a
dispatch-anything surface in a debug build is a hazard for the sake of messages nobody wanted to
send. Add a row to commands in src/Renderer/UI/DevHarness.fs
— that is the intended way to extend it. Each row sends the message the corresponding UI element
sends, so driving the app from here and driving it by hand cannot diverge.
How the render signal works
Renderer.view' calls DevHarness.recordModel before the view runs and DevHarness.renderDone
after it. Waiting callbacks are resolved on the next animation frame rather than immediately:
the view returning means React has been given the new elements, not that the DOM holds them, and a
caller waiting for a render wants the state it then reads to be the state it waited for.
A command that changes nothing renders nothing, so send bounds its wait rather than hanging, and
reports (no render within the timeout) if none arrived.
A window nobody can see does not render
Chromium treats a covered window as hidden, and a hidden page runs no requestAnimationFrame —
which is how Issie renders, since Elmish batches the view into one. A dev window left behind the
terminal driving it therefore stops rendering altogether: every send reports no render, every
Input.dispatchMouseEvent takes five seconds to be acknowledged, and a CPU profile of a drag shows
the renderer idle throughout. Nothing in any of that says "the window was covered"; it reads as the
app being slow, which is the worst possible answer to give someone measuring why the app is slow.
scripts/start.js turns occlusion tracking off for this reason, so npm run dev renders whether
or not its window is visible. If you launch Electron yourself, pass the same switches:
--disable-features=CalculateNativeWinOcclusion --disable-backgrounding-occluded-windows
--disable-renderer-backgrounding
document.visibilityState is what to check when results stop making sense: hidden while the
window is plainly on screen means occlusion tracking has decided otherwise, and every timing taken
in that state is worthless.