go-text / typesetting

High quality text shaping in pure Go.
Other
88 stars 11 forks source link

Shaper is leaking font data #47

Closed andydotxyz closed 1 year ago

andydotxyz commented 1 year ago

If I load a font, then shape it, then discard the font it will remain in memory. For an app that always uses the same font this is not a problem, but if switching data a lot this will grow in memory very fast!

For example, watch this code use 1GB in under a minute:

package main

import (
    "bytes"
    "io/ioutil"
    "time"

    "github.com/go-text/typesetting/di"
    "github.com/go-text/typesetting/font"
    "github.com/go-text/typesetting/shaping"
)

func main() {
    data, _ := ioutil.ReadFile("../../theme/font/NotoSans-Regular.ttf")

    for range time.Tick(time.Millisecond * 100) {
        f, _ := font.ParseTTF(bytes.NewReader(data))

        in := shaping.Input{
            Text:      []rune{' '},
            RunStart:  0,
            RunEnd:    1,
            Direction: di.DirectionLTR,
            Face:      f,
            Size:      14,
        }
        shaper := &shaping.HarfbuzzShaper{}
        _ = shaper.Shape(in) // this line is leaking the font!
    }
}
andydotxyz commented 1 year ago

Aha, I found a cache hidden away in harfbuzz. This will have to be cleared somehow, some times. Should it be on a timer, or on a public call? The simplest workaround was to add this in my local copy of the textlayout dep (harfbuzz/shape.go):


func ClearPlanCache() {
    planCacheLock.Lock()
    defer planCacheLock.Unlock()

    planCache = map[Face][]*shapePlan{}
}

The change will be different for the new API - but still I wanted to ask for opinions on how to solve this.

andydotxyz commented 1 year ago

Thanks to suggestions in gophers Slack I was able to make it neater by attaching to the shaping buffer instead of needing a new method to clear. https://github.com/go-text/typesetting/pull/48