Skip to content

Commit 8cf43ba

Browse files
authored
Emit standard fuzz observation artifacts (#1586)
Co-authored-by: Chris Huber <chris.huber@a8c.com>
1 parent 50d8f9d commit 8cf43ba

3 files changed

Lines changed: 240 additions & 12 deletions

File tree

packages/cli/src/commands/wordpress-runtime.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function runFuzzSuiteCommand(args: string[]): Promise<number> {
2828
}
2929

3030
const options = await parsePublicRuntimeCommandOptions(args)
31-
if (options.runnerMode === "runtime-backed" && !options.dryRun) {
31+
if (options.runnerMode === "runtime-backed" && !options.dryRun && !fuzzSuiteRequiresRecipeRuntime(options.input)) {
3232
await runRuntimeBackedFuzzSuiteCommand(options)
3333
return 0
3434
}
@@ -118,6 +118,11 @@ function runtimeBackedFuzzSuitePolicy(suite: FuzzSuiteContract): RuntimePolicy {
118118
}
119119
}
120120

121+
function fuzzSuiteRequiresRecipeRuntime(input: Record<string, unknown>): boolean {
122+
const requirements = fuzzSuiteRuntimeRequirements(input)
123+
return arrayOption(requirements?.extra_plugins).length > 0 || arrayOption(requirements?.component_contracts).length > 0
124+
}
125+
121126
function runtimeBackedFuzzSuiteCommands(suite: FuzzSuiteContract): string[] {
122127
const commands = new Set(RUNTIME_BACKED_FUZZ_SUITE_RUNNER_CAPABILITIES.commands ?? [])
123128
const addTargetCommand = (target: unknown): void => {

packages/runtime-playground/src/public.ts

Lines changed: 203 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createHash } from "node:crypto"
2+
import { benchRunCode } from "./bench-command-handlers.js"
23

34
import {
45
WORDPRESS_HOTSPOTS_SCHEMA,
@@ -314,7 +315,7 @@ async function executeRollbackSafeRestMutation(
314315
export function createWordPressFuzzSuiteRuntimeWorkloadExecutor(episode: Pick<RuntimeEpisode, "step">): FuzzSuiteRuntimeWorkloadExecutor {
315316
return {
316317
async executeRuntimeWorkload({ workload, case: fuzzCase }) {
317-
const steps = [...workloadSteps(workload.before, workload), ...workloadSteps(workload.steps, workload), ...workloadSteps(workload.after, workload)]
318+
const steps = [...workloadSteps(workload.before, workload, fuzzCase), ...workloadSteps(workload.steps, workload, fuzzCase), ...workloadSteps(workload.after, workload, fuzzCase)]
318319
const startedAt = new Date().toISOString()
319320
const executions: ExecutionResult[] = []
320321
for (const step of steps) {
@@ -424,17 +425,17 @@ export function executeWordPressFuzzSuite(
424425
},
425426
runtimeActionExecutor: async (input) => {
426427
const observation = await runtimeActionExecutor.executeRuntimeAction(input)
427-
pushHotspotObservation(hotspotObservations, performanceObservationFromRuntimeAction(observation), runtimeActionArtifactRefs(observation))
428+
pushHotspotObservation(hotspotObservations, performanceObservationFromRuntimeAction(observation), runtimeActionArtifactRefs(observation), { caseId: input.case.id, targetId: input.target.id ?? input.target.entrypoint, phase: input.action.type })
428429
return observation
429430
},
430431
runtimeWorkloadExecutor: async (input) => {
431432
const execution = await runtimeWorkloadExecutor.executeRuntimeWorkload(input)
432433
const observations = performanceObservationsFromWorkloadExecution(execution)
433434
if (observations.length === 0) {
434-
pushHotspotObservation(hotspotObservations, performanceObservationFromExecution({ command: execution.command, args: execution.args }, execution), executionArtifactRefs(execution))
435+
pushHotspotObservation(hotspotObservations, performanceObservationFromExecution({ command: execution.command, args: execution.args }, execution), executionArtifactRefs(execution), { caseId: input.case.id, targetId: input.target.id ?? input.target.entrypoint, phase: input.target.entrypoint ?? input.target.kind })
435436
} else {
436437
for (const observation of observations) {
437-
pushHotspotObservation(hotspotObservations, observation, executionArtifactRefs(execution))
438+
pushHotspotObservation(hotspotObservations, observation, executionArtifactRefs(execution), { caseId: input.case.id, targetId: input.target.id ?? input.target.entrypoint, phase: input.target.entrypoint ?? input.target.kind })
438439
}
439440
}
440441
return execution
@@ -458,7 +459,11 @@ function resultWithWordPressHotspotsArtifact(result: FuzzSuiteResultEnvelope, ob
458459
artifactRefs: result.artifactRefs,
459460
metadata: { suiteId: result.suite.id, runnerMode: result.metadata?.runnerMode },
460461
})
462+
const homeboyObservationSet = homeboyFuzzObservationSetArtifact(result, observations)
463+
const homeboyHotspotSet = homeboyFuzzHotspotSetArtifact(result, homeboyObservationSet.observations)
461464
const content = `${JSON.stringify(artifact, null, 2)}\n`
465+
const homeboyObservationContent = `${JSON.stringify(homeboyObservationSet, null, 2)}\n`
466+
const homeboyHotspotContent = `${JSON.stringify(homeboyHotspotSet, null, 2)}\n`
462467
const ref: FuzzSuiteArtifactRef = {
463468
path: "files/wordpress-hotspots.json",
464469
kind: "wordpress-hotspots",
@@ -468,28 +473,151 @@ function resultWithWordPressHotspotsArtifact(result: FuzzSuiteResultEnvelope, ob
468473
name: "wordpress-hotspots",
469474
metadata: { schema: WORDPRESS_HOTSPOTS_SCHEMA, source: "executeWordPressFuzzSuite" },
470475
}
476+
const homeboyObservationRef: FuzzSuiteArtifactRef = homeboyArtifactRef("files/fuzz-observations.json", "fuzz-observation-set", "homeboy/fuzz-observation-set/v1", homeboyObservationContent)
477+
const homeboyHotspotRef: FuzzSuiteArtifactRef = homeboyArtifactRef("files/fuzz-hotspots.json", "fuzz-hotspot-set", "homeboy/fuzz-hotspot-set/v1", homeboyHotspotContent)
471478

472479
return {
473480
...result,
474-
artifactRefs: dedupeFuzzSuiteArtifactRefs([...result.artifactRefs, ref]),
481+
artifactRefs: dedupeFuzzSuiteArtifactRefs([...result.artifactRefs, ref, homeboyObservationRef, homeboyHotspotRef]),
475482
metadata: {
476483
...result.metadata,
477484
artifacts: {
478485
...(recordValue(result.metadata?.artifacts) ?? {}),
479486
wordpressHotspots: artifact,
487+
fuzzObservationSet: homeboyObservationSet,
488+
fuzzHotspotSet: homeboyHotspotSet,
480489
},
481490
},
482491
}
483492
}
484493

485-
function pushHotspotObservation(out: WordPressHotspotObservationInput[], observation: PerformanceObservation | undefined, artifactRefs: FuzzSuiteArtifactRef[] = []): void {
494+
function pushHotspotObservation(out: WordPressHotspotObservationInput[], observation: PerformanceObservation | undefined, artifactRefs: FuzzSuiteArtifactRef[] = [], metadata: Record<string, unknown> = {}): void {
486495
if (!observation) return
487496
out.push({
488497
observation,
489498
artifactRefs: artifactRefs.map((ref) => ({ path: ref.path, kind: ref.kind, contentType: ref.contentType, sha256: ref.sha256, bytes: ref.bytes, name: ref.name, metadata: ref.metadata })),
499+
metadata,
490500
})
491501
}
492502

503+
function homeboyArtifactRef(path: string, kind: string, schema: string, content: string): FuzzSuiteArtifactRef {
504+
return {
505+
path,
506+
kind,
507+
contentType: "application/json",
508+
sha256: createHash("sha256").update(content).digest("hex"),
509+
bytes: Buffer.byteLength(content),
510+
name: kind,
511+
metadata: { schema, source: "executeWordPressFuzzSuite" },
512+
}
513+
}
514+
515+
function homeboyFuzzObservationSetArtifact(result: FuzzSuiteResultEnvelope, observations: WordPressHotspotObservationInput[]): { schema: string; generated_at: string; source: string; observations: Array<Record<string, unknown>>; summary: Record<string, unknown>; metadata: Record<string, unknown> } {
516+
const flattened = observations.flatMap((input) => homeboyFuzzObservations(input))
517+
return {
518+
schema: "homeboy/fuzz-observation-set/v1",
519+
generated_at: new Date().toISOString(),
520+
source: "wp-codebox",
521+
observations: flattened,
522+
summary: {
523+
total: flattened.length,
524+
families: countByString(flattened, "family"),
525+
metrics: countByString(flattened, "metric"),
526+
},
527+
metadata: { suite_id: result.suite.id, runner: "wp-codebox", runner_mode: result.metadata?.runnerMode },
528+
}
529+
}
530+
531+
function homeboyFuzzHotspotSetArtifact(result: FuzzSuiteResultEnvelope, observations: Array<Record<string, unknown>>): { schema: string; generated_at: string; source: string; hotspots: Array<Record<string, unknown>>; summary: Record<string, unknown>; metadata: Record<string, unknown> } {
532+
const grouped = new Map<string, Record<string, unknown>[]>()
533+
for (const observation of observations) {
534+
const key = [stringValue(observation.case_id), stringValue(observation.target_id), stringValue(observation.subject), stringValue(observation.metric)].join("|")
535+
grouped.set(key, [...(grouped.get(key) ?? []), observation])
536+
}
537+
const scored = [...grouped.values()].map((items) => {
538+
const first = items[0] ?? {}
539+
const value = items.reduce((sum, item) => sum + (numberValue(item.value) ?? 0), 0)
540+
return {
541+
family: first.family,
542+
case_id: first.case_id,
543+
target_id: first.target_id,
544+
operation_id: first.operation_id,
545+
phase: first.phase,
546+
subject: first.subject,
547+
metric: first.metric,
548+
value,
549+
unit: first.unit,
550+
score: homeboyHotspotScore(stringValue(first.metric), value),
551+
sample_count: items.reduce((sum, item) => sum + (numberValue(item.sample_count) ?? 1), 0),
552+
metadata: { sources: items.length },
553+
}
554+
}).sort((a, b) => b.score - a.score || String(a.subject).localeCompare(String(b.subject)))
555+
const maxScore = scored[0]?.score ?? 0
556+
const hotspots = scored.map((item, index) => ({ ...item, rank: index + 1, relative_score: maxScore > 0 ? Number((item.score / maxScore).toFixed(6)) : 0 }))
557+
return {
558+
schema: "homeboy/fuzz-hotspot-set/v1",
559+
generated_at: new Date().toISOString(),
560+
source: "wp-codebox",
561+
hotspots,
562+
summary: { total: hotspots.length, families: countByString(hotspots, "family"), max_score: maxScore },
563+
metadata: { suite_id: result.suite.id, runner: "wp-codebox", runner_mode: result.metadata?.runnerMode },
564+
}
565+
}
566+
567+
function homeboyFuzzObservations(input: WordPressHotspotObservationInput): Array<Record<string, unknown>> {
568+
const observation = input.observation
569+
const metadata = recordValue(input.metadata) ?? {}
570+
const common = {
571+
case_id: stringValue(metadata.caseId) ?? stringValue(observation.metadata?.caseId),
572+
target_id: stringValue(metadata.targetId) ?? observation.target,
573+
operation_id: observation.command,
574+
phase: stringValue(metadata.phase) ?? observation.kind,
575+
subject: observation.target ?? observation.command ?? observation.kind ?? "observation",
576+
metadata: { source: observation.source, execution_id: stringValue(observation.metadata?.executionId) },
577+
}
578+
return [
579+
homeboyFuzzObservation(common, "timing", "duration-ms", observation.timing?.durationMs, "ms"),
580+
homeboyFuzzObservation(common, "database", "query-count", observation.database?.queryCount, "count"),
581+
homeboyFuzzObservation(common, "database", "query-time-ms", observation.database?.totalTimeMs, "ms"),
582+
homeboyFuzzObservation(common, "memory", "memory-delta-bytes", observation.memory?.deltaBytes, "bytes"),
583+
homeboyFuzzObservation(common, "network", "network-failures", observation.network?.failures, "count"),
584+
...Object.entries(observation.browser?.metrics ?? {}).map(([name, value]) => homeboyFuzzObservation({ ...common, subject: `${common.subject}:${name}` }, "browser", name, value, undefined)),
585+
].filter((item): item is Record<string, unknown> => Boolean(item))
586+
}
587+
588+
function homeboyFuzzObservation(common: Record<string, unknown>, family: string, metric: string, value: unknown, unit: string | undefined): Record<string, unknown> | undefined {
589+
const number = numberValue(value)
590+
if (number === undefined || number <= 0) return undefined
591+
return {
592+
family,
593+
case_id: common.case_id,
594+
target_id: common.target_id,
595+
operation_id: common.operation_id,
596+
phase: common.phase,
597+
subject: common.subject,
598+
metric,
599+
value: number,
600+
unit,
601+
sample_count: 1,
602+
metadata: common.metadata,
603+
}
604+
}
605+
606+
function homeboyHotspotScore(metric: string | undefined, value: number): number {
607+
if (metric === "query-count") return value * 10
608+
if (metric === "network-failures") return value * 100
609+
if (metric === "memory-delta-bytes") return value / 1024 / 1024
610+
return value
611+
}
612+
613+
function countByString(items: Array<Record<string, unknown>>, key: string): Record<string, number> {
614+
return items.reduce<Record<string, number>>((summary, item) => {
615+
const value = stringValue(item[key])
616+
if (value) summary[value] = (summary[value] ?? 0) + 1
617+
return summary
618+
}, {})
619+
}
620+
493621
function performanceObservationFromRuntimeAction(observation: RuntimeActionObservation): PerformanceObservation | undefined {
494622
if (observation.performance) {
495623
const command = observation.performance.command ?? observation.step?.execution.command
@@ -536,7 +664,40 @@ function performanceObservationFromExecution(spec: Partial<ExecutionSpec>, execu
536664
function performanceObservationsFromWorkloadExecution(execution: ExecutionResult): PerformanceObservation[] {
537665
const json = recordValue(execution.result?.json) ?? parseJsonRecord(execution.stdout)
538666
const raw = arrayValue(json?.observations ?? json?.performanceObservations ?? json?.performance_observations)
539-
return raw.flatMap((item) => isPerformanceObservation(item) ? [item] : [])
667+
return [
668+
...raw.flatMap((item) => isPerformanceObservation(item) ? [item] : []),
669+
...performanceObservationsFromBenchResult(json, execution),
670+
]
671+
}
672+
673+
function performanceObservationsFromBenchResult(json: Record<string, unknown> | undefined, execution: ExecutionResult): PerformanceObservation[] {
674+
if (json?.schema !== "wp-codebox/bench-results/v1") return []
675+
const observations: PerformanceObservation[] = []
676+
for (const scenario of arrayValue(json.scenarios)) {
677+
const scenarioRecord = recordValue(scenario)
678+
const profile = recordValue(recordValue(scenarioRecord?.artifacts)?.["rest-db-query-profile"])
679+
if (profile?.schema !== "wp-codebox/wordpress-rest-db-query-profile/v1") continue
680+
for (const profileCase of arrayValue(profile.cases)) {
681+
const caseRecord = recordValue(profileCase)
682+
const summary = recordValue(caseRecord?.summary)
683+
const queryCount = numberValue(summary?.query_count)
684+
const totalTimeMs = numberValue(summary?.total_time_ms)
685+
const path = stringValue(caseRecord?.path) ?? stringValue(caseRecord?.case_id) ?? "rest-db-query-profile"
686+
const observation: PerformanceObservation = {
687+
schema: "wp-codebox/performance-observation/v1",
688+
command: "wordpress.run-workload",
689+
target: path,
690+
source: "rest-db-query-profiler",
691+
kind: "rest-request",
692+
database: queryCount !== undefined || totalTimeMs !== undefined ? { queryCount, totalTimeMs } : undefined,
693+
metadata: { executionId: execution.id, scenarioId: stringValue(scenarioRecord?.id), caseId: stringValue(caseRecord?.case_id) },
694+
}
695+
if (hasObservationMetrics(observation)) {
696+
observations.push(observation)
697+
}
698+
}
699+
}
700+
return observations
540701
}
541702

542703
function normalizeObservationDatabase(input: Record<string, unknown> | undefined): PerformanceObservation["database"] | undefined {
@@ -673,11 +834,11 @@ function numericRecord(value: Record<string, unknown>): Record<string, number> |
673834
return entries.length > 0 ? Object.fromEntries(entries) : undefined
674835
}
675836

676-
function workloadSteps(value: unknown, workload: Record<string, unknown>): Array<{ command: string; args?: string[]; timeoutMs?: number; allowFailure?: boolean; advisory?: boolean }> {
837+
function workloadSteps(value: unknown, workload: Record<string, unknown>, fuzzCase?: unknown): Array<{ command: string; args?: string[]; timeoutMs?: number; allowFailure?: boolean; advisory?: boolean }> {
677838
if (!Array.isArray(value)) {
678839
return []
679840
}
680-
return value.flatMap((step) => {
841+
const commandSteps = value.flatMap((step) => {
681842
if (!step || typeof step !== "object" || Array.isArray(step)) {
682843
return []
683844
}
@@ -696,6 +857,39 @@ function workloadSteps(value: unknown, workload: Record<string, unknown>): Array
696857
advisory: record.advisory === true,
697858
}]
698859
})
860+
if (commandSteps.length > 0) {
861+
return commandSteps
862+
}
863+
if (value.some((step) => step && typeof step === "object" && !Array.isArray(step) && typeof (step as Record<string, unknown>).type === "string")) {
864+
return [{ command: "wordpress.run-php", args: [`code=${typedWorkloadRunnerCode(workload, fuzzCase)}`] }]
865+
}
866+
return []
867+
}
868+
869+
function typedWorkloadRunnerCode(workload: Record<string, unknown>, fuzzCase?: unknown): string {
870+
return benchRunCode({
871+
componentId: stringValue(workload.componentId) ?? stringValue(workload.component_id) ?? "wordpress-workload",
872+
pluginSlug: typedWorkloadPluginSlug(workload, fuzzCase),
873+
iterations: 1,
874+
warmupIterations: 0,
875+
dependencySlugs: [],
876+
env: recordValue(workload.runtime_env) ?? recordValue(workload.runtimeEnv) ?? {},
877+
bootstrapFiles: [],
878+
workloads: [workload],
879+
lifecycle: {},
880+
resetPolicy: {},
881+
})
882+
}
883+
884+
function typedWorkloadPluginSlug(workload: Record<string, unknown>, fuzzCase?: unknown): string {
885+
const fuzzCaseRecord = recordValue(fuzzCase)
886+
const explicit = stringValue(workload.pluginSlug) ?? stringValue(workload.plugin_slug) ?? stringValue(recordValue(workload.metadata)?.plugin_slug) ?? stringValue(recordValue(fuzzCaseRecord?.metadata)?.plugin_slug)
887+
if (explicit) return explicit
888+
const metadata = recordValue(fuzzCaseRecord?.metadata)
889+
const caseMetadata = recordValue(metadata?.caseMetadata) ?? recordValue(metadata?.case_metadata) ?? metadata
890+
const activation = stringValue(recordValue(recordValue(recordValue(caseMetadata?.intent)?.plugin)?.activation)?.entrypoint) ?? stringValue(recordValue(recordValue(caseMetadata?.intent)?.plugin)?.activation)
891+
const slug = activation?.split("/")[0]?.trim()
892+
return slug || "wp-codebox-workload"
699893
}
700894

701895
function stepArgMap(args: string[] | undefined): Record<string, string> {

0 commit comments

Comments
 (0)