Skip to content

Commit 4b10b24

Browse files
committed
perf: avoid discarded transactional trace and copy-helper calls per Fire
Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
1 parent 200de10 commit 4b10b24

1 file changed

Lines changed: 31 additions & 15 deletions

File tree

state/fire.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
182191
func (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

Comments
 (0)