Skip to content

Commit 1f8b40b

Browse files
authored
Update Marshal to use strings.Builder (#18)
* added benchmark to demonstrate performance issue with marshal Before: Running tool: /usr/local/go/bin/go test -benchmem -run=^$ -bench ^BenchmarkMarshalNEntries$ github.com/go-ldap/ldif goos: linux goarch: amd64 pkg: github.com/go-ldap/ldif cpu: Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz BenchmarkMarshalNEntries/marshal_100-12 1000000000 0.001498 ns/op 0 B/op 0 allocs/op BenchmarkMarshalNEntries/marshal_1000-12 1000000000 0.1485 ns/op 0 B/op 0 allocs/op BenchmarkMarshalNEntries/marshal_10000-12 111295240700 ns/op 94516468448 B/op 349306 allocs/op PASS ok github.com/go-ldap/ldif 13.154s * updated Marshal to use strings.Builder Looks like at least 2 orders of magnitude improvement at 1000 entries After: Running tool: /usr/local/go/bin/go test -benchmem -run=^$ -bench ^BenchmarkMarshalNEntries$ github.com/go-ldap/ldif goos: linux goarch: amd64 pkg: github.com/go-ldap/ldif cpu: Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz BenchmarkMarshalNEntries/marshal_100-12 1000000000 0.0001769 ns/op 0 B/op 0 allocs/op BenchmarkMarshalNEntries/marshal_1000-12 1000000000 0.001784 ns/op 0 B/op 0 allocs/op BenchmarkMarshalNEntries/marshal_10000-12 1000000000 0.02262 ns/op 0 B/op 0 allocs/op BenchmarkMarshalNEntries/marshal_100000-12 1000000000 0.2143 ns/op 0 B/op 0 allocs/op PASS ok github.com/go-ldap/ldif 3.329s
1 parent a768d72 commit 1f8b40b

File tree

2 files changed

+101
-21
lines changed

2 files changed

+101
-21
lines changed

marshal.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"strings"
89

910
"github.com/go-ldap/ldap/v3"
1011
)
@@ -24,8 +25,9 @@ func Marshal(l *LDIF) (data string, err error) {
2425
hasEntry := false
2526
hasChange := false
2627

28+
var builder strings.Builder
2729
if l.Version > 0 {
28-
data = "version: 1\n"
30+
builder.WriteString("version: 1\n")
2931
}
3032

3133
fw := l.FoldWidth
@@ -40,8 +42,8 @@ func Marshal(l *LDIF) (data string, err error) {
4042
if hasEntry {
4143
return "", ErrMixed
4244
}
43-
data += foldLine("dn: "+e.Add.DN, fw) + "\n"
44-
data += "changetype: add\n"
45+
builder.WriteString(foldLine("dn: "+e.Add.DN, fw) + "\n")
46+
builder.WriteString("changetype: add\n")
4547
for _, add := range e.Add.Attributes {
4648
if len(add.Vals) == 0 {
4749
return "", errors.New("changetype 'add' requires non empty value list")
@@ -52,7 +54,7 @@ func Marshal(l *LDIF) (data string, err error) {
5254
if t {
5355
col = ":: "
5456
}
55-
data += foldLine(add.Type+col+ev, fw) + "\n"
57+
builder.WriteString(foldLine(add.Type+col+ev, fw) + "\n")
5658
}
5759
}
5860

@@ -61,16 +63,16 @@ func Marshal(l *LDIF) (data string, err error) {
6163
if hasEntry {
6264
return "", ErrMixed
6365
}
64-
data += foldLine("dn: "+e.Del.DN, fw) + "\n"
65-
data += "changetype: delete\n"
66+
builder.WriteString(foldLine("dn: "+e.Del.DN, fw) + "\n")
67+
builder.WriteString("changetype: delete\n")
6668

6769
case e.Modify != nil:
6870
hasChange = true
6971
if hasEntry {
7072
return "", ErrMixed
7173
}
72-
data += foldLine("dn: "+e.Modify.DN, fw) + "\n"
73-
data += "changetype: modify\n"
74+
builder.WriteString(foldLine("dn: "+e.Modify.DN, fw) + "\n")
75+
builder.WriteString("changetype: modify\n")
7476
for _, mod := range e.Modify.Changes {
7577
switch mod.Operation {
7678
// add operation - https://tools.ietf.org/html/rfc4511#section-4.6
@@ -79,43 +81,43 @@ func Marshal(l *LDIF) (data string, err error) {
7981
return "", errors.New("changetype 'modify', op 'add' requires non empty value list")
8082
}
8183

82-
data += "add: " + mod.Modification.Type + "\n"
84+
builder.WriteString("add: " + mod.Modification.Type + "\n")
8385
for _, v := range mod.Modification.Vals {
8486
ev, t := encodeValue(v)
8587
col := ": "
8688
if t {
8789
col = ":: "
8890
}
89-
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
91+
builder.WriteString(foldLine(mod.Modification.Type+col+ev, fw) + "\n")
9092
}
91-
data += "-\n"
93+
builder.WriteString("-\n")
9294
// delete operation - https://tools.ietf.org/html/rfc4511#section-4.6
9395
case 1:
94-
data += "delete: " + mod.Modification.Type + "\n"
96+
builder.WriteString("delete: " + mod.Modification.Type + "\n")
9597
for _, v := range mod.Modification.Vals {
9698
ev, t := encodeValue(v)
9799
col := ": "
98100
if t {
99101
col = ":: "
100102
}
101-
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
103+
builder.WriteString(foldLine(mod.Modification.Type+col+ev, fw) + "\n")
102104
}
103-
data += "-\n"
105+
builder.WriteString("-\n")
104106
// replace operation - https://tools.ietf.org/html/rfc4511#section-4.6
105107
case 2:
106108
if len(mod.Modification.Vals) == 0 {
107109
return "", errors.New("changetype 'modify', op 'replace' requires non empty value list")
108110
}
109-
data += "replace: " + mod.Modification.Type + "\n"
111+
builder.WriteString("replace: " + mod.Modification.Type + "\n")
110112
for _, v := range mod.Modification.Vals {
111113
ev, t := encodeValue(v)
112114
col := ": "
113115
if t {
114116
col = ":: "
115117
}
116-
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
118+
builder.WriteString(foldLine(mod.Modification.Type+col+ev, fw) + "\n")
117119
}
118-
data += "-\n"
120+
builder.WriteString("-\n")
119121
default:
120122
return "", fmt.Errorf("invalid type %s in modify request", mod.Modification.Type)
121123
}
@@ -125,21 +127,21 @@ func Marshal(l *LDIF) (data string, err error) {
125127
if hasChange {
126128
return "", ErrMixed
127129
}
128-
data += foldLine("dn: "+e.Entry.DN, fw) + "\n"
130+
builder.WriteString(foldLine("dn: "+e.Entry.DN, fw) + "\n")
129131
for _, av := range e.Entry.Attributes {
130132
for _, v := range av.Values {
131133
ev, t := encodeValue(v)
132134
col := ": "
133135
if t {
134136
col = ":: "
135137
}
136-
data += foldLine(av.Name+col+ev, fw) + "\n"
138+
builder.WriteString(foldLine(av.Name+col+ev, fw) + "\n")
137139
}
138140
}
139141
}
140-
data += "\n"
142+
builder.WriteString("\n")
141143
}
142-
return data, nil
144+
return builder.String(), nil
143145
}
144146

145147
func encodeValue(value string) (string, bool) {

marshal_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package ldif_test
22

33
import (
44
"bytes"
5+
"fmt"
6+
"strings"
57
"testing"
68

79
"github.com/go-ldap/ldap/v3"
@@ -71,6 +73,82 @@ var entries = []*ldap.Entry{
7173
},
7274
}
7375

76+
func nPeople(n int) ([]*ldap.Entry, string) {
77+
nEntries := make([]*ldap.Entry, n+1)
78+
var builder strings.Builder
79+
80+
nEntries[0] = entries[0]
81+
builder.WriteString(ouLDIF)
82+
83+
for i := 1; i <= n; i++ {
84+
nEntries[i] = &ldap.Entry{
85+
DN: fmt.Sprintf("uid=someone%d,ou=people,dc=example,dc=org", i),
86+
Attributes: []*ldap.EntryAttribute{
87+
{
88+
Name: "objectClass",
89+
Values: []string{
90+
"top",
91+
"person",
92+
"organizationalPerson",
93+
"inetOrgPerson",
94+
},
95+
},
96+
{
97+
Name: "uid",
98+
Values: []string{fmt.Sprintf("someone%d", i)},
99+
},
100+
{
101+
Name: "cn",
102+
Values: []string{fmt.Sprintf("Someone%d", i)},
103+
},
104+
{
105+
Name: "mail",
106+
Values: []string{fmt.Sprintf("someone%[email protected]", i)},
107+
},
108+
},
109+
}
110+
111+
builder.WriteString(fmt.Sprintf(""+
112+
"dn: uid=someone%d,ou=people,dc=example,dc=org\n"+
113+
"objectClass: top\n"+
114+
"objectClass: person\n"+
115+
"objectClass: organizationalPerson\n"+
116+
"objectClass: inetOrgPerson\n"+
117+
"uid: someone%d\n"+
118+
"cn: Someone%d\n"+
119+
"mail: someone%[email protected]\n"+
120+
"\n",
121+
i, i, i, i))
122+
}
123+
124+
return nEntries, builder.String()
125+
}
126+
127+
func BenchmarkMarshalNEntries(b *testing.B) {
128+
benchmarker := func(n int) {
129+
b.Run(fmt.Sprintf("marshal %d", n), func(b *testing.B) {
130+
entries, expected := nPeople(n)
131+
ldifEntries := make([]*ldif.Entry, len(entries))
132+
for i, entry := range entries {
133+
ldifEntries[i] = &ldif.Entry{Entry: entry}
134+
}
135+
l := &ldif.LDIF{Entries: ldifEntries}
136+
actual, err := ldif.Marshal(l)
137+
if err != nil {
138+
b.Errorf("Failed to marshal entry: %s", err)
139+
}
140+
if actual != expected {
141+
b.Errorf("expected: >>%s<<\nactual: >>%s<<\n", expected, actual)
142+
}
143+
})
144+
}
145+
146+
benchmarker(100)
147+
benchmarker(1000)
148+
benchmarker(10000)
149+
benchmarker(100000)
150+
}
151+
74152
func TestMarshalSingleEntry(t *testing.T) {
75153
l := &ldif.LDIF{
76154
Entries: []*ldif.Entry{

0 commit comments

Comments
 (0)