DEV Community 👩‍💻👨‍💻

Yaroslav Podorvanov
Yaroslav Podorvanov

Posted on

Обережно кодогенерація

Раніше вже писав про збільшення швидкодії та зменшення використання пам'яті після використання кодогенерації і ця історія має продовження, а саме розбір помилок.

Protobuf, перша помилка яка показала себе через пару тижнів після змін в коді

В проекті ми використовуємо офіційну бібліотеку Protobuf github.com/protocolbuffers/protobuf, яка підчас серіалізації використовує рефлексію і будує слайс байтів через append.
А потім я дізнався про "Protocol Buffers for Go with Gadgets" github.com/gogo/protobuf, бібліотеку-fork яка генерує додатковий код щоб прибрати рефексію підчас серіалізації і вже записує в слайс байтів по індексу бо так швидше.
Коли змінював одну бібліотеку на іншу то важливим вважав що стало працювати швидше і написані раніше тести пройшли успішно.
І все б було гаразд але в проекті існувала латка яка через пару тижнів після заміни перезапустила мікросервіс через паніку:

panic: runtime error: index out of range

Латка виглядала приблизно так:

import (
    "github.com/golang/protobuf/proto"
    google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
)

func example() {
    var popup = &google.Popup{
        Id:      uuid(),
        Viewed:  true,
        Clicked: false,
    }

    // some deep nested function
    go func() {
        var content, err = proto.Marshal(popup)

        if err != nil {
            // log error

            return
        }

        // store to database
        store(content)
    }()

    // some delay with other actions

    // @temporary hack
    go func() {
        popup.Clicked = true

        var content, err = proto.Marshal(popup)

        if err != nil {
            // log error

            return
        }

        // store to database again
        store(content)
    }()
}

І зі стандартною бібліотекою латка працювала без паніки для мікросервісу який працює постійно:

import (
    "github.com/golang/protobuf/proto"
    google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
    "testing"
)

const (
    n = 1000000
)

func TestGoogleProtoMarshal(t *testing.T) {
    for i := 0; i < n; i++ {
        var popup = &google.Popup{
            Id:      uint32(i),
            Viewed:  true,
            Clicked: false,
        }

        // some deep nested function
        go func() {
            _, _ = proto.Marshal(popup)
        }()

        // @temporary hack
        go func() {
            popup.Clicked = true

            _, _ = proto.Marshal(popup)
        }()
    }
}

А от з github.com/gogo/protobuf при аналогічному тесті вже видає паніку.
Якщо розглянути згенерований код:

func (m *Popup) Marshal() (dAtA []byte, err error) {
    size := m.Size()
    dAtA = make([]byte, size)
    n, err := m.MarshalToSizedBuffer(dAtA[:size])
    if err != nil {
        return nil, err
    }
    return dAtA[:n], nil
}

func (m *Popup) MarshalToSizedBuffer(dAtA []byte) (int, error) {
    i := len(dAtA)

    //...

    if m.Clicked {
        i--
        dAtA[i] = 1
        i--
        dAtA[i] = 0x18
    }

    //...

    return len(dAtA) - i, nil
}

то стає зрозуміло що розрахунок ємності слайсу відбувався за умов m.Clicked = false, а серіалізація за умов m.Clicked = true і таким чином отримав паніку "index out of range".
Звісно латку ми виправили і стало працювати навіть краще.

JSON, помилка у vendor бібліотеці

Бібліотека easyjson теж для серіалізації працює через додатковий код замість використання рефлексії.
Але після внесення в easyjson одної з оптимізацій, час від часу почали отримувати зламаний JSON, ось приклад тесту який покаже помилку.

package tests

import (
    "github.com/stretchr/testify/require"
    "gitlab.com/go-yp/go-warning-codegeneration/models/jsons/easy"
    "testing"
)

const (
    // language=JSON
    popupWithUnicodeContent = `{
        "title": "Some title with symbol \u201Dt",
        "description": "Any description"
    }`

    // language=JSON
    popupContent = `{
        "title": "Some title",
        "description": "Any description"
    }`
)

func TestEasyjsonUnmarshalJSON(t *testing.T) {
    content := make([]byte, 0, 1024)

    content = append(content[:0], popupWithUnicodeContent...)

    var popup easy.Popup

    unmarshalErr := popup.UnmarshalJSON(content)

    require.NoError(t, unmarshalErr)

    var expected = easy.Popup{
        Title:       "Some title with symbol \u201Dt",
        Description: "Any description",
    }

    require.Equal(t, expected, popup)

    content = append(content[:0], popupContent...)

    /**
    Failed:
    expected: easy.Popup{Title:"Some title with symbol ”t", Description:"Any description"}
    actual  : easy.Popup{Title:"Some title with symbol ”t", Description:" }y description"}
    */
    require.Equal(t, expected, popup)
}

В easyjson цю помилку вже виправили.

Висновки:

Звісно хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.
Приклади доступні в репозиторії.

Top comments (0)

16 Libraries You Should Know as a React Developer

Being a modern React developer is not about knowing just React itself. To stay competitive, it is highly recommended to explore the whole ecosystem. This article contains some of the most useful React component libraries to speed up your developer workflow.