- 投稿日:2019-03-01T19:38:14+09:00
Golang(Echo) x docker-composeでホットリロード用いた開発
About
Golang(Echo)でDockerfileを開発と本番で同じものを使いつつ、docker-composeを使う。
なおかつホットリロードもする。前提
- Golang
- 1.12
- docker
- 18.09.2
- docker-compose
- 1.23.2
- realize
- 2.0.2
Docker環境を用意する
とりあえず最低限のEchoサーバーを動かすこと前提
main.go
main.goを記述main.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }modulesの初期設定
go.modを手に入れる$ docker run -v `pwd`:/go/app -w /go/app golang:1.12-alpine go mod init app go: creating new go.mod: module app $ ls go.mod main.goここで
go.sumが欲しい人はよしなに。Dockerfile
DockerのMulti-Stage Buildを使ってレイヤーを2つ用意します。
1つ目のレイヤーはdocker-composeで使用するため、
2つ目のレイヤーは本番で使用するために使います。DockerfileFROM golang:1.12-alpine as build WORKDIR /go/app COPY . . RUN apk add --no-cache git \ && go build -o app FROM alpine WORKDIR /app COPY --from=build /go/app/app . RUN addgroup go \ && adduser -D -G go go \ && chown -R go:go /app/app CMD ["./app"]動作確認をしておきましょう。
$ docker build -t myapp . $ docker run -p 1323:1323 -d --name myapp myapp $ curl localhost:1323 Hello, World!動いたのを確認できたらコンテナを落とします。
$ docker stop down myapp $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $docker-compose
docker-composeを動かすために
docker-compose.ymlを記述します。この時に指定する
target: buildがポイントです。
Multi-Stage Buildのレイヤーを使用することができます。docker-compose.ymlversion: '3.5' services: app: build: context: . target: build volumes: - ./:/go/app command: go run main.go ports: - 1323:1323さて、動作確認です。
$ docker-compose up Recreating echo_app_1 ... done Attaching to echo_app_1 app_1 | go: finding github.com/labstack/gommon/color latest app_1 | go: finding github.com/labstack/gommon/log latest app_1 | go: finding golang.org/x/crypto/acme/autocert latest app_1 | go: finding golang.org/x/crypto/acme latest app_1 | go: finding golang.org/x/crypto latest app_1 | go: finding github.com/valyala/fasttemplate latest app_1 | app_1 | ____ __ app_1 | / __/___/ / ___ app_1 | / _// __/ _ \/ _ \ app_1 | /___/\__/_//_/\___/ v3.3.10-dev app_1 | High performance, minimalist Go web framework app_1 | https://echo.labstack.com app_1 | ____________________________________O/_______ app_1 | O\ app_1 | ? http server started on [::]:1323別のターミナルを起動してcurlを打ってみましょう
$ curl localhost:1323 Hello, World!動いてますね。
ホットリロードを導入する
oxequa/realize を使用して実現します
Dockerfileの編集
realizeのインストール
DockerfileFROM golang:1.12-alpine as build WORKDIR /go/app COPY . . RUN apk add --no-cache git \ - && go build -o app + && go build -o app \ + && go get github.com/oxequa/realize FROM alpine WORKDIR /app COPY --from=build /go/app/app . RUN addgroup go \ && adduser -D -G go go \ && chown -R go:go /app/app CMD ["./app"]docker-composeの編集
realizeを使用して起動するようにする
docker-compose.ymlversion: '3.5' services: app: build: context: . target: build volumes: - ./:/go/app - command: go run main.go + command: realize start --run --no-config ports: - 1323:1323動作確認
$ docker-compose up Recreating echo_app_1 ... done Attaching to echo_app_1 app_1 | len [0/0]0x0 app_1 | [10:25:29][APP] : Watching 1 file/s 1 folder/s app_1 | [10:25:29][APP] : Install started app_1 | [10:25:30][APP] : Install completed in 0.805 s app_1 | [10:25:30][APP] : Running.. app_1 | [10:25:30][APP] : ____ __ app_1 | [10:25:30][APP] : / __/___/ / ___ app_1 | [10:25:30][APP] : / _// __/ _ \/ _ \ app_1 | [10:25:30][APP] : /___/\__/_//_/\___/ v3.3.10-dev app_1 | [10:25:30][APP] : High performance, minimalist Go web framework app_1 | [10:25:30][APP] : https://echo.labstack.com app_1 | [10:25:30][APP] : ____________________________________O/_______ app_1 | [10:25:30][APP] : O\ app_1 | [10:25:30][APP] : ? http server started on [::]:1323動いてますね。
curlをして動作確認してみましょう。$ curl localhost:1323 Hello, World!最後に
main.goを編集してホットリロードされるかの確認ですmain.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") + return c.String(http.StatusOK, "Good Bye.") }) e.Logger.Fatal(e.Start(":1323")) }$ curl localhost:1323 Good Bye.
- 投稿日:2019-03-01T19:38:14+09:00
Golang(Echo)でdocker-composeを用いた開発
About
Golang(Echo)でDockerfileを開発と本番で同じものを使いつつ、docker-composeを使う。
なおかつホットリロードもする。前提
- Golang
- 1.12
- docker
- 18.09.2
- docker-compose
- 1.23.2
- realize
- 2.0.2
Docker環境を用意する
とりあえず最低限のEchoサーバーを動かすこと前提
main.go
main.goを記述main.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }modulesの初期設定
go.modを手に入れる$ docker run -v `pwd`:/go/app -w /go/app golang:1.12-alpine go mod init app go: creating new go.mod: module app $ ls go.mod main.goここで
go.sumが欲しい人はよしなに。Dockerfile
DockerのMulti-Stage Buildを使ってレイヤーを2つ用意します。
1つ目のレイヤーはdocker-composeで使用するため、
2つ目のレイヤーは本番で使用するために使います。DockerfileFROM golang:1.12-alpine as build WORKDIR /go/app COPY . . RUN apk add --no-cache git \ && go build -o app FROM alpine WORKDIR /app COPY --from=build /go/app/app . RUN addgroup go \ && adduser -D -G go go \ && chown -R go:go /app/app CMD ["./app"]動作確認をしておきましょう。
$ docker build -t myapp . $ docker run -p 1323:1323 -d --name myapp myapp $ curl localhost:1323 Hello, World!動いたのを確認できたらコンテナを落とします。
$ docker stop down myapp $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $docker-compose
docker-composeを動かすために
docker-compose.ymlを記述します。この時に指定する
target: buildがポイントです。
Multi-Stage Buildのレイヤーを使用することができます。docker-compose.ymlversion: '3.5' services: app: build: context: . target: build volumes: - ./:/go/app command: go run main.go ports: - 1323:1323さて、動作確認です。
$ docker-compose up Recreating echo_app_1 ... done Attaching to echo_app_1 app_1 | go: finding github.com/labstack/gommon/color latest app_1 | go: finding github.com/labstack/gommon/log latest app_1 | go: finding golang.org/x/crypto/acme/autocert latest app_1 | go: finding golang.org/x/crypto/acme latest app_1 | go: finding golang.org/x/crypto latest app_1 | go: finding github.com/valyala/fasttemplate latest app_1 | app_1 | ____ __ app_1 | / __/___/ / ___ app_1 | / _// __/ _ \/ _ \ app_1 | /___/\__/_//_/\___/ v3.3.10-dev app_1 | High performance, minimalist Go web framework app_1 | https://echo.labstack.com app_1 | ____________________________________O/_______ app_1 | O\ app_1 | ? http server started on [::]:1323別のターミナルを起動してcurlを打ってみましょう
$ curl localhost:1323 Hello, World!動いてますね。
ホットリロードを導入する
oxequa/realize を使用して実現します
Dockerfileの編集
realizeのインストール
DockerfileFROM golang:1.12-alpine as build WORKDIR /go/app COPY . . RUN apk add --no-cache git \ - && go build -o app + && go build -o app \ + && go get github.com/oxequa/realize FROM alpine WORKDIR /app COPY --from=build /go/app/app . RUN addgroup go \ && adduser -D -G go go \ && chown -R go:go /app/app CMD ["./app"]docker-composeの編集
realizeを使用して起動するようにする
docker-compose.ymlversion: '3.5' services: app: build: context: . target: build volumes: - ./:/go/app - command: go run main.go + command: realize start --run --no-config ports: - 1323:1323動作確認
$ docker-compose up Recreating echo_app_1 ... done Attaching to echo_app_1 app_1 | len [0/0]0x0 app_1 | [10:25:29][APP] : Watching 1 file/s 1 folder/s app_1 | [10:25:29][APP] : Install started app_1 | [10:25:30][APP] : Install completed in 0.805 s app_1 | [10:25:30][APP] : Running.. app_1 | [10:25:30][APP] : ____ __ app_1 | [10:25:30][APP] : / __/___/ / ___ app_1 | [10:25:30][APP] : / _// __/ _ \/ _ \ app_1 | [10:25:30][APP] : /___/\__/_//_/\___/ v3.3.10-dev app_1 | [10:25:30][APP] : High performance, minimalist Go web framework app_1 | [10:25:30][APP] : https://echo.labstack.com app_1 | [10:25:30][APP] : ____________________________________O/_______ app_1 | [10:25:30][APP] : O\ app_1 | [10:25:30][APP] : ? http server started on [::]:1323動いてますね。
curlをして動作確認してみましょう。$ curl localhost:1323 Hello, World!最後に
main.goを編集してホットリロードされるかの確認ですmain.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") + return c.String(http.StatusOK, "Good Bye.") }) e.Logger.Fatal(e.Start(":1323")) }$ curl localhost:1323 Good Bye.
- 投稿日:2019-03-01T15:14:11+09:00
Go 1.13のerrorsへの移行手順
Go 1.13のerrorsへの移行手順
https://github.com/pkg/errors を利用している前提で紹介します。
2019/3/1現在Go 1.13はリリースされていないため、Go 1.12の間に新しいerrorパッケージを使いたい場合はxerrorsへの移行手順 をご確認ください。
基本
errors.Wrap(err, "") を errors.Errorf(": %w", err)に変更
errors.Wrap(err, "message")errors.Errorf("message: %w", err)errors.Cause を errors.Unwrap に変更
err = errors.Cause(err)err = errors.Unwrap(err)エラーの値の比較をerrors.Isに変更
err = errors.Cause(err) if err == ErrNotFound { }// Unwrapは不要 if errors.Is(err, ErrNotFound) { }type assertion を Asに変更
if myErr, ok := err.(*MyError); ok { }var myErr *MyError if ok := errors.As(err, &retErr); ok { }応用
fmt.Formatterをerrors.ErrorFormatterに変更
func (e *Error) Format(s fmt.State, v rune) { }func (e *Error) FormatError(p errors.Printer) (next error) { p.Print(e.Error()) e.frame.Format(p) return e.err }その他
エラー出力でスタックトレースを表示
err := func1() fmt.Printf("%+v", err)関連情報
- 投稿日:2019-03-01T15:03:45+09:00
xerrorsからGo 1.13のerrorsへの移行手順
xerrorsからGo 1.13のerrorsへの移行手順
https://godoc.org/golang.org/x/xerrors を利用している前提で紹介します。
xerrors.Errorf を fmt.Errorf に変更
xerrors.Errorf("message: %w", err)fmt.Errorf("message: %w", err)不要なfmt.Formatterを削除
独自のエラー型に独自のエラーフォーマットを定義している場合のみ
// Fomatメソッドを削除する func (e *Error) Format(s fmt.State, v rune) { xerrors.FormatError(e, s, v) } // こちらはxerrors.Printerをerrors.Printerにする func (e *Error) FormatError(p xerrors.Printer) (next error) { p.Print(e.Error()) e.frame.Format(p) return e.err }残りのすべてのxerrorsをerrorsに変更する
New,Is,As,Opaque,Unwrapなど
https://godoc.org/golang.org/x/xerrors最終確認
xerrorsが見つからないことを確認します。
$ grep -r "xerrors" ./関連情報
- 投稿日:2019-03-01T14:53:20+09:00
xerrorsへの移行手順
xerrorsへの移行手順
https://github.com/pkg/errors を利用している前提で紹介します。
Go 1.13以降はxerrorsを使う必要はありません。
Go 1.13のerrorsへの移行手順 をご確認ください。基本
errors.New を xerrors.Newに変更
errors.New("message")xerrors.New("message")errors.Wrap(err, "") を xerrors.Errorf(": %w", err)に変更
errors.Wrap(err, "message")xerrors.Errorf("message: %w", err)fmt.Errorf を xerrors.Errorfに変更
fmt.Errorf("message: %v", msg)xerrors.Errorf("message: %v", msg)errors.Cause を xerrors.Unwrap に変更
err = errors.Cause(err)err = xerrors.Unwrap(err)エラーの値の比較をxerrors.Isに変更
err = errors.Cause(err) if err == ErrNotFound { }// Unwrapは不要 if xerrors.Is(err, ErrNotFound) { }type assertion を Asに変更
if myErr, ok := err.(*MyError); ok { }var myErr *MyError if ok := xerrors.As(err, &retErr); ok { }応用
fmt.Formatterをxerrors.ErrorFormatterに変更
func (e *Error) Format(s fmt.State, v rune) { }func (e *Error) Format(s fmt.State, v rune) { xerrors.FormatError(e, s, v) } func (e *Error) FormatError(p xerrors.Printer) (next error) { p.Print(e.Error()) e.frame.Format(p) return e.err }最終確認
errorsが見つからないことを確認します。
$ grep -r "errors" ./その他
エラー出力でスタックトレースを表示
err := func1() fmt.Printf("%+v", err)さいごに
Go 1.13がリリースされたあとは xerrorsからGo 1.13のerrorsへの移行手順 を参考に再度移行をしてください。
関連情報
- 投稿日:2019-03-01T07:48:44+09:00
常用漢字の中からランダムで元号を生成するコマンドを作りました
概要
元号は常用漢字から選ばれていると聞きましたので、常用漢字の中からランダムで元号を生成するコマンドを作りました。
https://github.com/suzuki86/gengo-generator
こんなことに役立つかもしれません
- 実はまだ元号が決まっていないので急ぎで元号のブレストがしたい。
- 元号が漏洩したので別の元号を急ぎで欲しい。
...など
インストール方法
go get github.com/suzuki86/gengo-generatorgo install github.com/suzuki86/gengo-generator以上で
gengo-generatorコマンドが使えるようになります。gengo-generatorコマンドを実行するとランダムに元号が出力されます。例
# gengo-generator 則栄 # gengo-generator 商湯 # gengo-generator 外附
- 投稿日:2019-03-01T07:08:39+09:00
大量のPNG画像を標準パッケージのみで2.4倍高速に処理する
はじめに
大量のPNG画像を標準パッケージで処理する機会があったのですが、あまりにも遅すぎたので、
どこがボトルネックになっているか調べ、どうすれば標準パッケージの範囲で改善できるか実験してみました!使用したソースコードや画像、実験結果はココにおいています。
環境
- OS: Ubuntu 16.04 LTS
- Go: go1.11.2 linux/amd64
- Memory: 2GB
- CPU: Intel Core i5-3340M CPU @ 2.70GHz(使用可能なコア数を1に制限)
使用する画像
https://github.com/ashleymcnamara/gophers に実験にちょうど良いPNG画像があったのでお借りしました。ありがとうございます
“Gopher Artwork” by Ashley McNamara is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.使用する画像の枚数は96枚、未処理の画像全体のサイズは約68MBとなっています。
改善前のソースコード
ソースコードを全て載せると冗長になるので、重要な箇所のみ載せておきます。ソースコード全体はこちらをご覧ください。処理としてはオーソドックスなネガポジ反転処理ですね。
画像のカラーモデルは、あらかじめ
color.NRGBAModelであることが分かっているとします。for _, path := range GetPathAll() { ... img, err := png.Decode(srcFile) ... imgNRGBA := img.(*image.NRGBA) bounds := imgNRGBA.Bounds() for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { c := imgNRGBA.At(x, y).(color.NRGBA) imgNRGBA.Set(x, y, color.RGBA{ uint8(255 - c.R), uint8(255 - c.G), uint8(255 - c.B), c.A, }) } } ... png.Encode(dstFile, img) ... }推測するな計測せよ
なにはともあれ、どこがボトルネックになっているかを把握しなければ改善のしようがありません。Goにはどの関数にどれだけの処理時間を費やしているか計測するプロファイリングツールとして pprof があるので、これを使っていきましょう!
以下が計測結果の一部です。
Duration: 2.85mins, Total samples = 157.11s (91.76%) Showing nodes accounting for 152.17s, 96.86% of 157.11s total Dropped 191 nodes (cum <= 0.79s) flat flat% sum% cum cum% 23.09s 14.70% 14.70% 35.19s 22.40% image/png.filter 16s 10.18% 24.88% 28s 17.82% runtime.mallocgc 12.89s 8.20% 33.09% 31.27s 19.90% compress/flate.(*compressor).deflate 9.85s 6.27% 39.35% 9.85s 6.27% compress/flate.matchLen 9.33s 5.94% 45.29% 9.33s 5.94% runtime.memmove 8.29s 5.28% 50.57% 43.39s 27.62% runtime.convT2Inoptr 5.59s 3.56% 54.13% 5.59s 3.56% image/png.abs (inline) 5.23s 3.33% 57.46% 34.02s 21.65% image.(*NRGBA).Set 5.04s 3.21% 60.66% 14.89s 9.48% compress/flate.(*compressor).findMatch 4.99s 3.18% 63.84% 76.50s 48.69% main.ProcessImage 4.53s 2.88% 66.72% 8.50s 5.41% image/png.paeth 4.38s 2.79% 69.51% 5.71s 3.63% image.(*NRGBA).NRGBAAt 3.85s 2.45% 71.96% 24.39s 15.52% image/color.nrgbaModel 3.60s 2.29% 74.25% 3.60s 2.29% image/png.abs8 (inline) 3.10s 1.97% 76.23% 3.10s 1.97% hash/adler32.update 3.07s 1.95% 78.18% 23.67s 15.07% image.(*NRGBA).At 2.97s 1.89% 80.07% 2.97s 1.89% image/color.RGBA.RGBA 2.89s 1.84% 81.91% 5.86s 3.73% image/color.(*RGBA).RGBA 2.77s 1.76% 83.67% 4.39s 2.79% image/png.filterPaeth 2.76s 1.76% 85.43% 27.15s 17.28% image/color.(*modelFunc).Convert 2.75s 1.75% 87.18% 2.75s 1.75% runtime.nextFreeFast (inline) 2.38s 1.51% 88.70% 2.38s 1.51% runtime.acquirem (inline) 2.37s 1.51% 90.20% 2.37s 1.51% runtime.releasem (inline) 1.70s 1.08% 91.29% 1.70s 1.08% compress/flate.hash4 (inline) 1.57s 1% 92.29% 1.57s 1% image.(*NRGBA).PixOffset (inline) 1.50s 0.95% 93.24% 10.98s 6.99% image/png.(*decoder).readImagePass 1.44s 0.92% 94.16% 1.44s 0.92% runtime.gomcache (inline) 1.40s 0.89% 95.05% 1.40s 0.89% image.Point.In (inline)
flatの列がサンプリングした時間における各関数の純粋な実行時間です。第一の改善: 配列へ直接アクセスする
最も時間がかかっているのは
image/png.filterで、これはPNG画像をエンコードする際に、画像の行毎にどのようなフィルタ1を用いればよいか選択する関数です。注意していただきたいのは、圧縮する処理ではなく、フィルタの選択に最も時間を費やしている点です2。本来であればこれを改善すれば、大幅な速度向上が期待できるのですが、残念ながら標準パッケージをいじらなければならないため、これを改善するのはあきらめましょう3。次に目につくのは、
runtime.mallocgcやruntime.memmoveなどのメモリ管理を行う関数や、image/colorパッケージの関数です。これらの関数は各画素の読み書きを間接的に行うために多く呼び出されています。実はimage.Imageはインターフェースであり、それを型アサーションした*image.NRGBAは各画素の画素値をRGBAの順で[]uint8型の変数Pixに保持しています。このPixはお分かりの通り構造体の外部からアクセス可能ですので、各画素への読み書きを直接行うことができます。よって、以下のように配列から直接読み書きできます。for _, path := range GetPathAll() { ... img, err := png.Decode(srcFile) ... pix := img.(*image.NRGBA).Pix for i := 0; i < len(pix); i += 4 { pix[i] = 255 - pix[i] pix[i+1] = 255 - pix[i+1] pix[i+2] = 255 - pix[i+2] } ... png.Encode(dstFile, img) ... }さて、処理速度はどうなったでしょうか? 計測結果を見ていきましょう!
Duration: 1.45mins, Total samples = 79.99s (91.86%) Showing nodes accounting for 78.52s, 98.16% of 79.99s total Dropped 120 nodes (cum <= 0.40s) flat flat% sum% cum cum% 22.43s 28.04% 28.04% 34.80s 43.51% image/png.filter 12.10s 15.13% 43.17% 30.80s 38.50% compress/flate.(*compressor).deflate 10.79s 13.49% 56.66% 10.79s 13.49% compress/flate.matchLen 5.98s 7.48% 64.13% 5.98s 7.48% image/png.abs (inline) 4.73s 5.91% 70.05% 15.52s 19.40% compress/flate.(*compressor).findMatch 4.33s 5.41% 75.46% 8.63s 10.79% image/png.paeth 3.74s 4.68% 80.14% 3.74s 4.68% image/png.abs8 (inline) 2.94s 3.68% 83.81% 2.94s 3.68% hash/adler32.update 2.20s 2.75% 86.56% 3.88s 4.85% image/png.filterPaeth 1.53s 1.91% 88.47% 1.53s 1.91% main.ProcessSlice 1.46s 1.83% 90.30% 1.46s 1.83% compress/flate.hash4 (inline) 1.43s 1.79% 92.09% 10.60s 13.25% image/png.(*decoder).readImagePass 1.42s 1.78% 93.86% 1.42s 1.78% runtime.memmove各関数の処理時間にはそれほど変化はないように見えますが、メモリ管理を行う関数の処理時間が大幅に減り、
image/colorパッケージの関数は皆無となっています。また全体の処理時間を見てみると、2.85分(2分51秒)から1.45分(1分27秒)へと約2倍高速に処理できていることが分かります。ちなみに、処理後の画像全体のサイズは約67MBになっていました。
第二の改善: 適切なエンコーダを使う
今までPNG画像をエンコードするときは、関数
image/png.Encodeを用いてきました。これはこれで手軽で便利なのですが、圧縮レベルを調節できず、Go言語にいいようにエンコードさせられていました。
そこでimage/pngパッケージをよく見てみると、圧縮レベルを調節するために構造体image/png.Encoderが用意されていることに気づきます4。今回はその中でも圧縮をより速く行えるように、圧縮レベルをpng.BestSpeedに設定してエンコードしていきます。encoder := png.Encoder{CompressionLevel: png.BestSpeed} for _, path := range GetPathAll() { ... img, err := png.Decode(srcFile) ... pix := img.(*image.NRGBA).Pix for i := 0; i < len(pix); i += 4 { pix[i] = 255 - pix[i] pix[i+1] = 255 - pix[i+1] pix[i+2] = 255 - pix[i+2] } ... encoder.Encode(dstFile, img) ... }今までと同じように計測結果を見ていきましょう!
Duration: 1.21mins, Total samples = 66.07s (90.69%) Showing nodes accounting for 63.40s, 95.96% of 66.07s total Dropped 136 nodes (cum <= 0.33s) flat flat% sum% cum cum% 25.85s 39.13% 39.13% 39.38s 59.60% image/png.filter 6.39s 9.67% 48.80% 6.39s 9.67% image/png.abs (inline) 5.06s 7.66% 56.46% 9.73s 14.73% image/png.paeth 3.80s 5.75% 62.21% 3.80s 5.75% image/png.abs8 (inline) 3.55s 5.37% 67.58% 3.55s 5.37% hash/adler32.update 2.75s 4.16% 71.74% 4.47s 6.77% image/png.filterPaeth 2.53s 3.83% 75.57% 2.53s 3.83% compress/flate.(*deflateFast).matchLen 1.95s 2.95% 78.52% 1.95s 2.95% runtime.memmove 1.84s 2.78% 81.31% 1.84s 2.78% main.ProcessSlice 1.64s 2.48% 83.79% 12.30s 18.62% image/png.(*decoder).readImagePass 1.39s 2.10% 85.89% 1.72s 2.60% compress/flate.(*huffmanEncoder).bitCounts 0.99s 1.50% 87.39% 1.34s 2.03% compress/flate.(*decompressor).huffSym 0.73s 1.10% 88.50% 3.43s 5.19% compress/flate.(*decompressor).huffmanBlock 0.66s 1% 89.50% 3.59s 5.43% compress/flate.(*deflateFast).encode第一の改善の計測結果とほぼ同じ結果となりましたが、圧縮レベルを
png.BestSpeedにしたため、圧縮を行うcompress/flateパッケージで用いる関数が微妙に異なっています。具体的には、関数compress/flate.matchLenが 関数compress/flate.(*deflateFast).matchLenに、関数compress/flate.(*compressor).deflateが 関数compress/flate.(*deflateFast).encodeへと、それぞれより高速に圧縮可能な関数を用いるように変更されています。気になるのは、圧縮処理を速くしたことにより逆に圧縮率が落ちているのではないのか?というところですが、予想通り処理後の画像全体のサイズは約74MBとなりました。これは処理前に比べて約8MBの増加、画像1枚あたりでは約63KB増加したことになります。この増加量を多いと見るか少ないと見るかは、実際にPNG画像を処理するプログラムを運用する環境のリソースなど、コンテキストによるので一概には言えないのですが、今回は目を瞑ることとします。
すると計測結果より、処理全体の実行時間は第一の改善と比較して約1.2倍高速になりました!
まとめ
- 各画素の読み書きを配列に対して直接行うことで約2倍高速化できる
- 適切なエンコーダを使うことでさらに約1.2倍高速化できる
- 全体で約2.4倍の高速化に成功した
おわりに
今回は標準パッケージの範囲内でいかに高速に大量のPNG画像を処理できるか実験していきました。
もちろん使用する画像によって処理時間は変わってきます。ですが、上記の方法である程度の高速化は可能だと思います。
このほかに標準パッケージの範囲内でより高速化できる方法をご存知の方は、コメント欄にてご教示お願いします余談
この記事を書くために
image/pngパッケージのソースコードを眺めていると、無駄な処理を発見してしまったのでちゃっかりコントリビュートしておきました
https://go-review.googlesource.com/c/go/+/164199
圧縮を行う
compress/flateパッケージの関数の実行時間をかき集めれば、フィルターの選択時間 < 圧縮時間となるようです。 ↩image/png.filter を読んでいただくと、一見各フィルタの
sumを計算するたびに同じ行を走査しているためメモリアクセス効率が悪そうですが、実験した限りでは、それよりもsumがbestを超すと即座に処理を中断(break)するほうが、処理速度は速くなる傾向にあるようです。 ↩
image/png.Encoderは圧縮レベルを調節できるだけでなく、エンコードの際に使用するバッファを1つの処理内や複数の画像間で使いまわすためのプールも保持しています。image/png.Encodeではこのimage/png.Encoderを毎回生成しているため、複数枚の画像をエンコードする場合はバッファプールを使いまわせず無駄が生まれることになります。しかし、image/png.filterなどほかの処理により時間がかかっているため、全体の処理時間に対して、image/png.Encoderの生成コストはそれほど高くはありません。 ↩
- 投稿日:2019-03-01T01:46:28+09:00
exec.Command()をモックする
Goの
exec.Command()をテスト(モック)したいときにどういう方法があるか調べたときのメモ
exec_test.goの手法この方法は
exec.Command()で実行するコマンド自体を差し替えて、テストコードのTestHelperProcess()を実行した結果にしてしまうというものです。この方法だとコマンドが実際に実行されたかどうかは検証できないため、モックと組み合わせてコマンドを実行したかどうかも検証できるようにしてみました。
exec/exec.gopackage main //go:generate mockgen -package=mock -destination=mock/exec.go -source=exec.go import ( "fmt" "os/exec" ) func main() { r := CommandRunner{exec: CommandWrapper{}} fmt.Print(r.Run()) } type CommandRunner struct { exec Exec } func (r CommandRunner) Run() string { output, _ := r.exec.Command("echo", "foo bar", "baz").CombinedOutput() return string(output) } type Exec interface { Command(name string, arg ...string) *exec.Cmd } type CommandWrapper struct{} func (w CommandWrapper) Command(name string, arg ...string) *exec.Cmd { return exec.Command(name, arg...) }
Execインターフェースを定義してモックを生成します。
そしてテストコードを書きます。exec/exec_test.gopackage main import ( "fmt" "os" "os/exec" "strings" "testing" "github.com/golang/mock/gomock" "exec/mock" ) func TestRun(t *testing.T) { c := gomock.NewController(t) defer c.Finish() mockExec := mock.NewMockExec(c) mockExec. EXPECT(). Command("echo", "foo bar", "baz"). Return(StubCommand("echo", "foo bar", "baz")) r := CommandRunner{exec: mockExec} if "foo bar baz" != r.Run() { t.Error("test run failed") } } func StubCommand(name string, arg ...string) *exec.Cmd { cs := []string{"-test.run=TestHelperProcess", "--", name} cs = append(cs, arg...) cmd := exec.Command(os.Args[0], cs...) cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} return cmd } func TestHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return } defer os.Exit(0) args := os.Args for len(args) > 0 { if args[0] == "--" { args = args[1:] break } args = args[1:] } if len(args) == 0 { fmt.Fprintf(os.Stderr, "No command\n") os.Exit(2) } cmd := strings.Join(args, " ") switch cmd { case "echo foo bar baz": fmt.Fprint(os.Stdout, "foo bar baz") default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) os.Exit(2) } }
StubCommand()とTestHelperProcess()がコマンド差し替えにあたる部分です。
このテストコードではexec.Command()の実行回数(1回)、引数、出力の検証ができています。k8s.io/utils/execを使う
(上の方法で実装してから気づいたのですが・・)
exec.Command()をテストできるライブラリk8s.io/utils/execがあります。
これを使う方法もためしてみました。exec/exec.gopackage main //go:generate mockgen -package=mock -destination=mock/exec.go k8s.io/utils/exec Interface import ( "fmt" "k8s.io/utils/exec" ) func main() { r := CommandRunner{exec: exec.New()} fmt.Print(r.Run()) } type CommandRunner struct { exec exec.Interface } func (r CommandRunner) Run() string { output, _ := r.exec.Command("echo", "foo bar", "baz").CombinedOutput() return string(output) }インターフェース
exec.Interfaceが用意されているのでそれを使います。
exec.Interfaceのモックを生成してテストコードを書きます。exec/exec_test.gopackage main import ( "testing" "github.com/golang/mock/gomock" "k8s.io/utils/exec" fakeexec "k8s.io/utils/exec/testing" "exec/mock" ) func TestRun(t *testing.T) { fakeCmd := &fakeexec.FakeCmd{ CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ func() ([]byte, error) { return []byte("foo bar baz"), nil }, }, } c := gomock.NewController(t) defer c.Finish() mockExec := mock.NewMockInterface(c) mockExec. EXPECT(). Command("echo", "foo bar", "baz"). Return(fakeCmd) r := CommandRunner{exec: mockExec} if "foo bar baz" != r.Run() { t.Error("test run failed") } }テストコード用の
FakeCmdなどが用意されているため短く書くことができます。まとめ
exec.Command()をモックする方法を紹介しました。はじめの
exec_test.goの手法でもテストはできます。ただテスト実行時の動きがややわかりづらかったり、記述が少し長くなってしまうのが難点です。
k8s.io/utils/execのライブラリを使うと短く簡単に書くことができます。