From a037d7ed2186dd8896bf9036f52dbbf0d51ceb48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?=
 <bjorn.erik.pedersen@gmail.com>
Date: Sun, 12 Jan 2025 17:58:11 +0200
Subject: [PATCH] tpl/collections: Use MapRange/SetIterKey/SetIterValue for
 Where, Sort and Merge
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Some relevant benchmarks:

Where with maps:

```
cpu: Apple M1 Pro
            │ master.bench │         fix-mapkeys.bench          │
            │    sec/op    │   sec/op     vs base               │
WhereMap-10    79.26µ ± 1%   26.58µ ± 1%  -66.46% (p=0.002 n=6)

            │ master.bench │         fix-mapkeys.bench         │
            │     B/op     │    B/op     vs base               │
WhereMap-10   56685.0 ± 0%   111.0 ± 1%  -99.80% (p=0.002 n=6)

            │ master.bench  │         fix-mapkeys.bench         │
            │   allocs/op   │ allocs/op   vs base               │
WhereMap-10   2003.000 ± 0%   4.000 ± 0%  -99.80% (p=0.002 n=6)
```

Merge:

```
         │ master.bench │         fix-mapkeys.bench          │
         │    sec/op    │   sec/op     vs base               │
Merge-10    3.285µ ± 0%   2.268µ ± 1%  -30.96% (p=0.002 n=6)

         │ master.bench │          fix-mapkeys.bench          │
         │     B/op     │     B/op      vs base               │
Merge-10   3.079Ki ± 0%   1.891Ki ± 0%  -38.58% (p=0.002 n=6)

         │ master.bench │         fix-mapkeys.bench         │
         │  allocs/op   │ allocs/op   vs base               │
Merge-10     64.00 ± 0%   26.00 ± 0%  -59.38% (p=0.002 n=6)
```
Sort:

```
cpu: Apple M1 Pro
           │ master.bench │         fix-mapkeys.bench         │
           │    sec/op    │   sec/op     vs base              │
SortMap-10   1008.0n ± 1%   915.5n ± 0%  -9.18% (p=0.002 n=6)

           │ master.bench │         fix-mapkeys.bench         │
           │     B/op     │    B/op     vs base               │
SortMap-10     640.0 ± 0%   512.0 ± 0%  -20.00% (p=0.002 n=6)

           │ master.bench │        fix-mapkeys.bench         │
           │  allocs/op   │ allocs/op   vs base              │
SortMap-10     16.00 ± 0%   15.00 ± 0%  -6.25% (p=0.002 n=6)
```
---
 tpl/collections/merge.go | 42 +++++++++++++++++++++++++++-------------
 tpl/collections/sort.go  | 16 +++++++++------
 tpl/collections/where.go | 10 ++++++----
 3 files changed, 45 insertions(+), 23 deletions(-)

diff --git a/tpl/collections/merge.go b/tpl/collections/merge.go
index 8c019e412a1..b9696b5026c 100644
--- a/tpl/collections/merge.go
+++ b/tpl/collections/merge.go
@@ -75,9 +75,13 @@ func caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {
 		return v, hreflect.IsTruthfulValue(v)
 	}
 
-	for _, key := range m.MapKeys() {
-		if strings.EqualFold(k.String(), key.String()) {
-			return m.MapIndex(key), true
+	k2 := reflect.New(m.Type().Key()).Elem()
+
+	iter := m.MapRange()
+	for iter.Next() {
+		k2.SetIterKey(iter)
+		if strings.EqualFold(k.String(), k2.String()) {
+			return iter.Value(), true
 		}
 	}
 
@@ -90,17 +94,28 @@ func mergeMap(dst, src reflect.Value) reflect.Value {
 	// If the destination is Params, we must lower case all keys.
 	_, lowerCase := dst.Interface().(maps.Params)
 
+	k := reflect.New(dst.Type().Key()).Elem()
+	v := reflect.New(dst.Type().Elem()).Elem()
+
 	// Copy the destination map.
-	for _, key := range dst.MapKeys() {
-		v := dst.MapIndex(key)
-		out.SetMapIndex(key, v)
+	iter := dst.MapRange()
+	for iter.Next() {
+		k.SetIterKey(iter)
+		v.SetIterValue(iter)
+		out.SetMapIndex(k, v)
 	}
 
 	// Add all keys in src not already in destination.
 	// Maps of the same type will be merged.
-	for _, key := range src.MapKeys() {
-		sv := src.MapIndex(key)
-		dv, found := caseInsensitiveLookup(dst, key)
+	k = reflect.New(src.Type().Key()).Elem()
+	sv := reflect.New(src.Type().Elem()).Elem()
+
+	iter = src.MapRange()
+	for iter.Next() {
+		sv.SetIterValue(iter)
+		k.SetIterKey(iter)
+
+		dv, found := caseInsensitiveLookup(dst, k)
 
 		if found {
 			// If both are the same map key type, merge.
@@ -112,14 +127,15 @@ func mergeMap(dst, src reflect.Value) reflect.Value {
 				}
 
 				if dve.Type().Key() == sve.Type().Key() {
-					out.SetMapIndex(key, mergeMap(dve, sve))
+					out.SetMapIndex(k, mergeMap(dve, sve))
 				}
 			}
 		} else {
-			if lowerCase && key.Kind() == reflect.String {
-				key = reflect.ValueOf(strings.ToLower(key.String()))
+			kk := k
+			if lowerCase && k.Kind() == reflect.String {
+				kk = reflect.ValueOf(strings.ToLower(k.String()))
 			}
-			out.SetMapIndex(key, sv)
+			out.SetMapIndex(kk, sv)
 		}
 	}
 
diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go
index 2040f8490c5..20862a45145 100644
--- a/tpl/collections/sort.go
+++ b/tpl/collections/sort.go
@@ -99,18 +99,21 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
 		}
 
 	case reflect.Map:
-		keys := seqv.MapKeys()
-		for i := 0; i < seqv.Len(); i++ {
-			p.Pairs[i].Value = seqv.MapIndex(keys[i])
 
+		iter := seqv.MapRange()
+		i := 0
+		for iter.Next() {
+			key := iter.Key()
+			value := iter.Value()
+			p.Pairs[i].Value = value
 			if sortByField == "" {
-				p.Pairs[i].Key = keys[i]
+				p.Pairs[i].Key = key
 			} else if sortByField == "value" {
 				p.Pairs[i].Key = p.Pairs[i].Value
 			} else {
 				v := p.Pairs[i].Value
 				var err error
-				for i, elemName := range path {
+				for j, elemName := range path {
 					v, err = evaluateSubElem(ctxv, v, elemName)
 					if err != nil {
 						return nil, err
@@ -120,12 +123,13 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
 					}
 					// Special handling of lower cased maps.
 					if params, ok := v.Interface().(maps.Params); ok {
-						v = reflect.ValueOf(params.GetNested(path[i+1:]...))
+						v = reflect.ValueOf(params.GetNested(path[j+1:]...))
 						break
 					}
 				}
 				p.Pairs[i].Key = v
 			}
+			i++
 		}
 	}
 
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
index bf3f750447a..a14a4863d37 100644
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -409,7 +409,6 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st
 				for i, elemName := range path {
 					var err error
 					vvv, err = evaluateSubElem(ctxv, vvv, elemName)
-
 					if err != nil {
 						continue
 					}
@@ -442,9 +441,12 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st
 // checkWhereMap handles the where-matching logic when the seqv value is a Map.
 func (ns *Namespace) checkWhereMap(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
 	rv := reflect.MakeMap(seqv.Type())
-	keys := seqv.MapKeys()
-	for _, k := range keys {
-		elemv := seqv.MapIndex(k)
+	k := reflect.New(seqv.Type().Key()).Elem()
+	elemv := reflect.New(seqv.Type().Elem()).Elem()
+	iter := seqv.MapRange()
+	for iter.Next() {
+		k.SetIterKey(iter)
+		elemv.SetIterValue(iter)
 		switch elemv.Kind() {
 		case reflect.Array, reflect.Slice:
 			r, err := ns.checkWhereArray(ctxv, elemv, kv, mv, path, op)