Skip to content

dazpy script-call batching + find_skeleton race fix - #39

Merged
ghirpara merged 14 commits into
masterfrom
feature/script-call-batching
Aug 22, 2026
Merged

dazpy script-call batching + find_skeleton race fix#39
ghirpara merged 14 commits into
masterfrom
feature/script-call-batching

Conversation

@ghirpara

@ghirpara ghirpara commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Implements the dazpy script-call batching epic: Batch.add_operation()/add_prelude() with size limits, DazElement.snapshot()/set_properties(), DazNode.set_transform(), DazSkeleton.set_state(), single-call zero_figure(), one-request async batch submission (execute_batch_async), and main-thread wait time exposed in /metrics.
  • Documents batching semantics, limits, and async batch submission in docs/api/batch.rst and docs/quickstart.rst.
  • Fixes daz-script-server-xtkd: DazScene.find_skeleton() now retries Scene.getSkeletonList() on a transient miss (observed under load from a burst of other main-thread script calls) instead of immediately raising NodeNotFoundError.

Test plan

  • python -m pytest tests/test_dazpy.py -q (710 passed)
  • python -m pytest tests/test_dazpy_aio.py -q (26 passed)
  • ./build.sh build --clean / ./build.sh build --sdk-version 6 --clean — both SDK4 and SDK6 built and installed cleanly (only pre-existing warnings)
  • Manual Studio smoke test (DAZ Studio 4, SDK4 build, v2.9.2, live Genesis 9 scene with 11 skeletons):
    • 20 individual set_bone_rotations() calls → 20 HTTP requests (/metrics total_requests delta confirms).
    • Same 20 mutations via Batch.add_operation() → 1 HTTP request.
    • avg_main_thread_wait_ms/max_main_thread_wait_ms populate correctly under load, confirming the Task 6 metric works end-to-end.
    • execute_batch_async() with 3 operations: one request_id, completed in 1ms, result correctly keyed _r0/_r1/_r2 in submission order.
    • find_skeleton() sanity check against the exact skeletons from the original bug report (Genesis9, Genesis9Eyelashes, Genesis9Tear) — all resolve correctly under normal load. (Did not attempt to reproduce the original race, which needs the same rapid restore() burst that previously froze Studio.)
    • One unrelated finding, not a regression: set_bone_rotations()/bone_rotations() round-trip evaluated values as raw values, so ERC-driven bones (auto-follow twist bones, foot chain) double-count the ERC contribution on restore — same class of issue DazPose already works around by using raw values. Scene was fully restored to its exact original pose after testing (verified per-bone).

🤖 Generated with Claude Code

ghirpara and others added 14 commits August 21, 2026 22:06
…zSceneState.apply()

DazPose.apply_full() rewrites every property on a skeleton, including the
internal FID_* property DAZ Studio uses to persist a conforming item's fit
registration -- rewriting it desyncs the live getFollowTarget() pointer
without raising. apply() now captures/restores each skeleton's conform/
fit-to relationship explicitly via getFollowTarget()/fit_to()/unfit(),
with busy-retry, independent of the generic pose restore. Closes daz-script-server-jz0e.

Also distinguish DzBackgroundProgress vs DzProgress in
mainThreadBusyMessage() so busy responses during scene-load streaming
report a more specific reason than the generic fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pins current client.execute() call counts for snapshot(), reset_transforms(),
zero_figure() default mode, and Batch.add() so upcoming batching fixes
(Tasks 2-4 of the script-call-batching plan) have a before/after to diff
against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
snapshot() previously called get_property() once per field, issuing one
HTTP round-trip per field. It now reads all requested fields in one
generated script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
set_transform() writes any combination of local position/rotation/scale in
one DazScript evaluation. reset_transforms() now calls it once instead of
issuing three separate set_local_position/set_local_rotation/set_scale calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds DazSkeleton._zero_bones_and_morphs(), a single generated script that
zeroes every bone rotation and non-zero DzMorph. zero_figure()'s default
(include_props=False) path now calls it once instead of issuing separate
bone_rotations()/morph_values()/set_bone_rotations()/set_morph_values()
round-trips. The include_props=True path (DazPose.apply_full()) is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Writes multiple properties by display label in a single DazScript
evaluation, returning {label: bool} indicating which labels resolved to a
real property.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ites

Sets any combination of bone rotations, morph values, and node properties
in one DazScript evaluation. bones/morphs use plain setValue(), matching
set_bone_rotations()/set_morph_values() exactly; props also uses plain
setValue() (not apply_full()'s ERC-avoidance raw-value path).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
add_operation() lets callers pass a result expression directly instead of
guessing the internally generated _rN key name (fixing add()'s own stale
docstring example). add_prelude() lets multiple operations share one setup
block emitted once. max_operations/max_script_length guard against an
oversized batch reaching Studio's main thread, raising
BatchLimitExceededError client-side before any HTTP call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
execute_batch_async() (DazClient and AsyncDazClient) composes a list of
{body_lines, result_expression} operations into one generated script via
the new build_operations_script() helper, then submits it through the
existing /execute/async route -- no C++ changes needed, since one script +
args was already all AsyncExecuteHandler accepts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds MetricsCollector::recordMainThreadWait()/getAvgMainThreadWaitMs()/
getMaxMainThreadWaitMs(), threads the HTTP-thread accepted-at timestamp
into handleExecuteRequest(), and surfaces avg/max_main_thread_wait_ms
as additive /metrics fields.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds add_operation()/add_prelude(), size limits, whole-batch
failure/no-rollback semantics, and execute_batch_async() to
docs/api/batch.rst, plus a before/after bulk-update example and
sync-vs-async guidance in docs/quickstart.rst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scene.getSkeletonList() has been observed to momentarily omit a skeleton
that is actually present, under load from a burst of other main-thread
script calls (daz-script-server-xtkd) -- most likely a race against DAZ
Studio's own deferred/background scene-graph work, in the same family as
dpi-mxq's partial-main-thread-execution-under-contention. find_skeleton()
now retries the lookup a few times with a short backoff before concluding
the skeleton is really absent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the dazpy script-call batching epic and the find_skeleton
retry-on-transient-miss fix landed on this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ghirpara
ghirpara merged commit c4f001e into master Aug 22, 2026
6 checks passed
@ghirpara
ghirpara deleted the feature/script-call-batching branch August 22, 2026 02:41
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