Skip to content

Port performance optimizations to speed up reading large collections from Android #2140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 45 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f09f0c1
No double encode
var-const Nov 28, 2018
c93bf47
Eliminate FSTMaybeDocumentDictionary
var-const Nov 29, 2018
884de74
Remove some FSTDocumentKey usages
var-const Nov 29, 2018
04df500
Leftovers
var-const Nov 29, 2018
628dd45
Main part compiles, on to tests
var-const Nov 30, 2018
5e0c6ed
Compiling tests wip
var-const Nov 30, 2018
3a05d5f
Unit tests compile
var-const Nov 30, 2018
c6074b6
Integration tests compile
var-const Nov 30, 2018
c9e1692
Eliminate 90% of FSTDocumentKey
var-const Nov 30, 2018
21c37e8
All tests pass
var-const Dec 1, 2018
7aaf87d
style.sh
var-const Dec 1, 2018
2e20327
TODO
var-const Dec 1, 2018
b1d050f
Do todo
var-const Dec 1, 2018
4d86e24
Port tests
var-const Dec 1, 2018
abbd403
Initial
var-const Dec 2, 2018
560ef0f
style.sh
var-const Dec 2, 2018
dade8c3
Merge branch 'master' into varconst/eliminate-fst-maybe-document-dict…
var-const Dec 2, 2018
afca380
Small fixes
var-const Dec 2, 2018
f345746
Merge branch 'master' into varconst/port-android-1000-reads
var-const Dec 2, 2018
958b739
Merge branch 'varconst/eliminate-fst-maybe-document-dictionary' into …
var-const Dec 2, 2018
96eba87
style.sh
var-const Dec 2, 2018
207ba72
Undo style.sh
var-const Dec 2, 2018
2b7a9ab
Remove spurious diff
var-const Dec 2, 2018
e14fe58
Comment
var-const Dec 2, 2018
83ea0a0
Review feedback
var-const Dec 5, 2018
c22879c
Review feedback
var-const Dec 5, 2018
005fbe5
Review feedback
var-const Dec 5, 2018
99ca679
Review feedback
var-const Dec 6, 2018
595e2c1
Comments
var-const Dec 6, 2018
4252fee
Fix silly bug
var-const Dec 6, 2018
229515f
Undo
var-const Dec 6, 2018
b9f0188
Merge branch 'varconst/eliminate-fst-maybe-document-dictionary' into …
var-const Dec 6, 2018
e48b1cb
DocumentMap take 2 wip
var-const Dec 10, 2018
40a03d8
DocumentMap take 2 compiles
var-const Dec 10, 2018
cca3650
style.sh
var-const Dec 10, 2018
5a2cd65
Fix unit tests
var-const Dec 10, 2018
1c1a102
Comments
var-const Dec 10, 2018
5cdd10e
No iterator?
var-const Dec 10, 2018
ab023b0
Add to CMake
var-const Dec 10, 2018
aedad29
Merge branch 'varconst/eliminate-fst-maybe-document-dictionary' into …
var-const Dec 10, 2018
9f069f2
Revert accidentally formatted files
var-const Dec 10, 2018
2886900
Merge branch 'varconst/eliminate-fst-maybe-document-dictionary' into …
var-const Dec 10, 2018
099e99e
Merge branch 'master' into varconst/port-android-1000-reads
var-const Dec 10, 2018
69066b4
Undo schema change
var-const Dec 10, 2018
5481ac4
Fix CMake build
var-const Dec 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ - (void)testSetAndReadADocument {
[self setAndReadADocumentAtPath:kDocPath];
}

- (void)testSetAndReadSeveralDocuments {
if (!self.remoteDocumentCache) return;

self.persistence.run("testSetAndReadSeveralDocuments", [=]() {
NSArray<FSTDocument *> *written =
@[ [self setTestDocumentAtPath:kDocPath], [self setTestDocumentAtPath:kLongDocPath] ];
MaybeDocumentMap read = [self.remoteDocumentCache
entriesForKeys:DocumentKeySet{testutil::Key(kDocPath), testutil::Key(kLongDocPath)}];
[self expectMap:read hasDocsInArray:written exactly:YES];
});
}

- (void)testSetAndReadSeveralDocumentsIncludingMissingDocument {
if (!self.remoteDocumentCache) return;

self.persistence.run("testSetAndReadSeveralDocumentsIncludingMissingDocument", [=]() {
NSArray<FSTDocument *> *written =
@[ [self setTestDocumentAtPath:kDocPath], [self setTestDocumentAtPath:kLongDocPath] ];
MaybeDocumentMap read =
[self.remoteDocumentCache entriesForKeys:DocumentKeySet{
testutil::Key(kDocPath),
testutil::Key(kLongDocPath),
testutil::Key("foo/nonexistent"),
}];
[self expectMap:read hasDocsInArray:written exactly:NO];
auto found = read.find(DocumentKey::FromPathString("foo/nonexistent"));
XCTAssertTrue(found != read.end());
XCTAssertNil(found->second);
});
}

- (void)testSetAndReadADocumentAtDeepPath {
if (!self.remoteDocumentCache) return;

Expand Down Expand Up @@ -144,7 +175,7 @@ - (void)testDocumentsMatchingQuery {

FSTQuery *query = FSTTestQuery("b");
DocumentMap results = [self.remoteDocumentCache documentsMatchingQuery:query];
[self expectMap:results
[self expectMap:results.underlying_map()
hasDocsInArray:@[
FSTTestDoc("b/1", kVersion, _kDocData, FSTDocumentStateSynced),
FSTTestDoc("b/2", kVersion, _kDocData, FSTDocumentStateSynced)
Expand All @@ -162,16 +193,16 @@ - (FSTDocument *)setTestDocumentAtPath:(const absl::string_view)path {
return doc;
}

- (void)expectMap:(const DocumentMap &)map
- (void)expectMap:(const MaybeDocumentMap &)map
hasDocsInArray:(NSArray<FSTDocument *> *)expected
exactly:(BOOL)exactly {
if (exactly) {
XCTAssertEqual(map.size(), [expected count]);
}
for (FSTDocument *doc in expected) {
FSTDocument *actual = nil;
auto found = map.underlying_map().find(doc.key);
if (found != map.underlying_map().end()) {
auto found = map.find(doc.key);
if (found != map.end()) {
actual = static_cast<FSTDocument *>(found->second);
}
XCTAssertEqualObjects(actual, doc);
Expand Down
18 changes: 18 additions & 0 deletions Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ - (nullable FSTMaybeDocument *)entryForKey:(const DocumentKey &)documentKey {
}
}

- (MaybeDocumentMap)entriesForKeys:(const DocumentKeySet &)keys {
MaybeDocumentMap results;

LevelDbRemoteDocumentKey currentKey;
auto it = _db.currentTransaction->NewIterator();

for (const DocumentKey &key : keys) {
it->Seek([self remoteDocumentKey:key]);
if (!it->Valid() || !currentKey.Decode(it->key()) || currentKey.document_key() != key) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our docs claim that if currentKey.Decode() fails, then the instance is left in an undefined state (until the next call to Decode()). So although this probably works, the next call to document_key() on line 94 isn't guaranteed to do what you want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify when decode key will fail? If we believe it only fails when something corrupts, I'd prefer let the code abort rather than continue and hide until later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rich, thanks for spotting it. I removed that line, so this is no longer applicable.

Zhi, my understanding is this: Seek will return an iterator that points either at the key that is being sought, or the first key that is larger. However, in leveldb all the keys are stored in a single table, so it's possible that Seek will return a key from the next "logical" table. For example, imagine that keys go like this:

remote_documents/foo
remote_documents/bar
write_batches/spam

and we're seeking for remote_documents/zzz. Seek will return an iterator to write_batches/spam, which is from a different logical table (by logical, I mean that Firestore uses prefixes like "remote_documents" to treat leveldb as if it had several tables). Then LevelDbRemoteDocumentKey will fail to Decode because it expects table name remote_documents, but it's actually write_batches.

results = results.insert(key, nil);
} else {
results = results.insert(key, [self decodeMaybeDocument:it->value() withKey:key]);
}
}

return results;
}

- (DocumentMap)documentsMatchingQuery:(FSTQuery *)query {
DocumentMap results;

Expand Down
7 changes: 7 additions & 0 deletions Firestore/Source/Local/FSTLocalDocumentsView.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ NS_ASSUME_NONNULL_BEGIN
- (firebase::firestore::model::MaybeDocumentMap)documentsForKeys:
(const firebase::firestore::model::DocumentKeySet &)keys;

/**
* Similar to `documentsForKeys`, but creates the local view from the given
* `baseDocs` without retrieving documents from the local store.
*/
- (firebase::firestore::model::MaybeDocumentMap)localViewsForDocuments:
(const firebase::firestore::model::MaybeDocumentMap &)baseDocs;

/** Performs a query against the local view of all documents. */
- (firebase::firestore::model::DocumentMap)documentsMatchingQuery:(FSTQuery *)query;

Expand Down
43 changes: 39 additions & 4 deletions Firestore/Source/Local/FSTLocalDocumentsView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,48 @@ - (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key
return document;
}

// Returns the view of the given `docs` as they would appear after applying all
// mutations in the given `batches`.
- (MaybeDocumentMap)applyLocalMutationsToDocuments:(const MaybeDocumentMap &)docs
fromBatches:(NSArray<FSTMutationBatch *> *)batches {
MaybeDocumentMap results;

for (const auto &kv : docs) {
const DocumentKey &key = kv.first;
FSTMaybeDocument *localView = kv.second;
for (FSTMutationBatch *batch in batches) {
localView = [batch applyToLocalDocument:localView documentKey:key];
}
results = results.insert(key, localView);
}
return results;
}

- (MaybeDocumentMap)documentsForKeys:(const DocumentKeySet &)keys {
MaybeDocumentMap docs = [self.remoteDocumentCache entriesForKeys:keys];
return [self localViewsForDocuments:docs];
}

/**
* Similar to `documentsForKeys`, but creates the local view from the given
* `baseDocs` without retrieving documents from the local store.
*/
- (MaybeDocumentMap)localViewsForDocuments:(const MaybeDocumentMap &)baseDocs {
MaybeDocumentMap results;

DocumentKeySet allKeys;
for (const auto &kv : baseDocs) {
allKeys = allKeys.insert(kv.first);
}
NSArray<FSTMutationBatch *> *batches =
[self.mutationQueue allMutationBatchesAffectingDocumentKeys:keys];
for (const DocumentKey &key : keys) {
// TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one.
FSTMaybeDocument *maybeDoc = [self documentForKey:key inBatches:batches];
[self.mutationQueue allMutationBatchesAffectingDocumentKeys:allKeys];

MaybeDocumentMap docs = [self applyLocalMutationsToDocuments:baseDocs fromBatches:batches];

for (const auto &kv : docs) {
const DocumentKey &key = kv.first;
FSTMaybeDocument *maybeDoc = kv.second;

// TODO(http://b/32275378): Don't conflate missing / deleted.
if (!maybeDoc) {
maybeDoc = [FSTDeletedDocument documentWithKey:key
Expand Down
6 changes: 5 additions & 1 deletion Firestore/Source/Local/FSTLocalSerializer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ - (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document {
proto.hasCommittedMutations = deletedDocument.hasCommittedMutations;
} else if ([document isKindOfClass:[FSTDocument class]]) {
FSTDocument *existingDocument = (FSTDocument *)document;
proto.document = [self encodedDocument:existingDocument];
if (existingDocument.proto != nil) {
proto.document = existingDocument.proto;
} else {
proto.document = [self encodedDocument:existingDocument];
}
proto.hasCommittedMutations = existingDocument.hasCommittedMutations;
} else if ([document isKindOfClass:[FSTUnknownDocument class]]) {
FSTUnknownDocument *unknownDocument = (FSTUnknownDocument *)document;
Expand Down
21 changes: 16 additions & 5 deletions Firestore/Source/Local/FSTLocalStore.mm
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,24 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
}
}

// TODO(klimt): This could probably be an NSMutableDictionary.
DocumentKeySet changedDocKeys;
MaybeDocumentMap changedDocs;
const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges;
DocumentKeySet updatedKeys;
for (const auto &kv : remoteEvent.documentUpdates) {
updatedKeys = updatedKeys.insert(kv.first);
}
// Each loop iteration only affects its "own" doc, so it's safe to get all the remote
// documents in advance in a single call.
MaybeDocumentMap existingDocs = [self.remoteDocumentCache entriesForKeys:updatedKeys];

for (const auto &kv : remoteEvent.documentUpdates) {
const DocumentKey &key = kv.first;
FSTMaybeDocument *doc = kv.second;
changedDocKeys = changedDocKeys.insert(key);
FSTMaybeDocument *existingDoc = [self.remoteDocumentCache entryForKey:key];
FSTMaybeDocument *existingDoc = nil;
auto foundExisting = existingDocs.find(key);
if (foundExisting != existingDocs.end()) {
existingDoc = foundExisting->second;
}

// If a document update isn't authoritative, make sure we don't apply an old document version
// to the remote cache. We make an exception for SnapshotVersion.MIN which can happen for
Expand All @@ -280,6 +290,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
(authoritativeUpdates.contains(doc.key) && !existingDoc.hasPendingWrites) ||
doc.version >= existingDoc.version) {
[self.remoteDocumentCache addEntry:doc];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profiler shows that currently ~30% of time is spent here, or more specifically, here:

void Put(absl::string_view key, GPBMessage* message) {
    NSData* data = [message data]; // THIS LINE; apparently, `data` is pretty time-consuming
    std::string key_string(key);
    mutations_[key_string] = std::string((const char*)data.bytes, data.length);
    version_++;
  }

I can't see a way to improve this immediately, but I won't be surprised if the nanopb-based serializer will bring a further performance improvement.

changedDocs = changedDocs.insert(key, doc);
} else {
LOG_DEBUG(
"FSTLocalStore Ignoring outdated watch update for %s. "
Expand All @@ -306,7 +317,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
[self.queryCache setLastRemoteSnapshotVersion:remoteVersion];
}

return [self.localDocuments documentsForKeys:changedDocKeys];
return [self.localDocuments localViewsForDocuments:changedDocs];
});
}

Expand Down
10 changes: 10 additions & 0 deletions Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ - (nullable FSTMaybeDocument *)entryForKey:(const DocumentKey &)key {
return found != _docs.end() ? found->second : nil;
}

- (MaybeDocumentMap)entriesForKeys:(const DocumentKeySet &)keys {
MaybeDocumentMap results;
for (const DocumentKey &key : keys) {
// Make sure each key has a corresponding entry, which is null in case the document is not
// found.
results = results.insert(key, [self entryForKey:key]);
}
return results;
}

- (DocumentMap)documentsMatchingQuery:(FSTQuery *)query {
DocumentMap result;

Expand Down
10 changes: 10 additions & 0 deletions Firestore/Source/Local/FSTRemoteDocumentCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable FSTMaybeDocument *)entryForKey:
(const firebase::firestore::model::DocumentKey &)documentKey;

/**
* Looks up a set of entries in the cache.
*
* @param documentKeys The keys of the entries to look up.
* @return The cached Document or NoDocument entries indexed by key. If an entry is not cached,
* the corresponding key will be mapped to a null value.
*/
- (firebase::firestore::model::MaybeDocumentMap)entriesForKeys:
(const firebase::firestore::model::DocumentKeySet &)documentKeys;

/**
* Executes a query against the cached FSTDocument entries
*
Expand Down
13 changes: 13 additions & 0 deletions Firestore/Source/Model/FSTDocument.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"

@class FSTFieldValue;
@class GCFSDocument;
@class FSTObjectValue;

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -57,12 +58,24 @@ typedef NS_ENUM(NSInteger, FSTDocumentState) {
version:(firebase::firestore::model::SnapshotVersion)version
state:(FSTDocumentState)state;

+ (instancetype)documentWithData:(FSTObjectValue *)data
key:(firebase::firestore::model::DocumentKey)key
version:(firebase::firestore::model::SnapshotVersion)version
state:(FSTDocumentState)state
proto:(GCFSDocument *)proto;

- (nullable FSTFieldValue *)fieldForPath:(const firebase::firestore::model::FieldPath &)path;
- (BOOL)hasLocalMutations;
- (BOOL)hasCommittedMutations;

@property(nonatomic, strong, readonly) FSTObjectValue *data;

/**
* Memoized serialized form of the document for optimization purposes (avoids repeated
* serialization). Might be nil.
*/
@property(nonatomic, strong, readonly) GCFSDocument *proto;

@end

@interface FSTDeletedDocument : FSTMaybeDocument
Expand Down
27 changes: 27 additions & 0 deletions Firestore/Source/Model/FSTDocument.mm
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ + (instancetype)documentWithData:(FSTObjectValue *)data
state:state];
}

+ (instancetype)documentWithData:(FSTObjectValue *)data
key:(DocumentKey)key
version:(SnapshotVersion)version
state:(FSTDocumentState)state
proto:(GCFSDocument *)proto {
return [[FSTDocument alloc] initWithData:data
key:std::move(key)
version:std::move(version)
state:state
proto:proto];
}

- (instancetype)initWithData:(FSTObjectValue *)data
key:(DocumentKey)key
version:(SnapshotVersion)version
Expand All @@ -96,6 +108,21 @@ - (instancetype)initWithData:(FSTObjectValue *)data
if (self) {
_data = data;
_documentState = state;
_proto = nil;
}
return self;
}

- (instancetype)initWithData:(FSTObjectValue *)data
key:(DocumentKey)key
version:(SnapshotVersion)version
state:(FSTDocumentState)state
proto:(GCFSDocument *)proto {
self = [super initWithKey:std::move(key) version:std::move(version)];
if (self) {
_data = data;
_documentState = state;
_proto = proto;
}
return self;
}
Expand Down
15 changes: 12 additions & 3 deletions Firestore/Source/Remote/FSTSerializerBeta.mm
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,11 @@ - (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response
HARD_ASSERT(version != SnapshotVersion::None(),
"Got a document response with no snapshot version");

return [FSTDocument documentWithData:value key:key version:version state:FSTDocumentStateSynced];
return [FSTDocument documentWithData:value
key:key
version:version
state:FSTDocumentStateSynced
proto:response.found];
}

- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response {
Expand Down Expand Up @@ -1144,8 +1148,13 @@ - (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change {
const DocumentKey key = [self decodedDocumentKey:change.document.name];
SnapshotVersion version = [self decodedVersion:change.document.updateTime];
HARD_ASSERT(version != SnapshotVersion::None(), "Got a document change with no snapshot version");
FSTMaybeDocument *document =
[FSTDocument documentWithData:value key:key version:version state:FSTDocumentStateSynced];
// The document may soon be re-serialized back to protos in order to store it in local
// persistence. Memoize the encoded form to avoid encoding it again.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read this: http://go/apidosdonts##2-use-different-messages-for-wire-and-storage

However, since this is just an optimization that we could drop if our wire/storage protos diverge, I think we're still ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some points:

  • the link lists performance as an exception, which I think applies to us, even if it's written in a very server-centric way;
  • the link seems to presume that code in question is a server. We probably won't have the problem of explosive growth of binaries dependent upon us;
  • the link has an exception for nested protos, which I believe applies here (though it depends on how they count nestedness);
  • last of all, none of the examples of things going wrong seem very applicable to me, though I won't particularly insist on this point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, we made this choice intentionally, knowing about that doc. In short:

  • Clients are tightly coupled to the server version anyway because there's so much in the way of semantics tied up in the representation and much of our storage is really just queueing requests for the backend
  • We're depending on a stable public API rather than an unstable internal one
  • So long as the field tags and types line up we can rename these into private types later

In the steady state we can trust there won't be breaking changes to the protos because unlike google3 we can't impose on customers that they upgrade.

The only point where there is some diceyness is around transitions between versions of the protocol. The transition from v1alpha1 to v1beta1 was grossly incompatible, but thankfully at the time we didn't have persistence yet. Note that v1alpha1 was essentially the Cloud Datastore Entity model with streaming bolted on, while v1beta1 was Firestore proper so this was an expected breaking change. The upcoming transition to v1 is a no-op because v1beta1 and v1 are wire compatible.

Even if the RPC protocol were to change radically we can likely insist that the Document part remain stable across versions. Regardless, a fork-when-we-need-to strategy has allowed us to defer this work for two years and counting so I'm going to keep betting on it :-).

FSTMaybeDocument *document = [FSTDocument documentWithData:value
key:key
version:version
state:FSTDocumentStateSynced
proto:change.document];

NSArray<NSNumber *> *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray];
NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
Expand Down
6 changes: 4 additions & 2 deletions Firestore/core/src/firebase/firestore/remote/watch_stream.mm
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@
return status;
}

LOG_DEBUG("%s response: %s", GetDebugDescription(),
serializer_bridge_.Describe(response));
if (bridge::IsLoggingEnabled()) {
LOG_DEBUG("%s response: %s", GetDebugDescription(),
serializer_bridge_.Describe(response));
}

// A successful response means the stream is healthy.
backoff_.Reset();
Expand Down