mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
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.
This commit is contained in:
1 parent
0d1b2cf4e8
commit
11fca22d36
3 files changed
+455
No files matched your search
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user