[Swift] Memory usage fix (#8643)

Allows a complete reset for the underlying memory of the
_InternalByteBuffers within FlatBuffers and FlexBuffers.
This commit is contained in:
mustiikhalil
2025-07-18 18:37:58 +02:00
committed by GitHub
parent 2e49b3ba60
commit ca73ff34b7
14 changed files with 146 additions and 58 deletions

View File

@@ -47,13 +47,11 @@ let package = Package(
.testTarget( .testTarget(
name: "FlatbuffersTests", name: "FlatbuffersTests",
dependencies: .dependencies, dependencies: .dependencies,
path: "tests/swift/Tests/Flatbuffers" path: "tests/swift/Tests/Flatbuffers"),
),
.testTarget( .testTarget(
name: "FlexbuffersTests", name: "FlexbuffersTests",
dependencies: ["FlexBuffers"], dependencies: ["FlexBuffers"],
path: "tests/swift/Tests/Flexbuffers" path: "tests/swift/Tests/Flexbuffers"),
)
]) ])
extension Array where Element == Package.Dependency { extension Array where Element == Package.Dependency {
@@ -75,7 +73,7 @@ extension Array where Element == PackageDescription.Target.Dependency {
// Test only Dependency // Test only Dependency
[ [
.product(name: "GRPC", package: "grpc-swift"), .product(name: "GRPC", package: "grpc-swift"),
"FlatBuffers" "FlatBuffers",
] ]
#endif #endif
} }

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif
@@ -72,8 +73,8 @@ public struct FlatBufferBuilder {
data.append( data.append(
ptr.baseAddress!.bindMemory( ptr.baseAddress!.bindMemory(
to: UInt8.self, to: UInt8.self,
capacity: _bb.capacity), capacity: ptr.count),
count: _bb.capacity) count: ptr.count)
return data return data
} }
} }
@@ -143,13 +144,13 @@ public struct FlatBufferBuilder {
} }
/// Clears the builder and the buffer from the written data. /// Clears the builder and the buffer from the written data.
mutating public func clear() { mutating public func clear(keepingCapacity: Bool = false) {
_minAlignment = 0 _minAlignment = 0
isNested = false isNested = false
stringOffsetMap.removeAll(keepingCapacity: true) stringOffsetMap.removeAll(keepingCapacity: keepingCapacity)
_vtables.removeAll(keepingCapacity: true) _vtables.removeAll(keepingCapacity: keepingCapacity)
_vtableStorage.clear() _vtableStorage.reset(keepingCapacity: keepingCapacity)
_bb.clear() _bb.clear(keepingCapacity: keepingCapacity)
} }
// MARK: - Create Tables // MARK: - Create Tables
@@ -852,10 +853,6 @@ extension FlatBufferBuilder: CustomDebugStringConvertible {
/// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer /// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer
@usableFromInline @usableFromInline
internal class VTableStorage { internal class VTableStorage {
/// Memory check since deallocating each time we want to clear would be expensive
/// and memory leaks would happen if we dont deallocate the first allocated memory.
/// memory is promised to be available before adding `FieldLoc`
private var memoryInUse = false
/// Size of FieldLoc in memory /// Size of FieldLoc in memory
let size = MemoryLayout<FieldLoc>.stride let size = MemoryLayout<FieldLoc>.stride
/// Memeory buffer /// Memeory buffer
@@ -905,6 +902,24 @@ extension FlatBufferBuilder: CustomDebugStringConvertible {
maxOffset = max(loc.position, maxOffset) maxOffset = max(loc.position, maxOffset)
} }
/// Clears the data stored related to the encoded buffer
@inline(__always)
func reset(keepingCapacity: Bool) {
maxOffset = 0
numOfFields = 0
writtenIndex = 0
if keepingCapacity {
memset(memory.baseAddress!, 0, memory.count)
} else {
capacity = 0
let memory = UnsafeMutableRawBufferPointer.allocate(
byteCount: 0,
alignment: 0)
self.memory.deallocate()
self.memory = memory
}
}
/// Clears the data stored related to the encoded buffer /// Clears the data stored related to the encoded buffer
@inline(__always) @inline(__always)
func clear() { func clear() {

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -29,12 +29,10 @@ struct _InternalByteBuffer {
/// deallocating the memory that was held by (memory: UnsafeMutableRawPointer) /// deallocating the memory that was held by (memory: UnsafeMutableRawPointer)
@usableFromInline @usableFromInline
final class Storage { final class Storage {
// This storage doesn't own the memory, therefore, we won't deallocate on deinit.
private let unowned: Bool
/// pointer to the start of the buffer object in memory /// pointer to the start of the buffer object in memory
var memory: UnsafeMutableRawPointer private(set) var memory: UnsafeMutableRawPointer
/// Capacity of UInt8 the buffer can hold /// Capacity of UInt8 the buffer can hold
var capacity: Int private(set) var capacity: Int
@usableFromInline @usableFromInline
init(count: Int, alignment: Int) { init(count: Int, alignment: Int) {
@@ -42,20 +40,14 @@ struct _InternalByteBuffer {
byteCount: count, byteCount: count,
alignment: alignment) alignment: alignment)
capacity = count capacity = count
unowned = false
} }
deinit { deinit {
if !unowned { memory.deallocate()
memory.deallocate()
}
} }
@usableFromInline @usableFromInline
func initialize(for size: Int) { func initialize(for size: Int) {
assert(
!unowned,
"initalize should NOT be called on a buffer that is built by assumingMemoryBound")
memset(memory, 0, size) memset(memory, 0, size)
} }
@@ -86,6 +78,7 @@ struct _InternalByteBuffer {
@usableFromInline var _storage: Storage @usableFromInline var _storage: Storage
private let initialSize: Int
/// The size of the elements written to the buffer + their paddings /// The size of the elements written to the buffer + their paddings
private var _writerSize: Int = 0 private var _writerSize: Int = 0
/// Alignment of the current memory being written to the buffer /// Alignment of the current memory being written to the buffer
@@ -108,9 +101,9 @@ struct _InternalByteBuffer {
/// - size: Length of the buffer /// - size: Length of the buffer
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(initialSize size: Int) { init(initialSize size: Int) {
let size = size.convertToPowerofTwo initialSize = size.convertToPowerofTwo
_storage = Storage(count: size, alignment: alignment) _storage = Storage(count: initialSize, alignment: alignment)
_storage.initialize(for: size) _storage.initialize(for: initialSize)
} }
/// Fills the buffer with padding by adding to the writersize /// Fills the buffer with padding by adding to the writersize
@@ -298,10 +291,14 @@ struct _InternalByteBuffer {
/// Clears the current instance of the buffer, replacing it with new memory /// Clears the current instance of the buffer, replacing it with new memory
@inline(__always) @inline(__always)
mutating public func clear() { mutating public func clear(keepingCapacity: Bool = false) {
_writerSize = 0 _writerSize = 0
alignment = 1 alignment = 1
_storage.initialize(for: _storage.capacity) if keepingCapacity {
_storage.initialize(for: _storage.capacity)
} else {
_storage = Storage(count: initialSize, alignment: alignment)
}
} }
/// Reads an object from the buffer /// Reads an object from the buffer

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
#if canImport(Common) #if canImport(Common)
import Common import Common
#endif #endif
@@ -72,14 +73,32 @@ public struct FlexBuffersWriter {
return ByteBuffer(byteBuffer: _bb) return ByteBuffer(byteBuffer: _bb)
} }
#if !os(WASI)
/// Data representation of the buffer
///
/// Should only be used after ``finish(offset:addPrefix:)`` is called
public var data: Data {
assert(finished, "Data shouldn't be called before finish()")
return _bb.withUnsafeSlicedBytes { ptr in
var data = Data()
data.append(
ptr.baseAddress!.bindMemory(
to: UInt8.self,
capacity: ptr.count),
count: ptr.count)
return data
}
}
#endif
/// Resets the internal state. Automatically called before building a new flexbuffer. /// Resets the internal state. Automatically called before building a new flexbuffer.
public mutating func reset() { public mutating func reset(keepingCapacity: Bool = false) {
_bb.clear() _bb.clear(keepingCapacity: keepingCapacity)
stack.removeAll(keepingCapacity: true) stack.removeAll(keepingCapacity: keepingCapacity)
finished = false finished = false
minBitWidth = .w8 minBitWidth = .w8
keyPool.removeAll() keyPool.removeAll(keepingCapacity: keepingCapacity)
stringPool.removeAll() stringPool.removeAll(keepingCapacity: keepingCapacity)
} }
// MARK: - Storing root // MARK: - Storing root

View File

@@ -30,8 +30,6 @@ struct _InternalByteBuffer {
/// deallocating the memory that was held by (memory: UnsafeMutableRawPointer) /// deallocating the memory that was held by (memory: UnsafeMutableRawPointer)
@usableFromInline @usableFromInline
final class Storage { final class Storage {
// This storage doesn't own the memory, therefore, we won't deallocate on deinit.
private let unowned: Bool
/// pointer to the start of the buffer object in memory /// pointer to the start of the buffer object in memory
var memory: UnsafeMutableRawPointer var memory: UnsafeMutableRawPointer
/// Capacity of UInt8 the buffer can hold /// Capacity of UInt8 the buffer can hold
@@ -43,35 +41,25 @@ struct _InternalByteBuffer {
byteCount: count, byteCount: count,
alignment: alignment) alignment: alignment)
capacity = count capacity = count
unowned = false
} }
@usableFromInline @usableFromInline
init(memory: UnsafeMutableRawPointer, capacity: Int, unowned: Bool) { init(memory: UnsafeMutableRawPointer, capacity: Int, unowned: Bool) {
self.memory = memory self.memory = memory
self.capacity = capacity self.capacity = capacity
self.unowned = unowned
} }
deinit { deinit {
if !unowned { memory.deallocate()
memory.deallocate()
}
} }
@usableFromInline @usableFromInline
func copy(from ptr: UnsafeRawPointer, count: Int) { func copy(from ptr: UnsafeRawPointer, count: Int) {
assert(
!unowned,
"copy should NOT be called on a buffer that is built by assumingMemoryBound")
memory.copyMemory(from: ptr, byteCount: count) memory.copyMemory(from: ptr, byteCount: count)
} }
@usableFromInline @usableFromInline
func initialize(for size: Int) { func initialize(for size: Int) {
assert(
!unowned,
"initalize should NOT be called on a buffer that is built by assumingMemoryBound")
memset(memory, 0, size) memset(memory, 0, size)
} }
@@ -100,6 +88,8 @@ struct _InternalByteBuffer {
} }
@usableFromInline var _storage: Storage @usableFromInline var _storage: Storage
// Initial size of the internal storage
private let initialSize: Int
/// The size of the elements written to the buffer + their paddings /// The size of the elements written to the buffer + their paddings
var writerIndex: Int = 0 var writerIndex: Int = 0
/// Alignment of the current memory being written to the buffer /// Alignment of the current memory being written to the buffer
@@ -122,17 +112,21 @@ struct _InternalByteBuffer {
/// - size: Length of the buffer /// - size: Length of the buffer
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer /// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(initialSize size: Int) { init(initialSize size: Int) {
let size = size.convertToPowerofTwo initialSize = size.convertToPowerofTwo
_storage = Storage(count: size, alignment: alignment) _storage = Storage(count: initialSize, alignment: alignment)
_storage.initialize(for: size) _storage.initialize(for: initialSize)
} }
/// Clears the current instance of the buffer, replacing it with new memory /// Clears the current instance of the buffer, replacing it with new memory
@inline(__always) @inline(__always)
mutating public func clear() { mutating public func clear(keepingCapacity: Bool = false) {
writerIndex = 0 writerIndex = 0
alignment = 1 alignment = 1
_storage.initialize(for: _storage.capacity) if keepingCapacity {
_storage.initialize(for: _storage.capacity)
} else {
_storage = Storage(count: initialSize, alignment: alignment)
}
} }
@inline(__always) @inline(__always)

View File

@@ -43,6 +43,34 @@ class FlatBuffersMonsterWriterTests: XCTestCase {
readVerifiedMonster(fb: _data) readVerifiedMonster(fb: _data)
} }
func testCreateMonsterData() {
let bytes = createMonster(withPrefix: false)
var buffer = ByteBuffer(data: bytes.data)
let monster: MyGame_Example_Monster = getRoot(byteBuffer: &buffer)
readMonster(monster: monster)
mutateMonster(fb: bytes.buffer)
readMonster(monster: monster)
}
func testCreateMonsterResetTests() {
var builder = createMonster(withPrefix: false)
var buffer = ByteBuffer(data: builder.data)
let monster: MyGame_Example_Monster = getRoot(byteBuffer: &buffer)
readMonster(monster: monster)
builder.clear()
XCTAssertEqual(builder.capacity, 1)
XCTAssertEqual(builder.size, 0)
write(fbb: &builder, prefix: false)
var _buffer = ByteBuffer(data: builder.data)
XCTAssertEqual(_buffer.capacity, 304)
let _monster: MyGame_Example_Monster = getRoot(byteBuffer: &_buffer)
readMonster(monster: _monster)
builder.clear(keepingCapacity: true)
XCTAssertEqual(builder.capacity, 512)
XCTAssertEqual(builder.size, 0)
}
func testCreateMonster() { func testCreateMonster() {
let bytes = createMonster(withPrefix: false) let bytes = createMonster(withPrefix: false)
// swiftformat:disable all // swiftformat:disable all
@@ -257,6 +285,11 @@ class FlatBuffersMonsterWriterTests: XCTestCase {
func createMonster(withPrefix prefix: Bool) -> FlatBufferBuilder { func createMonster(withPrefix prefix: Bool) -> FlatBufferBuilder {
var fbb = FlatBufferBuilder(initialSize: 1) var fbb = FlatBufferBuilder(initialSize: 1)
write(fbb: &fbb, prefix: prefix)
return fbb
}
func write(fbb: inout FlatBufferBuilder, prefix: Bool = false) {
let names = [ let names = [
fbb.create(string: "Frodo"), fbb.create(string: "Frodo"),
fbb.create(string: "Barney"), fbb.create(string: "Barney"),
@@ -313,7 +346,6 @@ class FlatBuffersMonsterWriterTests: XCTestCase {
Monster.addVectorOf(testarrayoftables: sortedArray, &fbb) Monster.addVectorOf(testarrayoftables: sortedArray, &fbb)
let end = Monster.endMonster(&fbb, start: mStart) let end = Monster.endMonster(&fbb, start: mStart)
Monster.finish(&fbb, end: end, prefix: prefix) Monster.finish(&fbb, end: end, prefix: prefix)
return fbb
} }
func mutateMonster(fb: ByteBuffer) { func mutateMonster(fb: ByteBuffer) {

View File

@@ -15,9 +15,10 @@
*/ */
import Common import Common
import FlexBuffers
import XCTest import XCTest
@testable import FlexBuffers
final class FlexBuffersReaderTests: XCTestCase { final class FlexBuffersReaderTests: XCTestCase {
func testReadingProperBuffer() throws { func testReadingProperBuffer() throws {
@@ -30,6 +31,29 @@ final class FlexBuffersReaderTests: XCTestCase {
try validate(buffer: buf) try validate(buffer: buf)
} }
func testReset() throws {
var fbx = FlexBuffersWriter(
initialSize: 8,
flags: .shareKeysAndStrings)
write(fbx: &fbx)
try validate(buffer: ByteBuffer(data: fbx.data))
XCTAssertEqual(fbx.capacity, 512)
fbx.reset()
XCTAssertEqual(fbx.writerIndex, 0)
XCTAssertEqual(fbx.capacity, 8)
write(fbx: &fbx)
try validate(buffer: ByteBuffer(data: fbx.data))
fbx.reset(keepingCapacity: true)
XCTAssertEqual(fbx.writerIndex, 0)
XCTAssertEqual(fbx.capacity, 512)
write(fbx: &fbx)
try validate(buffer: ByteBuffer(data: fbx.data))
XCTAssertEqual(fbx.capacity, 512)
}
private func validate(buffer buf: ByteBuffer) throws { private func validate(buffer buf: ByteBuffer) throws {
let reference = try getRoot(buffer: buf)! let reference = try getRoot(buffer: buf)!
XCTAssertEqual(reference.type, .map) XCTAssertEqual(reference.type, .map)

View File

@@ -32,7 +32,11 @@ func createProperBuffer() -> FlexBuffersWriter {
var fbx = FlexBuffersWriter( var fbx = FlexBuffersWriter(
initialSize: 8, initialSize: 8,
flags: .shareKeysAndStrings) flags: .shareKeysAndStrings)
write(fbx: &fbx)
return fbx
}
func write(fbx: inout FlexBuffersWriter) {
fbx.map { map in fbx.map { map in
map.vector(key: "vec") { v in map.vector(key: "vec") { v in
v.add(int64: -100) v.add(int64: -100)
@@ -57,5 +61,4 @@ func createProperBuffer() -> FlexBuffersWriter {
} }
fbx.finish() fbx.finish()
return fbx
} }