refactor(output): count flip merges in the presentation sink
Output queues are flushed every turn and never merge: drop the dead overflow-merge path from presented.Queue and the merged_flips stat and warning from drm and headless. Document the sink warning.
- commit
- abed7f58aa773b8e7155e9b81f463973711286f9
- author
- brice · · 2026-10-13 17:58 UTC
- parent
- c9850c5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +49 -75
M docs/performance.md +3 -2
| @@ -66,9 +66,10 @@ Every 10 s, each output logs a `stats` entry for the last interval. The main fie | |||
| 66 | 66 | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | |
| 67 | 67 | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | |
| 68 | 68 | | `redrawn_pixels` | Pixels the renderer drew | | |
| 69 | - | | `merged_flips` | Flips merged because wayland was not reading output reports. Headless outputs log `stats` too (`frames`, `merged_flips`) | | |
| 70 | 69 | ||
| 71 | - | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. | |
| 70 | + | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. Headless outputs log a `stats` entry too (`output`, `frames`). | |
| 71 | + | ||
| 72 | + | When outputs report faster than the display loop drains, wayland merges their flips and logs a `wayland` warning at most every 10 s (`merged`: flips merged since the last warning). Presentation feedback of a merged flip is answered as discarded. | |
| 72 | 73 | ||
| 73 | 74 | To find the cause of missed vblanks: | |
| 74 | 75 | ||
| @@ -66,9 +66,10 @@ Every 10 s, each output logs a `stats` entry for the last interval. The main fie | |||||
| 66 | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | 66 | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | ||
| 67 | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | 67 | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | ||
| 68 | | `redrawn_pixels` | Pixels the renderer drew | | 68 | | `redrawn_pixels` | Pixels the renderer drew | | ||
| 69 | - | | `merged_flips` | Flips merged because wayland was not reading output reports. Headless outputs log `stats` too (`frames`, `merged_flips`) | | |||
| 70 | 69 | ||||
| 71 | - | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. | 70 | + | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. Headless outputs log a `stats` entry too (`output`, `frames`). |
| 71 | + | ||||
| 72 | + | When outputs report faster than the display loop drains, wayland merges their flips and logs a `wayland` warning at most every 10 s (`merged`: flips merged since the last warning). Presentation feedback of a merged flip is answered as discarded. | |||
| 72 | 73 | ||||
| 73 | To find the cause of missed vblanks: | 74 | To find the cause of missed vblanks: | ||
| 74 | 75 | ||||
M internal/adapters/drm/output.go +2 -6
| @@ -159,10 +159,7 @@ type Output struct { | |||
| 159 | 159 | traceFlips bool | |
| 160 | 160 | lastFlipAt time.Duration | |
| 161 | 161 | flipStats flipStats | |
| 162 | - | // mergedFlips counts flips merged because wayland was not reading | |
| 163 | - | // reports, reported and reset with the periodic stats. | |
| 164 | - | mergedFlips int | |
| 165 | - | wantedAt time.Duration | |
| 162 | + | wantedAt time.Duration | |
| 166 | 163 | // vrrFlipGap is the minimum time between a game frame's flip event | |
| 167 | 164 | // and the next frame commit under VRR (render.vrr-flip-gap, 0: off); | |
| 168 | 165 | // flipGapUntil is when the next frame may commit: the gap from the | |
| @@ -1465,8 +1462,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1465 | 1462 | fs := o.takeFlipStats() | |
| 1466 | 1463 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | |
| 1467 | 1464 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | |
| 1468 | - | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()).Int("merged_flips", o.mergedFlips) | |
| 1469 | - | o.mergedFlips = 0 | |
| 1465 | + | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) | |
| 1470 | 1466 | if o.traceFlips { | |
| 1471 | 1467 | ev = ev.Int("late_fences", fs.lateFences) | |
| 1472 | 1468 | } | |
| @@ -159,10 +159,7 @@ type Output struct { | |||||
| 159 | traceFlips bool | 159 | traceFlips bool | ||
| 160 | lastFlipAt time.Duration | 160 | lastFlipAt time.Duration | ||
| 161 | flipStats flipStats | 161 | flipStats flipStats | ||
| 162 | - | // mergedFlips counts flips merged because wayland was not reading | 162 | + | wantedAt time.Duration |
| 163 | - | // reports, reported and reset with the periodic stats. | |||
| 164 | - | mergedFlips int | |||
| 165 | - | wantedAt time.Duration | |||
| 166 | // vrrFlipGap is the minimum time between a game frame's flip event | 163 | // vrrFlipGap is the minimum time between a game frame's flip event | ||
| 167 | // and the next frame commit under VRR (render.vrr-flip-gap, 0: off); | 164 | // and the next frame commit under VRR (render.vrr-flip-gap, 0: off); | ||
| 168 | // flipGapUntil is when the next frame may commit: the gap from the | 165 | // flipGapUntil is when the next frame may commit: the gap from the | ||
| @@ -1465,8 +1462,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1465 | fs := o.takeFlipStats() | 1462 | fs := o.takeFlipStats() | ||
| 1466 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | 1463 | ev = ev.Int("missed_vblanks", fs.missedVblanks).Float64("max_flip_interval_ms", ms(fs.maxInterval)) | ||
| 1467 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | 1464 | ev = ev.Float64("max_commit_delay_ms", ms(fs.maxCommitDelay)).Float64("max_flip_to_read_ms", ms(fs.maxFlipToRead)) | ||
| 1468 | - | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()).Int("merged_flips", o.mergedFlips) | 1465 | + | ev = ev.Int("redrawn_pixels", r.TakeRedrawn()) |
| 1469 | - | o.mergedFlips = 0 | |||
| 1470 | if o.traceFlips { | 1466 | if o.traceFlips { | ||
| 1471 | ev = ev.Int("late_fences", fs.lateFences) | 1467 | ev = ev.Int("late_fences", fs.lateFences) | ||
| 1472 | } | 1468 | } | ||
M internal/adapters/drm/output_test.go +6 -7
| @@ -14,7 +14,6 @@ import ( | |||
| 14 | 14 | "testing/synctest" | |
| 15 | 15 | "time" | |
| 16 | 16 | ||
| 17 | - | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |
| 18 | 17 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 19 | 18 | "github.com/bnema/neferwl/internal/ports" | |
| 20 | 19 | "github.com/bnema/zerowrap" | |
| @@ -356,11 +355,12 @@ func TestReportsKeepEveryFlip(t *testing.T) { | |||
| 356 | 355 | if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 { | |
| 357 | 356 | t.Fatalf("unsent %+v", o.reports.Unsent()) | |
| 358 | 357 | } | |
| 359 | - | for i := range presented.MaxUnsent + 3 { | |
| 358 | + | for i := range 20 { | |
| 360 | 359 | o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen) | |
| 361 | 360 | } | |
| 362 | - | if o.reports.Len() != presented.MaxUnsent || o.reports.Unsent()[0].Flip.Merged == 0 { | |
| 363 | - | t.Fatalf("cap: %d reports, first merged %d", o.reports.Len(), o.reports.Unsent()[0].Flip.Merged) | |
| 361 | + | // The wayland sink alone merges flips: the output queue keeps them all. | |
| 362 | + | if o.reports.Len() != 22 || o.reports.Unsent()[2].Flip.Merged != 0 { | |
| 363 | + | t.Fatalf("%d reports, merged %d", o.reports.Len(), o.reports.Unsent()[2].Flip.Merged) | |
| 364 | 364 | } | |
| 365 | 365 | } | |
| 366 | 366 | ||
| @@ -750,7 +750,6 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||
| 750 | 750 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | |
| 751 | 751 | o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | |
| 752 | 752 | o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2} | |
| 753 | - | o.mergedFlips = 4 | |
| 754 | 753 | trimmed := make(chan struct{}, 2) | |
| 755 | 754 | failure := os.ErrInvalid | |
| 756 | 755 | calls := 0 // Run's goroutine only | |
| @@ -777,12 +776,12 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||
| 777 | 776 | t.Fatalf("run: %v", err) | |
| 778 | 777 | } | |
| 779 | 778 | // Reported once, then reset. late_fences needs flip tracing. | |
| 780 | - | for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`, `"merged_flips":4`} { | |
| 779 | + | for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`} { | |
| 781 | 780 | if !strings.Contains(statsLog.String(), want) { | |
| 782 | 781 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | |
| 783 | 782 | } | |
| 784 | 783 | } | |
| 785 | - | if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) || o.mergedFlips != 0 { | |
| 784 | + | if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) { | |
| 786 | 785 | t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats) | |
| 787 | 786 | } | |
| 788 | 787 | } | |
| @@ -14,7 +14,6 @@ import ( | |||||
| 14 | "testing/synctest" | 14 | "testing/synctest" | ||
| 15 | "time" | 15 | "time" | ||
| 16 | 16 | ||||
| 17 | - | "github.com/bnema/neferwl/internal/adapters/outputkit/presented" | |||
| 18 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | 17 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | ||
| 19 | "github.com/bnema/neferwl/internal/ports" | 18 | "github.com/bnema/neferwl/internal/ports" | ||
| 20 | "github.com/bnema/zerowrap" | 19 | "github.com/bnema/zerowrap" | ||
| @@ -356,11 +355,12 @@ func TestReportsKeepEveryFlip(t *testing.T) { | |||||
| 356 | if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 { | 355 | if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 { | ||
| 357 | t.Fatalf("unsent %+v", o.reports.Unsent()) | 356 | t.Fatalf("unsent %+v", o.reports.Unsent()) | ||
| 358 | } | 357 | } | ||
| 359 | - | for i := range presented.MaxUnsent + 3 { | 358 | + | for i := range 20 { |
| 360 | o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen) | 359 | o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen) | ||
| 361 | } | 360 | } | ||
| 362 | - | if o.reports.Len() != presented.MaxUnsent || o.reports.Unsent()[0].Flip.Merged == 0 { | 361 | + | // The wayland sink alone merges flips: the output queue keeps them all. |
| 363 | - | t.Fatalf("cap: %d reports, first merged %d", o.reports.Len(), o.reports.Unsent()[0].Flip.Merged) | 362 | + | if o.reports.Len() != 22 || o.reports.Unsent()[2].Flip.Merged != 0 { |
| 363 | + | t.Fatalf("%d reports, merged %d", o.reports.Len(), o.reports.Unsent()[2].Flip.Merged) | |||
| 364 | } | 364 | } | ||
| 365 | } | 365 | } | ||
| 366 | 366 | ||||
| @@ -750,7 +750,6 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||||
| 750 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | 750 | var statsLog bytes.Buffer // written by Run's goroutine, read after it ends | ||
| 751 | o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | 751 | o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog}) | ||
| 752 | o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2} | 752 | o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2} | ||
| 753 | - | o.mergedFlips = 4 | |||
| 754 | trimmed := make(chan struct{}, 2) | 753 | trimmed := make(chan struct{}, 2) | ||
| 755 | failure := os.ErrInvalid | 754 | failure := os.ErrInvalid | ||
| 756 | calls := 0 // Run's goroutine only | 755 | calls := 0 // Run's goroutine only | ||
| @@ -777,12 +776,12 @@ func TestRunTrimsRendererOnStatsTick(t *testing.T) { | |||||
| 777 | t.Fatalf("run: %v", err) | 776 | t.Fatalf("run: %v", err) | ||
| 778 | } | 777 | } | ||
| 779 | // Reported once, then reset. late_fences needs flip tracing. | 778 | // Reported once, then reset. late_fences needs flip tracing. | ||
| 780 | - | for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`, `"merged_flips":4`} { | 779 | + | for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`} { |
| 781 | if !strings.Contains(statsLog.String(), want) { | 780 | if !strings.Contains(statsLog.String(), want) { | ||
| 782 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | 781 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | ||
| 783 | } | 782 | } | ||
| 784 | } | 783 | } | ||
| 785 | - | if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) || o.mergedFlips != 0 { | 784 | + | if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) { |
| 786 | t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats) | 785 | t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats) | ||
| 787 | } | 786 | } | ||
| 788 | } | 787 | } | ||
M internal/adapters/drm/report.go +2 -5
| @@ -10,17 +10,14 @@ import ( | |||
| 10 | 10 | // Reports to wayland: what the output shows and has read. | |
| 11 | 11 | ||
| 12 | 12 | // report queues what the output shows and has read, so replaced client | |
| 13 | - | // buffers can be released (see presented.Queue for merging). | |
| 13 | + | // buffers can be released (see presented.Queue for coalescing). | |
| 14 | 14 | func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | |
| 15 | 15 | o.capped = false | |
| 16 | 16 | var reads map[ports.WindowID]uint64 | |
| 17 | 17 | if o.capHidden != nil { | |
| 18 | 18 | seen, reads, o.capped = o.capHidden(seen) | |
| 19 | 19 | } | |
| 20 | - | if o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads}) { | |
| 21 | - | o.mergedFlips++ | |
| 22 | - | o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged") | |
| 23 | - | } | |
| 20 | + | o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads}) | |
| 24 | 21 | } | |
| 25 | 22 | ||
| 26 | 23 | // shownBy returns an immutable snapshot of the content Seq of drawn windows. | |
| @@ -10,17 +10,14 @@ import ( | |||||
| 10 | // Reports to wayland: what the output shows and has read. | 10 | // Reports to wayland: what the output shows and has read. | ||
| 11 | 11 | ||||
| 12 | // report queues what the output shows and has read, so replaced client | 12 | // report queues what the output shows and has read, so replaced client | ||
| 13 | - | // buffers can be released (see presented.Queue for merging). | 13 | + | // buffers can be released (see presented.Queue for coalescing). |
| 14 | func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | 14 | func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | ||
| 15 | o.capped = false | 15 | o.capped = false | ||
| 16 | var reads map[ports.WindowID]uint64 | 16 | var reads map[ports.WindowID]uint64 | ||
| 17 | if o.capHidden != nil { | 17 | if o.capHidden != nil { | ||
| 18 | seen, reads, o.capped = o.capHidden(seen) | 18 | seen, reads, o.capped = o.capHidden(seen) | ||
| 19 | } | 19 | } | ||
| 20 | - | if o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads}) { | 20 | + | o.reports.Push(ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: seen, ChildReads: reads}) |
| 21 | - | o.mergedFlips++ | |||
| 22 | - | o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged") | |||
| 23 | - | } | |||
| 24 | } | 21 | } | ||
| 25 | 22 | ||||
| 26 | // shownBy returns an immutable snapshot of the content Seq of drawn windows. | 23 | // shownBy returns an immutable snapshot of the content Seq of drawn windows. | ||
M internal/adapters/headless/output.go +8 -20
| @@ -117,8 +117,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 117 | 117 | // reports queues what the output read; every report is pushed to the | |
| 118 | 118 | // sink at once (Push never blocks), so it holds one at most. | |
| 119 | 119 | var reports presented.Queue | |
| 120 | - | // mergedFlips counts flips merged since the last stats line. | |
| 121 | - | mergedFlips := 0 | |
| 122 | 120 | var security ports.SecurityState | |
| 123 | 121 | securityKnown := false | |
| 124 | 122 | failRequests := func(reason error) { | |
| @@ -310,8 +308,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 310 | 308 | if err := r.Trim(clk.Now()); err != nil { | |
| 311 | 309 | return fmt.Errorf("trim renderer: %w", err) | |
| 312 | 310 | } | |
| 313 | - | opts.Log.Info().Str("output", opts.Name).Int("frames", frame).Int("merged_flips", mergedFlips).Msg("stats") | |
| 314 | - | mergedFlips = 0 | |
| 311 | + | opts.Log.Info().Str("output", opts.Name).Int("frames", frame).Msg("stats") | |
| 315 | 312 | continue | |
| 316 | 313 | case s, ok := <-scenes: | |
| 317 | 314 | if !ok { | |
| @@ -398,9 +395,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 398 | 395 | if !haveScene || !dirty || scene.Off { | |
| 399 | 396 | // Contents not drawn are still read: report them. An output | |
| 400 | 397 | // turned off draws nothing. | |
| 401 | - | if opts.report(&reports, nil, seen) { | |
| 402 | - | mergedFlips++ | |
| 403 | - | } | |
| 398 | + | opts.report(&reports, nil, seen) | |
| 404 | 399 | continue | |
| 405 | 400 | } | |
| 406 | 401 | dirty = false | |
| @@ -449,9 +444,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 449 | 444 | if opts.Presented != nil { | |
| 450 | 445 | _, reads, _ := pipeline.CapHiddenSeen(seen) | |
| 451 | 446 | if len(reads) > 0 { | |
| 452 | - | if reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) { | |
| 453 | - | mergedFlips++ | |
| 454 | - | } | |
| 447 | + | reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) | |
| 455 | 448 | reports.Flush(opts.Presented) | |
| 456 | 449 | } | |
| 457 | 450 | } | |
| @@ -548,9 +541,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 548 | 541 | pipeline.EndGate(nil) | |
| 549 | 542 | requests.List = capture.Waiting(requests.List, scene) | |
| 550 | 543 | frame++ | |
| 551 | - | if opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) { | |
| 552 | - | mergedFlips++ | |
| 553 | - | } | |
| 544 | + | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | |
| 554 | 545 | if scene.Animating && opts.Frames != nil { | |
| 555 | 546 | select { | |
| 556 | 547 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | |
| @@ -627,16 +618,13 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity | |||
| 627 | 618 | } | |
| 628 | 619 | ||
| 629 | 620 | // report queues what the output read, with flip for a drawn frame, and | |
| 630 | - | // pushes it to the sink. It reports whether a flip was merged. | |
| 631 | - | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) (merged bool) { | |
| 621 | + | // pushes it to the sink. | |
| 622 | + | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | |
| 632 | 623 | if opts.Presented == nil { | |
| 633 | - | return false | |
| 634 | - | } | |
| 635 | - | if merged = q.Push(ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: seen}); merged { | |
| 636 | - | opts.Log.Warn().Str("output", opts.Name).Msg("wayland is not reading output reports; flips merged") | |
| 624 | + | return | |
| 637 | 625 | } | |
| 626 | + | q.Push(ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: seen}) | |
| 638 | 627 | q.Flush(opts.Presented) | |
| 639 | - | return merged | |
| 640 | 628 | } | |
| 641 | 629 | ||
| 642 | 630 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| @@ -117,8 +117,6 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 117 | // reports queues what the output read; every report is pushed to the | 117 | // reports queues what the output read; every report is pushed to the | ||
| 118 | // sink at once (Push never blocks), so it holds one at most. | 118 | // sink at once (Push never blocks), so it holds one at most. | ||
| 119 | var reports presented.Queue | 119 | var reports presented.Queue | ||
| 120 | - | // mergedFlips counts flips merged since the last stats line. | |||
| 121 | - | mergedFlips := 0 | |||
| 122 | var security ports.SecurityState | 120 | var security ports.SecurityState | ||
| 123 | securityKnown := false | 121 | securityKnown := false | ||
| 124 | failRequests := func(reason error) { | 122 | failRequests := func(reason error) { | ||
| @@ -310,8 +308,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 310 | if err := r.Trim(clk.Now()); err != nil { | 308 | if err := r.Trim(clk.Now()); err != nil { | ||
| 311 | return fmt.Errorf("trim renderer: %w", err) | 309 | return fmt.Errorf("trim renderer: %w", err) | ||
| 312 | } | 310 | } | ||
| 313 | - | opts.Log.Info().Str("output", opts.Name).Int("frames", frame).Int("merged_flips", mergedFlips).Msg("stats") | 311 | + | opts.Log.Info().Str("output", opts.Name).Int("frames", frame).Msg("stats") |
| 314 | - | mergedFlips = 0 | |||
| 315 | continue | 312 | continue | ||
| 316 | case s, ok := <-scenes: | 313 | case s, ok := <-scenes: | ||
| 317 | if !ok { | 314 | if !ok { | ||
| @@ -398,9 +395,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 398 | if !haveScene || !dirty || scene.Off { | 395 | if !haveScene || !dirty || scene.Off { | ||
| 399 | // Contents not drawn are still read: report them. An output | 396 | // Contents not drawn are still read: report them. An output | ||
| 400 | // turned off draws nothing. | 397 | // turned off draws nothing. | ||
| 401 | - | if opts.report(&reports, nil, seen) { | 398 | + | opts.report(&reports, nil, seen) |
| 402 | - | mergedFlips++ | |||
| 403 | - | } | |||
| 404 | continue | 399 | continue | ||
| 405 | } | 400 | } | ||
| 406 | dirty = false | 401 | dirty = false | ||
| @@ -449,9 +444,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 449 | if opts.Presented != nil { | 444 | if opts.Presented != nil { | ||
| 450 | _, reads, _ := pipeline.CapHiddenSeen(seen) | 445 | _, reads, _ := pipeline.CapHiddenSeen(seen) | ||
| 451 | if len(reads) > 0 { | 446 | if len(reads) > 0 { | ||
| 452 | - | if reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) { | 447 | + | reports.Push(ports.OutputPresented{Output: opts.Name, Seen: seen, ChildReads: reads}) |
| 453 | - | mergedFlips++ | |||
| 454 | - | } | |||
| 455 | reports.Flush(opts.Presented) | 448 | reports.Flush(opts.Presented) | ||
| 456 | } | 449 | } | ||
| 457 | } | 450 | } | ||
| @@ -548,9 +541,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 548 | pipeline.EndGate(nil) | 541 | pipeline.EndGate(nil) | ||
| 549 | requests.List = capture.Waiting(requests.List, scene) | 542 | requests.List = capture.Waiting(requests.List, scene) | ||
| 550 | frame++ | 543 | frame++ | ||
| 551 | - | if opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) { | 544 | + | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) |
| 552 | - | mergedFlips++ | |||
| 553 | - | } | |||
| 554 | if scene.Animating && opts.Frames != nil { | 545 | if scene.Animating && opts.Frames != nil { | ||
| 555 | select { | 546 | select { | ||
| 556 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | 547 | case opts.Frames <- ports.OutputFrame{Output: opts.Name}: | ||
| @@ -627,16 +618,13 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity | |||||
| 627 | } | 618 | } | ||
| 628 | 619 | ||||
| 629 | // report queues what the output read, with flip for a drawn frame, and | 620 | // report queues what the output read, with flip for a drawn frame, and | ||
| 630 | - | // pushes it to the sink. It reports whether a flip was merged. | 621 | + | // pushes it to the sink. |
| 631 | - | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) (merged bool) { | 622 | + | func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { |
| 632 | if opts.Presented == nil { | 623 | if opts.Presented == nil { | ||
| 633 | - | return false | 624 | + | return |
| 634 | - | } | |||
| 635 | - | if merged = q.Push(ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: seen}); merged { | |||
| 636 | - | opts.Log.Warn().Str("output", opts.Name).Msg("wayland is not reading output reports; flips merged") | |||
| 637 | } | 625 | } | ||
| 626 | + | q.Push(ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: seen}) | |||
| 638 | q.Flush(opts.Presented) | 627 | q.Flush(opts.Presented) | ||
| 639 | - | return merged | |||
| 640 | } | 628 | } | ||
| 641 | 629 | ||||
| 642 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | 630 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | ||
M internal/adapters/headless/output_test.go +1 -1
| @@ -237,7 +237,7 @@ func TestRunTrimsRendererOnTick(t *testing.T) { | |||
| 237 | 237 | if got := strings.Count(statsLog.String(), `"message":"stats"`); got != 1 { | |
| 238 | 238 | t.Fatalf("%d stats entries: %s", got, statsLog.String()) | |
| 239 | 239 | } | |
| 240 | - | for _, want := range []string{`"output":"HEADLESS-1"`, `"frames":0`, `"merged_flips":0`} { | |
| 240 | + | for _, want := range []string{`"output":"HEADLESS-1"`, `"frames":0`} { | |
| 241 | 241 | if !strings.Contains(statsLog.String(), want) { | |
| 242 | 242 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | |
| 243 | 243 | } | |
| @@ -237,7 +237,7 @@ func TestRunTrimsRendererOnTick(t *testing.T) { | |||||
| 237 | if got := strings.Count(statsLog.String(), `"message":"stats"`); got != 1 { | 237 | if got := strings.Count(statsLog.String(), `"message":"stats"`); got != 1 { | ||
| 238 | t.Fatalf("%d stats entries: %s", got, statsLog.String()) | 238 | t.Fatalf("%d stats entries: %s", got, statsLog.String()) | ||
| 239 | } | 239 | } | ||
| 240 | - | for _, want := range []string{`"output":"HEADLESS-1"`, `"frames":0`, `"merged_flips":0`} { | 240 | + | for _, want := range []string{`"output":"HEADLESS-1"`, `"frames":0`} { |
| 241 | if !strings.Contains(statsLog.String(), want) { | 241 | if !strings.Contains(statsLog.String(), want) { | ||
| 242 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | 242 | t.Fatalf("stats entry lacks %s: %s", want, statsLog.String()) | ||
| 243 | } | 243 | } | ||
M internal/adapters/outputkit/presented/queue.go +8 -19
| @@ -4,17 +4,14 @@ package presented | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "maps" | |
| 7 | - | "slices" | |
| 8 | 7 | ||
| 9 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 10 | 9 | ) | |
| 11 | 10 | ||
| 12 | - | // MaxUnsent bounds reports waiting for wayland; past it the oldest flip is | |
| 13 | - | // merged into the next one. | |
| 14 | - | const MaxUnsent = 16 | |
| 15 | - | ||
| 16 | - | // Queue holds reports wayland has not read yet. Its owner goroutine is the | |
| 17 | - | // only user. The Seen and ChildReads maps of a queued report are immutable | |
| 11 | + | // Queue holds the reports an output has not pushed to wayland yet. The owner | |
| 12 | + | // goroutine flushes it every loop turn, so it holds the few reports of one | |
| 13 | + | // turn; the sink alone bounds what waits for the display loop (and merges | |
| 14 | + | // flips when that stalls). Its owner goroutine is the only user. The Seen and ChildReads maps of a queued report are immutable | |
| 18 | 15 | // snapshots: wayland may still read an earlier report on another goroutine, | |
| 19 | 16 | // so a snapshot is replaced by a clone when its content changes and an | |
| 20 | 17 | // unchanged report allocates nothing. | |
| @@ -25,9 +22,8 @@ type Queue struct { | |||
| 25 | 22 | ||
| 26 | 23 | // Push queues r. Its Seen and ChildReads may be the caller's scratch maps: | |
| 27 | 24 | // the queue keeps snapshots. A flip-less report replaces the newest queued | |
| 28 | - | // one when that one has no flip, or keeps its flip; flips are merged only | |
| 29 | - | // past MaxUnsent. Push reports whether a flip was merged that way. | |
| 30 | - | func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |
| 25 | + | // one when that one has no flip, or keeps its flip; every flip is queued. | |
| 26 | + | func (q *Queue) Push(r ports.OutputPresented) { | |
| 31 | 27 | if !maps.Equal(q.seen, r.Seen) { | |
| 32 | 28 | q.seen = maps.Clone(r.Seen) | |
| 33 | 29 | } | |
| @@ -40,21 +36,14 @@ func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |||
| 40 | 36 | r.Flip = q.unsent[n-1].Flip | |
| 41 | 37 | } | |
| 42 | 38 | q.unsent[n-1] = r | |
| 43 | - | return false | |
| 39 | + | return | |
| 44 | 40 | } | |
| 45 | 41 | q.unsent = append(q.unsent, r) | |
| 46 | - | if len(q.unsent) <= MaxUnsent { | |
| 47 | - | return false | |
| 48 | - | } | |
| 49 | - | if old, next := q.unsent[0].Flip, q.unsent[1].Flip; next != nil && old != nil { | |
| 50 | - | Merge(old, next) | |
| 51 | - | } | |
| 52 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |
| 53 | - | return true | |
| 54 | 42 | } | |
| 55 | 43 | ||
| 56 | 44 | // Merge folds old into next: next counts it as merged and shows what | |
| 57 | 45 | // old showed, so feedbacks waiting on old are answered (as discarded). | |
| 46 | + | // Wayland's presentation sink uses it when its ring overflows. | |
| 58 | 47 | func Merge(old, next *ports.FlipInfo) { | |
| 59 | 48 | next.Merged += 1 + old.Merged | |
| 60 | 49 | // A snapshot can be shared with previously sent flips. Clone only when | |
| @@ -4,17 +4,14 @@ package presented | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "maps" | 6 | "maps" | ||
| 7 | - | "slices" | |||
| 8 | 7 | ||||
| 9 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 10 | ) | 9 | ) | ||
| 11 | 10 | ||||
| 12 | - | // MaxUnsent bounds reports waiting for wayland; past it the oldest flip is | 11 | + | // Queue holds the reports an output has not pushed to wayland yet. The owner |
| 13 | - | // merged into the next one. | 12 | + | // goroutine flushes it every loop turn, so it holds the few reports of one |
| 14 | - | const MaxUnsent = 16 | 13 | + | // turn; the sink alone bounds what waits for the display loop (and merges |
| 15 | - | 14 | + | // flips when that stalls). Its owner goroutine is the only user. The Seen and ChildReads maps of a queued report are immutable | |
| 16 | - | // Queue holds reports wayland has not read yet. Its owner goroutine is the | |||
| 17 | - | // only user. The Seen and ChildReads maps of a queued report are immutable | |||
| 18 | // snapshots: wayland may still read an earlier report on another goroutine, | 15 | // snapshots: wayland may still read an earlier report on another goroutine, | ||
| 19 | // so a snapshot is replaced by a clone when its content changes and an | 16 | // so a snapshot is replaced by a clone when its content changes and an | ||
| 20 | // unchanged report allocates nothing. | 17 | // unchanged report allocates nothing. | ||
| @@ -25,9 +22,8 @@ type Queue struct { | |||||
| 25 | 22 | ||||
| 26 | // Push queues r. Its Seen and ChildReads may be the caller's scratch maps: | 23 | // Push queues r. Its Seen and ChildReads may be the caller's scratch maps: | ||
| 27 | // the queue keeps snapshots. A flip-less report replaces the newest queued | 24 | // the queue keeps snapshots. A flip-less report replaces the newest queued | ||
| 28 | - | // one when that one has no flip, or keeps its flip; flips are merged only | 25 | + | // one when that one has no flip, or keeps its flip; every flip is queued. |
| 29 | - | // past MaxUnsent. Push reports whether a flip was merged that way. | 26 | + | func (q *Queue) Push(r ports.OutputPresented) { |
| 30 | - | func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |||
| 31 | if !maps.Equal(q.seen, r.Seen) { | 27 | if !maps.Equal(q.seen, r.Seen) { | ||
| 32 | q.seen = maps.Clone(r.Seen) | 28 | q.seen = maps.Clone(r.Seen) | ||
| 33 | } | 29 | } | ||
| @@ -40,21 +36,14 @@ func (q *Queue) Push(r ports.OutputPresented) (merged bool) { | |||||
| 40 | r.Flip = q.unsent[n-1].Flip | 36 | r.Flip = q.unsent[n-1].Flip | ||
| 41 | } | 37 | } | ||
| 42 | q.unsent[n-1] = r | 38 | q.unsent[n-1] = r | ||
| 43 | - | return false | 39 | + | return |
| 44 | } | 40 | } | ||
| 45 | q.unsent = append(q.unsent, r) | 41 | q.unsent = append(q.unsent, r) | ||
| 46 | - | if len(q.unsent) <= MaxUnsent { | |||
| 47 | - | return false | |||
| 48 | - | } | |||
| 49 | - | if old, next := q.unsent[0].Flip, q.unsent[1].Flip; next != nil && old != nil { | |||
| 50 | - | Merge(old, next) | |||
| 51 | - | } | |||
| 52 | - | q.unsent = slices.Delete(q.unsent, 0, 1) | |||
| 53 | - | return true | |||
| 54 | } | 42 | } | ||
| 55 | 43 | ||||
| 56 | // Merge folds old into next: next counts it as merged and shows what | 44 | // Merge folds old into next: next counts it as merged and shows what | ||
| 57 | // old showed, so feedbacks waiting on old are answered (as discarded). | 45 | // old showed, so feedbacks waiting on old are answered (as discarded). | ||
| 46 | + | // Wayland's presentation sink uses it when its ring overflows. | |||
| 58 | func Merge(old, next *ports.FlipInfo) { | 47 | func Merge(old, next *ports.FlipInfo) { | ||
| 59 | next.Merged += 1 + old.Merged | 48 | next.Merged += 1 + old.Merged | ||
| 60 | // A snapshot can be shared with previously sent flips. Clone only when | 49 | // A snapshot can be shared with previously sent flips. Clone only when | ||
M internal/adapters/outputkit/presented/queue_test.go +19 -15
| @@ -56,24 +56,28 @@ func TestQueueKeepsEveryFlip(t *testing.T) { | |||
| 56 | 56 | require.Equal(t, uint64(3), u[1].Seen[1]) | |
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | - | // Past MaxUnsent the oldest flip folds into the next: counted as merged and | |
| 60 | - | // its shown windows carried, without mutating a shared Shows snapshot. | |
| 61 | - | func TestQueueMergesOldestFlipPastBound(t *testing.T) { | |
| 62 | - | var q Queue | |
| 59 | + | // Merge folds a flip into the next: counted as merged and its shown windows | |
| 60 | + | // carried, without mutating a shared Shows snapshot. | |
| 61 | + | func TestMergeCarriesShowsWithoutMutatingSharedSnapshot(t *testing.T) { | |
| 63 | 62 | shared := map[ports.WindowID]uint64{2: 5} | |
| 64 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 1, Shows: map[ports.WindowID]uint64{1: 4}}}) | |
| 65 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 2, Shows: shared}}) | |
| 66 | - | merged := false | |
| 67 | - | for i := range MaxUnsent - 1 { | |
| 68 | - | merged = q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: uint64(10 + i)}}) || merged | |
| 63 | + | old := &ports.FlipInfo{Seq: 1, Merged: 2, Shows: map[ports.WindowID]uint64{1: 4}} | |
| 64 | + | next := &ports.FlipInfo{Seq: 2, Shows: shared} | |
| 65 | + | Merge(old, next) | |
| 66 | + | require.Equal(t, 3, next.Merged) | |
| 67 | + | require.Equal(t, map[ports.WindowID]uint64{1: 4, 2: 5}, next.Shows) | |
| 68 | + | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // The queue never merges flips: every flip of a turn is pushed to the sink. | |
| 72 | + | func TestQueueDoesNotBoundFlips(t *testing.T) { | |
| 73 | + | var q Queue | |
| 74 | + | for i := range 40 { | |
| 75 | + | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: uint64(i + 1)}}) | |
| 69 | 76 | } | |
| 70 | - | require.True(t, merged) | |
| 71 | 77 | u := q.Unsent() | |
| 72 | - | require.Len(t, u, MaxUnsent) | |
| 73 | - | require.Equal(t, uint64(2), u[0].Flip.Seq) | |
| 74 | - | require.Equal(t, 1, u[0].Flip.Merged) | |
| 75 | - | require.Equal(t, map[ports.WindowID]uint64{1: 4, 2: 5}, u[0].Flip.Shows) | |
| 76 | - | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | |
| 78 | + | require.Len(t, u, 40) | |
| 79 | + | require.Zero(t, u[0].Flip.Merged) | |
| 80 | + | require.Equal(t, uint64(1), u[0].Flip.Seq) | |
| 77 | 81 | } | |
| 78 | 82 | ||
| 79 | 83 | func TestQueueFlushPushesInOrderAndEmpties(t *testing.T) { | |
| @@ -56,24 +56,28 @@ func TestQueueKeepsEveryFlip(t *testing.T) { | |||||
| 56 | require.Equal(t, uint64(3), u[1].Seen[1]) | 56 | require.Equal(t, uint64(3), u[1].Seen[1]) | ||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | - | // Past MaxUnsent the oldest flip folds into the next: counted as merged and | 59 | + | // Merge folds a flip into the next: counted as merged and its shown windows |
| 60 | - | // its shown windows carried, without mutating a shared Shows snapshot. | 60 | + | // carried, without mutating a shared Shows snapshot. |
| 61 | - | func TestQueueMergesOldestFlipPastBound(t *testing.T) { | 61 | + | func TestMergeCarriesShowsWithoutMutatingSharedSnapshot(t *testing.T) { |
| 62 | - | var q Queue | |||
| 63 | shared := map[ports.WindowID]uint64{2: 5} | 62 | shared := map[ports.WindowID]uint64{2: 5} | ||
| 64 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 1, Shows: map[ports.WindowID]uint64{1: 4}}}) | 63 | + | old := &ports.FlipInfo{Seq: 1, Merged: 2, Shows: map[ports.WindowID]uint64{1: 4}} |
| 65 | - | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: 2, Shows: shared}}) | 64 | + | next := &ports.FlipInfo{Seq: 2, Shows: shared} |
| 66 | - | merged := false | 65 | + | Merge(old, next) |
| 67 | - | for i := range MaxUnsent - 1 { | 66 | + | require.Equal(t, 3, next.Merged) |
| 68 | - | merged = q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: uint64(10 + i)}}) || merged | 67 | + | require.Equal(t, map[ports.WindowID]uint64{1: 4, 2: 5}, next.Shows) |
| 68 | + | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // The queue never merges flips: every flip of a turn is pushed to the sink. | |||
| 72 | + | func TestQueueDoesNotBoundFlips(t *testing.T) { | |||
| 73 | + | var q Queue | |||
| 74 | + | for i := range 40 { | |||
| 75 | + | q.Push(ports.OutputPresented{Flip: &ports.FlipInfo{Seq: uint64(i + 1)}}) | |||
| 69 | } | 76 | } | ||
| 70 | - | require.True(t, merged) | |||
| 71 | u := q.Unsent() | 77 | u := q.Unsent() | ||
| 72 | - | require.Len(t, u, MaxUnsent) | 78 | + | require.Len(t, u, 40) |
| 73 | - | require.Equal(t, uint64(2), u[0].Flip.Seq) | 79 | + | require.Zero(t, u[0].Flip.Merged) |
| 74 | - | require.Equal(t, 1, u[0].Flip.Merged) | 80 | + | require.Equal(t, uint64(1), u[0].Flip.Seq) |
| 75 | - | require.Equal(t, map[ports.WindowID]uint64{1: 4, 2: 5}, u[0].Flip.Shows) | |||
| 76 | - | require.Equal(t, map[ports.WindowID]uint64{2: 5}, shared, "shared snapshot untouched") | |||
| 77 | } | 81 | } | ||
| 78 | 82 | ||||
| 79 | func TestQueueFlushPushesInOrderAndEmpties(t *testing.T) { | 83 | func TestQueueFlushPushesInOrderAndEmpties(t *testing.T) { | ||