feat: custom json marshaling implementation, replace json and yaml library (#89)

* chore: replace gopkg.in/yaml.v3 vs goccy/go-yaml; replace encoding/json with bytedance/sonic

* fix: yaml unmarshal panic

* feat: custom json marshaler implementation

* chore: fix import and err marshal handling

---------

Co-authored-by: yusing <yusing@6uo.me>
This commit is contained in:
Yuzerion
2025-04-16 15:02:11 +08:00
committed by GitHub
parent 57292f0fe8
commit 80bc018a7f
65 changed files with 1749 additions and 205 deletions

View File

@@ -1,8 +1,9 @@
package period
import (
"encoding/json"
"time"
"github.com/yusing/go-proxy/pkg/json"
)
type Entries[T any] struct {
@@ -48,11 +49,11 @@ func (e *Entries[T]) Get() []*T {
return res
}
func (e *Entries[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
func (e *Entries[T]) MarshalJSONTo(buf []byte) []byte {
return json.MarshalTo(map[string]any{
"entries": e.Get(),
"interval": e.interval,
})
}, buf)
}
func (e *Entries[T]) UnmarshalJSON(data []byte) error {

View File

@@ -2,7 +2,6 @@ package period
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
@@ -15,6 +14,7 @@ import (
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/atomic"
"github.com/yusing/go-proxy/pkg/json"
)
type (

View File

@@ -0,0 +1,71 @@
package period
import (
"context"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/pkg/json"
)
func (p *Poller[T, AggregateT]) Test(t *testing.T, query url.Values) {
t.Helper()
for range 3 {
require.NoError(t, p.testPoll())
}
t.Run("periods", func(t *testing.T) {
assert.NoError(t, p.testMarshalPeriods(query))
})
t.Run("no period", func(t *testing.T) {
assert.NoError(t, p.testMarshalNoPeriod())
})
}
func (p *Poller[T, AggregateT]) testPeriod(period string, query url.Values) (any, error) {
query.Set("period", period)
return p.getRespData(&http.Request{URL: &url.URL{RawQuery: query.Encode()}})
}
func (p *Poller[T, AggregateT]) testPoll() error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
data, err := p.poll(ctx, p.lastResult.Load())
if err != nil {
return err
}
for _, period := range p.period.Entries {
period.Add(time.Now(), data)
}
p.lastResult.Store(data)
return nil
}
func (p *Poller[T, AggregateT]) testMarshalPeriods(query url.Values) error {
for period := range p.period.Entries {
data, err := p.testPeriod(string(period), query)
if err != nil {
return err
}
_, err = json.Marshal(data)
if err != nil {
return err
}
}
return nil
}
func (p *Poller[T, AggregateT]) testMarshalNoPeriod() error {
data, err := p.getRespData(&http.Request{URL: &url.URL{}})
if err != nil {
return err
}
_, err = json.Marshal(data)
if err != nil {
return err
}
return nil
}

View File

@@ -3,7 +3,6 @@ package systeminfo
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -20,6 +19,7 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/metrics/period"
"github.com/yusing/go-proxy/pkg/json"
)
// json tags are left for tests
@@ -55,7 +55,7 @@ type (
DownloadSpeed float64 `json:"download_speed"`
}
Sensors []sensors.TemperatureStat
Aggregated []map[string]any
Aggregated = json.MapSlice[any]
)
type SystemInfo struct {
@@ -295,8 +295,8 @@ func (s *SystemInfo) collectSensorsInfo(ctx context.Context) error {
}
// explicitly implement MarshalJSON to avoid reflection
func (s *SystemInfo) MarshalJSON() ([]byte, error) {
b := bytes.NewBuffer(make([]byte, 0, 1024))
func (s *SystemInfo) MarshalJSONTo(buf []byte) []byte {
b := bytes.NewBuffer(buf)
b.WriteRune('{')
@@ -315,7 +315,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
// memory
b.WriteString(`,"memory":`)
if s.Memory != nil {
b.WriteString(fmt.Sprintf(
b.Write(fmt.Appendf(nil,
`{"total":%d,"available":%d,"used":%d,"used_percent":%s}`,
s.Memory.Total,
s.Memory.Available,
@@ -329,13 +329,13 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
// disk
b.WriteString(`,"disks":`)
if len(s.Disks) > 0 {
b.WriteString("{")
b.WriteRune('{')
first := true
for device, disk := range s.Disks {
if !first {
b.WriteRune(',')
}
b.WriteString(fmt.Sprintf(
b.Write(fmt.Appendf(nil,
`"%s":{"device":%q,"path":%q,"fstype":%q,"total":%d,"free":%d,"used":%d,"used_percent":%s}`,
device,
device,
@@ -362,7 +362,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
if !first {
b.WriteRune(',')
}
b.WriteString(fmt.Sprintf(
b.Write(fmt.Appendf(nil,
`"%s":{"name":%q,"read_bytes":%d,"write_bytes":%d,"read_speed":%s,"write_speed":%s,"iops":%d}`,
name,
name,
@@ -382,7 +382,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
// network
b.WriteString(`,"network":`)
if s.Network != nil {
b.WriteString(fmt.Sprintf(
b.Write(fmt.Appendf(nil,
`{"bytes_sent":%d,"bytes_recv":%d,"upload_speed":%s,"download_speed":%s}`,
s.Network.BytesSent,
s.Network.BytesRecv,
@@ -396,13 +396,13 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
// sensors
b.WriteString(`,"sensors":`)
if len(s.Sensors) > 0 {
b.WriteString("{")
b.WriteRune('{')
first := true
for _, sensor := range s.Sensors {
if !first {
b.WriteRune(',')
}
b.WriteString(fmt.Sprintf(
b.Write(fmt.Appendf(nil,
`%q:{"name":%q,"temperature":%s,"high":%s,"critical":%s}`,
sensor.SensorKey,
sensor.SensorKey,
@@ -418,7 +418,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
}
b.WriteRune('}')
return []byte(b.String()), nil
return b.Bytes()
}
func (s *Sensors) UnmarshalJSON(data []byte) error {
@@ -560,43 +560,3 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
}
return len(aggregated), aggregated
}
func (result Aggregated) MarshalJSON() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
buf.WriteByte('[')
i := 0
n := len(result)
for _, entry := range result {
buf.WriteRune('{')
j := 0
m := len(entry)
for k, v := range entry {
buf.WriteByte('"')
buf.WriteString(k)
buf.WriteByte('"')
buf.WriteByte(':')
switch v := v.(type) {
case float64:
buf.WriteString(strconv.FormatFloat(v, 'f', 2, 64))
case uint64:
buf.WriteString(strconv.FormatUint(v, 10))
case int64:
buf.WriteString(strconv.FormatInt(v, 10))
default:
panic(fmt.Sprintf("unexpected type: %T", v))
}
if j != m-1 {
buf.WriteByte(',')
}
j++
}
buf.WriteByte('}')
if i != n-1 {
buf.WriteByte(',')
}
i++
}
buf.WriteByte(']')
return buf.Bytes(), nil
}

View File

@@ -1,13 +1,13 @@
package systeminfo
import (
"encoding/json"
"net/url"
"reflect"
"testing"
"github.com/shirou/gopsutil/v4/sensors"
. "github.com/yusing/go-proxy/internal/utils/testing"
"github.com/yusing/go-proxy/pkg/json"
)
func TestExcludeDisks(t *testing.T) {
@@ -191,8 +191,7 @@ func TestSerialize(t *testing.T) {
for _, query := range allQueries {
t.Run(query, func(t *testing.T) {
_, result := aggregate(entries, url.Values{"aggregate": []string{query}})
s, err := result.MarshalJSON()
ExpectNoError(t, err)
s := result.MarshalJSONTo(nil)
var v []map[string]any
ExpectNoError(t, json.Unmarshal(s, &v))
ExpectEqual(t, len(v), len(result))
@@ -206,31 +205,3 @@ func TestSerialize(t *testing.T) {
})
}
}
func BenchmarkJSONMarshal(b *testing.B) {
entries := make([]*SystemInfo, b.N)
for i := range b.N {
entries[i] = testInfo
}
queries := map[string]Aggregated{}
for _, query := range allQueries {
_, result := aggregate(entries, url.Values{"aggregate": []string{query}})
queries[query] = result
}
b.ReportAllocs()
b.ResetTimer()
b.Run("optimized", func(b *testing.B) {
for b.Loop() {
for _, query := range allQueries {
_, _ = queries[query].MarshalJSON()
}
}
})
b.Run("json", func(b *testing.B) {
for b.Loop() {
for _, query := range allQueries {
_, _ = json.Marshal([]map[string]any(queries[query]))
}
}
})
}

View File

@@ -0,0 +1,22 @@
package uptime
import (
"fmt"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type Status struct {
Status health.Status
Latency int64
Timestamp int64
}
type RouteStatuses map[string][]*Status
func (s *Status) MarshalJSONTo(buf []byte) []byte {
return fmt.Appendf(buf,
`{"status":"%s","latency":"%d","timestamp":"%d"}`,
s.Status, s.Latency, s.Timestamp,
)
}

View File

@@ -2,7 +2,6 @@ package uptime
import (
"context"
"encoding/json"
"net/url"
"sort"
"time"
@@ -13,20 +12,15 @@ import (
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/route/routes/routequery"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/pkg/json"
)
type (
StatusByAlias struct {
Map map[string]*routequery.HealthInfoRaw `json:"statuses"`
Timestamp int64 `json:"timestamp"`
Map json.Map[*routequery.HealthInfoRaw] `json:"statuses"`
Timestamp int64 `json:"timestamp"`
}
Status struct {
Status health.Status `json:"status"`
Latency int64 `json:"latency"`
Timestamp int64 `json:"timestamp"`
}
RouteStatuses map[string][]*Status
Aggregated []map[string]any
Aggregated = json.MapSlice[any]
)
var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
@@ -124,7 +118,3 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
}
return result
}
func (result Aggregated) MarshalJSON() ([]byte, error) {
return json.Marshal([]map[string]any(result))
}

View File

@@ -0,0 +1,10 @@
package uptime
import (
"net/url"
"testing"
)
func TestPoller(t *testing.T) {
Poller.Test(t, url.Values{"limit": []string{"1"}})
}