detached b69dc78

refactor(core): route every event through one dispatch path

The registry now implements EventSignature and Dispatch like any generated
object, so its events use the same validation and ownership path. Removed
the paths that never ran for registered objects: AddListener and
notifyListeners, the EventDispatcher (RegisterEventHandler, BatchDispatch,
RawEvent, PerformanceStats), and the unused lastError fields.
commit
b69dc789dffd2aec5137bc8dd11463d959fc395f
author
brice · · 2026-09-29 15:46 UTC
committer
brice · · 2026-09-29 15:46 UTC
parent
82fabc3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +51 -438
M client.go +46 -154
@@ -56,21 +56,11 @@ type Display struct {
5656 recvMu sync.Mutex
5757 dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
5858 signatures sync.Map
59- listenerMu sync.Mutex
60- listeners sync.Map // map[uint32]map[uint16][]func([]byte)
61-
62- // High-performance event dispatcher
63- dispatcher *EventDispatcher
6459
6560 // Core objects
6661 registry *Registry
6762 context *Context // Store context once
6863
69- // Error state
70- lastError error
71- lastErrorCode uint32
72- lastErrorObj uint32
73-
7464 // Connection-local receive state. rbuf holds bytes read from the socket
7565 // that do not yet form a complete frame; recvBuf is the scratch buffer
7666 // used for a single read. pendingFDs holds ancillary descriptors that
@@ -86,9 +76,8 @@ type Display struct {
8676 // newDisplay builds a Display over an established connection.
8777 func newDisplay(conn net.Conn) *Display {
8878 d := &Display{
89- conn: conn,
90- nextID: 2, // 1 is reserved for wl_display
91- dispatcher: NewEventDispatcher(),
79+ conn: conn,
80+ nextID: 2, // 1 is reserved for wl_display
9281 }
9382 if uc, ok := conn.(*net.UnixConn); ok {
9483 d.unix = uc
@@ -138,7 +127,6 @@ func (c *callbackObject) Dispatch(event *Event) {
138127 if event.Opcode != 0 { // done event
139128 return
140129 }
141- c.display.notifyListeners(c.id, 0, event.data)
142130 c.display.context.Unregister(c)
143131 }
144132
@@ -212,11 +200,6 @@ func (d *Display) ID() uint32 {
212200 return 1
213201 }
214202
215-// RegisterEventHandler registers a high-performance event handler
216-func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler EventHandler) {
217- d.dispatcher.RegisterHandler(objectID, opcode, handler)
218-}
219-
220203 // allocateID allocates a new object ID
221204 func (d *Display) allocateID() uint32 {
222205 d.idMu.Lock()
@@ -481,17 +464,13 @@ type signatureProvider interface{ EventSignature(uint16) (string, bool) }
481464 // dispatchFrame delivers one complete frame with precisely its signature's FDs.
482465 func (d *Display) dispatchFrame(f receivedFrame) error {
483466 d.recvMu.Lock()
484- obj, ev, kind, err := d.prepareFrame(f)
467+ obj, ev, err := d.prepareFrame(f)
485468 d.recvMu.Unlock()
486469 if err != nil {
487470 return err
488471 }
489- switch kind {
490- case 1:
472+ if obj == nil {
491473 return d.handleDisplayEvent(f.opcode, f.body)
492- case 2:
493- d.notifyListeners(f.object, f.opcode, f.body)
494- return nil
495474 }
496475 defer func() {
497476 for _, fd := range ev.fds {
@@ -503,47 +482,31 @@ func (d *Display) dispatchFrame(f receivedFrame) error {
503482 if _, isZombie := obj.(*zombie); isZombie {
504483 return nil
505484 }
506- if proxy, ok := obj.(Proxy); ok {
507- proxy.Dispatch(ev)
508- } else {
509- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
510- d.notifyListeners(f.object, f.opcode, f.body)
485+ if r, ok := obj.(eventReceiver); ok {
486+ r.Dispatch(ev)
511487 }
512488 return nil
513489 }
514490
491+// eventReceiver is any registered object that handles its own events:
492+// generated proxies, callbacks and the registry.
493+type eventReceiver interface{ Dispatch(*Event) }
494+
515495 // prepareFrame assigns owned descriptors under recvMu, but never calls user code.
516-func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
496+// A nil object with a nil error means the frame targets wl_display.
497+func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
517498 if d.closed.Load() {
518- return nil, nil, 0, net.ErrClosed
499+ return nil, nil, net.ErrClosed
519500 }
520501 if f.object == displayObjectID {
521502 if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
522- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
523- }
524- return nil, nil, 1, nil
525- }
526- if f.object == d.registry.id {
527- var sig string
528- switch f.opcode {
529- case 0:
530- sig = "uint,string,uint,"
531- case 1:
532- sig = "uint,"
533- default:
534- return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
503+ return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
535504 }
536- if err := validateEventBody(sig, f.body); err != nil {
537- return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
538- }
539- if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
540- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
541- }
542- return nil, nil, 2, nil
505+ return nil, nil, nil
543506 }
544507 obj, ok := d.objects.Load(f.object)
545508 if !ok {
546- return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
509+ return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
547510 }
548511 sigObj := obj
549512 z, isZombie := obj.(*zombie)
@@ -562,14 +525,14 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
562525 }
563526 }
564527 if !valid {
565- return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
528+ return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
566529 }
567530 if err := validateEventBody(sig, f.body); err != nil {
568- return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
531+ return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
569532 }
570533 count := strings.Count(sig, "fd,")
571534 if len(d.pendingFDs) < count {
572- return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
535+ return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
573536 }
574537 ev := eventPool.Get().(*Event)
575538 *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
@@ -588,11 +551,11 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
588551 }
589552 *ev = Event{}
590553 eventPool.Put(ev)
591- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
554+ return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
592555 }
593556 // For a zombie the descriptors were still consumed above so the stream
594557 // stays aligned; dispatchFrame closes them without running any handler.
595- return obj.(Object), ev, 0, nil
558+ return obj.(Object), ev, nil
596559 }
597560
598561 type signatureKey struct {
@@ -609,27 +572,6 @@ func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature
609572 d.signatures.Store(signatureKey{object, opcode}, signature)
610573 }
611574
612-// notifyListeners runs the listeners registered for an object and opcode.
613-func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
614- listeners, ok := d.listeners.Load(objectID)
615- if !ok {
616- return
617- }
618- opcodeMap, ok := listeners.(*sync.Map)
619- if !ok {
620- return
621- }
622- handlers, ok := opcodeMap.Load(opcode)
623- if !ok {
624- return
625- }
626- for _, handler := range handlers.([]func([]byte)) {
627- if handler != nil {
628- handler(body)
629- }
630- }
631-}
632-
633575 // handleDisplayEvent handles events on the display object
634576 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
635577 switch opcode {
@@ -640,12 +582,7 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
640582 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
641583 objectID := event.Uint32()
642584 code := event.Uint32()
643- message := event.String()
644-
645- d.lastErrorCode = code
646- d.lastErrorObj = objectID
647- d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message}
648- return d.lastError
585+ return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
649586
650587 case 1: // delete_id
651588 if len(data) != 4 {
@@ -692,35 +629,9 @@ func (d *Display) Roundtrip() error {
692629 }
693630 }
694631
695-// AddListener adds an event listener for an object
696-func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
697- if handler == nil {
698- return
699- }
700- // Load or create listener map for object
701- listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
702- opcodeMap := listeners.(*sync.Map)
703-
704- d.listenerMu.Lock()
705- defer d.listenerMu.Unlock()
706- var handlers []func([]byte)
707- if old, ok := opcodeMap.Load(opcode); ok {
708- handlers = old.([]func([]byte))
709- }
710- next := make([]func([]byte), len(handlers)+1)
711- copy(next, handlers)
712- next[len(handlers)] = handler
713- opcodeMap.Store(opcode, next)
714-}
715-
716-// getRegistry gets the global registry
632+// getRegistry sends wl_display.get_registry for the bootstrap registry.
717633 func (d *Display) getRegistry() error {
718- // Add registry listeners
719- d.AddListener(d.registry.id, 0, d.registry.handleGlobal)
720- d.AddListener(d.registry.id, 1, d.registry.handleGlobalRemove)
721-
722- // Send get_registry request (opcode 1)
723- return d.SendRequest(1, 1, d.registry.id)
634+ return d.SendRequest(displayObjectID, 1, d.registry.id)
724635 }
725636
726637 // Registry returns the global registry
@@ -733,59 +644,40 @@ func (r *Registry) ID() uint32 {
733644 return r.id
734645 }
735646
736-// handleGlobal handles global announcements
737-func (r *Registry) handleGlobal(data []byte) {
738- if len(data) < 8 {
739- return
740- }
741-
742- name := binary.LittleEndian.Uint32(data[0:4])
743- ifaceLen := binary.LittleEndian.Uint32(data[4:8])
744-
745- if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
746- return
647+// EventSignature reports the wl_registry event signatures.
648+func (r *Registry) EventSignature(opcode uint16) (string, bool) {
649+ switch opcode {
650+ case 0:
651+ return "uint,string,uint,", true
652+ case 1:
653+ return "uint,", true
747654 }
655+ return "", false
656+}
748657
749- // String includes null terminator in length
750- iface := string(data[8 : 8+ifaceLen-1]) // -1 to remove null terminator
751-
752- // Calculate padding for 32-bit alignment
753- padding := (4 - (ifaceLen % 4)) % 4
754- versionOffset := 8 + int(ifaceLen) + int(padding)
755-
756- if len(data) != versionOffset+4 {
757- return
658+// Dispatch handles wl_registry.global and wl_registry.global_remove. The
659+// transport has already validated the body against EventSignature.
660+func (r *Registry) Dispatch(event *Event) {
661+ switch event.Opcode {
662+ case 0:
663+ r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
664+ case 1:
665+ r.handleGlobalRemove(event.Uint32())
758666 }
667+}
759668
760- version := binary.LittleEndian.Uint32(data[versionOffset:])
761-
762- // Store global
669+func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
763670 r.mu.Lock()
764- r.globals[name] = Global{
765- Name: name,
766- Interface: iface,
767- Version: version,
768- }
769- r.mu.Unlock()
770-
771- // Call specific handler if registered
772- r.mu.RLock()
671+ r.globals[name] = Global{Name: name, Interface: iface, Version: version}
773672 handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
774673 handlers = append(handlers, r.handlers["*"]...)
775- r.mu.RUnlock()
674+ r.mu.Unlock()
776675 for _, handler := range handlers {
777676 handler(r, name, version)
778677 }
779678 }
780679
781-// handleGlobalRemove handles global removal
782-func (r *Registry) handleGlobalRemove(data []byte) {
783- if len(data) < 4 {
784- return
785- }
786-
787- name := binary.LittleEndian.Uint32(data[0:4])
788-
680+func (r *Registry) handleGlobalRemove(name uint32) {
789681 r.mu.Lock()
790682 delete(r.globals, name)
791683 handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
@@ -56,21 +56,11 @@ type Display struct {
56 recvMu sync.Mutex56 recvMu sync.Mutex
57 dispatchMu sync.Mutex // serialize readers without holding receive state across handlers57 dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
58 signatures sync.Map58 signatures sync.Map
59- listenerMu sync.Mutex
60- listeners sync.Map // map[uint32]map[uint16][]func([]byte)
61-
62- // High-performance event dispatcher
63- dispatcher *EventDispatcher
64 59
65 // Core objects60 // Core objects
66 registry *Registry61 registry *Registry
67 context *Context // Store context once62 context *Context // Store context once
68 63
69- // Error state
70- lastError error
71- lastErrorCode uint32
72- lastErrorObj uint32
73-
74 // Connection-local receive state. rbuf holds bytes read from the socket64 // Connection-local receive state. rbuf holds bytes read from the socket
75 // that do not yet form a complete frame; recvBuf is the scratch buffer65 // that do not yet form a complete frame; recvBuf is the scratch buffer
76 // used for a single read. pendingFDs holds ancillary descriptors that66 // used for a single read. pendingFDs holds ancillary descriptors that
@@ -86,9 +76,8 @@ type Display struct {
86 // newDisplay builds a Display over an established connection.76 // newDisplay builds a Display over an established connection.
87 func newDisplay(conn net.Conn) *Display {77 func newDisplay(conn net.Conn) *Display {
88 d := &Display{78 d := &Display{
89- conn: conn,79+ conn: conn,
90- nextID: 2, // 1 is reserved for wl_display80+ nextID: 2, // 1 is reserved for wl_display
91- dispatcher: NewEventDispatcher(),
92 }81 }
93 if uc, ok := conn.(*net.UnixConn); ok {82 if uc, ok := conn.(*net.UnixConn); ok {
94 d.unix = uc83 d.unix = uc
@@ -138,7 +127,6 @@ func (c *callbackObject) Dispatch(event *Event) {
138 if event.Opcode != 0 { // done event127 if event.Opcode != 0 { // done event
139 return128 return
140 }129 }
141- c.display.notifyListeners(c.id, 0, event.data)
142 c.display.context.Unregister(c)130 c.display.context.Unregister(c)
143 }131 }
144 132
@@ -212,11 +200,6 @@ func (d *Display) ID() uint32 {
212 return 1200 return 1
213 }201 }
214 202
215-// RegisterEventHandler registers a high-performance event handler
216-func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler EventHandler) {
217- d.dispatcher.RegisterHandler(objectID, opcode, handler)
218-}
219-
220 // allocateID allocates a new object ID203 // allocateID allocates a new object ID
221 func (d *Display) allocateID() uint32 {204 func (d *Display) allocateID() uint32 {
222 d.idMu.Lock()205 d.idMu.Lock()
@@ -481,17 +464,13 @@ type signatureProvider interface{ EventSignature(uint16) (string, bool) }
481 // dispatchFrame delivers one complete frame with precisely its signature's FDs.464 // dispatchFrame delivers one complete frame with precisely its signature's FDs.
482 func (d *Display) dispatchFrame(f receivedFrame) error {465 func (d *Display) dispatchFrame(f receivedFrame) error {
483 d.recvMu.Lock()466 d.recvMu.Lock()
484- obj, ev, kind, err := d.prepareFrame(f)467+ obj, ev, err := d.prepareFrame(f)
485 d.recvMu.Unlock()468 d.recvMu.Unlock()
486 if err != nil {469 if err != nil {
487 return err470 return err
488 }471 }
489- switch kind {472+ if obj == nil {
490- case 1:
491 return d.handleDisplayEvent(f.opcode, f.body)473 return d.handleDisplayEvent(f.opcode, f.body)
492- case 2:
493- d.notifyListeners(f.object, f.opcode, f.body)
494- return nil
495 }474 }
496 defer func() {475 defer func() {
497 for _, fd := range ev.fds {476 for _, fd := range ev.fds {
@@ -503,47 +482,31 @@ func (d *Display) dispatchFrame(f receivedFrame) error {
503 if _, isZombie := obj.(*zombie); isZombie {482 if _, isZombie := obj.(*zombie); isZombie {
504 return nil483 return nil
505 }484 }
506- if proxy, ok := obj.(Proxy); ok {485+ if r, ok := obj.(eventReceiver); ok {
507- proxy.Dispatch(ev)486+ r.Dispatch(ev)
508- } else {
509- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
510- d.notifyListeners(f.object, f.opcode, f.body)
511 }487 }
512 return nil488 return nil
513 }489 }
514 490
491+// eventReceiver is any registered object that handles its own events:
492+// generated proxies, callbacks and the registry.
493+type eventReceiver interface{ Dispatch(*Event) }
494+
515 // prepareFrame assigns owned descriptors under recvMu, but never calls user code.495 // prepareFrame assigns owned descriptors under recvMu, but never calls user code.
516-func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {496+// A nil object with a nil error means the frame targets wl_display.
497+func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
517 if d.closed.Load() {498 if d.closed.Load() {
518- return nil, nil, 0, net.ErrClosed499+ return nil, nil, net.ErrClosed
519 }500 }
520 if f.object == displayObjectID {501 if f.object == displayObjectID {
521 if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {502 if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
522- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}503+ return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
523- }
524- return nil, nil, 1, nil
525- }
526- if f.object == d.registry.id {
527- var sig string
528- switch f.opcode {
529- case 0:
530- sig = "uint,string,uint,"
531- case 1:
532- sig = "uint,"
533- default:
534- return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
535 }504 }
536- if err := validateEventBody(sig, f.body); err != nil {505+ return nil, nil, nil
537- return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
538- }
539- if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
540- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
541- }
542- return nil, nil, 2, nil
543 }506 }
544 obj, ok := d.objects.Load(f.object)507 obj, ok := d.objects.Load(f.object)
545 if !ok {508 if !ok {
546- return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}509+ return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
547 }510 }
548 sigObj := obj511 sigObj := obj
549 z, isZombie := obj.(*zombie)512 z, isZombie := obj.(*zombie)
@@ -562,14 +525,14 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
562 }525 }
563 }526 }
564 if !valid {527 if !valid {
565- return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}528+ return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
566 }529 }
567 if err := validateEventBody(sig, f.body); err != nil {530 if err := validateEventBody(sig, f.body); err != nil {
568- return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}531+ return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
569 }532 }
570 count := strings.Count(sig, "fd,")533 count := strings.Count(sig, "fd,")
571 if len(d.pendingFDs) < count {534 if len(d.pendingFDs) < count {
572- return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}535+ return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
573 }536 }
574 ev := eventPool.Get().(*Event)537 ev := eventPool.Get().(*Event)
575 *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}538 *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
@@ -588,11 +551,11 @@ func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
588 }551 }
589 *ev = Event{}552 *ev = Event{}
590 eventPool.Put(ev)553 eventPool.Put(ev)
591- return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}554+ return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
592 }555 }
593 // For a zombie the descriptors were still consumed above so the stream556 // For a zombie the descriptors were still consumed above so the stream
594 // stays aligned; dispatchFrame closes them without running any handler.557 // stays aligned; dispatchFrame closes them without running any handler.
595- return obj.(Object), ev, 0, nil558+ return obj.(Object), ev, nil
596 }559 }
597 560
598 type signatureKey struct {561 type signatureKey struct {
@@ -609,27 +572,6 @@ func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature
609 d.signatures.Store(signatureKey{object, opcode}, signature)572 d.signatures.Store(signatureKey{object, opcode}, signature)
610 }573 }
611 574
612-// notifyListeners runs the listeners registered for an object and opcode.
613-func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
614- listeners, ok := d.listeners.Load(objectID)
615- if !ok {
616- return
617- }
618- opcodeMap, ok := listeners.(*sync.Map)
619- if !ok {
620- return
621- }
622- handlers, ok := opcodeMap.Load(opcode)
623- if !ok {
624- return
625- }
626- for _, handler := range handlers.([]func([]byte)) {
627- if handler != nil {
628- handler(body)
629- }
630- }
631-}
632-
633 // handleDisplayEvent handles events on the display object575 // handleDisplayEvent handles events on the display object
634 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {576 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
635 switch opcode {577 switch opcode {
@@ -640,12 +582,7 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
640 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}582 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
641 objectID := event.Uint32()583 objectID := event.Uint32()
642 code := event.Uint32()584 code := event.Uint32()
643- message := event.String()585+ return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
644-
645- d.lastErrorCode = code
646- d.lastErrorObj = objectID
647- d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message}
648- return d.lastError
649 586
650 case 1: // delete_id587 case 1: // delete_id
651 if len(data) != 4 {588 if len(data) != 4 {
@@ -692,35 +629,9 @@ func (d *Display) Roundtrip() error {
692 }629 }
693 }630 }
694 631
695-// AddListener adds an event listener for an object632+// getRegistry sends wl_display.get_registry for the bootstrap registry.
696-func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
697- if handler == nil {
698- return
699- }
700- // Load or create listener map for object
701- listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
702- opcodeMap := listeners.(*sync.Map)
703-
704- d.listenerMu.Lock()
705- defer d.listenerMu.Unlock()
706- var handlers []func([]byte)
707- if old, ok := opcodeMap.Load(opcode); ok {
708- handlers = old.([]func([]byte))
709- }
710- next := make([]func([]byte), len(handlers)+1)
711- copy(next, handlers)
712- next[len(handlers)] = handler
713- opcodeMap.Store(opcode, next)
714-}
715-
716-// getRegistry gets the global registry
717 func (d *Display) getRegistry() error {633 func (d *Display) getRegistry() error {
718- // Add registry listeners634+ return d.SendRequest(displayObjectID, 1, d.registry.id)
719- d.AddListener(d.registry.id, 0, d.registry.handleGlobal)
720- d.AddListener(d.registry.id, 1, d.registry.handleGlobalRemove)
721-
722- // Send get_registry request (opcode 1)
723- return d.SendRequest(1, 1, d.registry.id)
724 }635 }
725 636
726 // Registry returns the global registry637 // Registry returns the global registry
@@ -733,59 +644,40 @@ func (r *Registry) ID() uint32 {
733 return r.id644 return r.id
734 }645 }
735 646
736-// handleGlobal handles global announcements647+// EventSignature reports the wl_registry event signatures.
737-func (r *Registry) handleGlobal(data []byte) {648+func (r *Registry) EventSignature(opcode uint16) (string, bool) {
738- if len(data) < 8 {649+ switch opcode {
739- return650+ case 0:
740- }651+ return "uint,string,uint,", true
741-652+ case 1:
742- name := binary.LittleEndian.Uint32(data[0:4])653+ return "uint,", true
743- ifaceLen := binary.LittleEndian.Uint32(data[4:8])
744-
745- if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
746- return
747 }654 }
655+ return "", false
656+}
748 657
749- // String includes null terminator in length658+// Dispatch handles wl_registry.global and wl_registry.global_remove. The
750- iface := string(data[8 : 8+ifaceLen-1]) // -1 to remove null terminator659+// transport has already validated the body against EventSignature.
751-660+func (r *Registry) Dispatch(event *Event) {
752- // Calculate padding for 32-bit alignment661+ switch event.Opcode {
753- padding := (4 - (ifaceLen % 4)) % 4662+ case 0:
754- versionOffset := 8 + int(ifaceLen) + int(padding)663+ r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
755-664+ case 1:
756- if len(data) != versionOffset+4 {665+ r.handleGlobalRemove(event.Uint32())
757- return
758 }666 }
667+}
759 668
760- version := binary.LittleEndian.Uint32(data[versionOffset:])669+func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
761-
762- // Store global
763 r.mu.Lock()670 r.mu.Lock()
764- r.globals[name] = Global{671+ r.globals[name] = Global{Name: name, Interface: iface, Version: version}
765- Name: name,
766- Interface: iface,
767- Version: version,
768- }
769- r.mu.Unlock()
770-
771- // Call specific handler if registered
772- r.mu.RLock()
773 handlers := append([]GlobalHandler(nil), r.handlers[iface]...)672 handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
774 handlers = append(handlers, r.handlers["*"]...)673 handlers = append(handlers, r.handlers["*"]...)
775- r.mu.RUnlock()674+ r.mu.Unlock()
776 for _, handler := range handlers {675 for _, handler := range handlers {
777 handler(r, name, version)676 handler(r, name, version)
778 }677 }
779 }678 }
780 679
781-// handleGlobalRemove handles global removal680+func (r *Registry) handleGlobalRemove(name uint32) {
782-func (r *Registry) handleGlobalRemove(data []byte) {
783- if len(data) < 4 {
784- return
785- }
786-
787- name := binary.LittleEndian.Uint32(data[0:4])
788-
789 r.mu.Lock()681 r.mu.Lock()
790 delete(r.globals, name)682 delete(r.globals, name)
791 handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)683 handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
M client_test.go +0 -98
@@ -40,86 +40,6 @@ func TestFixed(t *testing.T) {
4040 }
4141 }
4242
43-func TestEventPool(t *testing.T) {
44- // Test that the global event pool works without requiring a compositor
45- // Get an event
46- event := eventPool.Get().(*Event)
47- if event == nil {
48- t.Fatal("Expected event from pool, got nil")
49- }
50-
51- // Set some data
52- event.ProxyID = 123
53- event.Opcode = 456
54-
55- // Return to pool
56- eventPool.Put(event)
57-
58- // Get another event - should be reused
59- event2 := eventPool.Get().(*Event)
60- if event2 == nil {
61- t.Fatal("Expected reused event from pool, got nil")
62- }
63-
64- // The pool might return a clean event or the same one, both are valid
65- t.Logf("Event2: ProxyID=%d, Opcode=%d", event2.ProxyID, event2.Opcode)
66-
67- // Clean up
68- eventPool.Put(event2)
69-}
70-
71-func TestEventDispatcher(t *testing.T) {
72- dispatcher := NewEventDispatcher()
73-
74- called := false
75- handler := func(event *Event) {
76- called = true
77- if event.ProxyID != 123 || event.Opcode != 1 {
78- t.Errorf("Expected ProxyID=123, Opcode=1, got ProxyID=%d, Opcode=%d", event.ProxyID, event.Opcode)
79- }
80- }
81-
82- // Register handler
83- dispatcher.RegisterHandler(123, 1, handler)
84-
85- // Dispatch event
86- dispatcher.Dispatch(123, 1, []byte{})
87-
88- if !called {
89- t.Error("Handler should have been called")
90- }
91-}
92-
93-func TestEventDispatcherMultipleHandlers(t *testing.T) {
94- dispatcher := NewEventDispatcher()
95-
96- called1 := false
97- called2 := false
98-
99- handler1 := func(event *Event) {
100- called1 = true
101- }
102-
103- handler2 := func(event *Event) {
104- called2 = true
105- }
106-
107- // Register handlers for different opcodes
108- dispatcher.RegisterHandler(123, 1, handler1)
109- dispatcher.RegisterHandler(123, 2, handler2)
110-
111- // Dispatch different events
112- dispatcher.Dispatch(123, 1, []byte{})
113- dispatcher.Dispatch(123, 2, []byte{})
114-
115- if !called1 {
116- t.Error("Handler1 should have been called")
117- }
118- if !called2 {
119- t.Error("Handler2 should have been called")
120- }
121-}
122-
12343 func TestMessageMarshalingBasic(t *testing.T) {
12444 // Create a display to test marshaling (without connecting)
12545 d := &Display{
@@ -339,24 +259,6 @@ func TestRegistryGlobalStorage(t *testing.T) {
339259 }
340260 }
341261
342-func BenchmarkEventDispatch(b *testing.B) {
343- dispatcher := NewEventDispatcher()
344-
345- handler := func(event *Event) {
346- // Minimal handler for benchmarking
347- }
348-
349- // Register handler
350- dispatcher.RegisterHandler(123, 1, handler)
351-
352- data := []byte{0x01, 0x02, 0x03, 0x04}
353-
354- b.ResetTimer()
355- for i := 0; i < b.N; i++ {
356- dispatcher.Dispatch(123, 1, data)
357- }
358-}
359-
360262 func BenchmarkFixedConversion(b *testing.B) {
361263 values := []float64{1.0, 0.5, 123.456, -1.5, 256.789}
362264
@@ -40,86 +40,6 @@ func TestFixed(t *testing.T) {
40 }40 }
41 }41 }
42 42
43-func TestEventPool(t *testing.T) {
44- // Test that the global event pool works without requiring a compositor
45- // Get an event
46- event := eventPool.Get().(*Event)
47- if event == nil {
48- t.Fatal("Expected event from pool, got nil")
49- }
50-
51- // Set some data
52- event.ProxyID = 123
53- event.Opcode = 456
54-
55- // Return to pool
56- eventPool.Put(event)
57-
58- // Get another event - should be reused
59- event2 := eventPool.Get().(*Event)
60- if event2 == nil {
61- t.Fatal("Expected reused event from pool, got nil")
62- }
63-
64- // The pool might return a clean event or the same one, both are valid
65- t.Logf("Event2: ProxyID=%d, Opcode=%d", event2.ProxyID, event2.Opcode)
66-
67- // Clean up
68- eventPool.Put(event2)
69-}
70-
71-func TestEventDispatcher(t *testing.T) {
72- dispatcher := NewEventDispatcher()
73-
74- called := false
75- handler := func(event *Event) {
76- called = true
77- if event.ProxyID != 123 || event.Opcode != 1 {
78- t.Errorf("Expected ProxyID=123, Opcode=1, got ProxyID=%d, Opcode=%d", event.ProxyID, event.Opcode)
79- }
80- }
81-
82- // Register handler
83- dispatcher.RegisterHandler(123, 1, handler)
84-
85- // Dispatch event
86- dispatcher.Dispatch(123, 1, []byte{})
87-
88- if !called {
89- t.Error("Handler should have been called")
90- }
91-}
92-
93-func TestEventDispatcherMultipleHandlers(t *testing.T) {
94- dispatcher := NewEventDispatcher()
95-
96- called1 := false
97- called2 := false
98-
99- handler1 := func(event *Event) {
100- called1 = true
101- }
102-
103- handler2 := func(event *Event) {
104- called2 = true
105- }
106-
107- // Register handlers for different opcodes
108- dispatcher.RegisterHandler(123, 1, handler1)
109- dispatcher.RegisterHandler(123, 2, handler2)
110-
111- // Dispatch different events
112- dispatcher.Dispatch(123, 1, []byte{})
113- dispatcher.Dispatch(123, 2, []byte{})
114-
115- if !called1 {
116- t.Error("Handler1 should have been called")
117- }
118- if !called2 {
119- t.Error("Handler2 should have been called")
120- }
121-}
122-
123 func TestMessageMarshalingBasic(t *testing.T) {43 func TestMessageMarshalingBasic(t *testing.T) {
124 // Create a display to test marshaling (without connecting)44 // Create a display to test marshaling (without connecting)
125 d := &Display{45 d := &Display{
@@ -339,24 +259,6 @@ func TestRegistryGlobalStorage(t *testing.T) {
339 }259 }
340 }260 }
341 261
342-func BenchmarkEventDispatch(b *testing.B) {
343- dispatcher := NewEventDispatcher()
344-
345- handler := func(event *Event) {
346- // Minimal handler for benchmarking
347- }
348-
349- // Register handler
350- dispatcher.RegisterHandler(123, 1, handler)
351-
352- data := []byte{0x01, 0x02, 0x03, 0x04}
353-
354- b.ResetTimer()
355- for i := 0; i < b.N; i++ {
356- dispatcher.Dispatch(123, 1, data)
357- }
358-}
359-
360 func BenchmarkFixedConversion(b *testing.B) {262 func BenchmarkFixedConversion(b *testing.B) {
361 values := []float64{1.0, 0.5, 123.456, -1.5, 256.789}263 values := []float64{1.0, 0.5, 123.456, -1.5, 256.789}
362 264
M core.go +4 -0
@@ -30,6 +30,10 @@ type BaseProxy struct {
3030 context *Context
3131 }
3232
33+// eventPool recycles Events across dispatches; an Event is only valid for
34+// the duration of the handler call.
35+var eventPool = sync.Pool{New: func() any { return &Event{} }}
36+
3337 // Event represents a Wayland protocol event
3438 type Event struct {
3539 ProxyID uint32
@@ -30,6 +30,10 @@ type BaseProxy struct {
30 context *Context30 context *Context
31 }31 }
32 32
33+// eventPool recycles Events across dispatches; an Event is only valid for
34+// the duration of the handler call.
35+var eventPool = sync.Pool{New: func() any { return &Event{} }}
36+
33 // Event represents a Wayland protocol event37 // Event represents a Wayland protocol event
34 type Event struct {38 type Event struct {
35 ProxyID uint3239 ProxyID uint32
D event_pool.go +0 -185
@@ -1,185 +0,0 @@
1-//go:build linux
2-// +build linux
3-
4-package wlturbo
5-
6-import (
7- "sync"
8- "sync/atomic"
9-)
10-
11-// Event pool for zero-allocation event handling
12-var eventPool = sync.Pool{
13- New: func() interface{} {
14- return &Event{
15- data: make([]byte, 0, 4096), // Pre-allocate 4KB
16- }
17- },
18-}
19-
20-// EventHandler is a function type for handling events with zero allocations
21-type EventHandler func(event *Event)
22-
23-// EventDispatcher handles high-performance event dispatching
24-type EventDispatcher struct {
25- // Lock-free handler lookup using atomic operations
26- handlers [1024]atomic.Pointer[handlerEntry] // Fixed-size array for object IDs 0-1023
27-
28- // Extended handlers for object IDs >= 1024 (rare case)
29- extHandlers sync.Map
30-}
31-
32-// handlerEntry stores handlers for a specific object
33-type handlerEntry struct {
34- // Array of handlers indexed by opcode (most objects have < 16 opcodes)
35- handlers [32]EventHandler
36-
37- // Extended handlers for opcodes >= 32 (very rare)
38- extHandlers *sync.Map
39-}
40-
41-// NewEventDispatcher creates a high-performance event dispatcher
42-func NewEventDispatcher() *EventDispatcher {
43- return &EventDispatcher{}
44-}
45-
46-// RegisterHandler registers an event handler with zero allocation in common case
47-func (d *EventDispatcher) RegisterHandler(objectID uint32, opcode uint16, handler EventHandler) {
48- if objectID < 1024 {
49- // Fast path: use lock-free array
50- for {
51- entry := d.handlers[objectID].Load()
52- if entry == nil {
53- // Create new entry
54- newEntry := &handlerEntry{}
55- if opcode < 32 {
56- newEntry.handlers[opcode] = handler
57- } else {
58- newEntry.extHandlers = &sync.Map{}
59- newEntry.extHandlers.Store(opcode, handler)
60- }
61-
62- // Try to set atomically
63- if d.handlers[objectID].CompareAndSwap(nil, newEntry) {
64- return
65- }
66- // Retry if someone else created it
67- continue
68- }
69-
70- // Entry exists, update it
71- if opcode < 32 {
72- // Direct array access (no locking needed for write)
73- entry.handlers[opcode] = handler
74- } else {
75- // Extended handlers
76- if entry.extHandlers == nil {
77- entry.extHandlers = &sync.Map{}
78- }
79- entry.extHandlers.Store(opcode, handler)
80- }
81- return
82- }
83- } else {
84- // Slow path: use sync.Map for large object IDs
85- entry, _ := d.extHandlers.LoadOrStore(objectID, &handlerEntry{})
86- h := entry.(*handlerEntry)
87-
88- if opcode < 32 {
89- h.handlers[opcode] = handler
90- } else {
91- if h.extHandlers == nil {
92- h.extHandlers = &sync.Map{}
93- }
94- h.extHandlers.Store(opcode, handler)
95- }
96- }
97-}
98-
99-// Dispatch dispatches an event with minimal overhead
100-//
101-//go:inline
102-func (d *EventDispatcher) Dispatch(objectID uint32, opcode uint16, data []byte) {
103- var handler EventHandler
104-
105- // Fast lookup path
106- if objectID < 1024 {
107- entry := d.handlers[objectID].Load()
108- if entry != nil {
109- if opcode < 32 {
110- handler = entry.handlers[opcode]
111- } else if entry.extHandlers != nil {
112- if h, ok := entry.extHandlers.Load(opcode); ok {
113- handler = h.(EventHandler)
114- }
115- }
116- }
117- } else {
118- // Slow path for large object IDs
119- if entry, ok := d.extHandlers.Load(objectID); ok {
120- h := entry.(*handlerEntry)
121- if opcode < 32 {
122- handler = h.handlers[opcode]
123- } else if h.extHandlers != nil {
124- if fn, ok := h.extHandlers.Load(opcode); ok {
125- handler = fn.(EventHandler)
126- }
127- }
128- }
129- }
130-
131- if handler == nil {
132- return
133- }
134-
135- // Get event from pool
136- event := eventPool.Get().(*Event)
137- event.ProxyID = objectID
138- event.Opcode = opcode
139- event.data = append(event.data[:0], data...) // Reuse backing array
140- event.offset = 0
141-
142- // Call handler
143- handler(event)
144-
145- // Return to pool
146- eventPool.Put(event)
147-}
148-
149-// BatchDispatch processes multiple events in a batch for better cache locality
150-func (d *EventDispatcher) BatchDispatch(events []RawEvent) {
151- // Process events in batches to improve cache performance
152- for i := range events {
153- e := &events[i]
154- d.Dispatch(e.ObjectID, e.Opcode, e.Data)
155- }
156-}
157-
158-// RawEvent represents a raw event before dispatch
159-type RawEvent struct {
160- ObjectID uint32
161- Opcode uint16
162- Data []byte
163-}
164-
165-// Ensure cache line alignment for hot data structures
166-type cacheLinePad [64]byte
167-
168-// PerformanceStats tracks dispatcher performance
169-type PerformanceStats struct {
170- EventsDispatched atomic.Uint64
171- _ cacheLinePad
172- NanosecondsSpent atomic.Uint64
173- _ cacheLinePad
174- CacheMisses atomic.Uint64
175- _ cacheLinePad
176-}
177-
178-// GetAverageLatency returns average dispatch latency in nanoseconds
179-func (s *PerformanceStats) GetAverageLatency() uint64 {
180- events := s.EventsDispatched.Load()
181- if events == 0 {
182- return 0
183- }
184- return s.NanosecondsSpent.Load() / events
185-}
@@ -1,185 +0,0 @@
1-//go:build linux
2-// +build linux
3-
4-package wlturbo
5-
6-import (
7- "sync"
8- "sync/atomic"
9-)
10-
11-// Event pool for zero-allocation event handling
12-var eventPool = sync.Pool{
13- New: func() interface{} {
14- return &Event{
15- data: make([]byte, 0, 4096), // Pre-allocate 4KB
16- }
17- },
18-}
19-
20-// EventHandler is a function type for handling events with zero allocations
21-type EventHandler func(event *Event)
22-
23-// EventDispatcher handles high-performance event dispatching
24-type EventDispatcher struct {
25- // Lock-free handler lookup using atomic operations
26- handlers [1024]atomic.Pointer[handlerEntry] // Fixed-size array for object IDs 0-1023
27-
28- // Extended handlers for object IDs >= 1024 (rare case)
29- extHandlers sync.Map
30-}
31-
32-// handlerEntry stores handlers for a specific object
33-type handlerEntry struct {
34- // Array of handlers indexed by opcode (most objects have < 16 opcodes)
35- handlers [32]EventHandler
36-
37- // Extended handlers for opcodes >= 32 (very rare)
38- extHandlers *sync.Map
39-}
40-
41-// NewEventDispatcher creates a high-performance event dispatcher
42-func NewEventDispatcher() *EventDispatcher {
43- return &EventDispatcher{}
44-}
45-
46-// RegisterHandler registers an event handler with zero allocation in common case
47-func (d *EventDispatcher) RegisterHandler(objectID uint32, opcode uint16, handler EventHandler) {
48- if objectID < 1024 {
49- // Fast path: use lock-free array
50- for {
51- entry := d.handlers[objectID].Load()
52- if entry == nil {
53- // Create new entry
54- newEntry := &handlerEntry{}
55- if opcode < 32 {
56- newEntry.handlers[opcode] = handler
57- } else {
58- newEntry.extHandlers = &sync.Map{}
59- newEntry.extHandlers.Store(opcode, handler)
60- }
61-
62- // Try to set atomically
63- if d.handlers[objectID].CompareAndSwap(nil, newEntry) {
64- return
65- }
66- // Retry if someone else created it
67- continue
68- }
69-
70- // Entry exists, update it
71- if opcode < 32 {
72- // Direct array access (no locking needed for write)
73- entry.handlers[opcode] = handler
74- } else {
75- // Extended handlers
76- if entry.extHandlers == nil {
77- entry.extHandlers = &sync.Map{}
78- }
79- entry.extHandlers.Store(opcode, handler)
80- }
81- return
82- }
83- } else {
84- // Slow path: use sync.Map for large object IDs
85- entry, _ := d.extHandlers.LoadOrStore(objectID, &handlerEntry{})
86- h := entry.(*handlerEntry)
87-
88- if opcode < 32 {
89- h.handlers[opcode] = handler
90- } else {
91- if h.extHandlers == nil {
92- h.extHandlers = &sync.Map{}
93- }
94- h.extHandlers.Store(opcode, handler)
95- }
96- }
97-}
98-
99-// Dispatch dispatches an event with minimal overhead
100-//
101-//go:inline
102-func (d *EventDispatcher) Dispatch(objectID uint32, opcode uint16, data []byte) {
103- var handler EventHandler
104-
105- // Fast lookup path
106- if objectID < 1024 {
107- entry := d.handlers[objectID].Load()
108- if entry != nil {
109- if opcode < 32 {
110- handler = entry.handlers[opcode]
111- } else if entry.extHandlers != nil {
112- if h, ok := entry.extHandlers.Load(opcode); ok {
113- handler = h.(EventHandler)
114- }
115- }
116- }
117- } else {
118- // Slow path for large object IDs
119- if entry, ok := d.extHandlers.Load(objectID); ok {
120- h := entry.(*handlerEntry)
121- if opcode < 32 {
122- handler = h.handlers[opcode]
123- } else if h.extHandlers != nil {
124- if fn, ok := h.extHandlers.Load(opcode); ok {
125- handler = fn.(EventHandler)
126- }
127- }
128- }
129- }
130-
131- if handler == nil {
132- return
133- }
134-
135- // Get event from pool
136- event := eventPool.Get().(*Event)
137- event.ProxyID = objectID
138- event.Opcode = opcode
139- event.data = append(event.data[:0], data...) // Reuse backing array
140- event.offset = 0
141-
142- // Call handler
143- handler(event)
144-
145- // Return to pool
146- eventPool.Put(event)
147-}
148-
149-// BatchDispatch processes multiple events in a batch for better cache locality
150-func (d *EventDispatcher) BatchDispatch(events []RawEvent) {
151- // Process events in batches to improve cache performance
152- for i := range events {
153- e := &events[i]
154- d.Dispatch(e.ObjectID, e.Opcode, e.Data)
155- }
156-}
157-
158-// RawEvent represents a raw event before dispatch
159-type RawEvent struct {
160- ObjectID uint32
161- Opcode uint16
162- Data []byte
163-}
164-
165-// Ensure cache line alignment for hot data structures
166-type cacheLinePad [64]byte
167-
168-// PerformanceStats tracks dispatcher performance
169-type PerformanceStats struct {
170- EventsDispatched atomic.Uint64
171- _ cacheLinePad
172- NanosecondsSpent atomic.Uint64
173- _ cacheLinePad
174- CacheMisses atomic.Uint64
175- _ cacheLinePad
176-}
177-
178-// GetAverageLatency returns average dispatch latency in nanoseconds
179-func (s *PerformanceStats) GetAverageLatency() uint64 {
180- events := s.EventsDispatched.Load()
181- if events == 0 {
182- return 0
183- }
184- return s.NanosecondsSpent.Load() / events
185-}
M hardening_test.go +1 -1
@@ -51,7 +51,7 @@ func TestRegistryGlobalHandlersAccumulate(t *testing.T) {
5151 d.registry.AddGlobalHandler(countingGlobalHandler{&a})
5252 d.registry.AddGlobalHandler(countingGlobalHandler{&b})
5353 d.registry.AddHandler("abc", func(*Registry, uint32, uint32) { specific++ })
54- d.registry.handleGlobal([]byte{1, 0, 0, 0, 4, 0, 0, 0, 'a', 'b', 'c', 0, 1, 0, 0, 0})
54+ d.registry.handleGlobal(1, "abc", 1)
5555 if a != 1 || b != 1 || specific != 1 {
5656 t.Fatalf("calls a=%d b=%d specific=%d, want 1 each", a, b, specific)
5757 }
@@ -51,7 +51,7 @@ func TestRegistryGlobalHandlersAccumulate(t *testing.T) {
51 d.registry.AddGlobalHandler(countingGlobalHandler{&a})51 d.registry.AddGlobalHandler(countingGlobalHandler{&a})
52 d.registry.AddGlobalHandler(countingGlobalHandler{&b})52 d.registry.AddGlobalHandler(countingGlobalHandler{&b})
53 d.registry.AddHandler("abc", func(*Registry, uint32, uint32) { specific++ })53 d.registry.AddHandler("abc", func(*Registry, uint32, uint32) { specific++ })
54- d.registry.handleGlobal([]byte{1, 0, 0, 0, 4, 0, 0, 0, 'a', 'b', 'c', 0, 1, 0, 0, 0})54+ d.registry.handleGlobal(1, "abc", 1)
55 if a != 1 || b != 1 || specific != 1 {55 if a != 1 || b != 1 || specific != 1 {
56 t.Fatalf("calls a=%d b=%d specific=%d, want 1 each", a, b, specific)56 t.Fatalf("calls a=%d b=%d specific=%d, want 1 each", a, b, specific)
57 }57 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close