mirror of
https://github.com/golang/net.git
synced 2026-04-01 02:47:08 +09:00
The previous algorithm was linear in the size of the table.
The new algorithm is O(1). The new algorithm should behave exactly
like the old algorthm, except for one unimportant change that
triggered small updates to tests in encode_test.go:
When encoding "Field: Value" where the table has two entries,
[0]={"Field", "X"} and [1]={"Field", "Y"}, we can encode the field
name using either table entry. Previously, we selected the oldest
entry, but now we select the newest entry. The new implementation
should actually generate very slightly better compression because
new entries are encoded with smaller integers than old entries, and
HPACK uses a varint encoding for integers where smaller integers
are encoded in fewer bytes.
I added a synthetic microbenchmark which shows a big speedup in
hpack.Encoder.searchTable:
BenchmarkEncoderSearchTable-40 100000 127440 ns/op # before
BenchmarkEncoderSearchTable-40 50000 25121 ns/op # after
Change-Id: Ib87d61b6415d9f0ff38874fe2a719b2f00351590
Reviewed-on: https://go-review.googlesource.com/37406
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
387 lines
9.9 KiB
Go
387 lines
9.9 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package hpack
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/rand"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestEncoderTableSizeUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
size1, size2 uint32
|
|
wantHex string
|
|
}{
|
|
// Should emit 2 table size updates (2048 and 4096)
|
|
{2048, 4096, "3fe10f 3fe11f 82"},
|
|
|
|
// Should emit 1 table size update (2048)
|
|
{16384, 2048, "3fe10f 82"},
|
|
}
|
|
for _, tt := range tests {
|
|
var buf bytes.Buffer
|
|
e := NewEncoder(&buf)
|
|
e.SetMaxDynamicTableSize(tt.size1)
|
|
e.SetMaxDynamicTableSize(tt.size2)
|
|
if err := e.WriteField(pair(":method", "GET")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := removeSpace(tt.wantHex)
|
|
if got := hex.EncodeToString(buf.Bytes()); got != want {
|
|
t.Errorf("e.SetDynamicTableSize %v, %v = %q; want %q", tt.size1, tt.size2, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncoderWriteField(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
e := NewEncoder(&buf)
|
|
var got []HeaderField
|
|
d := NewDecoder(4<<10, func(f HeaderField) {
|
|
got = append(got, f)
|
|
})
|
|
|
|
tests := []struct {
|
|
hdrs []HeaderField
|
|
}{
|
|
{[]HeaderField{
|
|
pair(":method", "GET"),
|
|
pair(":scheme", "http"),
|
|
pair(":path", "/"),
|
|
pair(":authority", "www.example.com"),
|
|
}},
|
|
{[]HeaderField{
|
|
pair(":method", "GET"),
|
|
pair(":scheme", "http"),
|
|
pair(":path", "/"),
|
|
pair(":authority", "www.example.com"),
|
|
pair("cache-control", "no-cache"),
|
|
}},
|
|
{[]HeaderField{
|
|
pair(":method", "GET"),
|
|
pair(":scheme", "https"),
|
|
pair(":path", "/index.html"),
|
|
pair(":authority", "www.example.com"),
|
|
pair("custom-key", "custom-value"),
|
|
}},
|
|
}
|
|
for i, tt := range tests {
|
|
buf.Reset()
|
|
got = got[:0]
|
|
for _, hf := range tt.hdrs {
|
|
if err := e.WriteField(hf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_, err := d.Write(buf.Bytes())
|
|
if err != nil {
|
|
t.Errorf("%d. Decoder Write = %v", i, err)
|
|
}
|
|
if !reflect.DeepEqual(got, tt.hdrs) {
|
|
t.Errorf("%d. Decoded %+v; want %+v", i, got, tt.hdrs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncoderSearchTable(t *testing.T) {
|
|
e := NewEncoder(nil)
|
|
|
|
e.dynTab.add(pair("foo", "bar"))
|
|
e.dynTab.add(pair("blake", "miz"))
|
|
e.dynTab.add(pair(":method", "GET"))
|
|
|
|
tests := []struct {
|
|
hf HeaderField
|
|
wantI uint64
|
|
wantMatch bool
|
|
}{
|
|
// Name and Value match
|
|
{pair("foo", "bar"), uint64(staticTable.len()) + 3, true},
|
|
{pair("blake", "miz"), uint64(staticTable.len()) + 2, true},
|
|
{pair(":method", "GET"), 2, true},
|
|
|
|
// Only name match because Sensitive == true. This is allowed to match
|
|
// any ":method" entry. The current implementation uses the last entry
|
|
// added in newStaticTable.
|
|
{HeaderField{":method", "GET", true}, 3, false},
|
|
|
|
// Only Name matches
|
|
{pair("foo", "..."), uint64(staticTable.len()) + 3, false},
|
|
{pair("blake", "..."), uint64(staticTable.len()) + 2, false},
|
|
// As before, this is allowed to match any ":method" entry.
|
|
{pair(":method", "..."), 3, false},
|
|
|
|
// None match
|
|
{pair("foo-", "bar"), 0, false},
|
|
}
|
|
for _, tt := range tests {
|
|
if gotI, gotMatch := e.searchTable(tt.hf); gotI != tt.wantI || gotMatch != tt.wantMatch {
|
|
t.Errorf("d.search(%+v) = %v, %v; want %v, %v", tt.hf, gotI, gotMatch, tt.wantI, tt.wantMatch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendVarInt(t *testing.T) {
|
|
tests := []struct {
|
|
n byte
|
|
i uint64
|
|
want []byte
|
|
}{
|
|
// Fits in a byte:
|
|
{1, 0, []byte{0}},
|
|
{2, 2, []byte{2}},
|
|
{3, 6, []byte{6}},
|
|
{4, 14, []byte{14}},
|
|
{5, 30, []byte{30}},
|
|
{6, 62, []byte{62}},
|
|
{7, 126, []byte{126}},
|
|
{8, 254, []byte{254}},
|
|
|
|
// Multiple bytes:
|
|
{5, 1337, []byte{31, 154, 10}},
|
|
}
|
|
for _, tt := range tests {
|
|
got := appendVarInt(nil, tt.n, tt.i)
|
|
if !bytes.Equal(got, tt.want) {
|
|
t.Errorf("appendVarInt(nil, %v, %v) = %v; want %v", tt.n, tt.i, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendHpackString(t *testing.T) {
|
|
tests := []struct {
|
|
s, wantHex string
|
|
}{
|
|
// Huffman encoded
|
|
{"www.example.com", "8c f1e3 c2e5 f23a 6ba0 ab90 f4ff"},
|
|
|
|
// Not Huffman encoded
|
|
{"a", "01 61"},
|
|
|
|
// zero length
|
|
{"", "00"},
|
|
}
|
|
for _, tt := range tests {
|
|
want := removeSpace(tt.wantHex)
|
|
buf := appendHpackString(nil, tt.s)
|
|
if got := hex.EncodeToString(buf); want != got {
|
|
t.Errorf("appendHpackString(nil, %q) = %q; want %q", tt.s, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendIndexed(t *testing.T) {
|
|
tests := []struct {
|
|
i uint64
|
|
wantHex string
|
|
}{
|
|
// 1 byte
|
|
{1, "81"},
|
|
{126, "fe"},
|
|
|
|
// 2 bytes
|
|
{127, "ff00"},
|
|
{128, "ff01"},
|
|
}
|
|
for _, tt := range tests {
|
|
want := removeSpace(tt.wantHex)
|
|
buf := appendIndexed(nil, tt.i)
|
|
if got := hex.EncodeToString(buf); want != got {
|
|
t.Errorf("appendIndex(nil, %v) = %q; want %q", tt.i, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendNewName(t *testing.T) {
|
|
tests := []struct {
|
|
f HeaderField
|
|
indexing bool
|
|
wantHex string
|
|
}{
|
|
// Incremental indexing
|
|
{HeaderField{"custom-key", "custom-value", false}, true, "40 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"},
|
|
|
|
// Without indexing
|
|
{HeaderField{"custom-key", "custom-value", false}, false, "00 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"},
|
|
|
|
// Never indexed
|
|
{HeaderField{"custom-key", "custom-value", true}, true, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"},
|
|
{HeaderField{"custom-key", "custom-value", true}, false, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"},
|
|
}
|
|
for _, tt := range tests {
|
|
want := removeSpace(tt.wantHex)
|
|
buf := appendNewName(nil, tt.f, tt.indexing)
|
|
if got := hex.EncodeToString(buf); want != got {
|
|
t.Errorf("appendNewName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendIndexedName(t *testing.T) {
|
|
tests := []struct {
|
|
f HeaderField
|
|
i uint64
|
|
indexing bool
|
|
wantHex string
|
|
}{
|
|
// Incremental indexing
|
|
{HeaderField{":status", "302", false}, 8, true, "48 82 6402"},
|
|
|
|
// Without indexing
|
|
{HeaderField{":status", "302", false}, 8, false, "08 82 6402"},
|
|
|
|
// Never indexed
|
|
{HeaderField{":status", "302", true}, 8, true, "18 82 6402"},
|
|
{HeaderField{":status", "302", true}, 8, false, "18 82 6402"},
|
|
}
|
|
for _, tt := range tests {
|
|
want := removeSpace(tt.wantHex)
|
|
buf := appendIndexedName(nil, tt.f, tt.i, tt.indexing)
|
|
if got := hex.EncodeToString(buf); want != got {
|
|
t.Errorf("appendIndexedName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendTableSize(t *testing.T) {
|
|
tests := []struct {
|
|
i uint32
|
|
wantHex string
|
|
}{
|
|
// Fits into 1 byte
|
|
{30, "3e"},
|
|
|
|
// Extra byte
|
|
{31, "3f00"},
|
|
{32, "3f01"},
|
|
}
|
|
for _, tt := range tests {
|
|
want := removeSpace(tt.wantHex)
|
|
buf := appendTableSize(nil, tt.i)
|
|
if got := hex.EncodeToString(buf); want != got {
|
|
t.Errorf("appendTableSize(nil, %v) = %q; want %q", tt.i, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncoderSetMaxDynamicTableSize(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
e := NewEncoder(&buf)
|
|
tests := []struct {
|
|
v uint32
|
|
wantUpdate bool
|
|
wantMinSize uint32
|
|
wantMaxSize uint32
|
|
}{
|
|
// Set new table size to 2048
|
|
{2048, true, 2048, 2048},
|
|
|
|
// Set new table size to 16384, but still limited to
|
|
// 4096
|
|
{16384, true, 2048, 4096},
|
|
}
|
|
for _, tt := range tests {
|
|
e.SetMaxDynamicTableSize(tt.v)
|
|
if got := e.tableSizeUpdate; tt.wantUpdate != got {
|
|
t.Errorf("e.tableSizeUpdate = %v; want %v", got, tt.wantUpdate)
|
|
}
|
|
if got := e.minSize; tt.wantMinSize != got {
|
|
t.Errorf("e.minSize = %v; want %v", got, tt.wantMinSize)
|
|
}
|
|
if got := e.dynTab.maxSize; tt.wantMaxSize != got {
|
|
t.Errorf("e.maxSize = %v; want %v", got, tt.wantMaxSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncoderSetMaxDynamicTableSizeLimit(t *testing.T) {
|
|
e := NewEncoder(nil)
|
|
// 4095 < initialHeaderTableSize means maxSize is truncated to
|
|
// 4095.
|
|
e.SetMaxDynamicTableSizeLimit(4095)
|
|
if got, want := e.dynTab.maxSize, uint32(4095); got != want {
|
|
t.Errorf("e.dynTab.maxSize = %v; want %v", got, want)
|
|
}
|
|
if got, want := e.maxSizeLimit, uint32(4095); got != want {
|
|
t.Errorf("e.maxSizeLimit = %v; want %v", got, want)
|
|
}
|
|
if got, want := e.tableSizeUpdate, true; got != want {
|
|
t.Errorf("e.tableSizeUpdate = %v; want %v", got, want)
|
|
}
|
|
// maxSize will be truncated to maxSizeLimit
|
|
e.SetMaxDynamicTableSize(16384)
|
|
if got, want := e.dynTab.maxSize, uint32(4095); got != want {
|
|
t.Errorf("e.dynTab.maxSize = %v; want %v", got, want)
|
|
}
|
|
// 8192 > current maxSizeLimit, so maxSize does not change.
|
|
e.SetMaxDynamicTableSizeLimit(8192)
|
|
if got, want := e.dynTab.maxSize, uint32(4095); got != want {
|
|
t.Errorf("e.dynTab.maxSize = %v; want %v", got, want)
|
|
}
|
|
if got, want := e.maxSizeLimit, uint32(8192); got != want {
|
|
t.Errorf("e.maxSizeLimit = %v; want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func removeSpace(s string) string {
|
|
return strings.Replace(s, " ", "", -1)
|
|
}
|
|
|
|
func BenchmarkEncoderSearchTable(b *testing.B) {
|
|
e := NewEncoder(nil)
|
|
|
|
// A sample of possible header fields.
|
|
// This is not based on any actual data from HTTP/2 traces.
|
|
var possible []HeaderField
|
|
for _, f := range staticTable.ents {
|
|
if f.Value == "" {
|
|
possible = append(possible, f)
|
|
continue
|
|
}
|
|
// Generate 5 random values, except for cookie and set-cookie,
|
|
// which we know can have many values in practice.
|
|
num := 5
|
|
if f.Name == "cookie" || f.Name == "set-cookie" {
|
|
num = 25
|
|
}
|
|
for i := 0; i < num; i++ {
|
|
f.Value = fmt.Sprintf("%s-%d", f.Name, i)
|
|
possible = append(possible, f)
|
|
}
|
|
}
|
|
for k := 0; k < 10; k++ {
|
|
f := HeaderField{
|
|
Name: fmt.Sprintf("x-header-%d", k),
|
|
Sensitive: rand.Int()%2 == 0,
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
f.Value = fmt.Sprintf("%s-%d", f.Name, i)
|
|
possible = append(possible, f)
|
|
}
|
|
}
|
|
|
|
// Add a random sample to the dynamic table. This very loosely simulates
|
|
// a history of 100 requests with 20 header fields per request.
|
|
for r := 0; r < 100*20; r++ {
|
|
f := possible[rand.Int31n(int32(len(possible)))]
|
|
// Skip if this is in the staticTable verbatim.
|
|
if _, has := staticTable.search(f); !has {
|
|
e.dynTab.add(f)
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for n := 0; n < b.N; n++ {
|
|
for _, f := range possible {
|
|
e.searchTable(f)
|
|
}
|
|
}
|
|
}
|