From a805fcd4bda595b9713450abeb943b001ee9bdbe Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Mon, 20 Jul 2026 11:11:45 +0200
Subject: [PATCH 1/2] feat(processor): add new channel and workflow operation
processors
Introduce `AllTimelinesChannelProcessor` for handling shared operations across timelines and `ChatWorkflowOperationProcessor` for streamlined chat workflows. Include comprehensive tests for validating processor functionality and update README with relevant details.
---
README.md | 85 +-
build.gradle | 816 +++++++++++++++++-
settings.gradle | 15 +
.../WorkflowExecutionStateBenchmark.java | 48 ++
.../processor/CoordinationProcessors.java | 173 +++-
.../processor/bex/BexProcessingMetrics.java | 598 +++++++++++++
.../workflow/ComputeDefinitionResolver.java | 53 +-
.../processor/workflow/ComputeEffectPlan.java | 56 +-
.../workflow/ComputeProgramNormalizer.java | 98 ++-
.../workflow/ComputeProgramPlan.java | 200 +++++
.../workflow/ComputeProgramPlanCache.java | 244 ++++++
.../workflow/ComputeResultEmitter.java | 132 ++-
.../workflow/ComputeStepExecutor.java | 135 ++-
.../workflow/SequentialWorkflowPlan.java | 283 ++++++
.../workflow/SequentialWorkflowPlanCache.java | 149 ++++
.../workflow/SequentialWorkflowRunner.java | 188 ++--
.../workflow/StaticPayloadValidator.java | 2 +-
.../processor/workflow/StaticUpdatePlan.java | 231 +++++
.../workflow/StepExecutionContext.java | 107 ++-
.../workflow/UpdateDocumentStepExecutor.java | 99 ++-
.../workflow/WorkflowExecutionState.java | 209 +++++
.../workflow/WorkflowPatchEntry.java | 26 +-
.../processor/CoordinationProcessorsTest.java | 143 ++-
.../SequentialWorkflowExecutionTest.java | 45 +
.../bex/BexProcessingMetricsTest.java | 236 +++++
...puteFrozenPatchHandoffIntegrationTest.java | 191 ++++
.../ComputeProgramPlanIntegrationTest.java | 195 +++++
.../compute/ComputeWorkflowExecutionTest.java | 16 +-
.../LanguageRc15MetricsArtifactTest.java | 367 ++++++++
.../LanguageRc15MetricsArtifactWriter.java | 342 ++++++++
.../compute/ProcessingEventBindingTest.java | 4 +-
...resentativeWorkflowLifecycleSmokeTest.java | 404 +++++++++
...dateDocumentBatchApplyIntegrationTest.java | 19 +
.../workflow/ComputeEffectPlanTest.java | 59 +-
.../workflow/ComputeProgramPlanCacheTest.java | 266 ++++++
.../FrozenComputeDifferentialTest.java | 565 ++++++++++++
.../FrozenUpdateDocumentDifferentialTest.java | 503 +++++++++++
.../SequentialWorkflowPlanCacheTest.java | 275 ++++++
...SequentialWorkflowRunnerLifecycleTest.java | 521 +++++++++++
.../workflow/StaticUpdatePlanTest.java | 127 +++
.../workflow/WorkflowExecutionStateTest.java | 82 ++
.../workflow/WorkflowPatchEntryTest.java | 56 ++
42 files changed, 8067 insertions(+), 296 deletions(-)
create mode 100644 src/jmh/java/blue/coordination/processor/workflow/WorkflowExecutionStateBenchmark.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/ComputeProgramPlan.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/ComputeProgramPlanCache.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlanCache.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
create mode 100644 src/main/java/blue/coordination/processor/workflow/WorkflowExecutionState.java
create mode 100644 src/test/java/blue/coordination/processor/compute/ComputeFrozenPatchHandoffIntegrationTest.java
create mode 100644 src/test/java/blue/coordination/processor/compute/ComputeProgramPlanIntegrationTest.java
create mode 100644 src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactTest.java
create mode 100644 src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactWriter.java
create mode 100644 src/test/java/blue/coordination/processor/compute/RepresentativeWorkflowLifecycleSmokeTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/ComputeProgramPlanCacheTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/FrozenComputeDifferentialTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/FrozenUpdateDocumentDifferentialTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/SequentialWorkflowPlanCacheTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/SequentialWorkflowRunnerLifecycleTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/StaticUpdatePlanTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/WorkflowExecutionStateTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/WorkflowPatchEntryTest.java
diff --git a/README.md b/README.md
index b168ac5..5d31fd5 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ repositories {
}
dependencies {
- implementation "blue.coordination:blue-coordination-java:1.0.0"
+ implementation "blue.coordination:blue-coordination-java:2.0.0-rc.4"
}
```
@@ -29,8 +29,8 @@ The project targets Java 8-compatible bytecode, builds with JDK 25, runs tests
on Java 8, and depends on:
```groovy
-api "blue.language:blue-language-java:3.1.0-rc.10"
-api "blue.repo:blue-repo-java:3.0.0-rc.8"
+api "blue.language:blue-language-java:3.1.0-rc.11"
+api "blue.repo:blue-repo-java:3.0.0-rc.10"
api "blue.bex:blue-bex-java:1.1.0-rc.2"
```
@@ -224,6 +224,33 @@ Run tests:
./gradlew test
```
+Run the focused correctness and bounded-memory suites:
+
+```bash
+./gradlew workflowPlanDifferentialTest
+./gradlew complexFixtureIntegrationTest
+./gradlew memoryIntegrationTest
+```
+
+Each focused task uses one worker capped at 2 GiB. Passing `-PtestJfr`
+runs that focused task on the current modern Gradle JVM and records under
+`build/reports/jfr/`; the normal `test` task continues to run on Java 8.
+
+The released Blue Language dependency is pinned, while local validation can
+override its version, Maven repository, or source checkout without editing the
+build:
+
+```bash
+./gradlew test -PblueLanguageVersion=3.1.0-rc.11
+./gradlew test -PblueLanguageRepository=/absolute/path/to/maven-repository
+./gradlew test -PblueLanguageDir=/absolute/path/to/blue-language-java
+BLUE_LANGUAGE_DIR=/absolute/path/to/blue-language-java ./gradlew test
+```
+
+`-PblueLanguageDir` takes precedence over `BLUE_LANGUAGE_DIR` and uses Gradle
+composite substitution. A configured directory that does not exist fails the
+build instead of silently falling back to Maven Central.
+
Build jars:
```bash
@@ -236,6 +263,58 @@ Publish locally:
./gradlew publishToMavenLocal
```
+Stage the artifact without writing outside this repository:
+
+```bash
+./gradlew stageLocalMaven
+```
+
+The staged Maven repository is `build/staging-deploy`.
+
+Run JMH and generate JSON, CSV, Markdown, and environment metadata:
+
+```bash
+./gradlew jmh
+./gradlew jmh -PtestJfr
+```
+
+Reports are written to `build/reports/jmh`. Generic JMH gates are deliberately
+reported as `NOT_CONFIGURED`; operation-level acceptance gates belong to the
+Playground performance driver.
+
+Run black-box Playground validation against a supplied checkout:
+
+```bash
+./gradlew playgroundCompatibilityTest -PplaygroundDir=../blue-playground
+./gradlew playgroundPerformanceTest -PplaygroundDir=../blue-playground
+BLUE_PLAYGROUND_DIR=../blue-playground ./gradlew playgroundCompatibilityTest
+```
+
+These tasks first stage the current artifact, then copy a sanitized Playground
+checkout to `build/black-box-playground`. Dependency and lock-file overlays,
+caches, temporary files, reports, and optional JFR recordings are confined to
+this repository; the supplied Playground checkout remains read-only. The
+compatibility task runs the VetExt mandate/business suites and full Order
+suite. The performance task runs the VetExt performance driver and the full
+Order suite under its existing 2 GiB worker limit. Dependency verification is
+disabled only for this generated copy because its locally staged snapshot is
+not present in the supplied checkout's verification metadata.
+
+VetExt and Order are launched as independent phases and the task returns a
+combined failure after attempting both. Per-phase reports and raw test results
+are preserved under `build/reports/playground`, including when a phase fails or
+its test worker terminates.
+
+Create the reproducible source archive and SHA-256 sidecar:
+
+```bash
+./gradlew sourceArchive
+```
+
+Artifacts are written to `build/distributions`. The performance evidence and
+remaining acceptance work are tracked in
+[`docs/performance/complex-operations-coordination.md`](docs/performance/complex-operations-coordination.md).
+
## Test Coverage
Current test areas:
diff --git a/build.gradle b/build.gradle
index 2553add..7b88be5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,11 +15,26 @@ plugins {
group = 'blue.coordination'
version = determineProjectVersion()
+def blueLanguageVersion = providers.gradleProperty('blueLanguageVersion')
+ .getOrElse('3.1.0-rc.11')
+def binaryCompatibilityBaselineVersion = providers.gradleProperty(
+ 'binaryCompatibilityBaselineVersion').getOrElse('2.0.0-rc.4')
+
base {
archivesName = 'blue-coordination-java'
}
repositories {
+ def blueLanguageRepository = providers.gradleProperty('blueLanguageRepository').orNull
+ if (blueLanguageRepository) {
+ maven {
+ name = 'blueLanguageOverride'
+ url = uri(blueLanguageRepository)
+ content {
+ includeModule 'blue.language', 'blue-language-java'
+ }
+ }
+ }
if (!System.getenv('CI')) {
mavenLocal()
}
@@ -38,8 +53,16 @@ tasks.withType(JavaCompile).configureEach {
options.release = 8
}
+configurations {
+ binaryCompatibilityBaseline {
+ canBeConsumed = false
+ canBeResolved = true
+ transitive = false
+ }
+}
+
dependencies {
- api 'blue.language:blue-language-java:3.1.0-rc.10'
+ api "blue.language:blue-language-java:${blueLanguageVersion}"
api 'blue.repo:blue-repo-java:3.0.0-rc.10'
api 'blue.bex:blue-bex-java:1.1.0-rc.2'
@@ -49,6 +72,8 @@ dependencies {
testImplementation platform('org.junit:junit-bom:5.10.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ binaryCompatibilityBaseline "blue.coordination:blue-coordination-java:${binaryCompatibilityBaselineVersion}"
}
compileTestJava {
@@ -71,6 +96,412 @@ test {
}
}
+def testJfrEnabled = providers.gradleProperty('testJfr')
+ .map { !'false'.equalsIgnoreCase(it == null ? '' : it.trim()) }
+ .getOrElse(false)
+
+def configureFocusedTest = { Test focusedTest, List includedTests ->
+ focusedTest.group = 'verification'
+ focusedTest.testClassesDirs = sourceSets.test.output.classesDirs
+ focusedTest.classpath = sourceSets.test.runtimeClasspath
+ focusedTest.dependsOn tasks.named('testClasses')
+ focusedTest.useJUnitPlatform()
+ focusedTest.maxHeapSize = '2g'
+ focusedTest.maxParallelForks = 1
+ focusedTest.forkEvery = 0L
+ focusedTest.filter {
+ includedTests.each { includeTestsMatching(it) }
+ }
+ focusedTest.reports {
+ junitXml.required = true
+ html.required = true
+ }
+ focusedTest.testLogging {
+ events 'PASSED', 'FAILED', 'SKIPPED'
+ showStandardStreams = true
+ }
+ if (testJfrEnabled) {
+ def recording = layout.buildDirectory.file("reports/jfr/${focusedTest.name}.jfr").get().asFile
+ focusedTest.doFirst {
+ recording.parentFile.mkdirs()
+ if (recording.exists() && !recording.delete()) {
+ throw new GradleException("Could not replace JFR recording: ${recording}")
+ }
+ }
+ focusedTest.jvmArgs "-XX:StartFlightRecording=filename=${recording.absolutePath},settings=profile,dumponexit=true"
+ } else {
+ focusedTest.javaLauncher = javaToolchains.launcherFor {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+ }
+}
+
+tasks.register('workflowPlanDifferentialTest', Test) { focusedTest ->
+ description = 'Runs sequential-workflow planning, execution-state, static-update, and semantic differential tests.'
+ configureFocusedTest(focusedTest, [
+ 'blue.coordination.processor.CoordinationProcessorsTest',
+ 'blue.coordination.processor.SequentialWorkflowExecutionTest',
+ 'blue.coordination.processor.bex.BexProcessingMetricsTest',
+ 'blue.coordination.processor.compute.ComputeFrozenPatchHandoffIntegrationTest',
+ 'blue.coordination.processor.compute.ComputeProgramPlanIntegrationTest',
+ 'blue.coordination.processor.compute.ComputeWorkflowExecutionTest',
+ 'blue.coordination.processor.compute.UpdateDocumentBatchApplyIntegrationTest',
+ 'blue.coordination.processor.workflow.*DifferentialTest',
+ 'blue.coordination.processor.workflow.ComputeEffectPlanTest',
+ 'blue.coordination.processor.workflow.SequentialWorkflowRunnerLifecycleTest',
+ 'blue.coordination.processor.workflow.StaticUpdatePlanTest',
+ 'blue.coordination.processor.workflow.WorkflowExecutionStateTest',
+ 'blue.coordination.processor.workflow.WorkflowPatchEntryTest'
+ ])
+}
+
+tasks.register('complexFixtureIntegrationTest', Test) { focusedTest ->
+ description = 'Runs PayNote, embedded-document, mandate, and termination workflow fixtures.'
+ configureFocusedTest(focusedTest, [
+ 'blue.coordination.processor.EmbeddedTerminationWorkflowTest',
+ 'blue.coordination.processor.compute.*Paynote*',
+ 'blue.coordination.processor.compute.*Embedded*',
+ 'blue.coordination.processor.compute.*Mandate*',
+ 'blue.coordination.processor.compute.*Termination*',
+ 'blue.coordination.processor.compute.TerminateProcessingWorkflowTest'
+ ])
+}
+
+tasks.register('memoryIntegrationTest', Test) { focusedTest ->
+ description = 'Runs bounded-cache, round-trip, stress, and memory-oriented workflow tests in one 2 GiB worker.'
+ configureFocusedTest(focusedTest, [
+ 'blue.coordination.processor.*Stress*',
+ 'blue.coordination.processor.*Memory*',
+ 'blue.coordination.processor.compute.*RoundTrip*',
+ 'blue.coordination.processor.compute.*Memory*',
+ 'blue.coordination.processor.compute.RepresentativeWorkflowLifecycleSmokeTest',
+ 'blue.coordination.processor.workflow.*CacheTest'
+ ])
+}
+
+tasks.register('languageRc15MetricsArtifactTest', Test) { focusedTest ->
+ description = 'Runs the four representative rc15 scenarios and writes deterministic JSON/CSV metrics evidence.'
+ configureFocusedTest(focusedTest, [
+ 'blue.coordination.processor.compute.LanguageRc15MetricsArtifactTest'
+ ])
+ outputs.files(
+ layout.buildDirectory.file('reports/language-rc15-adoption/scenario-metrics.json'),
+ layout.buildDirectory.file('reports/language-rc15-adoption/scenario-metrics.csv'))
+}
+
+// Minimal class-file reader used by the verification tasks below. Keeping this
+// in the build avoids adding a release plugin solely for two deterministic
+// checks, and compares JVM descriptors rather than source formatting.
+def parseBinaryApiClass = { byte[] bytecode ->
+ def input = new DataInputStream(new ByteArrayInputStream(bytecode))
+ if (input.readInt() != (0xCAFEBABE as int)) {
+ throw new GradleException('Invalid class-file magic')
+ }
+ int minorVersion = input.readUnsignedShort()
+ int majorVersion = input.readUnsignedShort()
+ int constantPoolCount = input.readUnsignedShort()
+ Object[] constantPool = new Object[constantPoolCount]
+ for (int index = 1; index < constantPoolCount; index++) {
+ int tag = input.readUnsignedByte()
+ if (tag == 1) {
+ constantPool[index] = input.readUTF()
+ } else if (tag == 3 || tag == 4) {
+ input.readInt()
+ } else if (tag == 5 || tag == 6) {
+ input.readLong()
+ index++
+ } else if (tag == 7) {
+ constantPool[index] = Integer.valueOf(input.readUnsignedShort())
+ } else if (tag == 8 || tag == 16 || tag == 19 || tag == 20) {
+ input.readUnsignedShort()
+ } else if (tag == 9 || tag == 10 || tag == 11 || tag == 12
+ || tag == 17 || tag == 18) {
+ input.readUnsignedShort()
+ input.readUnsignedShort()
+ } else if (tag == 15) {
+ input.readUnsignedByte()
+ input.readUnsignedShort()
+ } else {
+ throw new GradleException("Unsupported constant-pool tag ${tag}")
+ }
+ }
+
+ def utf8 = { int index ->
+ Object value = constantPool[index]
+ if (!(value instanceof String)) {
+ throw new GradleException("Invalid UTF-8 constant-pool index ${index}")
+ }
+ (String) value
+ }
+ def className = { int index ->
+ if (index == 0) {
+ return null
+ }
+ Object nameIndex = constantPool[index]
+ if (!(nameIndex instanceof Integer)) {
+ throw new GradleException("Invalid class constant-pool index ${index}")
+ }
+ utf8(((Integer) nameIndex).intValue()).replace('/', '.')
+ }
+ def skipAttributes = { DataInputStream stream ->
+ int attributeCount = stream.readUnsignedShort()
+ for (int attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) {
+ stream.readUnsignedShort()
+ long remaining = ((long) stream.readInt()) & 0xffffffffL
+ while (remaining > 0L) {
+ long skipped = stream.skip(remaining)
+ if (skipped <= 0L) {
+ if (stream.read() < 0) {
+ throw new EOFException('Truncated class-file attribute')
+ }
+ skipped = 1L
+ }
+ remaining -= skipped
+ }
+ }
+ }
+ def readMembers = { DataInputStream stream ->
+ def members = new TreeMap()
+ int memberCount = stream.readUnsignedShort()
+ for (int memberIndex = 0; memberIndex < memberCount; memberIndex++) {
+ int access = stream.readUnsignedShort()
+ String name = utf8(stream.readUnsignedShort())
+ String descriptor = utf8(stream.readUnsignedShort())
+ skipAttributes(stream)
+ if ((access & (0x0001 | 0x0004)) != 0 && name != '') {
+ members.put(name + descriptor, Integer.valueOf(access))
+ }
+ }
+ members
+ }
+
+ int access = input.readUnsignedShort()
+ String name = className(input.readUnsignedShort())
+ String superName = className(input.readUnsignedShort())
+ int interfaceCount = input.readUnsignedShort()
+ def interfaces = new TreeSet()
+ for (int interfaceIndex = 0; interfaceIndex < interfaceCount; interfaceIndex++) {
+ interfaces.add(className(input.readUnsignedShort()))
+ }
+ Map fields = readMembers(input)
+ Map methods = readMembers(input)
+ skipAttributes(input)
+ [
+ name : name,
+ access : access,
+ superName : superName,
+ interfaces : interfaces,
+ fields : fields,
+ methods : methods,
+ minorVersion: minorVersion,
+ majorVersion: majorVersion
+ ]
+}
+
+def readBinaryApi = { File archive ->
+ def classes = new TreeMap>()
+ def zip = new java.util.zip.ZipFile(archive)
+ try {
+ def entries = Collections.list(zip.entries())
+ .findAll { entry ->
+ !entry.directory
+ && entry.name.endsWith('.class')
+ && !entry.name.startsWith('META-INF/versions/')
+ && !entry.name.endsWith('/module-info.class')
+ && entry.name != 'module-info.class'
+ }
+ .sort { left, right -> left.name <=> right.name }
+ entries.each { entry ->
+ Map parsed = zip.getInputStream(entry).withCloseable { stream ->
+ parseBinaryApiClass(stream.bytes)
+ }
+ if ((((Integer) parsed.access).intValue() & (0x0001 | 0x0004)) != 0) {
+ classes.put((String) parsed.name, parsed)
+ }
+ }
+ } finally {
+ zip.close()
+ }
+ classes
+}
+
+def visibilityCompatible = { int oldAccess, int newAccess ->
+ boolean oldPublic = (oldAccess & 0x0001) != 0
+ boolean newPublic = (newAccess & 0x0001) != 0
+ boolean newProtected = (newAccess & 0x0004) != 0
+ oldPublic ? newPublic : (newPublic || newProtected)
+}
+
+def incompatibleModifierChanges = { int oldAccess, int newAccess, boolean method ->
+ def changes = new ArrayList()
+ if (((oldAccess ^ newAccess) & 0x0008) != 0) {
+ changes.add('static modifier changed')
+ }
+ if ((oldAccess & 0x0010) == 0 && (newAccess & 0x0010) != 0) {
+ changes.add('became final')
+ }
+ if (method && (oldAccess & 0x0400) == 0 && (newAccess & 0x0400) != 0) {
+ changes.add('became abstract')
+ }
+ changes
+}
+
+def binaryCompatibilityReport = layout.buildDirectory.file(
+ 'reports/binary-compatibility/blue-coordination-java.txt')
+def binaryCompatibilityCheck = tasks.register('binaryCompatibilityCheck') {
+ group = 'verification'
+ description = 'Checks public/protected JVM API compatibility with the previous Coordination candidate.'
+ dependsOn tasks.named('jar')
+ inputs.files(configurations.binaryCompatibilityBaseline)
+ inputs.file(tasks.named('jar').flatMap { it.archiveFile })
+ inputs.property('baselineVersion', binaryCompatibilityBaselineVersion)
+ outputs.file(binaryCompatibilityReport)
+ doLast {
+ Set resolvedBaseline = configurations.binaryCompatibilityBaseline.resolve()
+ if (resolvedBaseline.size() != 1) {
+ throw new GradleException(
+ "Expected one binary compatibility baseline JAR, found ${resolvedBaseline}")
+ }
+ File baselineJar = resolvedBaseline.iterator().next()
+ File currentJar = tasks.named('jar').get().archiveFile.get().asFile
+ Map> baselineApi = readBinaryApi(baselineJar)
+ Map> currentApi = readBinaryApi(currentJar)
+ def problems = new ArrayList()
+
+ baselineApi.each { String className, Map oldClass ->
+ Map newClass = currentApi.get(className)
+ if (newClass == null) {
+ problems.add("${className}: public/protected class was removed")
+ return
+ }
+ int oldAccess = ((Integer) oldClass.access).intValue()
+ int newAccess = ((Integer) newClass.access).intValue()
+ if (!visibilityCompatible(oldAccess, newAccess)) {
+ problems.add("${className}: class visibility was reduced")
+ }
+ if (((oldAccess ^ newAccess) & (0x0200 | 0x2000 | 0x4000)) != 0) {
+ problems.add("${className}: class/interface/annotation/enum kind changed")
+ }
+ if ((oldAccess & 0x0010) == 0 && (newAccess & 0x0010) != 0) {
+ problems.add("${className}: class became final")
+ }
+ if ((oldAccess & 0x0400) == 0 && (newAccess & 0x0400) != 0) {
+ problems.add("${className}: class became abstract")
+ }
+ if (oldClass.superName != newClass.superName) {
+ problems.add("${className}: superclass changed from ${oldClass.superName} to ${newClass.superName}")
+ }
+ ((Set) oldClass.interfaces).each { String interfaceName ->
+ if (!((Set) newClass.interfaces).contains(interfaceName)) {
+ problems.add("${className}: directly implemented interface ${interfaceName} was removed")
+ }
+ }
+
+ ['field', 'method'].each { String memberKind ->
+ Map oldMembers = memberKind == 'field'
+ ? (Map) oldClass.fields
+ : (Map) oldClass.methods
+ Map newMembers = memberKind == 'field'
+ ? (Map) newClass.fields
+ : (Map) newClass.methods
+ oldMembers.each { String signature, Integer oldMemberAccess ->
+ Integer newMemberAccess = newMembers.get(signature)
+ if (newMemberAccess == null) {
+ problems.add("${className}: ${memberKind} ${signature} was removed or changed descriptor")
+ return
+ }
+ if (!visibilityCompatible(oldMemberAccess.intValue(),
+ newMemberAccess.intValue())) {
+ problems.add("${className}: ${memberKind} ${signature} visibility was reduced")
+ }
+ incompatibleModifierChanges(oldMemberAccess.intValue(),
+ newMemberAccess.intValue(), memberKind == 'method').each { change ->
+ problems.add("${className}: ${memberKind} ${signature} ${change}")
+ }
+ }
+ }
+ }
+
+ def digest = java.security.MessageDigest.getInstance('SHA-256')
+ baselineJar.withInputStream { stream ->
+ byte[] buffer = new byte[8192]
+ int read
+ while ((read = stream.read(buffer)) >= 0) {
+ if (read > 0) {
+ digest.update(buffer, 0, read)
+ }
+ }
+ }
+ String baselineSha256 = digest.digest()
+ .collect { String.format('%02x', it & 0xff) }.join()
+ File report = binaryCompatibilityReport.get().asFile
+ report.parentFile.mkdirs()
+ report.withWriter('UTF-8') { writer ->
+ writer.writeLine("baseline=blue.coordination:blue-coordination-java:${binaryCompatibilityBaselineVersion}")
+ writer.writeLine("baselineJar=${baselineJar.name}")
+ writer.writeLine("baselineSha256=${baselineSha256}")
+ writer.writeLine("currentJar=${currentJar.name}")
+ writer.writeLine("baselineApiClasses=${baselineApi.size()}")
+ writer.writeLine("currentApiClasses=${currentApi.size()}")
+ writer.writeLine("compatible=${problems.isEmpty()}")
+ problems.sort().each { writer.writeLine("problem=${it}") }
+ }
+ if (!problems.isEmpty()) {
+ throw new GradleException(
+ "Binary compatibility check failed with ${problems.size()} problem(s); see ${report}")
+ }
+ }
+}
+
+def java8BytecodeReport = layout.buildDirectory.file(
+ 'reports/bytecode/java8-bytecode.txt')
+def verifyJava8Bytecode = tasks.register('verifyJava8Bytecode') {
+ group = 'verification'
+ description = 'Verifies that every class in the published Coordination JAR targets Java 8 or earlier.'
+ dependsOn tasks.named('jar')
+ inputs.file(tasks.named('jar').flatMap { it.archiveFile })
+ outputs.file(java8BytecodeReport)
+ doLast {
+ File currentJar = tasks.named('jar').get().archiveFile.get().asFile
+ def versions = new TreeMap()
+ def zip = new java.util.zip.ZipFile(currentJar)
+ try {
+ Collections.list(zip.entries())
+ .findAll { entry -> !entry.directory && entry.name.endsWith('.class') }
+ .sort { left, right -> left.name <=> right.name }
+ .each { entry ->
+ Map parsed = zip.getInputStream(entry).withCloseable { stream ->
+ parseBinaryApiClass(stream.bytes)
+ }
+ versions.put(entry.name, (Integer) parsed.majorVersion)
+ }
+ } finally {
+ zip.close()
+ }
+ def incompatible = versions.findAll { String name, Integer major -> major > 52 }
+ File report = java8BytecodeReport.get().asFile
+ report.parentFile.mkdirs()
+ report.withWriter('UTF-8') { writer ->
+ writer.writeLine("jar=${currentJar.name}")
+ writer.writeLine("classCount=${versions.size()}")
+ writer.writeLine("maximumAllowedMajorVersion=52")
+ writer.writeLine("maximumObservedMajorVersion=${versions.values().max() ?: 0}")
+ writer.writeLine("compatible=${incompatible.isEmpty()}")
+ incompatible.each { name, major ->
+ writer.writeLine("problem=${name}: major version ${major}")
+ }
+ }
+ if (!incompatible.isEmpty()) {
+ throw new GradleException(
+ "Java 8 bytecode verification failed for ${incompatible.size()} class(es); see ${report}")
+ }
+ }
+}
+
+tasks.named('check') {
+ dependsOn binaryCompatibilityCheck, verifyJava8Bytecode
+}
+
jmh {
jmhVersion = '1.37'
warmupIterations = 3
@@ -78,9 +509,88 @@ jmh {
iterations = 5
timeOnIteration = '1s'
fork = 2
- profilers = ['gc']
+ profilers = testJfrEnabled
+ ? ['gc', "jfr:dir=${file("$buildDir/reports/jfr/jmh").absolutePath}"]
+ : ['gc']
+ jvmArgs = ['-Xms512m', '-Xmx2g']
resultFormat = 'JSON'
- resultsFile = file("$buildDir/reports/jmh/resolved-processing-host-story.json")
+ resultsFile = file("$buildDir/reports/jmh/jmh-results.json")
+}
+
+tasks.named('jmh') {
+ def jsonReport = file("$buildDir/reports/jmh/jmh-results.json")
+ def csvReport = file("$buildDir/reports/jmh/jmh-results.csv")
+ def markdownReport = file("$buildDir/reports/jmh/jmh-results.md")
+ def environmentReport = file("$buildDir/reports/jmh/environment.properties")
+ outputs.files(csvReport, markdownReport, environmentReport)
+ doLast {
+ if (!jsonReport.isFile()) {
+ throw new GradleException("JMH JSON report was not created: ${jsonReport}")
+ }
+ def results = new groovy.json.JsonSlurper().parse(jsonReport)
+ def csvCell = { Object value ->
+ '"' + String.valueOf(value == null ? '' : value).replace('"', '""') + '"'
+ }
+ def markdownCell = { Object value ->
+ String.valueOf(value == null ? '' : value).replace('|', '\\|').replace('\n', ' ')
+ }
+
+ csvReport.parentFile.mkdirs()
+ csvReport.withWriter('UTF-8') { writer ->
+ writer.writeLine('benchmark,mode,threads,forks,score,scoreError,scoreUnit,gate')
+ results.each { result ->
+ def metric = result.primaryMetric ?: [:]
+ writer.writeLine([
+ result.benchmark,
+ result.mode,
+ result.threads,
+ result.forks,
+ metric.score,
+ metric.scoreError,
+ metric.scoreUnit,
+ 'NOT_CONFIGURED'
+ ].collect(csvCell).join(','))
+ }
+ }
+
+ def environment = new TreeMap()
+ environment.put('blueCoordinationVersion', project.version.toString())
+ environment.put('blueLanguageVersion', blueLanguageVersion)
+ environment.put('gradleVersion', gradle.gradleVersion)
+ environment.put('gradleJvmMaxHeapBytes', String.valueOf(Runtime.runtime.maxMemory()))
+ environment.put('javaVendor', System.getProperty('java.vendor', 'unknown'))
+ environment.put('javaVersion', System.getProperty('java.version', 'unknown'))
+ environment.put('jmhForkJvmArgs', '-Xms512m,-Xmx2g')
+ environment.put('osArch', System.getProperty('os.arch', 'unknown'))
+ environment.put('osName', System.getProperty('os.name', 'unknown'))
+ environment.put('osVersion', System.getProperty('os.version', 'unknown'))
+ environment.put('processors', String.valueOf(Runtime.runtime.availableProcessors()))
+ environmentReport.withWriter('UTF-8') { writer ->
+ environment.each { key, value -> writer.writeLine("${key}=${value}") }
+ }
+
+ markdownReport.withWriter('UTF-8') { writer ->
+ writer.writeLine('# Coordination JMH results')
+ writer.writeLine('')
+ writer.writeLine('Environment metadata is recorded in `environment.properties`. Generic JMH hard gates are not configured; each row is marked `NOT_CONFIGURED`.')
+ writer.writeLine('')
+ writer.writeLine('| Benchmark | Mode | Threads | Forks | Score | Error | Unit | Gate |')
+ writer.writeLine('|---|---:|---:|---:|---:|---:|---|---|')
+ results.each { result ->
+ def metric = result.primaryMetric ?: [:]
+ writer.writeLine('| ' + [
+ result.benchmark,
+ result.mode,
+ result.threads,
+ result.forks,
+ metric.score,
+ metric.scoreError,
+ metric.scoreUnit,
+ 'NOT_CONFIGURED'
+ ].collect(markdownCell).join(' | ') + ' |')
+ }
+ }
+ }
}
ext.genResourcesDir = file("$buildDir/generated-resources")
@@ -144,6 +654,306 @@ publishing {
}
}
+tasks.register('sourceArchive', Zip) {
+ group = 'distribution'
+ description = 'Creates a reproducible source-only archive and SHA-256 checksum.'
+ def sourceArchiveName = "${rootProject.name}-${project.version}-source.zip"
+ def checksumFile = layout.buildDirectory.file("distributions/${sourceArchiveName}.sha256")
+ archiveFileName = sourceArchiveName
+ destinationDirectory = layout.buildDirectory.dir('distributions')
+ outputs.file(checksumFile)
+ preserveFileTimestamps = false
+ reproducibleFileOrder = true
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ from(projectDir) {
+ into("${rootProject.name}-${project.version}")
+ include '.cz.toml'
+ include '.github/**'
+ include '.gitignore'
+ include 'LICENSE'
+ include 'README.md'
+ include 'build.gradle'
+ include 'settings.gradle'
+ include 'gradle.properties'
+ include 'gradle/**'
+ include 'gradlew'
+ include 'gradlew.bat'
+ include 'docs/**'
+ include 'scripts/**'
+ include 'src/**'
+ exclude '**/.DS_Store'
+ exclude '**/*.db'
+ exclude '**/*.hprof'
+ exclude '**/*.jfr'
+ exclude '**/*.log'
+ exclude '**/*.zip'
+ exclude '**/build/**'
+ exclude '**/dumps/**'
+ exclude '**/logs/**'
+ exclude '**/node_modules/**'
+ exclude '**/recordings/**'
+ }
+ doLast {
+ def archive = archiveFile.get().asFile
+ def digest = java.security.MessageDigest.getInstance('SHA-256')
+ archive.withInputStream { input ->
+ byte[] buffer = new byte[8192]
+ int read
+ while ((read = input.read(buffer)) >= 0) {
+ if (read > 0) {
+ digest.update(buffer, 0, read)
+ }
+ }
+ }
+ def checksum = digest.digest().collect { String.format('%02x', it & 0xff) }.join()
+ checksumFile.get().asFile.setText(
+ "${checksum} ${archive.name}\n", 'UTF-8')
+ }
+}
+
+def configuredPlaygroundDir = providers.gradleProperty('playgroundDir')
+ .orElse(providers.environmentVariable('BLUE_PLAYGROUND_DIR'))
+def blackBoxPlaygroundDir = layout.buildDirectory.dir('black-box-playground')
+def blackBoxStagingRepository = layout.buildDirectory.dir('staging-deploy')
+
+def validatePlaygroundDir = tasks.register('validatePlaygroundDir') {
+ group = 'verification'
+ description = 'Validates the read-only Blue Playground source used by black-box tasks.'
+ doLast {
+ if (!configuredPlaygroundDir.isPresent()) {
+ throw new GradleException(
+ 'Set -PplaygroundDir=/path/to/blue-playground or BLUE_PLAYGROUND_DIR before running Playground tasks.')
+ }
+ def source = file(configuredPlaygroundDir.get()).canonicalFile
+ def destination = blackBoxPlaygroundDir.get().asFile.canonicalFile
+ if (!source.isDirectory()) {
+ throw new GradleException("Configured Blue Playground directory does not exist: ${source}")
+ }
+ if (!new File(source, 'gradlew').isFile()
+ || !new File(source, 'build.gradle.kts').isFile()
+ || !new File(source, 'gradle/libs.versions.toml').isFile()) {
+ throw new GradleException("Configured directory is not a supported Blue Playground checkout: ${source}")
+ }
+ if (source == destination || source.toPath().startsWith(destination.toPath())) {
+ throw new GradleException('The Blue Playground source must be separate from build/black-box-playground.')
+ }
+ }
+}
+
+def publishBlackBoxArtifact = tasks.named('publishMavenPublicationToMavenRepository')
+publishBlackBoxArtifact.configure {
+ mustRunAfter validatePlaygroundDir
+}
+
+def stageLocalMaven = tasks.register('stageLocalMaven') {
+ group = 'publishing'
+ description = 'Publishes the current Coordination artifact into build/staging-deploy.'
+ dependsOn publishBlackBoxArtifact
+}
+stageLocalMaven.configure {
+ mustRunAfter validatePlaygroundDir
+}
+
+def prepareBlackBoxPlayground = tasks.register('prepareBlackBoxPlayground', Sync) {
+ group = 'verification'
+ description = 'Copies a sanitized Blue Playground checkout under this project build directory.'
+ dependsOn validatePlaygroundDir
+ mustRunAfter stageLocalMaven
+ // The overlay intentionally mutates only the generated copy. Re-sync on
+ // every black-box run so a prior overlay can never become the next input.
+ outputs.upToDateWhen { false }
+ doFirst {
+ // A previous copied build may contain node_modules/.bin symlinks.
+ // Remove only this managed build output before Sync inspects it.
+ project.delete(blackBoxPlaygroundDir.get().asFile)
+ }
+ from {
+ configuredPlaygroundDir.isPresent() ? file(configuredPlaygroundDir.get()) : []
+ }
+ into blackBoxPlaygroundDir
+ includeEmptyDirs = false
+ exclude '**/.git', '**/.git/**'
+ exclude '**/.gradle', '**/.gradle/**'
+ exclude '**/build', '**/build/**'
+ exclude '**/node_modules', '**/node_modules/**'
+ exclude 'db', 'db/**'
+ exclude 'dumps', 'dumps/**'
+ exclude 'logs', 'logs/**'
+ exclude 'recordings', 'recordings/**'
+ exclude '**/*.db', '**/*.hprof', '**/*.jfr', '**/*.log', '**/*.zip'
+}
+
+def overlayBlackBoxPlayground = tasks.register('overlayBlackBoxPlayground') {
+ group = 'verification'
+ description = 'Overlays staged Coordination and Blue Language versions only in the copied Playground.'
+ dependsOn stageLocalMaven, prepareBlackBoxPlayground
+ inputs.property('coordinationVersion', project.version.toString())
+ inputs.property('blueLanguageVersion', blueLanguageVersion)
+ inputs.file('scripts/playground-local.init.gradle')
+ outputs.upToDateWhen { false }
+ doLast {
+ def destination = blackBoxPlaygroundDir.get().asFile
+ def buildFile = new File(destination, 'build.gradle.kts')
+ def catalogFile = new File(destination, 'gradle/libs.versions.toml')
+ def lockFile = new File(destination, 'gradle.lockfile')
+ def versionGuardFile = new File(destination,
+ 'src/main/java/com/blue/playground/blue/ResolvedBlueVersions.java')
+ [buildFile, catalogFile, lockFile, versionGuardFile].each { required ->
+ if (!required.isFile()) {
+ throw new GradleException("Copied Playground is missing required file: ${required}")
+ }
+ }
+
+ def replaceCoordinateVersion = { File target, String coordinate, String replacementVersion ->
+ def pattern = java.util.regex.Pattern.compile(
+ java.util.regex.Pattern.quote(coordinate) + '[^\\s"\'=,\\)]+')
+ def content = target.getText('UTF-8')
+ def matcher = pattern.matcher(content)
+ if (!matcher.find()) {
+ throw new GradleException("Could not locate ${coordinate} in copied file ${target}")
+ }
+ target.setText(matcher.replaceAll(java.util.regex.Matcher.quoteReplacement(
+ coordinate + replacementVersion)), 'UTF-8')
+ }
+ def replaceCatalogVersion = { File target, String key, String replacementVersion ->
+ def pattern = java.util.regex.Pattern.compile(
+ '(?m)^(\\s*' + java.util.regex.Pattern.quote(key) + '\\s*=\\s*")[^"]+')
+ def content = target.getText('UTF-8')
+ def matcher = pattern.matcher(content)
+ if (!matcher.find()) {
+ throw new GradleException("Could not locate ${key} in copied version catalog ${target}")
+ }
+ def replacement = matcher.group(1) + replacementVersion
+ target.setText(matcher.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)), 'UTF-8')
+ }
+ def catalogVersion = { File target, String key ->
+ def pattern = java.util.regex.Pattern.compile(
+ '(?m)^\s*' + java.util.regex.Pattern.quote(key) + '\s*=\s*"([^"]+)"')
+ def matcher = pattern.matcher(target.getText('UTF-8'))
+ if (!matcher.find()) {
+ throw new GradleException("Could not read ${key} from copied version catalog ${target}")
+ }
+ matcher.group(1)
+ }
+ def replaceLiteral = { File target, String currentValue, String replacementValue ->
+ if (currentValue == replacementValue) {
+ return
+ }
+ def content = target.getText('UTF-8')
+ if (!content.contains(currentValue)) {
+ throw new GradleException("Could not locate ${currentValue} in copied file ${target}")
+ }
+ target.setText(content.replace(currentValue, replacementValue), 'UTF-8')
+ }
+
+ def originalCoordinationVersion = catalogVersion(catalogFile, 'blueCoordination')
+ def originalLanguageVersion = catalogVersion(catalogFile, 'blueLanguage')
+ replaceLiteral(buildFile, originalCoordinationVersion, project.version.toString())
+ replaceLiteral(buildFile, originalLanguageVersion, blueLanguageVersion)
+ replaceLiteral(versionGuardFile, originalCoordinationVersion, project.version.toString())
+ replaceLiteral(versionGuardFile, originalLanguageVersion, blueLanguageVersion)
+
+ if (project.version.toString().endsWith('-SNAPSHOT')) {
+ def guardSource = versionGuardFile.getText('UTF-8')
+ def exactGuard = 'if (!location.contains(artifact + "-" + version + ".jar")) {'
+ def snapshotAwareGuard = '''boolean exactJar = location.contains(artifact + "-" + version + ".jar");
+ boolean versionedMavenPath = location.contains("/" + version + "/" + artifact + "-");
+ if (!exactJar && !versionedMavenPath) {'''
+ if (!guardSource.contains(exactGuard)) {
+ throw new GradleException("Could not locate runtime version guard in copied file ${versionGuardFile}")
+ }
+ versionGuardFile.setText(guardSource.replace(exactGuard, snapshotAwareGuard), 'UTF-8')
+ }
+
+ replaceCoordinateVersion(buildFile,
+ 'blue.coordination:blue-coordination-java:', project.version.toString())
+ replaceCoordinateVersion(buildFile,
+ 'blue.language:blue-language-java:', blueLanguageVersion)
+ replaceCoordinateVersion(lockFile,
+ 'blue.coordination:blue-coordination-java:', project.version.toString())
+ replaceCoordinateVersion(lockFile,
+ 'blue.language:blue-language-java:', blueLanguageVersion)
+ replaceCatalogVersion(catalogFile, 'blueCoordination', project.version.toString())
+ replaceCatalogVersion(catalogFile, 'blueLanguage', blueLanguageVersion)
+
+ java.nio.file.Files.copy(
+ file('scripts/playground-local.init.gradle').toPath(),
+ new File(destination, 'coordination-local.init.gradle').toPath(),
+ java.nio.file.StandardCopyOption.REPLACE_EXISTING)
+ new File(destination, 'coordination-overlay.properties').setText(
+ "blueCoordinationVersion=${project.version}\n"
+ + "blueLanguageVersion=${blueLanguageVersion}\n"
+ + "stagingRepository=${blackBoxStagingRepository.get().asFile.toURI()}\n",
+ 'UTF-8')
+ }
+}
+
+def registerPlaygroundSuite = { String taskName, String suite, String taskDescription ->
+ def playgroundTask = tasks.register(taskName, Exec) { suiteTask ->
+ group = 'verification'
+ description = taskDescription
+ dependsOn overlayBlackBoxPlayground
+ outputs.upToDateWhen { false }
+ doFirst {
+ def cacheRoot = layout.buildDirectory.dir('black-box-gradle-user-home').get().asFile
+ def npmCache = layout.buildDirectory.dir('black-box-npm-cache').get().asFile
+ def tempRoot = layout.buildDirectory.dir('black-box-tmp').get().asFile
+ def jfrRoot = layout.buildDirectory.dir("reports/jfr/${taskName}").get().asFile
+ [cacheRoot, npmCache, tempRoot].each { it.mkdirs() }
+ if (testJfrEnabled) {
+ jfrRoot.mkdirs()
+ }
+ environment 'GRADLE_USER_HOME', cacheRoot.absolutePath
+ environment 'npm_config_cache', npmCache.absolutePath
+ environment 'XDG_CACHE_HOME', new File(cacheRoot, 'xdg').absolutePath
+ environment 'TMPDIR', tempRoot.absolutePath
+ environment 'TEMP', tempRoot.absolutePath
+ environment 'TMP', tempRoot.absolutePath
+ commandLine 'bash', file('scripts/run-playground-suite.sh').absolutePath,
+ suite,
+ blackBoxPlaygroundDir.get().asFile.absolutePath,
+ blackBoxStagingRepository.get().asFile.toURI().toString(),
+ project.version.toString(),
+ blueLanguageVersion,
+ testJfrEnabled ? jfrRoot.absolutePath : ''
+ }
+ }
+ def reportTask = tasks.register("${taskName}Reports") {
+ group = 'verification'
+ description = "Preserves ${suite} Playground reports even when its acceptance task fails."
+ mustRunAfter playgroundTask
+ doLast {
+ def sourceBuild = new File(blackBoxPlaygroundDir.get().asFile, 'build')
+ def reportRoot = layout.buildDirectory.dir("reports/playground/${suite}").get().asFile
+ project.delete(reportRoot)
+ project.copy {
+ from new File(sourceBuild, 'reports')
+ into new File(reportRoot, 'reports')
+ }
+ project.copy {
+ from new File(sourceBuild, 'test-results')
+ into new File(reportRoot, 'test-results')
+ }
+ project.copy {
+ from new File(sourceBuild, 'coordination-suite-results')
+ into new File(reportRoot, 'runs')
+ }
+ }
+ }
+ playgroundTask.configure {
+ finalizedBy reportTask
+ }
+}
+
+registerPlaygroundSuite(
+ 'playgroundCompatibilityTest',
+ 'compatibility',
+ 'Runs VetExt mandate/business and full Order semantic tests in a sanitized Playground copy.')
+registerPlaygroundSuite(
+ 'playgroundPerformanceTest',
+ 'performance',
+ 'Runs the VetExt performance gate and full Order scenario in a sanitized Playground copy.')
+
if (System.getenv('CI')) {
jreleaser {
signing {
diff --git a/settings.gradle b/settings.gradle
index 58270ff..9a24ff5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,6 +4,21 @@ plugins {
rootProject.name = 'blue-coordination-java'
+def localBlueLanguagePath = providers.gradleProperty('blueLanguageDir')
+ .orElse(providers.environmentVariable('BLUE_LANGUAGE_DIR'))
+ .orNull
+if (localBlueLanguagePath) {
+ def localBlueLanguage = file(localBlueLanguagePath)
+ if (!localBlueLanguage.isDirectory()) {
+ throw new GradleException("Configured Blue Language directory does not exist: ${localBlueLanguage}")
+ }
+ includeBuild(localBlueLanguage) {
+ dependencySubstitution {
+ substitute module('blue.language:blue-language-java') using project(':')
+ }
+ }
+}
+
def localBlueRepository = file('../blue-repository-java')
if (providers.gradleProperty('useLocalBlueRepository').map { it.toBoolean() }.getOrElse(false)
&& localBlueRepository.isDirectory()) {
diff --git a/src/jmh/java/blue/coordination/processor/workflow/WorkflowExecutionStateBenchmark.java b/src/jmh/java/blue/coordination/processor/workflow/WorkflowExecutionStateBenchmark.java
new file mode 100644
index 0000000..20db362
--- /dev/null
+++ b/src/jmh/java/blue/coordination/processor/workflow/WorkflowExecutionStateBenchmark.java
@@ -0,0 +1,48 @@
+package blue.coordination.processor.workflow;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/** Allocation/scaling comparison for the per-step result-state boundary. */
+@State(Scope.Thread)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class WorkflowExecutionStateBenchmark {
+
+ @Param({"100", "500", "1000"})
+ public int steps;
+
+ @Benchmark
+ public void revisionedReadOnlyViews(Blackhole blackhole) {
+ WorkflowExecutionState state = new WorkflowExecutionState();
+ for (int index = 0; index < steps; index++) {
+ WorkflowExecutionState.Snapshot view = state.snapshotView();
+ blackhole.consume(view.size());
+ state.record("Step" + index, Integer.valueOf(index), (index & 1) == 0);
+ }
+ blackhole.consume(state.snapshotView());
+ }
+
+ @Benchmark
+ public void legacyWholeMapCopies(Blackhole blackhole) {
+ Map results = new LinkedHashMap();
+ for (int index = 0; index < steps; index++) {
+ Map snapshot = Collections.unmodifiableMap(
+ new LinkedHashMap(results));
+ blackhole.consume(snapshot.size());
+ results.put("Step" + index, Integer.valueOf(index));
+ }
+ blackhole.consume(results);
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/CoordinationProcessors.java b/src/main/java/blue/coordination/processor/CoordinationProcessors.java
index 725e7a0..adc2d24 100644
--- a/src/main/java/blue/coordination/processor/CoordinationProcessors.java
+++ b/src/main/java/blue/coordination/processor/CoordinationProcessors.java
@@ -2,9 +2,11 @@
import blue.bex.api.BexEngine;
import blue.coordination.processor.merge.CoordinationMerging;
+import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.coordination.processor.workflow.SequentialWorkflowRunner;
import blue.language.Blue;
import blue.language.processor.DocumentProcessor;
+import blue.language.processor.ProcessingMetricsSink;
import blue.language.utils.TypeClassResolver;
import blue.repo.BlueRepositoryModels;
@@ -20,21 +22,19 @@ public static Blue registerWith(Blue blue, CoordinationProcessorOptions options)
if (blue == null) {
throw new IllegalArgumentException("blue must not be null");
}
+ BexProcessingMetrics metrics = processingMetrics(options);
+ if (metrics != null) {
+ installProcessingMetrics(blue.getDocumentProcessor(), metrics);
+ }
SequentialWorkflowRunner runner = workflowRunner(options);
BlueRepositoryModels.registerAll(blue.getDocumentProcessor().getContractTypeResolver());
blue.registerContractProcessor(new TimelineChannelProcessor());
blue.registerContractProcessor(new AllTimelinesChannelProcessor());
blue.registerContractProcessor(new CompositeTimelineChannelProcessor());
blue.registerContractProcessor(new OperationProcessor());
- blue.registerContractProcessor(runner != null
- ? new ChatWorkflowOperationProcessor(runner)
- : new ChatWorkflowOperationProcessor());
- blue.registerContractProcessor(runner != null
- ? new SequentialWorkflowProcessor(runner)
- : new SequentialWorkflowProcessor());
- blue.registerContractProcessor(runner != null
- ? new SequentialWorkflowOperationProcessor(runner)
- : new SequentialWorkflowOperationProcessor());
+ blue.registerContractProcessor(new ChatWorkflowOperationProcessor(runner));
+ blue.registerContractProcessor(new SequentialWorkflowProcessor(runner));
+ blue.registerContractProcessor(new SequentialWorkflowOperationProcessor(runner));
CoordinationMerging.install(blue);
return blue;
}
@@ -48,6 +48,10 @@ public static DocumentProcessor.Builder configure(DocumentProcessor.Builder buil
if (builder == null) {
throw new IllegalArgumentException("builder must not be null");
}
+ BexProcessingMetrics metrics = processingMetrics(options);
+ if (metrics != null) {
+ builder.withProcessingMetricsSink(metrics);
+ }
SequentialWorkflowRunner runner = workflowRunner(options);
TypeClassResolver resolver = BlueRepositoryModels.registerAll(
new TypeClassResolver("blue.language.processor.model"));
@@ -57,29 +61,148 @@ public static DocumentProcessor.Builder configure(DocumentProcessor.Builder buil
.registerContractProcessor(new AllTimelinesChannelProcessor())
.registerContractProcessor(new CompositeTimelineChannelProcessor())
.registerContractProcessor(new OperationProcessor())
- .registerContractProcessor(runner != null
- ? new ChatWorkflowOperationProcessor(runner)
- : new ChatWorkflowOperationProcessor())
- .registerContractProcessor(runner != null
- ? new SequentialWorkflowProcessor(runner)
- : new SequentialWorkflowProcessor())
- .registerContractProcessor(runner != null
- ? new SequentialWorkflowOperationProcessor(runner)
- : new SequentialWorkflowOperationProcessor());
+ .registerContractProcessor(new ChatWorkflowOperationProcessor(runner))
+ .registerContractProcessor(new SequentialWorkflowProcessor(runner))
+ .registerContractProcessor(new SequentialWorkflowOperationProcessor(runner));
}
- private static SequentialWorkflowRunner workflowRunner(CoordinationProcessorOptions options) {
- if (options == null) {
- return null;
+ private static BexProcessingMetrics processingMetrics(CoordinationProcessorOptions options) {
+ return options != null ? options.processingMetrics() : null;
+ }
+
+ private static void installProcessingMetrics(DocumentProcessor processor,
+ BexProcessingMetrics coordinationMetrics) {
+ ProcessingMetricsSink existing = processor.processingMetricsSink();
+ if (existing == coordinationMetrics) {
+ return;
+ }
+ if (existing == null || existing == ProcessingMetricsSink.NOOP) {
+ processor.processingMetricsSink(coordinationMetrics);
+ return;
}
- if (options.sequentialWorkflowRunner() != null) {
+ processor.processingMetricsSink(new CompositeProcessingMetricsSink(
+ existing, coordinationMetrics));
+ }
+
+ private static SequentialWorkflowRunner workflowRunner(CoordinationProcessorOptions options) {
+ if (options != null && options.sequentialWorkflowRunner() != null) {
return options.sequentialWorkflowRunner();
}
- BexEngine bexEngine = options.bexEngine() != null
+ BexEngine bexEngine = options != null && options.bexEngine() != null
? options.bexEngine()
: BexEngine.builder().build();
return SequentialWorkflowRunner.withBexEngine(bexEngine,
- options.defaultComputeGasLimit(),
- options.processingMetrics());
+ options != null ? options.defaultComputeGasLimit() : 100_000L,
+ processingMetrics(options));
+ }
+
+ /** Static, allocation-free-per-sample fan-out for preserving an independently installed sink. */
+ private static final class CompositeProcessingMetricsSink implements ProcessingMetricsSink {
+ private final ProcessingMetricsSink first;
+ private final ProcessingMetricsSink second;
+
+ private CompositeProcessingMetricsSink(ProcessingMetricsSink first,
+ ProcessingMetricsSink second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override public void addMetric(String name, long delta) { first.addMetric(name, delta); second.addMetric(name, delta); }
+ @Override public void setMetric(String name, long value) { first.setMetric(name, value); second.setMetric(name, value); }
+ @Override public void recordMetricHighWater(String name, long value) { first.recordMetricHighWater(name, value); second.recordMetricHighWater(name, value); }
+ @Override public void addProcessDocumentNanos(long value) { first.addProcessDocumentNanos(value); second.addProcessDocumentNanos(value); }
+ @Override public void addBlueProcessDocumentNanos(long value) { first.addBlueProcessDocumentNanos(value); second.addBlueProcessDocumentNanos(value); }
+ @Override public void addEventPreprocessNanos(long value) { first.addEventPreprocessNanos(value); second.addEventPreprocessNanos(value); }
+ @Override public void addResultSnapshotAttachNanos(long value) { first.addResultSnapshotAttachNanos(value); second.addResultSnapshotAttachNanos(value); }
+ @Override public void addBlueIdCalculationNanos(long value) { first.addBlueIdCalculationNanos(value); second.addBlueIdCalculationNanos(value); }
+ @Override public void addProcessingSnapshotCacheLookupNanos(long value) { first.addProcessingSnapshotCacheLookupNanos(value); second.addProcessingSnapshotCacheLookupNanos(value); }
+ @Override public void incrementProcessingSnapshotCacheHits() { first.incrementProcessingSnapshotCacheHits(); second.incrementProcessingSnapshotCacheHits(); }
+ @Override public void incrementProcessingSnapshotCacheMisses() { first.incrementProcessingSnapshotCacheMisses(); second.incrementProcessingSnapshotCacheMisses(); }
+ @Override public void addProcessingSnapshotFromDocumentNanos(long value) { first.addProcessingSnapshotFromDocumentNanos(value); second.addProcessingSnapshotFromDocumentNanos(value); }
+ @Override public void incrementProcessingSnapshotFromDocumentBuilds() { first.incrementProcessingSnapshotFromDocumentBuilds(); second.incrementProcessingSnapshotFromDocumentBuilds(); }
+ @Override public void incrementProcessEventSnapshotAttempts() { first.incrementProcessEventSnapshotAttempts(); second.incrementProcessEventSnapshotAttempts(); }
+ @Override public void incrementProcessEventSnapshotBuilds() { first.incrementProcessEventSnapshotBuilds(); second.incrementProcessEventSnapshotBuilds(); }
+ @Override public void incrementProcessEventSnapshotFailures() { first.incrementProcessEventSnapshotFailures(); second.incrementProcessEventSnapshotFailures(); }
+ @Override public void addProcessEventSnapshotConstructionNanos(long value) { first.addProcessEventSnapshotConstructionNanos(value); second.addProcessEventSnapshotConstructionNanos(value); }
+ @Override public void addBundleLoadNanos(long value) { first.addBundleLoadNanos(value); second.addBundleLoadNanos(value); }
+ @Override public void addBundleLoadCacheKeyBuildNanos(long value) { first.addBundleLoadCacheKeyBuildNanos(value); second.addBundleLoadCacheKeyBuildNanos(value); }
+ @Override public void addBundleLoadActualBuildNanos(long value) { first.addBundleLoadActualBuildNanos(value); second.addBundleLoadActualBuildNanos(value); }
+ @Override public void addBundleLoadReuseNanos(long value) { first.addBundleLoadReuseNanos(value); second.addBundleLoadReuseNanos(value); }
+ @Override public void incrementBundleLoadCacheHits() { first.incrementBundleLoadCacheHits(); second.incrementBundleLoadCacheHits(); }
+ @Override public void incrementBundleLoadCacheMisses() { first.incrementBundleLoadCacheMisses(); second.incrementBundleLoadCacheMisses(); }
+ @Override public void incrementBundlesBuilt() { first.incrementBundlesBuilt(); second.incrementBundlesBuilt(); }
+ @Override public void incrementBundlesReused() { first.incrementBundlesReused(); second.incrementBundlesReused(); }
+ @Override public void incrementBundleScopeLoadAttempts() { first.incrementBundleScopeLoadAttempts(); second.incrementBundleScopeLoadAttempts(); }
+ @Override public void incrementBundleScopeExecutionCacheHits() { first.incrementBundleScopeExecutionCacheHits(); second.incrementBundleScopeExecutionCacheHits(); }
+ @Override public void incrementBundleScopeRefreshes() { first.incrementBundleScopeRefreshes(); second.incrementBundleScopeRefreshes(); }
+ @Override public void addBundleScopeTerminationCheckNanos(long value) { first.addBundleScopeTerminationCheckNanos(value); second.addBundleScopeTerminationCheckNanos(value); }
+ @Override public void addBundleScopeResolvedLookupNanos(long value) { first.addBundleScopeResolvedLookupNanos(value); second.addBundleScopeResolvedLookupNanos(value); }
+ @Override public void addBundleScopeContractLoadNanos(long value) { first.addBundleScopeContractLoadNanos(value); second.addBundleScopeContractLoadNanos(value); }
+ @Override public void addChannelDiscoveryNanos(long value) { first.addChannelDiscoveryNanos(value); second.addChannelDiscoveryNanos(value); }
+ @Override public void addChannelMatchNanos(long value) { first.addChannelMatchNanos(value); second.addChannelMatchNanos(value); }
+ @Override public void incrementChannelEvaluations() { first.incrementChannelEvaluations(); second.incrementChannelEvaluations(); }
+ @Override public void incrementRoutedChannelDeliveries() { first.incrementRoutedChannelDeliveries(); second.incrementRoutedChannelDeliveries(); }
+ @Override public void incrementDeduplicatedChannelDeliveries() { first.incrementDeduplicatedChannelDeliveries(); second.incrementDeduplicatedChannelDeliveries(); }
+ @Override public void addHandlerDiscoveryNanos(long value) { first.addHandlerDiscoveryNanos(value); second.addHandlerDiscoveryNanos(value); }
+ @Override public void addHandlerMatchNanos(long value) { first.addHandlerMatchNanos(value); second.addHandlerMatchNanos(value); }
+ @Override public void incrementHandlerMatchAttempts() { first.incrementHandlerMatchAttempts(); second.incrementHandlerMatchAttempts(); }
+ @Override public void addHandlerExecutionNanos(long value) { first.addHandlerExecutionNanos(value); second.addHandlerExecutionNanos(value); }
+ @Override public void incrementHandlersExecuted() { first.incrementHandlersExecuted(); second.incrementHandlersExecuted(); }
+ @Override public void addTriggeredEventRoutingNanos(long value) { first.addTriggeredEventRoutingNanos(value); second.addTriggeredEventRoutingNanos(value); }
+ @Override public void incrementTriggeredEventsRouted() { first.incrementTriggeredEventsRouted(); second.incrementTriggeredEventsRouted(); }
+ @Override public void addCheckpointUpdateNanos(long value) { first.addCheckpointUpdateNanos(value); second.addCheckpointUpdateNanos(value); }
+ @Override public void addCheckpointEnsureNanos(long value) { first.addCheckpointEnsureNanos(value); second.addCheckpointEnsureNanos(value); }
+ @Override public void addCheckpointFindNanos(long value) { first.addCheckpointFindNanos(value); second.addCheckpointFindNanos(value); }
+ @Override public void addCheckpointCurrentIdentityNanos(long value) { first.addCheckpointCurrentIdentityNanos(value); second.addCheckpointCurrentIdentityNanos(value); }
+ @Override public void addCheckpointIsNewerNanos(long value) { first.addCheckpointIsNewerNanos(value); second.addCheckpointIsNewerNanos(value); }
+ @Override public void addCheckpointDuplicateNanos(long value) { first.addCheckpointDuplicateNanos(value); second.addCheckpointDuplicateNanos(value); }
+ @Override public void addCheckpointPersistNanos(long value) { first.addCheckpointPersistNanos(value); second.addCheckpointPersistNanos(value); }
+ @Override public void incrementCheckpointIdentityCacheHits() { first.incrementCheckpointIdentityCacheHits(); second.incrementCheckpointIdentityCacheHits(); }
+ @Override public void incrementCheckpointIdentityCacheMisses() { first.incrementCheckpointIdentityCacheMisses(); second.incrementCheckpointIdentityCacheMisses(); }
+ @Override public void incrementCheckpointStoredIdentityCacheHits() { first.incrementCheckpointStoredIdentityCacheHits(); second.incrementCheckpointStoredIdentityCacheHits(); }
+ @Override public void incrementCheckpointStoredIdentityCacheMisses() { first.incrementCheckpointStoredIdentityCacheMisses(); second.incrementCheckpointStoredIdentityCacheMisses(); }
+ @Override public void addCheckpointDirectBlueIdNanos(long value) { first.addCheckpointDirectBlueIdNanos(value); second.addCheckpointDirectBlueIdNanos(value); }
+ @Override public void addCheckpointContentBlueIdNanos(long value) { first.addCheckpointContentBlueIdNanos(value); second.addCheckpointContentBlueIdNanos(value); }
+ @Override public void addCheckpointFallbackNanos(long value) { first.addCheckpointFallbackNanos(value); second.addCheckpointFallbackNanos(value); }
+ @Override public void addSnapshotCommitNanos(long value) { first.addSnapshotCommitNanos(value); second.addSnapshotCommitNanos(value); }
+ @Override public void addPostProcessingNanos(long value) { first.addPostProcessingNanos(value); second.addPostProcessingNanos(value); }
+ @Override public void addPatchBoundaryNanos(long value) { first.addPatchBoundaryNanos(value); second.addPatchBoundaryNanos(value); }
+ @Override public void addPatchGasNanos(long value) { first.addPatchGasNanos(value); second.addPatchGasNanos(value); }
+ @Override public void addDocumentUpdateRoutingNanos(long value) { first.addDocumentUpdateRoutingNanos(value); second.addDocumentUpdateRoutingNanos(value); }
+ @Override public void incrementDocumentUpdateEventsBuilt() { first.incrementDocumentUpdateEventsBuilt(); second.incrementDocumentUpdateEventsBuilt(); }
+ @Override public void incrementDocumentUpdateEventsSkippedNoChannel() { first.incrementDocumentUpdateEventsSkippedNoChannel(); second.incrementDocumentUpdateEventsSkippedNoChannel(); }
+ @Override public void addBatchPatchPlanningNanos(long value) { first.addBatchPatchPlanningNanos(value); second.addBatchPatchPlanningNanos(value); }
+ @Override public void addBatchPatchConformanceNanos(long value) { first.addBatchPatchConformanceNanos(value); second.addBatchPatchConformanceNanos(value); }
+ @Override public void addBatchPatchBuildUpdatesNanos(long value) { first.addBatchPatchBuildUpdatesNanos(value); second.addBatchPatchBuildUpdatesNanos(value); }
+ @Override public void addBatchPatchCommitNanos(long value) { first.addBatchPatchCommitNanos(value); second.addBatchPatchCommitNanos(value); }
+ @Override public void incrementDocumentUpdateBeforeMaterializations() { first.incrementDocumentUpdateBeforeMaterializations(); second.incrementDocumentUpdateBeforeMaterializations(); }
+ @Override public void incrementDocumentUpdateAfterMaterializations() { first.incrementDocumentUpdateAfterMaterializations(); second.incrementDocumentUpdateAfterMaterializations(); }
+ @Override public void incrementPatchSequencesPrepared() { first.incrementPatchSequencesPrepared(); second.incrementPatchSequencesPrepared(); }
+ @Override public void addPatchesPrepared(long value) { first.addPatchesPrepared(value); second.addPatchesPrepared(value); }
+ @Override public void incrementSingletonPatchTransactions() { first.incrementSingletonPatchTransactions(); second.incrementSingletonPatchTransactions(); }
+ @Override public void addSequencePlanningNanos(long value) { first.addSequencePlanningNanos(value); second.addSequencePlanningNanos(value); }
+ @Override public void addSequenceConformanceNanos(long value) { first.addSequenceConformanceNanos(value); second.addSequenceConformanceNanos(value); }
+ @Override public void addSequenceCommitNanos(long value) { first.addSequenceCommitNanos(value); second.addSequenceCommitNanos(value); }
+ @Override public void addSequenceFinalCacheCommitNanos(long value) { first.addSequenceFinalCacheCommitNanos(value); second.addSequenceFinalCacheCommitNanos(value); }
+ @Override public void incrementSequenceIntermediateSnapshotAdvances() { first.incrementSequenceIntermediateSnapshotAdvances(); second.incrementSequenceIntermediateSnapshotAdvances(); }
+ @Override public void incrementSequenceSharedSnapshotCacheInserts() { first.incrementSequenceSharedSnapshotCacheInserts(); second.incrementSequenceSharedSnapshotCacheInserts(); }
+ @Override public void incrementSequenceFinalSnapshotCacheInserts() { first.incrementSequenceFinalSnapshotCacheInserts(); second.incrementSequenceFinalSnapshotCacheInserts(); }
+ @Override public void incrementSequenceSuffixRebases() { first.incrementSequenceSuffixRebases(); second.incrementSequenceSuffixRebases(); }
+ @Override public void incrementSequenceStalePreviewFallbacks() { first.incrementSequenceStalePreviewFallbacks(); second.incrementSequenceStalePreviewFallbacks(); }
+ @Override public void incrementSequenceFallbackPatches() { first.incrementSequenceFallbackPatches(); second.incrementSequenceFallbackPatches(); }
+ @Override public void incrementParsedPointerCacheHits() { first.incrementParsedPointerCacheHits(); second.incrementParsedPointerCacheHits(); }
+ @Override public void incrementParsedPointerCacheMisses() { first.incrementParsedPointerCacheMisses(); second.incrementParsedPointerCacheMisses(); }
+ @Override public void incrementFrozenPatchValueHits() { first.incrementFrozenPatchValueHits(); second.incrementFrozenPatchValueHits(); }
+ @Override public void incrementPatchValueMaterializations() { first.incrementPatchValueMaterializations(); second.incrementPatchValueMaterializations(); }
+ @Override public void incrementFrozenNodesCreated() { first.incrementFrozenNodesCreated(); second.incrementFrozenNodesCreated(); }
+ @Override public void incrementFrozenNodesReused() { first.incrementFrozenNodesReused(); second.incrementFrozenNodesReused(); }
+ @Override public void incrementCanonicalIdentityCalculations() { first.incrementCanonicalIdentityCalculations(); second.incrementCanonicalIdentityCalculations(); }
+ @Override public void incrementResolvedIdentityCalculations() { first.incrementResolvedIdentityCalculations(); second.incrementResolvedIdentityCalculations(); }
+ @Override public void addCanonicalBytesWritten(long value) { first.addCanonicalBytesWritten(value); second.addCanonicalBytesWritten(value); }
+ @Override public void incrementJcsFallbacks() { first.incrementJcsFallbacks(); second.incrementJcsFallbacks(); }
+ @Override public void addBase58EncodeNanos(long value) { first.addBase58EncodeNanos(value); second.addBase58EncodeNanos(value); }
+ @Override public void addBase58DecodeNanos(long value) { first.addBase58DecodeNanos(value); second.addBase58DecodeNanos(value); }
+ @Override public void addBlueIdDigestNanos(long value) { first.addBlueIdDigestNanos(value); second.addBlueIdDigestNanos(value); }
+ @Override public void incrementResolvedStructuralKeyBuilds() { first.incrementResolvedStructuralKeyBuilds(); second.incrementResolvedStructuralKeyBuilds(); }
}
}
diff --git a/src/main/java/blue/coordination/processor/bex/BexProcessingMetrics.java b/src/main/java/blue/coordination/processor/bex/BexProcessingMetrics.java
index c3e4f8b..13e0e6a 100644
--- a/src/main/java/blue/coordination/processor/bex/BexProcessingMetrics.java
+++ b/src/main/java/blue/coordination/processor/bex/BexProcessingMetrics.java
@@ -3,11 +3,46 @@
import blue.bex.result.BexMetrics;
import blue.language.processor.ProcessingMetricsSink;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
public final class BexProcessingMetrics implements ProcessingMetricsSink {
+ /**
+ * Language currently emits a fixed vocabulary, but keep the adapter safe if a future
+ * integration accidentally supplies data-derived names.
+ */
+ public static final int MAX_LANGUAGE_METRIC_NAMES = 256;
+
+ private final Object languageMetricNameLock = new Object();
+ private final ConcurrentMap languageMetricNames =
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap languageCounters =
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap languageGauges =
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap languageHighWaterMarks =
+ new ConcurrentHashMap<>();
+ private final AtomicLong droppedLanguageMetricNames = new AtomicLong();
+
private final AtomicLong workflowStepsExecuted = new AtomicLong();
+ private final AtomicLong workflowPlansBuilt = new AtomicLong();
+ private final AtomicLong workflowPlanCacheHits = new AtomicLong();
+ private final AtomicLong workflowPlanCacheMisses = new AtomicLong();
+ private final AtomicLong workflowPlanCacheEvictions = new AtomicLong();
+ private final AtomicLong workflowPlanWeightBytes = new AtomicLong();
+ private final AtomicLong workflowExecutorLookups = new AtomicLong();
+ private final AtomicLong workflowStepResultSnapshotsCreated = new AtomicLong();
+ private final AtomicLong workflowStepResultViewHits = new AtomicLong();
private final AtomicLong computeStepsExecuted = new AtomicLong();
+ private final AtomicLong computePlansBuilt = new AtomicLong();
+ private final AtomicLong computePlanCacheHits = new AtomicLong();
+ private final AtomicLong computePlanCacheMisses = new AtomicLong();
+ private final AtomicLong computePlanCacheEvictions = new AtomicLong();
+ private final AtomicLong computePlanWeightBytes = new AtomicLong();
private final AtomicLong updateDocumentStepsExecuted = new AtomicLong();
private final AtomicLong triggerEventStepsExecuted = new AtomicLong();
private final AtomicLong directBexChangesetHits = new AtomicLong();
@@ -19,6 +54,9 @@ public final class BexProcessingMetrics implements ProcessingMetricsSink {
private final AtomicLong computeResultValidationFailures = new AtomicLong();
private final AtomicLong computeProgramNormalizations = new AtomicLong();
private final AtomicLong computeDefinitionNormalizations = new AtomicLong();
+ private final AtomicLong computeDefinitionMaterializations = new AtomicLong();
+ private final AtomicLong computeDefinitionFrozenDirectHits = new AtomicLong();
+ private final AtomicLong computeProgramSourceBuilds = new AtomicLong();
private final AtomicLong computeDefinitionResolveHits = new AtomicLong();
private final AtomicLong computeDefinitionResolveMisses = new AtomicLong();
private final AtomicLong workflowRunnerNanos = new AtomicLong();
@@ -33,6 +71,9 @@ public final class BexProcessingMetrics implements ProcessingMetricsSink {
private final AtomicLong updatePatchApplyNanos = new AtomicLong();
private final AtomicLong updateBatchPatchApplications = new AtomicLong();
private final AtomicLong updateIndividualPatchApplications = new AtomicLong();
+ private final AtomicLong updateStaticTemplatesBuilt = new AtomicLong();
+ private final AtomicLong updateStaticTemplateHits = new AtomicLong();
+ private final AtomicLong updateReflectionFallbacks = new AtomicLong();
private final AtomicLong triggerStepNanos = new AtomicLong();
private final AtomicLong triggerEmitEventNanos = new AtomicLong();
private final AtomicLong bexCompileNanos = new AtomicLong();
@@ -42,6 +83,8 @@ public final class BexProcessingMetrics implements ProcessingMetricsSink {
private final AtomicLong bexCompiledExecutions = new AtomicLong();
private final AtomicLong bexNodeWriterNanos = new AtomicLong();
private final AtomicLong directBexPatchEntryConversions = new AtomicLong();
+ private final AtomicLong bexPatchFrozenDirectConversions = new AtomicLong();
+ private final AtomicLong bexPatchNodeMaterializations = new AtomicLong();
private final AtomicLong processDocumentNanos = new AtomicLong();
private final AtomicLong blueProcessDocumentNanos = new AtomicLong();
private final AtomicLong eventPreprocessNanos = new AtomicLong();
@@ -107,6 +150,23 @@ public final class BexProcessingMetrics implements ProcessingMetricsSink {
private final AtomicLong batchPatchCommitNanos = new AtomicLong();
private final AtomicLong documentUpdateBeforeMaterializations = new AtomicLong();
private final AtomicLong documentUpdateAfterMaterializations = new AtomicLong();
+ private final AtomicLong patchSequencesPrepared = new AtomicLong();
+ private final AtomicLong patchesPrepared = new AtomicLong();
+ private final AtomicLong singletonPatchTransactions = new AtomicLong();
+ private final AtomicLong sequencePlanningNanos = new AtomicLong();
+ private final AtomicLong sequenceConformanceNanos = new AtomicLong();
+ private final AtomicLong sequenceCommitNanos = new AtomicLong();
+ private final AtomicLong sequenceFinalCacheCommitNanos = new AtomicLong();
+ private final AtomicLong sequenceIntermediateSnapshotAdvances = new AtomicLong();
+ private final AtomicLong sequenceSharedSnapshotCacheInserts = new AtomicLong();
+ private final AtomicLong sequenceFinalSnapshotCacheInserts = new AtomicLong();
+ private final AtomicLong sequenceSuffixRebases = new AtomicLong();
+ private final AtomicLong sequenceStalePreviewFallbacks = new AtomicLong();
+ private final AtomicLong sequenceFallbackPatches = new AtomicLong();
+ private final AtomicLong parsedPointerCacheHits = new AtomicLong();
+ private final AtomicLong parsedPointerCacheMisses = new AtomicLong();
+ private final AtomicLong frozenPatchValueHits = new AtomicLong();
+ private final AtomicLong patchValueMaterializations = new AtomicLong();
private final AtomicLong workflowDocumentViewsFromFrozen = new AtomicLong();
private final AtomicLong workflowDocumentViewsFromDocument = new AtomicLong();
private final AtomicLong workflowDocumentViewMisses = new AtomicLong();
@@ -119,10 +179,62 @@ public void incrementWorkflowStepsExecuted() {
workflowStepsExecuted.incrementAndGet();
}
+ public void incrementWorkflowPlansBuilt() {
+ workflowPlansBuilt.incrementAndGet();
+ }
+
+ public void incrementWorkflowPlanCacheHits() {
+ workflowPlanCacheHits.incrementAndGet();
+ }
+
+ public void incrementWorkflowPlanCacheMisses() {
+ workflowPlanCacheMisses.incrementAndGet();
+ }
+
+ public void incrementWorkflowPlanCacheEvictions() {
+ workflowPlanCacheEvictions.incrementAndGet();
+ }
+
+ public void addWorkflowPlanWeightBytes(long delta) {
+ addToNonNegativeGauge(workflowPlanWeightBytes, delta);
+ }
+
+ public void incrementWorkflowExecutorLookups() {
+ workflowExecutorLookups.incrementAndGet();
+ }
+
+ public void incrementWorkflowStepResultSnapshotsCreated() {
+ workflowStepResultSnapshotsCreated.incrementAndGet();
+ }
+
+ public void incrementWorkflowStepResultViewHits() {
+ workflowStepResultViewHits.incrementAndGet();
+ }
+
public void incrementComputeStepsExecuted() {
computeStepsExecuted.incrementAndGet();
}
+ public void incrementComputePlansBuilt() {
+ computePlansBuilt.incrementAndGet();
+ }
+
+ public void incrementComputePlanCacheHits() {
+ computePlanCacheHits.incrementAndGet();
+ }
+
+ public void incrementComputePlanCacheMisses() {
+ computePlanCacheMisses.incrementAndGet();
+ }
+
+ public void incrementComputePlanCacheEvictions() {
+ computePlanCacheEvictions.incrementAndGet();
+ }
+
+ public void addComputePlanWeightBytes(long delta) {
+ addToNonNegativeGauge(computePlanWeightBytes, delta);
+ }
+
public void incrementUpdateDocumentStepsExecuted() {
updateDocumentStepsExecuted.incrementAndGet();
}
@@ -167,6 +279,18 @@ public void incrementComputeDefinitionNormalizations() {
computeDefinitionNormalizations.incrementAndGet();
}
+ public void incrementComputeDefinitionMaterializations() {
+ computeDefinitionMaterializations.incrementAndGet();
+ }
+
+ public void incrementComputeDefinitionFrozenDirectHits() {
+ computeDefinitionFrozenDirectHits.incrementAndGet();
+ }
+
+ public void incrementComputeProgramSourceBuilds() {
+ computeProgramSourceBuilds.incrementAndGet();
+ }
+
public void incrementComputeDefinitionResolveHits() {
computeDefinitionResolveHits.incrementAndGet();
}
@@ -223,6 +347,18 @@ public void incrementUpdateIndividualPatchApplications() {
updateIndividualPatchApplications.incrementAndGet();
}
+ public void incrementUpdateStaticTemplatesBuilt() {
+ updateStaticTemplatesBuilt.incrementAndGet();
+ }
+
+ public void incrementUpdateStaticTemplateHits() {
+ updateStaticTemplateHits.incrementAndGet();
+ }
+
+ public void incrementUpdateReflectionFallbacks() {
+ updateReflectionFallbacks.incrementAndGet();
+ }
+
public void addTriggerStepNanos(long nanos) {
triggerStepNanos.addAndGet(nonNegative(nanos));
}
@@ -239,6 +375,14 @@ public void incrementDirectBexPatchEntryConversions() {
directBexPatchEntryConversions.incrementAndGet();
}
+ public void incrementBexPatchFrozenDirectConversions() {
+ bexPatchFrozenDirectConversions.incrementAndGet();
+ }
+
+ public void incrementBexPatchNodeMaterializations() {
+ bexPatchNodeMaterializations.incrementAndGet();
+ }
+
public void addBexMetrics(BexMetrics metrics) {
if (metrics == null) {
return;
@@ -575,6 +719,91 @@ public void incrementDocumentUpdateAfterMaterializations() {
documentUpdateAfterMaterializations.incrementAndGet();
}
+ @Override
+ public void incrementPatchSequencesPrepared() {
+ patchSequencesPrepared.incrementAndGet();
+ }
+
+ @Override
+ public void addPatchesPrepared(long count) {
+ patchesPrepared.addAndGet(count);
+ }
+
+ @Override
+ public void incrementSingletonPatchTransactions() {
+ singletonPatchTransactions.incrementAndGet();
+ }
+
+ @Override
+ public void addSequencePlanningNanos(long nanos) {
+ sequencePlanningNanos.addAndGet(nonNegative(nanos));
+ }
+
+ @Override
+ public void addSequenceConformanceNanos(long nanos) {
+ sequenceConformanceNanos.addAndGet(nonNegative(nanos));
+ }
+
+ @Override
+ public void addSequenceCommitNanos(long nanos) {
+ sequenceCommitNanos.addAndGet(nonNegative(nanos));
+ }
+
+ @Override
+ public void addSequenceFinalCacheCommitNanos(long nanos) {
+ sequenceFinalCacheCommitNanos.addAndGet(nonNegative(nanos));
+ }
+
+ @Override
+ public void incrementSequenceIntermediateSnapshotAdvances() {
+ sequenceIntermediateSnapshotAdvances.incrementAndGet();
+ }
+
+ @Override
+ public void incrementSequenceSharedSnapshotCacheInserts() {
+ sequenceSharedSnapshotCacheInserts.incrementAndGet();
+ }
+
+ @Override
+ public void incrementSequenceFinalSnapshotCacheInserts() {
+ sequenceFinalSnapshotCacheInserts.incrementAndGet();
+ }
+
+ @Override
+ public void incrementSequenceSuffixRebases() {
+ sequenceSuffixRebases.incrementAndGet();
+ }
+
+ @Override
+ public void incrementSequenceStalePreviewFallbacks() {
+ sequenceStalePreviewFallbacks.incrementAndGet();
+ }
+
+ @Override
+ public void incrementSequenceFallbackPatches() {
+ sequenceFallbackPatches.incrementAndGet();
+ }
+
+ @Override
+ public void incrementParsedPointerCacheHits() {
+ parsedPointerCacheHits.incrementAndGet();
+ }
+
+ @Override
+ public void incrementParsedPointerCacheMisses() {
+ parsedPointerCacheMisses.incrementAndGet();
+ }
+
+ @Override
+ public void incrementFrozenPatchValueHits() {
+ frozenPatchValueHits.incrementAndGet();
+ }
+
+ @Override
+ public void incrementPatchValueMaterializations() {
+ patchValueMaterializations.incrementAndGet();
+ }
+
public void incrementWorkflowDocumentViewsFromFrozen() {
workflowDocumentViewsFromFrozen.incrementAndGet();
}
@@ -607,10 +836,62 @@ public long workflowStepsExecuted() {
return workflowStepsExecuted.get();
}
+ public long workflowPlansBuilt() {
+ return workflowPlansBuilt.get();
+ }
+
+ public long workflowPlanCacheHits() {
+ return workflowPlanCacheHits.get();
+ }
+
+ public long workflowPlanCacheMisses() {
+ return workflowPlanCacheMisses.get();
+ }
+
+ public long workflowPlanCacheEvictions() {
+ return workflowPlanCacheEvictions.get();
+ }
+
+ public long workflowPlanWeightBytes() {
+ return workflowPlanWeightBytes.get();
+ }
+
+ public long workflowExecutorLookups() {
+ return workflowExecutorLookups.get();
+ }
+
+ public long workflowStepResultSnapshotsCreated() {
+ return workflowStepResultSnapshotsCreated.get();
+ }
+
+ public long workflowStepResultViewHits() {
+ return workflowStepResultViewHits.get();
+ }
+
public long computeStepsExecuted() {
return computeStepsExecuted.get();
}
+ public long computePlansBuilt() {
+ return computePlansBuilt.get();
+ }
+
+ public long computePlanCacheHits() {
+ return computePlanCacheHits.get();
+ }
+
+ public long computePlanCacheMisses() {
+ return computePlanCacheMisses.get();
+ }
+
+ public long computePlanCacheEvictions() {
+ return computePlanCacheEvictions.get();
+ }
+
+ public long computePlanWeightBytes() {
+ return computePlanWeightBytes.get();
+ }
+
public long updateDocumentStepsExecuted() {
return updateDocumentStepsExecuted.get();
}
@@ -655,6 +936,18 @@ public long computeDefinitionNormalizations() {
return computeDefinitionNormalizations.get();
}
+ public long computeDefinitionMaterializations() {
+ return computeDefinitionMaterializations.get();
+ }
+
+ public long computeDefinitionFrozenDirectHits() {
+ return computeDefinitionFrozenDirectHits.get();
+ }
+
+ public long computeProgramSourceBuilds() {
+ return computeProgramSourceBuilds.get();
+ }
+
public long computeDefinitionResolveHits() {
return computeDefinitionResolveHits.get();
}
@@ -711,6 +1004,18 @@ public long updateIndividualPatchApplications() {
return updateIndividualPatchApplications.get();
}
+ public long updateStaticTemplatesBuilt() {
+ return updateStaticTemplatesBuilt.get();
+ }
+
+ public long updateStaticTemplateHits() {
+ return updateStaticTemplateHits.get();
+ }
+
+ public long updateReflectionFallbacks() {
+ return updateReflectionFallbacks.get();
+ }
+
public long triggerStepNanos() {
return triggerStepNanos.get();
}
@@ -747,6 +1052,14 @@ public long directBexPatchEntryConversions() {
return directBexPatchEntryConversions.get();
}
+ public long bexPatchFrozenDirectConversions() {
+ return bexPatchFrozenDirectConversions.get();
+ }
+
+ public long bexPatchNodeMaterializations() {
+ return bexPatchNodeMaterializations.get();
+ }
+
public long processDocumentNanos() {
return processDocumentNanos.get();
}
@@ -1007,6 +1320,96 @@ public long documentUpdateAfterMaterializations() {
return documentUpdateAfterMaterializations.get();
}
+ /**
+ * Number of reusable Language patch-sequence planning sessions. This is the raw value from
+ * {@link #incrementPatchSequencesPrepared()}.
+ */
+ public long preparedPatchSequences() {
+ return patchSequencesPrepared.get();
+ }
+
+ /** Number of patches accepted by Language sequence sessions. */
+ public long preparedPatches() {
+ return patchesPrepared.get();
+ }
+
+ /**
+ * Closest public proxy for a Language sequence transaction.
+ *
+ * The callback identifies a reusable sequential planning session,
+ * not the Language runtime's internal transaction counter, so reports must
+ * retain this qualification.
+ */
+ public long languageSequenceTransactions() {
+ return patchSequencesPrepared.get();
+ }
+
+ /** Number of legacy standalone one-patch Language transactions. */
+ public long languageSingletonTransactions() {
+ return singletonPatchTransactions.get();
+ }
+
+ public long sequencePlanningNanos() {
+ return sequencePlanningNanos.get();
+ }
+
+ public long sequenceConformanceNanos() {
+ return sequenceConformanceNanos.get();
+ }
+
+ public long sequenceCommitNanos() {
+ return sequenceCommitNanos.get();
+ }
+
+ public long sequenceFinalCacheCommitNanos() {
+ return sequenceFinalCacheCommitNanos.get();
+ }
+
+ public long languageIntermediateSnapshotAdvances() {
+ return sequenceIntermediateSnapshotAdvances.get();
+ }
+
+ public long sequenceSharedSnapshotCacheInserts() {
+ return sequenceSharedSnapshotCacheInserts.get();
+ }
+
+ /** Maps to Language's final sequence snapshot-cache insertion callback. */
+ public long languageFinalSnapshotPromotions() {
+ return sequenceFinalSnapshotCacheInserts.get();
+ }
+
+ public long sequenceFinalSnapshotCacheInserts() {
+ return sequenceFinalSnapshotCacheInserts.get();
+ }
+
+ public long languageSuffixRebases() {
+ return sequenceSuffixRebases.get();
+ }
+
+ public long sequenceStalePreviewFallbacks() {
+ return sequenceStalePreviewFallbacks.get();
+ }
+
+ public long languageFallbackPatches() {
+ return sequenceFallbackPatches.get();
+ }
+
+ public long parsedPointerCacheHits() {
+ return parsedPointerCacheHits.get();
+ }
+
+ public long parsedPointerCacheMisses() {
+ return parsedPointerCacheMisses.get();
+ }
+
+ public long frozenPatchValueHits() {
+ return frozenPatchValueHits.get();
+ }
+
+ public long languagePatchValueMaterializations() {
+ return patchValueMaterializations.get();
+ }
+
public long workflowDocumentViewsFromFrozen() {
return workflowDocumentViewsFromFrozen.get();
}
@@ -1035,17 +1438,137 @@ public long bexDocumentViewUndefinedHits() {
return bexDocumentViewUndefinedHits.get();
}
+ @Override
+ public void addMetric(String metricName, long delta) {
+ AtomicLong counter = languageMetric(languageCounters, metricName);
+ if (counter != null) {
+ counter.addAndGet(delta);
+ }
+ }
+
+ @Override
+ public void setMetric(String metricName, long value) {
+ AtomicLong gauge = languageMetric(languageGauges, metricName);
+ if (gauge != null) {
+ gauge.set(value);
+ }
+ }
+
+ @Override
+ public void recordMetricHighWater(String metricName, long value) {
+ AtomicLong highWater = languageMetric(languageHighWaterMarks, metricName);
+ if (highWater == null) {
+ return;
+ }
+ long current = highWater.get();
+ while (value > current && !highWater.compareAndSet(current, value)) {
+ current = highWater.get();
+ }
+ }
+
+ /** Immutable, name-sorted snapshot of Language's generic additive counters. */
+ public Map languageCounters() {
+ return immutableSortedValues(languageCounters);
+ }
+
+ /** Immutable, name-sorted snapshot of Language's generic current-value gauges. */
+ public Map languageGauges() {
+ return immutableSortedValues(languageGauges);
+ }
+
+ /** Immutable, name-sorted snapshot of Language's generic high-water gauges. */
+ public Map languageHighWaterMarks() {
+ return immutableSortedValues(languageHighWaterMarks);
+ }
+
+ /** Number of generic metric samples dropped because their new name exceeded the cap. */
+ public long droppedLanguageMetricNames() {
+ return droppedLanguageMetricNames.get();
+ }
+
public Snapshot snapshot() {
return new Snapshot(this);
}
+ private AtomicLong languageMetric(ConcurrentMap metrics,
+ String metricName) {
+ requireMetricName(metricName);
+ if (!acceptLanguageMetricName(metricName)) {
+ return null;
+ }
+ AtomicLong current = metrics.get(metricName);
+ if (current != null) {
+ return current;
+ }
+ AtomicLong created = new AtomicLong();
+ AtomicLong raced = metrics.putIfAbsent(metricName, created);
+ return raced != null ? raced : created;
+ }
+
+ private boolean acceptLanguageMetricName(String metricName) {
+ if (languageMetricNames.containsKey(metricName)) {
+ return true;
+ }
+ synchronized (languageMetricNameLock) {
+ if (languageMetricNames.containsKey(metricName)) {
+ return true;
+ }
+ if (languageMetricNames.size() >= MAX_LANGUAGE_METRIC_NAMES) {
+ droppedLanguageMetricNames.incrementAndGet();
+ return false;
+ }
+ languageMetricNames.put(metricName, Boolean.TRUE);
+ return true;
+ }
+ }
+
+ private static void requireMetricName(String metricName) {
+ if (metricName == null || metricName.isEmpty()) {
+ throw new IllegalArgumentException("metricName must not be empty");
+ }
+ }
+
+ private static Map immutableSortedValues(
+ ConcurrentMap source) {
+ Map values = new TreeMap<>();
+ for (Map.Entry entry : source.entrySet()) {
+ values.put(entry.getKey(), entry.getValue().get());
+ }
+ return Collections.unmodifiableMap(values);
+ }
+
+ private static void addToNonNegativeGauge(final AtomicLong gauge, final long delta) {
+ gauge.updateAndGet(current -> {
+ if (delta >= 0L) {
+ return current > Long.MAX_VALUE - delta ? Long.MAX_VALUE : current + delta;
+ }
+ if (delta == Long.MIN_VALUE || current < -delta) {
+ return 0L;
+ }
+ return current + delta;
+ });
+ }
+
private static long nonNegative(long nanos) {
return nanos > 0L ? nanos : 0L;
}
public static final class Snapshot {
public final long workflowStepsExecuted;
+ public final long workflowPlansBuilt;
+ public final long workflowPlanCacheHits;
+ public final long workflowPlanCacheMisses;
+ public final long workflowPlanCacheEvictions;
+ public final long workflowPlanWeightBytes;
+ public final long workflowExecutorLookups;
+ public final long workflowStepResultSnapshotsCreated;
+ public final long workflowStepResultViewHits;
public final long computeStepsExecuted;
+ public final long computePlansBuilt;
+ public final long computePlanCacheHits;
+ public final long computePlanCacheMisses;
+ public final long computePlanCacheEvictions;
+ public final long computePlanWeightBytes;
public final long updateDocumentStepsExecuted;
public final long triggerEventStepsExecuted;
public final long directBexChangesetHits;
@@ -1057,6 +1580,9 @@ public static final class Snapshot {
public final long computeResultValidationFailures;
public final long computeProgramNormalizations;
public final long computeDefinitionNormalizations;
+ public final long computeDefinitionMaterializations;
+ public final long computeDefinitionFrozenDirectHits;
+ public final long computeProgramSourceBuilds;
public final long computeDefinitionResolveHits;
public final long computeDefinitionResolveMisses;
public final long workflowRunnerNanos;
@@ -1071,6 +1597,9 @@ public static final class Snapshot {
public final long updatePatchApplyNanos;
public final long updateBatchPatchApplications;
public final long updateIndividualPatchApplications;
+ public final long updateStaticTemplatesBuilt;
+ public final long updateStaticTemplateHits;
+ public final long updateReflectionFallbacks;
public final long triggerStepNanos;
public final long triggerEmitEventNanos;
public final long bexCompileNanos;
@@ -1080,6 +1609,8 @@ public static final class Snapshot {
public final long bexCompiledExecutions;
public final long bexNodeWriterNanos;
public final long directBexPatchEntryConversions;
+ public final long bexPatchFrozenDirectConversions;
+ public final long bexPatchNodeMaterializations;
public final long processDocumentNanos;
public final long blueProcessDocumentNanos;
public final long eventPreprocessNanos;
@@ -1145,6 +1676,25 @@ public static final class Snapshot {
public final long batchPatchCommitNanos;
public final long documentUpdateBeforeMaterializations;
public final long documentUpdateAfterMaterializations;
+ public final long preparedPatchSequences;
+ public final long preparedPatches;
+ public final long languageSequenceTransactions;
+ public final long languageSingletonTransactions;
+ public final long sequencePlanningNanos;
+ public final long sequenceConformanceNanos;
+ public final long sequenceCommitNanos;
+ public final long sequenceFinalCacheCommitNanos;
+ public final long languageIntermediateSnapshotAdvances;
+ public final long sequenceSharedSnapshotCacheInserts;
+ public final long languageFinalSnapshotPromotions;
+ public final long sequenceFinalSnapshotCacheInserts;
+ public final long languageSuffixRebases;
+ public final long sequenceStalePreviewFallbacks;
+ public final long languageFallbackPatches;
+ public final long parsedPointerCacheHits;
+ public final long parsedPointerCacheMisses;
+ public final long frozenPatchValueHits;
+ public final long languagePatchValueMaterializations;
public final long workflowDocumentViewsFromFrozen;
public final long workflowDocumentViewsFromDocument;
public final long workflowDocumentViewMisses;
@@ -1152,10 +1702,27 @@ public static final class Snapshot {
public final long bexDocumentViewFrozenDirectHits;
public final long bexDocumentViewFrozenRootFallbackHits;
public final long bexDocumentViewUndefinedHits;
+ public final Map languageCounters;
+ public final Map languageGauges;
+ public final Map languageHighWaterMarks;
+ public final long droppedLanguageMetricNames;
private Snapshot(BexProcessingMetrics metrics) {
this.workflowStepsExecuted = metrics.workflowStepsExecuted();
+ this.workflowPlansBuilt = metrics.workflowPlansBuilt();
+ this.workflowPlanCacheHits = metrics.workflowPlanCacheHits();
+ this.workflowPlanCacheMisses = metrics.workflowPlanCacheMisses();
+ this.workflowPlanCacheEvictions = metrics.workflowPlanCacheEvictions();
+ this.workflowPlanWeightBytes = metrics.workflowPlanWeightBytes();
+ this.workflowExecutorLookups = metrics.workflowExecutorLookups();
+ this.workflowStepResultSnapshotsCreated = metrics.workflowStepResultSnapshotsCreated();
+ this.workflowStepResultViewHits = metrics.workflowStepResultViewHits();
this.computeStepsExecuted = metrics.computeStepsExecuted();
+ this.computePlansBuilt = metrics.computePlansBuilt();
+ this.computePlanCacheHits = metrics.computePlanCacheHits();
+ this.computePlanCacheMisses = metrics.computePlanCacheMisses();
+ this.computePlanCacheEvictions = metrics.computePlanCacheEvictions();
+ this.computePlanWeightBytes = metrics.computePlanWeightBytes();
this.updateDocumentStepsExecuted = metrics.updateDocumentStepsExecuted();
this.triggerEventStepsExecuted = metrics.triggerEventStepsExecuted();
this.directBexChangesetHits = metrics.directBexChangesetHits();
@@ -1167,6 +1734,9 @@ private Snapshot(BexProcessingMetrics metrics) {
this.computeResultValidationFailures = metrics.computeResultValidationFailures();
this.computeProgramNormalizations = metrics.computeProgramNormalizations();
this.computeDefinitionNormalizations = metrics.computeDefinitionNormalizations();
+ this.computeDefinitionMaterializations = metrics.computeDefinitionMaterializations();
+ this.computeDefinitionFrozenDirectHits = metrics.computeDefinitionFrozenDirectHits();
+ this.computeProgramSourceBuilds = metrics.computeProgramSourceBuilds();
this.computeDefinitionResolveHits = metrics.computeDefinitionResolveHits();
this.computeDefinitionResolveMisses = metrics.computeDefinitionResolveMisses();
this.workflowRunnerNanos = metrics.workflowRunnerNanos();
@@ -1181,6 +1751,9 @@ private Snapshot(BexProcessingMetrics metrics) {
this.updatePatchApplyNanos = metrics.updatePatchApplyNanos();
this.updateBatchPatchApplications = metrics.updateBatchPatchApplications();
this.updateIndividualPatchApplications = metrics.updateIndividualPatchApplications();
+ this.updateStaticTemplatesBuilt = metrics.updateStaticTemplatesBuilt();
+ this.updateStaticTemplateHits = metrics.updateStaticTemplateHits();
+ this.updateReflectionFallbacks = metrics.updateReflectionFallbacks();
this.triggerStepNanos = metrics.triggerStepNanos();
this.triggerEmitEventNanos = metrics.triggerEmitEventNanos();
this.bexCompileNanos = metrics.bexCompileNanos();
@@ -1190,6 +1763,8 @@ private Snapshot(BexProcessingMetrics metrics) {
this.bexCompiledExecutions = metrics.bexCompiledExecutions();
this.bexNodeWriterNanos = metrics.bexNodeWriterNanos();
this.directBexPatchEntryConversions = metrics.directBexPatchEntryConversions();
+ this.bexPatchFrozenDirectConversions = metrics.bexPatchFrozenDirectConversions();
+ this.bexPatchNodeMaterializations = metrics.bexPatchNodeMaterializations();
this.processDocumentNanos = metrics.processDocumentNanos();
this.blueProcessDocumentNanos = metrics.blueProcessDocumentNanos();
this.eventPreprocessNanos = metrics.eventPreprocessNanos();
@@ -1255,6 +1830,25 @@ private Snapshot(BexProcessingMetrics metrics) {
this.batchPatchCommitNanos = metrics.batchPatchCommitNanos();
this.documentUpdateBeforeMaterializations = metrics.documentUpdateBeforeMaterializations();
this.documentUpdateAfterMaterializations = metrics.documentUpdateAfterMaterializations();
+ this.preparedPatchSequences = metrics.preparedPatchSequences();
+ this.preparedPatches = metrics.preparedPatches();
+ this.languageSequenceTransactions = metrics.languageSequenceTransactions();
+ this.languageSingletonTransactions = metrics.languageSingletonTransactions();
+ this.sequencePlanningNanos = metrics.sequencePlanningNanos();
+ this.sequenceConformanceNanos = metrics.sequenceConformanceNanos();
+ this.sequenceCommitNanos = metrics.sequenceCommitNanos();
+ this.sequenceFinalCacheCommitNanos = metrics.sequenceFinalCacheCommitNanos();
+ this.languageIntermediateSnapshotAdvances = metrics.languageIntermediateSnapshotAdvances();
+ this.sequenceSharedSnapshotCacheInserts = metrics.sequenceSharedSnapshotCacheInserts();
+ this.languageFinalSnapshotPromotions = metrics.languageFinalSnapshotPromotions();
+ this.sequenceFinalSnapshotCacheInserts = metrics.sequenceFinalSnapshotCacheInserts();
+ this.languageSuffixRebases = metrics.languageSuffixRebases();
+ this.sequenceStalePreviewFallbacks = metrics.sequenceStalePreviewFallbacks();
+ this.languageFallbackPatches = metrics.languageFallbackPatches();
+ this.parsedPointerCacheHits = metrics.parsedPointerCacheHits();
+ this.parsedPointerCacheMisses = metrics.parsedPointerCacheMisses();
+ this.frozenPatchValueHits = metrics.frozenPatchValueHits();
+ this.languagePatchValueMaterializations = metrics.languagePatchValueMaterializations();
this.workflowDocumentViewsFromFrozen = metrics.workflowDocumentViewsFromFrozen();
this.workflowDocumentViewsFromDocument = metrics.workflowDocumentViewsFromDocument();
this.workflowDocumentViewMisses = metrics.workflowDocumentViewMisses();
@@ -1262,6 +1856,10 @@ private Snapshot(BexProcessingMetrics metrics) {
this.bexDocumentViewFrozenDirectHits = metrics.bexDocumentViewFrozenDirectHits();
this.bexDocumentViewFrozenRootFallbackHits = metrics.bexDocumentViewFrozenRootFallbackHits();
this.bexDocumentViewUndefinedHits = metrics.bexDocumentViewUndefinedHits();
+ this.languageCounters = metrics.languageCounters();
+ this.languageGauges = metrics.languageGauges();
+ this.languageHighWaterMarks = metrics.languageHighWaterMarks();
+ this.droppedLanguageMetricNames = metrics.droppedLanguageMetricNames();
}
}
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeDefinitionResolver.java b/src/main/java/blue/coordination/processor/workflow/ComputeDefinitionResolver.java
index cc42134..3f4eeed 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeDefinitionResolver.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeDefinitionResolver.java
@@ -3,12 +3,9 @@
import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.model.Node;
import blue.language.snapshot.FrozenNode;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
final class ComputeDefinitionResolver {
private final BexProcessingMetrics metrics;
- private final ConcurrentMap cache = new ConcurrentHashMap();
ComputeDefinitionResolver() {
this(null);
@@ -19,6 +16,12 @@ final class ComputeDefinitionResolver {
}
FrozenNode resolve(FrozenNode stepNode, StepExecutionContext context) {
+ return resolve(stepNode, context, metrics);
+ }
+
+ FrozenNode resolve(FrozenNode stepNode,
+ StepExecutionContext context,
+ BexProcessingMetrics invocationMetrics) {
FrozenNode definition = FrozenNodeUtil.property(stepNode, "definition");
if (definition == null || FrozenNodeUtil.isEmpty(definition)) {
return null;
@@ -26,13 +29,19 @@ FrozenNode resolve(FrozenNode stepNode, StepExecutionContext context) {
String text = FrozenNodeUtil.text(definition);
if (text != null && !text.trim().isEmpty()) {
String pointer = resolvePointer(text.trim(), context);
- FrozenNode frozen = cachedFrozenAt(pointer, context);
+ // This lookup is deliberately performed against the current
+ // WorkingDocument on every invocation. The exact returned frozen
+ // identity participates in the Compute plan key, so a definition
+ // changed by an earlier step can never reuse a stale plan.
+ FrozenNode frozen = context.workingResolvedAt(pointer);
if (frozen == null) {
context.processorContext().throwFatal("Compute definition not found: " + text);
return null;
}
+ incrementFrozenDirectHit(invocationMetrics);
return frozen;
}
+ incrementFrozenDirectHit(invocationMetrics);
return definition;
}
@@ -44,13 +53,17 @@ FrozenNode resolve(Node stepNode, StepExecutionContext context) {
String text = NodeUtil.text(definition);
if (text != null && !text.trim().isEmpty()) {
String pointer = resolvePointer(text.trim(), context);
- FrozenNode frozen = cachedFrozenAt(pointer, context);
+ FrozenNode frozen = context.workingResolvedAt(pointer);
if (frozen == null) {
context.processorContext().throwFatal("Compute definition not found: " + text);
return null;
}
+ incrementFrozenDirectHit(metrics);
return frozen;
}
+ if (metrics != null) {
+ metrics.incrementComputeDefinitionMaterializations();
+ }
return FrozenNode.fromResolvedNode(definition);
}
@@ -65,31 +78,13 @@ String resolvePointer(String reference, StepExecutionContext context) {
return appendPointer(parent, reference);
}
- private FrozenNode cachedFrozenAt(String pointer, StepExecutionContext context) {
- String key = cacheKey(pointer, context);
- FrozenNode cached = cache.get(key);
- if (cached != null) {
- if (metrics != null) {
- metrics.incrementComputeDefinitionResolveHits();
- }
- return cached;
+ private void incrementFrozenDirectHit(BexProcessingMetrics invocationMetrics) {
+ BexProcessingMetrics activeMetrics = invocationMetrics != null
+ ? invocationMetrics
+ : metrics;
+ if (activeMetrics != null) {
+ activeMetrics.incrementComputeDefinitionFrozenDirectHits();
}
- FrozenNode frozen = context.workingResolvedAt(pointer);
- if (frozen != null) {
- cache.putIfAbsent(key, frozen);
- }
- if (metrics != null) {
- metrics.incrementComputeDefinitionResolveMisses();
- }
- return frozen;
- }
-
- private String cacheKey(String pointer, StepExecutionContext context) {
- FrozenNode contract = context.currentContractFrozenNode();
- String contractId = contract != null && contract.blueId() != null
- ? contract.blueId()
- : String.valueOf(context.processorContext().scopePath()) + ":" + String.valueOf(context.processorContext().contractKey());
- return contractId + "|" + pointer;
}
private String currentContractPointer(StepExecutionContext context) {
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java b/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
index c9fd1f3..758edd9 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
@@ -1,7 +1,7 @@
package blue.coordination.processor.workflow;
import blue.language.model.Node;
-import blue.language.processor.model.JsonPatch;
+import blue.language.processor.model.FrozenJsonPatch;
import blue.language.snapshot.FrozenNode;
import java.util.ArrayList;
@@ -16,21 +16,26 @@
* duplicate buffering when a caller retries delivery of the same plan.
*/
final class ComputeEffectPlan {
- private final List patches;
+ private final List patches;
private final List events;
private final boolean terminationRequested;
private final String terminationReason;
private final boolean changesetHandled;
private final AtomicBoolean bufferingClaimed = new AtomicBoolean();
- ComputeEffectPlan(List patches,
+ ComputeEffectPlan(List patches,
List events,
boolean terminationRequested,
String terminationReason,
boolean changesetHandled) {
- List frozenPatches = new ArrayList(patches.size());
- for (JsonPatch patch : patches) {
- frozenPatches.add(PlannedPatch.from(patch));
+ List frozenPatches = new ArrayList(patches.size());
+ for (FrozenJsonPatch patch : patches) {
+ if (patch == null) {
+ throw new IllegalArgumentException("Compute effect plan patch must not be null");
+ }
+ // FrozenJsonPatch is immutable, so retaining the patch itself is safe. The
+ // list still needs a defensive copy because callers may reuse its storage.
+ frozenPatches.add(patch);
}
this.patches = Collections.unmodifiableList(frozenPatches);
List frozenEvents = new ArrayList(events.size());
@@ -46,12 +51,8 @@ final class ComputeEffectPlan {
this.changesetHandled = changesetHandled;
}
- List patches() {
- List materialized = new ArrayList(patches.size());
- for (PlannedPatch patch : patches) {
- materialized.add(patch.materialize());
- }
- return Collections.unmodifiableList(materialized);
+ List patches() {
+ return patches;
}
List events() {
@@ -76,35 +77,4 @@ void claimForBuffering() {
}
}
- private static final class PlannedPatch {
- private final JsonPatch.Op op;
- private final String path;
- private final FrozenNode value;
-
- private PlannedPatch(JsonPatch.Op op, String path, FrozenNode value) {
- this.op = op;
- this.path = path;
- this.value = value;
- }
-
- private static PlannedPatch from(JsonPatch patch) {
- if (patch == null) {
- throw new IllegalArgumentException("Compute effect plan patch must not be null");
- }
- FrozenNode value = patch.getOp() == JsonPatch.Op.REMOVE
- ? null
- : FrozenNode.fromResolvedNode(patch.getVal());
- return new PlannedPatch(patch.getOp(), patch.getPath(), value);
- }
-
- private JsonPatch materialize() {
- if (op == JsonPatch.Op.ADD) {
- return JsonPatch.add(path, value.toNode());
- }
- if (op == JsonPatch.Op.REPLACE) {
- return JsonPatch.replace(path, value.toNode());
- }
- return JsonPatch.remove(path);
- }
- }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java b/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
index c72847c..b8e208e 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
@@ -1,20 +1,65 @@
package blue.coordination.processor.workflow;
import blue.coordination.processor.RepositoryTypeAliasPreprocessor;
+import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.model.Node;
+import blue.language.snapshot.FrozenNode;
import java.util.LinkedHashMap;
import java.util.Map;
final class ComputeProgramNormalizer {
+ private static final String NORMALIZATION_VERSION =
+ "compute-program-v2|repository-aliases-3.0.0-rc.10";
+
private final RepositoryTypeAliasPreprocessor typeAliasPreprocessor;
+ private final BexProcessingMetrics metrics;
ComputeProgramNormalizer() {
- this(new RepositoryTypeAliasPreprocessor());
+ this(new RepositoryTypeAliasPreprocessor(), null);
+ }
+
+ ComputeProgramNormalizer(BexProcessingMetrics metrics) {
+ this(new RepositoryTypeAliasPreprocessor(), metrics);
}
ComputeProgramNormalizer(RepositoryTypeAliasPreprocessor typeAliasPreprocessor) {
+ this(typeAliasPreprocessor, null);
+ }
+
+ private ComputeProgramNormalizer(RepositoryTypeAliasPreprocessor typeAliasPreprocessor,
+ BexProcessingMetrics metrics) {
+ if (typeAliasPreprocessor == null) {
+ throw new IllegalArgumentException("typeAliasPreprocessor must not be null");
+ }
this.typeAliasPreprocessor = typeAliasPreprocessor;
+ this.metrics = metrics;
+ }
+
+ String normalizationVersion() {
+ return NORMALIZATION_VERSION;
+ }
+
+ /**
+ * Normalizes only the authored Compute projection of a frozen step. This
+ * avoids materializing unrelated resolved-contract content. The selected
+ * subtrees still use the mutable alias preprocessor as a conservative,
+ * semantics-preserving cold-path fallback; the resulting frozen plan is
+ * reused on every warm invocation.
+ */
+ FrozenNode program(FrozenNode stepNode) {
+ if (metrics != null) {
+ metrics.incrementComputeProgramNormalizations();
+ }
+ return FrozenNode.fromResolvedNode(program(frozenProgramInput(stepNode)));
+ }
+
+ FrozenNode definition(FrozenNode definitionNode) {
+ if (metrics != null) {
+ metrics.incrementComputeDefinitionNormalizations();
+ metrics.incrementComputeDefinitionMaterializations();
+ }
+ return FrozenNode.fromResolvedNode(definition(frozenDefinitionInput(definitionNode)));
}
Node program(Node stepNode) {
@@ -48,6 +93,48 @@ Node definition(Node definitionNode) {
return typeAliasPreprocessor.preprocess(definition);
}
+ private Node frozenProgramInput(FrozenNode source) {
+ Node input = new Node();
+ copyMetadata(input, source);
+ Map properties = new LinkedHashMap();
+ copyFrozenProperty(properties, source, "expr");
+ copyFrozenProperty(properties, source, "do");
+ copyFrozenProperty(properties, source, "definition");
+ copyFrozenProperty(properties, source, "entry");
+ copyFrozenProperty(properties, source, "constants");
+ copyFrozenProperty(properties, source, "functions");
+ copyFrozenProperty(properties, source, "gasLimit");
+ copyFrozenProperty(properties, source, "emitEvents");
+ copyFrozenProperty(properties, source, "returnResult");
+ if (!properties.isEmpty()) {
+ input.properties(properties);
+ }
+ return input;
+ }
+
+ private Node frozenDefinitionInput(FrozenNode source) {
+ Node input = new Node();
+ copyMetadata(input, source);
+ Map properties = new LinkedHashMap();
+ copyFrozenProperty(properties, source, "constants");
+ copyFrozenProperty(properties, source, "functions");
+ if (!properties.isEmpty()) {
+ input.properties(properties);
+ }
+ return input;
+ }
+
+ private void copyFrozenProperty(Map target,
+ FrozenNode source,
+ String key) {
+ FrozenNode value = source != null && source.getProperties() != null
+ ? source.getProperties().get(key)
+ : null;
+ if (value != null) {
+ target.put(key, value.toNode());
+ }
+ }
+
private Node normalizeFunctions(Node functions) {
if (functions == null || functions.getProperties() == null || functions.getProperties().isEmpty()) {
return null;
@@ -120,4 +207,13 @@ private void copyMetadata(Node target, Node source) {
target.description(source.getDescription());
target.type(source.getType() != null ? source.getType().clone() : null);
}
+
+ private void copyMetadata(Node target, FrozenNode source) {
+ if (source == null) {
+ return;
+ }
+ target.name(source.getName());
+ target.description(source.getDescription());
+ target.type(source.getType() != null ? source.getType().toNode() : null);
+ }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlan.java b/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlan.java
new file mode 100644
index 0000000..5f01c08
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlan.java
@@ -0,0 +1,200 @@
+package blue.coordination.processor.workflow;
+
+import blue.bex.api.BexProgramSource;
+import blue.bex.compile.BexCompiledProgramKey;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable, reusable input plan for one exact Compute program and definition.
+ * Compiled BEX artifacts deliberately remain owned by BEX's bounded cache.
+ */
+final class ComputeProgramPlan {
+ private static final long PLAN_OVERHEAD_BYTES = 256L;
+ private static final long NODE_OVERHEAD_BYTES = 96L;
+ private static final long COLLECTION_ENTRY_OVERHEAD_BYTES = 32L;
+
+ private final FrozenNode programNode;
+ private final FrozenNode definitionNode;
+ private final BexProgramSource source;
+ private final BexCompiledProgramKey sourceIdentity;
+ private final String entry;
+ private final long gasLimit;
+ private final boolean emitEvents;
+ private final boolean returnResult;
+ private final long approximateWeightBytes;
+
+ ComputeProgramPlan(FrozenNode programNode,
+ FrozenNode definitionNode,
+ BexProgramSource source,
+ String entry,
+ long gasLimit,
+ boolean emitEvents,
+ boolean returnResult,
+ FrozenNode rawStepNode,
+ FrozenNode rawDefinitionNode) {
+ if (programNode == null) {
+ throw new IllegalArgumentException("programNode must not be null");
+ }
+ if (source == null) {
+ throw new IllegalArgumentException("source must not be null");
+ }
+ if (gasLimit <= 0L) {
+ throw new IllegalArgumentException("gasLimit must be positive");
+ }
+ this.programNode = programNode;
+ this.definitionNode = definitionNode;
+ this.source = source;
+ this.sourceIdentity = BexCompiledProgramKey.from(source);
+ this.entry = entry;
+ this.gasLimit = gasLimit;
+ this.emitEvents = emitEvents;
+ this.returnResult = returnResult;
+ this.approximateWeightBytes = approximateWeight(rawStepNode,
+ rawDefinitionNode,
+ programNode,
+ definitionNode,
+ entry,
+ sourceIdentity);
+ }
+
+ FrozenNode programNode() {
+ return programNode;
+ }
+
+ FrozenNode definitionNode() {
+ return definitionNode;
+ }
+
+ BexProgramSource source() {
+ return source;
+ }
+
+ BexCompiledProgramKey sourceIdentity() {
+ return sourceIdentity;
+ }
+
+ String entry() {
+ return entry;
+ }
+
+ long gasLimit() {
+ return gasLimit;
+ }
+
+ boolean emitEvents() {
+ return emitEvents;
+ }
+
+ boolean returnResult() {
+ return returnResult;
+ }
+
+ long approximateWeightBytes() {
+ return approximateWeightBytes;
+ }
+
+ private static long approximateWeight(FrozenNode rawStepNode,
+ FrozenNode rawDefinitionNode,
+ FrozenNode programNode,
+ FrozenNode definitionNode,
+ String entry,
+ BexCompiledProgramKey sourceIdentity) {
+ long weight = PLAN_OVERHEAD_BYTES;
+ // Raw nodes approximate the independently retained structural cache
+ // keys, while normalized nodes approximate the plan/source graph.
+ // Deduplicate sharing within each retained graph, not across them.
+ IdentityHashMap keyNodes = new IdentityHashMap();
+ weight = saturatedAdd(weight, nodeWeight(rawStepNode, keyNodes));
+ weight = saturatedAdd(weight, nodeWeight(rawDefinitionNode, keyNodes));
+ IdentityHashMap planNodes = new IdentityHashMap();
+ weight = saturatedAdd(weight, nodeWeight(programNode, planNodes));
+ weight = saturatedAdd(weight, nodeWeight(definitionNode, planNodes));
+ weight = saturatedAdd(weight, stringWeight(entry));
+ weight = saturatedAdd(weight, stringWeight(sourceIdentity.programIdentity()));
+ weight = saturatedAdd(weight, stringWeight(sourceIdentity.definitionIdentity()));
+ weight = saturatedAdd(weight, stringWeight(sourceIdentity.entryName()));
+ return Math.max(PLAN_OVERHEAD_BYTES, weight);
+ }
+
+ private static long nodeWeight(FrozenNode root,
+ IdentityHashMap visited) {
+ if (root == null) {
+ return 0L;
+ }
+ long weight = 0L;
+ Deque pending = new ArrayDeque();
+ pending.push(root);
+ while (!pending.isEmpty()) {
+ FrozenNode node = pending.pop();
+ if (visited.put(node, Boolean.TRUE) != null) {
+ continue;
+ }
+ weight = saturatedAdd(weight, NODE_OVERHEAD_BYTES);
+ weight = saturatedAdd(weight, stringWeight(node.getName()));
+ weight = saturatedAdd(weight, stringWeight(node.getDescription()));
+ weight = saturatedAdd(weight, stringWeight(node.getReferenceBlueId()));
+ weight = saturatedAdd(weight, stringWeight(node.getMergePolicy()));
+ weight = saturatedAdd(weight, stringWeight(node.getPreviousBlueId()));
+ Object value = node.getValue();
+ if (value instanceof String) {
+ weight = saturatedAdd(weight, stringWeight((String) value));
+ } else if (value != null) {
+ weight = saturatedAdd(weight, 32L);
+ }
+ pushIfPresent(pending, node.getType());
+ pushIfPresent(pending, node.getItemType());
+ pushIfPresent(pending, node.getKeyType());
+ pushIfPresent(pending, node.getValueType());
+ pushIfPresent(pending, node.getContracts());
+ pushIfPresent(pending, node.getBlue());
+ List items = node.getItems();
+ if (items != null) {
+ weight = saturatedAdd(weight,
+ saturatedMultiply(COLLECTION_ENTRY_OVERHEAD_BYTES, items.size()));
+ for (FrozenNode item : items) {
+ pushIfPresent(pending, item);
+ }
+ }
+ Map properties = node.getProperties();
+ if (properties != null) {
+ weight = saturatedAdd(weight,
+ saturatedMultiply(COLLECTION_ENTRY_OVERHEAD_BYTES, properties.size()));
+ for (Map.Entry property : properties.entrySet()) {
+ weight = saturatedAdd(weight, stringWeight(property.getKey()));
+ pushIfPresent(pending, property.getValue());
+ }
+ }
+ }
+ return weight;
+ }
+
+ private static void pushIfPresent(Deque pending, FrozenNode node) {
+ if (node != null) {
+ pending.push(node);
+ }
+ }
+
+ private static long stringWeight(String value) {
+ return value == null ? 0L : 40L + (2L * value.length());
+ }
+
+ private static long saturatedAdd(long left, long right) {
+ if (right <= 0L) {
+ return left;
+ }
+ return left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right;
+ }
+
+ private static long saturatedMultiply(long left, int right) {
+ if (left <= 0L || right <= 0) {
+ return 0L;
+ }
+ return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right;
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlanCache.java b/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlanCache.java
new file mode 100644
index 0000000..fc9df0a
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeProgramPlanCache.java
@@ -0,0 +1,244 @@
+package blue.coordination.processor.workflow;
+
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/** Thread-safe, entry- and weight-bounded LRU for immutable Compute plans. */
+final class ComputeProgramPlanCache implements AutoCloseable {
+ static final int DEFAULT_MAX_ENTRIES = 256;
+ static final long DEFAULT_MAX_WEIGHT_BYTES = 16L * 1024L * 1024L;
+
+ interface PlanFactory {
+ ComputeProgramPlan create();
+ }
+
+ static final class Key {
+ private final FrozenNode.ResolvedStructuralKey rawStepIdentity;
+ private final FrozenNode.ResolvedStructuralKey definitionIdentity;
+ private final String effectiveEntry;
+ private final String normalizationVersion;
+ private final int hashCode;
+
+ private Key(FrozenNode rawStepNode,
+ FrozenNode rawDefinitionNode,
+ String effectiveEntry,
+ String normalizationVersion) {
+ if (rawStepNode == null) {
+ throw new IllegalArgumentException("rawStepNode must not be null");
+ }
+ if (normalizationVersion == null || normalizationVersion.isEmpty()) {
+ throw new IllegalArgumentException("normalizationVersion must not be empty");
+ }
+ this.rawStepIdentity = rawStepNode.resolvedStructuralKey();
+ this.definitionIdentity = rawDefinitionNode != null
+ ? rawDefinitionNode.resolvedStructuralKey()
+ : null;
+ this.effectiveEntry = effectiveEntry;
+ this.normalizationVersion = normalizationVersion;
+ this.hashCode = Objects.hash(rawStepIdentity,
+ definitionIdentity,
+ effectiveEntry,
+ normalizationVersion);
+ }
+
+ static Key from(FrozenNode rawStepNode,
+ FrozenNode rawDefinitionNode,
+ String effectiveEntry,
+ String normalizationVersion) {
+ return new Key(rawStepNode,
+ rawDefinitionNode,
+ effectiveEntry,
+ normalizationVersion);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Key)) {
+ return false;
+ }
+ Key that = (Key) other;
+ return rawStepIdentity.equals(that.rawStepIdentity)
+ && Objects.equals(definitionIdentity, that.definitionIdentity)
+ && Objects.equals(effectiveEntry, that.effectiveEntry)
+ && normalizationVersion.equals(that.normalizationVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+ }
+
+ static final class Lookup {
+ private final Key key;
+ private final ComputeProgramPlan plan;
+ private final boolean cacheHit;
+ private final long generation;
+
+ private Lookup(Key key,
+ ComputeProgramPlan plan,
+ boolean cacheHit,
+ long generation) {
+ this.key = key;
+ this.plan = plan;
+ this.cacheHit = cacheHit;
+ this.generation = generation;
+ }
+
+ ComputeProgramPlan plan() {
+ return plan;
+ }
+
+ boolean cacheHit() {
+ return cacheHit;
+ }
+ }
+
+ private static final class Entry {
+ private final ComputeProgramPlan plan;
+ private final long weightBytes;
+
+ private Entry(ComputeProgramPlan plan) {
+ this.plan = plan;
+ this.weightBytes = plan.approximateWeightBytes();
+ }
+ }
+
+ private final int maxEntries;
+ private final long maxWeightBytes;
+ private final BexProcessingMetrics metrics;
+ private final LinkedHashMap entries =
+ new LinkedHashMap(16, 0.75f, true);
+ private long weightBytes;
+ private long generation;
+ private boolean closed;
+
+ ComputeProgramPlanCache() {
+ this(DEFAULT_MAX_ENTRIES, DEFAULT_MAX_WEIGHT_BYTES, null);
+ }
+
+ ComputeProgramPlanCache(int maxEntries,
+ long maxWeightBytes,
+ BexProcessingMetrics metrics) {
+ if (maxEntries <= 0) {
+ throw new IllegalArgumentException("maxEntries must be positive");
+ }
+ if (maxWeightBytes <= 0L) {
+ throw new IllegalArgumentException("maxWeightBytes must be positive");
+ }
+ this.maxEntries = maxEntries;
+ this.maxWeightBytes = maxWeightBytes;
+ this.metrics = metrics;
+ }
+
+ Lookup lookup(Key key, PlanFactory factory) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+ long lookupGeneration;
+ synchronized (this) {
+ Entry cached = entries.get(key);
+ if (cached != null) {
+ if (metrics != null) {
+ metrics.incrementComputePlanCacheHits();
+ }
+ return new Lookup(key, cached.plan, true, generation);
+ }
+ if (metrics != null) {
+ metrics.incrementComputePlanCacheMisses();
+ }
+ lookupGeneration = generation;
+ }
+
+ ComputeProgramPlan plan = factory.create();
+ if (plan == null) {
+ throw new IllegalStateException("Compute plan factory returned null");
+ }
+ if (metrics != null) {
+ metrics.incrementComputePlansBuilt();
+ }
+ return new Lookup(key, plan, false, lookupGeneration);
+ }
+
+ void publish(Lookup lookup) {
+ if (lookup == null || lookup.cacheHit) {
+ return;
+ }
+ Entry added = new Entry(lookup.plan);
+ if (added.weightBytes > maxWeightBytes) {
+ return;
+ }
+ synchronized (this) {
+ if (closed || lookup.generation != generation) {
+ return;
+ }
+ Entry existing = entries.get(lookup.key);
+ if (existing != null) {
+ return;
+ }
+ entries.put(lookup.key, added);
+ weightBytes = saturatedAdd(weightBytes, added.weightBytes);
+ if (metrics != null) {
+ metrics.addComputePlanWeightBytes(added.weightBytes);
+ }
+ evictIfNeeded();
+ }
+ }
+
+ synchronized int size() {
+ return entries.size();
+ }
+
+ synchronized long weightBytes() {
+ return weightBytes;
+ }
+
+ synchronized boolean isClosed() {
+ return closed;
+ }
+
+ synchronized void clear() {
+ if (weightBytes != 0L && metrics != null) {
+ metrics.addComputePlanWeightBytes(-weightBytes);
+ }
+ entries.clear();
+ weightBytes = 0L;
+ generation++;
+ }
+
+ @Override
+ public synchronized void close() {
+ clear();
+ closed = true;
+ }
+
+ private void evictIfNeeded() {
+ Iterator> iterator = entries.entrySet().iterator();
+ while ((entries.size() > maxEntries || weightBytes > maxWeightBytes)
+ && iterator.hasNext()) {
+ Map.Entry eldest = iterator.next();
+ long evictedWeight = eldest.getValue().weightBytes;
+ iterator.remove();
+ weightBytes -= evictedWeight;
+ if (metrics != null) {
+ metrics.incrementComputePlanCacheEvictions();
+ metrics.addComputePlanWeightBytes(-evictedWeight);
+ }
+ }
+ }
+
+ private long saturatedAdd(long left, long right) {
+ return left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right;
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java b/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
index bf0b022..433aca1 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
@@ -3,13 +3,14 @@
import blue.bex.result.BexChangeset;
import blue.bex.result.BexExecutionResult;
import blue.bex.result.BexPatchEntry;
+import blue.bex.value.BexFrozenWriter;
import blue.bex.value.BexNodeWriter;
import blue.bex.value.BexValue;
import blue.bex.value.BexValues;
import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.model.Node;
import blue.language.processor.WorkingDocument;
-import blue.language.processor.model.JsonPatch;
+import blue.language.processor.model.FrozenJsonPatch;
import blue.language.snapshot.FrozenNode;
import java.util.ArrayList;
@@ -36,7 +37,7 @@ ComputeEffectPlan plan(BexExecutionResult result,
}
try {
boolean returnedChangeset = hasReturnedChangeset(result);
- List patches;
+ List patches;
try {
patches = changesetPatches(result, context);
} catch (ComputeResultValidationException ex) {
@@ -76,7 +77,7 @@ void buffer(ComputeEffectPlan plan, StepExecutionContext context) {
throw new IllegalArgumentException("plan must not be null");
}
plan.claimForBuffering();
- List patches = plan.patches();
+ List patches = plan.patches();
if (!patches.isEmpty()) {
applyPatches(patches, context);
}
@@ -154,7 +155,8 @@ private Termination termination(BexExecutionResult result) {
return Termination.requested(reason.asText());
}
- private List changesetPatches(BexExecutionResult result, StepExecutionContext context) {
+ private List changesetPatches(BexExecutionResult result,
+ StepExecutionContext context) {
BexValue changeset = result.value() != null ? result.value().get("changeset") : BexValues.undefined();
BexChangeset accumulated = result.changeset();
if (changeset.isUndefined() || changeset.isNull()) {
@@ -169,7 +171,7 @@ private List changesetPatches(BexExecutionResult result, StepExecutio
if (isAccumulatedChangesetValue(changeset, accumulated)) {
return patchesFromBexChangeset(accumulated, context);
}
- List patches = new ArrayList(changeset.size());
+ List patches = new ArrayList(changeset.size());
for (int i = 0; i < changeset.size(); i++) {
WorkflowPatchEntry entry = patchEntry(changeset.get(String.valueOf(i)), i);
patches.add(toPatch(entry, context));
@@ -177,7 +179,8 @@ private List changesetPatches(BexExecutionResult result, StepExecutio
return patches;
}
- private List patchesFromBexChangeset(BexChangeset changeset, StepExecutionContext context) {
+ private List patchesFromBexChangeset(BexChangeset changeset,
+ StepExecutionContext context) {
if (changeset == null || changeset.entries().isEmpty()) {
return Collections.emptyList();
}
@@ -186,7 +189,8 @@ private List patchesFromBexChangeset(BexChangeset changeset, StepExec
}
long conversionStart = System.nanoTime();
try {
- List patches = new ArrayList(changeset.entries().size());
+ List patches =
+ new ArrayList(changeset.entries().size());
for (BexPatchEntry entry : changeset.entries()) {
patches.add(toPatch(entry, context));
if (metrics != null) {
@@ -213,38 +217,33 @@ private WorkflowPatchEntry patchEntry(BexValue item, int index) {
if (path == null || path.trim().isEmpty()) {
throw invalid("Compute result changeset entry " + index + " missing path");
}
- Node nodeValue = null;
+ FrozenNode nodeValue = null;
if (!"remove".equals(op)) {
BexValue val = item.get("val");
if (val.isUndefined()) {
throw invalid("Compute result changeset entry " + index + " missing val");
}
- long writerStart = System.nanoTime();
- try {
- nodeValue = BexNodeWriter.toNode(val);
- } finally {
- if (metrics != null) {
- metrics.addBexNodeWriterNanos(System.nanoTime() - writerStart);
- }
- }
+ nodeValue = freezePatchValue(val);
}
return new WorkflowPatchEntry(op, path, nodeValue);
}
- private JsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext context) {
+ private FrozenJsonPatch toPatch(WorkflowPatchEntry entry,
+ StepExecutionContext context) {
String normalizedOp = entry.op().trim().toLowerCase();
String path = resolvedPointer(entry.path(), context);
if ("remove".equals(normalizedOp)) {
- return JsonPatch.remove(path);
+ return FrozenJsonPatch.remove(path);
}
if ("add".equals(normalizedOp)) {
- return JsonPatch.add(path, entry.val());
+ return FrozenJsonPatch.add(path, entry.val());
}
// patchEntry has already restricted this branch to replace.
- return JsonPatch.replace(path, entry.val());
+ return FrozenJsonPatch.replace(path, entry.val());
}
- private JsonPatch toPatch(BexPatchEntry entry, StepExecutionContext context) {
+ private FrozenJsonPatch toPatch(BexPatchEntry entry,
+ StepExecutionContext context) {
if (entry == null) {
throw invalid("Compute result accumulated patch is incomplete");
}
@@ -255,22 +254,14 @@ private JsonPatch toPatch(BexPatchEntry entry, StepExecutionContext context) {
}
String path = resolvedPointer(entry.authoredPath(), context);
if (remove) {
- return JsonPatch.remove(path);
- }
- long writerStart = System.nanoTime();
- Node value;
- try {
- value = BexNodeWriter.toNode(entry.val());
- } finally {
- if (metrics != null) {
- metrics.addBexNodeWriterNanos(System.nanoTime() - writerStart);
- }
+ return FrozenJsonPatch.remove(path);
}
+ FrozenNode value = freezePatchValue(entry.val());
if ("add".equals(normalizedOp)) {
- return JsonPatch.add(path, value);
+ return FrozenJsonPatch.add(path, value);
}
// BexPatchEntry has already restricted this branch to replace.
- return JsonPatch.replace(path, value);
+ return FrozenJsonPatch.replace(path, value);
}
private String resolvedPointer(String authoredPath, StepExecutionContext context) {
@@ -281,14 +272,32 @@ private String resolvedPointer(String authoredPath, StepExecutionContext context
}
}
- private void applyPatches(List patches, StepExecutionContext context) {
+ private void applyPatches(List patches,
+ StepExecutionContext context) {
long applyStart = System.nanoTime();
boolean applied = false;
+ boolean previewTransferred = false;
+ WorkingDocument.Preview preview = null;
+ long frozenValueCount = frozenValueCount(patches);
try {
- WorkingDocument.Preview preview = context.advanceWorkingDocument(patches);
- context.processorContext().applyPreviewedPatches(patches, preview);
+ preview = context.advanceWorkingDocumentFrozen(patches);
+ if (preview == null) {
+ return;
+ }
+ if (metrics != null) {
+ metrics.addMetric("frozenPatchesHandedToLanguage", patches.size());
+ metrics.addMetric("frozenPatchValuesHandedToLanguage", frozenValueCount);
+ }
+ context.processorContext().applyPreviewedFrozenPatches(patches, preview);
+ previewTransferred = true;
+ if (metrics != null) {
+ metrics.addMetric("frozenPatchValuesHandedToLanguage", frozenValueCount);
+ }
applied = true;
} finally {
+ if (!previewTransferred && preview != null) {
+ preview.close();
+ }
if (metrics != null) {
metrics.addUpdatePatchApplyNanos(System.nanoTime() - applyStart);
if (applied) {
@@ -299,6 +308,16 @@ private void applyPatches(List patches, StepExecutionContext context)
}
}
+ private long frozenValueCount(List patches) {
+ long count = 0L;
+ for (FrozenJsonPatch patch : patches) {
+ if (patch.getValue() != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
private boolean isAccumulatedChangesetValue(BexValue value, BexChangeset changeset) {
if (value == null || !value.isList() || changeset == null) {
return false;
@@ -324,13 +343,52 @@ private boolean isAccumulatedChangesetValue(BexValue value, BexChangeset changes
if (val != null && !val.isUndefined() && !val.isNull()) {
return false;
}
- } else if (val == null || val.isUndefined() || !Objects.equals(entry.val().toSimple(), val.toSimple())) {
+ } else if (val == null || val.isUndefined()) {
+ return false;
+ } else if (entry.val() != val
+ && !Objects.equals(entry.val().toSimple(), val.toSimple())) {
+ // BEX's accumulated changeset view preserves the exact value object
+ // held by each BexPatchEntry. The identity branch is therefore the
+ // normal path; deep conversion remains only for an independently
+ // authored result that happens to be semantically equivalent.
return false;
}
}
return true;
}
+ FrozenNode freezePatchValue(BexValue value) {
+ // BEX exposes the exact FrozenNode only through BexFrozenWriter. Avoid
+ // invoking that writer for ordinary values because rc2's fallback factory
+ // itself performs Node round trips. A non-null frozen BlueId identifies the
+ // zero-materialization FrozenNode-backed lane.
+ if (BexValues.frozenBlueId(value) != null) {
+ FrozenNode frozen = BexFrozenWriter.toFrozen(value);
+ if (frozen.isStrictCanonical()) {
+ if (metrics != null) {
+ metrics.incrementBexPatchFrozenDirectConversions();
+ }
+ return frozen;
+ }
+ }
+ return materializePatchValue(value);
+ }
+
+ private FrozenNode materializePatchValue(BexValue value) {
+ long writerStart = System.nanoTime();
+ try {
+ // This is the one unavoidable rc2 boundary for newly computed values:
+ // take a mutable BEX rendering and immediately freeze it as authored
+ // canonical content. No mutable value crosses into Language.
+ return FrozenNode.fromNode(BexNodeWriter.toNode(value));
+ } finally {
+ if (metrics != null) {
+ metrics.addBexNodeWriterNanos(System.nanoTime() - writerStart);
+ metrics.incrementBexPatchNodeMaterializations();
+ }
+ }
+ }
+
private String textValue(BexValue value) {
if (value == null || value.isUndefined() || value.isNull()) {
return null;
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
index acc9b80..168be75 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
@@ -13,13 +13,14 @@
import blue.repo.coordination.Compute;
import blue.repo.coordination.SequentialWorkflowStep;
-public final class ComputeStepExecutor implements WorkflowStepExecutor {
+public final class ComputeStepExecutor implements WorkflowStepExecutor, AutoCloseable {
private final BexEngine bexEngine;
private final long defaultGasLimit;
private final ComputeDefinitionResolver definitionResolver;
private final BexWorkflowContextFactory contextFactory;
private final ComputeResultEmitter resultEmitter;
private final ComputeProgramNormalizer normalizer;
+ private final ComputeProgramPlanCache planCache;
private final BexProcessingMetrics metrics;
public ComputeStepExecutor() {
@@ -61,7 +62,11 @@ public ComputeStepExecutor(BexEngine bexEngine, long defaultGasLimit) {
this.definitionResolver = definitionResolver;
this.contextFactory = contextFactory;
this.resultEmitter = resultEmitter;
- this.normalizer = new ComputeProgramNormalizer();
+ this.normalizer = new ComputeProgramNormalizer(metrics);
+ this.planCache = new ComputeProgramPlanCache(
+ ComputeProgramPlanCache.DEFAULT_MAX_ENTRIES,
+ ComputeProgramPlanCache.DEFAULT_MAX_WEIGHT_BYTES,
+ metrics);
this.metrics = metrics;
}
@@ -77,35 +82,46 @@ public WorkflowStepResult execute(Compute step, StepExecutionContext context) {
if (metrics != null) {
metrics.incrementComputeStepsExecuted();
}
- Node rawStepNode = context.stepNodeRef();
+ FrozenNode rawStepNode = context.stepFrozenNode();
if (rawStepNode == null) {
- context.processorContext().throwFatal("Compute step must have a raw step node");
- return WorkflowStepResult.none();
+ Node mutableStepNode = context.stepNodeRef();
+ if (mutableStepNode == null) {
+ context.processorContext().throwFatal("Compute step must have a raw step node");
+ return WorkflowStepResult.none();
+ }
+ rawStepNode = FrozenNode.fromResolvedNode(mutableStepNode);
}
- FrozenNode programNode = FrozenNode.fromResolvedNode(normalizer.program(rawStepNode));
long resolveStart = System.nanoTime();
- FrozenNode definitionNode = definitionResolver.resolve(programNode, context);
- if (definitionNode != null) {
- definitionNode = FrozenNode.fromResolvedNode(normalizer.definition(definitionNode.toNode()));
- }
+ final FrozenNode resolvedDefinitionNode = definitionResolver.resolve(rawStepNode,
+ context,
+ metrics);
if (metrics != null) {
metrics.addComputeDefinitionResolveNanos(System.nanoTime() - resolveStart);
}
- String entry = FrozenNodeUtil.textProperty(programNode, "entry");
- long sourceStart = System.nanoTime();
- BexProgramSource source = definitionNode != null
- ? BexProgramSource.withDefinition(programNode, definitionNode, entry)
- : BexProgramSource.inline(programNode);
- if (metrics != null) {
- metrics.addComputeProgramSourceBuildNanos(System.nanoTime() - sourceStart);
- }
+ final FrozenNode exactRawStepNode = rawStepNode;
+ final String effectiveEntry = FrozenNodeUtil.textProperty(rawStepNode, "entry");
+ ComputeProgramPlanCache.Key planKey = ComputeProgramPlanCache.Key.from(
+ rawStepNode,
+ resolvedDefinitionNode,
+ effectiveEntry,
+ normalizer.normalizationVersion());
+ ComputeProgramPlanCache.Lookup lookup = planCache.lookup(planKey,
+ new ComputeProgramPlanCache.PlanFactory() {
+ @Override
+ public ComputeProgramPlan create() {
+ return buildPlan(exactRawStepNode,
+ resolvedDefinitionNode,
+ effectiveEntry);
+ }
+ });
+ ComputeProgramPlan computePlan = lookup.plan();
long contextStart = System.nanoTime();
- BexExecutionContext bexContext = contextFactory.create(context, computeGasLimit(programNode));
+ BexExecutionContext bexContext = contextFactory.create(context, computePlan.gasLimit());
if (metrics != null) {
metrics.addComputeContextBuildNanos(System.nanoTime() - contextStart);
}
long executeStart = System.nanoTime();
- BexExecutionResult result = bexEngine.compileAndExecute(source, bexContext);
+ BexExecutionResult result = bexEngine.compileAndExecute(computePlan.source(), bexContext);
if (metrics != null) {
metrics.addComputeCompileExecuteNanos(System.nanoTime() - executeStart);
metrics.addBexMetrics(result.metrics());
@@ -113,19 +129,24 @@ public WorkflowStepResult execute(Compute step, StepExecutionContext context) {
if (result.gasUsed() > 0L) {
context.processorContext().consumeGas(result.gasUsed());
}
- ComputeEffectPlan plan = resultEmitter.plan(result,
+ ComputeEffectPlan effectPlan = resultEmitter.plan(result,
context,
- FrozenNodeUtil.booleanProperty(programNode, "emitEvents", true));
- resultEmitter.buffer(plan, context);
- boolean returnResult = FrozenNodeUtil.booleanProperty(programNode, "returnResult", true);
- if (plan.terminationRequested()) {
- return returnResult
- ? WorkflowStepResult.terminalValue(result, plan.changesetHandled())
+ computePlan.emitEvents());
+ resultEmitter.buffer(effectPlan, context);
+ WorkflowStepResult stepResult;
+ if (effectPlan.terminationRequested()) {
+ stepResult = computePlan.returnResult()
+ ? WorkflowStepResult.terminalValue(result, effectPlan.changesetHandled())
: WorkflowStepResult.terminal();
+ } else {
+ stepResult = computePlan.returnResult()
+ ? WorkflowStepResult.value(result, effectPlan.changesetHandled())
+ : WorkflowStepResult.none();
}
- return returnResult
- ? WorkflowStepResult.value(result, plan.changesetHandled())
- : WorkflowStepResult.none();
+ // Failed compilation, execution, result validation, gas handling,
+ // or effect buffering never publishes a candidate plan.
+ planCache.publish(lookup);
+ return stepResult;
} catch (ComputeResultValidationException ex) {
if (metrics != null) {
metrics.incrementComputeResultValidationFailures();
@@ -158,4 +179,58 @@ private long computeGasLimit(FrozenNode stepNode) {
return parsed.longValue();
}
+ /** Clears reusable Compute plans while keeping this executor usable. */
+ public void clearPlanCache() {
+ planCache.clear();
+ }
+
+ @Override
+ public void close() {
+ planCache.close();
+ }
+
+ int cachedPlanCount() {
+ return planCache.size();
+ }
+
+ long cachedPlanWeightBytes() {
+ return planCache.weightBytes();
+ }
+
+ boolean isPlanCacheClosed() {
+ return planCache.isClosed();
+ }
+
+ private ComputeProgramPlan buildPlan(FrozenNode rawStepNode,
+ FrozenNode rawDefinitionNode,
+ String effectiveEntry) {
+ FrozenNode programNode = normalizer.program(rawStepNode);
+ FrozenNode definitionNode = rawDefinitionNode != null
+ ? normalizer.definition(rawDefinitionNode)
+ : null;
+ String normalizedEntry = FrozenNodeUtil.textProperty(programNode, "entry");
+ // The key is built from the authored effective entry. Retain the
+ // normalized value in the source to preserve the pre-cache behavior.
+ if (effectiveEntry == null ? normalizedEntry != null : !effectiveEntry.equals(normalizedEntry)) {
+ throw new BexException("Compute entry changed during normalization");
+ }
+ long sourceStart = System.nanoTime();
+ BexProgramSource source = definitionNode != null
+ ? BexProgramSource.withDefinition(programNode, definitionNode, normalizedEntry)
+ : BexProgramSource.inline(programNode);
+ if (metrics != null) {
+ metrics.incrementComputeProgramSourceBuilds();
+ metrics.addComputeProgramSourceBuildNanos(System.nanoTime() - sourceStart);
+ }
+ return new ComputeProgramPlan(programNode,
+ definitionNode,
+ source,
+ normalizedEntry,
+ computeGasLimit(programNode),
+ FrozenNodeUtil.booleanProperty(programNode, "emitEvents", true),
+ FrozenNodeUtil.booleanProperty(programNode, "returnResult", true),
+ rawStepNode,
+ rawDefinitionNode);
+ }
+
}
diff --git a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
new file mode 100644
index 0000000..0295de4
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
@@ -0,0 +1,283 @@
+package blue.coordination.processor.workflow;
+
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.snapshot.FrozenNode;
+import blue.repo.coordination.Compute;
+import blue.repo.coordination.SequentialWorkflowStep;
+import blue.repo.coordination.TerminateProcessing;
+import blue.repo.coordination.TriggerEvent;
+import blue.repo.coordination.UpdateDocument;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Immutable execution structure for one exact frozen workflow contract. */
+final class SequentialWorkflowPlan {
+ private static final long PLAN_BASE_BYTES = 128L;
+ private static final long STEP_PLAN_BYTES = 96L;
+
+ private final FrozenNode.ResolvedStructuralKey contractIdentity;
+ private final List steps;
+ private final long approximateWeightBytes;
+
+ private SequentialWorkflowPlan(FrozenNode contractNode, List steps) {
+ this.contractIdentity = contractNode != null ? contractNode.resolvedStructuralKey() : null;
+ this.steps = Collections.unmodifiableList(new ArrayList(steps));
+ this.approximateWeightBytes = estimateWeight(contractNode, this.steps);
+ }
+
+ static SequentialWorkflowPlan build(FrozenNode contractNode,
+ List workflowSteps,
+ List> executors,
+ BexProcessingMetrics metrics) {
+ List frozenSteps = stepNodes(contractNode);
+ int plannedStepCount = Math.max(workflowSteps.size(), frozenSteps.size());
+ List planned = new ArrayList(plannedStepCount);
+ for (int i = 0; i < plannedStepCount; i++) {
+ FrozenNode frozenStep = i < frozenSteps.size() ? frozenSteps.get(i) : null;
+ SequentialWorkflowStep workflowStep = i < workflowSteps.size() ? workflowSteps.get(i) : null;
+ planned.add(planStep(workflowStep, frozenStep, i, executors, metrics));
+ }
+ return new SequentialWorkflowPlan(contractNode, planned);
+ }
+
+ static StepPlan planStep(SequentialWorkflowStep step,
+ FrozenNode frozenStep,
+ int index,
+ List> executors,
+ BexProcessingMetrics metrics) {
+ WorkflowStepExecutor extends SequentialWorkflowStep> selected = null;
+ if (step != null) {
+ for (WorkflowStepExecutor extends SequentialWorkflowStep> executor : executors) {
+ if (metrics != null) {
+ metrics.incrementWorkflowExecutorLookups();
+ }
+ if (executor.supports(step)) {
+ selected = executor;
+ break;
+ }
+ }
+ }
+ StaticUpdatePlan staticUpdatePlan = null;
+ if (step instanceof UpdateDocument && frozenStep != null) {
+ StaticUpdatePlan candidate = StaticUpdatePlan.compile(
+ FrozenNodeUtil.property(frozenStep, "changeset"), metrics);
+ if (candidate.valid()) {
+ staticUpdatePlan = candidate;
+ if (metrics != null) {
+ metrics.incrementUpdateStaticTemplatesBuilt();
+ }
+ }
+ }
+ return new StepPlan(index,
+ stepKey(frozenStep, index),
+ stepName(step),
+ frozenStep,
+ step != null ? step.getClass() : null,
+ selected,
+ step instanceof TerminateProcessing,
+ staticUpdatePlan);
+ }
+
+ FrozenNode.ResolvedStructuralKey contractIdentity() {
+ return contractIdentity;
+ }
+
+ int stepCount() {
+ return steps.size();
+ }
+
+ StepPlan step(int index) {
+ return steps.get(index);
+ }
+
+ long approximateWeightBytes() {
+ return approximateWeightBytes;
+ }
+
+ private static List stepNodes(FrozenNode contractNode) {
+ if (contractNode == null || contractNode.getProperties() == null) {
+ return Collections.emptyList();
+ }
+ FrozenNode stepsNode = contractNode.getProperties().get("steps");
+ if (stepsNode == null || stepsNode.getItems() == null) {
+ return Collections.emptyList();
+ }
+ return stepsNode.getItems();
+ }
+
+ private static String stepKey(FrozenNode stepNode, int index) {
+ if (stepNode != null && stepNode.getName() != null && !stepNode.getName().trim().isEmpty()) {
+ return stepNode.getName().trim();
+ }
+ return "Step" + (index + 1);
+ }
+
+ private static String stepName(SequentialWorkflowStep step) {
+ if (step == null) {
+ return "null sequential workflow step";
+ }
+ if (step instanceof TriggerEvent) {
+ return "Coordination/Trigger Event";
+ }
+ if (step instanceof Compute) {
+ return "Coordination/Compute";
+ }
+ if (step instanceof TerminateProcessing) {
+ return "Coordination/Terminate Processing";
+ }
+ return step.getClass().getName();
+ }
+
+ private static long estimateWeight(FrozenNode contractNode, List steps) {
+ WeightEstimator identityEstimator = new WeightEstimator();
+ WeightEstimator retainedStepEstimator = new WeightEstimator();
+ long weight = PLAN_BASE_BYTES;
+ // The exact structural key retains representation data comparable to
+ // one traversal of the frozen graph.
+ weight = saturatedAdd(weight, identityEstimator.node(contractNode));
+ for (StepPlan step : steps) {
+ weight = saturatedAdd(weight, STEP_PLAN_BYTES);
+ weight = saturatedAdd(weight, retainedStepEstimator.string(step.key));
+ weight = saturatedAdd(weight, retainedStepEstimator.string(step.kind));
+ weight = saturatedAdd(weight, retainedStepEstimator.node(step.frozenStep));
+ if (step.staticUpdatePlan != null) {
+ weight = saturatedAdd(weight, step.staticUpdatePlan.approximateWeightBytes());
+ }
+ }
+ return Math.max(1L, weight);
+ }
+
+ private static long saturatedAdd(long left, long right) {
+ if (right > 0L && left > Long.MAX_VALUE - right) {
+ return Long.MAX_VALUE;
+ }
+ return left + right;
+ }
+
+ static final class StepPlan {
+ private final int index;
+ private final String key;
+ private final String kind;
+ private final FrozenNode frozenStep;
+ private final Class> runtimeStepClass;
+ private final WorkflowStepExecutor extends SequentialWorkflowStep> executor;
+ private final boolean declarativelyTerminal;
+ private final StaticUpdatePlan staticUpdatePlan;
+
+ private StepPlan(int index,
+ String key,
+ String kind,
+ FrozenNode frozenStep,
+ Class> runtimeStepClass,
+ WorkflowStepExecutor extends SequentialWorkflowStep> executor,
+ boolean declarativelyTerminal,
+ StaticUpdatePlan staticUpdatePlan) {
+ this.index = index;
+ this.key = key;
+ this.kind = kind;
+ this.frozenStep = frozenStep;
+ this.runtimeStepClass = runtimeStepClass;
+ this.executor = executor;
+ this.declarativelyTerminal = declarativelyTerminal;
+ this.staticUpdatePlan = staticUpdatePlan;
+ }
+
+ int index() {
+ return index;
+ }
+
+ String key() {
+ return key;
+ }
+
+ String kind() {
+ return kind;
+ }
+
+ FrozenNode frozenStep() {
+ return frozenStep;
+ }
+
+ WorkflowStepExecutor extends SequentialWorkflowStep> executor() {
+ return executor;
+ }
+
+ boolean matches(SequentialWorkflowStep step) {
+ return step == null ? runtimeStepClass == null : step.getClass() == runtimeStepClass;
+ }
+
+ boolean declarativelyTerminal() {
+ return declarativelyTerminal;
+ }
+
+ StaticUpdatePlan staticUpdatePlan() {
+ return staticUpdatePlan;
+ }
+ }
+
+ private static final class WeightEstimator {
+ private final IdentityHashMap visited = new IdentityHashMap();
+
+ private long node(FrozenNode node) {
+ if (node == null || visited.put(node, Boolean.TRUE) != null) {
+ return 0L;
+ }
+ long weight = 160L;
+ weight = saturatedAdd(weight, string(node.getName()));
+ weight = saturatedAdd(weight, string(node.getDescription()));
+ weight = saturatedAdd(weight, string(node.getReferenceBlueId()));
+ weight = saturatedAdd(weight, string(node.getMergePolicy()));
+ weight = saturatedAdd(weight, string(node.getPreviousBlueId()));
+ weight = saturatedAdd(weight, scalar(node.getValue()));
+ weight = saturatedAdd(weight, node(node.getType()));
+ weight = saturatedAdd(weight, node(node.getItemType()));
+ weight = saturatedAdd(weight, node(node.getKeyType()));
+ weight = saturatedAdd(weight, node(node.getValueType()));
+ weight = saturatedAdd(weight, node(node.getContracts()));
+ weight = saturatedAdd(weight, node(node.getBlue()));
+ if (node.getSchema() != null) {
+ weight = saturatedAdd(weight, 256L);
+ }
+ if (node.getItems() != null) {
+ weight = saturatedAdd(weight, 24L + 8L * node.getItems().size());
+ for (FrozenNode item : node.getItems()) {
+ weight = saturatedAdd(weight, node(item));
+ }
+ }
+ if (node.getProperties() != null) {
+ weight = saturatedAdd(weight, 48L + 48L * node.getProperties().size());
+ for (Map.Entry entry : node.getProperties().entrySet()) {
+ weight = saturatedAdd(weight, string(entry.getKey()));
+ weight = saturatedAdd(weight, node(entry.getValue()));
+ }
+ }
+ return weight;
+ }
+
+ private long string(String value) {
+ return value == null ? 0L : 40L + 2L * value.length();
+ }
+
+ private long scalar(Object value) {
+ if (value == null) {
+ return 0L;
+ }
+ if (value instanceof String) {
+ return string((String) value);
+ }
+ if (value instanceof BigInteger) {
+ return 48L + ((BigInteger) value).bitLength() / 8L;
+ }
+ if (value instanceof BigDecimal) {
+ return 64L;
+ }
+ return 24L;
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlanCache.java b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlanCache.java
new file mode 100644
index 0000000..01c641f
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlanCache.java
@@ -0,0 +1,149 @@
+package blue.coordination.processor.workflow;
+
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Per-runner, entry- and weight-bounded LRU for immutable workflow plans. */
+final class SequentialWorkflowPlanCache implements AutoCloseable {
+ static final int DEFAULT_MAX_ENTRIES = 512;
+ static final long DEFAULT_MAX_WEIGHT_BYTES = 32L * 1024L * 1024L;
+ private static final long CACHE_ENTRY_BYTES = 64L;
+
+ private final int maxEntries;
+ private final long maxWeightBytes;
+ private final BexProcessingMetrics metrics;
+ private final LinkedHashMap entries =
+ new LinkedHashMap(16, 0.75f, true);
+ private long weightBytes;
+ private boolean closed;
+
+ SequentialWorkflowPlanCache(BexProcessingMetrics metrics) {
+ this(DEFAULT_MAX_ENTRIES, DEFAULT_MAX_WEIGHT_BYTES, metrics);
+ }
+
+ SequentialWorkflowPlanCache(int maxEntries,
+ long maxWeightBytes,
+ BexProcessingMetrics metrics) {
+ if (maxEntries <= 0) {
+ throw new IllegalArgumentException("maxEntries must be positive");
+ }
+ if (maxWeightBytes <= 0L) {
+ throw new IllegalArgumentException("maxWeightBytes must be positive");
+ }
+ this.maxEntries = maxEntries;
+ this.maxWeightBytes = maxWeightBytes;
+ this.metrics = metrics;
+ }
+
+ synchronized SequentialWorkflowPlan getOrBuild(FrozenNode.ResolvedStructuralKey identity,
+ PlanFactory factory) {
+ if (identity == null) {
+ throw new IllegalArgumentException("identity must not be null");
+ }
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+ CacheEntry cached = entries.get(identity);
+ if (cached != null) {
+ if (metrics != null) {
+ metrics.incrementWorkflowPlanCacheHits();
+ }
+ return cached.plan;
+ }
+ if (metrics != null) {
+ metrics.incrementWorkflowPlanCacheMisses();
+ }
+ SequentialWorkflowPlan plan = factory.build();
+ if (plan == null) {
+ throw new IllegalStateException("workflow plan factory returned null");
+ }
+ if (!identity.equals(plan.contractIdentity())) {
+ throw new IllegalStateException("workflow plan identity does not match cache key");
+ }
+ if (metrics != null) {
+ metrics.incrementWorkflowPlansBuilt();
+ }
+ long entryWeight = entryWeight(plan);
+ if (closed || entryWeight > maxWeightBytes) {
+ return plan;
+ }
+ entries.put(identity, new CacheEntry(plan, entryWeight));
+ adjustWeight(entryWeight);
+ evictToBounds();
+ return plan;
+ }
+
+ synchronized int size() {
+ return entries.size();
+ }
+
+ synchronized long weightBytes() {
+ return weightBytes;
+ }
+
+ synchronized boolean isClosed() {
+ return closed;
+ }
+
+ synchronized void clear() {
+ if (entries.isEmpty()) {
+ return;
+ }
+ entries.clear();
+ long removed = weightBytes;
+ weightBytes = 0L;
+ if (metrics != null && removed != 0L) {
+ metrics.addWorkflowPlanWeightBytes(-removed);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ closed = true;
+ clear();
+ }
+
+ private void evictToBounds() {
+ Iterator> iterator = entries.entrySet().iterator();
+ while ((entries.size() > maxEntries || weightBytes > maxWeightBytes) && iterator.hasNext()) {
+ CacheEntry eldest = iterator.next().getValue();
+ iterator.remove();
+ adjustWeight(-eldest.weightBytes);
+ if (metrics != null) {
+ metrics.incrementWorkflowPlanCacheEvictions();
+ }
+ }
+ }
+
+ private long entryWeight(SequentialWorkflowPlan plan) {
+ long planWeight = plan.approximateWeightBytes();
+ return planWeight > Long.MAX_VALUE - CACHE_ENTRY_BYTES
+ ? Long.MAX_VALUE
+ : planWeight + CACHE_ENTRY_BYTES;
+ }
+
+ private void adjustWeight(long delta) {
+ weightBytes += delta;
+ if (metrics != null) {
+ metrics.addWorkflowPlanWeightBytes(delta);
+ }
+ }
+
+ interface PlanFactory {
+ SequentialWorkflowPlan build();
+ }
+
+ private static final class CacheEntry {
+ private final SequentialWorkflowPlan plan;
+ private final long weightBytes;
+
+ private CacheEntry(SequentialWorkflowPlan plan, long weightBytes) {
+ this.plan = plan;
+ this.weightBytes = weightBytes;
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
index 472063c..510259a 100644
--- a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
+++ b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
@@ -14,15 +14,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-public final class SequentialWorkflowRunner {
+public final class SequentialWorkflowRunner implements AutoCloseable {
private final List> executors;
private final BexProcessingMetrics metrics;
+ private final SequentialWorkflowPlanCache planCache;
public SequentialWorkflowRunner() {
this(defaultExecutors());
@@ -34,11 +31,24 @@ public SequentialWorkflowRunner(List> executors,
BexProcessingMetrics metrics) {
+ this(executors,
+ metrics,
+ SequentialWorkflowPlanCache.DEFAULT_MAX_ENTRIES,
+ SequentialWorkflowPlanCache.DEFAULT_MAX_WEIGHT_BYTES);
+ }
+
+ SequentialWorkflowRunner(List> executors,
+ BexProcessingMetrics metrics,
+ int planCacheMaxEntries,
+ long planCacheMaxWeightBytes) {
if (executors == null) {
throw new IllegalArgumentException("executors must not be null");
}
this.executors = Collections.unmodifiableList(new ArrayList>(executors));
this.metrics = metrics;
+ this.planCache = new SequentialWorkflowPlanCache(planCacheMaxEntries,
+ planCacheMaxWeightBytes,
+ metrics);
}
public void execute(SequentialWorkflow workflow, ProcessorExecutionContext context) {
@@ -47,36 +57,43 @@ public void execute(SequentialWorkflow workflow, ProcessorExecutionContext conte
if (workflow.getSteps() == null) {
return;
}
- Map stepResults = new LinkedHashMap();
- Set handledChangesetSteps = new LinkedHashSet();
FrozenNode contractNode = rawContractNode(context);
- List stepNodes = stepNodes(contractNode);
List steps = workflow.getSteps();
- WorkingDocument workingDocument = rootWorkingDocument(context);
- for (int i = 0; i < steps.size(); i++) {
- SequentialWorkflowStep step = steps.get(i);
- FrozenNode stepNode = i < stepNodes.size() ? stepNodes.get(i) : null;
- if (metrics != null) {
- metrics.incrementWorkflowStepsExecuted();
- }
- WorkflowStepResult result = executeStep(workflow,
- step,
- stepNode,
- contractNode,
- i,
- stepResults,
- handledChangesetSteps,
- context,
- workingDocument);
- if (result != null && result.hasValue()) {
- String key = stepKey(stepNode, i);
- stepResults.put(key, result.value());
- if (result.changesetHandled()) {
- handledChangesetSteps.add(key);
+ SequentialWorkflowPlan plan = workflowPlan(contractNode, steps);
+ WorkflowExecutionState executionState = new WorkflowExecutionState();
+ try (WorkingDocument workingDocument = rootWorkingDocument(context)) {
+ for (int i = 0; i < steps.size(); i++) {
+ SequentialWorkflowStep step = steps.get(i);
+ SequentialWorkflowPlan.StepPlan stepPlan = i < plan.stepCount()
+ ? plan.step(i)
+ : SequentialWorkflowPlan.planStep(step,
+ null,
+ i,
+ executors,
+ metrics);
+ if (!stepPlan.matches(step)) {
+ stepPlan = SequentialWorkflowPlan.planStep(step,
+ stepPlan.frozenStep(),
+ i,
+ executors,
+ metrics);
+ }
+ if (metrics != null) {
+ metrics.incrementWorkflowStepsExecuted();
+ }
+ WorkflowStepResult result = executeStep(workflow,
+ step,
+ stepPlan,
+ contractNode,
+ executionState,
+ context,
+ workingDocument);
+ if (result != null && result.hasValue()) {
+ executionState.record(stepPlan.key(), result.value(), result.changesetHandled());
+ }
+ if (result != null && result.isTerminal()) {
+ break;
}
- }
- if (result != null && result.isTerminal()) {
- break;
}
}
} finally {
@@ -88,33 +105,34 @@ public void execute(SequentialWorkflow workflow, ProcessorExecutionContext conte
private WorkflowStepResult executeStep(SequentialWorkflow workflow,
SequentialWorkflowStep step,
- FrozenNode stepNode,
+ SequentialWorkflowPlan.StepPlan stepPlan,
FrozenNode contractNode,
- int stepIndex,
- Map stepResults,
- Set handledChangesetSteps,
+ WorkflowExecutionState executionState,
ProcessorExecutionContext context,
WorkingDocument workingDocument) {
if (step == null) {
context.throwFatal("Unsupported null sequential workflow step");
return WorkflowStepResult.none();
}
- for (WorkflowStepExecutor extends SequentialWorkflowStep> executor : executors) {
- if (executor.supports(step)) {
- StepExecutionContext stepContext = new StepExecutionContext(context,
- workflow,
- step,
- stepNode,
- contractNode,
- stepIndex,
- stepResults,
- handledChangesetSteps,
- workingDocument);
- return executeSupported(executor, step, stepContext);
- }
+ WorkflowStepExecutor extends SequentialWorkflowStep> executor = stepPlan.executor();
+ if (executor == null) {
+ context.throwFatal("Unsupported sequential workflow step: " + stepPlan.kind());
+ return WorkflowStepResult.none();
+ }
+ WorkflowExecutionState.Snapshot stateView = executionState.snapshotView();
+ if (metrics != null) {
+ metrics.incrementWorkflowStepResultViewHits();
}
- context.throwFatal("Unsupported sequential workflow step: " + stepName(step));
- return WorkflowStepResult.none();
+ StepExecutionContext stepContext = new StepExecutionContext(context,
+ workflow,
+ step,
+ stepPlan.frozenStep(),
+ contractNode,
+ stepPlan.index(),
+ stateView,
+ stepPlan.staticUpdatePlan(),
+ workingDocument);
+ return executeSupported(executor, step, stepContext);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -124,19 +142,6 @@ private WorkflowStepResult executeSupported(WorkflowStepExecutor executor,
return executor.execute(step, context);
}
- private String stepName(SequentialWorkflowStep step) {
- if (step instanceof TriggerEvent) {
- return "Coordination/Trigger Event";
- }
- if (step instanceof Compute) {
- return "Coordination/Compute";
- }
- if (step instanceof TerminateProcessing) {
- return "Coordination/Terminate Processing";
- }
- return step.getClass().getName();
- }
-
private static List> defaultExecutors() {
return executorsFor(BexEngine.builder().build(), 100_000L);
}
@@ -186,22 +191,55 @@ private static List> exec
new UpdateDocumentStepExecutor(metrics));
}
- private List stepNodes(FrozenNode contractNode) {
- if (contractNode == null || contractNode.getProperties() == null) {
- return Collections.emptyList();
+ private SequentialWorkflowPlan workflowPlan(final FrozenNode contractNode,
+ final List steps) {
+ if (contractNode == null) {
+ if (metrics != null) {
+ metrics.incrementWorkflowPlanCacheMisses();
+ }
+ SequentialWorkflowPlan plan = SequentialWorkflowPlan.build(null, steps, executors, metrics);
+ if (metrics != null) {
+ metrics.incrementWorkflowPlansBuilt();
+ }
+ return plan;
}
- FrozenNode steps = contractNode.getProperties().get("steps");
- if (steps == null || steps.getItems() == null) {
- return Collections.emptyList();
+ return planCache.getOrBuild(contractNode.resolvedStructuralKey(),
+ new SequentialWorkflowPlanCache.PlanFactory() {
+ @Override
+ public SequentialWorkflowPlan build() {
+ return SequentialWorkflowPlan.build(contractNode, steps, executors, metrics);
+ }
+ });
+ }
+
+ /** Clears all immutable workflow plans retained by this runner. */
+ public void clearCaches() {
+ planCache.clear();
+ for (WorkflowStepExecutor extends SequentialWorkflowStep> executor : executors) {
+ if (executor instanceof ComputeStepExecutor) {
+ ((ComputeStepExecutor) executor).clearPlanCache();
+ }
}
- return steps.getItems();
}
- private String stepKey(FrozenNode stepNode, int index) {
- if (stepNode != null && stepNode.getName() != null && !stepNode.getName().trim().isEmpty()) {
- return stepNode.getName().trim();
+ /** Current number of retained workflow plans. */
+ public int workflowPlanCacheSize() {
+ return planCache.size();
+ }
+
+ /** Current approximate retained workflow-plan weight. */
+ public long workflowPlanCacheWeightBytes() {
+ return planCache.weightBytes();
+ }
+
+ @Override
+ public void close() {
+ planCache.close();
+ for (WorkflowStepExecutor extends SequentialWorkflowStep> executor : executors) {
+ if (executor instanceof ComputeStepExecutor) {
+ ((ComputeStepExecutor) executor).close();
+ }
}
- return "Step" + (index + 1);
}
private FrozenNode rawContractNode(ProcessorExecutionContext context) {
diff --git a/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java b/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
index c9c9989..31bad7d 100644
--- a/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
+++ b/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
@@ -17,7 +17,7 @@ static boolean rejectBexOperators(FrozenNode node, StepExecutionContext context,
return true;
}
- private static String firstBexOperatorPath(FrozenNode node, String path) {
+ static String firstBexOperatorPath(FrozenNode node, String path) {
if (node == null) {
return null;
}
diff --git a/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java b/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
new file mode 100644
index 0000000..fe3a85d
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
@@ -0,0 +1,231 @@
+package blue.coordination.processor.workflow;
+
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.processor.model.FrozenJsonPatch;
+import blue.language.processor.model.JsonPatch;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable, scope-independent form of one literal Update Document changeset.
+ *
+ * Authored pointers are resolved only when the step executes. Patch values
+ * remain frozen through the Language boundary. Resolved-construction fallback
+ * inputs are normalized once while the plan is compiled, never once per
+ * execution.
+ */
+final class StaticUpdatePlan {
+ private final List patches;
+ private final String validationFailure;
+ private final long approximateWeightBytes;
+
+ private StaticUpdatePlan(List patches,
+ String validationFailure,
+ long approximateWeightBytes) {
+ this.patches = Collections.unmodifiableList(new ArrayList(patches));
+ this.validationFailure = validationFailure;
+ this.approximateWeightBytes = approximateWeightBytes;
+ }
+
+ static StaticUpdatePlan compile(FrozenNode changeset) {
+ return compile(changeset, null);
+ }
+
+ static StaticUpdatePlan compile(FrozenNode changeset, BexProcessingMetrics metrics) {
+ String bexPath = StaticPayloadValidator.firstBexOperatorPath(changeset, "");
+ if (bexPath != null) {
+ return invalid("Update Document changeset must be static; BEX operator object is not allowed at "
+ + bexPath);
+ }
+ if (changeset == null || changeset.getItems() == null) {
+ return invalid("Update Document changeset must be a static patch list");
+ }
+ List templates = new ArrayList(changeset.getItems().size());
+ long weight = 96L;
+ for (int index = 0; index < changeset.getItems().size(); index++) {
+ FrozenNode item = changeset.getItems().get(index);
+ Map properties = item != null ? item.getProperties() : null;
+ if (properties == null) {
+ return invalid("Update Document changeset entry " + index
+ + " must be a static patch object");
+ }
+ ScalarText op = scalarText(properties.get("op"));
+ if (op.invalidType) {
+ return invalid("Update Document changeset entry " + index
+ + " field 'op' must be text");
+ }
+ ScalarText path = scalarText(properties.get("path"));
+ if (path.invalidType) {
+ return invalid("Update Document changeset entry " + index
+ + " field 'path' must be text");
+ }
+ if (op.value == null || op.value.trim().isEmpty()) {
+ return invalid("Update Document patch operation is required");
+ }
+ if (path.value == null || path.value.trim().isEmpty()) {
+ return invalid("Update Document patch path is required");
+ }
+ String normalizedOp = op.value.trim().toLowerCase(java.util.Locale.ROOT);
+ JsonPatch.Op patchOp;
+ if ("add".equals(normalizedOp)) {
+ patchOp = JsonPatch.Op.ADD;
+ } else if ("replace".equals(normalizedOp)) {
+ patchOp = JsonPatch.Op.REPLACE;
+ } else if ("remove".equals(normalizedOp)) {
+ patchOp = JsonPatch.Op.REMOVE;
+ } else {
+ return invalid("Unsupported Update Document patch operation: " + op.value);
+ }
+ FrozenNode value = null;
+ if (patchOp != JsonPatch.Op.REMOVE) {
+ value = properties.get("val");
+ if (value == null) {
+ return invalid("Update Document patch value is required for operation: "
+ + op.value);
+ }
+ if (!value.isStrictCanonical()) {
+ // Compatibility-only boundary for callers that compiled the
+ // workflow graph in resolved construction mode. Preserve the
+ // exact visible authored shape and pay this conversion once.
+ value = FrozenNode.fromNode(value.toNode());
+ if (metrics != null) {
+ metrics.addMetric("staticUpdateResolvedValueCanonicalizations", 1L);
+ }
+ }
+ }
+ templates.add(new PatchTemplate(patchOp, path.value, value));
+ weight += 72L + stringWeight(path.value) + frozenWeight(value);
+ }
+ return new StaticUpdatePlan(templates, null, weight);
+ }
+
+ private static StaticUpdatePlan invalid(String message) {
+ return new StaticUpdatePlan(Collections.emptyList(), message, 64L + stringWeight(message));
+ }
+
+ boolean valid() {
+ return validationFailure == null;
+ }
+
+ String validationFailure() {
+ return validationFailure;
+ }
+
+ List patches() {
+ return patches;
+ }
+
+ long approximateWeightBytes() {
+ return approximateWeightBytes;
+ }
+
+ private static ScalarText scalarText(FrozenNode node) {
+ if (node == null) {
+ return ScalarText.absent();
+ }
+ Object value = node.getValue();
+ if (value == null) {
+ return ScalarText.absent();
+ }
+ return value instanceof String
+ ? ScalarText.value((String) value)
+ : ScalarText.invalid();
+ }
+
+ private static long stringWeight(String value) {
+ return value != null ? 40L + (long) value.length() * 2L : 0L;
+ }
+
+ private static long frozenWeight(FrozenNode node) {
+ return frozenWeight(node, new IdentityHashMap());
+ }
+
+ private static long frozenWeight(FrozenNode node,
+ IdentityHashMap visited) {
+ if (node == null || visited.put(node, Boolean.TRUE) != null) {
+ return 0L;
+ }
+ long weight = 96L
+ + stringWeight(node.getName())
+ + stringWeight(node.getDescription())
+ + stringWeight(node.getReferenceBlueId());
+ if (node.getValue() instanceof String) {
+ weight += stringWeight((String) node.getValue());
+ } else if (node.getValue() != null) {
+ weight += 32L;
+ }
+ if (node.getItems() != null) {
+ weight += 16L + (long) node.getItems().size() * 8L;
+ for (FrozenNode item : node.getItems()) {
+ weight += frozenWeight(item, visited);
+ }
+ }
+ if (node.getProperties() != null) {
+ weight += 32L + (long) node.getProperties().size() * 40L;
+ for (Map.Entry entry : node.getProperties().entrySet()) {
+ weight += stringWeight(entry.getKey()) + frozenWeight(entry.getValue(), visited);
+ }
+ }
+ weight += frozenWeight(node.getType(), visited);
+ weight += frozenWeight(node.getItemType(), visited);
+ weight += frozenWeight(node.getKeyType(), visited);
+ weight += frozenWeight(node.getValueType(), visited);
+ return weight;
+ }
+
+ static final class PatchTemplate {
+ private final JsonPatch.Op op;
+ private final String authoredPath;
+ private final FrozenNode value;
+
+ private PatchTemplate(JsonPatch.Op op, String authoredPath, FrozenNode value) {
+ this.op = op;
+ this.authoredPath = authoredPath;
+ this.value = value;
+ }
+
+ FrozenJsonPatch bind(String absolutePath) {
+ if (op == JsonPatch.Op.ADD) {
+ return FrozenJsonPatch.add(absolutePath, value);
+ }
+ if (op == JsonPatch.Op.REPLACE) {
+ return FrozenJsonPatch.replace(absolutePath, value);
+ }
+ return FrozenJsonPatch.remove(absolutePath);
+ }
+
+ String authoredPath() {
+ return authoredPath;
+ }
+ }
+
+ private static final class ScalarText {
+ private static final ScalarText ABSENT = new ScalarText(null, false);
+ private static final ScalarText INVALID = new ScalarText(null, true);
+
+ private final String value;
+ private final boolean invalidType;
+
+ private ScalarText(String value, boolean invalidType) {
+ this.value = value;
+ this.invalidType = invalidType;
+ }
+
+ private static ScalarText absent() {
+ return ABSENT;
+ }
+
+ private static ScalarText invalid() {
+ return INVALID;
+ }
+
+ private static ScalarText value(String value) {
+ return new ScalarText(value, false);
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/StepExecutionContext.java b/src/main/java/blue/coordination/processor/workflow/StepExecutionContext.java
index ec036eb..4898624 100644
--- a/src/main/java/blue/coordination/processor/workflow/StepExecutionContext.java
+++ b/src/main/java/blue/coordination/processor/workflow/StepExecutionContext.java
@@ -3,13 +3,11 @@
import blue.language.model.Node;
import blue.language.processor.ProcessorExecutionContext;
import blue.language.processor.WorkingDocument;
+import blue.language.processor.model.FrozenJsonPatch;
import blue.language.processor.model.JsonPatch;
import blue.language.snapshot.FrozenNode;
import blue.repo.coordination.SequentialWorkflow;
import blue.repo.coordination.SequentialWorkflowStep;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -23,8 +21,8 @@ public final class StepExecutionContext {
private final FrozenNode stepFrozenNode;
private final FrozenNode currentContractFrozenNode;
private final int stepIndex;
- private final Map stepResults;
- private final Set handledChangesetSteps;
+ private final WorkflowExecutionState.Snapshot workflowStateView;
+ private final StaticUpdatePlan staticUpdatePlan;
private final Node eventRef;
private WorkingDocument workingDocument;
@@ -111,6 +109,51 @@ public StepExecutionContext(ProcessorExecutionContext processorContext,
workingDocument);
}
+ StepExecutionContext(ProcessorExecutionContext processorContext,
+ SequentialWorkflow workflow,
+ SequentialWorkflowStep step,
+ FrozenNode stepFrozenNode,
+ FrozenNode currentContractFrozenNode,
+ int stepIndex,
+ WorkflowExecutionState.Snapshot workflowStateView,
+ WorkingDocument workingDocument) {
+ this(processorContext,
+ workflow,
+ step,
+ null,
+ null,
+ stepFrozenNode,
+ currentContractFrozenNode,
+ stepIndex,
+ workflowStateView,
+ null,
+ true,
+ workingDocument);
+ }
+
+ StepExecutionContext(ProcessorExecutionContext processorContext,
+ SequentialWorkflow workflow,
+ SequentialWorkflowStep step,
+ FrozenNode stepFrozenNode,
+ FrozenNode currentContractFrozenNode,
+ int stepIndex,
+ WorkflowExecutionState.Snapshot workflowStateView,
+ StaticUpdatePlan staticUpdatePlan,
+ WorkingDocument workingDocument) {
+ this(processorContext,
+ workflow,
+ step,
+ null,
+ null,
+ stepFrozenNode,
+ currentContractFrozenNode,
+ stepIndex,
+ workflowStateView,
+ staticUpdatePlan,
+ true,
+ workingDocument);
+ }
+
private StepExecutionContext(ProcessorExecutionContext processorContext,
SequentialWorkflow workflow,
SequentialWorkflowStep step,
@@ -122,6 +165,32 @@ private StepExecutionContext(ProcessorExecutionContext processorContext,
Map stepResults,
Set handledChangesetSteps,
WorkingDocument workingDocument) {
+ this(processorContext,
+ workflow,
+ step,
+ stepNode,
+ currentContractNode,
+ stepFrozenNode,
+ currentContractFrozenNode,
+ stepIndex,
+ WorkflowExecutionState.snapshotOf(stepResults, handledChangesetSteps),
+ null,
+ true,
+ workingDocument);
+ }
+
+ private StepExecutionContext(ProcessorExecutionContext processorContext,
+ SequentialWorkflow workflow,
+ SequentialWorkflowStep step,
+ Node stepNode,
+ Node currentContractNode,
+ FrozenNode stepFrozenNode,
+ FrozenNode currentContractFrozenNode,
+ int stepIndex,
+ WorkflowExecutionState.Snapshot workflowStateView,
+ StaticUpdatePlan staticUpdatePlan,
+ boolean useSnapshotView,
+ WorkingDocument workingDocument) {
if (processorContext == null) {
throw new IllegalArgumentException("processorContext must not be null");
}
@@ -136,10 +205,10 @@ private StepExecutionContext(ProcessorExecutionContext processorContext,
this.stepFrozenNode = stepFrozenNode;
this.currentContractFrozenNode = currentContractFrozenNode;
this.stepIndex = stepIndex;
- this.stepResults = Collections.unmodifiableMap(new LinkedHashMap(
- stepResults != null ? stepResults : Collections.emptyMap()));
- this.handledChangesetSteps = Collections.unmodifiableSet(new LinkedHashSet(
- handledChangesetSteps != null ? handledChangesetSteps : Collections.emptySet()));
+ this.workflowStateView = workflowStateView != null
+ ? workflowStateView
+ : new WorkflowExecutionState().snapshotView();
+ this.staticUpdatePlan = staticUpdatePlan;
this.eventRef = processorContext.event();
this.workingDocument = workingDocument;
}
@@ -197,11 +266,15 @@ public int stepIndex() {
}
public Map stepResults() {
- return stepResults;
+ return workflowStateView.results();
}
boolean wasChangesetHandled(String stepKey) {
- return stepKey != null && handledChangesetSteps.contains(stepKey);
+ return workflowStateView.wasChangesetHandled(stepKey);
+ }
+
+ StaticUpdatePlan staticUpdatePlan() {
+ return staticUpdatePlan;
}
public Node event() {
@@ -245,4 +318,16 @@ WorkingDocument.Preview advanceWorkingDocument(List patches) {
return null;
}
}
+
+ WorkingDocument.Preview advanceWorkingDocumentFrozen(List patches) {
+ if (patches == null || patches.isEmpty()) {
+ return null;
+ }
+ try {
+ return workingDocument().previewAndApplyFrozenPatches(patches);
+ } catch (RuntimeException ex) {
+ processorContext.throwFatal("Working document preview failed: " + ex.getMessage());
+ return null;
+ }
+ }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
index 3662e68..976923a 100644
--- a/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
@@ -3,7 +3,7 @@
import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.model.Node;
import blue.language.processor.WorkingDocument;
-import blue.language.processor.model.JsonPatch;
+import blue.language.processor.model.FrozenJsonPatch;
import blue.language.snapshot.FrozenNode;
import blue.language.utils.MergeReverser;
import blue.repo.coordination.SequentialWorkflowStep;
@@ -11,6 +11,7 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
public final class UpdateDocumentStepExecutor implements WorkflowStepExecutor {
private final BexProcessingMetrics metrics;
@@ -35,6 +36,14 @@ public WorkflowStepResult execute(UpdateDocument step, StepExecutionContext cont
if (metrics != null) {
metrics.incrementUpdateDocumentStepsExecuted();
}
+ StaticUpdatePlan staticPlan = context.staticUpdatePlan();
+ if (staticPlan != null) {
+ if (metrics != null) {
+ metrics.incrementUpdateStaticTemplateHits();
+ }
+ applyStaticPlan(staticPlan, context);
+ return WorkflowStepResult.none();
+ }
FrozenNode rawFrozenChangeset = FrozenNodeUtil.property(context.stepFrozenNode(), "changeset");
if (StaticPayloadValidator.rejectBexOperators(rawFrozenChangeset,
context,
@@ -52,7 +61,7 @@ public WorkflowStepResult execute(UpdateDocument step, StepExecutionContext cont
return WorkflowStepResult.none();
}
long conversionStart = System.nanoTime();
- List patches = new ArrayList(changeset.size());
+ List patches = new ArrayList(changeset.size());
for (WorkflowPatchEntry entry : changeset) {
patches.add(toPatch(entry, context));
}
@@ -106,10 +115,24 @@ private WorkflowPatchEntry literalPatchEntry(Object item, int index, StepExecuti
return literalPatchEntry((Node) item, index, context);
}
try {
+ if (metrics != null) {
+ metrics.incrementUpdateReflectionFallbacks();
+ }
String op = (String) invokeNoArg(item, "getOp");
String path = (String) invokeNoArg(item, "getPath");
- Node val = (Node) invokeNoArg(item, "getVal");
- return new WorkflowPatchEntry(op, path, val);
+ if (isRemove(op)) {
+ return new WorkflowPatchEntry(op, path, (FrozenNode) null);
+ }
+ Object val = invokeNoArg(item, "getVal");
+ if (val == null || val instanceof Node) {
+ return new WorkflowPatchEntry(op, path, (Node) val);
+ }
+ if (val instanceof FrozenNode) {
+ return new WorkflowPatchEntry(op, path, (FrozenNode) val);
+ }
+ context.processorContext().throwFatal("Update Document changeset entry " + index
+ + " field 'val' must be a node");
+ return null;
} catch (ReflectiveOperationException ex) {
context.processorContext().throwFatal("Update Document changeset entry " + index
+ " cannot be read as a patch entry: " + ex.getMessage());
@@ -129,8 +152,8 @@ private WorkflowPatchEntry literalPatchEntry(Node item, int index, StepExecution
}
String op = stringProperty(item, "op", index, context);
String path = stringProperty(item, "path", index, context);
- Node val = item.getProperties().get("val");
- return new WorkflowPatchEntry(op, path, val != null ? val.clone() : null);
+ Node val = isRemove(op) ? null : item.getProperties().get("val");
+ return new WorkflowPatchEntry(op, path, val);
}
private String stringProperty(Node item, String key, int index, StepExecutionContext context) {
@@ -152,7 +175,7 @@ private Object invokeNoArg(Object target, String methodName) throws ReflectiveOp
return method.invoke(target);
}
- private JsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext context) {
+ private FrozenJsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext context) {
if (entry == null) {
context.processorContext().throwFatal("Update Document changeset contains a null patch entry");
return null;
@@ -168,36 +191,56 @@ private JsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext context
return null;
}
String absolutePath = context.processorContext().resolvePointer(path);
- String normalizedOp = op.trim().toLowerCase();
+ String normalizedOp = op.trim().toLowerCase(Locale.ROOT);
if ("remove".equals(normalizedOp)) {
- return JsonPatch.remove(absolutePath);
+ return FrozenJsonPatch.remove(absolutePath);
}
- Node value = entry.val();
+ FrozenNode value = entry.val();
if (value == null) {
context.processorContext().throwFatal("Update Document patch value is required for operation: " + op);
return null;
}
if ("add".equals(normalizedOp)) {
- return JsonPatch.add(absolutePath, value);
+ return FrozenJsonPatch.add(absolutePath, value);
}
if ("replace".equals(normalizedOp)) {
- return JsonPatch.replace(absolutePath, value);
+ return FrozenJsonPatch.replace(absolutePath, value);
}
context.processorContext().throwFatal("Unsupported Update Document patch operation: " + op);
return null;
}
- private void applyPatches(List patches, StepExecutionContext context) {
+ private static boolean isRemove(String op) {
+ return op != null && "remove".equals(op.trim().toLowerCase(Locale.ROOT));
+ }
+
+ private void applyPatches(List patches, StepExecutionContext context) {
if (patches == null || patches.isEmpty()) {
return;
}
long applyStart = System.nanoTime();
boolean applied = false;
+ boolean previewTransferred = false;
+ WorkingDocument.Preview preview = null;
try {
- WorkingDocument.Preview preview = context.advanceWorkingDocument(patches);
- context.processorContext().applyPreviewedPatches(patches, preview);
+ preview = context.advanceWorkingDocumentFrozen(patches);
+ if (preview == null) {
+ return;
+ }
+ if (metrics != null) {
+ metrics.addMetric("frozenPatchValuesHandedToLanguage", valuePatchCount(patches));
+ }
+ context.processorContext().applyPreviewedFrozenPatches(patches, preview);
+ previewTransferred = true;
+ if (metrics != null) {
+ metrics.addMetric("frozenPatchesHandedToLanguage", patches.size());
+ metrics.addMetric("frozenPatchValuesHandedToLanguage", valuePatchCount(patches));
+ }
applied = true;
} finally {
+ if (!previewTransferred && preview != null) {
+ preview.close();
+ }
if (metrics != null) {
metrics.addUpdatePatchApplyNanos(System.nanoTime() - applyStart);
if (applied) {
@@ -207,4 +250,30 @@ private void applyPatches(List patches, StepExecutionContext context)
}
}
}
+
+ private static long valuePatchCount(List patches) {
+ long count = 0L;
+ for (FrozenJsonPatch patch : patches) {
+ if (patch != null && patch.getOp() != blue.language.processor.model.JsonPatch.Op.REMOVE) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private void applyStaticPlan(StaticUpdatePlan plan, StepExecutionContext context) {
+ if (plan.patches().isEmpty()) {
+ return;
+ }
+ long conversionStart = System.nanoTime();
+ List patches = new ArrayList(plan.patches().size());
+ for (StaticUpdatePlan.PatchTemplate template : plan.patches()) {
+ String absolutePath = context.processorContext().resolvePointer(template.authoredPath());
+ patches.add(template.bind(absolutePath));
+ }
+ if (metrics != null) {
+ metrics.addUpdatePatchConversionNanos(System.nanoTime() - conversionStart);
+ }
+ applyPatches(patches, context);
+ }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/WorkflowExecutionState.java b/src/main/java/blue/coordination/processor/workflow/WorkflowExecutionState.java
new file mode 100644
index 0000000..87bde74
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/workflow/WorkflowExecutionState.java
@@ -0,0 +1,209 @@
+package blue.coordination.processor.workflow;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * Per-workflow append-only result state.
+ *
+ * Each read-only view captures a revision rather than copying all preceding
+ * entries. This keeps workflow allocation linear while retaining the stable
+ * snapshot behavior previously provided by {@code StepExecutionContext}.
+ */
+final class WorkflowExecutionState {
+ private final List orderedSlots = new ArrayList();
+ private final Map slotsByKey = new HashMap();
+ private final Map firstHandledRevision = new HashMap();
+ private long revision;
+
+ WorkflowExecutionState() {
+ }
+
+ static Snapshot snapshotOf(Map results, Set handledChangesets) {
+ WorkflowExecutionState state = new WorkflowExecutionState();
+ if (results != null) {
+ for (Map.Entry entry : results.entrySet()) {
+ state.record(entry.getKey(), entry.getValue(), false);
+ }
+ }
+ if (handledChangesets != null) {
+ for (String key : handledChangesets) {
+ state.markHandledAtCurrentRevision(key);
+ }
+ }
+ return state.snapshotView();
+ }
+
+ Snapshot snapshotView() {
+ return new Snapshot(this, revision, orderedSlots.size());
+ }
+
+ void record(String key, Object value, boolean changesetHandled) {
+ revision++;
+ ResultSlot slot = slotsByKey.get(key);
+ if (slot == null) {
+ slot = new ResultSlot(key);
+ slotsByKey.put(key, slot);
+ orderedSlots.add(slot);
+ }
+ slot.latest = new ResultVersion(revision, value, slot.latest);
+ if (changesetHandled && !firstHandledRevision.containsKey(key)) {
+ firstHandledRevision.put(key, Long.valueOf(revision));
+ }
+ }
+
+ private void markHandledAtCurrentRevision(String key) {
+ if (!firstHandledRevision.containsKey(key)) {
+ firstHandledRevision.put(key, Long.valueOf(revision));
+ }
+ }
+
+ private Object valueAt(String key, long visibleRevision) {
+ ResultSlot slot = slotsByKey.get(key);
+ if (slot == null) {
+ return null;
+ }
+ ResultVersion version = slot.latest;
+ while (version != null && version.revision > visibleRevision) {
+ version = version.previous;
+ }
+ return version != null ? version.value : null;
+ }
+
+ private boolean containsAt(String key, long visibleRevision) {
+ ResultSlot slot = slotsByKey.get(key);
+ if (slot == null) {
+ return false;
+ }
+ ResultVersion version = slot.latest;
+ while (version != null && version.revision > visibleRevision) {
+ version = version.previous;
+ }
+ return version != null;
+ }
+
+ private boolean wasChangesetHandledAt(String key, long visibleRevision) {
+ Long handledRevision = firstHandledRevision.get(key);
+ return handledRevision != null && handledRevision.longValue() <= visibleRevision;
+ }
+
+ static final class Snapshot extends AbstractMap {
+ private final WorkflowExecutionState state;
+ private final long visibleRevision;
+ private final int visibleSlotCount;
+ private final Set> entries;
+ private final Map readOnlyResults;
+
+ private Snapshot(WorkflowExecutionState state,
+ long visibleRevision,
+ int visibleSlotCount) {
+ this.state = state;
+ this.visibleRevision = visibleRevision;
+ this.visibleSlotCount = visibleSlotCount;
+ this.entries = new SnapshotEntrySet();
+ this.readOnlyResults = Collections.unmodifiableMap(this);
+ }
+
+ Map results() {
+ return readOnlyResults;
+ }
+
+ boolean wasChangesetHandled(String stepKey) {
+ return stepKey != null && state.wasChangesetHandledAt(stepKey, visibleRevision);
+ }
+
+ @Override
+ public Object get(Object key) {
+ if (key != null && !(key instanceof String)) {
+ return null;
+ }
+ return state.valueAt((String) key, visibleRevision);
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ if (key != null && !(key instanceof String)) {
+ return false;
+ }
+ return state.containsAt((String) key, visibleRevision);
+ }
+
+ @Override
+ public int size() {
+ return visibleSlotCount;
+ }
+
+ @Override
+ public Set> entrySet() {
+ return entries;
+ }
+
+ private final class SnapshotEntrySet extends AbstractSet> {
+ @Override
+ public Iterator> iterator() {
+ return new Iterator>() {
+ private int index;
+
+ @Override
+ public boolean hasNext() {
+ return index < visibleSlotCount;
+ }
+
+ @Override
+ public Map.Entry next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ ResultSlot slot = state.orderedSlots.get(index++);
+ return new SimpleImmutableEntry(slot.key,
+ state.valueAt(slot.key, visibleRevision));
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("workflow state is read-only");
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return visibleSlotCount;
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException("workflow state is read-only");
+ }
+ }
+ }
+
+ private static final class ResultSlot {
+ private final String key;
+ private ResultVersion latest;
+
+ private ResultSlot(String key) {
+ this.key = key;
+ }
+ }
+
+ private static final class ResultVersion {
+ private final long revision;
+ private final Object value;
+ private final ResultVersion previous;
+
+ private ResultVersion(long revision, Object value, ResultVersion previous) {
+ this.revision = revision;
+ this.value = value;
+ this.previous = previous;
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java b/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
index f4f4e22..4dc9c82 100644
--- a/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
+++ b/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
@@ -1,16 +1,25 @@
package blue.coordination.processor.workflow;
import blue.language.model.Node;
+import blue.language.snapshot.FrozenNode;
+
+import java.util.Locale;
final class WorkflowPatchEntry {
private final String op;
private final String path;
- private final Node val;
+ private final FrozenNode val;
WorkflowPatchEntry(String op, String path, Node val) {
this.op = op;
this.path = path;
- this.val = val;
+ this.val = isRemove(op) || val == null ? null : FrozenNode.fromNode(val);
+ }
+
+ WorkflowPatchEntry(String op, String path, FrozenNode val) {
+ this.op = op;
+ this.path = path;
+ this.val = isRemove(op) ? null : canonicalSnapshot(val);
}
String op() {
@@ -21,7 +30,18 @@ String path() {
return path;
}
- Node val() {
+ FrozenNode val() {
return val;
}
+
+ private static FrozenNode canonicalSnapshot(FrozenNode value) {
+ if (value == null || value.isStrictCanonical()) {
+ return value;
+ }
+ return FrozenNode.fromNode(value.toNode());
+ }
+
+ private static boolean isRemove(String op) {
+ return op != null && "remove".equals(op.trim().toLowerCase(Locale.ROOT));
+ }
}
diff --git a/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java b/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
index 7b83f03..ef3d123 100644
--- a/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
+++ b/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
@@ -1,5 +1,6 @@
package blue.coordination.processor;
+import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.Blue;
import blue.language.model.Node;
import blue.language.processor.ContractProcessorRegistry;
@@ -29,6 +30,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CoordinationProcessorsTest {
@@ -48,6 +50,113 @@ void configureBuilderRegistersCoordinationProcessors() {
assertCoordinationProcessorsRegistered(processor);
}
+ @Test
+ void registerWithBlueInstallsOptionsMetricsAsLanguageSink() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ Blue blue = CoordinationTestResources.configuredBlue(BlueRepository.latest());
+
+ CoordinationProcessors.registerWith(blue, CoordinationProcessorOptions.builder()
+ .processingMetrics(metrics)
+ .build());
+
+ assertSame(metrics, blue.getDocumentProcessor().processingMetricsSink());
+ }
+
+ @Test
+ void registerWithBluePreservesAndFansOutToIndependentLanguageSink() {
+ BexProcessingMetrics existing = new BexProcessingMetrics();
+ BexProcessingMetrics coordination = new BexProcessingMetrics();
+ Blue blue = CoordinationTestResources.configuredBlue(BlueRepository.latest());
+ blue.getDocumentProcessor().processingMetricsSink(existing);
+
+ CoordinationProcessors.registerWith(blue, CoordinationProcessorOptions.builder()
+ .processingMetrics(coordination)
+ .build());
+ blue.getDocumentProcessor().processingMetricsSink().incrementPatchSequencesPrepared();
+ blue.getDocumentProcessor().processingMetricsSink().addPatchesPrepared(3L);
+
+ assertEquals(1L, existing.preparedPatchSequences());
+ assertEquals(3L, existing.preparedPatches());
+ assertEquals(1L, coordination.preparedPatchSequences());
+ assertEquals(3L, coordination.preparedPatches());
+ }
+
+ @Test
+ void registerWithBlueFansOutGenericLanguageMetricsToBothSinks() {
+ BexProcessingMetrics existing = new BexProcessingMetrics();
+ BexProcessingMetrics coordination = new BexProcessingMetrics();
+ Blue blue = CoordinationTestResources.configuredBlue(BlueRepository.latest());
+ blue.getDocumentProcessor().processingMetricsSink(existing);
+
+ CoordinationProcessors.registerWith(blue, CoordinationProcessorOptions.builder()
+ .processingMetrics(coordination)
+ .build());
+ blue.getDocumentProcessor().processingMetricsSink()
+ .incrementFullSnapshotFallback("compositeTest");
+ blue.getDocumentProcessor().processingMetricsSink()
+ .incrementNodeCloneCalls("compositeTest");
+ blue.getDocumentProcessor().processingMetricsSink()
+ .setCacheEntries("compositeTest", 4L);
+ blue.getDocumentProcessor().processingMetricsSink()
+ .recordCacheHighWaterBytes("compositeTest", 12L);
+
+ assertEquals(existing.languageCounters(), coordination.languageCounters());
+ assertEquals(existing.languageGauges(), coordination.languageGauges());
+ assertEquals(existing.languageHighWaterMarks(), coordination.languageHighWaterMarks());
+ assertEquals(1L, existing.languageCounters().get("fullSnapshotFallbacks"));
+ assertEquals(1L, existing.languageCounters()
+ .get("fullSnapshotFallbackReason.compositeTest"));
+ assertEquals(1L, existing.languageCounters()
+ .get("nodeCloneCallsByPurpose.compositeTest"));
+ assertEquals(4L, existing.languageGauges().get("cache.compositeTest.entries"));
+ assertEquals(12L, existing.languageHighWaterMarks()
+ .get("cache.compositeTest.highWaterBytes"));
+ }
+
+ @Test
+ void configureBuilderInstallsOptionsMetricsAsLanguageSink() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ DocumentProcessor processor = CoordinationProcessors.configure(
+ DocumentProcessor.builder(),
+ CoordinationProcessorOptions.builder().processingMetrics(metrics).build())
+ .build();
+
+ assertSame(metrics, processor.processingMetricsSink());
+ }
+
+ @Test
+ void optionsMetricsReceiveRealLanguageMultiPatchSequenceCallbacks() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ Fixture fixture = configuredFixture(CoordinationProcessorOptions.builder()
+ .processingMetrics(metrics)
+ .build());
+ Node preprocessed = fixture.blue.preprocess(multiPatchCounterDocument(fixture.repository));
+ DocumentProcessingResult initialized = fixture.blue.initializeDocument(preprocessed);
+ BexProcessingMetrics.Snapshot before = metrics.snapshot();
+
+ DocumentProcessingResult processed = fixture.blue.processDocument(initialized.document(),
+ TestTimelineProvider.timelineEntry(fixture.blue,
+ fixture.repository,
+ "owner",
+ 1,
+ CoordinationTestResources.operationRequest(
+ "increment", "ownerChannel", new Node().value(7))));
+ BexProcessingMetrics.Snapshot after = metrics.snapshot();
+
+ assertFalse(processed.capabilityFailure(), processed.failureReason());
+ assertEquals(BigInteger.valueOf(3), processed.document().get("/counter"));
+ assertEquals(1L, after.preparedPatchSequences - before.preparedPatchSequences);
+ assertEquals(3L, after.preparedPatches - before.preparedPatches);
+ assertEquals(1L, after.languageSequenceTransactions - before.languageSequenceTransactions);
+ assertEquals(0L, after.languageSingletonTransactions - before.languageSingletonTransactions);
+ assertEquals(0L, after.languageSuffixRebases - before.languageSuffixRebases);
+ assertEquals(0L, after.languageFallbackPatches - before.languageFallbackPatches);
+ assertEquals(2L, after.languageIntermediateSnapshotAdvances
+ - before.languageIntermediateSnapshotAdvances);
+ assertEquals(1L, after.languageFinalSnapshotPromotions
+ - before.languageFinalSnapshotPromotions);
+ }
+
@Test
void realRepositoryCoordinationContractsLoadAndInitialize() {
Fixture fixture = configuredFixture();
@@ -141,12 +250,44 @@ private static void assertCoordinationProcessorsRegistered(DocumentProcessor pro
}
private static Fixture configuredFixture() {
+ return configuredFixture(null);
+ }
+
+ private static Fixture configuredFixture(CoordinationProcessorOptions options) {
BlueRepository repository = BlueRepository.latest();
Blue blue = CoordinationTestResources.configuredBlue(repository);
- CoordinationProcessors.registerWith(blue);
+ CoordinationProcessors.registerWith(blue, options);
return new Fixture(repository, blue);
}
+ private static Node multiPatchCounterDocument(BlueRepository repository) {
+ Node update = new Node()
+ .type("Coordination/Update Document")
+ .properties("changeset", new Node().items(
+ replacePatch("/counter", 1),
+ replacePatch("/counter", 2),
+ replacePatch("/counter", 3)));
+ Map contracts = new LinkedHashMap<>();
+ contracts.put("ownerChannel", TestTimelineProvider.channel("owner"));
+ contracts.put("increment", new Node()
+ .type("Coordination/Sequential Workflow Operation")
+ .properties("channel", new Node().value("ownerChannel"))
+ .properties("request", new Node().type("Integer"))
+ .properties("steps", new Node().items(update)));
+ return new Node()
+ .blue(repository.typeAliasBlue())
+ .name("MultiPatchCounter")
+ .properties("counter", new Node().value(0))
+ .properties("contracts", new Node().properties(contracts));
+ }
+
+ private static Node replacePatch(String path, int value) {
+ return new Node()
+ .properties("op", new Node().value("replace"))
+ .properties("path", new Node().value(path))
+ .properties("val", new Node().value(value));
+ }
+
private static Node counterDocument(BlueRepository repository, String operationChannel) {
Map contracts = new LinkedHashMap<>();
contracts.put("ownerChannel", TestTimelineProvider.channel("owner"));
diff --git a/src/test/java/blue/coordination/processor/SequentialWorkflowExecutionTest.java b/src/test/java/blue/coordination/processor/SequentialWorkflowExecutionTest.java
index 1e6abb2..7cfb201 100644
--- a/src/test/java/blue/coordination/processor/SequentialWorkflowExecutionTest.java
+++ b/src/test/java/blue/coordination/processor/SequentialWorkflowExecutionTest.java
@@ -21,6 +21,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
@@ -342,6 +343,50 @@ public WorkflowStepResult execute(TriggerEvent step, StepExecutionContext contex
assertEquals(Boolean.TRUE, sawNullResult.get());
}
+ @Test
+ void workflowPlanReusesExactContractAndReplansChangedContract() {
+ AtomicInteger supportsCalls = new AtomicInteger();
+ WorkflowStepExecutor executor = new WorkflowStepExecutor() {
+ @Override
+ public boolean supports(SequentialWorkflowStep step) {
+ supportsCalls.incrementAndGet();
+ return step instanceof TriggerEvent;
+ }
+
+ @Override
+ public WorkflowStepResult execute(TriggerEvent step, StepExecutionContext context) {
+ return WorkflowStepResult.none();
+ }
+ };
+ SequentialWorkflowRunner runner = new SequentialWorkflowRunner(
+ Arrays.>asList(executor));
+ Fixture fixture = configuredFixture(null, runner);
+
+ Node first = initializedDocument(fixture, directWorkflowStepsDocument(fixture.repository,
+ 0,
+ "same contract",
+ triggerEventStep("ignored")));
+ Node afterFirst = processChat(fixture, first, "owner", 1, "run").document();
+ processChat(fixture, afterFirst, "owner", 2, "run");
+ Node equivalent = initializedDocument(fixture, directWorkflowStepsDocument(fixture.repository,
+ 0,
+ "same contract",
+ triggerEventStep("ignored")));
+ processChat(fixture, equivalent, "owner", 1, "run");
+ Node changed = initializedDocument(fixture, directWorkflowStepsDocument(fixture.repository,
+ 0,
+ "changed contract",
+ triggerEventStep("ignored")));
+ processChat(fixture, changed, "owner", 1, "run");
+
+ assertEquals(2, supportsCalls.get());
+ assertEquals(2, runner.workflowPlanCacheSize());
+ assertTrue(runner.workflowPlanCacheWeightBytes() > 0L);
+ runner.clearCaches();
+ assertEquals(0, runner.workflowPlanCacheSize());
+ assertEquals(0L, runner.workflowPlanCacheWeightBytes());
+ }
+
private static Node processOperationRequest(Fixture fixture,
Node document,
String timelineId,
diff --git a/src/test/java/blue/coordination/processor/bex/BexProcessingMetricsTest.java b/src/test/java/blue/coordination/processor/bex/BexProcessingMetricsTest.java
index c75c4ba..d08f857 100644
--- a/src/test/java/blue/coordination/processor/bex/BexProcessingMetricsTest.java
+++ b/src/test/java/blue/coordination/processor/bex/BexProcessingMetricsTest.java
@@ -2,10 +2,130 @@
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
class BexProcessingMetricsTest {
+ @Test
+ void genericLanguageMetricsAreThreadSafeSortedAndImmutable() throws Exception {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ int workers = 8;
+ int additionsPerWorker = 2_000;
+ ExecutorService executor = Executors.newFixedThreadPool(workers);
+ CountDownLatch start = new CountDownLatch(1);
+ List> futures = new ArrayList<>();
+ try {
+ for (int worker = 0; worker < workers; worker++) {
+ futures.add(executor.submit(() -> {
+ start.await();
+ for (int addition = 0; addition < additionsPerWorker; addition++) {
+ metrics.addMetric("concurrent.additions", 1L);
+ }
+ return null;
+ }));
+ }
+ start.countDown();
+ for (Future future : futures) {
+ future.get();
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ metrics.addMetric("alpha", 2L);
+ metrics.addMetric("zulu", 3L);
+ metrics.setMetric("cache.plan.entries", 1L);
+ metrics.setMetric("cache.plan.entries", 7L);
+ metrics.recordMetricHighWater("cache.plan.highWaterBytes", 5L);
+ metrics.recordMetricHighWater("cache.plan.highWaterBytes", 11L);
+ metrics.recordMetricHighWater("cache.plan.highWaterBytes", 9L);
+
+ BexProcessingMetrics.Snapshot snapshot = metrics.snapshot();
+ assertEquals((long) workers * additionsPerWorker,
+ snapshot.languageCounters.get("concurrent.additions"));
+ assertEquals(7L, snapshot.languageGauges.get("cache.plan.entries"));
+ assertEquals(11L,
+ snapshot.languageHighWaterMarks.get("cache.plan.highWaterBytes"));
+ assertEquals(Arrays.asList("alpha", "concurrent.additions", "zulu"),
+ new ArrayList<>(snapshot.languageCounters.keySet()));
+
+ assertThrows(UnsupportedOperationException.class,
+ () -> snapshot.languageCounters.put("later", 1L));
+ assertThrows(UnsupportedOperationException.class,
+ () -> snapshot.languageGauges.clear());
+ assertThrows(UnsupportedOperationException.class,
+ () -> snapshot.languageHighWaterMarks.remove("cache.plan.highWaterBytes"));
+
+ metrics.addMetric("alpha", 5L);
+ metrics.setMetric("cache.plan.entries", 9L);
+ metrics.recordMetricHighWater("cache.plan.highWaterBytes", 13L);
+ assertEquals(2L, snapshot.languageCounters.get("alpha"));
+ assertEquals(7L, snapshot.languageGauges.get("cache.plan.entries"));
+ assertEquals(11L,
+ snapshot.languageHighWaterMarks.get("cache.plan.highWaterBytes"));
+ }
+
+ @Test
+ void genericLanguageMetricsRetainSuffixesAndCacheMetricKinds() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+
+ metrics.incrementFullSnapshotFallback("stalePreview");
+ metrics.incrementNodeCloneCalls("patchValue");
+ metrics.incrementNodeCloneCalls("patchValue");
+ metrics.incrementCacheHits("processingSnapshot");
+ metrics.setCacheCurrentWeightBytes("processingSnapshot", 40L);
+ metrics.setCacheEntries("processingSnapshot", 3L);
+ metrics.recordCacheHighWaterBytes("processingSnapshot", 40L);
+ metrics.recordCacheHighWaterBytes("processingSnapshot", 35L);
+ metrics.recordCacheHighWaterBytes("processingSnapshot", 52L);
+
+ Map counters = metrics.languageCounters();
+ assertEquals(1L, counters.get("fullSnapshotFallbacks"));
+ assertEquals(1L, counters.get("fullSnapshotFallbackReason.stalePreview"));
+ assertEquals(2L, counters.get("nodeCloneCallsByPurpose.patchValue"));
+ assertEquals(1L, counters.get("cache.processingSnapshot.hits"));
+ assertEquals(40L,
+ metrics.languageGauges().get("cache.processingSnapshot.currentWeightBytes"));
+ assertEquals(3L,
+ metrics.languageGauges().get("cache.processingSnapshot.entries"));
+ assertEquals(52L, metrics.languageHighWaterMarks()
+ .get("cache.processingSnapshot.highWaterBytes"));
+ }
+
+ @Test
+ void genericLanguageMetricNamesAreCappedAcrossMetricKinds() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ for (int index = 0; index < BexProcessingMetrics.MAX_LANGUAGE_METRIC_NAMES; index++) {
+ metrics.addMetric("bounded." + index, 1L);
+ }
+
+ metrics.setMetric("bounded.0", 7L);
+ metrics.setMetric("overflow.gauge", 9L);
+ metrics.recordMetricHighWater("overflow.highWater", 11L);
+ metrics.addMetric("overflow.counter", 1L);
+ BexProcessingMetrics.Snapshot snapshot = metrics.snapshot();
+
+ assertEquals(BexProcessingMetrics.MAX_LANGUAGE_METRIC_NAMES,
+ snapshot.languageCounters.size());
+ assertEquals(7L, snapshot.languageGauges.get("bounded.0"));
+ assertFalse(snapshot.languageGauges.containsKey("overflow.gauge"));
+ assertFalse(snapshot.languageHighWaterMarks.containsKey("overflow.highWater"));
+ assertFalse(snapshot.languageCounters.containsKey("overflow.counter"));
+ assertEquals(3L, snapshot.droppedLanguageMetricNames);
+ assertEquals(3L, metrics.droppedLanguageMetricNames());
+ }
+
@Test
void bexProcessingMetricsExposeProcessEventSnapshotCounters() {
BexProcessingMetrics metrics = new BexProcessingMetrics();
@@ -57,6 +177,107 @@ void terminationMetricsAreBoundedCountersAndSnapshotsAreImmutable() {
assertTerminationSnapshot(second, 2L);
}
+ @Test
+ void languageSequenceCallbacksExposeProofAliasesAndImmutableSnapshots() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ metrics.incrementPatchSequencesPrepared();
+ metrics.addPatchesPrepared(3L);
+ metrics.incrementSingletonPatchTransactions();
+ metrics.addSequencePlanningNanos(-1L);
+ metrics.addSequenceConformanceNanos(2L);
+ metrics.addSequenceCommitNanos(3L);
+ metrics.addSequenceFinalCacheCommitNanos(4L);
+ metrics.incrementSequenceIntermediateSnapshotAdvances();
+ metrics.incrementSequenceSharedSnapshotCacheInserts();
+ metrics.incrementSequenceFinalSnapshotCacheInserts();
+ metrics.incrementSequenceSuffixRebases();
+ metrics.incrementSequenceStalePreviewFallbacks();
+ metrics.incrementSequenceFallbackPatches();
+ metrics.incrementParsedPointerCacheHits();
+ metrics.incrementParsedPointerCacheMisses();
+ metrics.incrementFrozenPatchValueHits();
+ metrics.incrementPatchValueMaterializations();
+ BexProcessingMetrics.Snapshot first = metrics.snapshot();
+
+ metrics.incrementPatchSequencesPrepared();
+ metrics.addPatchesPrepared(2L);
+ metrics.incrementSequenceFinalSnapshotCacheInserts();
+ metrics.incrementPatchValueMaterializations();
+ BexProcessingMetrics.Snapshot second = metrics.snapshot();
+
+ assertLanguageSnapshot(first, 1L, 3L, 1L, 1L);
+ assertEquals(0L, first.sequencePlanningNanos);
+ assertEquals(2L, first.sequenceConformanceNanos);
+ assertEquals(3L, first.sequenceCommitNanos);
+ assertEquals(4L, first.sequenceFinalCacheCommitNanos);
+ assertEquals(1L, first.languageIntermediateSnapshotAdvances);
+ assertEquals(1L, first.sequenceSharedSnapshotCacheInserts);
+ assertEquals(1L, first.languageSuffixRebases);
+ assertEquals(1L, first.sequenceStalePreviewFallbacks);
+ assertEquals(1L, first.languageFallbackPatches);
+ assertEquals(1L, first.parsedPointerCacheHits);
+ assertEquals(1L, first.parsedPointerCacheMisses);
+ assertEquals(1L, first.frozenPatchValueHits);
+ assertLanguageSnapshot(second, 2L, 5L, 2L, 2L);
+ }
+
+ @Test
+ void planAndConversionMetricsUseImmutableSnapshotsAndNonNegativeWeightGauges() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ metrics.incrementWorkflowPlansBuilt();
+ metrics.incrementWorkflowPlanCacheHits();
+ metrics.incrementWorkflowPlanCacheMisses();
+ metrics.incrementWorkflowPlanCacheEvictions();
+ metrics.addWorkflowPlanWeightBytes(100L);
+ metrics.incrementWorkflowExecutorLookups();
+ metrics.incrementWorkflowStepResultSnapshotsCreated();
+ metrics.incrementWorkflowStepResultViewHits();
+ metrics.incrementComputePlansBuilt();
+ metrics.incrementComputePlanCacheHits();
+ metrics.incrementComputePlanCacheMisses();
+ metrics.incrementComputePlanCacheEvictions();
+ metrics.addComputePlanWeightBytes(200L);
+ metrics.incrementComputeDefinitionMaterializations();
+ metrics.incrementComputeDefinitionFrozenDirectHits();
+ metrics.incrementComputeProgramSourceBuilds();
+ metrics.incrementBexPatchFrozenDirectConversions();
+ metrics.incrementBexPatchNodeMaterializations();
+ metrics.incrementUpdateStaticTemplatesBuilt();
+ metrics.incrementUpdateStaticTemplateHits();
+ metrics.incrementUpdateReflectionFallbacks();
+ BexProcessingMetrics.Snapshot first = metrics.snapshot();
+
+ metrics.addWorkflowPlanWeightBytes(-150L);
+ metrics.addComputePlanWeightBytes(-50L);
+ metrics.incrementWorkflowPlansBuilt();
+ BexProcessingMetrics.Snapshot second = metrics.snapshot();
+
+ assertEquals(1L, first.workflowPlansBuilt);
+ assertEquals(1L, first.workflowPlanCacheHits);
+ assertEquals(1L, first.workflowPlanCacheMisses);
+ assertEquals(1L, first.workflowPlanCacheEvictions);
+ assertEquals(100L, first.workflowPlanWeightBytes);
+ assertEquals(1L, first.workflowExecutorLookups);
+ assertEquals(1L, first.workflowStepResultSnapshotsCreated);
+ assertEquals(1L, first.workflowStepResultViewHits);
+ assertEquals(1L, first.computePlansBuilt);
+ assertEquals(1L, first.computePlanCacheHits);
+ assertEquals(1L, first.computePlanCacheMisses);
+ assertEquals(1L, first.computePlanCacheEvictions);
+ assertEquals(200L, first.computePlanWeightBytes);
+ assertEquals(1L, first.computeDefinitionMaterializations);
+ assertEquals(1L, first.computeDefinitionFrozenDirectHits);
+ assertEquals(1L, first.computeProgramSourceBuilds);
+ assertEquals(1L, first.bexPatchFrozenDirectConversions);
+ assertEquals(1L, first.bexPatchNodeMaterializations);
+ assertEquals(1L, first.updateStaticTemplatesBuilt);
+ assertEquals(1L, first.updateStaticTemplateHits);
+ assertEquals(1L, first.updateReflectionFallbacks);
+ assertEquals(2L, second.workflowPlansBuilt);
+ assertEquals(0L, second.workflowPlanWeightBytes);
+ assertEquals(150L, second.computePlanWeightBytes);
+ }
+
private static void assertSnapshot(BexProcessingMetrics.Snapshot snapshot,
long attempts,
long builds,
@@ -74,4 +295,19 @@ private static void assertTerminationSnapshot(BexProcessingMetrics.Snapshot snap
assertEquals(expected, snapshot.declarativeTerminationSteps);
assertEquals(expected, snapshot.computeResultValidationFailures);
}
+
+ private static void assertLanguageSnapshot(BexProcessingMetrics.Snapshot snapshot,
+ long sequences,
+ long patches,
+ long finalPromotions,
+ long materializations) {
+ assertEquals(sequences, snapshot.preparedPatchSequences);
+ assertEquals(patches, snapshot.preparedPatches);
+ assertEquals(sequences, snapshot.languageSequenceTransactions,
+ "the prepared-session count is the closest public transaction proxy");
+ assertEquals(1L, snapshot.languageSingletonTransactions);
+ assertEquals(finalPromotions, snapshot.languageFinalSnapshotPromotions);
+ assertEquals(finalPromotions, snapshot.sequenceFinalSnapshotCacheInserts);
+ assertEquals(materializations, snapshot.languagePatchValueMaterializations);
+ }
}
diff --git a/src/test/java/blue/coordination/processor/compute/ComputeFrozenPatchHandoffIntegrationTest.java b/src/test/java/blue/coordination/processor/compute/ComputeFrozenPatchHandoffIntegrationTest.java
new file mode 100644
index 0000000..7bc8121
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/compute/ComputeFrozenPatchHandoffIntegrationTest.java
@@ -0,0 +1,191 @@
+package blue.coordination.processor.compute;
+
+import blue.coordination.processor.CoordinationProcessorOptions;
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.model.Node;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ProcessorStatus;
+import blue.language.processor.registry.RuntimeBlueIds;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Focused proof that built-in Compute effects use the rc15 frozen patch boundary. */
+class ComputeFrozenPatchHandoffIntegrationTest {
+
+ @Test
+ void accumulatedChangesetRetainsCanonicalFrozenBindingWithoutNodeMaterialization() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node document = support.initializedOperationWorkflow(String.join("\n",
+ " steps:",
+ " - name: CopyChannel",
+ " type: Coordination/Compute",
+ " do:",
+ " - $appendChange:",
+ " op: add",
+ " path: /copiedChannel",
+ " val:",
+ " $currentContract: /channel",
+ " - $return:",
+ " changeset:",
+ " $changeset: true"));
+ Counters before = Counters.capture(metrics);
+
+ DocumentProcessingResult result = support.processRun(document);
+
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertEquals("ownerChannel", result.document().get("/copiedChannel"));
+ assertEquals(1L, metrics.directBexChangesetHits());
+ assertEquals(1L, metrics.bexPatchFrozenDirectConversions());
+ assertEquals(0L, metrics.bexPatchNodeMaterializations());
+ assertEquals(1L, delta(metrics, before, "frozenPatchesHandedToLanguage"));
+ assertEquals(2L, delta(metrics, before, "frozenPatchValuesAccepted"),
+ "the frozen value is accepted during preview and runtime consumption");
+ assertEquals(2L, delta(metrics, before, "frozenPatchValuesHandedToLanguage"));
+ assertEquals(0L, delta(metrics, before, "mutablePatchValuesFrozen"));
+ assertEquals(0L, delta(metrics, before, "frozenPatchValuesMaterialized"));
+ }
+
+ @Test
+ void independentlyReturnedChangesetEventsAndTerminationKeepEffectOrder() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node document = support.initialize(support.yaml(
+ support.operationWorkflowDocumentWithStatus("removeMe: old", String.join("\n",
+ " steps:",
+ " - name: ReturnEffects",
+ " type: Coordination/Compute",
+ " do:",
+ " - $return:",
+ " changeset:",
+ " - op: add",
+ " path: /added",
+ " val:",
+ " nested: value",
+ " - op: replace",
+ " path: /status",
+ " val: changed",
+ " - op: remove",
+ " path: /removeMe",
+ " events:",
+ " - type: Coordination/Event",
+ " kind: first",
+ " - type: Coordination/Event",
+ " kind: second",
+ " termination:",
+ " reason: complete",
+ " - name: MustNotRun",
+ " type: Coordination/Update Document",
+ " changeset:",
+ " - op: replace",
+ " path: /status",
+ " val: forbidden")))).document();
+ Counters before = Counters.capture(metrics);
+
+ DocumentProcessingResult result = support.processRun(document);
+
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertEquals("changed", result.document().get("/status"));
+ assertEquals("value", result.document().get("/added/nested"));
+ assertFalse(hasPath(result.document(), "/removeMe"));
+ assertEquals("graceful", result.document().get("/contracts/terminated/cause"));
+ assertEquals("complete", result.document().get("/contracts/terminated/reason"));
+ assertEquals(Arrays.asList("first", "second"), selectedKinds(result));
+ assertTrue(indexOfKind(result, "second") < indexOfType(result,
+ RuntimeBlueIds.DOCUMENT_PROCESSING_TERMINATED));
+
+ assertEquals(0L, metrics.directBexChangesetHits(),
+ "the returned list is independent of BEX's accumulated changeset");
+ assertEquals(0L, metrics.bexPatchFrozenDirectConversions());
+ assertEquals(2L, metrics.bexPatchNodeMaterializations());
+ assertEquals(3L, delta(metrics, before, "frozenPatchesHandedToLanguage"));
+ assertEquals(4L, delta(metrics, before, "frozenPatchValuesAccepted"),
+ "each add/replace value is accepted by preview and runtime consumption");
+ assertEquals(4L, delta(metrics, before, "frozenPatchValuesHandedToLanguage"));
+ assertEquals(0L, delta(metrics, before, "mutablePatchValuesFrozen"));
+ assertEquals(0L, delta(metrics, before, "frozenPatchValuesMaterialized"));
+ assertEquals(2L, metrics.eventsEmitted());
+ assertEquals(1L, metrics.successfulComputeTerminationRequests());
+ }
+
+ private static ComputeWorkflowTestSupport support(BexProcessingMetrics metrics) {
+ return ComputeWorkflowTestSupport.create(CoordinationProcessorOptions.builder()
+ .processingMetrics(metrics)
+ .build());
+ }
+
+ private static long delta(BexProcessingMetrics metrics, Counters before, String name) {
+ return metric(metrics.languageCounters(), name) - metric(before.values, name);
+ }
+
+ private static long metric(Map values, String name) {
+ Long value = values.get(name);
+ return value != null ? value.longValue() : 0L;
+ }
+
+ private static boolean hasPath(Node document, String path) {
+ try {
+ return document.getNode(path) != null;
+ } catch (RuntimeException ex) {
+ return false;
+ }
+ }
+
+ private static List selectedKinds(DocumentProcessingResult result) {
+ List selected = new ArrayList();
+ for (Node event : result.triggeredEvents()) {
+ Object kind = valueAt(event, "/kind");
+ if ("first".equals(kind) || "second".equals(kind)) {
+ selected.add((String) kind);
+ }
+ }
+ return selected;
+ }
+
+ private static int indexOfKind(DocumentProcessingResult result, String kind) {
+ for (int index = 0; index < result.triggeredEvents().size(); index++) {
+ if (kind.equals(valueAt(result.triggeredEvents().get(index), "/kind"))) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private static Object valueAt(Node node, String path) {
+ try {
+ return node.get(path);
+ } catch (RuntimeException ex) {
+ return null;
+ }
+ }
+
+ private static int indexOfType(DocumentProcessingResult result, String typeBlueId) {
+ for (int index = 0; index < result.triggeredEvents().size(); index++) {
+ Node event = result.triggeredEvents().get(index);
+ if (event.getType() != null && typeBlueId.equals(event.getType().getBlueId())) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private static final class Counters {
+ private final Map values;
+
+ private Counters(Map values) {
+ this.values = values;
+ }
+
+ private static Counters capture(BexProcessingMetrics metrics) {
+ return new Counters(metrics.languageCounters());
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/compute/ComputeProgramPlanIntegrationTest.java b/src/test/java/blue/coordination/processor/compute/ComputeProgramPlanIntegrationTest.java
new file mode 100644
index 0000000..a488a58
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/compute/ComputeProgramPlanIntegrationTest.java
@@ -0,0 +1,195 @@
+package blue.coordination.processor.compute;
+
+import blue.coordination.processor.CoordinationProcessorOptions;
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.model.Node;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ProcessorStatus;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ComputeProgramPlanIntegrationTest {
+ @Test
+ void unchangedInlineComputeMissesOnceThenReusesItsFrozenPlan() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node document = support.initializedOperationWorkflow(String.join("\n",
+ " steps:",
+ " - name: Build",
+ " type: Coordination/Compute",
+ " do:",
+ " - $return:",
+ " value: warm"));
+
+ DocumentProcessingResult first = support.processRun(document);
+ DocumentProcessingResult second = support.processRun(first.document());
+
+ assertFalse(first.capabilityFailure(), first.failureReason());
+ assertFalse(second.capabilityFailure(), second.failureReason());
+ assertEquals(1L, metrics.computePlanCacheMisses());
+ assertEquals(1L, metrics.computePlanCacheHits());
+ assertEquals(1L, metrics.computePlansBuilt());
+ assertEquals(1L, metrics.computeProgramNormalizations());
+ assertEquals(1L, metrics.computeProgramSourceBuilds());
+ assertEquals(0L, metrics.computeDefinitionNormalizations());
+ assertTrue(metrics.computePlanWeightBytes() > 0L);
+ }
+
+ @Test
+ void referencedDefinitionIsReadFrozenEveryTimeButNormalizedOnlyOnMiss() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node document = definitionDocument(support, "Warm Definition");
+
+ DocumentProcessingResult first = support.processRun(document);
+ DocumentProcessingResult second = support.processRun(first.document());
+
+ assertEquals("Warm Definition", onlyEvent(first).get("/kind"));
+ assertEquals("Warm Definition", onlyEvent(second).get("/kind"));
+ assertEquals(1L, metrics.computePlanCacheMisses());
+ assertEquals(1L, metrics.computePlanCacheHits());
+ assertEquals(1L, metrics.computePlansBuilt());
+ assertEquals(1L, metrics.computeProgramNormalizations());
+ assertEquals(1L, metrics.computeDefinitionNormalizations());
+ assertEquals(1L, metrics.computeDefinitionMaterializations());
+ assertEquals(2L, metrics.computeDefinitionFrozenDirectHits());
+ assertEquals(1L, metrics.computeProgramSourceBuilds());
+ }
+
+ @Test
+ void sameContractAndStepNamesAcrossDocumentsUseExactDefinitionIdentity() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node documentA = definitionDocument(support, "Definition A");
+ Node documentB = definitionDocument(support, "Definition B");
+
+ DocumentProcessingResult firstA = support.processRun(documentA);
+ DocumentProcessingResult firstB = support.processRun(documentB);
+ DocumentProcessingResult warmA = support.processRun(firstA.document());
+ DocumentProcessingResult warmB = support.processRun(firstB.document());
+
+ assertEquals("Definition A", onlyEvent(firstA).get("/kind"));
+ assertEquals("Definition B", onlyEvent(firstB).get("/kind"));
+ assertEquals("Definition A", onlyEvent(warmA).get("/kind"));
+ assertEquals("Definition B", onlyEvent(warmB).get("/kind"));
+ assertEquals(2L, metrics.computePlanCacheMisses());
+ assertEquals(2L, metrics.computePlanCacheHits());
+ assertEquals(2L, metrics.computePlansBuilt());
+ assertEquals(2L, metrics.computeDefinitionNormalizations());
+ assertEquals(2L, metrics.computeProgramSourceBuilds());
+ }
+
+ @Test
+ void changedStepContentBuildsASeparatePlan() {
+ BexProcessingMetrics metrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport support = support(metrics);
+ Node documentA = inlineDocument(support, "A");
+ Node documentB = inlineDocument(support, "B");
+
+ assertFalse(support.processRun(documentA).capabilityFailure());
+ assertFalse(support.processRun(documentB).capabilityFailure());
+
+ assertEquals(2L, metrics.computePlanCacheMisses());
+ assertEquals(0L, metrics.computePlanCacheHits());
+ assertEquals(2L, metrics.computePlansBuilt());
+ assertEquals(2L, metrics.computeProgramNormalizations());
+ assertEquals(2L, metrics.computeProgramSourceBuilds());
+ }
+
+ @Test
+ void malformedProgramAndFatalResultNeverPoisonPlanCache() {
+ BexProcessingMetrics malformedMetrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport malformedSupport = support(malformedMetrics);
+ Node malformed = malformedSupport.initialize(malformedSupport.yaml(
+ malformedSupport.operationWorkflowDocumentWithContracts(String.join("\n",
+ " computeLogic:",
+ " type: Coordination/Compute Definition",
+ " functions:",
+ " build:",
+ " do:",
+ " - $return: {}"),
+ String.join("\n",
+ " steps:",
+ " - name: Build",
+ " type: Coordination/Compute",
+ " definition: computeLogic",
+ " entry: missing")))).document();
+
+ assertRuntimeFatal(malformedSupport.processRun(malformed), "Unknown entry function");
+ assertRuntimeFatal(malformedSupport.processRun(malformed), "Unknown entry function");
+ assertEquals(2L, malformedMetrics.computePlanCacheMisses());
+ assertEquals(0L, malformedMetrics.computePlanCacheHits());
+ assertEquals(2L, malformedMetrics.computePlansBuilt());
+
+ BexProcessingMetrics fatalMetrics = new BexProcessingMetrics();
+ ComputeWorkflowTestSupport fatalSupport = support(fatalMetrics);
+ Node fatal = fatalSupport.initializedOperationWorkflow(String.join("\n",
+ " steps:",
+ " - name: Build",
+ " type: Coordination/Compute",
+ " do:",
+ " - $return:",
+ " events: malformed"));
+
+ assertRuntimeFatal(fatalSupport.processRun(fatal), "Compute result events must be a list");
+ assertRuntimeFatal(fatalSupport.processRun(fatal), "Compute result events must be a list");
+ assertEquals(2L, fatalMetrics.computePlanCacheMisses());
+ assertEquals(0L, fatalMetrics.computePlanCacheHits());
+ assertEquals(2L, fatalMetrics.computePlansBuilt());
+ }
+
+ private static ComputeWorkflowTestSupport support(BexProcessingMetrics metrics) {
+ return ComputeWorkflowTestSupport.create(CoordinationProcessorOptions.builder()
+ .processingMetrics(metrics)
+ .build());
+ }
+
+ private static Node inlineDocument(ComputeWorkflowTestSupport support, String value) {
+ return support.initializedOperationWorkflow(String.join("\n",
+ " steps:",
+ " - name: Build",
+ " type: Coordination/Compute",
+ " do:",
+ " - $return:",
+ " value: " + value));
+ }
+
+ private static Node definitionDocument(ComputeWorkflowTestSupport support, String kind) {
+ return support.initialize(support.yaml(support.operationWorkflowDocumentWithContracts(
+ String.join("\n",
+ " computeLogic:",
+ " type: Coordination/Compute Definition",
+ " constants:",
+ " kind: " + kind,
+ " functions:",
+ " build:",
+ " do:",
+ " - $appendEvent:",
+ " type: Coordination/Event",
+ " kind:",
+ " $const: kind",
+ " - $return: {}"),
+ String.join("\n",
+ " steps:",
+ " - name: Build",
+ " type: Coordination/Compute",
+ " definition: computeLogic",
+ " entry: build")))).document();
+ }
+
+ private static Node onlyEvent(DocumentProcessingResult result) {
+ assertEquals(1, result.triggeredEvents().size(), result.failureReason());
+ return result.triggeredEvents().get(0);
+ }
+
+ private static void assertRuntimeFatal(DocumentProcessingResult result,
+ String expectedMessage) {
+ assertEquals(ProcessorStatus.RUNTIME_FATAL, result.status(), result.failureReason());
+ assertTrue(result.failureReason() != null
+ && result.failureReason().contains(expectedMessage),
+ result.failureReason());
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/compute/ComputeWorkflowExecutionTest.java b/src/test/java/blue/coordination/processor/compute/ComputeWorkflowExecutionTest.java
index 816e467..530da4f 100644
--- a/src/test/java/blue/coordination/processor/compute/ComputeWorkflowExecutionTest.java
+++ b/src/test/java/blue/coordination/processor/compute/ComputeWorkflowExecutionTest.java
@@ -817,13 +817,23 @@ public void accept(BexMetrics item) {
" $document: /status"));
Node afterFirst = support.processRun(document).document();
+ long hitsAfterWarmup = 0L;
+ long missesAfterWarmup = 0L;
+ for (BexMetrics item : metrics) {
+ hitsAfterWarmup += item.compileCacheHits();
+ missesAfterWarmup += item.compileCacheMisses();
+ }
+
support.processRun(afterFirst);
- long hits = 0L;
+ long totalHits = 0L;
+ long totalMisses = 0L;
for (BexMetrics item : metrics) {
- hits += item.compileCacheHits();
+ totalHits += item.compileCacheHits();
+ totalMisses += item.compileCacheMisses();
}
- assertTrue(hits > 0L);
+ assertTrue(totalHits - hitsAfterWarmup > 0L);
+ assertEquals(0L, totalMisses - missesAfterWarmup);
}
@Test
diff --git a/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactTest.java b/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactTest.java
new file mode 100644
index 0000000..a6c038c
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactTest.java
@@ -0,0 +1,367 @@
+package blue.coordination.processor.compute;
+
+import blue.bex.api.BexEngine;
+import blue.coordination.processor.CoordinationProcessorOptions;
+import blue.coordination.processor.CoordinationTestResources;
+import blue.coordination.processor.RepositoryTypeAliasPreprocessor;
+import blue.coordination.processor.TestTimelineProvider;
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.coordination.processor.workflow.SequentialWorkflowRunner;
+import blue.language.model.Node;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ProcessorStatus;
+import blue.language.snapshot.ResolvedSnapshot;
+import blue.repo.coordination.StatusPending;
+import blue.repo.mandate.Mandate;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Produces deterministic machine-readable evidence for the rc15 adoption scenarios. */
+class LanguageRc15MetricsArtifactTest {
+ private static final String PAYNOTE_RESOURCE =
+ "/processor-delay/paynote-resale-reduced-bex.yaml";
+ private static final Path REPORT_DIRECTORY = Paths.get(System.getProperty("user.dir"),
+ "build", "reports", "language-rc15-adoption");
+
+ @Test
+ void writesJsonAndCsvForRequiredRepresentativeScenarios() throws Exception {
+ List scenarios = Arrays.asList(
+ staticUpdateDocumentScenario(),
+ multiPatchComputeScenario(),
+ payNoteFixtureScenario(),
+ mandateFixtureScenario());
+
+ LanguageRc15MetricsArtifactWriter.write(REPORT_DIRECTORY, scenarios);
+
+ Path json = REPORT_DIRECTORY.resolve(LanguageRc15MetricsArtifactWriter.JSON_FILE_NAME);
+ Path csv = REPORT_DIRECTORY.resolve(LanguageRc15MetricsArtifactWriter.CSV_FILE_NAME);
+ assertTrue(Files.isRegularFile(json));
+ assertTrue(Files.isRegularFile(csv));
+ assertJsonScenarios(json);
+ assertCsvScenarios(csv);
+ }
+
+ private static LanguageRc15MetricsArtifactWriter.Scenario staticUpdateDocumentScenario() {
+ OwnedScenario fixture = new OwnedScenario();
+ try {
+ DocumentProcessingResult initialized = fixture.support.initialize(fixture.support.yaml(
+ fixture.support.operationWorkflowDocumentWithStatus("count: 0", String.join("\n",
+ " steps:",
+ " - name: ApplyStaticChanges",
+ " type: Coordination/Update Document",
+ " changeset:",
+ " - op: replace",
+ " path: /status",
+ " val: static-updated",
+ " - op: replace",
+ " path: /count",
+ " val: 1"))));
+ Node document = initialized.document();
+ BexProcessingMetrics.Snapshot baseline = fixture.metrics.snapshot();
+
+ DocumentProcessingResult result = fixture.support.processRun(document);
+ assertSuccess(result);
+ assertEquals("static-updated", result.document().get("/status"));
+ assertEquals(BigInteger.ONE, result.document().get("/count"));
+ assertEquals(2L, fixture.metrics.patchesApplied());
+ assertTrue(metric(fixture.metrics, "frozenPatchesHandedToLanguage") >= 2L);
+ return LanguageRc15MetricsArtifactWriter.capture(
+ "static-update-document",
+ "static-update-document",
+ "inline Coordination/Update Document with two authored patches",
+ result,
+ fixture.metrics,
+ baseline);
+ } finally {
+ fixture.close();
+ }
+ }
+
+ private static LanguageRc15MetricsArtifactWriter.Scenario multiPatchComputeScenario() {
+ OwnedScenario fixture = new OwnedScenario();
+ try {
+ DocumentProcessingResult initialized = fixture.support.initialize(fixture.support.yaml(
+ fixture.support.operationWorkflowDocumentWithStatus("count: 0", String.join("\n",
+ " steps:",
+ " - name: BuildMultiPatchChangeset",
+ " type: Coordination/Compute",
+ " do:",
+ " - $appendChange:",
+ " op: replace",
+ " path: /status",
+ " val: first",
+ " - $appendChange:",
+ " op: replace",
+ " path: /count",
+ " val: 2",
+ " - $appendChange:",
+ " op: replace",
+ " path: /status",
+ " val: computed",
+ " - $return:",
+ " changeset:",
+ " $changeset: true"))));
+ Node document = initialized.document();
+ BexProcessingMetrics.Snapshot baseline = fixture.metrics.snapshot();
+
+ DocumentProcessingResult result = fixture.support.processRun(document);
+ assertSuccess(result);
+ assertEquals("computed", result.document().get("/status"));
+ assertEquals(BigInteger.valueOf(2L), result.document().get("/count"));
+ assertEquals(3L, fixture.metrics.patchesApplied());
+ assertEquals(1L, fixture.metrics.updateBatchPatchApplications());
+ assertEquals(0L, fixture.metrics.updateIndividualPatchApplications());
+ return LanguageRc15MetricsArtifactWriter.capture(
+ "multi-patch-compute",
+ "multi-patch-compute",
+ "inline Coordination/Compute accumulated three-patch BEX changeset",
+ result,
+ fixture.metrics,
+ baseline);
+ } finally {
+ fixture.close();
+ }
+ }
+
+ private static LanguageRc15MetricsArtifactWriter.Scenario payNoteFixtureScenario() {
+ OwnedScenario fixture = new OwnedScenario();
+ try {
+ DocumentProcessingResult initialized = fixture.support.blue.initializeDocument(
+ fixture.support.yamlResource(PAYNOTE_RESOURCE));
+ assertSuccess(initialized);
+ BexProcessingMetrics.Snapshot baseline = fixture.metrics.snapshot();
+
+ Node event = fixture.support.operationRequest(
+ "hotel-participant",
+ 1_700_000_100,
+ "hotelResaleOrderPlaced",
+ "hotelParticipantChannel",
+ subscriptionUpdate());
+ DocumentProcessingResult result = fixture.support.blue.processDocument(
+ initialized.snapshot(), event);
+
+ assertSuccess(result);
+ assertEquals(Boolean.TRUE,
+ result.document().get("/orders/package-order-a/hotelOrder/resalePlaced"));
+ return LanguageRc15MetricsArtifactWriter.capture(
+ "paynote-resale-fixture",
+ "paynote-fixture",
+ "classpath:" + PAYNOTE_RESOURCE,
+ result,
+ fixture.metrics,
+ baseline);
+ } finally {
+ fixture.close();
+ }
+ }
+
+ private static LanguageRc15MetricsArtifactWriter.Scenario mandateFixtureScenario() {
+ OwnedScenario fixture = new OwnedScenario();
+ try {
+ Node mandate = mandateDocument();
+ mandate.blue(fixture.support.repository.typeAliasBlue());
+ Node aliasesResolved = new RepositoryTypeAliasPreprocessor(
+ fixture.support.repository).preprocess(mandate);
+ ResolvedSnapshot resolved = fixture.support.blue.resolveToSnapshot(
+ fixture.support.blue.preprocess(aliasesResolved));
+ DocumentProcessingResult initialized =
+ fixture.support.blue.initializeDocument(resolved);
+ assertSuccess(initialized);
+ assertEquals(StatusPending.blueId(),
+ initialized.canonicalDocument().getAsText("/status/type/blueId"));
+ BexProcessingMetrics.Snapshot baseline = fixture.metrics.snapshot();
+
+ Node event = TestTimelineProvider.timelineEntry(
+ fixture.support.blue,
+ fixture.support.repository,
+ "guarantor",
+ "guarantor",
+ BigInteger.valueOf(7_000_001L),
+ CoordinationTestResources.operationRequest(
+ "confirmMandateAuthority",
+ "mandateGuarantorChannel",
+ new Node()));
+ DocumentProcessingResult result = fixture.support.blue.processDocument(
+ initialized.snapshot(), event);
+
+ assertSuccess(result);
+ assertEquals(BigInteger.valueOf(7_000_001L),
+ result.document().get("/authorityConfirmedAt"));
+ return LanguageRc15MetricsArtifactWriter.capture(
+ "mandate-authority-confirmation",
+ "mandate-fixture",
+ "generated Mandate authority-confirmation lifecycle fixture",
+ result,
+ fixture.metrics,
+ baseline);
+ } finally {
+ fixture.close();
+ }
+ }
+
+ private static Node subscriptionUpdate() {
+ return new Node()
+ .type("Sample/Subscription Update")
+ .properties("subscriptionId", new Node().value("hotel-resale-agreement"))
+ .properties("targetSessionId", new Node().value("hotel-agreement-session"))
+ .properties("update", new Node()
+ .properties("kind", new Node().value("Resale Order Placed"))
+ .properties("inResponseTo", new Node()
+ .properties("requestId", new Node().value("hotel-request-a")))
+ .properties("orderSessionId", new Node().value("hotel-order-session-a")));
+ }
+
+ private static Node mandateDocument() {
+ return new Node()
+ .name("Metrics artifact mandate")
+ .type(Mandate.qualifiedName())
+ .properties("activateOnAuthorityConfirmation", new Node().value(false))
+ .properties("contracts", new Node()
+ .properties("mandateGuarantorChannel",
+ TestTimelineProvider.channel("guarantor"))
+ .properties("authorityHolderChannel",
+ TestTimelineProvider.channel("holder"))
+ .properties("authorizedActorChannel",
+ TestTimelineProvider.channel("authorized")));
+ }
+
+ private static long metric(BexProcessingMetrics metrics, String name) {
+ Long value = metrics.languageCounters().get(name);
+ return value != null ? value.longValue() : 0L;
+ }
+
+ private static void assertSuccess(DocumentProcessingResult result) {
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertNotNull(result.snapshot());
+ assertNotNull(result.blueId());
+ }
+
+ private static void assertJsonScenarios(Path json) throws IOException {
+ JsonNode root = new ObjectMapper().readTree(Files.newInputStream(json));
+ assertEquals(2, root.path("schemaVersion").asInt());
+ assertEquals("after-scenario-before-runtime-and-runner-close",
+ root.path("capturePhase").asText());
+ assertEquals("runtime-construction-through-scenario",
+ root.path("cumulativeMetricScope").asText());
+ assertEquals("after-document-initialization-before-scenario",
+ root.path("proofCounterDeltaBaseline").asText());
+ assertTrue(root.path("coordinationElapsedTimingMetricsExcluded").asBoolean());
+ assertTrue(root.path("genericElapsedTimingMetricsExcluded").asBoolean());
+ assertEquals(4, root.path("scenarios").size());
+
+ Set ids = new HashSet();
+ for (JsonNode scenario : root.path("scenarios")) {
+ ids.add(scenario.path("scenarioId").asText());
+ assertEquals("SUCCESS", scenario.path("result").path("status").asText());
+ assertTrue(scenario.path("result").path("totalGas").asLong() > 0L);
+ assertTrue(scenario.path("metrics").path("coordinationStrong").size() > 0);
+ assertTrue(scenario.path("metrics").path("language").path("counters").size() > 0);
+ assertTrue(scenario.path("metrics").path("language").path("gauges").size() > 0);
+ assertTrue(scenario.path("metrics").path("language")
+ .path("highWaterMarks").size() > 0);
+ assertNoTimingMetrics(scenario.path("metrics").path("language").path("counters"));
+ assertNoTimingMetrics(scenario.path("metrics").path("language").path("gauges"));
+ assertNoTimingMetrics(scenario.path("metrics").path("language")
+ .path("highWaterMarks"));
+
+ JsonNode proof = scenario.path("metrics")
+ .path("proofCounterDeltasSinceInitialization");
+ assertEquals(6, proof.size());
+ assertEquals(expectedPatches(scenario.path("scenarioId").asText()),
+ proof.path("frozenPatchesHandedToLanguage").asLong());
+ assertTrue(proof.path("frozenPatchValuesHandedToLanguage").asLong() > 0L);
+ assertEquals(proof.path("frozenPatchValuesHandedToLanguage").asLong(),
+ proof.path("frozenPatchValuesAccepted").asLong());
+ assertEquals(0L, proof.path("mutablePatchesHandedToLanguage").asLong());
+ assertEquals(0L, proof.path("mutablePatchValuesFrozen").asLong());
+ assertEquals(0L, proof.path("frozenPatchValuesMaterialized").asLong());
+ }
+ assertEquals(new HashSet(Arrays.asList(
+ "static-update-document",
+ "multi-patch-compute",
+ "paynote-resale-fixture",
+ "mandate-authority-confirmation")), ids);
+ }
+
+ private static void assertCsvScenarios(Path csv) throws IOException {
+ String content = new String(Files.readAllBytes(csv), StandardCharsets.UTF_8);
+ assertTrue(content.startsWith("scenario_id,scenario_kind,fixture,status"));
+ assertTrue(content.contains("\"static-update-document\""));
+ assertTrue(content.contains("\"multi-patch-compute\""));
+ assertTrue(content.contains("\"paynote-resale-fixture\""));
+ assertTrue(content.contains("\"mandate-authority-confirmation\""));
+ assertTrue(content.contains("\"coordination-cumulative\",\"strong\""));
+ assertTrue(content.contains("\"language-cumulative\",\"counter\""));
+ assertTrue(content.contains("\"language-cumulative\",\"gauge\""));
+ assertTrue(content.contains("\"language-cumulative\",\"high_water\""));
+ assertTrue(content.contains("\"workflow-since-initialization\",\"counter_delta\","
+ + "\"frozenPatchValuesMaterialized\",\"0\""));
+ assertFalse(content.contains("Nanos"));
+ }
+
+ private static void assertNoTimingMetrics(JsonNode metrics) {
+ Iterator names = metrics.fieldNames();
+ while (names.hasNext()) {
+ assertFalse(names.next().endsWith("Nanos"));
+ }
+ }
+
+ private static long expectedPatches(String scenarioId) {
+ if ("static-update-document".equals(scenarioId)) {
+ return 2L;
+ }
+ if ("multi-patch-compute".equals(scenarioId)) {
+ return 3L;
+ }
+ if ("paynote-resale-fixture".equals(scenarioId)) {
+ return 6L;
+ }
+ if ("mandate-authority-confirmation".equals(scenarioId)) {
+ return 2L;
+ }
+ throw new AssertionError("Unexpected scenario id: " + scenarioId);
+ }
+
+ private static final class OwnedScenario implements AutoCloseable {
+ private final BexProcessingMetrics metrics = new BexProcessingMetrics();
+ private final BexEngine engine = BexEngine.builder().build();
+ private final SequentialWorkflowRunner runner = SequentialWorkflowRunner.withBexEngine(
+ engine, 100_000L, metrics);
+ private final ComputeWorkflowTestSupport support = ComputeWorkflowTestSupport.create(
+ CoordinationProcessorOptions.builder()
+ .bexEngine(engine)
+ .sequentialWorkflowRunner(runner)
+ .defaultComputeGasLimit(100_000L)
+ .processingMetrics(metrics)
+ .build());
+
+ @Override
+ public void close() {
+ try {
+ support.blue.close();
+ } finally {
+ runner.close();
+ }
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactWriter.java b/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactWriter.java
new file mode 100644
index 0000000..f100ff7
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/compute/LanguageRc15MetricsArtifactWriter.java
@@ -0,0 +1,342 @@
+package blue.coordination.processor.compute;
+
+import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.processor.DocumentProcessingResult;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** Deterministic JSON/CSV serialization for representative rc15-adoption metrics. */
+final class LanguageRc15MetricsArtifactWriter {
+ static final String JSON_FILE_NAME = "scenario-metrics.json";
+ static final String CSV_FILE_NAME = "scenario-metrics.csv";
+ private static final String CAPTURE_PHASE =
+ "after-scenario-before-runtime-and-runner-close";
+ private static final String CUMULATIVE_METRIC_SCOPE =
+ "runtime-construction-through-scenario";
+ private static final String PROOF_COUNTER_DELTA_BASELINE =
+ "after-document-initialization-before-scenario";
+ private static final String[] REQUIRED_PROOF_COUNTERS = {
+ "frozenPatchesHandedToLanguage",
+ "frozenPatchValuesHandedToLanguage",
+ "mutablePatchesHandedToLanguage",
+ "frozenPatchValuesAccepted",
+ "mutablePatchValuesFrozen",
+ "frozenPatchValuesMaterialized"
+ };
+
+ private LanguageRc15MetricsArtifactWriter() {
+ }
+
+ static Scenario capture(String scenarioId,
+ String scenarioKind,
+ String fixture,
+ DocumentProcessingResult result,
+ BexProcessingMetrics metrics,
+ BexProcessingMetrics.Snapshot postInitializationBaseline) {
+ if (result == null) {
+ throw new IllegalArgumentException("result must not be null");
+ }
+ if (metrics == null) {
+ throw new IllegalArgumentException("metrics must not be null");
+ }
+ if (postInitializationBaseline == null) {
+ throw new IllegalArgumentException("postInitializationBaseline must not be null");
+ }
+
+ BexProcessingMetrics.Snapshot snapshot = metrics.snapshot();
+ return new Scenario(scenarioId,
+ scenarioKind,
+ fixture,
+ result.status().name(),
+ result.errorCategory() != null ? result.errorCategory().name() : null,
+ result.failureReason(),
+ result.totalGas(),
+ result.triggeredEvents().size(),
+ result.blueId(),
+ strongMetrics(snapshot),
+ deterministicGenericMetrics(snapshot.languageCounters),
+ deterministicGenericMetrics(snapshot.languageGauges),
+ deterministicGenericMetrics(snapshot.languageHighWaterMarks),
+ proofCounterDeltas(postInitializationBaseline, snapshot));
+ }
+
+ static void write(Path reportDirectory, List scenarios) throws IOException {
+ if (reportDirectory == null) {
+ throw new IllegalArgumentException("reportDirectory must not be null");
+ }
+ if (scenarios == null || scenarios.isEmpty()) {
+ throw new IllegalArgumentException("scenarios must not be empty");
+ }
+
+ List ordered = new ArrayList(scenarios);
+ Collections.sort(ordered, new Comparator() {
+ @Override
+ public int compare(Scenario left, Scenario right) {
+ return left.scenarioId.compareTo(right.scenarioId);
+ }
+ });
+ rejectDuplicateScenarioIds(ordered);
+
+ Files.createDirectories(reportDirectory);
+ writeAtomically(reportDirectory.resolve(JSON_FILE_NAME), json(ordered));
+ writeAtomically(reportDirectory.resolve(CSV_FILE_NAME), csv(ordered));
+ }
+
+ private static Map strongMetrics(BexProcessingMetrics.Snapshot snapshot) {
+ Map values = new TreeMap();
+ for (Field field : BexProcessingMetrics.Snapshot.class.getFields()) {
+ if (field.getType() != Long.TYPE
+ || !Modifier.isPublic(field.getModifiers())
+ || Modifier.isStatic(field.getModifiers())
+ || field.getName().endsWith("Nanos")) {
+ continue;
+ }
+ try {
+ values.put(field.getName(), Long.valueOf(field.getLong(snapshot)));
+ } catch (IllegalAccessException ex) {
+ throw new IllegalStateException("Cannot read metrics field " + field.getName(), ex);
+ }
+ }
+ return Collections.unmodifiableMap(values);
+ }
+
+ private static Map deterministicGenericMetrics(Map source) {
+ Map values = new TreeMap();
+ for (Map.Entry metric : source.entrySet()) {
+ if (!metric.getKey().endsWith("Nanos")) {
+ values.put(metric.getKey(), metric.getValue());
+ }
+ }
+ return Collections.unmodifiableMap(values);
+ }
+
+ private static Map proofCounterDeltas(
+ BexProcessingMetrics.Snapshot baseline,
+ BexProcessingMetrics.Snapshot current) {
+ Map values = new TreeMap();
+ for (String name : REQUIRED_PROOF_COUNTERS) {
+ long delta = metric(current.languageCounters, name)
+ - metric(baseline.languageCounters, name);
+ if (delta < 0L) {
+ throw new IllegalStateException("Counter decreased after initialization: " + name);
+ }
+ values.put(name, Long.valueOf(delta));
+ }
+ return Collections.unmodifiableMap(values);
+ }
+
+ private static long metric(Map metrics, String name) {
+ Long value = metrics.get(name);
+ return value != null ? value.longValue() : 0L;
+ }
+
+ private static byte[] json(List scenarios) throws IOException {
+ Map root = new LinkedHashMap();
+ root.put("schemaVersion", Integer.valueOf(2));
+ root.put("capturePhase", CAPTURE_PHASE);
+ root.put("cumulativeMetricScope", CUMULATIVE_METRIC_SCOPE);
+ root.put("proofCounterDeltaBaseline", PROOF_COUNTER_DELTA_BASELINE);
+ root.put("coordinationElapsedTimingMetricsExcluded", Boolean.TRUE);
+ root.put("genericElapsedTimingMetricsExcluded", Boolean.TRUE);
+
+ List