Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements #32

Merged
merged 10 commits into from
Jun 25, 2024
8 changes: 4 additions & 4 deletions command/call/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ package call

import (
"fmt"
"github.com/percipia/eslgo/command"
"net/textproto"
"strconv"

"github.com/percipia/eslgo/command"
)

type Execute struct {
Expand Down Expand Up @@ -73,7 +74,7 @@ func (e *Execute) BuildMessage() string {
}
sendMsg := command.SendMessage{
UUID: e.UUID,
Headers: make(textproto.MIMEHeader),
Headers: make(textproto.MIMEHeader, 4), // preallocating for the 4+ headers that are always set to reduce amount of dynamic allocations
Sync: e.Sync,
SyncPri: e.SyncPri,
}
Expand All @@ -87,8 +88,7 @@ func (e *Execute) BuildMessage() string {

// According to documentation that is the max header length
if len(e.AppArgs) > 2048 || e.ForceBody {
sendMsg.Headers.Set("content-type", "text/plain")
sendMsg.Headers.Set("content-length", strconv.Itoa(len(e.AppArgs)))
sendMsg.Headers.Set("Content-Type", "text/plain")
sendMsg.Body = e.AppArgs
} else {
sendMsg.Headers.Set("execute-app-arg", e.AppArgs)
Expand Down
35 changes: 20 additions & 15 deletions command/call/execute_test.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
/*
* Copyright (c) 2020 Percipia
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Contributor(s):
* Andrew Querol <[email protected]>
*/
package call

import (
"github.com/stretchr/testify/assert"
"sort"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// Normalizes the headers in a message to ensure they are always in the same order before comparison
func normalizeMessage(message string) string {
parts := strings.Split(message, "\r\n\r\n")
headers := strings.Split(parts[0], "\r\n")
sort.Strings(headers)
normalizedHeaders := strings.Join(headers, "\r\n")

if len(parts) > 1 {
return normalizedHeaders + "\r\n\r\n" + parts[1]
}
return normalizedHeaders
}

var (
TestExecMessage = strings.ReplaceAll(`sendmsg none
Call-Command: execute
Expand Down Expand Up @@ -54,7 +59,7 @@ func TestExecute_BuildMessage(t *testing.T) {
AppName: "playback",
AppArgs: "/tmp/test.wav",
}
assert.Equal(t, TestExecMessage, exec.BuildMessage())
assert.Equal(t, normalizeMessage(TestExecMessage), normalizeMessage(exec.BuildMessage()))
}

func TestSet_BuildMessage(t *testing.T) {
Expand All @@ -63,7 +68,7 @@ func TestSet_BuildMessage(t *testing.T) {
Key: "hello",
Value: "world",
}
assert.Equal(t, TestSetMessage, set.BuildMessage())
assert.Equal(t, normalizeMessage(TestSetMessage), normalizeMessage(set.BuildMessage()))
}

func TestExport_BuildMessage(t *testing.T) {
Expand All @@ -72,7 +77,7 @@ func TestExport_BuildMessage(t *testing.T) {
Key: "hello",
Value: "world",
}
assert.Equal(t, TestExportMessage, export.BuildMessage())
assert.Equal(t, normalizeMessage(TestExportMessage), normalizeMessage(export.BuildMessage()))
}

func TestPush_BuildMessage(t *testing.T) {
Expand All @@ -81,5 +86,5 @@ func TestPush_BuildMessage(t *testing.T) {
Key: "hello",
Value: "world",
}
assert.Equal(t, TestPushMessage, push.BuildMessage())
assert.Equal(t, normalizeMessage(TestPushMessage), normalizeMessage(push.BuildMessage()))
}
5 changes: 3 additions & 2 deletions command/call/nomedia_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
package call

import (
"github.com/stretchr/testify/assert"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

var TestNoMediaMessage = strings.ReplaceAll(`sendmsg none
Expand All @@ -25,5 +26,5 @@ func TestNoMedia_BuildMessage(t *testing.T) {
UUID: "none",
NoMediaUUID: "test",
}
assert.Equal(t, TestNoMediaMessage, nomedia.BuildMessage())
assert.Equal(t, normalizeMessage(TestNoMediaMessage), normalizeMessage(nomedia.BuildMessage()))
}
5 changes: 3 additions & 2 deletions command/call/unicast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
package call

import (
"github.com/stretchr/testify/assert"
"net"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

var TestUnicastMessage = strings.ReplaceAll(`sendmsg none
Expand All @@ -35,5 +36,5 @@ func TestUnicast_BuildMessage(t *testing.T) {
Remote: testRemote,
Flags: "native",
}
assert.Equal(t, TestUnicastMessage, unicast.BuildMessage())
assert.Equal(t, normalizeMessage(TestUnicastMessage), normalizeMessage(unicast.BuildMessage()))
}
15 changes: 14 additions & 1 deletion command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var crlfToLF = strings.NewReplacer("\r\n", "\n")
// FormatHeaderString - Writes headers in a FreeSWITCH ESL friendly format. Converts headers containing \r\n to \n
func FormatHeaderString(headers textproto.MIMEHeader) string {
var ws strings.Builder
ws.Grow(estimateSize(headers))

keys := make([]string, len(headers))
i := 0
Expand All @@ -46,5 +47,17 @@ func FormatHeaderString(headers textproto.MIMEHeader) string {
}
}
// Remove the extra \r\n
return ws.String()[:ws.Len()-2]
str := ws.String()
return str[:len(str)-2]
}

// helper for FormatHeaderString that estimates the size of the final header string to avoid multiple allocations
func estimateSize(headers textproto.MIMEHeader) int {
size := 0
for key, values := range headers {
for _, value := range values {
size += len(key) + len(value) + 4 // 4 extra characters for ": " and "\r\n"
}
}
return size
}
22 changes: 17 additions & 5 deletions command/sendmsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"net/textproto"
"strconv"
"strings"
)

type SendMessage struct {
Expand All @@ -25,28 +26,39 @@ type SendMessage struct {
}

func (s *SendMessage) BuildMessage() string {
var headers []string

if s.Headers == nil {
s.Headers = make(textproto.MIMEHeader)
}

// Waits for this event to finish before continuing even in async mode
if s.Sync {
s.Headers.Set("event-lock", "true")
headers = append(headers, "event-lock: true")
}

// No documentation on this flag, I assume it takes priority over the other flag?
if s.SyncPri {
s.Headers.Set("event-lock-pri", "true")
headers = append(headers, "event-lock-pri: true")
}

// Ensure the correct content length is set in the header
if len(s.Body) > 0 {
s.Headers.Set("Content-Length", strconv.Itoa(len(s.Body)))
headers = append(headers, "Content-Length: "+strconv.Itoa(len(s.Body)))
} else {
delete(s.Headers, "Content-Length")
}

// Format the headers
headerString := FormatHeaderString(s.Headers)
if _, ok := s.Headers["Content-Length"]; ok {
for key, values := range s.Headers {
for _, value := range values {
headers = append(headers, key+": "+value)
}
}

headerString := strings.Join(headers, "\r\n")

if len(s.Body) > 0 {
return fmt.Sprintf("sendmsg %s\r\n%s\r\n\r\n%s", s.UUID, headerString, s.Body)
}
return fmt.Sprintf("sendmsg %s\r\n%s", s.UUID, headerString)
Expand Down
52 changes: 31 additions & 21 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,32 @@ import (
"bufio"
"context"
"errors"
"github.com/google/uuid"
"github.com/percipia/eslgo/command"
"fmt"
"net"
"net/textproto"
"sync"
"time"

"github.com/percipia/eslgo/command"
)

type Conn struct {
conn net.Conn
reader *bufio.Reader
header *textproto.Reader
writeLock sync.Mutex
runningContext context.Context
stopFunc func()
responseChannels map[string]chan *RawResponse
responseChanMutex sync.RWMutex
eventListenerLock sync.RWMutex
eventListeners map[string]map[string]EventListener
outbound bool
logger Logger
exitTimeout time.Duration
closeOnce sync.Once
closeDelay time.Duration
conn net.Conn
reader *bufio.Reader
header *textproto.Reader
writeLock sync.Mutex
runningContext context.Context
stopFunc func()
responseChannels map[string]chan *RawResponse
responseChanMutex sync.RWMutex
eventListenerLock sync.RWMutex
eventListeners map[string]map[string]EventListener
eventListenerCounter int
outbound bool
logger Logger
exitTimeout time.Duration
closeOnce sync.Once
closeDelay time.Duration
}

// Options - Generic options for an ESL connection, either inbound or outbound
Expand Down Expand Up @@ -97,7 +99,8 @@ func (c *Conn) RegisterEventListener(channelUUID string, listener EventListener)
c.eventListenerLock.Lock()
defer c.eventListenerLock.Unlock()

id := uuid.New().String()
c.eventListenerCounter++
id := fmt.Sprintf("%d", c.eventListenerCounter)
if _, ok := c.eventListeners[channelUUID]; ok {
c.eventListeners[channelUUID][id] = listener
} else {
Expand All @@ -118,10 +121,8 @@ func (c *Conn) RemoveEventListener(channelUUID string, id string) {

// SendCommand - Sends the specified ESL command to FreeSWITCH with the provided context. Returns the response data and any errors encountered.
func (c *Conn) SendCommand(ctx context.Context, cmd command.Command) (*RawResponse, error) {
c.writeLock.Lock()
defer c.writeLock.Unlock()

if linger, ok := cmd.(command.Linger); ok {
c.writeLock.Lock()
if linger.Enabled {
if linger.Seconds > 0 {
c.closeDelay = linger.Seconds
Expand All @@ -131,19 +132,26 @@ func (c *Conn) SendCommand(ctx context.Context, cmd command.Command) (*RawRespon
} else {
c.closeDelay = 0
}
c.writeLock.Unlock()
}

if deadline, ok := ctx.Deadline(); ok {
c.writeLock.Lock()
_ = c.conn.SetWriteDeadline(deadline)
c.writeLock.Unlock()
}

c.writeLock.Lock()
_, err := c.conn.Write([]byte(cmd.BuildMessage() + EndOfMessage))
c.writeLock.Unlock()
if err != nil {
return nil, err
}

// Get response
c.responseChanMutex.RLock()
defer c.responseChanMutex.RUnlock()
timeout := time.After(10 * time.Second)
select {
case response := <-c.responseChannels[TypeReply]:
if response == nil {
Expand All @@ -159,6 +167,8 @@ func (c *Conn) SendCommand(ctx context.Context, cmd command.Command) (*RawRespon
return response, nil
case <-ctx.Done():
return nil, ctx.Err()
case <-timeout:
return nil, errors.New("command timed out while waiting for a response from Freeswitch")
}
}

Expand Down
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ module github.com/percipia/eslgo

go 1.14

require (
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.7.0
)
require github.com/stretchr/testify v1.7.0
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/percipia/eslgo v1.4.1 h1:FpYtzCwrzQuSgB24NyuQC5ibMSHVqCdzaieAcDTltYw=
github.com/percipia/eslgo v1.4.1/go.mod h1:Icri58AZUSyAo+ObKUXhVwSC2aUPZaLzJYO/vKE3kew=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18 changes: 14 additions & 4 deletions helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
"context"
"errors"
"fmt"
"github.com/percipia/eslgo/command"
"github.com/percipia/eslgo/command/call"
"io"
"log"
"time"

"github.com/percipia/eslgo/command"
"github.com/percipia/eslgo/command/call"
)

func (c *Conn) EnableEvents(ctx context.Context) error {
Expand Down Expand Up @@ -79,9 +81,17 @@ func (c *Conn) WaitForDTMF(ctx context.Context, uuid string) (byte, error) {
if event.GetName() == "DTMF" {
dtmf := event.GetHeader("DTMF-Digit")
if len(dtmf) > 0 {
done <- dtmf[0]
select {
case done <- dtmf[0]:
default:
}
} else {
select {
case done <- 0:
default:
}
}
done <- 0
time.Sleep(10 * time.Millisecond)
}
})
defer func() {
Expand Down
Loading