Skip to content

Commit 125c4d0

Browse files
afrindfacebook-github-bot
authored andcommitted
Improve encoder eviction algorithm and datastructure use
Summary: The QPACK encoder needs to track what entries in its table are possibly in use by the peer to prevent evicting them too early. The existing code keeps a reference count for every header in the table, and only evicts when the ref count is 0. When sending a block referencing tht table, the encoder keeps a list of the absolute table indexes that block is referencing, so it can do the accounting. This is expensive in both space and time, and unecessary. We only need to know the minimum value in the table that is referenced, since eviction is in FIFO order. So we get rid of the (relatively large) refcount table and just keep a minInUseIndex. The encoder only needs to track the min index for each outstanding block. The trick is computing the new min efficiently after a header acknowledgement or reset. Since std::heap functions do not provide a function for removal of an arbitrary element, QPACKEncoder keeps a vector of mins, and the min-min. Insert (encoding) is O(1). Removing on ack is O(n), where n is very commonly *at most* 100 and frequently much-much smaller, and the constant factor is vector traversal. The removal and recomputation of the min is handled in one pass. Reviewed By: jbeshay Differential Revision: D56500544 fbshipit-source-id: 476024c43dd3d11d86d60ef76b9773052cad5ece
1 parent a17d7f7 commit 125c4d0

File tree

6 files changed

+95
-98
lines changed

6 files changed

+95
-98
lines changed

proxygen/lib/http/codec/compress/QPACKEncoder.cpp

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,19 @@ std::unique_ptr<folly::IOBuf> QPACKEncoder::completeEncode(
8484
streamBuffer->prependChain(std::move(streamBlock));
8585
}
8686

87-
// curOutstanding_.references could be empty, if the block encodes only static
88-
// headers and/or literals. If so we don't track anything.
89-
if (!curOutstanding_.references.empty()) {
87+
// curOutstanding_.minInUseIndex could be max, if the block encodes only
88+
// static headers and/or literals. If so we don't track anything.
89+
if (curOutstanding_.minInUseIndex != std::numeric_limits<uint32_t>::max()) {
90+
outstandingMins_.push_back(curOutstanding_.minInUseIndex);
9091
if (curOutstanding_.vulnerable) {
9192
DCHECK(allowVulnerable());
9293
numVulnerable_++;
9394
}
9495
numOutstandingBlocks_++;
9596
outstanding_[streamId].emplace_back(std::move(curOutstanding_));
9697
curOutstanding_.vulnerable = false;
98+
curOutstanding_.minInUseIndex = std::numeric_limits<uint32_t>::max();
99+
curOutstanding_.maxInUseIndex = 0;
97100
}
98101

99102
controlBuffer_.setWriteBuf(nullptr);
@@ -275,14 +278,17 @@ void QPACKEncoder::trackReference(uint32_t absoluteIndex,
275278
CHECK_NE(absoluteIndex, 0);
276279
if (absoluteIndex > requiredInsertCount) {
277280
requiredInsertCount = absoluteIndex;
281+
curOutstanding_.maxInUseIndex = requiredInsertCount;
278282
if (table_.isVulnerable(absoluteIndex)) {
279283
curOutstanding_.vulnerable = true;
280284
}
281285
}
282-
auto res = curOutstanding_.references.insert(absoluteIndex);
283-
if (res.second) {
284-
VLOG(5) << "Bumping refcount for absoluteIndex=" << absoluteIndex;
285-
table_.addRef(absoluteIndex);
286+
if (absoluteIndex < curOutstanding_.minInUseIndex) {
287+
curOutstanding_.minInUseIndex = absoluteIndex;
288+
minOutstandingMin_ =
289+
std::min(minOutstandingMin_, curOutstanding_.minInUseIndex);
290+
// Update table min here to prevent new encodes from evicting reference
291+
table_.setMinInUseIndex(minOutstandingMin_);
286292
}
287293
}
288294

@@ -437,44 +443,60 @@ HPACK::DecodeError QPACKEncoder::onHeaderAck(uint64_t streamId, bool all) {
437443
VLOG(5) << ((all) ? "onCancelStream" : "onHeaderAck")
438444
<< " streamId=" << streamId;
439445
if (all) {
440-
// Happens when a stream is reset
446+
// Happens when a stream is reset (should be rare)
441447
for (auto& block : it->second) {
442-
for (auto i : block.references) {
443-
VLOG(5) << "Decrementing refcount for absoluteIndex=" << i;
444-
table_.subRef(i);
445-
}
446448
if (block.vulnerable) {
447449
numVulnerable_--;
448450
}
451+
removeFromMinOutstanding(block.minInUseIndex);
449452
}
450453
numOutstandingBlocks_ -= it->second.size();
451454
it->second.clear();
452455
} else {
453456
auto block = std::move(it->second.front());
454457
numOutstandingBlocks_--;
455458
it->second.pop_front();
456-
// a different stream, sub all the references
457-
for (auto i : block.references) {
458-
VLOG(5) << "Decrementing refcount for absoluteIndex=" << i;
459-
table_.subRef(i);
460-
}
461459
if (block.vulnerable) {
462460
numVulnerable_--;
463461
}
464-
// requiredInsertCount is implicitly acknowledged
465-
if (!block.references.empty()) {
466-
auto requiredInsertCount = *block.references.rbegin();
467-
VLOG(5) << "Implicitly acknowledging requiredInsertCount="
468-
<< requiredInsertCount;
469-
table_.setAcknowledgedInsertCount(requiredInsertCount);
470-
}
462+
CHECK_NE(block.minInUseIndex, std::numeric_limits<uint32_t>::max());
463+
removeFromMinOutstanding(block.minInUseIndex);
464+
// Up through maxInUseIndex is implicitly acknowledged
465+
VLOG(5) << "Implicitly acknowledging requiredInsertCount="
466+
<< block.maxInUseIndex;
467+
table_.setAcknowledgedInsertCount(block.maxInUseIndex);
471468
}
472469
if (it->second.empty()) {
473470
outstanding_.erase(it);
474471
}
472+
VLOG(6) << "New min use index=" << minOutstandingMin_;
473+
table_.setMinInUseIndex(minOutstandingMin_);
475474
return HPACK::DecodeError::NONE;
476475
}
477476

477+
void QPACKEncoder::removeFromMinOutstanding(uint32_t valToRemove) {
478+
CHECK(!outstandingMins_.empty());
479+
VLOG(10) << "mins remove val=" << valToRemove;
480+
bool recomputeMin = (valToRemove == minOutstandingMin_);
481+
uint32_t newMin = std::numeric_limits<uint32_t>::max();
482+
size_t i = 0;
483+
for (; i < outstandingMins_.size(); i++) {
484+
auto value = outstandingMins_.at(i);
485+
if (value == valToRemove) {
486+
outstandingMins_.at(i) = outstandingMins_.back();
487+
outstandingMins_.pop_back();
488+
break;
489+
}
490+
newMin = std::min(newMin, value);
491+
}
492+
if (recomputeMin) {
493+
for (; i < outstandingMins_.size(); i++) {
494+
newMin = std::min(newMin, outstandingMins_.at(i));
495+
}
496+
minOutstandingMin_ = newMin;
497+
}
498+
}
499+
478500
void QPACKEncoder::setMaxNumOutstandingBlocks(uint32_t value) {
479501
maxNumOutstandingBlocks_ = value;
480502
}

proxygen/lib/http/codec/compress/QPACKEncoder.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,18 @@ class QPACKEncoder
170170
return maxEncoderStreamBytes_ >= 0;
171171
}
172172

173+
void removeFromMinOutstanding(uint32_t val);
174+
173175
HPACKEncodeBuffer controlBuffer_;
174-
using BlockReferences = std::set<uint32_t>;
175176
struct OutstandingBlock {
176-
BlockReferences references;
177+
uint32_t minInUseIndex{std::numeric_limits<uint32_t>::max()};
178+
uint32_t maxInUseIndex{0};
177179
bool vulnerable{false};
178180
};
179181
// Map streamID -> list of table index references for each outstanding block;
180182
std::unordered_map<uint64_t, std::list<OutstandingBlock>> outstanding_;
183+
std::vector<uint32_t> outstandingMins_;
184+
uint32_t minOutstandingMin_{std::numeric_limits<uint32_t>::max()};
181185
OutstandingBlock curOutstanding_;
182186
uint32_t maxDepends_{0};
183187
uint32_t maxVulnerable_{HPACK::kDefaultBlocking};

proxygen/lib/http/codec/compress/QPACKHeaderTable.cpp

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ namespace proxygen {
3434
QPACKHeaderTable::QPACKHeaderTable(uint32_t capacityVal, bool trackReferences)
3535
: HeaderTable(capacityVal) {
3636
if (trackReferences) {
37-
refCount_ = std::make_unique<std::vector<uint16_t>>(table_.size(), 0);
3837
minFree_ = getMinFree(capacityVal);
38+
trackReferences_ = true;
3939
} else {
4040
minFree_ = 0;
4141
disableNamesIndex();
@@ -49,13 +49,10 @@ bool QPACKHeaderTable::add(HPACKHeader header) {
4949
return false;
5050
}
5151

52-
DVLOG(6) << "Adding header=" << header;
52+
DVLOG(6) << "Adding header=" << header << " absIndex=" << insertCount_ + 1;
5353
if (!HeaderTable::add(std::move(header))) {
5454
return false;
5555
}
56-
if (refCount_) {
57-
(*refCount_)[head_] = 0;
58-
}
5956
DCHECK_EQ(internalToAbsolute(head_), insertCount_);
6057
// Increase minUsable_ until the free space + drainedBytes is >= minFree.
6158
// For HPACK, minFree is 0 and this is a no-op.
@@ -74,7 +71,7 @@ bool QPACKHeaderTable::setCapacity(uint32_t capacity) {
7471
if (!HeaderTable::setCapacity(capacity)) {
7572
return false;
7673
}
77-
if (refCount_) {
74+
if (trackReferences_) {
7875
minFree_ = getMinFree(capacity);
7976
} // else minFree is always 0
8077
return true;
@@ -135,8 +132,9 @@ const HPACKHeader& QPACKHeaderTable::getHeader(uint32_t index,
135132

136133
uint32_t QPACKHeaderTable::removeLast() {
137134
auto idx = tail();
138-
if (refCount_) {
139-
CHECK_EQ((*refCount_)[idx], 0) << "Removed header with nonzero references";
135+
if (trackReferences_) {
136+
CHECK_LT(internalToAbsolute(idx), minInUseIndex_)
137+
<< "Removed in use header";
140138
}
141139
auto removedBytes = HeaderTable::removeLast();
142140
// Only non-zero when minUsable_ > insertCount_ - size_.
@@ -165,24 +163,6 @@ void QPACKHeaderTable::increaseTableLengthTo(uint32_t newLength) {
165163
}
166164
}
167165

168-
void QPACKHeaderTable::resizeTable(uint32_t newLength) {
169-
HeaderTable::resizeTable(newLength);
170-
if (refCount_) {
171-
refCount_->resize(newLength);
172-
}
173-
}
174-
175-
void QPACKHeaderTable::updateResizedTable(uint32_t oldTail,
176-
uint32_t oldLength,
177-
uint32_t newLength) {
178-
HeaderTable::updateResizedTable(oldTail, oldLength, newLength);
179-
if (refCount_) {
180-
std::move_backward(refCount_->begin() + oldTail,
181-
refCount_->begin() + oldLength,
182-
refCount_->begin() + newLength);
183-
}
184-
}
185-
186166
uint32_t QPACKHeaderTable::evict(uint32_t needed, uint32_t desiredCapacity) {
187167
if (bytes_ + needed < desiredCapacity ||
188168
!canEvict(bytes_ + needed - desiredCapacity)) {
@@ -192,21 +172,22 @@ uint32_t QPACKHeaderTable::evict(uint32_t needed, uint32_t desiredCapacity) {
192172
}
193173

194174
bool QPACKHeaderTable::canEvict(uint32_t needed) {
195-
if (size_ == 0 || !refCount_) {
175+
if (size_ == 0 || !trackReferences_) {
196176
return needed <= capacity_;
197177
}
198178
uint32_t freeable = 0;
199179
uint32_t i = tail();
200180
uint32_t nChecked = 0;
201181
while (nChecked++ < size() && freeable < needed &&
202-
((*refCount_)[i] == 0) && // don't evict referenced or unacked headers
182+
internalToAbsolute(i) < minInUseIndex_ && // don't evict referenced or
183+
// unacked headers
203184
internalToAbsolute(i) <= ackedInsertCount_) {
204185
freeable += table_[i].bytes();
205186
i = next(i);
206187
}
207188
if (freeable < needed) {
208189
DVLOG(5) << "header=" << table_[i].name << ":" << table_[i].value
209-
<< " blocked eviction, recount=" << (*refCount_)[i];
190+
<< " blocked eviction, minInUseIndex_=" << minInUseIndex_;
210191
return false;
211192
}
212193
return true;
@@ -253,20 +234,6 @@ std::pair<bool, uint32_t> QPACKHeaderTable::maybeDuplicate(
253234
return {false, absIndex};
254235
}
255236

256-
void QPACKHeaderTable::addRef(uint32_t absIndex) {
257-
// refCount is 16 bits. It should really never get this big in practice,
258-
// unless a decoder is not sending HEADER_ACK in a timely way.
259-
CHECK(refCount_);
260-
(*refCount_)[absoluteToInternal(absIndex)]++;
261-
}
262-
263-
void QPACKHeaderTable::subRef(uint32_t absIndex) {
264-
CHECK(refCount_);
265-
uint32_t index = absoluteToInternal(absIndex);
266-
CHECK_GT((*refCount_)[index], 0);
267-
(*refCount_)[index]--;
268-
}
269-
270237
// Converts an array index in [0..table_.size() - 1] to an absolute
271238
// external index
272239
uint32_t QPACKHeaderTable::internalToAbsolute(uint32_t internalIndex) const {

proxygen/lib/http/codec/compress/QPACKHeaderTable.h

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,10 @@ class QPACKHeaderTable : public HeaderTable {
161161
return insertCount_ - absIndex + 1;
162162
}
163163

164-
/**
165-
* Add a reference for the given index. Entries with non-zero references
166-
* cannot be evicted
167-
*/
168-
void addRef(uint32_t absIndex);
169-
170-
/**
171-
* Subtract a reference for the given index
172-
*/
173-
void subRef(uint32_t absIndex);
164+
void setMinInUseIndex(
165+
uint32_t minInUseIndex = std::numeric_limits<uint32_t>::max()) {
166+
minInUseIndex_ = minInUseIndex;
167+
}
174168

175169
private:
176170
/*
@@ -186,11 +180,6 @@ class QPACKHeaderTable : public HeaderTable {
186180
*/
187181
void increaseTableLengthTo(uint32_t newLength) override;
188182

189-
void resizeTable(uint32_t newLength) override;
190-
191-
void updateResizedTable(uint32_t oldTail,
192-
uint32_t oldLength,
193-
uint32_t newLength) override;
194183
/**
195184
* Removes one header entry from the beginning of the header table.
196185
*/
@@ -218,7 +207,8 @@ class QPACKHeaderTable : public HeaderTable {
218207
uint32_t minUsable_{1};
219208
uint32_t ackedInsertCount_{0};
220209
uint32_t minFree_{0};
221-
std::unique_ptr<std::vector<uint16_t>> refCount_;
210+
bool trackReferences_{false};
211+
uint32_t minInUseIndex_{std::numeric_limits<uint32_t>::max()};
222212
};
223213

224214
} // namespace proxygen

proxygen/lib/http/codec/compress/test/QPACKContextTests.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,27 @@ TEST(QPACKContextTests, TestAcks) {
484484
EXPECT_EQ(encoder.onInsertCountIncrement(0), HPACK::DecodeError::INVALID_ACK);
485485
}
486486

487+
TEST(QPACKContextTests, TestEncodeBlocksSelfEviction) {
488+
// The references already made in an encode prevent eviction even before
489+
// finishing the encode.
490+
QPACKEncoder encoder(false, 192); // min free 48
491+
QPACKDecoder decoder(192);
492+
493+
vector<HPACKHeader> req;
494+
req.emplace_back("aaaa", "xxxxxxxxxxxx"); // 48
495+
auto result = encoder.encode(req, 0, 1);
496+
verifyDecode(decoder, std::move(result), req);
497+
EXPECT_EQ(encoder.onHeaderAck(1, false), HPACK::DecodeError::NONE);
498+
499+
req.emplace_back("bbbb", "xxxxxxxxxxxx"); // 48
500+
req.emplace_back("cccc", "xxxxxxxxxxxxx"); // 49, drains A
501+
req.emplace_back("dddd", "xxxxxxxxxxxxx"); // 48, not enough space
502+
result = encoder.encode(req, 0, 1);
503+
EXPECT_FALSE(stringInOutput(result.stream.get(), "aaaa"));
504+
EXPECT_TRUE(stringInOutput(result.stream.get(), "dddd"));
505+
verifyDecode(decoder, std::move(result), req);
506+
}
507+
487508
TEST(QPACKContextTests, TestImplicitAcks) {
488509
QPACKEncoder encoder(false, 1024);
489510
QPACKDecoder decoder(1024);

proxygen/lib/http/codec/compress/test/QPACKHeaderTableTests.cpp

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,12 @@ TEST_F(QPACKHeaderTableTests, Eviction) {
5050
for (auto i = 0; i < max; i++) {
5151
EXPECT_TRUE(table_.add(accept.copy()));
5252
}
53-
for (auto i = 1; i <= max; i++) {
54-
table_.addRef(i);
55-
}
53+
table_.setMinInUseIndex(1);
5654
table_.setAcknowledgedInsertCount(max);
5755
EXPECT_FALSE(table_.canIndex(accept.name, accept.value));
5856
EXPECT_FALSE(table_.add(accept.copy()));
59-
table_.subRef(1);
60-
EXPECT_TRUE(table_.canIndex(accept.name, accept.value));
61-
EXPECT_TRUE(table_.add(accept.copy()));
6257

63-
table_.subRef(3);
64-
EXPECT_FALSE(table_.canIndex(accept.name, accept.value));
65-
table_.subRef(2);
58+
table_.setMinInUseIndex(std::numeric_limits<uint32_t>::max());
6659
EXPECT_TRUE(table_.canIndex(accept.name, accept.value));
6760
}
6861

@@ -82,11 +75,11 @@ TEST_F(QPACKHeaderTableTests, BadEviction) {
8275

8376
// Ack all headers but mark the first as in use
8477
table_.setAcknowledgedInsertCount(max);
85-
table_.addRef(1);
78+
table_.setMinInUseIndex(1);
8679
EXPECT_FALSE(table_.setCapacity(capacity / 2));
8780

8881
// Clear all refs
89-
table_.subRef(1);
82+
table_.setMinInUseIndex(std::numeric_limits<uint32_t>::max());
9083
EXPECT_TRUE(table_.setCapacity(capacity / 2));
9184
EXPECT_EQ(table_.size(), max / 2);
9285
}
@@ -167,7 +160,7 @@ TEST_F(QPACKHeaderTableTests, Duplication) {
167160

168161
// Hold a ref to oldest entry, prevents eviction
169162
auto oldestAbsolute = table_.getInsertCount() - table_.size() + 1;
170-
table_.addRef(oldestAbsolute);
163+
table_.setMinInUseIndex(oldestAbsolute);
171164

172165
// Table should be full
173166
EXPECT_FALSE(table_.canIndex(accept.name, accept.value));
@@ -186,7 +179,7 @@ TEST_F(QPACKHeaderTableTests, CanEvictWithRoom) {
186179
table_.setAcknowledgedInsertCount(table_.getInsertCount());
187180
// abs index = 1 is evictable, but index = 2 is referenced, so we can
188181
// insert up to (320 - 8 * 39) + 39 = 47
189-
table_.addRef(2);
182+
table_.setMinInUseIndex(2);
190183
EXPECT_TRUE(table_.canIndex(fortySevenBytes.name, fortySevenBytes.value));
191184
EXPECT_TRUE(table_.add(fortySevenBytes.copy()));
192185
}

0 commit comments

Comments
 (0)