Skip to content

Commit 1951844

Browse files
Strengthen machine strategy guardrails (#76)
1 parent fcddd68 commit 1951844

7 files changed

Lines changed: 687 additions & 56 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Add direct generic/indexed planner and generic/compiled runtime strategy guardrails, including startup, targetless, reentry, invoke, snapshot-stability, and generated-model coverage. Expand the runtime benchmark suite with hierarchical and parallel planning, observed hierarchical execution, and generic process lifecycle measurements.

perf/runtime/counter.mjs

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync } from "node:fs"
1+
import { existsSync, readFileSync } from "node:fs"
22
import { createRequire } from "node:module"
33
import { dirname, join, resolve } from "node:path"
44
import { fileURLToPath, pathToFileURL } from "node:url"
@@ -11,9 +11,14 @@ const effectPackagePath = implementationRequire.resolve("effect/package.json")
1111
const effectPackage = JSON.parse(readFileSync(effectPackagePath, "utf8"))
1212
const effect = await import(pathToFileURL(resolve(dirname(effectPackagePath), effectPackage.exports["."])).href)
1313
const { Machine } = await import(pathToFileURL(join(implementationRoot, "dist/index.js")).href)
14-
const machineRuntime = await import(
15-
pathToFileURL(join(implementationRoot, "dist/internal/machineRuntime.js")).href
16-
)
14+
const machineRuntimePath = [
15+
join(implementationRoot, "dist/internal/machine/runtime.js"),
16+
join(implementationRoot, "dist/internal/machineRuntime.js")
17+
].find(existsSync)
18+
if (machineRuntimePath === undefined) {
19+
throw new Error("Effect Machine benchmark could not locate the internal runtime module")
20+
}
21+
const machineRuntime = await import(pathToFileURL(machineRuntimePath).href)
1722
const { Effect, Fiber, Option, Schema, Stream } = effect
1823

1924
const CounterState = Schema.TaggedUnion({
@@ -221,6 +226,12 @@ const parallelFinishEvent = makeEvent(parallelCounterMachine, HierarchicalEvent.
221226
export const initialCounterSnapshot = Effect.runSync(
222227
Machine.planInitial(counterMachine).pipe(Effect.map((planned) => planned.state))
223228
)
229+
const initialHierarchicalSnapshot = Effect.runSync(
230+
Machine.planInitial(hierarchicalCounterMachine).pipe(Effect.map((planned) => planned.state))
231+
)
232+
const initialParallelSnapshot = Effect.runSync(
233+
Machine.planInitial(parallelCounterMachine).pipe(Effect.map((planned) => planned.state))
234+
)
224235

225236
export const counterValue = (snapshot) => snapshot.value.value
226237

@@ -236,6 +247,32 @@ export const planCounterBatch = (size) => {
236247
)
237248
}
238249

250+
const planHierarchicalCounterBatch = (size) =>
251+
Effect.runSync(
252+
Effect.gen(function*() {
253+
let snapshot = initialHierarchicalSnapshot
254+
for (let index = 0; index < size; index += 1) {
255+
snapshot = (yield* Machine.plan(hierarchicalCounterMachine, snapshot, hierarchicalIncrementEvent)).next
256+
}
257+
return snapshot.state.value.value
258+
})
259+
)
260+
261+
const planParallelCounterBatch = (size) =>
262+
Effect.runSync(
263+
Effect.gen(function*() {
264+
let snapshot = initialParallelSnapshot
265+
for (let index = 0; index < size; index += 1) {
266+
snapshot = (yield* Machine.plan(
267+
parallelCounterMachine,
268+
snapshot,
269+
index % 2 === 0 ? parallelIncrementLeftEvent : parallelIncrementRightEvent
270+
)).next
271+
}
272+
return snapshot.states.Left.value.value + snapshot.states.Right.value.value
273+
})
274+
)
275+
239276
export const startCounter = () => Effect.runPromise(Machine.start(counterMachine))
240277

241278
export const stopCounter = (ref) => Effect.runPromise(ref.stop)
@@ -324,6 +361,16 @@ export const runCounterBurst = (ref, size) =>
324361
const startHierarchicalCounter = () => Effect.runPromise(Machine.start(hierarchicalCounterMachine))
325362
const startParallelCounter = () => Effect.runPromise(Machine.start(parallelCounterMachine))
326363

364+
const startObservedHierarchicalCounter = () =>
365+
Effect.runPromise(
366+
Effect.gen(function*() {
367+
const ref = yield* Machine.start(hierarchicalCounterMachine)
368+
const observer = yield* ref.changes.pipe(Stream.runDrain, Effect.forkDetach)
369+
yield* Effect.yieldNow
370+
return { ref, observer }
371+
})
372+
)
373+
327374
const runHierarchicalCounterBurst = (ref, size) =>
328375
Effect.runPromise(
329376
Effect.gen(function*() {
@@ -346,6 +393,19 @@ const runParallelCounterBurst = (ref, size) =>
346393
})
347394
)
348395

396+
const runObservedHierarchicalCounterBurst = ({ ref, observer }, size) =>
397+
Effect.runPromise(
398+
Effect.gen(function*() {
399+
for (let index = 0; index < size; index += 1) {
400+
yield* ref.send(hierarchicalIncrementEvent)
401+
}
402+
yield* ref.send(hierarchicalFinishEvent)
403+
const value = yield* ref.join
404+
yield* Fiber.join(observer)
405+
return value
406+
})
407+
)
408+
349409
export const startCounters = (count) =>
350410
Effect.runPromise(
351411
Effect.forEach(
@@ -502,6 +562,26 @@ export const effectMachineAdapter = {
502562
stopObservedCounter,
503563
stopCounters,
504564
additionalMachineBenchmarks: [
565+
{
566+
id: "hierarchical-plan-counter",
567+
label: "Plan transitions through a compound state",
568+
unit: "transitions/s",
569+
operations: ({ planningBatchSize }) => planningBatchSize,
570+
expected: (operations) => operations,
571+
start: () => undefined,
572+
run: (_, operations) => planHierarchicalCounterBatch(operations),
573+
stop: () => undefined
574+
},
575+
{
576+
id: "parallel-plan-counter",
577+
label: "Plan transitions through parallel regions",
578+
unit: "transitions/s",
579+
operations: ({ planningBatchSize }) => planningBatchSize,
580+
expected: (operations) => operations,
581+
start: () => undefined,
582+
run: (_, operations) => planParallelCounterBatch(operations),
583+
stop: () => undefined
584+
},
505585
{
506586
id: "hierarchical-runtime-burst",
507587
label: "Drain burst through a compound state",
@@ -521,9 +601,34 @@ export const effectMachineAdapter = {
521601
start: startParallelCounter,
522602
run: runParallelCounterBurst,
523603
stop: stopCounter
604+
},
605+
{
606+
id: "observed-hierarchical-runtime-burst",
607+
label: "Drain a compound-state burst with a change observer",
608+
unit: "events/s",
609+
operations: ({ burstBatchSize }) => burstBatchSize,
610+
expected: (operations) => operations,
611+
start: startObservedHierarchicalCounter,
612+
run: runObservedHierarchicalCounterBurst,
613+
stop: stopObservedCounter
524614
}
525615
],
526616
runtimeBenchmarks: [
617+
{
618+
id: "generic-process-start-stop",
619+
label: "Start and stop a raw generic process",
620+
unit: "processes/s",
621+
async: true,
622+
operations: () => 1,
623+
run: async () => {
624+
const refs = await startRawProcesses(1)
625+
try {
626+
return refs.length
627+
} finally {
628+
await stopCounters(refs)
629+
}
630+
}
631+
},
527632
{
528633
id: "compiled-process-start-stop",
529634
label: "Start and stop a raw compiled process",

src/internal/machinePlanner.ts

Lines changed: 90 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2724,64 +2724,102 @@ export interface CompiledExecutionPlan {
27242724

27252725
const executionPlanCache = new WeakMap<Machine.Any, CompiledExecutionPlan>()
27262726

2727+
const makeActiveExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => ({
2728+
fromConfiguration: (configuration) => configuration,
2729+
toConfiguration: (state) => state as ActiveConfiguration,
2730+
snapshot: (state) => snapshotFromConfiguration(machine, state as ActiveConfiguration),
2731+
plan: (state, event) => planConfiguration(machine as any, state as ActiveConfiguration, event as any)
2732+
})
2733+
2734+
const makeIndexedExecutionPlan = (
2735+
machine: Machine.Any,
2736+
indexed: IndexedExecutionDescriptor
2737+
): CompiledExecutionPlan => ({
2738+
fromConfiguration: (configuration) => indexedConfigurationFromActive(indexed, configuration),
2739+
toConfiguration: (state) => activeConfigurationFromIndexed(indexed, state as IndexedConfiguration),
2740+
snapshot: (state) => snapshotFromIndexed(indexed, state as IndexedConfiguration),
2741+
plan: (state, event) => planIndexedConfiguration(machine, indexed, state as IndexedConfiguration, event),
2742+
initial: (args) => {
2743+
const inputArgs = machine.input === undefined
2744+
? args
2745+
: args.length === 0
2746+
? (decodeInputSync(machine, machine.input, undefined), args)
2747+
: [decodeInputSync(machine, machine.input, args[0])]
2748+
const initial = machine.initial(...inputArgs as any)
2749+
const active = normalizeConfigurationSync(machine, initial as Machine.Snapshot<any>)
2750+
validateInitialConfiguration(machine, active)
2751+
const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
2752+
const configuration = indexedConfigurationFromActive(indexed, completed)
2753+
const state = snapshotFromIndexed(indexed, configuration)
2754+
const done = isActiveFinalConfiguration(machine, completed)
2755+
if (!done) {
2756+
return {
2757+
state,
2758+
configuration,
2759+
activeConfiguration: completed,
2760+
initialEntryPaths: getInitialEntryPaths(machine, completed),
2761+
done: false,
2762+
output: undefined
2763+
}
2764+
}
2765+
const root = getRootPath(machine, completed)
2766+
if (!completed.outputs.has(root)) {
2767+
throw new Error("Machine reached a terminal configuration without a completed root output")
2768+
}
2769+
return {
2770+
state,
2771+
configuration,
2772+
activeConfiguration: completed,
2773+
initialEntryPaths: getInitialEntryPaths(machine, completed),
2774+
done: true,
2775+
output: completed.outputs.get(root)
2776+
}
2777+
}
2778+
})
2779+
2780+
export type ExecutionPlanStrategy = "generic" | "indexed-flat" | "indexed-hierarchical" | "auto"
2781+
2782+
export interface SelectedExecutionPlan {
2783+
readonly strategy: Exclude<ExecutionPlanStrategy, "auto">
2784+
readonly plan: CompiledExecutionPlan
2785+
}
2786+
2787+
const selectExecutionPlan = (
2788+
machine: Machine.Any,
2789+
strategy: ExecutionPlanStrategy
2790+
): SelectedExecutionPlan => {
2791+
if (strategy === "generic") {
2792+
return { strategy, plan: makeActiveExecutionPlan(machine) }
2793+
}
2794+
const indexed = compileIndexedExecutionDescriptor(machine)
2795+
if (indexed === undefined) {
2796+
if (strategy === "auto") {
2797+
return { strategy: "generic", plan: makeActiveExecutionPlan(machine) }
2798+
}
2799+
throw new Error(`Machine cannot compile the requested ${strategy} execution plan`)
2800+
}
2801+
const selected = indexed.flat ? "indexed-flat" : "indexed-hierarchical"
2802+
if (strategy !== "auto" && strategy !== selected) {
2803+
throw new Error(`Machine compiled ${selected}, not the requested ${strategy} execution plan`)
2804+
}
2805+
return { strategy: selected, plan: makeIndexedExecutionPlan(machine, indexed) }
2806+
}
2807+
2808+
/** @internal Test-only uncached strategy selection. */
2809+
export const selectExecutionPlanForTesting = (
2810+
machine: Machine.Any,
2811+
strategy: ExecutionPlanStrategy
2812+
): SelectedExecutionPlan => selectExecutionPlan(machine, strategy)
2813+
27272814
export const compileExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => {
27282815
const cached = executionPlanCache.get(machine)
27292816
if (cached !== undefined) {
27302817
return cached
27312818
}
27322819
const indexed = compileIndexedExecutionDescriptor(machine)
2733-
const activePlan = (
2734-
plan: (configuration: ActiveConfiguration, event: unknown) => MacrostepPlan<ActiveConfiguration, any, any, any, any>
2735-
): CompiledExecutionPlan => ({
2736-
fromConfiguration: (configuration) => configuration,
2737-
toConfiguration: (state) => state as ActiveConfiguration,
2738-
snapshot: (state) => snapshotFromConfiguration(machine, state as ActiveConfiguration),
2739-
plan: (state, event) => plan(state as ActiveConfiguration, event)
2740-
})
2741-
const compiled: CompiledExecutionPlan = indexed !== undefined
2742-
? {
2743-
fromConfiguration: (configuration) => indexedConfigurationFromActive(indexed, configuration),
2744-
toConfiguration: (state) => activeConfigurationFromIndexed(indexed, state as IndexedConfiguration),
2745-
snapshot: (state) => snapshotFromIndexed(indexed, state as IndexedConfiguration),
2746-
plan: (state, event) => planIndexedConfiguration(machine, indexed, state as IndexedConfiguration, event),
2747-
initial: (args) => {
2748-
const inputArgs = machine.input === undefined
2749-
? args
2750-
: args.length === 0
2751-
? (decodeInputSync(machine, machine.input, undefined), args)
2752-
: [decodeInputSync(machine, machine.input, args[0])]
2753-
const initial = machine.initial(...inputArgs as any)
2754-
const active = normalizeConfigurationSync(machine, initial as Machine.Snapshot<any>)
2755-
validateInitialConfiguration(machine, active)
2756-
const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
2757-
const configuration = indexedConfigurationFromActive(indexed, completed)
2758-
const state = snapshotFromIndexed(indexed, configuration)
2759-
const done = isActiveFinalConfiguration(machine, completed)
2760-
if (!done) {
2761-
return {
2762-
state,
2763-
configuration,
2764-
activeConfiguration: completed,
2765-
initialEntryPaths: getInitialEntryPaths(machine, completed),
2766-
done: false,
2767-
output: undefined
2768-
}
2769-
}
2770-
const root = getRootPath(machine, completed)
2771-
if (!completed.outputs.has(root)) {
2772-
throw new Error("Machine reached a terminal configuration without a completed root output")
2773-
}
2774-
return {
2775-
state,
2776-
configuration,
2777-
activeConfiguration: completed,
2778-
initialEntryPaths: getInitialEntryPaths(machine, completed),
2779-
done: true,
2780-
output: completed.outputs.get(root)
2781-
}
2782-
}
2783-
}
2784-
: activePlan((configuration, event) => planConfiguration(machine as any, configuration, event as any))
2820+
const compiled = indexed === undefined
2821+
? makeActiveExecutionPlan(machine)
2822+
: makeIndexedExecutionPlan(machine, indexed)
27852823
executionPlanCache.set(machine, compiled)
27862824
return compiled
27872825
}

src/internal/machineProcess.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,30 @@ const toResumedProcessLogic = (
978978
): internalRuntime.ProcessLogic<any, any, any, any, any, any> =>
979979
(makeProcessLogic as any)(machine, { _tag: "Resume", snapshot })
980980

981+
/** @internal Test-only runtime strategy selection for a fresh machine. */
982+
export const startWithRuntimeStrategyForTesting = (
983+
machine: Machine.Any,
984+
strategy: internalRuntime.ProcessRuntimeStrategy,
985+
...args: ReadonlyArray<unknown>
986+
): Effect.Effect<internalRuntime.MachineRef<any, any, any, any>, any, any> =>
987+
internalRuntime.startProcessWithStrategyForTesting(
988+
(toProcessLogic as any)(machine, ...args),
989+
strategy,
990+
machine.id === undefined ? undefined : { id: machine.id }
991+
)
992+
993+
/** @internal Test-only runtime strategy selection for a resumed machine. */
994+
export const resumeWithRuntimeStrategyForTesting = (
995+
machine: Machine.Any,
996+
snapshot: Machine.Snapshot<any>,
997+
strategy: internalRuntime.ProcessRuntimeStrategy
998+
): Effect.Effect<internalRuntime.MachineRef<any, any, any, any>, any, any> =>
999+
internalRuntime.startProcessWithStrategyForTesting(
1000+
toResumedProcessLogic(machine, snapshot),
1001+
strategy,
1002+
machine.id === undefined ? undefined : { id: machine.id }
1003+
)
1004+
9811005
export const start: <
9821006
const States extends Machine.StateSchemas,
9831007
const Events extends ReadonlyArray<Machine.TaggedSchema>,

0 commit comments

Comments
 (0)