diff --git a/README.md b/README.md index 1c7f8a6..4b2e719 100644 --- a/README.md +++ b/README.md @@ -73,46 +73,47 @@ goos: linux goarch: amd64 pkg: github.com/gortc/stun PASS -benchmark iter time/iter throughput bytes alloc allocs ---------- ---- --------- ---------- ----------- ------ -BenchmarkMappedAddress_AddTo-12 100000000 22.50 ns/op 0 B/op 0 allocs/op -BenchmarkAlternateServer_AddTo-12 100000000 22.20 ns/op 0 B/op 0 allocs/op -BenchmarkAgent_GC-12 1000000 2038.00 ns/op 0 B/op 0 allocs/op -BenchmarkAgent_Process-12 30000000 47.60 ns/op 0 B/op 0 allocs/op -BenchmarkMessage_GetNotFound-12 300000000 4.29 ns/op 0 B/op 0 allocs/op -BenchmarkClient_Do-12 2000000 534.00 ns/op 16 B/op 1 allocs/op -BenchmarkErrorCode_AddTo-12 30000000 42.00 ns/op 0 B/op 0 allocs/op -BenchmarkErrorCodeAttribute_AddTo-12 50000000 30.90 ns/op 0 B/op 0 allocs/op -BenchmarkErrorCodeAttribute_GetFrom-12 200000000 7.78 ns/op 0 B/op 0 allocs/op -BenchmarkFingerprint_AddTo-12 30000000 47.30 ns/op 931.09 MB/s 0 B/op 0 allocs/op -BenchmarkFingerprint_Check-12 50000000 38.20 ns/op 1360.04 MB/s 0 B/op 0 allocs/op -BenchmarkBuildOverhead/Build-12 10000000 139.00 ns/op 0 B/op 0 allocs/op -BenchmarkBuildOverhead/BuildNonPointer-12 5000000 249.00 ns/op 100 B/op 4 allocs/op -BenchmarkBuildOverhead/Raw-12 20000000 114.00 ns/op 0 B/op 0 allocs/op -BenchmarkMessageIntegrity_AddTo-12 2000000 1022.00 ns/op 19.56 MB/s 480 B/op 6 allocs/op -BenchmarkMessageIntegrity_Check-12 1000000 1084.00 ns/op 29.50 MB/s 480 B/op 6 allocs/op -BenchmarkMessage_Write-12 100000000 16.10 ns/op 1743.03 MB/s 0 B/op 0 allocs/op -BenchmarkMessageType_Value-12 2000000000 0.23 ns/op 0 B/op 0 allocs/op -BenchmarkMessage_WriteTo-12 200000000 8.11 ns/op 0 B/op 0 allocs/op -BenchmarkMessage_ReadFrom-12 100000000 18.30 ns/op 1095.75 MB/s 0 B/op 0 allocs/op -BenchmarkMessage_ReadBytes-12 100000000 10.70 ns/op 1870.83 MB/s 0 B/op 0 allocs/op -BenchmarkIsMessage-12 2000000000 0.68 ns/op 29576.35 MB/s 0 B/op 0 allocs/op -BenchmarkMessage_NewTransactionID-12 3000000 393.00 ns/op 0 B/op 0 allocs/op -BenchmarkMessageFull-12 10000000 138.00 ns/op 0 B/op 0 allocs/op -BenchmarkMessageFullHardcore-12 30000000 53.90 ns/op 0 B/op 0 allocs/op -BenchmarkMessage_WriteHeader-12 300000000 5.45 ns/op 0 B/op 0 allocs/op -BenchmarkUsername_AddTo-12 100000000 15.00 ns/op 0 B/op 0 allocs/op -BenchmarkUsername_GetFrom-12 100000000 11.90 ns/op 0 B/op 0 allocs/op -BenchmarkNonce_AddTo-12 100000000 20.70 ns/op 0 B/op 0 allocs/op -BenchmarkNonce_AddTo_BadLength-12 100000000 27.40 ns/op 32 B/op 1 allocs/op -BenchmarkNonce_GetFrom-12 200000000 11.90 ns/op 0 B/op 0 allocs/op -BenchmarkUnknownAttributes/AddTo-12 100000000 18.90 ns/op 0 B/op 0 allocs/op -BenchmarkUnknownAttributes/GetFrom-12 100000000 13.70 ns/op 0 B/op 0 allocs/op -BenchmarkXOR-12 100000000 14.60 ns/op 70066.01 MB/s 0 B/op 0 allocs/op -BenchmarkXORSafe-12 20000000 98.80 ns/op 10363.12 MB/s 0 B/op 0 allocs/op -BenchmarkXORFast-12 100000000 13.80 ns/op 74327.93 MB/s 0 B/op 0 allocs/op -BenchmarkXORMappedAddress_AddTo-12 50000000 35.20 ns/op 0 B/op 0 allocs/op -BenchmarkXORMappedAddress_GetFrom-12 100000000 22.30 ns/op 0 B/op 0 allocs/op +benchmark iter time/iter throughput bytes alloc allocs +--------- ---- --------- ---------- ----------- ------ +BenchmarkMappedAddress_AddTo-12 1000000000 22.90 ns/op 0 B/op 0 allocs/op +BenchmarkAlternateServer_AddTo-12 1000000000 23.00 ns/op 0 B/op 0 allocs/op +BenchmarkAgent_GC-12 10000000 2217.00 ns/op 0 B/op 0 allocs/op +BenchmarkAgent_Process-12 300000000 52.80 ns/op 0 B/op 0 allocs/op +BenchmarkMessage_GetNotFound-12 3000000000 4.13 ns/op 0 B/op 0 allocs/op +BenchmarkClient_Do-12 30000000 495.00 ns/op 0 B/op 0 allocs/op +BenchmarkErrorCode_AddTo-12 300000000 41.10 ns/op 0 B/op 0 allocs/op +BenchmarkErrorCodeAttribute_AddTo-12 500000000 30.90 ns/op 0 B/op 0 allocs/op +BenchmarkErrorCodeAttribute_GetFrom-12 2000000000 7.84 ns/op 0 B/op 0 allocs/op +BenchmarkFingerprint_AddTo-12 300000000 46.60 ns/op 944.52 MB/s 0 B/op 0 allocs/op +BenchmarkFingerprint_Check-12 500000000 37.20 ns/op 1397.50 MB/s 0 B/op 0 allocs/op +BenchmarkBuildOverhead/Build-12 100000000 133.00 ns/op 0 B/op 0 allocs/op +BenchmarkBuildOverhead/BuildNonPointer-12 100000000 262.00 ns/op 100 B/op 4 allocs/op +BenchmarkBuildOverhead/Raw-12 100000000 119.00 ns/op 0 B/op 0 allocs/op +BenchmarkMessageIntegrity_AddTo-12 20000000 1058.00 ns/op 18.89 MB/s 480 B/op 6 allocs/op +BenchmarkMessageIntegrity_Check-12 20000000 1023.00 ns/op 31.27 MB/s 480 B/op 6 allocs/op +BenchmarkMessage_Write-12 1000000000 16.10 ns/op 1734.19 MB/s 0 B/op 0 allocs/op +BenchmarkMessageType_Value-12 10000000000 0.23 ns/op 0 B/op 0 allocs/op +BenchmarkMessage_WriteTo-12 2000000000 8.19 ns/op 0 B/op 0 allocs/op +BenchmarkMessage_ReadFrom-12 1000000000 18.00 ns/op 1108.32 MB/s 0 B/op 0 allocs/op +BenchmarkMessage_ReadBytes-12 2000000000 10.50 ns/op 1896.39 MB/s 0 B/op 0 allocs/op +BenchmarkIsMessage-12 10000000000 0.64 ns/op 31241.13 MB/s 0 B/op 0 allocs/op +BenchmarkMessage_NewTransactionID-12 50000000 385.00 ns/op 0 B/op 0 allocs/op +BenchmarkMessageFull-12 100000000 134.00 ns/op 0 B/op 0 allocs/op +BenchmarkMessageFullHardcore-12 300000000 52.70 ns/op 0 B/op 0 allocs/op +BenchmarkMessage_WriteHeader-12 3000000000 5.21 ns/op 0 B/op 0 allocs/op +BenchmarkUsername_AddTo-12 1000000000 14.60 ns/op 0 B/op 0 allocs/op +BenchmarkUsername_GetFrom-12 2000000000 11.70 ns/op 0 B/op 0 allocs/op +BenchmarkNonce_AddTo-12 1000000000 20.00 ns/op 0 B/op 0 allocs/op +BenchmarkNonce_AddTo_BadLength-12 300000000 49.40 ns/op 32 B/op 1 allocs/op +BenchmarkNonce_GetFrom-12 2000000000 11.80 ns/op 0 B/op 0 allocs/op +BenchmarkUnknownAttributes/AddTo-12 1000000000 18.50 ns/op 0 B/op 0 allocs/op +BenchmarkUnknownAttributes/GetFrom-12 1000000000 13.40 ns/op 0 B/op 0 allocs/op +BenchmarkXOR-12 1000000000 14.00 ns/op 73071.52 MB/s 0 B/op 0 allocs/op +BenchmarkXORSafe-12 100000000 102.00 ns/op 9945.44 MB/s 0 B/op 0 allocs/op +BenchmarkXORFast-12 1000000000 13.60 ns/op 75332.68 MB/s 0 B/op 0 allocs/op +BenchmarkXORMappedAddress_AddTo-12 500000000 35.20 ns/op 0 B/op 0 allocs/op +BenchmarkXORMappedAddress_GetFrom-12 1000000000 22.30 ns/op 0 B/op 0 allocs/op +ok github.com/gortc/stun 698.712s ``` # development diff --git a/agent.go b/agent.go index abe82b6..a5c97c0 100644 --- a/agent.go +++ b/agent.go @@ -8,7 +8,7 @@ import ( // AgentOptions are required to initialize Agent. type AgentOptions struct { - Handler AgentFn // Default handler, can be nil. + Handler Handler // Default handler, can be nil. } // NewAgent initializes and returns new Agent from options. @@ -32,17 +32,28 @@ type Agent struct { transactions map[transactionID]agentTransaction closed bool // all calls are invalid if true mux sync.Mutex // protects transactions and closed - zeroHandler AgentFn // handles non-registered transactions if set + zeroHandler Handler // handles non-registered transactions if set } -// AgentFn is called on transaction state change. -// Usage of e is valid only during call, user must -// copy needed fields explicitly. -type AgentFn func(e AgentEvent) +// Handler handles state changes of transaction. +type Handler interface { + // HandleEvent is called on transaction state change. + // Usage of e is valid only during call, user must + // copy needed fields explicitly. + HandleEvent(e Event) +} -// AgentEvent is set of arguments passed to AgentFn, describing +// HandlerFunc is function that implements Handler interface. +type HandlerFunc func(e Event) + +// HandleEvent implements Handler. +func (f HandlerFunc) HandleEvent(e Event) { + f(e) +} + +// Event is set of arguments passed to AgentFn, describing // an transaction event. Do not reuse outside AgentFn. -type AgentEvent struct { +type Event struct { Message *Message Error error } @@ -54,7 +65,7 @@ type AgentEvent struct { type agentTransaction struct { id transactionID deadline time.Time - f AgentFn + h Handler } var ( @@ -81,7 +92,7 @@ func (a *Agent) StopWithError(id [TransactionIDSize]byte, err error) error { if !exists { return ErrTransactionNotExists } - t.f(AgentEvent{ + t.h.HandleEvent(Event{ Error: err, }) return nil @@ -101,7 +112,7 @@ var ErrAgentClosed = errors.New("agent is closed") // Could return ErrAgentClosed, ErrTransactionExists. // Callback f is guaranteed to be eventually called. See AgentFn for // callback processing constraints. -func (a *Agent) Start(id [TransactionIDSize]byte, deadline time.Time, f AgentFn) error { +func (a *Agent) Start(id [TransactionIDSize]byte, deadline time.Time, h Handler) error { a.mux.Lock() defer a.mux.Unlock() if a.closed { @@ -113,7 +124,7 @@ func (a *Agent) Start(id [TransactionIDSize]byte, deadline time.Time, f AgentFn) } a.transactions[id] = agentTransaction{ id: id, - f: f, + h: h, deadline: deadline, } return nil @@ -132,7 +143,7 @@ var ErrTransactionTimeOut = errors.New("transaction is timed out") // // It is safe to call Collect concurrently but makes no sense. func (a *Agent) Collect(gcTime time.Time) error { - toCall := make([]AgentFn, 0, agentCollectCap) + toCall := make([]Handler, 0, agentCollectCap) toRemove := make([]transactionID, 0, agentCollectCap) a.mux.Lock() if a.closed { @@ -149,7 +160,7 @@ func (a *Agent) Collect(gcTime time.Time) error { for id, t := range a.transactions { if t.deadline.Before(gcTime) { toRemove = append(toRemove, id) - toCall = append(toCall, t.f) + toCall = append(toCall, t.h) } } // Un-registering timed out transactions. @@ -161,11 +172,11 @@ func (a *Agent) Collect(gcTime time.Time) error { a.mux.Unlock() // Sending ErrTransactionTimeOut to all callbacks, blocking // Collect until last one. - event := AgentEvent{ + event := Event{ Error: ErrTransactionTimeOut, } - for _, f := range toCall { - f(event) + for _, handler := range toCall { + handler.HandleEvent(event) } return nil } @@ -175,7 +186,7 @@ func (a *Agent) Collect(gcTime time.Time) error { // handle is not provided, message is silently ignored. // Call blocks until handler returns. func (a *Agent) Process(m *Message) error { - e := AgentEvent{ + e := Event{ Message: m, } a.mux.Lock() @@ -187,9 +198,9 @@ func (a *Agent) Process(m *Message) error { delete(a.transactions, m.TransactionID) a.mux.Unlock() if ok { - t.f(e) + t.h.HandleEvent(e) } else if a.zeroHandler != nil { - a.zeroHandler(e) + a.zeroHandler.HandleEvent(e) } return nil } @@ -197,7 +208,7 @@ func (a *Agent) Process(m *Message) error { // Close terminates all transactions with ErrAgentClosed and renders Agent to // closed state. func (a *Agent) Close() error { - e := AgentEvent{ + e := Event{ Error: ErrAgentClosed, } a.mux.Lock() @@ -206,7 +217,7 @@ func (a *Agent) Close() error { return ErrAgentClosed } for _, t := range a.transactions { - t.f(e) + t.h.HandleEvent(e) } a.transactions = nil a.closed = true diff --git a/agent_test.go b/agent_test.go index 9024bd7..4a8c3b5 100644 --- a/agent_test.go +++ b/agent_test.go @@ -8,22 +8,21 @@ import ( func TestAgent_ProcessInTransaction(t *testing.T) { m := New() a := NewAgent(AgentOptions{ - Handler: func(e AgentEvent) { + Handler: HandlerFunc(func(e Event) { t.Error("should not be called") - }, + }), }) if err := m.NewTransactionID(); err != nil { t.Fatal(err) } - if err := a.Start(m.TransactionID, time.Time{}, func(e AgentEvent) { + if err := a.Start(m.TransactionID, time.Time{}, HandlerFunc(func(e Event) { if e.Error != nil { t.Errorf("got error: %s", e.Error) } if !e.Message.Equal(m) { t.Errorf("%s (got) != %s (expected)", e.Message, m) } - - }); err != nil { + })); err != nil { t.Fatal(err) } if err := a.Process(m); err != nil { @@ -37,14 +36,14 @@ func TestAgent_ProcessInTransaction(t *testing.T) { func TestAgent_Process(t *testing.T) { m := New() a := NewAgent(AgentOptions{ - Handler: func(e AgentEvent) { + Handler: HandlerFunc(func(e Event) { if e.Error != nil { t.Errorf("got error: %s", e.Error) } if !e.Message.Equal(m) { t.Errorf("%s (got) != %s (expected)", e.Message, m) } - }, + }), }) if err := m.NewTransactionID(); err != nil { t.Fatal(err) @@ -91,11 +90,11 @@ func TestAgent_Stop(t *testing.T) { t.Fatalf("unexpected error: %s, should be %s", err, ErrTransactionNotExists) } id := NewTransactionID() - called := make(chan AgentEvent, 1) + called := make(chan Event, 1) timeout := time.Millisecond * 200 - if err := a.Start(id, time.Now().Add(timeout), func(e AgentEvent) { + if err := a.Start(id, time.Now().Add(timeout), HandlerFunc(func(e Event) { called <- e - }); err != nil { + })); err != nil { t.Fatal(err) } if err := a.Stop(id); err != nil { @@ -119,18 +118,18 @@ func TestAgent_Stop(t *testing.T) { } } -var noopHandler = func(e AgentEvent) {} +var noopHandler HandlerFunc = func(e Event) {} func TestAgent_GC(t *testing.T) { a := NewAgent(AgentOptions{ Handler: noopHandler, }) - shouldTimeOut := func(e AgentEvent) { + var shouldTimeOut HandlerFunc = func(e Event) { if e.Error != ErrTransactionTimeOut { t.Errorf("should time out, but got <%s>", e.Error) } } - shouldNotTimeOut := func(e AgentEvent) { + var shouldNotTimeOut HandlerFunc = func(e Event) { if e.Error == ErrTransactionTimeOut { t.Error("should not time out") } diff --git a/client.go b/client.go index 03d7a86..48d7edc 100644 --- a/client.go +++ b/client.go @@ -65,7 +65,7 @@ type Connection interface { type ClientAgent interface { Process(*Message) error Close() error - Start(id [TransactionIDSize]byte, deadline time.Time, f AgentFn) error + Start(id [TransactionIDSize]byte, deadline time.Time, f Handler) error Stop(id [TransactionIDSize]byte) error Collect(time.Time) error } @@ -184,21 +184,14 @@ func (c *Client) Indicate(m *Message) error { return c.Start(m, time.Time{}, nil) } -type clientCallState struct { - callback func(event AgentEvent) +// callbackWaitHandler blocks on wait() call until callback is called. +type callbackWaitHandler struct { + callback func(event Event) cond *sync.Cond processed bool } -func (s *clientCallState) wait() { - s.cond.L.Lock() - for !s.processed { - s.cond.Wait() - } - s.cond.L.Unlock() -} - -func (s *clientCallState) wrapper(e AgentEvent) { +func (s *callbackWaitHandler) HandleEvent(e Event) { if s.callback == nil { panic("s.callback is nil") } @@ -209,66 +202,73 @@ func (s *clientCallState) wrapper(e AgentEvent) { s.cond.L.Unlock() } -func (s *clientCallState) setCallback(f func(event AgentEvent)) { +func (s *callbackWaitHandler) wait() { + s.cond.L.Lock() + for !s.processed { + s.cond.Wait() + } + s.cond.L.Unlock() +} + +func (s *callbackWaitHandler) setCallback(f func(event Event)) { if f == nil { panic("f is nil") } s.callback = f } -func (s *clientCallState) reset() { +func (s *callbackWaitHandler) reset() { s.processed = false s.callback = nil } -func newClientCallState(f func(event AgentEvent)) *clientCallState { - return &clientCallState{ - cond: sync.NewCond(new(sync.Mutex)), - callback: f, - } -} - -var clientCallStatePool = sync.Pool{ +var callbackWaitHandlerPool = sync.Pool{ New: func() interface{} { - return newClientCallState(nil) + return &callbackWaitHandler{ + cond: sync.NewCond(new(sync.Mutex)), + } }, } // Do is Start wrapper that waits until callback is called. If no callback // provided, Indicate is called instead. // -// Do has memory allocation overhead due to blocking, see BenchmarkClient_Do. -// Use Start for zero overhead. -func (c *Client) Do(m *Message, d time.Time, f func(AgentEvent)) error { +// Do has cpu overhead due to blocking, see BenchmarkClient_Do. +// Use Start method for less overhead. +func (c *Client) Do(m *Message, d time.Time, f func(Event)) error { if f == nil { return c.Indicate(m) } - state := clientCallStatePool.Get().(*clientCallState) - state.setCallback(f) - err := c.Start(m, d, state.wrapper) - state.wait() - state.reset() - clientCallStatePool.Put(state) - return err + h := callbackWaitHandlerPool.Get().(*callbackWaitHandler) + h.setCallback(f) + defer func() { + h.reset() + callbackWaitHandlerPool.Put(h) + }() + if err := c.Start(m, d, h); err != nil { + return err + } + h.wait() + return nil } -// Start starts transaction (if f set) and writes message to server, callback +// Start starts transaction (if f set) and writes message to server, handler // is called asynchronously. -func (c *Client) Start(m *Message, d time.Time, f func(AgentEvent)) error { +func (c *Client) Start(m *Message, d time.Time, h Handler) error { c.closedMux.RLock() closed := c.closed c.closedMux.RUnlock() if closed { return ErrClientClosed } - if f != nil { - // Starting transaction only if f is set. Useful for indications. - if err := c.a.Start(m.TransactionID, d, f); err != nil { + if h != nil { + // Starting transaction only if h is set. Useful for indications. + if err := c.a.Start(m.TransactionID, d, h); err != nil { return err } } _, err := m.WriteTo(c.c) - if err != nil && f != nil { + if err != nil && h != nil { // Stopping transaction instead of waiting until deadline. if stopErr := c.a.Stop(m.TransactionID); stopErr != nil { return StopErr{ diff --git a/client_test.go b/client_test.go index 48e062d..56edbb6 100644 --- a/client_test.go +++ b/client_test.go @@ -8,7 +8,7 @@ import ( ) type TestAgent struct { - f chan AgentFn + f chan Handler } func (n *TestAgent) Close() error { @@ -20,7 +20,7 @@ func (TestAgent) Collect(time.Time) error { return nil } func (TestAgent) Process(m *Message) error { return nil } -func (n *TestAgent) Start(id [TransactionIDSize]byte, deadline time.Time, f AgentFn) error { +func (n *TestAgent) Start(id [TransactionIDSize]byte, deadline time.Time, f Handler) error { n.f <- f return nil } @@ -47,7 +47,7 @@ func (noopConnection) Close() error { func BenchmarkClient_Do(b *testing.B) { b.ReportAllocs() agent := &TestAgent{ - f: make(chan AgentFn, 1000), + f: make(chan Handler, 1000), } client := NewClient(ClientOptions{ Agent: agent, @@ -55,17 +55,17 @@ func BenchmarkClient_Do(b *testing.B) { }) defer client.Close() go func() { - e := AgentEvent{ + e := Event{ Error: nil, Message: nil, } for f := range agent.f { - f(e) + f.HandleEvent(e) } }() m := new(Message) m.Encode() - noopF := func(event AgentEvent) { + noopF := func(event Event) { // pass } for i := 0; i < b.N; i++ { @@ -140,7 +140,7 @@ func TestClient_Do(t *testing.T) { m.TransactionID = response.TransactionID m.Encode() d := time.Now().Add(time.Second) - if err := c.Do(m, d, func(event AgentEvent) { + if err := c.Do(m, d, func(event Event) { if event.Error != nil { t.Error(event.Error) } diff --git a/cmd/stun-client/stun-client.go b/cmd/stun-client/stun-client.go index 7808ff5..5cfc0d1 100644 --- a/cmd/stun-client/stun-client.go +++ b/cmd/stun-client/stun-client.go @@ -25,7 +25,7 @@ func main() { log.Fatal("dial:", err) } deadline := time.Now().Add(time.Second * 5) - if err := c.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest), deadline, func(res stun.AgentEvent) { + if err := c.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest), deadline, func(res stun.Event) { if res.Error != nil { log.Fatalln(err) } diff --git a/integration-test/main.go b/integration-test/main.go index 8e5f1c1..459b723 100644 --- a/integration-test/main.go +++ b/integration-test/main.go @@ -44,7 +44,7 @@ func main() { } timeout := time.Second deadline := time.Now().Add(timeout) - if err := client.Do(request, deadline, func(event stun.AgentEvent) { + if err := client.Do(request, deadline, func(event stun.Event) { if event.Error != nil { log.Fatalln("got event with error:", event.Error) }