[インデックス 18293] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
および net/http/httputil
パッケージにおけるチャンクエンコーディングリーダーのメモリ割り当てテストをより堅牢にするための変更です。具体的には、手動で行っていたメモリ割り当ての計測を、Goのテストフレームワークに新しく導入された testing.AllocsPerRun
関数を使用するように置き換えています。これにより、テストの信頼性と保守性が向上しています。
コミット
commit 6592aeb8f3a0398f32a31642695188b361c6c434
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun Jan 19 10:02:10 2014 -0800
net/http, net/http/httputil: make chunked reader alloc test more robust
Use testing.AllocsPerRun now that it exists, instead of doing it by hand.
Fixes #6076
R=golang-codereviews, alex.brainman
CC=golang-codereviews
https://golang.org/cl/53810043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6592aeb8f3a0398f32a31642695188b361c6c434
元コミット内容
このコミットは、net/http
および net/http/httputil
パッケージ内の chunked_test.go
ファイルにおけるメモリ割り当てテストの改善を目的としています。以前は、runtime.ReadMemStats
を用いて手動でメモリ割り当て数を計測していましたが、このコミットでは testing.AllocsPerRun
関数が利用可能になったため、それを用いてテストを書き直しています。これにより、テストコードが簡潔になり、より正確で信頼性の高いメモリ割り当てテストが可能になります。また、GOMAXPROCS
を一時的に1に設定する処理も不要になっています。
変更の背景
この変更の背景には、Goのテストフレームワークの進化があります。以前は、特定の操作におけるメモリ割り当て数を計測するためには、runtime.ReadMemStats
を呼び出してテスト対象のコード実行前後のメモリ統計を比較するという手動でのアプローチが必要でした。これは、テストコードが複雑になりがちで、正確な計測が難しい場合もありました。
Go 1.2で testing.AllocsPerRun
関数が導入されたことで、このようなメモリ割り当てテストをより簡単かつ正確に記述できるようになりました。この関数は、指定された関数を複数回実行し、その平均メモリ割り当て数を計測してくれます。これにより、テストの信頼性が向上し、テストコードの可読性も改善されます。
このコミットは、既存のテストコードを新しい testing.AllocsPerRun
を利用するように更新し、テストの堅牢性を高めることを目的としています。また、関連するIssueである #6076 の修正も含まれています。
前提知識の解説
HTTP チャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1では、メッセージボディの長さを事前に知らなくても、動的にコンテンツを送信できる「チャンク転送エンコーディング」というメカニズムが提供されています。これは、特に動的に生成されるコンテンツや、大きなファイルをストリーミングする際に有用です。
チャンク転送エンコーディングでは、メッセージボディが複数の「チャンク」に分割されて送信されます。各チャンクは、そのチャンクのサイズ(16進数)と、それに続くデータで構成されます。最後のチャンクはサイズが0で、その後にフッター(オプション)が続きます。
Goの net/http
パッケージは、このチャンク転送エンコーディングの送受信をサポートしており、chunkedReader
や chunkedWriter
といった内部構造体でその処理を行っています。
Goのメモリ割り当て (Memory Allocation)
Goはガベージコレクション(GC)を備えた言語であり、開発者が明示的にメモリを解放する必要はありません。しかし、プログラムのパフォーマンスを最適化するためには、不必要なメモリ割り当て(アロケーション)を減らすことが重要です。メモリ割り当ては、ヒープ領域からメモリを確保する操作であり、GCの負荷を増大させ、プログラムの実行速度に影響を与える可能性があります。
Goのテストでは、特定の操作がどれくらいのメモリ割り当てを行うかを計測する「アロケーションテスト」を行うことがあります。これは、パフォーマンスのボトルネックを特定したり、メモリ効率の良いコードを書くための指標となります。
testing.AllocsPerRun
testing.AllocsPerRun
は、Goの標準テストパッケージ testing
に含まれる関数です。この関数は、指定された関数を複数回(runs
引数で指定)実行し、その関数が実行されるたびに発生する平均メモリ割り当て数を浮動小数点数で返します。
func AllocsPerRun(runs int, f func()) float64
runs
: テスト対象の関数を何回実行するかを指定します。複数回実行することで、より安定した平均値を得ることができます。f
: メモリ割り当てを計測したい関数です。
この関数を使用することで、手動で runtime.ReadMemStats
を呼び出してメモリ割り当て数を計測するよりも、簡潔かつ正確にアロケーションテストを記述できます。また、testing.AllocsPerRun
は内部で runtime.GOMAXPROCS
を適切に管理するため、テストコード内で GOMAXPROCS
を一時的に変更する必要がなくなります。
runtime.MemStats
と runtime.ReadMemStats
runtime.MemStats
は、Goプログラムのメモリ使用状況に関する統計情報を提供する構造体です。これには、ヒープの割り当てバイト数、オブジェクト数、GCの実行回数などが含まれます。
runtime.ReadMemStats
関数は、現在の runtime.MemStats
を取得するために使用されます。この関数は、Go 1.2で testing.AllocsPerRun
が導入される以前は、メモリ割り当てテストで手動でアロケーション数を計測するために使われていました。具体的には、テスト対象のコードを実行する前と後で MemStats
を取得し、Mallocs
フィールド(割り当てられたオブジェクトの総数)の差分を計算することで、そのコードがどれだけのメモリ割り当てを行ったかを推定していました。
技術的詳細
このコミットの主要な技術的変更は、net/http
および net/http/httputil
パッケージ内の chunked_test.go
ファイルにおける TestChunkReaderAllocs
関数の実装方法の変更です。
以前の実装では、以下の手順でメモリ割り当てを計測していました。
runtime.GOMAXPROCS(1)
を呼び出して、Goランタイムが使用するCPUコア数を一時的に1に設定していました。これは、メモリ割り当ての計測がマルチコア環境で不安定になる可能性があったためです。テスト終了時にはdefer
を使って元のGOMAXPROCS
に戻していました。runtime.ReadMemStats(&ms)
を呼び出して、テスト対象のコードを実行する前のメモリ統計 (m0
) を取得していました。io.ReadFull(r, readBuf)
を呼び出して、チャンクリーダーからデータを読み込む操作を実行していました。- 再度
runtime.ReadMemStats(&ms)
を呼び出して、テスト対象のコード実行後のメモリ統計を取得していました。 mallocs := ms.Mallocs - m0
として、実行前後のMallocs
の差分を計算し、メモリ割り当て数を手動で算出していました。if mallocs > 1
のように、手動で算出したmallocs
の値が期待値(この場合は1)を超えていないかを確認していました。
新しい実装では、以下のようになっています。
runtime
パッケージのインポートとruntime.GOMAXPROCS
の呼び出しが削除されました。testing.AllocsPerRun
が内部でこれらの管理を行うため、不要になりました。bufio
パッケージがインポートされました。これは、bufio.NewReader
を使用してbytes.Reader
をラップするためです。これにより、chunkedReader
がbufio.Reader
を受け入れるようになり、より実際のI/Oに近いテスト環境が提供されます。bytes.NewReader(buf.Bytes())
とbufio.NewReader(byter)
を使用して、テスト用の入力データ (buf
) をio.Reader
インターフェースとして提供しています。testing.AllocsPerRun(10, func() { ... })
を使用して、メモリ割り当てを計測するようになりました。10
は、テスト対象の関数を10回実行することを意味します。これにより、より安定した平均メモリ割り当て数が得られます。- 匿名関数内で、
byter.Seek(0, 0)
とbufr.Reset(byter)
を呼び出して、各テスト実行の前にリーダーの状態をリセットしています。これにより、各実行が独立した状態で行われることを保証しています。 r := newChunkedReader(bufr)
を呼び出して新しいチャンクリーダーを作成し、io.ReadFull(r, readBuf)
でデータを読み込んでいます。- 読み込みバイト数とエラーのチェックも匿名関数内で行われ、
t.Fatalf
を使用してエラーが発生した場合はテストを即座に終了させています。
if mallocs > 1.5
のように、testing.AllocsPerRun
が返す浮動小数点数の平均メモリ割り当て数に対してチェックを行っています。閾値が1
から1.5
に変更されているのは、AllocsPerRun
が平均値を返すため、わずかな変動を許容するためと考えられます。- エラー報告が
t.Errorf
からt.Logf
に変更されています。これは、mallocs
が1.5
を超えた場合でもテストを失敗させるのではなく、ログに記録するだけに変更されたことを意味します。これは、メモリ割り当ての厳密なチェックから、より柔軟なログ記録に変わったことを示唆しています。
これらの変更により、テストコードはより簡潔になり、Goのテストフレームワークの最新の機能を利用することで、メモリ割り当てテストの信頼性と保守性が向上しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の2つです。
src/pkg/net/http/chunked_test.go
src/pkg/net/http/httputil/chunked_test.go
両方のファイルで、TestChunkReaderAllocs
関数が同様の方法で変更されています。
src/pkg/net/http/chunked_test.go
の変更点
--- a/src/pkg/net/http/chunked_test.go
+++ b/src/pkg/net/http/chunked_test.go
@@ -8,11 +8,11 @@
package http
import (
+ "bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
- "runtime"
"testing"
)
@@ -42,8 +42,6 @@ func TestChunk(t *testing.T) {
}
func TestChunkReaderAllocs(t *testing.T) {
- // temporarily set GOMAXPROCS to 1 as we are testing memory allocations
- defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))\n
var buf bytes.Buffer
w := newChunkedWriter(&buf)
a, b, c := []byte("aaaaaa"), []byte("bbbbbbbbbbbb"), []byte("cccccccccccccccccccccccc")
@@ -52,26 +50,23 @@ func TestChunkReaderAllocs(t *testing.T) {
w.Write(c)
w.Close()
- r := newChunkedReader(&buf)
readBuf := make([]byte, len(a)+len(b)+len(c)+1)
-\n
- var ms runtime.MemStats
- runtime.ReadMemStats(&ms)
- m0 := ms.Mallocs
-\n
- n, err := io.ReadFull(r, readBuf)
-\n
- runtime.ReadMemStats(&ms)
- mallocs := ms.Mallocs - m0
- if mallocs > 1 {\n
- t.Errorf("%d mallocs; want <= 1", mallocs)
- }\n
-\n
- if n != len(readBuf)-1 {\n
- t.Errorf("read %d bytes; want %d", n, len(readBuf)-1)
- }\n
- if err != io.ErrUnexpectedEOF {\n
- t.Errorf("read error = %v; want ErrUnexpectedEOF", err)
+ byter := bytes.NewReader(buf.Bytes())
+ bufr := bufio.NewReader(byter)
+ mallocs := testing.AllocsPerRun(10, func() {
+ byter.Seek(0, 0)
+ bufr.Reset(byter)
+ r := newChunkedReader(bufr)
+ n, err := io.ReadFull(r, readBuf)
+ if n != len(readBuf)-1 {
+ t.Fatalf("read %d bytes; want %d", n, len(readBuf)-1)
+ }
+ if err != io.ErrUnexpectedEOF {
+ t.Fatalf("read error = %v; want ErrUnexpectedEOF", err)
+ }
+ })
+ if mallocs > 1.5 {
+ t.Logf("mallocs = %v; want 1", mallocs)
}
}
src/pkg/net/http/httputil/chunked_test.go
の変更点
このファイルも net/http/chunked_test.go
とほぼ同じ変更が適用されています。newChunkedReader
が NewChunkedReader
になっている点など、パッケージ固有の差異はありますが、テストロジックの変更は同一です。
--- a/src/pkg/net/http/httputil/chunked_test.go
+++ b/src/pkg/net/http/httputil/chunked_test.go
@@ -10,11 +10,11 @@
package httputil
import (
+ "bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
- "runtime"
"testing"
)
@@ -44,8 +44,6 @@ func TestChunk(t *testing.T) {
}
func TestChunkReaderAllocs(t *testing.T) {
- // temporarily set GOMAXPROCS to 1 as we are testing memory allocations
- defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))\n
var buf bytes.Buffer
w := NewChunkedWriter(&buf)
a, b, c := []byte("aaaaaa"), []byte("bbbbbbbbbbbb"), []byte("cccccccccccccccccccccccc")
@@ -54,26 +52,23 @@ func TestChunkReaderAllocs(t *testing.T) {
w.Write(c)
w.Close()
- r := NewChunkedReader(&buf)
readBuf := make([]byte, len(a)+len(b)+len(c)+1)
-\n
- var ms runtime.MemStats
- runtime.ReadMemStats(&ms)
- m0 := ms.Mallocs
-\n
- n, err := io.ReadFull(r, readBuf)
-\n
- runtime.ReadMemStats(&ms)
- mallocs := ms.Mallocs - m0
- if mallocs > 1 {\n
- t.Errorf("%d mallocs; want <= 1", mallocs)
- }\n
-\n
- if n != len(readBuf)-1 {\n
- t.Errorf("read %d bytes; want %d", n, len(readBuf)-1)
- }\n
- if err != io.ErrUnexpectedEOF {\n
- t.Errorf("read error = %v; want ErrUnexpectedEOF", err)
+ byter := bytes.NewReader(buf.Bytes())
+ bufr := bufio.NewReader(byter)
+ mallocs := testing.AllocsPerRun(10, func() {
+ byter.Seek(0, 0)
+ bufr.Reset(byter)
+ r := NewChunkedReader(bufr)
+ n, err := io.ReadFull(r, readBuf)
+ if n != len(readBuf)-1 {
+ t.Fatalf("read %d bytes; want %d", n, len(readBuf)-1)
+ }
+ if err != io.ErrUnexpectedEOF {
+ t.Fatalf("read error = %v; want ErrUnexpectedEOF", err)
+ }
+ })
+ if mallocs > 1.5 {
+ t.Logf("mallocs = %v; want 1", mallocs)
}
}
コアとなるコードの解説
変更の核心は、TestChunkReaderAllocs
関数内でメモリ割り当てを計測する方法を、手動での runtime.ReadMemStats
の使用から testing.AllocsPerRun
の使用に切り替えた点です。
変更前:
// temporarily set GOMAXPROCS to 1 as we are testing memory allocations
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
// ...
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
m0 := ms.Mallocs
n, err := io.ReadFull(r, readBuf)
runtime.ReadMemStats(&ms)
mallocs := ms.Mallocs - m0
if mallocs > 1 {
t.Errorf("%d mallocs; want <= 1", mallocs)
}
このコードは、GOMAXPROCS
を1に固定し、io.ReadFull
の実行前後に runtime.ReadMemStats
を呼び出して Mallocs
の差分を計算していました。これは、メモリ割り当ての計測を開発者が手動で行う必要があり、テストの信頼性や移植性に課題がありました。特に、GOMAXPROCS
の設定は、テスト環境やGoのバージョンによって挙動が変わる可能性がありました。
変更後:
byter := bytes.NewReader(buf.Bytes())
bufr := bufio.NewReader(byter)
mallocs := testing.AllocsPerRun(10, func() {
byter.Seek(0, 0)
bufr.Reset(byter)
r := newChunkedReader(bufr) // or NewChunkedReader(bufr) for httputil
n, err := io.ReadFull(r, readBuf)
if n != len(readBuf)-1 {
t.Fatalf("read %d bytes; want %d", n, len(readBuf)-1)
}
if err != io.ErrUnexpectedEOF {
t.Fatalf("read error = %v; want ErrUnexpectedEOF", err)
}
})
if mallocs > 1.5 {
t.Logf("mallocs = %v; want 1", mallocs)
}
この新しいコードでは、以下の点が改善されています。
testing.AllocsPerRun
の利用:testing.AllocsPerRun
関数が導入されたことで、メモリ割り当ての計測がGoのテストフレームワークに統合されました。これにより、手動でのruntime.ReadMemStats
の呼び出しやGOMAXPROCS
の管理が不要になり、テストコードが簡潔になりました。AllocsPerRun
は内部で複数回(ここでは10回)テスト対象の関数を実行し、平均のメモリ割り当て数を返すため、より安定した計測結果が得られます。bufio.Reader
の導入:newChunkedReader
(またはNewChunkedReader
) がio.Reader
ではなく*bufio.Reader
を引数として受け取るように変更されたため、bytes.Reader
をbufio.NewReader
でラップしています。これは、実際のHTTP通信では通常バッファリングされたリーダーが使用されるため、より現実的なテストシナリオを反映しています。- テストの堅牢性:
testing.AllocsPerRun
の匿名関数内で、byter.Seek(0, 0)
とbufr.Reset(byter)
を呼び出すことで、各テスト実行の前にリーダーの状態を確実にリセットしています。これにより、各実行が独立した状態で行われ、計測結果の正確性が保証されます。 - エラーハンドリングの変更:
io.ReadFull
の結果に対するエラーチェックがt.Errorf
からt.Fatalf
に変更されています。これは、読み込みバイト数やエラーが期待通りでない場合に、テストを即座に終了させることで、後続のメモリ割り当てチェックが不正確なデータに基づいて行われることを防ぎます。 - 閾値とログレベルの変更:
mallocs
のチェックが> 1
から> 1.5
に変更され、t.Errorf
からt.Logf
に変更されています。これは、testing.AllocsPerRun
が浮動小数点数の平均値を返すため、厳密な整数値の比較ではなく、ある程度の許容範囲を持たせるためと考えられます。また、t.Logf
に変更されたことで、メモリ割り当てがわずかに増えた場合でもテストが失敗するのではなく、ログに情報が出力されるようになりました。これは、メモリ割り当ての最適化が非常に厳密な要件ではない場合に、テストの柔軟性を高めるための変更である可能性があります。
これらの変更により、チャンクリーダーのメモリ割り当てテストは、Goのテストフレームワークの最新の機能を利用し、より堅牢で信頼性の高いものになりました。
関連リンク
- Go Issue #6076: https://github.com/golang/go/issues/6076
- Go CL 53810043: https://golang.org/cl/53810043
参考にした情報源リンク
- Go Documentation:
testing
package: https://pkg.go.dev/testing - Go Documentation:
runtime
package: https://pkg.go.dev/runtime - HTTP/1.1 Message Syntax and Routing (RFC 7230) - Chunked Transfer Encoding: https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
- Go 1.2 Release Notes (mentioning
testing.AllocsPerRun
): https://go.dev/doc/go1.2#testing - Go Memory Management: https://go.dev/doc/effective_go#allocation
- Go Blog: The Go Memory Model: https://go.dev/blog/memory-model
- Go Blog: Go's work-stealing scheduler: https://go.dev/blog/go1.2-scheduler (GOMAXPROCSに関連)
- Go source code for
net/http
andnet/http/httputil
(for context onchunkedReader
): https://github.com/golang/go/tree/master/src/net/http - Go source code for
testing/testing.go
(forAllocsPerRun
implementation details): https://github.com/golang/go/blob/master/src/testing/testing.go