Skip to content

perf(runtime): compute a statement's order and stamped footprint once per model - #1355

Merged
HuiJun merged 4 commits into
developfrom
perf/stamp-statement-footprint-once
Oct 21, 2026
Merged

HuiJun merged 4 commits into
developfrom
perf/stamp-statement-footprint-once

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The 0.11.0 performance record against v0.10.1 (#1337) attributes the growth of every executor statement row — ActionLoop +48 % allocations at a thousand passes, ActionChain/chain1000 +22 % allocations and +72 % time, StateLoop and ExecuteState +12 %, ExecuteAction +15 %, the bytes of MaterializePlainAttributes and SetFeatureValueNoDependents — to #1136 (transitive order dependence), by first-parent bisection with exact allocation counts. Three per-execution costs on the statement path cause it:

  • An assignment's footprint was stamped on every execution. actionStmtHost.statementFootprint stamps the statement's footprint with the performance's own places so propagateMarks records the write as the performance's; placesOf cloned the read and write place slices on every call, two allocations per executed assignment. The stamped footprint depends only on the statement and the performed node, so it is now memoized per model (Model.stamped), and a footprint with nothing to stamp is used as it is.
  • A body's statement order was computed once per executor, not once per model. performances.statementOrder memoized lower.BodyStatementOrder on the performances value, which is one per ActionExecutor — so a chain of a thousand actions rebuilt every body's order on every run, and a state's entry or do behavior, whose host makes a fresh performances per entry (behaviorHost), rebuilt its order on every entry, allocating a synthetic ActionGraph to do so. The memo is now Model.statementOrder, keyed by the body's node, its first statement's syntax (lower.StatementNode, exported for it) and its length — one list per model has those, and lowered statement slices are per executor, so their addresses could not key it.
  • The third cost, two closures per transition taken (StateExecutor.taking / Context.underControl), is the other half of the state rows and is fixed separately in its own pull request, so each change stays one mechanism.

No behaviour changes: the orders and footprints computed are the same values, computed once.

How it was verified

Failing-before differential tests in internal/exec/runtime/statement_footprint_stamp_test.go (testing.AllocsPerRun, the cost of one more unit as the difference between 2n and n units; develop → this branch, allocations exact):

test develop this branch bound
one more pass of an action's loop, assigning the parent's attribute (no stamp) 6.00 4.00 4.5
one more pass, assigning the loop action's own attribute (stamped, memo hit) 6.00 4.00 4.5
one more action in a chain of assigning actions (second executor of the model) 84.58 70.54 74
one more event of a counting state machine (entry behavior per event) 133.50 126.00 128

A/B over six interleaved rounds (go test -bench -count 1 per round, nice; pre-#1136 d5e2317 | develop a662c2b | this branch). Allocations are exact; the machine was loaded (builds and tests ran beside it), so times carry ±100–500 % spreads and only the allocation and byte columns are stated:

row allocs/op B/op
ActionLoop/for10 271 | 308 | 273 33.2 | 38.1 | 35.4 KiB
ActionLoop/for100 634 | 851 | 636 71.6 | 94.8 | 75.2 KiB
ActionLoop/for1000 4 238 | 6 256 | 4 241 415 | 622 | 433 KiB
ActionChain/chain10 819 | 968 | 823 196 | 208 | 201 KiB
ActionChain/chain100 6 738 | 8 156 | 6 744 801 | 895 | 832 KiB
ActionChain/chain1000 65.97k | 80.77k | 66.42k 8.20 | 9.39 | 8.67 MiB
ExecuteAction 252 | 289 | 254 190 | 174 | 165 KiB
ExecuteActionFreshContext 347 | 394 | 376 40.8 | 47.5 | 47.5 KiB
ExecuteState 12.29k | 13.75k | 13.00k 844 | 915 | 856 KiB
StateLoop/count50 12.26k | 13.72k | 12.97k 886 | 972 | 920 KiB
StateLoop/count500 119.4k | 134.0k | 126.4k 6.35 | 7.16 | 6.61 MiB
StateLoop/count5000 1.191M | 1.336M | 1.261M 61.2 | 70.1 | 63.9 MiB
MaterializePlainAttributes 618 | 621 | 621 45.7 | 51.2 | 51.2 KiB
SetFeatureValueNoDependents 9 | 9 | 9 384 | 416 | 416 B

The action rows return to the pre-#1136 count (+0.1–0.8 %). The state rows recover half; the other half is the two closures per transition taken, fixed in #1356 (both together: 120.00 allocations per state event in the differential test, against 133.50 on develop). ExecuteActionFreshContext keeps what a fresh context's first run computes for its model (the memos are per model, and that benchmark makes a model per operation). MaterializePlainAttributes and SetFeatureValueNoDependents move in bytes only and are not this cost; the record (#1337) carries them as #1136's. ActionChain/chain1000 time: 17.4 | 33.1 | 18.6 ms (p=0.13 against pre-#1136).

Gates: gofmt -l . empty, go vet ./..., go build ./..., go test ./internal/exec/... ./internal/ir/lower/... ./tests/hygiene, python3 scripts/changelog.py check.

Checklist

  • make test and make lint pass locally (the packages above; the full suite runs in CI)
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (none covers this memo)
  • Changelog entry added as changes/unreleased/stamp-statement-footprint-once.fixed.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (none moved)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/cf22d7a746814d20b2f0eaf11682fef9
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/cf22d7a746814d20b2f0eaf11682fef9?variant=devin
Requested by: @HuiJun


Devin Review

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits October 21, 2026 14:31
Co-Authored-By: jason.han <hanhuijun@gmail.com>
… per model

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title perf(runtime): stamp a statement's footprint once per performance perf(runtime): compute a statement's order and stamped footprint once per model Oct 21, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread internal/exec/runtime/model.go Outdated
… statement kind keyed

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review October 21, 2026 15:39
@HuiJun
HuiJun merged commit 0fbfba8 into develop Oct 21, 2026
50 checks passed
@HuiJun
HuiJun deleted the perf/stamp-statement-footprint-once branch October 21, 2026 19:39
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