Architecture, Development: a large graph is moved, not drawn again (#292)
1 parent
f59d7604bf
commit
4ff5495274
2 files changed
+53
-4
No files matched your search
+52
-3
@@ -260,7 +260,7 @@ src/editor/graph/ GraphEditor (header, breadcrumb), GraphCanvas and what it
|
||||
`GraphCanvasHost`: wiring (making edges — a drag from a pin, the palette a drop into
|
||||
empty space opens, a transition armed from a state's menu), graphDrags (nodes, groups and
|
||||
comments moved or resized, each gesture one undo step), annotationLayer (the boxes behind
|
||||
the edges); graphView (zoom limits, grid, panAndZoom — shared with NodeBoard and the Tile tab's TileCanvas), EdgeLayer (curves and fold marks), graphMenus,
|
||||
the edges); graphView (zoom limits, grid, panAndZoom — shared with NodeBoard and the Tile tab's TileCanvas — and `inView`, which nodes are in the picture, #292), EdgeLayer (curves and fold marks), graphMenus,
|
||||
NodeView, NodePalette + paletteEntries (which also ranks a search), folding (hidden links, collapsed nodes), Breadcrumb + trail (the path into nested behaviors), beads (the run's pulses and the clock that launches them),
|
||||
AnnotationView + annotationDrag (groups and comments: what a drag carries, each thing moved once),
|
||||
SnapToggle (the magnet every tab that snaps carries, the vector art tab included, #194)
|
||||
@@ -356,8 +356,9 @@ gesture runs, its own copy of the rings is the truth and it never reads the mode
|
||||
Every other drag goes through `editor/framePreview.ts` (#213): `FramePreview.request()` in the
|
||||
move handler, `cancel()` on release, one instance per gesture owner. The model is still written on
|
||||
every pointer move, only `state.preview()` waits for the frame. The Scene view (moving objects,
|
||||
polygon corners, tile strokes), the Texture tab (frames, slice edges, collision corners) and the
|
||||
graph canvases (nodes, groups, comments) use it. Measured with 120 extra objects in the scene and
|
||||
polygon corners, tile strokes) and the Texture tab (frames, slice edges, collision corners) use
|
||||
it; the graph canvases did until #292 and now follow a drag without a project event at all, see
|
||||
below. Measured with 120 extra objects in the scene and
|
||||
120 pointer moves 8 ms apart: 26, 34 and 29 ms per move before, 18, 18 and 17 after (the probe's
|
||||
own pause and round trip are in every number). The Texture tab had been paying twice: Pixi repeats the
|
||||
last pointer move a few milliseconds later to update hover, with the same native event, and
|
||||
@@ -371,6 +372,54 @@ stop, at 47 ms per move. Now it is 51 to 61 events and 22 ms per move. The Pixel
|
||||
panel (the colour of a palette swatch, the opacity of a layer) and the Curve tab's `t` and `v`
|
||||
fields follow the same rule; those were changed without a measurement of their own.
|
||||
|
||||
**A graph of many nodes is moved, not drawn again** (#292). Five rules keep a graph of a few
|
||||
hundred nodes quick to pan, zoom and drag in, all in `editor/graph/`:
|
||||
|
||||
- *Panning and zooming move the picture.* `GraphCanvas.applyView` sets the position and scale of
|
||||
the graph and draws anew only what is laid out on the screen: the grid and the rubber band. It
|
||||
used to call `redraw()`, which rebuilt every box and every curve on every pointer move.
|
||||
- *A node draws its box and makes its texts only when what they show has changed.* `NodeView`
|
||||
keeps a signature of everything `redraw` reads and one of everything `rebuildTexts` reads, and
|
||||
returns early while they hold. A model event still asks every node (`GraphCanvas.sync`), and a
|
||||
run still calls `redraw()` every frame, but a text made anew is a picture uploaded anew, and that
|
||||
was the cost: one node dragged remade around 1500 texts per frame in a graph of 388 nodes.
|
||||
- *A drag does not announce a changed project.* `GraphDrags` writes the model on every pointer move
|
||||
and asks the canvas once a frame to follow (`GraphCanvasHost.moved`): the views are put where
|
||||
their nodes are and the links drawn again. Where a node stands concerns no panel, no run and no
|
||||
picture, so `state.preview()` is left out until the command of the release.
|
||||
- *Nodes outside the picture are not drawn.* `inView` in `graphView.ts` is the rule, `cull()` sets
|
||||
`visible` after every redraw and every change of the view. At a working zoom that is most of a
|
||||
large graph.
|
||||
- *The graph is a render group, and its shapes go into the batch.* `canvas.isRenderGroup` lets Pixi
|
||||
move the whole graph as one and keeps a node entering the picture from rebuilding the drawing of
|
||||
the whole editor. The mask sits on a container **above** the group: on the group itself it is
|
||||
panned and zoomed along with the graph, which cut the picture down to a small window. Pixi draws
|
||||
a `Graphics` of more than 200 vertices on its own, and the round pins of a node are that, so
|
||||
box and pins set `context.batchMode = 'batch'`: 474 draw calls a frame became 160.
|
||||
|
||||
Measured in the CI image with a behavior of 97 nodes and 135 links taken from a real project, and
|
||||
with four copies of it (388 nodes, 540 links), 240 pointer moves 16 ms apart, fitted to the canvas:
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| pan, 97 nodes: script per move | 6.7 ms | 1.9 ms |
|
||||
| pan, 388 nodes: script per move | 25.2 ms | 2.0 ms |
|
||||
| pan, 388 nodes: frames per second | 4.4 | 18.7 |
|
||||
| drag a node, 97 nodes: frames per second | 4.4 | 33.4 |
|
||||
| drag a node, 388 nodes: frames per second | 2.7 | 22.4 |
|
||||
| the still picture, 388 nodes: frames per second | 9 | 21 |
|
||||
|
||||
Zoomed in to 1 the same gestures run at 59 frames a second with 97 nodes and at 29 to 38 with 388.
|
||||
The frame rates are those of the test browser, whose GPU sits behind WSL's d3d12 driver and pays
|
||||
for every draw call: with the graph hidden altogether the Behavior tab draws at 37 frames a second
|
||||
there, because the panels around it cost 148 draw calls of their own, one pair per mask. The script
|
||||
times are the part that carries over to another machine.
|
||||
|
||||
What a check can see of this: `__gm.graph.made()` counts how often texts, boxes and links were
|
||||
really made, and `__gm.graph.nodesDrawn()` how many nodes are left in the picture. A probe that
|
||||
wants the frame rate of a picture nobody changes has to ask for every frame itself (`__gm.wake()`
|
||||
in a `requestAnimationFrame` loop): the editor draws on demand.
|
||||
|
||||
The Texture tab draws its frames in two layers (#220). `sheet` holds every frame but the selected
|
||||
one with its outlines, `overlay` the selected frame, its handles and a frame being spanned. Both
|
||||
are drawn without the pan, in a container that sits at the pan. A pan only moves that container,
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun
|
||||
# has and the web server's container has not (SKIP there)
|
||||
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
|
||||
php tests/php/releases.test.php # the release store (#266), the same way
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1656 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1660 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
|
||||
Reference in new issue
Block a user