Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package quickfix
import (
"errors"
"fmt"
"strconv"
)

// ErrDoNotSend is a convenience error to indicate a DoNotSend in ToApp.
Expand Down Expand Up @@ -124,8 +125,9 @@ func ValueIsIncorrect(tag Tag) MessageRejectError {
}

// ConditionallyRequiredFieldMissing indicates that the requested field could not be found in the FIX message.
// Uses strconv.Itoa instead of fmt.Sprintf to avoid format string parsing overhead.
func ConditionallyRequiredFieldMissing(tag Tag) MessageRejectError {
return NewBusinessMessageRejectError(fmt.Sprintf("Conditionally Required Field Missing (%d)", tag), rejectReasonConditionallyRequiredFieldMissing, &tag)
return NewBusinessMessageRejectError("Conditionally Required Field Missing ("+strconv.Itoa(int(tag))+")", rejectReasonConditionallyRequiredFieldMissing, &tag)
}

// valueIsIncorrectNoTag returns an error indicating a field with value that is not valid.
Expand Down
20 changes: 20 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,23 @@ func TestInvalidTagNumber(t *testing.T) {
t.Error("Expected IsBusinessReject to be false\n")
}
}

func TestRejectLogon(t *testing.T) {
rej := RejectLogon{Text: "logon rejected"}

if rej.Error() != "logon rejected" {
t.Errorf("expected 'logon rejected', got: %s", rej.Error())
}
if rej.RefTagID() != nil {
t.Error("expected nil RefTagID")
}
if rej.RejectReason() != 0 {
t.Errorf("expected 0, got: %d", rej.RejectReason())
}
if rej.BusinessRejectRefID() != "" {
t.Errorf("expected empty string, got: %s", rej.BusinessRejectRefID())
}
if rej.IsBusinessReject() {
t.Error("expected false")
}
}
38 changes: 14 additions & 24 deletions field_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,6 @@ func (m FieldMap) GetField(tag Tag, parser FieldValueReader) MessageRejectError
return nil
}

// GetField parses of a field with Tag tag. Returned reject may indicate the field is not present, or the field value is invalid.
func (m FieldMap) getFieldNoLock(tag Tag, parser FieldValueReader) MessageRejectError {
f, ok := m.tagLookup[tag]
if !ok {
return ConditionallyRequiredFieldMissing(tag)
}

if err := parser.Read(f[0].value); err != nil {
return IncorrectDataFormatForValue(tag)
}

return nil
}

// GetBytes is a zero-copy GetField wrapper for []bytes fields.
func (m FieldMap) GetBytes(tag Tag) ([]byte, MessageRejectError) {
m.rwLock.RLock()
Expand Down Expand Up @@ -196,7 +182,7 @@ func (m FieldMap) GetTime(tag Tag) (t time.Time, err MessageRejectError) {
m.rwLock.RLock()
defer m.rwLock.RUnlock()

bytes, err := m.GetBytes(tag)
bytes, err := m.getBytesNoLock(tag)
if err != nil {
return
}
Expand All @@ -210,21 +196,25 @@ func (m FieldMap) GetTime(tag Tag) (t time.Time, err MessageRejectError) {
}

// GetString is a GetField wrapper for string fields.
// Optimized to directly access tagLookup and convert bytes to string,
// avoiding the intermediate FIXString allocation that GetField would create.
func (m FieldMap) GetString(tag Tag) (string, MessageRejectError) {
var val FIXString
if err := m.GetField(tag, &val); err != nil {
return "", err
m.rwLock.RLock()
f, ok := m.tagLookup[tag]
m.rwLock.RUnlock()
if !ok {
return "", ConditionallyRequiredFieldMissing(tag)
}
return string(val), nil
return string(f[0].value), nil
}

// GetString is a GetField wrapper for string fields.
// getStringNoLock is a lock-free GetField wrapper for string fields.
func (m FieldMap) getStringNoLock(tag Tag) (string, MessageRejectError) {
var val FIXString
if err := m.getFieldNoLock(tag, &val); err != nil {
return "", err
f, ok := m.tagLookup[tag]
if !ok {
return "", ConditionallyRequiredFieldMissing(tag)
}
return string(val), nil
return string(f[0].value), nil
}

// GetGroup is a Get function specific to Group Fields.
Expand Down
20 changes: 20 additions & 0 deletions field_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,23 @@ func TestFieldMap_Remove(t *testing.T) {
assert.False(t, fMap.Has(1))
assert.True(t, fMap.Has(2))
}

func TestFieldMap_Tags(t *testing.T) {
var fMap FieldMap
fMap.init()

fMap.SetField(1, FIXString("hello"))
fMap.SetField(2, FIXString("world"))
fMap.SetField(44, FIXString("price"))

tags := fMap.Tags()
assert.Len(t, tags, 3)

tagSet := make(map[Tag]bool)
for _, tag := range tags {
tagSet[tag] = true
}
assert.True(t, tagSet[Tag(1)])
assert.True(t, tagSet[Tag(2)])
assert.True(t, tagSet[Tag(44)])
}
34 changes: 34 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@ import (
"bytes"
"fmt"
"math"
"sync"
"time"

"github.com/quickfixgo/quickfix/datadictionary"
)

// messagePool provides reusable Message objects to reduce allocations.
var messagePool = sync.Pool{
New: func() interface{} {
m := &Message{}
m.Header.Init()
m.Body.Init()
m.Trailer.Init()
return m
},
}

// Header is first section of a FIX Message.
type Header struct{ FieldMap }

Expand Down Expand Up @@ -139,6 +151,28 @@ func NewMessage() *Message {
return m
}

// AcquireMessage returns a Message from the pool, reducing allocations.
// The returned Message must be released with ReleaseMessage when no longer needed.
func AcquireMessage() *Message {
return messagePool.Get().(*Message)
}

// ReleaseMessage returns a Message to the pool for reuse.
// The Message should not be used after calling this function.
func ReleaseMessage(m *Message) {
if m == nil {
return
}
m.Header.Clear()
m.Body.Clear()
m.Trailer.Clear()
m.rawMessage = nil
m.bodyBytes = nil
m.fields = m.fields[:0]
m.ReceiveTime = time.Time{}
messagePool.Put(m)
}

// CopyInto erases the dest messages and copies the currency message content
// into it.
func (m *Message) CopyInto(to *Message) {
Expand Down
68 changes: 68 additions & 0 deletions message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,71 @@ func checkFieldString(s *MessageSuite, fields FieldMap, tag int, expected string
s.NoError(err)
s.Equal(expected, toCheck)
}

func BenchmarkParseMessageNew(b *testing.B) {
// SOH = 0x01 is the FIX field delimiter
rawMsgStr := "8=FIX.4.2\x019=104\x0135=D\x0134=2\x0149=TW\x0152=20140515-19:49:56.659\x0156=ISLD\x0111=100\x0121=1\x0140=1\x0154=1\x0155=TSLA\x0160=00010101-00:00:00.000\x0110=039\x01"

for i := 0; i < b.N; i++ {
msg := NewMessage()
rawMsg := bytes.NewBufferString(rawMsgStr)
_ = ParseMessage(msg, rawMsg)
}
}

func BenchmarkGetString(b *testing.B) {
// SOH = 0x01 is the FIX field delimiter
rawMsgStr := "8=FIX.4.2\x019=104\x0135=D\x0134=2\x0149=TW\x0152=20140515-19:49:56.659\x0156=ISLD\x0111=100\x0121=1\x0140=1\x0154=1\x0155=TSLA\x0160=00010101-00:00:00.000\x0110=039\x01"
msg := NewMessage()
rawMsg := bytes.NewBufferString(rawMsgStr)
if err := ParseMessage(msg, rawMsg); err != nil {
b.Fatalf("ParseMessage failed: %v", err)
}

// Verify tags exist before benchmarking
if !msg.Header.Has(tagBeginString) {
b.Fatal("tagBeginString not found")
}
if !msg.Header.Has(tagSenderCompID) {
b.Fatal("tagSenderCompID not found")
}
if !msg.Header.Has(tagTargetCompID) {
b.Fatal("tagTargetCompID not found")
}
if !msg.Body.Has(Tag(55)) {
b.Fatal("tag 55 not found")
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = msg.Header.GetString(tagBeginString)
_, _ = msg.Header.GetString(tagSenderCompID)
_, _ = msg.Header.GetString(tagTargetCompID)
_, _ = msg.Body.GetString(Tag(55))
}
}

func BenchmarkRepeatingGroupRead(b *testing.B) {
// Message with repeating group, SOH = 0x01 is the FIX field delimiter
rawMsgStr := "8=FIX.4.4\x019=165\x0135=D\x0134=2\x0149=01001\x0150=01001a\x0152=20231231-20:19:41\x0156=TEST\x011=acct1\x0111=13976\x0121=1\x0138=1\x0140=2\x0144=12\x0154=1\x0155=SYMABC\x0159=0\x0160=20231231-20:19:41\x01453=1\x01448=4501\x01447=D\x01452=28\x0110=026\x01"

dict, _ := datadictionary.Parse("spec/FIX44.xml")
msg := NewMessage()

for i := 0; i < b.N; i++ {
rawMsg := bytes.NewBufferString(rawMsgStr)
_ = ParseMessageWithDataDictionary(msg, rawMsg, dict, dict)
}
}

func BenchmarkParseMessagePool(b *testing.B) {
// SOH = 0x01 is the FIX field delimiter
rawMsgStr := "8=FIX.4.2\x019=104\x0135=D\x0134=2\x0149=TW\x0152=20140515-19:49:56.659\x0156=ISLD\x0111=100\x0121=1\x0140=1\x0154=1\x0155=TSLA\x0160=00010101-00:00:00.000\x0110=039\x01"

for i := 0; i < b.N; i++ {
msg := AcquireMessage()
rawMsg := bytes.NewBufferString(rawMsgStr)
_ = ParseMessage(msg, rawMsg)
ReleaseMessage(msg)
}
}
Loading
Loading