20190301のGoに関する記事は8件です。

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.go
package 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つ目のレイヤーは本番で使用するために使います。

Dockerfile
FROM 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.yml
version: '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のインストール

Dockerfile
FROM 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.yml
version: '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.go
package 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.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.go
package 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つ目のレイヤーは本番で使用するために使います。

Dockerfile
FROM 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.yml
version: '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のインストール

Dockerfile
FROM 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.yml
version: '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.go
package 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.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)

関連情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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" ./

関連情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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への移行手順 を参考に再度移行をしてください。

関連情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

常用漢字の中からランダムで元号を生成するコマンドを作りました

概要

元号は常用漢字から選ばれていると聞きましたので、常用漢字の中からランダムで元号を生成するコマンドを作りました。

https://github.com/suzuki86/gengo-generator

こんなことに役立つかもしれません

  • 実はまだ元号が決まっていないので急ぎで元号のブレストがしたい。
  • 元号が漏洩したので別の元号を急ぎで欲しい。

...など

インストール方法

go get github.com/suzuki86/gengo-generator
go install github.com/suzuki86/gengo-generator

以上で gengo-generator コマンドが使えるようになります。 gengo-generator コマンドを実行するとランダムに元号が出力されます。

# gengo-generator 
則栄
# gengo-generator 
商湯
# gengo-generator 
外附

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大量の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画像があったのでお借りしました。ありがとうございます:pray:
“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.mallocgcruntime.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画像を処理できるか実験していきました。
もちろん使用する画像によって処理時間は変わってきます。ですが、上記の方法である程度の高速化は可能だと思います。
このほかに標準パッケージの範囲内でより高速化できる方法をご存知の方は、コメント欄にてご教示お願いします:bow:

余談

この記事を書くために image/png パッケージのソースコードを眺めていると、無駄な処理を発見してしまったのでちゃっかりコントリビュートしておきました:v:
https://go-review.googlesource.com/c/go/+/164199


  1. https://hoshi-sano.hatenablog.com/entry/2013/08/18/113434 

  2. 圧縮を行う compress/flate パッケージの関数の実行時間をかき集めれば、フィルターの選択時間 < 圧縮時間となるようです。 

  3. image/png.filter を読んでいただくと、一見各フィルタのsumを計算するたびに同じ行を走査しているためメモリアクセス効率が悪そうですが、実験した限りでは、それよりもsumbestを超すと即座に処理を中断(break)するほうが、処理速度は速くなる傾向にあるようです。 

  4. image/png.Encoder は圧縮レベルを調節できるだけでなく、エンコードの際に使用するバッファを1つの処理内や複数の画像間で使いまわすためのプールも保持しています。image/png.Encode ではこの image/png.Encoder を毎回生成しているため、複数枚の画像をエンコードする場合はバッファプールを使いまわせず無駄が生まれることになります。しかし、image/png.filter などほかの処理により時間がかかっているため、全体の処理時間に対して、image/png.Encoder の生成コストはそれほど高くはありません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

exec.Command()をモックする

Goのexec.Command()をテスト(モック)したいときにどういう方法があるか調べたときのメモ

exec_test.goの手法

この方法はexec.Command()で実行するコマンド自体を差し替えて、テストコードのTestHelperProcess()を実行した結果にしてしまうというものです。

この方法だとコマンドが実際に実行されたかどうかは検証できないため、モックと組み合わせてコマンドを実行したかどうかも検証できるようにしてみました。

exec/exec.go
package 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.go
package 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.go
package 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.go
package 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のライブラリを使うと短く簡単に書くことができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む