Files
sanderling/cmd/internal-tools/exploration-reach/reach.go
T
pj 11fca22d36 feat(exploration-reach): count the distinct structural states a stored run visited
the state is the settle path's structural hash of the recorded hierarchy, the same function the drivers wait on, so a state boundary here is the one the harness itself uses. --reference reports the observation at which two runs' hierarchies first differ. trace only: no device, no replay.
2026-08-16 17:45:35 +05:30

134 lines
4.2 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"sort"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion"
"github.com/priyanshujain/sanderling/internal/tracecorpus"
)
// observation is one hierarchy-bearing step: the index the run gave it and the
// state it observed.
type observation struct {
Step int
State string
}
// Reach is what one run explored. Distinct counts the structural states its
// observations visited, which is the measure; Observations is how many looks
// it took to visit them.
type Reach struct {
Directory string `json:"directory"`
Seed int64 `json:"seed"`
Platform string `json:"platform"`
Arm string `json:"arm,omitempty"`
Observations int `json:"observations"`
Distinct int `json:"distinct_states"`
// Unobserved counts steps carrying no hierarchy, which the run-end
// finalize record is, so an observation count cannot be read as a step
// count by accident.
Unobserved int `json:"steps_without_hierarchy"`
observations []observation
}
// measure keys each observation by a digest of its structural hash. The
// measure is equality between hashes and nothing else, and a corpus holds
// thousands of trees whose hashes run to tens of kilobytes each.
func measure(run tracecorpus.Run) Reach {
reach := Reach{
Directory: run.Directory,
Seed: run.Meta.Seed,
Platform: run.Meta.Platform,
Arm: run.Meta.Arm,
}
distinct := map[string]bool{}
for _, step := range run.Steps {
if step.Hierarchy == nil {
reach.Unobserved++
continue
}
digest := sha256.Sum256([]byte(ioscompanion.StructuralHash(step.Hierarchy)))
key := hex.EncodeToString(digest[:])
reach.observations = append(
reach.observations,
observation{Step: step.Index, State: key},
)
distinct[key] = true
}
reach.Observations = len(reach.observations)
reach.Distinct = len(distinct)
return reach
}
// corpusDistinct counts the structural states the whole corpus reached, which
// is not the sum of the per-run counts: runs of one application revisit the
// same screens.
func corpusDistinct(reaches []Reach) int {
distinct := map[string]bool{}
for _, reach := range reaches {
for _, seen := range reach.observations {
distinct[seen.State] = true
}
}
return len(distinct)
}
// Divergence is where a replay stopped observing what the reference observed.
// Step is the index of the first observation whose structural state differs;
// Diverged is false when the replay matched the reference for every
// observation the two share, in which case Step is that shared length and the
// observation is right-censored.
type Divergence struct {
Directory string `json:"directory"`
Seed int64 `json:"seed"`
Step int `json:"step"`
Diverged bool `json:"diverged"`
Compared int `json:"observations_compared"`
}
func diverge(reference, replay Reach) Divergence {
result := Divergence{Directory: replay.Directory, Seed: replay.Seed}
shared := len(reference.observations)
if len(replay.observations) < shared {
shared = len(replay.observations)
}
result.Compared = shared
for position := 0; position < shared; position++ {
if reference.observations[position].State != replay.observations[position].State {
result.Step = reference.observations[position].Step
result.Diverged = true
return result
}
}
if shared > 0 {
result.Step = reference.observations[shared-1].Step
}
return result
}
// medianDivergence is E6's number: the median observation index at which a
// replay first diverges from the reference. A replay that never diverged
// enters at the last index the two share, which is where the observation is
// censored rather than where it broke, so the count of such replays is
// reported beside the median rather than folded into it.
func medianDivergence(divergences []Divergence) (median float64, censored int) {
if len(divergences) == 0 {
return 0, 0
}
steps := make([]int, 0, len(divergences))
for _, divergence := range divergences {
steps = append(steps, divergence.Step)
if !divergence.Diverged {
censored++
}
}
sort.Ints(steps)
middle := len(steps) / 2
if len(steps)%2 == 1 {
return float64(steps[middle]), censored
}
return float64(steps[middle-1]+steps[middle]) / 2, censored
}