[インデックス 13812] ファイルの概要
このコミットは、Go言語の標準ライブラリ image/png
パッケージにおけるPNGエンコーディングのパフォーマンス改善を目的としています。特に image.Gray
および image.NRGBA
型の画像をエンコードする際の処理が最適化され、大幅な速度向上が図られています。
コミット
commit 237ee3926906ad08a048e764920e036ecdb08b11
Author: Nigel Tao <nigeltao@golang.org>
Date: Thu Sep 13 15:47:12 2012 +1000
image/png: optimize encoding image.Gray and image.NRGBA images.
benchmark old ns/op new ns/op delta
BenchmarkEncodeGray 23616080 5624558 -76.18%
BenchmarkEncodeNRGBOpaque 34181260 17144380 -49.84%
BenchmarkEncodeNRGBA 41235820 20345990 -50.66%
BenchmarkEncodePaletted 5594652 5620362 +0.46%
BenchmarkEncodeRGBOpaque 17242210 17168820 -0.43%
BenchmarkEncodeRGBA 66515720 67243560 +1.09%
R=r
CC=golang-dev
https://golang.org/cl/6490099
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/237ee3926906ad08a048e764920e036ecdb08b11
元コミット内容
image/png
パッケージにおいて、image.Gray
および image.NRGBA
型の画像エンコーディングを最適化しました。ベンチマーク結果は以下の通りです。
BenchmarkEncodeGray
: 23616080 ns/op から 5624558 ns/op へ、-76.18% の改善。BenchmarkEncodeNRGBOpaque
: 34181260 ns/op から 17144380 ns/op へ、-49.84% の改善。BenchmarkEncodeNRGBA
: 41235820 ns/op から 20345990 ns/op へ、-50.66% の改善。BenchmarkEncodePaletted
: 5594652 ns/op から 5620362 ns/op へ、+0.46% の変化。BenchmarkEncodeRGBOpaque
: 17242210 ns/op から 17168820 ns/op へ、-0.43% の変化。BenchmarkEncodeRGBA
: 66515720 ns/op から 67243560 ns/op へ、+1.09% の変化。
この変更は、GoのコードレビューシステムであるGerritの変更リスト https://golang.org/cl/6490099
に基づいています。
変更の背景
Go言語の image
パッケージは、様々な画像フォーマットを扱うための汎用的なインターフェースを提供しています。しかし、汎用的なインターフェース (image.Image
インターフェースの At(x, y)
メソッドなど) を介したピクセルアクセスは、その抽象化のオーバーヘッドにより、特定の具体的な画像型 (image.Gray
, image.NRGBA
など) の内部データ構造に直接アクセスするよりも遅くなる可能性があります。
PNGエンコーディングは、画像をピクセルデータに変換し、それを圧縮してファイルに書き出すプロセスです。このプロセスにおいて、ピクセルデータを効率的に読み出すことは全体のパフォーマンスに大きく影響します。特に、image.Gray
(グレースケール) や image.NRGBA
(非アルファ事前乗算RGBA) のように、ピクセルデータがメモリ上で連続したバイト配列として格納されている場合、At(x, y)
メソッドを繰り返し呼び出すよりも、そのバイト配列を直接コピーする方がはるかに高速です。
このコミットの背景には、これらの特定の画像型に対するPNGエンコーディングのボトルネックを解消し、より高速な画像処理を実現するという目的があります。ベンチマーク結果が示すように、特に image.Gray
と image.NRGBA
のエンコーディングにおいて顕著なパフォーマンス改善が見込まれました。
前提知識の解説
Go言語の image
パッケージ
Go言語の image
パッケージは、画像処理のための基本的なインターフェースと実装を提供します。
image.Image
インターフェース: 全ての画像型が実装する基本的なインターフェースです。Bounds()
,ColorModel()
,At(x, y) Color
の3つのメソッドを持ちます。At(x, y)
メソッドは指定された座標のピクセルの色を返します。- 具体的な画像型:
image
パッケージには、image.RGBA
,image.NRGBA
,image.Gray
,image.Paletted
など、様々な具体的な画像型が定義されています。これらの型は、それぞれ異なるピクセルフォーマットとメモリレイアウトを持ちます。image.Gray
: 8ビットグレースケール画像。Pix
フィールドにピクセルデータがバイト配列として格納されます。image.NRGBA
: 非アルファ事前乗算RGBA画像。各ピクセルはR, G, B, Aの4バイトで表現され、Pix
フィールドに連続して格納されます。アルファ値が0の場合、RGB値も0になります。image.RGBA
: アルファ事前乗算RGBA画像。image.NRGBA
と同様に4バイトで表現されますが、アルファ値が乗算されたRGB値が格納されます。
Pix
フィールドとStride
フィールド:image.Gray
,image.RGBA
,image.NRGBA
などの具体的な画像型は、通常Pix []uint8
とStride int
というフィールドを持ちます。Pix
: 画像のピクセルデータを格納するバイトスライスです。Stride
: 各行の開始点間のバイト数を示します。これにより、画像データがメモリ上でどのように配置されているかを効率的に計算できます。
型アサーション
Go言語の型アサーション (value.(Type)
) は、インターフェース型の変数が、特定の具体的な型を保持しているかどうかをチェックし、その具体的な型の値を取得するために使用されます。例えば、m.(image.Gray)
は、m
が image.Image
インターフェース型であり、その基底の具体的な型が image.Gray
である場合に、image.Gray
型の値と true
を返します。そうでない場合は、nil
と false
を返します。この機能は、インターフェースの抽象化を保ちつつ、特定の具体的な型の最適化された処理パスを利用するために重要です。
PNGエンコーディング
PNG (Portable Network Graphics) は、可逆圧縮を特徴とするラスターグラフィックスファイルフォーマットです。PNGエンコーディングのプロセスには、通常、ピクセルデータのフィルタリング(差分エンコーディングなど)と、Deflate圧縮アルゴリズムによる圧縮が含まれます。このコミットで最適化された部分は、ピクセルデータをフィルタリングのために準備する段階、つまり image.Image
からバイト配列への変換効率です。
技術的詳細
このコミットの主要な最適化戦略は、Goの型アサーションを利用して、入力された image.Image
が特定の具体的な型 (image.Gray
, image.RGBA
, image.NRGBA
) であるかどうかを判別し、もしそうであれば、その具体的な型の内部データ表現 (Pix
スライス) を直接利用してピクセルデータをコピーすることです。これにより、汎用的な image.Image.At(x, y)
メソッドを各ピクセルに対して呼び出すオーバーヘッドを回避し、大幅なパフォーマンス向上を実現しています。
具体的には、src/pkg/image/png/writer.go
の writeImage
関数内で、各ピクセル行を処理するループにおいて、以下の変更が加えられました。
-
画像型の事前判別: ループに入る前に、入力画像
m
が*image.Gray
,*image.RGBA
,*image.Paletted
,*image.NRGBA
のいずれかの具体的な型であるかを型アサーションによって判別し、それぞれの変数に格納します。gray, _ := m.(*image.Gray) rgba, _ := m.(*image.RGBA) paletted, _ := m.(*image.Paletted) nrgba, _ := m.(*image.NRGBA)
これにより、ループ内で何度も型アサーションを行う必要がなくなり、効率が向上します。
-
cbG8
(グレースケール8ビット) の最適化:- 変更前は、
m.At(x, y)
を呼び出してcolor.Gray
に変換し、そのY
成分を1バイトずつコピーしていました。 - 変更後は、もし
gray != nil
(つまり入力画像が*image.Gray
型である) ならば、gray.Pix
スライスから該当する行のピクセルデータをcopy
関数を使って直接cr[0][1:]
にコピーします。gray.Pix
はグレースケールデータが連続して格納されているため、この直接コピーは非常に高速です。 offset := (y - b.Min.Y) * gray.Stride
で、現在の行のPix
スライス内での開始オフセットを計算します。
- 変更前は、
-
cbTC8
(トゥルーカラー8ビット、不透明) の最適化:- 変更前は、
m
が*image.RGBA
型の場合にのみrgba.Pix
を直接利用していました。 - 変更後は、
*image.RGBA
または*image.NRGBA
のいずれかである場合に、それぞれのPix
スライスとStride
を取得し、そこからピクセルデータを直接コピーするように拡張されました。これにより、image.NRGBA
型の不透明画像もこの高速パスの恩恵を受けられるようになりました。
- 変更前は、
-
cbTCA8
(トゥルーカラーアルファ8ビット) の最適化:- 変更前は、
m.At(x, y)
を呼び出してcolor.NRGBA
に変換し、R, G, B, Aの各成分を1バイトずつコピーしていました。 - 変更後は、もし
nrgba != nil
(つまり入力画像が*image.NRGBA
型である) ならば、nrgba.Pix
スライスから該当する行のピクセルデータをcopy
関数を使って直接cr[0][1:]
にコピーします。nrgba.Pix
はNRGBAデータが連続して格納されているため、この直接コピーは非常に高速です。
- 変更前は、
これらの変更により、image.Gray
と image.NRGBA
のような、内部データが連続したバイト配列として効率的に格納されている画像型に対して、PNGエンコーディング時のピクセルデータ読み出しが大幅に高速化されました。他の画像型や、具体的な型にキャストできない汎用的な image.Image
実装の場合には、従来の At(x, y)
を使用するフォールバックパスが引き続き利用されます。
src/pkg/image/png/writer_test.go
では、これらの最適化の効果を測定するために、BenchmarkEncodeGray
, BenchmarkEncodeNRGBOpaque
, BenchmarkEncodeNRGBA
といった新しいベンチマークが追加されました。これにより、変更が意図した通りにパフォーマンスを向上させていることが確認できます。
コアとなるコードの変更箇所
src/pkg/image/png/writer.go
--- a/src/pkg/image/png/writer.go
+++ b/src/pkg/image/png/writer.go
@@ -290,26 +290,42 @@ func writeImage(w io.Writer, m image.Image, cb int) error {
}\n pr := make([]uint8, 1+bpp*b.Dx())\n \n+\tgray, _ := m.(*image.Gray)\n+\trgba, _ := m.(*image.RGBA)\n+\tpaletted, _ := m.(*image.Paletted)\n+\tnrgba, _ := m.(*image.NRGBA)\n+\n for y := b.Min.Y; y < b.Max.Y; y++ {\n \t\t// Convert from colors to bytes.\n \t\ti := 1\n \t\tswitch cb {\n \t\tcase cbG8:\n-\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n-\t\t\t\tc := color.GrayModel.Convert(m.At(x, y)).(color.Gray)\n-\t\t\t\tcr[0][i] = c.Y
-\t\t\t\ti++
+\t\t\tif gray != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * gray.Stride\n+\t\t\t\tcopy(cr[0][1:], gray.Pix[offset:offset+b.Dx()])\n+\t\t\t} else {\n+\t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n+\t\t\t\t\tc := color.GrayModel.Convert(m.At(x, y)).(color.Gray)\n+\t\t\t\t\tcr[0][i] = c.Y
+\t\t\t\t\ti++
+\t\t\t\t}\n \t\t\t}\n \t\tcase cbTC8:\n \t\t\t// We have previously verified that the alpha value is fully opaque.\n \t\t\tcr0 := cr[0]\n-\t\t\tif rgba, _ := m.(*image.RGBA); rgba != nil {\n-\t\t\t\tj0 := (y - b.Min.Y) * rgba.Stride\n+\t\t\tstride, pix := 0, []byte(nil)\n+\t\t\tif rgba != nil {\n+\t\t\t\tstride, pix = rgba.Stride, rgba.Pix\n+\t\t\t} else if nrgba != nil {\n+\t\t\t\tstride, pix = nrgba.Stride, nrgba.Pix\n+\t\t\t}\n+\t\t\tif stride != 0 {\n+\t\t\t\tj0 := (y - b.Min.Y) * stride\n \t\t\t\tj1 := j0 + b.Dx()*4\n \t\t\t\tfor j := j0; j < j1; j += 4 {\n-\t\t\t\t\tcr0[i+0] = rgba.Pix[j+0]\n-\t\t\t\t\tcr0[i+1] = rgba.Pix[j+1]\n-\t\t\t\t\tcr0[i+2] = rgba.Pix[j+2]\n+\t\t\t\t\tcr0[i+0] = pix[j+0]\n+\t\t\t\t\tcr0[i+1] = pix[j+1]\n+\t\t\t\t\tcr0[i+2] = pix[j+2]\n \t\t\t\t\ti += 3\n \t\t\t\t}\n \t\t\t} else {\n@@ -322,9 +338,9 @@ func writeImage(w io.Writer, m image.Image, cb int) error {\n \t\t\t\t}\n \t\t\t}\n \t\tcase cbP8:\n-\t\t\tif p, _ := m.(*image.Paletted); p != nil {\n-\t\t\t\toffset := (y - b.Min.Y) * p.Stride\n-\t\t\t\tcopy(cr[0][1:], p.Pix[offset:offset+b.Dx()])\n+\t\t\tif paletted != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * paletted.Stride\n+\t\t\t\tcopy(cr[0][1:], paletted.Pix[offset:offset+b.Dx()])\n \t\t\t} else {\n \t\t\t\tpi := m.(image.PalettedImage)\n \t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n@@ -333,14 +349,19 @@ func writeImage(w io.Writer, m image.Image, cb int) error {\n \t\t\t\t}\n \t\t\t}\n \t\tcase cbTCA8:\n-\t\t\t// Convert from image.Image (which is alpha-premultiplied) to PNG\'s non-alpha-premultiplied.\n-\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n-\t\t\t\tc := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA)\n-\t\t\t\tcr[0][i+0] = c.R\n-\t\t\t\tcr[0][i+1] = c.G\n-\t\t\t\tcr[0][i+2] = c.B\n-\t\t\t\tcr[0][i+3] = c.A\n-\t\t\t\ti += 4\n+\t\t\tif nrgba != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * nrgba.Stride\n+\t\t\t\tcopy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4])\n+\t\t\t} else {\n+\t\t\t\t// Convert from image.Image (which is alpha-premultiplied) to PNG\'s non-alpha-premultiplied.\n+\t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n+\t\t\t\t\tc := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA)\n+\t\t\t\t\tcr[0][i+0] = c.R\n+\t\t\t\t\tcr[0][i+1] = c.G\n+\t\t\t\t\tcr[0][i+2] = c.B\n+\t\t\t\t\tcr[0][i+3] = c.A\n+\t\t\t\t\ti += 4\n+\t\t\t\t}\n \t\t\t}\n \t\tcase cbG16:\
src/pkg/image/png/writer_test.go
--- a/src/pkg/image/png/writer_test.go
+++ b/src/pkg/image/png/writer_test.go
@@ -101,6 +101,49 @@ func TestSubImage(t *testing.T) {
}\n }\n \n+func BenchmarkEncodeGray(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewGray(image.Rect(0, 0, 640, 480))\n+\tb.SetBytes(640 * 480 * 1)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n+func BenchmarkEncodeNRGBOpaque(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewNRGBA(image.Rect(0, 0, 640, 480))\n+\t// Set all pixels to 0xFF alpha to force opaque mode.\n+\tbo := img.Bounds()\n+\tfor y := bo.Min.Y; y < bo.Max.Y; y++ {\n+\t\tfor x := bo.Min.X; x < bo.Max.X; x++ {\n+\t\t\timg.Set(x, y, color.NRGBA{0, 0, 0, 255})\n+\t\t}\n+\t}\n+\tif !img.Opaque() {\n+\t\tb.Fatal(\"expected image to be opaque\")\n+\t}\n+\tb.SetBytes(640 * 480 * 4)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n+func BenchmarkEncodeNRGBA(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewNRGBA(image.Rect(0, 0, 640, 480))\n+\tif img.Opaque() {\n+\t\tb.Fatal(\"expected image not to be opaque\")\n+\t}\n+\tb.SetBytes(640 * 480 * 4)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n func BenchmarkEncodePaletted(b *testing.B) {\n \tb.StopTimer()\n \timg := image.NewPaletted(image.Rect(0, 0, 640, 480), color.Palette{\n@@ -138,7 +181,7 @@ func BenchmarkEncodeRGBA(b *testing.B) {\n \tb.StopTimer()\n \timg := image.NewRGBA(image.Rect(0, 0, 640, 480))\n \tif img.Opaque() {\n-\t\tb.Fatal(\"expected image to not be opaque\")\n+\t\tb.Fatal(\"expected image not to be opaque\")\n \t}\n \tb.SetBytes(640 * 480 * 4)\n \tb.StartTimer()\
コアとなるコードの解説
src/pkg/image/png/writer.go
の変更点
writeImage
関数は、image.Image
インターフェースを受け取り、その画像をPNG形式でエンコードする主要なロジックを含んでいます。この関数は、画像のカラータイプ (cb
変数) に応じて異なるエンコーディングパスを選択します。
-
型アサーションの導入:
gray, _ := m.(*image.Gray) rgba, _ := m.(*image.RGBA) paletted, _ := m.(*image.Paletted) nrgba, _ := m.(*image.NRGBA)
m
はimage.Image
インターフェース型ですが、その基底の具体的な型が*image.Gray
,*image.RGBA
,*image.Paletted
,*image.NRGBA
のいずれかであるかを事前にチェックし、それぞれのポインタ変数に格納しています。これにより、後続のswitch
文内で何度も型アサーションを行う手間を省き、コードの可読性と効率を向上させています。 -
cbG8
(グレースケール) の最適化:case cbG8: if gray != nil { offset := (y - b.Min.Y) * gray.Stride copy(cr[0][1:], gray.Pix[offset:offset+b.Dx()]) } else { for x := b.Min.X; x < b.Max.X; x++ { c := color.GrayModel.Convert(m.At(x, y)).(color.Gray) cr[0][i] = c.Y i++ } }
if gray != nil
の条件が追加されました。これは、入力画像m
が実際に*image.Gray
型のインスタンスである場合に真となります。- この条件が真の場合、
gray.Pix
スライスから直接ピクセルデータをコピーします。gray.Stride
を使って現在の行の開始オフセットを計算し、copy
関数でその行の全ピクセルデータをcr[0][1:]
(PNGエンコーダの内部バッファ) に効率的に転送します。image.Gray
はピクセルデータが連続したバイト配列として格納されているため、この直接コピーはm.At(x, y)
を繰り返し呼び出すよりもはるかに高速です。 else
ブロックは、m
が*image.Gray
型ではない場合のフォールバックパスです。この場合、従来のm.At(x, y)
を使用したピクセルごとの変換とコピーが行われます。
-
cbTC8
(トゥルーカラー、不透明) の最適化:case cbTC8: // We have previously verified that the alpha value is fully opaque. cr0 := cr[0] stride, pix := 0, []byte(nil) if rgba != nil { stride, pix = rgba.Stride, rgba.Pix } else if nrgba != nil { stride, pix = nrgba.Stride, nrgba.Pix } if stride != 0 { j0 := (y - b.Min.Y) * stride j1 := j0 + b.Dx()*4 for j := j0; j < j1; j += 4 { cr0[i+0] = pix[j+0] cr0[i+1] = pix[j+1] cr0[i+2] = pix[j+2] i += 3 } } else { // ... (既存のフォールバックロジック) }
stride, pix := 0, []byte(nil)
で、ピクセルデータとストライドを保持する変数を初期化します。if rgba != nil
またはelse if nrgba != nil
の条件で、入力画像が*image.RGBA
または*image.NRGBA
のいずれかであるかをチェックします。- いずれかの条件が真の場合、それぞれの
Stride
とPix
フィールドをstride
とpix
変数に代入します。これにより、image.RGBA
とimage.NRGBA
の両方の不透明画像が、この高速な直接ピクセルアクセスパスを利用できるようになります。 if stride != 0
の条件が真の場合、pix
スライスから直接R, G, B成分をコピーします。
-
cbP8
(パレット) の変更:- if p, _ := m.(*image.Paletted); p != nil { - offset := (y - b.Min.Y) * p.Stride - copy(cr[0][1:], p.Pix[offset:offset+b.Dx()]) + if paletted != nil { + offset := (y - b.Min.Y) * paletted.Stride + copy(cr[0][1:], paletted.Pix[offset:offset+b.Dx()]) } else { // ... }
- この変更は機能的な最適化ではなく、事前に取得した
paletted
変数を使用するようにコードをクリーンアップしたものです。ロジック自体は変更されていません。
- この変更は機能的な最適化ではなく、事前に取得した
-
cbTCA8
(トゥルーカラーアルファ) の最適化:case cbTCA8: if nrgba != nil { offset := (y - b.Min.Y) * nrgba.Stride copy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4]) } else { // Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied. for x := b.Min.X; x < b.Max.X; x++ { c := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA) cr[0][i+0] = c.R cr[0][i+1] = c.G cr[0][i+2] = c.B cr[0][i+3] = c.A i += 4 } }
if nrgba != nil
の条件が追加されました。これは、入力画像m
が*image.NRGBA
型のインスタンスである場合に真となります。- この条件が真の場合、
nrgba.Pix
スライスから直接ピクセルデータをコピーします。image.NRGBA
は各ピクセルがR, G, B, Aの4バイトで連続して格納されているため、copy
関数でその行の全ピクセルデータを効率的に転送できます。 else
ブロックは、m
が*image.NRGBA
型ではない場合のフォールバックパスです。この場合、従来のm.At(x, y)
を使用したピクセルごとの変換とコピーが行われます。
src/pkg/image/png/writer_test.go
の変更点
- 新しいベンチマーク関数の追加:
BenchmarkEncodeGray
:image.NewGray
で作成した画像をエンコードするベンチマーク。BenchmarkEncodeNRGBOpaque
:image.NewNRGBA
で作成し、全てのピクセルのアルファ値を0xFF
(完全に不透明) に設定した画像をエンコードするベンチマーク。これにより、不透明なNRGBA
画像のエンコーディング性能を測定します。BenchmarkEncodeNRGBA
:image.NewNRGBA
で作成した、アルファ値が不透明ではない画像をエンコードするベンチマーク。これにより、アルファチャンネルを持つNRGBA
画像のエンコーディング性能を測定します。
- 既存ベンチマークの修正:
BenchmarkEncodeRGBA
の中で、img.Opaque()
がtrue
でないことを確認するアサーションが追加されました。これは、このベンチマークがアルファチャンネルを持つRGBA
画像のエンコーディングを正確に測定していることを保証するためです。
これらのベンチマークの追加により、image.Gray
と image.NRGBA
のエンコーディング最適化が実際にパフォーマンスに貢献していることを定量的に確認できるようになりました。
関連リンク
- Go言語
image
パッケージのドキュメント: https://pkg.go.dev/image - Go言語
image/png
パッケージのドキュメント: https://pkg.go.dev/image/png - PNG (Portable Network Graphics) の仕様: https://www.w3.org/TR/PNG/
参考にした情報源リンク
- Go言語の型アサーションに関する公式ドキュメント: https://go.dev/tour/methods/15
- Go言語のベンチマークに関する公式ドキュメント: https://go.dev/doc/articles/go_benchmarking.html
- Gerrit Change-ID:
I237ee3926906ad08a048e764920e036ecdb08b11
(GoのGerritシステムにおける変更リストのID) - GoのGerritシステム: https://go-review.googlesource.com/
- このコミットのGerrit変更リスト: https://golang.org/cl/6490099 (コミットメッセージに記載されているリンク)