gopdfにコントリビュートした話;メモリ使用量を最大614倍、処理時間を最大517倍向上させるFontContainer

#OSS#Go#コントリビュート
2025/07/21
ArticleImage:01K0QPQ9R9B96MV6RRSBV7R51K

GoでPDF作成のライブラリといえば gopdf(signintech/gopdf) が広く使われているかと思います
そんなgopdfですがフォント設定のオーバーヘッドがイシューで報告されていました

私が携わっているプロジェクトでもそれが引き金となってしまい、バッチ処理がOOM Killされてしまうという障害が発生しました

オーバーヘッドの原因

gopdfにはgopdf.SubsetFontObjというフォントを表す構造体があります

Copy
// SubsetFontObj pdf subsetFont object
type SubsetFontObj struct {
	ttfp                  core.TTFParser
	Family                string
	CharacterToGlyphIndex *MapOfCharacterToGlyphIndex
	CountOfFont           int
	indexObjCIDFont       int
	indexObjUnicodeMap    int
	ttfFontOption         TtfOption
	funcKernOverride      FuncKernOverride
	funcGetRoot           func() *GoPdf
	addCharsBuff          []rune
}

SubsetFontObj はユーザーランドで生成・管理されることを想定されておらず、gopdf.GoPdf#AddTTFFont.* メソッドを通して内部で生成されます
また、フォントデータの実体は SubsetFontObj.ttfp もといgopdf/fontmaker/core.TTFParserという構造体で表されます

Copy
// TTFParser true type font parser
type TTFParser struct {
	tables map[string]TableDirectoryEntry
	//head
	unitsPerEm       uint
	xMin             int
	yMin             int
	xMax             int
	yMax             int
	indexToLocFormat int
	//Hhea
	numberOfHMetrics uint
	ascender         int
	descender        int
	//end Hhea

	numGlyphs      uint
	widths         []uint
	chars          map[int]uint
	postScriptName string

	//os2
	os2Version    uint
	Embeddable    bool
	Bold          bool
	typoAscender  int
	typoDescender int
	capHeight     int
	sxHeight      int

	//post
	italicAngle        int
	underlinePosition  int
	underlineThickness int
	isFixedPitch       bool
	sTypoLineGap       int
	usWinAscent        uint
	usWinDescent       uint

	//cmap
	IsShortIndex  bool
	LocaTable     []uint
	SegCount      uint
	StartCount    []uint
	EndCount      []uint
	IdRangeOffset []uint
	IdDelta       []uint
	GlyphIdArray  []uint
	symbol        bool

	//cmap format 12
	groupingTables []CmapFormat12GroupingTable

	//data of font
	cachedFontData []byte

	//kerning
	useKerning bool //user config for use or not use kerning
	kern       *KernTable
}

ttfp は SubsetFontObj#AddTTFFont.* メソッドを通じてフォントをパースして生成されます
そのため、パースされたフォントデータはPDFごとに管理され、PDFを生成するたびにフォントデータをパースし直す必要がありました

FontContainer

gopdf.FontContainerは SubsetFontObj の再利用性を高めるための構造体です
SubsetFontObj のライフサイクルを gopdf.GoPdf から分離することによって、O(n)であったパース処理をO(1)に改善することができました

Copy
func main() {
+	fontContainer := &gopdf.FontContainer{}
+	err := fontContainer.AddTTFFont("LiberationSerif-Regular", "path/to/LiberationSerif-Regular.ttf")
+	if err != nil {
+		// handle error
+	}
	pdf := &gopdf.GoPdf{}
	pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
-	err = pdf.AddTTFFont("LiberationSerif-Regular", "path/to/LiberationSerif-Regular.ttf")
+	err = pdf.AddTTFFontFromFontContainer("LiberationSerif-Regular", fontContainer)
	if err != nil {
		// handle error
	}
}

FontContainer へのフォント追加は sync.Map によってgoroutine safeに実装されていますが、同名フォントの追加(上書き)を許容しています
そのため、フォント追加はinit関数、もしくは sync.Once で行うのが個人的にはおすすめです

Copy
package main

import (
	"sync"

	"github.com/signintech/gopdf"
)

type UseCase struct {
	fontContainer *gopdf.FontContainer
	once          sync.Once
}

func (u *UseCase) Exec() error {
	var err error
	u.once.Do(func() {
		err = u.fontContainer.AddTTFFont("LiberationSerif-Regular", "path/to/LiberationSerif-Regular.ttf")
	})
	if err != nil {
		return err
	}

	pdf := &gopdf.GoPdf{}
	pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
	err := pdf.AddTTFFontFromFontContainer("LiberationSerif-Regular", u.fontContainer)
	if err != nil {
		return err
	}
	// PDFの生成処理
	return nil
}

SubsetFontObj には一部PDFごとに異なる値を必要とするフィールドが存在するため、FontContainer ではこれらのフィールドをゼロ値のままで保持し、GoPdf#AddTTFFontFromFontContainer によって値を解決しています

ベンチマーク

Codespaces 4-core 上でベンチマークテストを実行すると以下のような結果が得られました
利用したフォントは LiberationSerif-Regular のTTF(388,352 バイト)です

Copy
go test -bench '^BenchmarkAddTTFFont.*$' -count 6 -benchmem | tee benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/signintech/gopdf
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkAddTTFFontFromFontContainer-4            490024              2172 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontFromFontContainer-4            531178              2356 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontFromFontContainer-4            531594              2117 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontFromFontContainer-4            511210              2243 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontFromFontContainer-4            564414              2253 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontFromFontContainer-4            502152              2160 ns/op            3740 B/op         45 allocs/op
BenchmarkAddTTFFontByReader-4                       1020           1124092 ns/op         2354553 B/op       6039 allocs/op
BenchmarkAddTTFFontByReader-4                       1033           1140968 ns/op         2353839 B/op       6039 allocs/op
BenchmarkAddTTFFontByReader-4                       1069           1276476 ns/op         2354374 B/op       6039 allocs/op
BenchmarkAddTTFFontByReader-4                       1044           1179830 ns/op         2355059 B/op       6039 allocs/op
BenchmarkAddTTFFontByReader-4                       1090           1020190 ns/op         2353677 B/op       6039 allocs/op
BenchmarkAddTTFFontByReader-4                        981           1142982 ns/op         2354842 B/op       6039 allocs/op
BenchmarkAddTTFFontData-4                           3322            371453 ns/op          359668 B/op       6011 allocs/op
BenchmarkAddTTFFontData-4                           3295            366550 ns/op          359782 B/op       6011 allocs/op
BenchmarkAddTTFFontData-4                           3276            359499 ns/op          359392 B/op       6011 allocs/op
BenchmarkAddTTFFontData-4                           3174            358636 ns/op          359632 B/op       6011 allocs/op
BenchmarkAddTTFFontData-4                           3430            364703 ns/op          359680 B/op       6011 allocs/op
BenchmarkAddTTFFontData-4                           3440            357494 ns/op          359584 B/op       6011 allocs/op
PASS
ok      github.com/signintech/gopdf     24.522s
Copy
benchstat benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/signintech/gopdf
cpu: AMD EPYC 7763 64-Core Processor                
                              │ benchstat.txt │
                              │    sec/op     │
AddTTFFontFromFontContainer-4    2.208µ ±  7%
AddTTFFontByReader-4             1.142m ± 12%
AddTTFFontData-4                 362.1µ ±  3%
geomean                          97.01µ

                              │ benchstat.txt │
                              │     B/op      │
AddTTFFontFromFontContainer-4    3.652Ki ± 0%
AddTTFFontByReader-4             2.245Mi ± 0%
AddTTFFontData-4                 351.2Ki ± 0%
geomean                          143.4Kibenchstat.txt │
                              │   allocs/op   │
AddTTFFontFromFontContainer-4      45.00 ± 0%
AddTTFFontByReader-4              6.039k ± 0%
AddTTFFontData-4                  6.011k ± 0%
geomean                           1.178k

AddTTFFontFromFontContainer についても45回ものアロケーションが発生しているため、完全に最適化できているわけではありませんが、既存メソッドと比較すると十分な改善と言えるかと思います
以下はbenchstatをもとにした AddTTFFontByReader, AddTTFFontData との比較結果です

メソッド 処理時間 メモリ使用量  アロケーション
AddTTFFontByReader 517.2x 614.7x 134.2x
AddTTFFontData  163.9x 96.1x 133.5x

おわりに

gopdf.FontContainer の導入によって、フォントのパース処理をO(1)に改善し、メモリ使用量を最大614倍、処理時間を最大517倍向上させることが可能になりました
この機能はv0.33.0から利用可能です

Copy
go get -u github.com/signintech/gopdf@v0.33.0

私個人の話をすると実務で活用しているOSSへのコントリビュートは初めてだったため、ひとつコンフォートゾーンを抜け出すことができたように感じます
この機能がいつか誰かの助けになればエンジニア冥利に尽きます

関連リンク


Recommend Articles

ArticleImage:01JHXFCPQKFK3ARMEAKRFXQF3K

書評『Web配信の技術』gofiber/fiber@v3.0.0-beta.4リリースに寄せて

gofiber/fiber v3.0.0-beta.4リリース gofiber/fiber v3.0.0-beta.4がリリースされ(て)ました Release v3.0.0-beta.4 · gofiber/fiber🚀 New Features Add Startup P…

2025/01/18

ArticleImage:01JFT54WPPZ0ET3XTKZ0JTCMRE

Gatsby.jsとgqlgenでブログを作った

はじめに 最初は単にGraphQL、gRPC、ECS、New Relic etc 自分が興味のある技術トピックでHello World Enterprise Editionをやるだけのつもりだったものの GraphQL -> ヘッドレスCMS -> ブログ という連想ゲームの結…

2024/12/23

ArticleImage:01JJHA5TFHVR4MP9ZWJSDD713A

S3 + CloudFrontで実現する独自ドメインGoパッケージ

はじめに go.uber.org/mock google.golang.org/grpc gorm.io/gorm 独自ドメインで配布されているGoパッケージはカッコいい 、 のような のPrefixがない分、importディレクティブがスッキリして見える 今回は当ブログのバ…

2025/01/26

Copyright © miyamo2 All rights reserved.