Merge branch 'main' into dev

This commit is contained in:
yusing
2026-01-02 22:12:11 +08:00
11 changed files with 120 additions and 390 deletions

View File

@@ -2956,43 +2956,6 @@
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"HealthInfo": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"latency": {
"description": "latency in microseconds",
"type": "number",
"x-nullable": false,
"x-omitempty": false
},
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy",
"napping",
"starting",
"error",
"unknown"
],
"x-nullable": false,
"x-omitempty": false
},
"uptime": {
"description": "uptime in milliseconds",
"type": "number",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"HealthInfoWithoutDetail": { "HealthInfoWithoutDetail": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3047,22 +3010,14 @@
"x-nullable": true "x-nullable": true
}, },
"lastSeen": { "lastSeen": {
"description": "unix timestamp in seconds",
"type": "integer", "type": "integer",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"lastSeenStr": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"latency": { "latency": {
"type": "number", "description": "latency in milliseconds",
"x-nullable": false, "type": "integer",
"x-omitempty": false
},
"latencyStr": {
"type": "string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
@@ -3072,30 +3027,22 @@
"x-omitempty": false "x-omitempty": false
}, },
"started": { "started": {
"description": "unix timestamp in seconds",
"type": "integer", "type": "integer",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"startedStr": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"status": { "status": {
"type": "string", "$ref": "#/definitions/HealthStatusString",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"uptime": { "uptime": {
"description": "uptime in seconds",
"type": "number", "type": "number",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"uptimeStr": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"url": { "url": {
"type": "string", "type": "string",
"x-nullable": false, "x-nullable": false,
@@ -3108,11 +3055,32 @@
"HealthMap": { "HealthMap": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"$ref": "#/definitions/HealthInfo" "$ref": "#/definitions/HealthStatusString"
}, },
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"HealthStatusString": {
"type": "string",
"enum": [
"unknown",
"healthy",
"napping",
"starting",
"unhealthy",
"error"
],
"x-enum-varnames": [
"StatusUnknownStr",
"StatusHealthyStr",
"StatusNappingStr",
"StatusStartingStr",
"StatusUnhealthyStr",
"StatusErrorStr"
],
"x-nullable": false,
"x-omitempty": false
},
"HomepageCategory": { "HomepageCategory": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -302,26 +302,6 @@ definitions:
additionalProperties: {} additionalProperties: {}
type: object type: object
type: object type: object
HealthInfo:
properties:
detail:
type: string
latency:
description: latency in microseconds
type: number
status:
enum:
- healthy
- unhealthy
- napping
- starting
- error
- unknown
type: string
uptime:
description: uptime in milliseconds
type: number
type: object
HealthInfoWithoutDetail: HealthInfoWithoutDetail:
properties: properties:
latency: latency:
@@ -351,32 +331,44 @@ definitions:
- $ref: '#/definitions/HealthExtra' - $ref: '#/definitions/HealthExtra'
x-nullable: true x-nullable: true
lastSeen: lastSeen:
description: unix timestamp in seconds
type: integer type: integer
lastSeenStr:
type: string
latency: latency:
type: number description: latency in milliseconds
latencyStr: type: integer
type: string
name: name:
type: string type: string
started: started:
description: unix timestamp in seconds
type: integer type: integer
startedStr:
type: string
status: status:
type: string $ref: '#/definitions/HealthStatusString'
uptime: uptime:
description: uptime in seconds
type: number type: number
uptimeStr:
type: string
url: url:
type: string type: string
type: object type: object
HealthMap: HealthMap:
additionalProperties: additionalProperties:
$ref: '#/definitions/HealthInfo' $ref: '#/definitions/HealthStatusString'
type: object type: object
HealthStatusString:
enum:
- unknown
- healthy
- napping
- starting
- unhealthy
- error
type: string
x-enum-varnames:
- StatusUnknownStr
- StatusHealthyStr
- StatusNappingStr
- StatusStartingStr
- StatusUnhealthyStr
- StatusErrorStr
HomepageCategory: HomepageCategory:
properties: properties:
items: items:

View File

@@ -12,8 +12,6 @@ import (
_ "github.com/yusing/goutils/apitypes" _ "github.com/yusing/goutils/apitypes"
) )
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
// @x-id "health" // @x-id "health"
// @BasePath /api/v1 // @BasePath /api/v1
// @Summary Get routes health info // @Summary Get routes health info
@@ -21,16 +19,16 @@ type HealthMap = map[string]routes.HealthInfo // @name HealthMap
// @Tags v1,websocket // @Tags v1,websocket
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} HealthMap "Health info by route name" // @Success 200 {object} routes.HealthMap "Health info by route name"
// @Failure 403 {object} apitypes.ErrorResponse // @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse
// @Router /health [get] // @Router /health [get]
func Health(c *gin.Context) { func Health(c *gin.Context) {
if httpheaders.IsWebsocket(c.Request.Header) { if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) { websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
return routes.GetHealthInfo(), nil return routes.GetHealthInfoSimple(), nil
}) })
} else { } else {
c.JSON(http.StatusOK, routes.GetHealthInfo()) c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
} }
} }

View File

@@ -296,7 +296,7 @@ func (state *state) initProxmox() error {
errs := gperr.NewBuilder() errs := gperr.NewBuilder()
for _, cfg := range proxmoxCfg { for _, cfg := range proxmoxCfg {
if err := cfg.Init(); err != nil { if err := cfg.Init(state.task.Context()); err != nil {
errs.Add(err.Subject(cfg.URL)) errs.Add(err.Subject(cfg.URL))
} }
} }

View File

@@ -91,14 +91,14 @@ func IsBlacklisted(c *types.Container) bool {
return IsBlacklistedImage(c.Image) || isDatabase(c) return IsBlacklistedImage(c.Image) || isDatabase(c)
} }
func UpdatePorts(c *types.Container) error { func UpdatePorts(ctx context.Context, c *types.Container) error {
dockerClient, err := NewClient(c.DockerCfg) dockerClient, err := NewClient(c.DockerCfg)
if err != nil { if err != nil {
return err return err
} }
defer dockerClient.Close() defer dockerClient.Close()
inspect, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID, client.ContainerInspectOptions{}) inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -25,14 +25,14 @@ const proxmoxStateCheckInterval = 1 * time.Second
var ErrNodeNotFound = gperr.New("node not found in pool") var ErrNodeNotFound = gperr.New("node not found in pool")
func NewProxmoxProvider(nodeName string, vmid int) (idlewatcher.Provider, error) { func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) {
node, ok := proxmox.Nodes.Get(nodeName) node, ok := proxmox.Nodes.Get(nodeName)
if !ok { if !ok {
return nil, ErrNodeNotFound.Subject(nodeName). return nil, ErrNodeNotFound.Subject(nodeName).
Withf("available nodes: %s", proxmox.AvailableNodeNames()) Withf("available nodes: %s", proxmox.AvailableNodeNames())
} }
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() defer cancel()
lxcName, err := node.LXCName(ctx, vmid) lxcName, err := node.LXCName(ctx, vmid)

View File

@@ -32,7 +32,7 @@ func (c *Config) Client() *Client {
return c.client return c.client
} }
func (c *Config) Init() gperr.Error { func (c *Config) Init(ctx context.Context) gperr.Error {
var tr *http.Transport var tr *http.Transport
if c.NoTLSVerify { if c.NoTLSVerify {
// user specified // user specified
@@ -56,7 +56,7 @@ func (c *Config) Init() gperr.Error {
} }
c.client = NewClient(c.URL, opts...) c.client = NewClient(c.URL, opts...)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() defer cancel()
if err := c.client.UpdateClusterInfo(ctx); err != nil { if err := c.client.UpdateClusterInfo(ctx); err != nil {

View File

@@ -79,7 +79,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
} }
if container.IsHostNetworkMode { if container.IsHostNetworkMode {
err := docker.UpdatePorts(container) err := docker.UpdatePorts(ctx, container)
if err != nil { if err != nil {
errs.Add(gperr.PrependSubject(container.ContainerName, err)) errs.Add(gperr.PrependSubject(container.ContainerName, err))
continue continue

View File

@@ -17,6 +17,8 @@ type HealthInfoWithoutDetail struct {
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
} // @name HealthInfoWithoutDetail } // @name HealthInfoWithoutDetail
type HealthMap = map[string]types.HealthStatusString // @name HealthMap
// GetHealthInfo returns a map of route name to health info. // GetHealthInfo returns a map of route name to health info.
// //
// The health info is for all routes, including excluded routes. // The health info is for all routes, including excluded routes.
@@ -39,6 +41,14 @@ func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail {
return healthMap return healthMap
} }
func GetHealthInfoSimple() map[string]types.HealthStatus {
healthMap := make(map[string]types.HealthStatus, NumAllRoutes())
for r := range IterAll {
healthMap[r.Name()] = getHealthInfoSimple(r)
}
return healthMap
}
func getHealthInfo(r types.Route) HealthInfo { func getHealthInfo(r types.Route) HealthInfo {
mon := r.HealthMonitor() mon := r.HealthMonitor()
if mon == nil { if mon == nil {
@@ -73,6 +83,14 @@ func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail {
} }
} }
func getHealthInfoSimple(r types.Route) types.HealthStatus {
mon := r.HealthMonitor()
if mon == nil {
return types.StatusUnknown
}
return mon.Status()
}
// ByProvider returns a map of provider name to routes. // ByProvider returns a map of provider name to routes.
// //
// The routes are all routes, including excluded routes. // The routes are all routes, including excluded routes.

View File

@@ -8,12 +8,12 @@ import (
"time" "time"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
type ( type (
HealthStatus uint8 HealthStatus uint8 // @name HealthStatus
HealthStatusString string // @name HealthStatusString
HealthCheckResult struct { HealthCheckResult struct {
Healthy bool `json:"healthy"` Healthy bool `json:"healthy"`
@@ -45,20 +45,16 @@ type (
HealthChecker HealthChecker
} }
HealthJSON struct { HealthJSON struct {
Name string `json:"name"` Name string `json:"name"`
Config *HealthCheckConfig `json:"config"` Config *HealthCheckConfig `json:"config"`
Started int64 `json:"started"` Started int64 `json:"started"` // unix timestamp in seconds
StartedStr string `json:"startedStr"` Status HealthStatusString `json:"status"`
Status string `json:"status"` Uptime float64 `json:"uptime"` // uptime in seconds
Uptime float64 `json:"uptime"` Latency int64 `json:"latency"` // latency in milliseconds
UptimeStr string `json:"uptimeStr"` LastSeen int64 `json:"lastSeen"` // unix timestamp in seconds
Latency float64 `json:"latency"` Detail string `json:"detail"`
LatencyStr string `json:"latencyStr"` URL string `json:"url"`
LastSeen int64 `json:"lastSeen"` Extra *HealthExtra `json:"extra,omitempty" extensions:"x-nullable"`
LastSeenStr string `json:"lastSeenStr"`
Detail string `json:"detail"`
URL string `json:"url"`
Extra *HealthExtra `json:"extra,omitempty" extensions:"x-nullable"`
} // @name HealthJSON } // @name HealthJSON
HealthJSONRepr struct { HealthJSONRepr struct {
@@ -88,12 +84,12 @@ const (
StatusUnhealthy StatusUnhealthy
StatusError StatusError
StatusUnknownStr = "unknown" StatusUnknownStr HealthStatusString = "unknown"
StatusHealthyStr = "healthy" StatusHealthyStr HealthStatusString = "healthy"
StatusNappingStr = "napping" StatusNappingStr HealthStatusString = "napping"
StatusStartingStr = "starting" StatusStartingStr HealthStatusString = "starting"
StatusUnhealthyStr = "unhealthy" StatusUnhealthyStr HealthStatusString = "unhealthy"
StatusErrorStr = "error" StatusErrorStr HealthStatusString = "error"
NumStatuses int = iota - 1 NumStatuses int = iota - 1
@@ -102,15 +98,15 @@ const (
) )
var ( var (
StatusHealthyStr2 = strconv.Itoa(int(StatusHealthy)) StatusHealthyStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusHealthy)))
StatusNappingStr2 = strconv.Itoa(int(StatusNapping)) StatusNappingStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusNapping)))
StatusStartingStr2 = strconv.Itoa(int(StatusStarting)) StatusStartingStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusStarting)))
StatusUnhealthyStr2 = strconv.Itoa(int(StatusUnhealthy)) StatusUnhealthyStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusUnhealthy)))
StatusErrorStr2 = strconv.Itoa(int(StatusError)) StatusErrorStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusError)))
) )
func NewHealthStatusFromString(s string) HealthStatus { func NewHealthStatusFromString(s string) HealthStatus {
switch s { switch HealthStatusString(s) {
case StatusHealthyStr, StatusHealthyStr2: case StatusHealthyStr, StatusHealthyStr2:
return StatusHealthy return StatusHealthy
case StatusUnhealthyStr, StatusUnhealthyStr2: case StatusUnhealthyStr, StatusUnhealthyStr2:
@@ -126,7 +122,7 @@ func NewHealthStatusFromString(s string) HealthStatus {
} }
} }
func (s HealthStatus) String() string { func (s HealthStatus) StatusString() HealthStatusString {
switch s { switch s {
case StatusHealthy: case StatusHealthy:
return StatusHealthyStr return StatusHealthyStr
@@ -143,6 +139,11 @@ func (s HealthStatus) String() string {
} }
} }
// String implements fmt.Stringer.
func (s HealthStatus) String() string {
return string(s.StatusString())
}
func (s HealthStatus) Good() bool { func (s HealthStatus) Good() bool {
return s&HealthyMask != 0 return s&HealthyMask != 0
} }
@@ -178,19 +179,15 @@ func (jsonRepr *HealthJSONRepr) MarshalJSON() ([]byte, error) {
url = "" url = ""
} }
return sonic.Marshal(HealthJSON{ return sonic.Marshal(HealthJSON{
Name: jsonRepr.Name, Name: jsonRepr.Name,
Config: jsonRepr.Config, Config: jsonRepr.Config,
Started: jsonRepr.Started.Unix(), Started: jsonRepr.Started.Unix(),
StartedStr: strutils.FormatTime(jsonRepr.Started), Status: HealthStatusString(jsonRepr.Status.String()),
Status: jsonRepr.Status.String(), Uptime: jsonRepr.Uptime.Seconds(),
Uptime: jsonRepr.Uptime.Seconds(), Latency: jsonRepr.Latency.Milliseconds(),
UptimeStr: strutils.FormatDuration(jsonRepr.Uptime), LastSeen: jsonRepr.LastSeen.Unix(),
Latency: jsonRepr.Latency.Seconds(), Detail: jsonRepr.Detail,
LatencyStr: strconv.Itoa(int(jsonRepr.Latency.Milliseconds())) + " ms", URL: url,
LastSeen: jsonRepr.LastSeen.Unix(), Extra: jsonRepr.Extra,
LastSeenStr: strutils.FormatLastSeen(jsonRepr.LastSeen),
Detail: jsonRepr.Detail,
URL: url,
Extra: jsonRepr.Extra,
}) })
} }

View File

@@ -1,243 +0,0 @@
package utils
import (
"reflect"
"unsafe"
)
// DeepEqual reports whether x and y are deeply equal.
// It supports numerics, strings, maps, slices, arrays, and structs (exported fields only).
// It's optimized for performance by avoiding reflection for common types and
// adaptively choosing between BFS and DFS traversal strategies.
func DeepEqual(x, y any) bool {
if x == nil || y == nil {
return x == y
}
v1 := reflect.ValueOf(x)
v2 := reflect.ValueOf(y)
if v1.Type() != v2.Type() {
return false
}
return deepEqual(v1, v2, make(map[visit]bool), 0)
}
// visit represents a visit to a pair of values during comparison
type visit struct {
a1, a2 unsafe.Pointer
typ reflect.Type
}
// deepEqual performs the actual deep comparison with cycle detection
func deepEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if !v1.IsValid() || !v2.IsValid() {
return v1.IsValid() == v2.IsValid()
}
if v1.Type() != v2.Type() {
return false
}
// Handle cycle detection for pointer-like types
if v1.CanAddr() && v2.CanAddr() {
addr1 := unsafe.Pointer(v1.UnsafeAddr())
addr2 := unsafe.Pointer(v2.UnsafeAddr())
typ := v1.Type()
v := visit{addr1, addr2, typ}
if visited[v] {
return true // already visiting, assume equal
}
visited[v] = true
defer delete(visited, v)
}
switch v1.Kind() {
case reflect.Bool:
return v1.Bool() == v2.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v1.Int() == v2.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v1.Uint() == v2.Uint()
case reflect.Float32, reflect.Float64:
return floatEqual(v1.Float(), v2.Float())
case reflect.Complex64, reflect.Complex128:
c1, c2 := v1.Complex(), v2.Complex()
return floatEqual(real(c1), real(c2)) && floatEqual(imag(c1), imag(c2))
case reflect.String:
return v1.String() == v2.String()
case reflect.Array:
return deepEqualArray(v1, v2, visited, depth)
case reflect.Slice:
return deepEqualSlice(v1, v2, visited, depth)
case reflect.Map:
return deepEqualMap(v1, v2, visited, depth)
case reflect.Struct:
return deepEqualStruct(v1, v2, visited, depth)
case reflect.Ptr:
if v1.IsNil() || v2.IsNil() {
return v1.IsNil() && v2.IsNil()
}
return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1)
case reflect.Interface:
if v1.IsNil() || v2.IsNil() {
return v1.IsNil() && v2.IsNil()
}
return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1)
default:
// For unsupported types (func, chan, etc.), fall back to basic equality
return v1.Interface() == v2.Interface()
}
}
// floatEqual handles NaN cases properly
func floatEqual(f1, f2 float64) bool {
return f1 == f2 || (f1 != f1 && f2 != f2) // NaN == NaN
}
// deepEqualArray compares arrays using DFS (since arrays have fixed size)
func deepEqualArray(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
for i := range v1.Len() {
if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
return false
}
}
return true
}
// deepEqualSlice compares slices, choosing strategy based on size and depth
func deepEqualSlice(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if v1.IsNil() != v2.IsNil() {
return false
}
if v1.Len() != v2.Len() {
return false
}
if v1.IsNil() {
return true
}
// Use BFS for large slices at shallow depth to improve cache locality
// Use DFS for small slices or deep nesting to reduce memory overhead
if shouldUseBFS(v1.Len(), depth) {
return deepEqualSliceBFS(v1, v2, visited, depth)
}
return deepEqualSliceDFS(v1, v2, visited, depth)
}
// deepEqualSliceDFS uses depth-first traversal
func deepEqualSliceDFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
for i := range v1.Len() {
if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
return false
}
}
return true
}
// deepEqualSliceBFS uses breadth-first traversal for better cache locality
func deepEqualSliceBFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
length := v1.Len()
// First, check all direct elements
for i := range length {
elem1, elem2 := v1.Index(i), v2.Index(i)
// For simple types, compare directly
if isSimpleType(elem1.Kind()) {
if !deepEqual(elem1, elem2, visited, depth+1) {
return false
}
}
}
// Then, recursively check complex elements
for i := range length {
elem1, elem2 := v1.Index(i), v2.Index(i)
if !isSimpleType(elem1.Kind()) {
if !deepEqual(elem1, elem2, visited, depth+1) {
return false
}
}
}
return true
}
// deepEqualMap compares maps
func deepEqualMap(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if v1.IsNil() != v2.IsNil() {
return false
}
if v1.Len() != v2.Len() {
return false
}
if v1.IsNil() {
return true
}
// Check all keys and values
for _, key := range v1.MapKeys() {
val1 := v1.MapIndex(key)
val2 := v2.MapIndex(key)
if !val2.IsValid() {
return false // key doesn't exist in v2
}
if !deepEqual(val1, val2, visited, depth+1) {
return false
}
}
return true
}
// deepEqualStruct compares structs (exported fields only)
func deepEqualStruct(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
typ := v1.Type()
for i := range typ.NumField() {
field := typ.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
if !deepEqual(v1.Field(i), v2.Field(i), visited, depth+1) {
return false
}
}
return true
}
// shouldUseBFS determines whether to use BFS or DFS based on slice size and depth
func shouldUseBFS(length, depth int) bool {
// Use BFS for large slices at shallow depth (better cache locality)
// Use DFS for small slices or deep nesting (lower memory overhead)
return length > 100 && depth < 3
}
// isSimpleType checks if a type can be compared without deep recursion
func isSimpleType(kind reflect.Kind) bool {
if kind >= reflect.Bool && kind <= reflect.Complex128 {
return true
}
return kind == reflect.String
}