Skip to content

Commit 4f9d972

Browse files
Lukasathomasvl
authored andcommitted
Add modify operation to ExtensionFieldValueSet.
During decoding, modifications are frequently made to the extension values in ExtensionFieldValueSet. In particular for repeated extensions, these modifications include modifications to copy-on-write data types. Because the _modify accessor hasn't been standardised yet, ExtensionFieldValueSet implements get/set only, forcing a modification of existing data to be done by a get/set pair. This will always emit a copy-on-write operation. The result of the above is that decoding repeated extensions is a quadratic operation, making it diabolically slow. oss-fuzz caught this behaviour. This patch adds a non-API modify function to ExtensionFieldValueSet and uses it on all decoding paths. This code simply encapsulates a somewhat delicate function invocation that manages to convince the Swift compiler to use Dictionary's _modify accessor for the underlying operation. The result is that the oss-fuzz sample runtime decreases from 140s to 5s on my machine. This patch will likely have miscellaneous performance improvements across the board. It adds the oss-fuzz case for regression purposes as well.
1 parent 3a4b54d commit 4f9d972

File tree

5 files changed

+32
-35
lines changed

5 files changed

+32
-35
lines changed
Binary file not shown.

Sources/SwiftProtobuf/BinaryDecoder.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,18 +1110,16 @@ internal struct BinaryDecoder: Decoder {
11101110
) throws {
11111111
assert(!consumed)
11121112
assert(fieldNumber == ext.fieldNumber)
1113-
var fieldValue = values[fieldNumber]
1114-
// Message/Group extensions both will call back into the matching
1115-
// decode methods, so the recursion depth will be tracked there.
1116-
if fieldValue != nil {
1117-
try fieldValue!.decodeExtensionField(decoder: &self)
1118-
} else {
1119-
fieldValue = try ext._protobuf_newField(decoder: &self)
1120-
}
1121-
if consumed {
1113+
1114+
try values.modify(index: fieldNumber) { fieldValue in
1115+
// Message/Group extensions both will call back into the matching
1116+
// decode methods, so the recursion depth will be tracked there.
11221117
if fieldValue != nil {
1123-
values[fieldNumber] = fieldValue
1118+
try fieldValue!.decodeExtensionField(decoder: &self)
11241119
} else {
1120+
fieldValue = try ext._protobuf_newField(decoder: &self)
1121+
}
1122+
if consumed && fieldValue == nil {
11251123
// Really things should never get here, if the decoder says
11261124
// the bytes were consumed, then there should have been a
11271125
// field that consumed them (existing or created). This

Sources/SwiftProtobuf/ExtensionFieldValueSet.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ public struct ExtensionFieldValueSet: Hashable {
7878
set { values[index] = newValue }
7979
}
8080

81+
mutating func modify<ReturnType>(index: Int, _ modifier: (inout AnyExtensionField?) throws -> ReturnType) rethrows -> ReturnType {
82+
// This internal helper exists to invoke the _modify accessor on Dictionary for the given operation, which can avoid CoWs
83+
// during the modification operation.
84+
return try modifier(&values[index])
85+
}
86+
8187
public var isInitialized: Bool {
8288
for (_, v) in values {
8389
if !v.isInitialized {

Sources/SwiftProtobuf/JSONDecoder.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -724,18 +724,12 @@ internal struct JSONDecoder: Decoder {
724724
// Force-unwrap: we can only get here if the extension exists.
725725
let ext = scanner.extensions[messageType, fieldNumber]!
726726

727-
var fieldValue = values[fieldNumber]
728-
if fieldValue != nil {
729-
try fieldValue!.decodeExtensionField(decoder: &self)
730-
} else {
731-
fieldValue = try ext._protobuf_newField(decoder: &self)
732-
}
733-
// If the value was `null`, then the 'else' clause will return nil, as there
734-
// is nothing to assign. If the api ever supports merging JSON into an
735-
// object to update it, then the 'then' clause likely should be update to
736-
// support clearing the the value rather that keeping its current value.
737-
if fieldValue != nil {
738-
values[fieldNumber] = fieldValue
727+
try values.modify(index: fieldNumber) { fieldValue in
728+
if fieldValue != nil {
729+
try fieldValue!.decodeExtensionField(decoder: &self)
730+
} else {
731+
fieldValue = try ext._protobuf_newField(decoder: &self)
732+
}
739733
}
740734
}
741735
}

Sources/SwiftProtobuf/TextFormatDecoder.swift

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -708,19 +708,18 @@ internal struct TextFormatDecoder: Decoder {
708708

709709
mutating func decodeExtensionField(values: inout ExtensionFieldValueSet, messageType: Message.Type, fieldNumber: Int) throws {
710710
if let ext = scanner.extensions?[messageType, fieldNumber] {
711-
var fieldValue = values[fieldNumber]
712-
if fieldValue != nil {
713-
try fieldValue!.decodeExtensionField(decoder: &self)
714-
} else {
715-
fieldValue = try ext._protobuf_newField(decoder: &self)
716-
}
717-
if fieldValue != nil {
718-
values[fieldNumber] = fieldValue
719-
} else {
720-
// Really things should never get here, for TextFormat, decoding
721-
// the value should always work or throw an error. This specific
722-
// error result is to allow this to be more detectable.
723-
throw TextFormatDecodingError.internalExtensionError
711+
try values.modify(index: fieldNumber) { fieldValue in
712+
if fieldValue != nil {
713+
try fieldValue!.decodeExtensionField(decoder: &self)
714+
} else {
715+
fieldValue = try ext._protobuf_newField(decoder: &self)
716+
}
717+
if fieldValue == nil {
718+
// Really things should never get here, for TextFormat, decoding
719+
// the value should always work or throw an error. This specific
720+
// error result is to allow this to be more detectable.
721+
throw TextFormatDecodingError.internalExtensionError
722+
}
724723
}
725724
}
726725
}

0 commit comments

Comments
 (0)