Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 パッケージは、このチャンク転送エンコーディングの送受信をサポートしており、chunkedReaderchunkedWriter といった内部構造体でその処理を行っています。

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.MemStatsruntime.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 関数の実装方法の変更です。

以前の実装では、以下の手順でメモリ割り当てを計測していました。

  1. runtime.GOMAXPROCS(1) を呼び出して、Goランタイムが使用するCPUコア数を一時的に1に設定していました。これは、メモリ割り当ての計測がマルチコア環境で不安定になる可能性があったためです。テスト終了時には defer を使って元の GOMAXPROCS に戻していました。
  2. runtime.ReadMemStats(&ms) を呼び出して、テスト対象のコードを実行する前のメモリ統計 (m0) を取得していました。
  3. io.ReadFull(r, readBuf) を呼び出して、チャンクリーダーからデータを読み込む操作を実行していました。
  4. 再度 runtime.ReadMemStats(&ms) を呼び出して、テスト対象のコード実行後のメモリ統計を取得していました。
  5. mallocs := ms.Mallocs - m0 として、実行前後の Mallocs の差分を計算し、メモリ割り当て数を手動で算出していました。
  6. if mallocs > 1 のように、手動で算出した mallocs の値が期待値(この場合は1)を超えていないかを確認していました。

新しい実装では、以下のようになっています。

  1. runtime パッケージのインポートと runtime.GOMAXPROCS の呼び出しが削除されました。testing.AllocsPerRun が内部でこれらの管理を行うため、不要になりました。
  2. bufio パッケージがインポートされました。これは、bufio.NewReader を使用して bytes.Reader をラップするためです。これにより、chunkedReaderbufio.Reader を受け入れるようになり、より実際のI/Oに近いテスト環境が提供されます。
  3. bytes.NewReader(buf.Bytes())bufio.NewReader(byter) を使用して、テスト用の入力データ (buf) を io.Reader インターフェースとして提供しています。
  4. testing.AllocsPerRun(10, func() { ... }) を使用して、メモリ割り当てを計測するようになりました。
    • 10 は、テスト対象の関数を10回実行することを意味します。これにより、より安定した平均メモリ割り当て数が得られます。
    • 匿名関数内で、byter.Seek(0, 0)bufr.Reset(byter) を呼び出して、各テスト実行の前にリーダーの状態をリセットしています。これにより、各実行が独立した状態で行われることを保証しています。
    • r := newChunkedReader(bufr) を呼び出して新しいチャンクリーダーを作成し、io.ReadFull(r, readBuf) でデータを読み込んでいます。
    • 読み込みバイト数とエラーのチェックも匿名関数内で行われ、t.Fatalf を使用してエラーが発生した場合はテストを即座に終了させています。
  5. if mallocs > 1.5 のように、testing.AllocsPerRun が返す浮動小数点数の平均メモリ割り当て数に対してチェックを行っています。閾値が 1 から 1.5 に変更されているのは、AllocsPerRun が平均値を返すため、わずかな変動を許容するためと考えられます。
  6. エラー報告が t.Errorf から t.Logf に変更されています。これは、mallocs1.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 とほぼ同じ変更が適用されています。newChunkedReaderNewChunkedReader になっている点など、パッケージ固有の差異はありますが、テストロジックの変更は同一です。

--- 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)
	}

この新しいコードでは、以下の点が改善されています。

  1. testing.AllocsPerRun の利用: testing.AllocsPerRun 関数が導入されたことで、メモリ割り当ての計測がGoのテストフレームワークに統合されました。これにより、手動での runtime.ReadMemStats の呼び出しや GOMAXPROCS の管理が不要になり、テストコードが簡潔になりました。AllocsPerRun は内部で複数回(ここでは10回)テスト対象の関数を実行し、平均のメモリ割り当て数を返すため、より安定した計測結果が得られます。
  2. bufio.Reader の導入: newChunkedReader (または NewChunkedReader) が io.Reader ではなく *bufio.Reader を引数として受け取るように変更されたため、bytes.Readerbufio.NewReader でラップしています。これは、実際のHTTP通信では通常バッファリングされたリーダーが使用されるため、より現実的なテストシナリオを反映しています。
  3. テストの堅牢性: testing.AllocsPerRun の匿名関数内で、byter.Seek(0, 0)bufr.Reset(byter) を呼び出すことで、各テスト実行の前にリーダーの状態を確実にリセットしています。これにより、各実行が独立した状態で行われ、計測結果の正確性が保証されます。
  4. エラーハンドリングの変更: io.ReadFull の結果に対するエラーチェックが t.Errorf から t.Fatalf に変更されています。これは、読み込みバイト数やエラーが期待通りでない場合に、テストを即座に終了させることで、後続のメモリ割り当てチェックが不正確なデータに基づいて行われることを防ぎます。
  5. 閾値とログレベルの変更: mallocs のチェックが > 1 から > 1.5 に変更され、t.Errorf から t.Logf に変更されています。これは、testing.AllocsPerRun が浮動小数点数の平均値を返すため、厳密な整数値の比較ではなく、ある程度の許容範囲を持たせるためと考えられます。また、t.Logf に変更されたことで、メモリ割り当てがわずかに増えた場合でもテストが失敗するのではなく、ログに情報が出力されるようになりました。これは、メモリ割り当ての最適化が非常に厳密な要件ではない場合に、テストの柔軟性を高めるための変更である可能性があります。

これらの変更により、チャンクリーダーのメモリ割り当てテストは、Goのテストフレームワークの最新の機能を利用し、より堅牢で信頼性の高いものになりました。

関連リンク

参考にした情報源リンク