Skip to content

Commit 600c92d

Browse files
authored
Perf/optimization final results (#542)
* perf(text): optimize TextNode with pre-tokenized tokens and NodeWriter support * perf(node): full NodeWriter integration for ForeachNode and other core nodes * perf(refactor): optimize reflectValueToString and finalize performance work
1 parent 3a59ce1 commit 600c92d

File tree

6 files changed

+211
-160
lines changed

6 files changed

+211
-160
lines changed

node/condition.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package node
1818

1919
import (
2020
"errors"
21+
"strings"
2122

2223
"github.com/go-juicedev/juice/driver"
2324
"github.com/go-juicedev/juice/eval"
@@ -51,6 +52,20 @@ func (c *ConditionNode) Parse(test string) (err error) {
5152
return err
5253
}
5354

55+
func (c *ConditionNode) AcceptTo(translator driver.Translator, p eval.Parameter, builder *strings.Builder, args *[]any) error {
56+
p = c.BindNodes.ConvertParameter(p)
57+
58+
matched, err := c.Match(p)
59+
if err != nil {
60+
return err
61+
}
62+
if !matched {
63+
return nil
64+
}
65+
66+
return c.Nodes.AcceptTo(translator, p, builder, args)
67+
}
68+
5469
// Accept accepts parameters and returns query and arguments.
5570
// Accept implements Node interface.
5671
func (c *ConditionNode) Accept(translator driver.Translator, p eval.Parameter) (query string, args []any, err error) {
@@ -85,4 +100,4 @@ func (c *ConditionNode) Match(p eval.Parameter) (bool, error) {
85100
return !value.IsZero(), nil
86101
}
87102

88-
var _ Node = (*ConditionNode)(nil)
103+
var _ NodeWriter = (*ConditionNode)(nil)

node/foreach.go

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package node
1919
import (
2020
"fmt"
2121
"reflect"
22+
"strings"
2223

2324
"github.com/go-juicedev/juice/driver"
2425
"github.com/go-juicedev/juice/eval"
@@ -77,22 +78,65 @@ type ForeachNode struct {
7778

7879
// Accept accepts parameters and returns query and arguments.
7980
func (f ForeachNode) Accept(translator driver.Translator, p eval.Parameter) (query string, args []any, err error) {
81+
builder := getStringBuilder()
82+
defer putStringBuilder(builder)
83+
84+
// Estimation logic for pre-allocation
85+
var totalPlaceholders int
86+
for _, node := range f.Nodes {
87+
if t, ok := node.(*TextNode); ok {
88+
for _, token := range t.tokens {
89+
if !token.isFormat {
90+
totalPlaceholders++
91+
}
92+
}
93+
} else {
94+
totalPlaceholders += 2
95+
}
96+
}
97+
98+
// We don't know the exact length yet, but we can guess from p
99+
coll, exists := p.Get(f.Collection)
100+
var length int
101+
if exists {
102+
c := coll
103+
for c.Kind() == reflect.Interface || c.Kind() == reflect.Ptr {
104+
if c.IsNil() {
105+
break
106+
}
107+
c = c.Elem()
108+
}
109+
if c.Kind() == reflect.Slice || c.Kind() == reflect.Array || c.Kind() == reflect.Map {
110+
length = c.Len()
111+
}
112+
}
113+
114+
argsList := make([]any, 0, length*totalPlaceholders)
115+
116+
if err = f.AcceptTo(translator, p, builder, &argsList); err != nil {
117+
return "", nil, err
118+
}
119+
120+
return builder.String(), argsList, nil
121+
}
122+
123+
func (f ForeachNode) AcceptTo(translator driver.Translator, p eval.Parameter, builder *strings.Builder, args *[]any) error {
80124
p = f.BindNodes.ConvertParameter(p)
81125

82126
// if item already exists
83127
if _, exists := p.Get(f.Item); exists {
84-
return "", nil, fmt.Errorf("item %s already exists", f.Item)
128+
return fmt.Errorf("item %s already exists", f.Item)
85129
}
86130

87131
// one collection from parameter
88132
value, exists := p.Get(f.Collection)
89133
if !exists {
90-
return "", nil, fmt.Errorf("collection %s not found", f.Collection)
134+
return fmt.Errorf("collection %s not found", f.Collection)
91135
}
92136

93137
// if valueItem can not be iterated
94138
if !value.CanInterface() {
95-
return "", nil, fmt.Errorf("collection %s can not be iterated", f.Collection)
139+
return fmt.Errorf("collection %s can not be iterated", f.Collection)
96140
}
97141

98142
// if valueItem is not a slice
@@ -102,36 +146,21 @@ func (f ForeachNode) Accept(translator driver.Translator, p eval.Parameter) (que
102146

103147
switch value.Kind() {
104148
case reflect.Array, reflect.Slice:
105-
return f.acceptSlice(value, translator, p)
149+
return f.acceptSliceTo(value, translator, p, builder, args)
106150
case reflect.Map:
107-
return f.acceptMap(value, translator, p)
151+
return f.acceptMapTo(value, translator, p, builder, args)
108152
default:
109-
return "", nil, fmt.Errorf("collection %s is not a slice or map", f.Collection)
153+
return fmt.Errorf("collection %s is not a slice or map", f.Collection)
110154
}
111155
}
112156

113-
func (f ForeachNode) acceptSlice(value reflect.Value, translator driver.Translator, p eval.Parameter) (query string, args []any, err error) {
157+
func (f ForeachNode) acceptSliceTo(value reflect.Value, translator driver.Translator, p eval.Parameter, builder *strings.Builder, args *[]any) error {
114158
sliceLength := value.Len()
115159

116160
if sliceLength == 0 {
117-
return "", nil, nil
161+
return nil
118162
}
119163

120-
// Pre-allocate args slice capacity to avoid multiple growths
121-
// Estimate: number of slice elements * number of Nodes
122-
estimatedArgsLen := sliceLength * len(f.Nodes)
123-
124-
args = make([]any, 0, estimatedArgsLen)
125-
126-
// Pre-allocate string builder capacity to minimize buffer reallocations
127-
// Capacity = open + items + separators + close
128-
estimatedBuilderCap := len(f.Open) + (2 * sliceLength) + (len(f.Separator) * (sliceLength - 1)) + len(f.Close)
129-
130-
var builder = getStringBuilder()
131-
defer putStringBuilder(builder)
132-
133-
builder.Grow(estimatedBuilderCap)
134-
135164
builder.WriteString(f.Open)
136165

137166
end := sliceLength - 1
@@ -147,15 +176,21 @@ func (f ForeachNode) acceptSlice(value reflect.Value, translator driver.Translat
147176
}
148177

149178
for _, node := range f.Nodes {
150-
q, a, err := node.Accept(translator, fp)
151-
if err != nil {
152-
return "", nil, err
153-
}
154-
if len(q) > 0 {
155-
builder.WriteString(q)
156-
}
157-
if len(a) > 0 {
158-
args = append(args, a...)
179+
if nw, ok := node.(NodeWriter); ok {
180+
if err := nw.AcceptTo(translator, fp, builder, args); err != nil {
181+
return err
182+
}
183+
} else {
184+
q, a, err := node.Accept(translator, fp)
185+
if err != nil {
186+
return err
187+
}
188+
if len(q) > 0 {
189+
builder.WriteString(q)
190+
}
191+
if len(a) > 0 {
192+
*args = append(*args, a...)
193+
}
159194
}
160195
}
161196

@@ -168,31 +203,16 @@ func (f ForeachNode) acceptSlice(value reflect.Value, translator driver.Translat
168203
// if sliceLength is not zero, add close
169204
builder.WriteString(f.Close)
170205

171-
return builder.String(), args, nil
206+
return nil
172207
}
173208

174-
func (f ForeachNode) acceptMap(value reflect.Value, translator driver.Translator, p eval.Parameter) (query string, args []any, err error) {
209+
func (f ForeachNode) acceptMapTo(value reflect.Value, translator driver.Translator, p eval.Parameter, builder *strings.Builder, args *[]any) error {
175210
mapLength := value.Len()
176211

177212
if mapLength == 0 {
178-
return "", nil, nil
213+
return nil
179214
}
180215

181-
// Pre-allocate args slice capacity to avoid multiple growths
182-
// Estimate: number of elements * number of Nodes
183-
estimatedArgsLen := mapLength * len(f.Nodes)
184-
185-
args = make([]any, 0, estimatedArgsLen)
186-
187-
// Pre-allocate string builder capacity to minimize buffer reallocations
188-
// Capacity = open + items + separators + close
189-
estimatedBuilderCap := len(f.Open) + (2 * mapLength) + (len(f.Separator) * (mapLength - 1)) + len(f.Close)
190-
191-
var builder = getStringBuilder()
192-
defer putStringBuilder(builder)
193-
194-
builder.Grow(estimatedBuilderCap)
195-
196216
builder.WriteString(f.Open)
197217

198218
end := mapLength - 1
@@ -210,15 +230,21 @@ func (f ForeachNode) acceptMap(value reflect.Value, translator driver.Translator
210230
fp.IndexValue = iter.Key()
211231

212232
for _, node := range f.Nodes {
213-
q, a, err := node.Accept(translator, fp)
214-
if err != nil {
215-
return "", nil, err
216-
}
217-
if len(q) > 0 {
218-
builder.WriteString(q)
219-
}
220-
if len(a) > 0 {
221-
args = append(args, a...)
233+
if nw, ok := node.(NodeWriter); ok {
234+
if err := nw.AcceptTo(translator, fp, builder, args); err != nil {
235+
return err
236+
}
237+
} else {
238+
q, a, err := node.Accept(translator, fp)
239+
if err != nil {
240+
return err
241+
}
242+
if len(q) > 0 {
243+
builder.WriteString(q)
244+
}
245+
if len(a) > 0 {
246+
*args = append(*args, a...)
247+
}
222248
}
223249
}
224250

@@ -233,7 +259,7 @@ func (f ForeachNode) acceptMap(value reflect.Value, translator driver.Translator
233259

234260
builder.WriteString(f.Close)
235261

236-
return builder.String(), args, nil
262+
return nil
237263
}
238264

239-
var _ Node = (*ForeachNode)(nil)
265+
var _ NodeWriter = (*ForeachNode)(nil)

node/node.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,28 +183,35 @@ var _ NodeWriter = (Group)(nil)
183183
// reflectValueToString converts reflect.Value to string
184184
func reflectValueToString(v reflect.Value) string {
185185
v = reflectlite.Unwrap(v)
186-
switch t := v.Interface().(type) {
187-
case nil:
186+
if !v.IsValid() {
188187
return ""
189-
case string:
190-
return t
191-
case []byte:
192-
return string(v.Bytes())
193-
case fmt.Stringer:
194-
return t.String()
195-
case int, int8, int16, int32, int64:
188+
}
189+
switch v.Kind() {
190+
case reflect.String:
191+
return v.String()
192+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
196193
return strconv.FormatInt(v.Int(), 10)
197-
case uint, uint8, uint16, uint32, uint64:
194+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
198195
return strconv.FormatUint(v.Uint(), 10)
199-
case float32:
196+
case reflect.Float32:
200197
return strconv.FormatFloat(v.Float(), 'g', -1, 32)
201-
case float64:
198+
case reflect.Float64:
202199
return strconv.FormatFloat(v.Float(), 'g', -1, 64)
203-
case bool:
200+
case reflect.Bool:
204201
return strconv.FormatBool(v.Bool())
205-
default:
206-
return fmt.Sprintf("%v", t)
202+
case reflect.Slice:
203+
if v.Type().Elem().Kind() == reflect.Uint8 {
204+
return string(v.Bytes())
205+
}
206+
}
207+
208+
if v.CanInterface() {
209+
if t, ok := v.Interface().(fmt.Stringer); ok {
210+
return t.String()
211+
}
212+
return fmt.Sprintf("%v", v.Interface())
207213
}
214+
return ""
208215
}
209216

210217
// bindScope provides lookup and execution of bind variables within a scope.

node/sql.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package node
1818

1919
import (
20+
"strings"
21+
2022
"github.com/go-juicedev/juice/driver"
2123
"github.com/go-juicedev/juice/eval"
2224
)
@@ -63,11 +65,16 @@ type SQLNode struct {
6365
BindNodes BindNodeGroup
6466
}
6567

66-
// Accept accepts parameters and returns query and arguments.
68+
// Accept processes the node and returns query and arguments.
6769
func (s SQLNode) Accept(translator driver.Translator, p eval.Parameter) (query string, args []any, err error) {
6870
p = s.BindNodes.ConvertParameter(p)
6971

7072
return s.Nodes.Accept(translator, p)
7173
}
7274

73-
var _ Node = (*SQLNode)(nil)
75+
func (s SQLNode) AcceptTo(translator driver.Translator, p eval.Parameter, builder *strings.Builder, args *[]any) error {
76+
p = s.BindNodes.ConvertParameter(p)
77+
return s.Nodes.AcceptTo(translator, p, builder, args)
78+
}
79+
80+
var _ NodeWriter = (*SQLNode)(nil)

0 commit comments

Comments
 (0)