goccy / go-json

Fast JSON encoder/decoder compatible with encoding/json for Go
MIT License
3.11k stars 148 forks source link

High Mem & GC when compiling large cyclic data structure #516

Open kaijietti opened 4 months ago

kaijietti commented 4 months ago

go.mod:

module gojsontest

go 1.21

require (
    github.com/goccy/go-json v0.10.3
    github.com/stripe/stripe-go/v79 v79.2.0
)

main.go:

package main

import (
    "fmt"
    "log"
    "net/http"
    _ "net/http/pprof"

    "github.com/goccy/go-json"
    "github.com/stripe/stripe-go/v79"
)

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    bytes, err := json.Marshal(&stripe.CheckoutSession{})
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bytes))
}

The above code stuck in func (c *Compiler) compile(typeptr uintptr) phase. Plenty of memory was allocated and then GC contributed most of the CPU time.

A pprof sample is here. pprof.samples.cpu.001.pb.gz

And I used the following script to check how many cycle definition:

package main

import (
    "fmt"
    "reflect"

    "github.com/stripe/stripe-go/v79"
)

func findTypeCycle(t reflect.Type, visited map[reflect.Type]bool, cycleCount *int) {
    if visited[t] {
        *cycleCount++
        return
    }

    visited[t] = true
    defer delete(visited, t)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fieldType := field.Type

        if fieldType.Kind() == reflect.Ptr {
            fieldType = fieldType.Elem()
        }

        if fieldType.Kind() == reflect.Struct {
            findTypeCycle(fieldType, visited, cycleCount)
        }
    }
}

func main() {
    cycleCount := 0
    findTypeCycle(reflect.TypeOf(stripe.CheckoutSession{}), make(map[reflect.Type]bool), &cycleCount)
    fmt.Printf("Total number of cycles detected: %d\n", cycleCount)
}

And the output is:

Total number of cycles detected: 1083720