Skip to content

perf(binding): compact measure-callback constraints - #52

Closed
hyfdev wants to merge 3 commits into
mainfrom
agent/measure-callback-cheap-01a02a18
Closed

hyfdev wants to merge 3 commits into
mainfrom
agent/measure-callback-cheap-01a02a18

Conversation

@hyfdev

@hyfdev hyfdev commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Measure callbacks receive the same public MeasureArgs values. The private crossing now carries the constraints in a caller-owned reusable Float64Array record, and the callback argument is only the existing getStyle handle. Extra properties on the returned size are ignored.

tree.computeLayout({
  root,
  availableSpace: { width: 400, height: 800 },
  measure({ knownDimensions, availableSpace, node, getStyle }) {
    return { width: 40, height: 16 };
  },
});

A return of { width: 40, height: 16, extra: 3 } measures as { width: 40, height: 16 }. Missing fields, null, arrays, and non-number payloads still throw TypeError.

Why

Each measured layout pauses into JavaScript once per exact request. The cost of that pause was dominated by building a nested constraint object per request and enumerating the returned object's keys. In the coding-agent chat scenarios, #computeMeasuredLayout itself was 68.9% and 77.6% of CPU on the @taffyjs/node path.

The record keeps the private transport independent of the public representation, so no numeric slot has to double as its own tag:

  • Presence of each known dimension and the AvailableSpaceKind of each axis occupy one tag slot. A caller-supplied definite -1 or -2 therefore stays a definite available space rather than becoming MinContent or MaxContent.
  • Taffy's node id occupies two slots as 32-bit halves. Its slot version advances on every reuse, so ordinary node churn walks the id past the range a double holds exactly; split halves keep every id addressing the measure function and context that belong to it.
  • The wrapper rebuilds knownDimensions, availableSpace, and NodeId before calling the user function, and those values stay ordinary mutable objects created per request.

One shared record serves the common case. A measure callback may lay out another tree, so a nested compute takes its own record and leaves the outer one untouched.

This does not change how often Taffy asks for a measurement, and it does not make @taffyjs/node match yoga-layout's callback count.

Performance

Same machine, Node v24.19.0, local vp run benchmark profile, main and this branch measured in alternating rounds. Scenarios were not edited.

Scenario, @taffyjs/node main This branch
coding-agent-chat-initial-layout 3.5746 ms 2.2199 ms
coding-agent-chat-viewport-resize 1.4675 ms 0.9132 ms

The yoga-layout control moved by less than the spread between rounds: 0.8129 ms and 0.9057 ms for the initial layout, 0.2078 ms and 0.2085 ms for the resize.

benchmarks/results/published.json is unchanged.

Validation

  • vp run check
  • vp run check:wasm
  • A definite -1 and -2 available space reaches the callback as Definite, alongside the existing -4, -0, NaN, and infinity cases
  • A node id past the exact-double range still reaches its own measure function and context
  • Extra-field measure return succeeds without changing the measured size

hyfdev and others added 3 commits August 23, 2026 00:11
Pass five numbers across the private measure crossing and reconstruct
public MeasureArgs in the wrapper. Ignore extra fields on the returned size.
Encode absent known sizes as NaN, MinContent as -1, and MaxContent as -2.
Layout sizes are non-negative, so those values do not collide with a definite size.
Constraints cross in a caller-owned Float64Array that the binding fills
before each request, and the callback argument is only the getStyle
handle. Presence and AvailableSpaceKind live in their own tag slot, so a
numeric slot never doubles as its own tag: a caller-supplied definite -1
or -2 stays a definite available space instead of arriving as MinContent
or MaxContent. Taffy's node id crosses as two 32-bit halves, so a slot
whose version has advanced past the double-precision range still reaches
the right measure function instead of failing the layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016docmM5eMBWUXY4vvhztBN
@hyfdev

hyfdev commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Landed on main as 8e9d6ce.

That commit carries the measure constraints in a caller-owned reusable Float64Array record rather than in named fields on the callback argument, and the callback argument is only the getStyle handle. Every numeric slot then carries only its value: presence of each known dimension and the AvailableSpaceKind of each axis occupy one tag slot, so a caller-supplied definite -1 or -2 stays a definite available space, and Taffy's node id occupies two slots as 32-bit halves, so an id whose slot version has advanced past the range a double holds exactly still reaches its own measure function and context.

Ignoring extra own properties on a measure result lands unchanged.

@hyfdev hyfdev closed this Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant