feat(input): log pointer motions only with --debug=input-motion and warn when the input queue backs up
Pointer motions are counted in the input stats instead of logged; the input-motion debug category, never part of all, logs each one. The forwarder queue stays unbounded (no event dropped) and warns once past 4096 queued events and once when it drains.
- commit
- 76522c2902d77107df1f86e325e0d6c2cfca1fdd
- author
- brice · · 2026-09-26 12:14 UTC
- parent
- 9454687
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +120 -9
M cmd/nefertty/main.go +1 -1
| @@ -119,7 +119,7 @@ func run() error { | |||
| 119 | 119 | noTerminal := flags.Bool("no-terminal", false, "skip initial terminal") | |
| 120 | 120 | noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps") | |
| 121 | 121 | timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)") | |
| 122 | - | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | |
| 122 | + | debugFlag := flags.String("debug", "", "debug components (comma-separated or all; input-motion logs every pointer motion)") | |
| 123 | 123 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | |
| 124 | 124 | session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services") | |
| 125 | 125 | if err := flags.Parse(os.Args[1:]); err != nil { | |
| @@ -119,7 +119,7 @@ func run() error { | |||||
| 119 | noTerminal := flags.Bool("no-terminal", false, "skip initial terminal") | 119 | noTerminal := flags.Bool("no-terminal", false, "skip initial terminal") | ||
| 120 | noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps") | 120 | noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps") | ||
| 121 | timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)") | 121 | timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)") | ||
| 122 | - | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | 122 | + | debugFlag := flags.String("debug", "", "debug components (comma-separated or all; input-motion logs every pointer motion)") |
| 123 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | 123 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | ||
| 124 | session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services") | 124 | session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services") | ||
| 125 | if err := flags.Parse(os.Args[1:]); err != nil { | 125 | if err := flags.Parse(os.Args[1:]); err != nil { | ||
M internal/adapters/libinput/forward.go +19 -1
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "time" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | "github.com/bnema/zerowrap" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | // forwarder decouples reading libinput from core. libinput keeps reading | |
| @@ -17,8 +18,15 @@ type forwarder struct { | |||
| 17 | 18 | queue []ports.InputEvent | |
| 18 | 19 | wake chan struct{} | |
| 19 | 20 | stats forwardStats | |
| 21 | + | // The queue is unbounded (no event is ever dropped); long is set | |
| 22 | + | // while it is past forwardWarn entries, warned once each way. | |
| 23 | + | log zerowrap.Logger | |
| 24 | + | long bool | |
| 20 | 25 | } | |
| 21 | 26 | ||
| 27 | + | // forwardWarn is the queue length that means core has stalled. | |
| 28 | + | const forwardWarn = 4096 | |
| 29 | + | ||
| 22 | 30 | // forwardStats is reset by take. | |
| 23 | 31 | type forwardStats struct { | |
| 24 | 32 | Motions int // pointer motions read | |
| @@ -27,7 +35,9 @@ type forwardStats struct { | |||
| 27 | 35 | MaxBlock time.Duration // longest wait for core to accept one event | |
| 28 | 36 | } | |
| 29 | 37 | ||
| 30 | - | func newForwarder() *forwarder { return &forwarder{wake: make(chan struct{}, 1)} } | |
| 38 | + | func newForwarder(log zerowrap.Logger) *forwarder { | |
| 39 | + | return &forwarder{wake: make(chan struct{}, 1), log: log} | |
| 40 | + | } | |
| 31 | 41 | ||
| 32 | 42 | // push queues ev without blocking. | |
| 33 | 43 | func (f *forwarder) push(ev ports.InputEvent) { | |
| @@ -49,6 +59,10 @@ func (f *forwarder) push(ev ports.InputEvent) { | |||
| 49 | 59 | } | |
| 50 | 60 | } | |
| 51 | 61 | f.queue = append(f.queue, ev) | |
| 62 | + | if len(f.queue) > forwardWarn && !f.long { | |
| 63 | + | f.long = true | |
| 64 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("core is not reading input; events queue up") | |
| 65 | + | } | |
| 52 | 66 | f.mu.Unlock() | |
| 53 | 67 | select { | |
| 54 | 68 | case f.wake <- struct{}{}: | |
| @@ -65,6 +79,10 @@ func (f *forwarder) pop() (ports.InputEvent, bool) { | |||
| 65 | 79 | ev := f.queue[0] | |
| 66 | 80 | f.queue[0] = nil | |
| 67 | 81 | f.queue = f.queue[1:] | |
| 82 | + | if f.long && len(f.queue) < forwardWarn { | |
| 83 | + | f.long = false | |
| 84 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("input queue drained") | |
| 85 | + | } | |
| 68 | 86 | return ev, true | |
| 69 | 87 | } | |
| 70 | 88 | ||
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| 9 | + | "github.com/bnema/zerowrap" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | // forwarder decouples reading libinput from core. libinput keeps reading | 12 | // forwarder decouples reading libinput from core. libinput keeps reading | ||
| @@ -17,8 +18,15 @@ type forwarder struct { | |||||
| 17 | queue []ports.InputEvent | 18 | queue []ports.InputEvent | ||
| 18 | wake chan struct{} | 19 | wake chan struct{} | ||
| 19 | stats forwardStats | 20 | stats forwardStats | ||
| 21 | + | // The queue is unbounded (no event is ever dropped); long is set | |||
| 22 | + | // while it is past forwardWarn entries, warned once each way. | |||
| 23 | + | log zerowrap.Logger | |||
| 24 | + | long bool | |||
| 20 | } | 25 | } | ||
| 21 | 26 | ||||
| 27 | + | // forwardWarn is the queue length that means core has stalled. | |||
| 28 | + | const forwardWarn = 4096 | |||
| 29 | + | ||||
| 22 | // forwardStats is reset by take. | 30 | // forwardStats is reset by take. | ||
| 23 | type forwardStats struct { | 31 | type forwardStats struct { | ||
| 24 | Motions int // pointer motions read | 32 | Motions int // pointer motions read | ||
| @@ -27,7 +35,9 @@ type forwardStats struct { | |||||
| 27 | MaxBlock time.Duration // longest wait for core to accept one event | 35 | MaxBlock time.Duration // longest wait for core to accept one event | ||
| 28 | } | 36 | } | ||
| 29 | 37 | ||||
| 30 | - | func newForwarder() *forwarder { return &forwarder{wake: make(chan struct{}, 1)} } | 38 | + | func newForwarder(log zerowrap.Logger) *forwarder { |
| 39 | + | return &forwarder{wake: make(chan struct{}, 1), log: log} | |||
| 40 | + | } | |||
| 31 | 41 | ||||
| 32 | // push queues ev without blocking. | 42 | // push queues ev without blocking. | ||
| 33 | func (f *forwarder) push(ev ports.InputEvent) { | 43 | func (f *forwarder) push(ev ports.InputEvent) { | ||
| @@ -49,6 +59,10 @@ func (f *forwarder) push(ev ports.InputEvent) { | |||||
| 49 | } | 59 | } | ||
| 50 | } | 60 | } | ||
| 51 | f.queue = append(f.queue, ev) | 61 | f.queue = append(f.queue, ev) | ||
| 62 | + | if len(f.queue) > forwardWarn && !f.long { | |||
| 63 | + | f.long = true | |||
| 64 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("core is not reading input; events queue up") | |||
| 65 | + | } | |||
| 52 | f.mu.Unlock() | 66 | f.mu.Unlock() | ||
| 53 | select { | 67 | select { | ||
| 54 | case f.wake <- struct{}{}: | 68 | case f.wake <- struct{}{}: | ||
| @@ -65,6 +79,10 @@ func (f *forwarder) pop() (ports.InputEvent, bool) { | |||||
| 65 | ev := f.queue[0] | 79 | ev := f.queue[0] | ||
| 66 | f.queue[0] = nil | 80 | f.queue[0] = nil | ||
| 67 | f.queue = f.queue[1:] | 81 | f.queue = f.queue[1:] | ||
| 82 | + | if f.long && len(f.queue) < forwardWarn { | |||
| 83 | + | f.long = false | |||
| 84 | + | f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("input queue drained") | |||
| 85 | + | } | |||
| 68 | return ev, true | 86 | return ev, true | ||
| 69 | } | 87 | } | ||
| 70 | 88 | ||||
M internal/adapters/libinput/forward_test.go +63 -3
| @@ -1,17 +1,22 @@ | |||
| 1 | 1 | package libinput | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 6 | + | "strings" | |
| 7 | + | "sync" | |
| 5 | 8 | "testing" | |
| 6 | 9 | "time" | |
| 7 | 10 | ||
| 8 | 11 | "github.com/bnema/nefertty/internal/ports" | |
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | "github.com/rs/zerolog" | |
| 9 | 14 | ) | |
| 10 | 15 | ||
| 11 | 16 | // A slow consumer must not block push, and stale motion must collapse into | |
| 12 | 17 | // the latest position while keys and buttons keep their order. | |
| 13 | 18 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |
| 14 | - | f := newForwarder() | |
| 19 | + | f := newForwarder(zerowrap.Default()) | |
| 15 | 20 | input := make(chan ports.InputEvent) // unbuffered: core is busy | |
| 16 | 21 | start := time.Now() | |
| 17 | 22 | for i := range 1000 { | |
| @@ -56,7 +61,7 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |||
| 56 | 61 | } | |
| 57 | 62 | ||
| 58 | 63 | func TestForwarderStopsOnCancel(t *testing.T) { | |
| 59 | - | f := newForwarder() | |
| 64 | + | f := newForwarder(zerowrap.Default()) | |
| 60 | 65 | f.push(ports.PointerMotion{X: 1}) | |
| 61 | 66 | ctx, cancel := context.WithCancel(context.Background()) | |
| 62 | 67 | done := make(chan struct{}) | |
| @@ -71,7 +76,7 @@ func TestForwarderStopsOnCancel(t *testing.T) { | |||
| 71 | 76 | ||
| 72 | 77 | // Coalesced motions keep the sum of their relative deltas. | |
| 73 | 78 | func TestForwarderSumsDeltas(t *testing.T) { | |
| 74 | - | f := newForwarder() | |
| 79 | + | f := newForwarder(zerowrap.Default()) | |
| 75 | 80 | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | |
| 76 | 81 | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | |
| 77 | 82 | ev, _ := f.pop() | |
| @@ -79,3 +84,58 @@ func TestForwarderSumsDeltas(t *testing.T) { | |||
| 79 | 84 | t.Fatalf("got %+v, want %+v", ev, want) | |
| 80 | 85 | } | |
| 81 | 86 | } | |
| 87 | + | ||
| 88 | + | // 10 000 motions with core stalled are one event with exact delta sums. | |
| 89 | + | func TestForwarderManyMotionsOneEvent(t *testing.T) { | |
| 90 | + | f := newForwarder(zerowrap.Default()) | |
| 91 | + | for i := range 10000 { | |
| 92 | + | f.push(ports.PointerMotion{X: float64(i), DX: 0.5, UnaccelDY: 1}) | |
| 93 | + | } | |
| 94 | + | if len(f.queue) != 1 { | |
| 95 | + | t.Fatalf("%d queued", len(f.queue)) | |
| 96 | + | } | |
| 97 | + | ev, _ := f.pop() | |
| 98 | + | if m := ev.(ports.PointerMotion); m.X != 9999 || m.DX != 5000 || m.UnaccelDY != 10000 { | |
| 99 | + | t.Fatalf("%+v", m) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // Past forwardWarn queued events the forwarder warns once, and once more | |
| 104 | + | // when the queue drains below it; nothing is dropped. | |
| 105 | + | func TestForwarderWarnsOnceEachWay(t *testing.T) { | |
| 106 | + | var buf lockedBuffer | |
| 107 | + | f := newForwarder(zerowrap.Logger{Logger: zerolog.New(&buf)}) | |
| 108 | + | for range forwardWarn + 100 { | |
| 109 | + | f.push(ports.PointerButton{Button: 272}) | |
| 110 | + | } | |
| 111 | + | if n := strings.Count(buf.String(), "events queue up"); n != 1 { | |
| 112 | + | t.Fatalf("%d warnings", n) | |
| 113 | + | } | |
| 114 | + | for range 200 { | |
| 115 | + | f.pop() | |
| 116 | + | } | |
| 117 | + | if n := strings.Count(buf.String(), "input queue drained"); n != 1 { | |
| 118 | + | t.Fatalf("%d drained logs", n) | |
| 119 | + | } | |
| 120 | + | if len(f.queue) != forwardWarn-100 { | |
| 121 | + | t.Fatalf("dropped: %d left", len(f.queue)) | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // lockedBuffer is an io.Writer for a logger. | |
| 126 | + | type lockedBuffer struct { | |
| 127 | + | mu sync.Mutex | |
| 128 | + | b bytes.Buffer | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func (l *lockedBuffer) Write(p []byte) (int, error) { | |
| 132 | + | l.mu.Lock() | |
| 133 | + | defer l.mu.Unlock() | |
| 134 | + | return l.b.Write(p) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (l *lockedBuffer) String() string { | |
| 138 | + | l.mu.Lock() | |
| 139 | + | defer l.mu.Unlock() | |
| 140 | + | return l.b.String() | |
| 141 | + | } | |
| @@ -1,17 +1,22 @@ | |||||
| 1 | package libinput | 1 | package libinput | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "context" | 5 | "context" | ||
| 6 | + | "strings" | |||
| 7 | + | "sync" | |||
| 5 | "testing" | 8 | "testing" | ||
| 6 | "time" | 9 | "time" | ||
| 7 | 10 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 11 | "github.com/bnema/nefertty/internal/ports" | ||
| 12 | + | "github.com/bnema/zerowrap" | |||
| 13 | + | "github.com/rs/zerolog" | |||
| 9 | ) | 14 | ) | ||
| 10 | 15 | ||||
| 11 | // A slow consumer must not block push, and stale motion must collapse into | 16 | // A slow consumer must not block push, and stale motion must collapse into | ||
| 12 | // the latest position while keys and buttons keep their order. | 17 | // the latest position while keys and buttons keep their order. | ||
| 13 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | 18 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | ||
| 14 | - | f := newForwarder() | 19 | + | f := newForwarder(zerowrap.Default()) |
| 15 | input := make(chan ports.InputEvent) // unbuffered: core is busy | 20 | input := make(chan ports.InputEvent) // unbuffered: core is busy | ||
| 16 | start := time.Now() | 21 | start := time.Now() | ||
| 17 | for i := range 1000 { | 22 | for i := range 1000 { | ||
| @@ -56,7 +61,7 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |||||
| 56 | } | 61 | } | ||
| 57 | 62 | ||||
| 58 | func TestForwarderStopsOnCancel(t *testing.T) { | 63 | func TestForwarderStopsOnCancel(t *testing.T) { | ||
| 59 | - | f := newForwarder() | 64 | + | f := newForwarder(zerowrap.Default()) |
| 60 | f.push(ports.PointerMotion{X: 1}) | 65 | f.push(ports.PointerMotion{X: 1}) | ||
| 61 | ctx, cancel := context.WithCancel(context.Background()) | 66 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 62 | done := make(chan struct{}) | 67 | done := make(chan struct{}) | ||
| @@ -71,7 +76,7 @@ func TestForwarderStopsOnCancel(t *testing.T) { | |||||
| 71 | 76 | ||||
| 72 | // Coalesced motions keep the sum of their relative deltas. | 77 | // Coalesced motions keep the sum of their relative deltas. | ||
| 73 | func TestForwarderSumsDeltas(t *testing.T) { | 78 | func TestForwarderSumsDeltas(t *testing.T) { | ||
| 74 | - | f := newForwarder() | 79 | + | f := newForwarder(zerowrap.Default()) |
| 75 | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | 80 | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | ||
| 76 | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | 81 | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | ||
| 77 | ev, _ := f.pop() | 82 | ev, _ := f.pop() | ||
| @@ -79,3 +84,58 @@ func TestForwarderSumsDeltas(t *testing.T) { | |||||
| 79 | t.Fatalf("got %+v, want %+v", ev, want) | 84 | t.Fatalf("got %+v, want %+v", ev, want) | ||
| 80 | } | 85 | } | ||
| 81 | } | 86 | } | ||
| 87 | + | ||||
| 88 | + | // 10 000 motions with core stalled are one event with exact delta sums. | |||
| 89 | + | func TestForwarderManyMotionsOneEvent(t *testing.T) { | |||
| 90 | + | f := newForwarder(zerowrap.Default()) | |||
| 91 | + | for i := range 10000 { | |||
| 92 | + | f.push(ports.PointerMotion{X: float64(i), DX: 0.5, UnaccelDY: 1}) | |||
| 93 | + | } | |||
| 94 | + | if len(f.queue) != 1 { | |||
| 95 | + | t.Fatalf("%d queued", len(f.queue)) | |||
| 96 | + | } | |||
| 97 | + | ev, _ := f.pop() | |||
| 98 | + | if m := ev.(ports.PointerMotion); m.X != 9999 || m.DX != 5000 || m.UnaccelDY != 10000 { | |||
| 99 | + | t.Fatalf("%+v", m) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // Past forwardWarn queued events the forwarder warns once, and once more | |||
| 104 | + | // when the queue drains below it; nothing is dropped. | |||
| 105 | + | func TestForwarderWarnsOnceEachWay(t *testing.T) { | |||
| 106 | + | var buf lockedBuffer | |||
| 107 | + | f := newForwarder(zerowrap.Logger{Logger: zerolog.New(&buf)}) | |||
| 108 | + | for range forwardWarn + 100 { | |||
| 109 | + | f.push(ports.PointerButton{Button: 272}) | |||
| 110 | + | } | |||
| 111 | + | if n := strings.Count(buf.String(), "events queue up"); n != 1 { | |||
| 112 | + | t.Fatalf("%d warnings", n) | |||
| 113 | + | } | |||
| 114 | + | for range 200 { | |||
| 115 | + | f.pop() | |||
| 116 | + | } | |||
| 117 | + | if n := strings.Count(buf.String(), "input queue drained"); n != 1 { | |||
| 118 | + | t.Fatalf("%d drained logs", n) | |||
| 119 | + | } | |||
| 120 | + | if len(f.queue) != forwardWarn-100 { | |||
| 121 | + | t.Fatalf("dropped: %d left", len(f.queue)) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // lockedBuffer is an io.Writer for a logger. | |||
| 126 | + | type lockedBuffer struct { | |||
| 127 | + | mu sync.Mutex | |||
| 128 | + | b bytes.Buffer | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (l *lockedBuffer) Write(p []byte) (int, error) { | |||
| 132 | + | l.mu.Lock() | |||
| 133 | + | defer l.mu.Unlock() | |||
| 134 | + | return l.b.Write(p) | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (l *lockedBuffer) String() string { | |||
| 138 | + | l.mu.Lock() | |||
| 139 | + | defer l.mu.Unlock() | |||
| 140 | + | return l.b.String() | |||
| 141 | + | } | |||
M internal/adapters/libinput/libinput.go +7 -2
| @@ -160,6 +160,9 @@ type Options struct { | |||
| 160 | 160 | // the physical position on it. | |
| 161 | 161 | MoveCursor func(output string, x, y float64) | |
| 162 | 162 | Log zerowrap.Logger | |
| 163 | + | // LogMotion logs every pointer motion (--debug=input-motion); motions | |
| 164 | + | // are otherwise only counted in the input stats. | |
| 165 | + | LogMotion bool | |
| 163 | 166 | } | |
| 164 | 167 | ||
| 165 | 168 | // statsEvery is how often Run logs input throughput. | |
| @@ -192,7 +195,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error | |||
| 192 | 195 | p := newPointer(opts.Layout) | |
| 193 | 196 | p.moved(opts.MoveCursor) | |
| 194 | 197 | fd := getFD(li) | |
| 195 | - | fwd := newForwarder() | |
| 198 | + | fwd := newForwarder(opts.Log) | |
| 196 | 199 | fwdCtx, stopFwd := context.WithCancel(ctx) | |
| 197 | 200 | var fwdDone sync.WaitGroup | |
| 198 | 201 | fwdDone.Go(func() { fwd.run(fwdCtx, input) }) | |
| @@ -278,7 +281,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||
| 278 | 281 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | |
| 279 | 282 | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | |
| 280 | 283 | p.moved(opts.MoveCursor) | |
| 281 | - | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |
| 284 | + | if opts.LogMotion { | |
| 285 | + | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |
| 286 | + | } | |
| 282 | 287 | return m, nil | |
| 283 | 288 | case evPointerAbs: | |
| 284 | 289 | // Absolute devices (tablets, VMs) map to the whole layout. | |
| @@ -160,6 +160,9 @@ type Options struct { | |||||
| 160 | // the physical position on it. | 160 | // the physical position on it. | ||
| 161 | MoveCursor func(output string, x, y float64) | 161 | MoveCursor func(output string, x, y float64) | ||
| 162 | Log zerowrap.Logger | 162 | Log zerowrap.Logger | ||
| 163 | + | // LogMotion logs every pointer motion (--debug=input-motion); motions | |||
| 164 | + | // are otherwise only counted in the input stats. | |||
| 165 | + | LogMotion bool | |||
| 163 | } | 166 | } | ||
| 164 | 167 | ||||
| 165 | // statsEvery is how often Run logs input throughput. | 168 | // statsEvery is how often Run logs input throughput. | ||
| @@ -192,7 +195,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error | |||||
| 192 | p := newPointer(opts.Layout) | 195 | p := newPointer(opts.Layout) | ||
| 193 | p.moved(opts.MoveCursor) | 196 | p.moved(opts.MoveCursor) | ||
| 194 | fd := getFD(li) | 197 | fd := getFD(li) | ||
| 195 | - | fwd := newForwarder() | 198 | + | fwd := newForwarder(opts.Log) |
| 196 | fwdCtx, stopFwd := context.WithCancel(ctx) | 199 | fwdCtx, stopFwd := context.WithCancel(ctx) | ||
| 197 | var fwdDone sync.WaitGroup | 200 | var fwdDone sync.WaitGroup | ||
| 198 | fwdDone.Go(func() { fwd.run(fwdCtx, input) }) | 201 | fwdDone.Go(func() { fwd.run(fwdCtx, input) }) | ||
| @@ -278,7 +281,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||||
| 278 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | 281 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | ||
| 279 | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | 282 | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | ||
| 280 | p.moved(opts.MoveCursor) | 283 | p.moved(opts.MoveCursor) | ||
| 281 | - | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | 284 | + | if opts.LogMotion { |
| 285 | + | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |||
| 286 | + | } | |||
| 282 | return m, nil | 287 | return m, nil | ||
| 283 | case evPointerAbs: | 288 | case evPointerAbs: | ||
| 284 | // Absolute devices (tablets, VMs) map to the whole layout. | 289 | // Absolute devices (tablets, VMs) map to the whole layout. | ||
M internal/app/app.go +1 -1
| @@ -161,7 +161,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 161 | 161 | return | |
| 162 | 162 | } | |
| 163 | 163 | done <- safe("input", func() error { | |
| 164 | - | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | |
| 164 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) | |
| 165 | 165 | }) | |
| 166 | 166 | return | |
| 167 | 167 | } | |
| @@ -161,7 +161,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 161 | return | 161 | return | ||
| 162 | } | 162 | } | ||
| 163 | done <- safe("input", func() error { | 163 | done <- safe("input", func() error { | ||
| 164 | - | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | 164 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) |
| 165 | }) | 165 | }) | ||
| 166 | return | 166 | return | ||
| 167 | } | 167 | } | ||
M internal/logging/logging.go +11 -1
| @@ -20,6 +20,10 @@ var components = map[string]bool{ | |||
| 20 | 20 | "render": true, "sync": true, "config": true, "app": true, | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | + | // categories are debug switches narrower than a component, only on when | |
| 24 | + | // named: "all" leaves them off (input-motion logs every pointer motion). | |
| 25 | + | var categories = map[string]bool{"input-motion": true} | |
| 26 | + | ||
| 23 | 27 | type debugKey struct{} | |
| 24 | 28 | type levelKey struct{} | |
| 25 | 29 | type debugSet map[string]bool | |
| @@ -34,7 +38,7 @@ func ParseDebug(value string) (map[string]bool, error) { | |||
| 34 | 38 | selected[name] = true | |
| 35 | 39 | continue | |
| 36 | 40 | } | |
| 37 | - | if !components[name] { | |
| 41 | + | if !components[name] && !categories[name] { | |
| 38 | 42 | return nil, fmt.Errorf("invalid debug component %q", name) | |
| 39 | 43 | } | |
| 40 | 44 | selected[name] = true | |
| @@ -104,6 +108,12 @@ func (w *consoleAndFile) Write(p []byte) (int, error) { | |||
| 104 | 108 | return w.file.Write(p) | |
| 105 | 109 | } | |
| 106 | 110 | ||
| 111 | + | // Enabled reports whether a debug category was named in --debug. | |
| 112 | + | func Enabled(ctx context.Context, category string) bool { | |
| 113 | + | selected, _ := ctx.Value(debugKey{}).(debugSet) | |
| 114 | + | return selected[category] | |
| 115 | + | } | |
| 116 | + | ||
| 107 | 117 | // For returns a component logger with debug enabled only for selected components. | |
| 108 | 118 | func For(ctx context.Context, component string) zerowrap.Logger { | |
| 109 | 119 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | |
| @@ -20,6 +20,10 @@ var components = map[string]bool{ | |||||
| 20 | "render": true, "sync": true, "config": true, "app": true, | 20 | "render": true, "sync": true, "config": true, "app": true, | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | + | // categories are debug switches narrower than a component, only on when | |||
| 24 | + | // named: "all" leaves them off (input-motion logs every pointer motion). | |||
| 25 | + | var categories = map[string]bool{"input-motion": true} | |||
| 26 | + | ||||
| 23 | type debugKey struct{} | 27 | type debugKey struct{} | ||
| 24 | type levelKey struct{} | 28 | type levelKey struct{} | ||
| 25 | type debugSet map[string]bool | 29 | type debugSet map[string]bool | ||
| @@ -34,7 +38,7 @@ func ParseDebug(value string) (map[string]bool, error) { | |||||
| 34 | selected[name] = true | 38 | selected[name] = true | ||
| 35 | continue | 39 | continue | ||
| 36 | } | 40 | } | ||
| 37 | - | if !components[name] { | 41 | + | if !components[name] && !categories[name] { |
| 38 | return nil, fmt.Errorf("invalid debug component %q", name) | 42 | return nil, fmt.Errorf("invalid debug component %q", name) | ||
| 39 | } | 43 | } | ||
| 40 | selected[name] = true | 44 | selected[name] = true | ||
| @@ -104,6 +108,12 @@ func (w *consoleAndFile) Write(p []byte) (int, error) { | |||||
| 104 | return w.file.Write(p) | 108 | return w.file.Write(p) | ||
| 105 | } | 109 | } | ||
| 106 | 110 | ||||
| 111 | + | // Enabled reports whether a debug category was named in --debug. | |||
| 112 | + | func Enabled(ctx context.Context, category string) bool { | |||
| 113 | + | selected, _ := ctx.Value(debugKey{}).(debugSet) | |||
| 114 | + | return selected[category] | |||
| 115 | + | } | |||
| 116 | + | ||||
| 107 | // For returns a component logger with debug enabled only for selected components. | 117 | // For returns a component logger with debug enabled only for selected components. | ||
| 108 | func For(ctx context.Context, component string) zerowrap.Logger { | 118 | func For(ctx context.Context, component string) zerowrap.Logger { | ||
| 109 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | 119 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | ||
M internal/logging/logging_test.go +18 -0
| @@ -18,6 +18,7 @@ func TestParseDebug(t *testing.T) { | |||
| 18 | 18 | }{ | |
| 19 | 19 | {"", false, nil}, {"core,app", false, []string{"core", "app"}}, | |
| 20 | 20 | {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil}, | |
| 21 | + | {"all,input-motion", false, []string{"all", "input-motion"}}, | |
| 21 | 22 | } { | |
| 22 | 23 | got, err := ParseDebug(tc.input) | |
| 23 | 24 | if (err != nil) != tc.wantError { | |
| @@ -82,3 +83,20 @@ func splitLines(data []byte) [][]byte { | |||
| 82 | 83 | } | |
| 83 | 84 | return lines | |
| 84 | 85 | } | |
| 86 | + | ||
| 87 | + | // Narrow categories are on only when named, never through "all". | |
| 88 | + | func TestEnabledCategories(t *testing.T) { | |
| 89 | + | for _, tc := range []struct { | |
| 90 | + | debug string | |
| 91 | + | want bool | |
| 92 | + | }{{"all", false}, {"input", false}, {"input-motion", true}, {"all,input-motion", true}} { | |
| 93 | + | sel, err := ParseDebug(tc.debug) | |
| 94 | + | if err != nil { | |
| 95 | + | t.Fatal(err) | |
| 96 | + | } | |
| 97 | + | ctx := context.WithValue(context.Background(), debugKey{}, debugSet(sel)) | |
| 98 | + | if got := Enabled(ctx, "input-motion"); got != tc.want { | |
| 99 | + | t.Errorf("%q: %v", tc.debug, got) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | } | |
| @@ -18,6 +18,7 @@ func TestParseDebug(t *testing.T) { | |||||
| 18 | }{ | 18 | }{ | ||
| 19 | {"", false, nil}, {"core,app", false, []string{"core", "app"}}, | 19 | {"", false, nil}, {"core,app", false, []string{"core", "app"}}, | ||
| 20 | {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil}, | 20 | {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil}, | ||
| 21 | + | {"all,input-motion", false, []string{"all", "input-motion"}}, | |||
| 21 | } { | 22 | } { | ||
| 22 | got, err := ParseDebug(tc.input) | 23 | got, err := ParseDebug(tc.input) | ||
| 23 | if (err != nil) != tc.wantError { | 24 | if (err != nil) != tc.wantError { | ||
| @@ -82,3 +83,20 @@ func splitLines(data []byte) [][]byte { | |||||
| 82 | } | 83 | } | ||
| 83 | return lines | 84 | return lines | ||
| 84 | } | 85 | } | ||
| 86 | + | ||||
| 87 | + | // Narrow categories are on only when named, never through "all". | |||
| 88 | + | func TestEnabledCategories(t *testing.T) { | |||
| 89 | + | for _, tc := range []struct { | |||
| 90 | + | debug string | |||
| 91 | + | want bool | |||
| 92 | + | }{{"all", false}, {"input", false}, {"input-motion", true}, {"all,input-motion", true}} { | |||
| 93 | + | sel, err := ParseDebug(tc.debug) | |||
| 94 | + | if err != nil { | |||
| 95 | + | t.Fatal(err) | |||
| 96 | + | } | |||
| 97 | + | ctx := context.WithValue(context.Background(), debugKey{}, debugSet(sel)) | |||
| 98 | + | if got := Enabled(ctx, "input-motion"); got != tc.want { | |||
| 99 | + | t.Errorf("%q: %v", tc.debug, got) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | } | |||