@@ -121,9 +121,13 @@ func (i *Instance[S, E, C]) fireCore(ctx context.Context, event E) FireResult[S]
121121 // The cancellation cause is surfaced raw on res.Err and flows through the same
122122 // transactional-rollback branch as a failed Fire below, so the instance is left
123123 // at its pre-Fire configuration with no leaked effects.
124- res := FireResult [S ]{NewState : i .current , Trace : i .seedTrace (fmt .Sprint (event ))}
124+ //
125+ // The seed Trace (with its fmt.Sprint(event) and label lookups) is built only on
126+ // this early-return path: on the normal path fireOnce produces and returns its own
127+ // trace, so seeding one here just to discard it would tax every successful Fire.
128+ var res FireResult [S ]
125129 if err := ctx .Err (); err != nil {
126- res . Err = err
130+ res = FireResult [ S ]{ NewState : i . current , Err : err , Trace : i . seedTrace ( fmt . Sprint ( event ))}
127131 } else {
128132 res = i .fireOnce (ctx , event )
129133 if res .Err == nil {
@@ -173,21 +177,33 @@ type fireTxn[S comparable, E comparable, C any] struct {
173177// restores it is exact. Aliasing avoids a per-Fire slice allocation on every Fire,
174178// including the successful ones that never roll back.
175179//
176- // The history maps and the raised queue are copied only when NON-EMPTY (copyMap /
177- // copyLeafMap / the append return nil for an empty input, allocating nothing). recordHistory
178- // writes the live history maps in place, so a non-empty map must be copied to keep the
179- // snapshot intact; the steady-state machine carries empty history and an empty queue, so
180- // this costs no allocation on the hot path. The context and current leaf are values.
181- // beginTxn never touches a clock or IO, so Fire stays pure.
180+ // The history maps and the raised queue are copied only when NON-EMPTY. The empty
181+ // checks are inlined here rather than delegated to copyMap/copyLeafMap so the common
182+ // hot path (a steady-state machine carries empty history and an empty queue) makes no
183+ // copy-helper call at all — those helpers hold loops and do not inline, so calling
184+ // them every Fire just to have them return nil taxes every successful Fire. The result
185+ // is identical: copyMap/copyLeafMap return nil for an empty input and append onto a nil
186+ // slice from an empty source allocates nothing, so a snapshot built here is byte-for-byte
187+ // what the unconditional form produced. recordHistory writes the live history maps in
188+ // place, so a non-empty map must still be copied to keep the snapshot intact. The
189+ // configuration slice is the only field left aliased (see above); the context and current
190+ // leaf are values. beginTxn never touches a clock or IO, so Fire stays pure.
182191func (i * Instance [S , E , C ]) beginTxn () fireTxn [S , E , C ] {
183- return fireTxn [S , E , C ]{
184- current : i .current ,
185- config : i .config ,
186- entity : i .entity ,
187- historyShallow : copyMap (i .historyShallow ),
188- historyDeep : copyLeafMap (i .historyDeep ),
189- raised : append ([]E (nil ), i .raised ... ),
192+ txn := fireTxn [S , E , C ]{
193+ current : i .current ,
194+ config : i .config ,
195+ entity : i .entity ,
196+ }
197+ if len (i .historyShallow ) > 0 {
198+ txn .historyShallow = copyMap (i .historyShallow )
199+ }
200+ if len (i .historyDeep ) > 0 {
201+ txn .historyDeep = copyLeafMap (i .historyDeep )
202+ }
203+ if len (i .raised ) > 0 {
204+ txn .raised = append ([]E (nil ), i .raised ... )
190205 }
206+ return txn
191207}
192208
193209// rollback restores the instance to the state captured by beginTxn, discarding every
0 commit comments