From 290af4e31120212391449d17673c30b8d405028e Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 15 Oct 2025 14:35:44 +0800 Subject: [PATCH] perf(mem): replace Scheme and ExcludedReason string with uint8 type to reduce mem usage --- .../entrypoint/entrypoint_benchmark_test.go | 5 +- internal/route/port_selection.go | 16 ++-- internal/route/provider/docker_test.go | 24 ++--- internal/route/route.go | 95 ++++++++++++++----- internal/route/route_test.go | 2 +- internal/route/types/scheme.go | 79 ++++++++++++--- 6 files changed, 160 insertions(+), 61 deletions(-) diff --git a/internal/entrypoint/entrypoint_benchmark_test.go b/internal/entrypoint/entrypoint_benchmark_test.go index f5796fce..562bab5c 100644 --- a/internal/entrypoint/entrypoint_benchmark_test.go +++ b/internal/entrypoint/entrypoint_benchmark_test.go @@ -13,6 +13,7 @@ import ( . "github.com/yusing/godoxy/internal/entrypoint" "github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route/routes" + routeTypes "github.com/yusing/godoxy/internal/route/types" "github.com/yusing/godoxy/internal/types" "github.com/yusing/goutils/task" ) @@ -78,7 +79,7 @@ func BenchmarkEntrypointReal(b *testing.B) { r := &route.Route{ Alias: "test", - Scheme: "http", + Scheme: routeTypes.SchemeHTTP, Host: host, Port: route.Port{Proxy: portInt}, HealthCheck: &types.HealthCheckConfig{Disable: true}, @@ -119,7 +120,7 @@ func BenchmarkEntrypoint(b *testing.B) { r := &route.Route{ Alias: "test", - Scheme: "http", + Scheme: routeTypes.SchemeHTTP, Host: "localhost", Port: route.Port{ Proxy: 8080, diff --git a/internal/route/port_selection.go b/internal/route/port_selection.go index eb5afadb..0c2be6ab 100644 --- a/internal/route/port_selection.go +++ b/internal/route/port_selection.go @@ -1,5 +1,7 @@ package route +import route "github.com/yusing/godoxy/internal/route/types" + var ( ImageNamePortMapTCP = map[string]int{ "mssql": 1433, @@ -57,25 +59,25 @@ var ( } ) -func getSchemePortByImageName(imageName string) (scheme string, port int, ok bool) { +func getSchemePortByImageName(imageName string) (scheme route.Scheme, port int, ok bool) { if port, ok := ImageNamePortMapHTTP[imageName]; ok { - return "http", port, true + return route.SchemeHTTP, port, true } if port, ok := ImageNamePortMapHTTPS[imageName]; ok { - return "https", port, true + return route.SchemeHTTPS, port, true } if port, ok := ImageNamePortMapTCP[imageName]; ok { - return "tcp", port, true + return route.SchemeTCP, port, true } return scheme, port, ok } -func getSchemePortByAlias(alias string) (scheme string, port int, ok bool) { +func getSchemePortByAlias(alias string) (scheme route.Scheme, port int, ok bool) { if port, ok := AliasPortMapHTTP[alias]; ok { - return "http", port, true + return route.SchemeHTTP, port, true } if port, ok := AliasPortMapHTTPS[alias]; ok { - return "https", port, true + return route.SchemeHTTPS, port, true } return scheme, port, ok } diff --git a/internal/route/provider/docker_test.go b/internal/route/provider/docker_test.go index 34ae657c..65c6df0d 100644 --- a/internal/route/provider/docker_test.go +++ b/internal/route/provider/docker_test.go @@ -9,7 +9,7 @@ import ( "github.com/docker/docker/client" D "github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/route" - T "github.com/yusing/godoxy/internal/route/types" + routeTypes "github.com/yusing/godoxy/internal/route/types" expect "github.com/yusing/goutils/testing" ) @@ -91,8 +91,8 @@ func TestApplyLabel(t *testing.T) { b, ok := entries["b"] expect.True(t, ok) - expect.Equal(t, a.Scheme, "https") - expect.Equal(t, b.Scheme, "https") + expect.Equal(t, a.Scheme, routeTypes.SchemeHTTPS) + expect.Equal(t, b.Scheme, routeTypes.SchemeHTTPS) expect.Equal(t, a.Host, "app") expect.Equal(t, b.Host, "app") @@ -152,12 +152,12 @@ func TestApplyLabelWithAlias(t *testing.T) { c, ok := entries["c"] expect.True(t, ok) - expect.Equal(t, a.Scheme, "http") + expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP) expect.Equal(t, a.Port.Proxy, 3333) expect.Equal(t, a.NoTLSVerify, true) - expect.Equal(t, b.Scheme, "http") + expect.Equal(t, b.Scheme, routeTypes.SchemeHTTP) expect.Equal(t, b.Port.Proxy, 1234) - expect.Equal(t, c.Scheme, "https") + expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS) } func TestApplyLabelWithRef(t *testing.T) { @@ -180,11 +180,11 @@ func TestApplyLabelWithRef(t *testing.T) { c, ok := entries["c"] expect.True(t, ok) - expect.Equal(t, a.Scheme, "http") + expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP) expect.Equal(t, a.Host, "localhost") expect.Equal(t, a.Port.Proxy, 4444) expect.Equal(t, b.Port.Proxy, 9999) - expect.Equal(t, c.Scheme, "https") + expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS) expect.Equal(t, c.Port.Proxy, 1111) } @@ -229,12 +229,12 @@ func TestDynamicAliases(t *testing.T) { r, ok := entries["app1"] expect.True(t, ok) - expect.Equal(t, r.Scheme, "http") + expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP) expect.Equal(t, r.Port.Proxy, 1234) r, ok = entries["app1_backend"] expect.True(t, ok) - expect.Equal(t, r.Scheme, "http") + expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP) expect.Equal(t, r.Port.Proxy, 5678) } @@ -327,7 +327,7 @@ func TestStreamDefaultValues(t *testing.T) { r, ok := makeRoutes(cont)["a"] expect.True(t, ok) expect.NoError(t, r.Validate()) - expect.Equal(t, r.Scheme, T.Scheme("udp")) + expect.Equal(t, r.Scheme, routeTypes.SchemeUDP) expect.Equal(t, r.TargetURL().Hostname(), privIP) expect.Equal(t, r.Port.Listening, 0) expect.Equal(t, r.Port.Proxy, int(privPort)) @@ -337,7 +337,7 @@ func TestStreamDefaultValues(t *testing.T) { r, ok := makeRoutes(cont, testIP)["a"] expect.True(t, ok) expect.NoError(t, r.Validate()) - expect.Equal(t, r.Scheme, T.Scheme("udp")) + expect.Equal(t, r.Scheme, routeTypes.SchemeUDP) expect.Equal(t, r.TargetURL().Hostname(), testIP) expect.Equal(t, r.Port.Listening, 0) expect.Equal(t, r.Port.Proxy, int(pubPort)) diff --git a/internal/route/route.go b/internal/route/route.go index 9c2e283d..f72f39b3 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -8,6 +8,7 @@ import ( "os" "reflect" "runtime" + "strconv" "strings" "sync" "time" @@ -40,7 +41,7 @@ type ( _ utils.NoCopy Alias string `json:"alias"` - Scheme route.Scheme `json:"scheme,omitempty"` + Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string"` Host string `json:"host,omitempty"` Port route.Port `json:"port"` Root string `json:"root,omitempty"` @@ -71,8 +72,8 @@ type ( LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"` ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"` - Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"` - ExcludedReason string `json:"excluded_reason,omitempty" extensions:"x-nullable"` + Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"` + ExcludedReason ExcludedReason `json:"excluded_reason,omitempty" extensions:"x-nullable"` HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"` // for swagger @@ -272,7 +273,7 @@ func (r *Route) Validate() gperr.Error { r.impl = impl r.Excluded = r.ShouldExclude() if r.Excluded { - r.ExcludedReason = r.GetExcludedReason() + r.ExcludedReason = r.findExcludedReason() } return nil } @@ -518,31 +519,73 @@ func (r *Route) ShouldExclude() bool { return false } -func (r *Route) GetExcludedReason() string { - if r.lastError != nil { - return string(gperr.Plain(r.lastError)) +type ExcludedReason uint8 + +const ( + ExcludedReasonNone ExcludedReason = iota + ExcludedReasonError + ExcludedReasonManual + ExcludedReasonNoPortContainer + ExcludedReasonNoPortSpecified + ExcludedReasonBlacklisted + ExcludedReasonBuildx + ExcludedReasonOld +) + +func (re ExcludedReason) String() string { + switch re { + case ExcludedReasonNone: + return "" + case ExcludedReasonError: + return "Error" + case ExcludedReasonManual: + return "Manual exclusion" + case ExcludedReasonNoPortContainer: + return "No port exposed in container" + case ExcludedReasonNoPortSpecified: + return "No port specified" + case ExcludedReasonBlacklisted: + return "Blacklisted (backend service or database)" + case ExcludedReasonBuildx: + return "Buildx" + case ExcludedReasonOld: + return "Container renaming intermediate state" + default: + return "Unknown" } - if r.ExcludedReason != "" { +} + +func (re ExcludedReason) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, re.String()), nil +} + +// no need to unmarshal json because we don't store this + +func (r *Route) findExcludedReason() ExcludedReason { + if r.lastError != nil { + return ExcludedReasonError + } + if r.ExcludedReason != ExcludedReasonNone { return r.ExcludedReason } if r.Container != nil { switch { case r.Container.IsExcluded: - return "Manual exclusion" + return ExcludedReasonManual case r.IsZeroPort() && !r.UseIdleWatcher(): - return "No port exposed in container" + return ExcludedReasonNoPortContainer case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container): - return "Blacklisted (backend service or database)" + return ExcludedReasonBlacklisted case strings.HasPrefix(r.Container.ContainerName, "buildx_"): - return "Buildx" + return ExcludedReasonBuildx } } else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer { - return "No port specified" + return ExcludedReasonNoPortSpecified } if strings.HasSuffix(r.Alias, "-old") { - return "Container renaming intermediate state" + return ExcludedReasonOld } - return "" + return ExcludedReasonNone } func (r *Route) UseLoadBalance() bool { @@ -594,8 +637,8 @@ func (r *Route) Finalize() { if isDocker { scheme, port, ok := getSchemePortByImageName(cont.Image.Name) if ok { - if r.Scheme == "" { - r.Scheme = route.Scheme(scheme) + if r.Scheme == route.SchemeNone { + r.Scheme = scheme } if pp == 0 { pp = port @@ -604,8 +647,8 @@ func (r *Route) Finalize() { } if scheme, port, ok := getSchemePortByAlias(r.Alias); ok { - if r.Scheme == "" { - r.Scheme = route.Scheme(scheme) + if r.Scheme == route.SchemeNone { + r.Scheme = scheme } if pp == 0 { pp = port @@ -620,7 +663,7 @@ func (r *Route) Finalize() { } else { pp = preferredPort(cont.PrivatePortMapping) } - case r.Scheme == "https": + case r.Scheme == route.SchemeHTTPS: pp = 443 default: pp = 80 @@ -628,10 +671,10 @@ func (r *Route) Finalize() { } if isDocker { - if r.Scheme == "" { + if r.Scheme == route.SchemeNone { for _, p := range cont.PublicPortMapping { if int(p.PrivatePort) == pp && p.Type == "udp" { - r.Scheme = "udp" + r.Scheme = route.SchemeUDP break } } @@ -649,14 +692,14 @@ func (r *Route) Finalize() { } } - if r.Scheme == "" { + if r.Scheme == route.SchemeNone { switch { case lp != 0: - r.Scheme = "tcp" + r.Scheme = route.SchemeTCP case pp%1000 == 443: - r.Scheme = "https" + r.Scheme = route.SchemeHTTPS default: // assume its http - r.Scheme = "http" + r.Scheme = route.SchemeHTTP } } diff --git a/internal/route/route_test.go b/internal/route/route_test.go index 63ce73d6..f4664ef1 100644 --- a/internal/route/route_test.go +++ b/internal/route/route_test.go @@ -113,7 +113,7 @@ func TestRouteValidate(t *testing.T) { t.Run("InvalidScheme", func(t *testing.T) { r := &Route{ Alias: "test", - Scheme: "invalid", + Scheme: 123, Host: "example.com", Port: route.Port{Proxy: 80}, } diff --git a/internal/route/types/scheme.go b/internal/route/types/scheme.go index 1b2e33e4..9cb1dc80 100644 --- a/internal/route/types/scheme.go +++ b/internal/route/types/scheme.go @@ -1,29 +1,82 @@ package route import ( + "strconv" + + "github.com/bytedance/sonic" gperr "github.com/yusing/goutils/errs" ) -type Scheme string +type Scheme uint8 var ErrInvalidScheme = gperr.New("invalid scheme") const ( - SchemeHTTP Scheme = "http" - SchemeHTTPS Scheme = "https" - SchemeTCP Scheme = "tcp" - SchemeUDP Scheme = "udp" - SchemeFileServer Scheme = "fileserver" + SchemeHTTP Scheme = 1 << iota + SchemeHTTPS + SchemeTCP + SchemeUDP + SchemeFileServer + SchemeNone Scheme = 0 + + schemeReverseProxy = SchemeHTTP | SchemeHTTPS + schemeStream = SchemeTCP | SchemeUDP + + schemeStrHTTP = "http" + schemeStrHTTPS = "https" + schemeStrTCP = "tcp" + schemeStrUDP = "udp" + schemeStrFileServer = "fileserver" + schemeStrUnknown = "unknown" ) -func (s Scheme) Validate() gperr.Error { +func (s Scheme) String() string { switch s { - case SchemeHTTP, SchemeHTTPS, - SchemeTCP, SchemeUDP, SchemeFileServer: - return nil + case SchemeHTTP: + return schemeStrHTTP + case SchemeHTTPS: + return schemeStrHTTPS + case SchemeTCP: + return schemeStrTCP + case SchemeUDP: + return schemeStrUDP + case SchemeFileServer: + return schemeStrFileServer + default: + return schemeStrUnknown } - return ErrInvalidScheme.Subject(string(s)) } -func (s Scheme) IsReverseProxy() bool { return s == SchemeHTTP || s == SchemeHTTPS } -func (s Scheme) IsStream() bool { return s == SchemeTCP || s == SchemeUDP } +func (s Scheme) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, s.String()), nil +} + +func (s *Scheme) UnmarshalJSON(data []byte) error { + var v string + if err := sonic.Unmarshal(data, &v); err != nil { + return err + } + return s.Parse(v) +} + +// Parse implements strutils.Parser +func (s *Scheme) Parse(v string) error { + switch v { + case schemeStrHTTP: + *s = SchemeHTTP + case schemeStrHTTPS: + *s = SchemeHTTPS + case schemeStrTCP: + *s = SchemeTCP + case schemeStrUDP: + *s = SchemeUDP + case schemeStrFileServer: + *s = SchemeFileServer + default: + return ErrInvalidScheme.Subject(v) + } + return nil +} + +func (s Scheme) IsReverseProxy() bool { return s&schemeReverseProxy != 0 } +func (s Scheme) IsStream() bool { return s&schemeStream != 0 }