Add deterministic seeds, debug tracing for assignRows cycle detection
This commit is contained in:
@@ -28,7 +28,7 @@ func main() {
|
|||||||
runAndExit = strings.Fields(*runAndExitStr)
|
runAndExit = strings.Fields(*runAndExitStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
show := GenerateMockShow(5, 20, 4, 5)
|
show := GenerateMockShow(42, 5, 20, 4, 5)
|
||||||
if err := show.Validate(); err != nil {
|
if err := show.Validate(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error validating show: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error validating show: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ type mockShowGen struct {
|
|||||||
chainFrom []chainable
|
chainFrom []chainable
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateMockShow(numTracks, numScenes, avgCuesPerScene, avgBlocksPerCue int) *Show {
|
func GenerateMockShow(seed uint64, numTracks, numScenes, avgCuesPerScene, avgBlocksPerCue int) *Show {
|
||||||
g := &mockShowGen{
|
g := &mockShowGen{
|
||||||
rng: rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64())),
|
rng: rand.New(rand.NewPCG(seed, seed)),
|
||||||
show: &Show{},
|
show: &Show{},
|
||||||
numTracks: numTracks,
|
numTracks: numTracks,
|
||||||
triggerIdx: map[TriggerSource]*Trigger{},
|
triggerIdx: map[TriggerSource]*Trigger{},
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cueTrackID = "_cue"
|
const cueTrackID = "_cue"
|
||||||
@@ -22,6 +24,7 @@ type Timeline struct {
|
|||||||
cellIdx map[cellKey]*TimelineCell `json:"-"`
|
cellIdx map[cellKey]*TimelineCell `json:"-"`
|
||||||
constraints []constraint `json:"-"`
|
constraints []constraint `json:"-"`
|
||||||
exclusives []exclusiveGroup `json:"-"`
|
exclusives []exclusiveGroup `json:"-"`
|
||||||
|
debugW io.Writer `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CellType string
|
type CellType string
|
||||||
@@ -123,17 +126,64 @@ func (g exclusiveGroup) String() string {
|
|||||||
return s + ")"
|
return s + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) debugf(format string, args ...any) {
|
||||||
|
if tl.debugW != nil {
|
||||||
|
fmt.Fprintf(tl.debugW, format+"\n", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) debugState() {
|
||||||
|
if tl.debugW == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tl.debugW, "=== state ===\n")
|
||||||
|
for _, t := range tl.Tracks {
|
||||||
|
var parts []string
|
||||||
|
for _, c := range t.Cells {
|
||||||
|
switch c.Type {
|
||||||
|
case CellEvent, CellSignal:
|
||||||
|
parts = append(parts, fmt.Sprintf("r%d:%s/%s", c.row, c.BlockID, c.Event))
|
||||||
|
case CellTitle:
|
||||||
|
parts = append(parts, fmt.Sprintf("r%d:title(%s)", c.row, c.BlockID))
|
||||||
|
default:
|
||||||
|
parts = append(parts, fmt.Sprintf("r%d:%s", c.row, c.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tl.debugW, " %s: [%s]\n", t.ID, strings.Join(parts, " "))
|
||||||
|
}
|
||||||
|
for _, c := range tl.constraints {
|
||||||
|
sat := "OK"
|
||||||
|
if !c.satisfied() {
|
||||||
|
sat = "UNSATISFIED"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tl.debugW, " constraint %s %s\n", c, sat)
|
||||||
|
}
|
||||||
|
for _, g := range tl.exclusives {
|
||||||
|
sat := "OK"
|
||||||
|
if !g.satisfied(tl.Tracks) {
|
||||||
|
sat = "UNSATISFIED"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tl.debugW, " exclusive %s %s\n", g, sat)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tl.debugW, "=============\n")
|
||||||
|
}
|
||||||
|
|
||||||
type cellKey struct {
|
type cellKey struct {
|
||||||
blockID string
|
blockID string
|
||||||
event string
|
event string
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildTimeline(show *Show) (Timeline, error) {
|
func BuildTimeline(show *Show) (Timeline, error) {
|
||||||
|
return BuildTimelineDebug(show, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildTimelineDebug(show *Show, debugW io.Writer) (Timeline, error) {
|
||||||
tl := Timeline{
|
tl := Timeline{
|
||||||
show: show,
|
show: show,
|
||||||
Blocks: map[string]*Block{},
|
Blocks: map[string]*Block{},
|
||||||
trackIdx: map[string]*TimelineTrack{},
|
trackIdx: map[string]*TimelineTrack{},
|
||||||
cellIdx: map[cellKey]*TimelineCell{},
|
cellIdx: map[cellKey]*TimelineCell{},
|
||||||
|
debugW: debugW,
|
||||||
}
|
}
|
||||||
|
|
||||||
cueTrack := &TimelineTrack{Track: &Track{ID: cueTrackID, Name: "Cue"}}
|
cueTrack := &TimelineTrack{Track: &Track{ID: cueTrackID, Name: "Cue"}}
|
||||||
@@ -176,21 +226,16 @@ func BuildTimeline(show *Show) (Timeline, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) sortBlocks() []*Block {
|
func (tl *Timeline) sortBlocks() []*Block {
|
||||||
for i, b := range tl.show.Blocks {
|
for _, b := range tl.show.Blocks {
|
||||||
b.weight = uint64(i) << 32
|
b.weight = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := true
|
changed := true
|
||||||
for changed {
|
for changed {
|
||||||
changed = false
|
changed = false
|
||||||
for _, t := range tl.show.Triggers {
|
for i, b := range tl.show.Blocks {
|
||||||
src := tl.Blocks[t.Source.Block]
|
if b.Type == "cue" {
|
||||||
for _, target := range t.Targets {
|
changed = tl.setWeightRecursive(b, uint64(i+1)<<32) || changed
|
||||||
dst := tl.Blocks[target.Block]
|
|
||||||
if dst.weight <= src.weight {
|
|
||||||
dst.weight = src.weight + 1
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +247,35 @@ func (tl *Timeline) sortBlocks() []*Block {
|
|||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) setWeight(b *Block, weight uint64) bool {
|
||||||
|
if weight <= b.weight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b.weight = weight
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) setWeightRecursive(b *Block, weight uint64) bool {
|
||||||
|
changed := tl.setWeight(b, weight)
|
||||||
|
|
||||||
|
for _, t := range tl.show.Triggers {
|
||||||
|
// TODO: needs a lookup table
|
||||||
|
if t.Source.Block != b.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range t.Targets {
|
||||||
|
trg := tl.Blocks[target.Block]
|
||||||
|
changed = tl.setWeightRecursive(trg, b.weight+1) || changed
|
||||||
|
if trg.Track == b.Track {
|
||||||
|
changed = tl.setWeight(b, trg.weight-1) || changed
|
||||||
|
}
|
||||||
|
// TODO: needs to go to other targets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
func (tl *Timeline) addConstraint(kind constraintKind, a, b *TimelineCell) {
|
func (tl *Timeline) addConstraint(kind constraintKind, a, b *TimelineCell) {
|
||||||
tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b})
|
tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b})
|
||||||
}
|
}
|
||||||
@@ -293,13 +367,15 @@ func (tl *Timeline) buildConstraints() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) assignRows() error {
|
func (tl *Timeline) assignRows() error {
|
||||||
for range 1000000 {
|
tl.debugState()
|
||||||
if tl.enforceConstraints() {
|
for i := range 1000000 {
|
||||||
|
if tl.enforceConstraints(i) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tl.enforceExclusives() {
|
if tl.enforceExclusives(i) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
tl.debugf("converged after %d iterations", i)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, c := range tl.constraints {
|
for _, c := range tl.constraints {
|
||||||
@@ -315,7 +391,7 @@ func (tl *Timeline) assignRows() error {
|
|||||||
return fmt.Errorf("assignRows: did not converge")
|
return fmt.Errorf("assignRows: did not converge")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) enforceConstraints() bool {
|
func (tl *Timeline) enforceConstraints(iter int) bool {
|
||||||
for _, c := range tl.constraints {
|
for _, c := range tl.constraints {
|
||||||
if c.satisfied() {
|
if c.satisfied() {
|
||||||
continue
|
continue
|
||||||
@@ -323,8 +399,10 @@ func (tl *Timeline) enforceConstraints() bool {
|
|||||||
switch c.kind {
|
switch c.kind {
|
||||||
case constraintSameRow:
|
case constraintSameRow:
|
||||||
if c.a.row < c.b.row {
|
if c.a.row < c.b.row {
|
||||||
|
tl.debugf("iter %d: constraint %s: insert gap on %s before r%d", iter, c, c.a.track.ID, c.a.row)
|
||||||
tl.insertGap(c.a.track, c.a.row)
|
tl.insertGap(c.a.track, c.a.row)
|
||||||
} else {
|
} else {
|
||||||
|
tl.debugf("iter %d: constraint %s: insert gap on %s before r%d", iter, c, c.b.track.ID, c.b.row)
|
||||||
tl.insertGap(c.b.track, c.b.row)
|
tl.insertGap(c.b.track, c.b.row)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -335,19 +413,22 @@ func (tl *Timeline) enforceConstraints() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) enforceExclusives() bool {
|
func (tl *Timeline) enforceExclusives(iter int) bool {
|
||||||
for _, g := range tl.exclusives {
|
for _, g := range tl.exclusives {
|
||||||
if g.satisfied(tl.Tracks) {
|
if g.satisfied(tl.Tracks) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row := g.members[0].row
|
row := g.members[0].row
|
||||||
|
tl.debugf("iter %d: exclusive %s: split at r%d", iter, g, row)
|
||||||
for _, t := range tl.Tracks {
|
for _, t := range tl.Tracks {
|
||||||
if row >= len(t.Cells) {
|
if row >= len(t.Cells) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if g.memberTracks[t] {
|
if g.memberTracks[t] {
|
||||||
|
tl.debugf(" member %s: insertGapInt before r%d", t.ID, row)
|
||||||
tl.insertGapInt(t, row)
|
tl.insertGapInt(t, row)
|
||||||
} else {
|
} else {
|
||||||
|
tl.debugf(" non-member %s: insertGapInt before r%d", t.ID, row+1)
|
||||||
tl.insertGapInt(t, row+1)
|
tl.insertGapInt(t, row+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,6 +470,7 @@ func (tl *Timeline) reindexRowsFrom(track *TimelineTrack, start int) {
|
|||||||
|
|
||||||
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
||||||
if tl.isAllRemovableGapRow(beforeIndex, track) {
|
if tl.isAllRemovableGapRow(beforeIndex, track) {
|
||||||
|
tl.debugf(" insertGap(%s, r%d): removable row, removing from other tracks", track.ID, beforeIndex)
|
||||||
for _, t := range tl.Tracks {
|
for _, t := range tl.Tracks {
|
||||||
if t == track {
|
if t == track {
|
||||||
continue
|
continue
|
||||||
@@ -396,10 +478,12 @@ func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
|||||||
if beforeIndex >= len(t.Cells) {
|
if beforeIndex >= len(t.Cells) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
tl.debugf(" removeGap %s r%d (%s)", t.ID, beforeIndex, t.Cells[beforeIndex].Type)
|
||||||
tl.removeGapAt(t, beforeIndex)
|
tl.removeGapAt(t, beforeIndex)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
tl.debugf(" insertGap(%s, r%d): inserting", track.ID, beforeIndex)
|
||||||
tl.insertGapInt(track, beforeIndex)
|
tl.insertGapInt(track, beforeIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildTimelineFromMockShow(t *testing.T) {
|
func TestBuildTimelineFromMockShow(t *testing.T) {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
show := GenerateMockShow(5, 20, 4, 5)
|
show := GenerateMockShow(rand.Uint64(), 5, 20, 4, 5)
|
||||||
t.Logf("GenerateMockShow: %v (%d blocks, %d triggers)", time.Since(t0), len(show.Blocks), len(show.Triggers))
|
t.Logf("GenerateMockShow: %v (%d blocks, %d triggers)", time.Since(t0), len(show.Blocks), len(show.Triggers))
|
||||||
|
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
@@ -25,8 +27,19 @@ func TestBuildTimelineFromMockShow(t *testing.T) {
|
|||||||
t.Logf("tracks=%d blocks=%d", len(tl.Tracks), len(tl.Blocks))
|
t.Logf("tracks=%d blocks=%d", len(tl.Tracks), len(tl.Blocks))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildTimelineSeed11(t *testing.T) {
|
||||||
|
show := GenerateMockShow(11, 5, 20, 4, 5)
|
||||||
|
if err := show.Validate(); err != nil {
|
||||||
|
t.Fatalf("validate: %v", err)
|
||||||
|
}
|
||||||
|
_, err := BuildTimelineDebug(show, os.Stderr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildTimeline failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkBuildTimeline(b *testing.B) {
|
func BenchmarkBuildTimeline(b *testing.B) {
|
||||||
show := GenerateMockShow(5, 20, 4, 5)
|
show := GenerateMockShow(42, 5, 20, 4, 5)
|
||||||
if err := show.Validate(); err != nil {
|
if err := show.Validate(); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -36,33 +49,24 @@ func BenchmarkBuildTimeline(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimelineOrderDependency(t *testing.T) {
|
func TestTimelineShuffle(t *testing.T) {
|
||||||
show := &Show{
|
show := GenerateMockShow(rand.Uint64(), 5, 20, 4, 5)
|
||||||
Tracks: []*Track{
|
|
||||||
{ID: "T1", Name: "Track 1"},
|
var cues []*Block
|
||||||
{ID: "T2", Name: "Track 2"},
|
var others []*Block
|
||||||
},
|
for _, b := range show.Blocks {
|
||||||
Blocks: []*Block{
|
if b.Type == "cue" {
|
||||||
{ID: "B", Type: "media", Track: "T1", Name: "Block B"},
|
cues = append(cues, b)
|
||||||
{ID: "A", Type: "media", Track: "T1", Name: "Block A"},
|
} else {
|
||||||
{ID: "C", Type: "media", Track: "T2", Name: "Block C"},
|
others = append(others, b)
|
||||||
{ID: "C1", Type: "cue", Name: "Cue 1"},
|
|
||||||
},
|
|
||||||
Triggers: []*Trigger{
|
|
||||||
{
|
|
||||||
Source: TriggerSource{Block: "C1", Signal: "GO"},
|
|
||||||
Targets: []TriggerTarget{{Block: "A", Hook: "START"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Source: TriggerSource{Block: "A", Signal: "END"},
|
|
||||||
Targets: []TriggerTarget{{Block: "C", Hook: "START"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Source: TriggerSource{Block: "C", Signal: "END"},
|
|
||||||
Targets: []TriggerTarget{{Block: "B", Hook: "START"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Shuffle(len(others), func(i, j int) {
|
||||||
|
others[i], others[j] = others[j], others[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
show.Blocks = append(cues, others...)
|
||||||
|
|
||||||
if err := show.Validate(); err != nil {
|
if err := show.Validate(); err != nil {
|
||||||
t.Fatalf("Validate failed: %v", err)
|
t.Fatalf("Validate failed: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user