Skip to content

Commit 6c0a3e8

Browse files
authored
Performance optimizations for data reading/writing (#942)
* (135743158) Performance optimizations for data reading/writing * Fix Windows build
1 parent dcd7a97 commit 6c0a3e8

File tree

6 files changed

+273
-258
lines changed

6 files changed

+273
-258
lines changed

Benchmarks/Benchmarks/DataIO/BenchmarkDataIO.swift

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ import Glibc
3030
import Darwin
3131
#endif
3232

33-
func testPath() -> URL {
34-
#if compiler(>=6)
35-
FileManager.default.temporaryDirectory.appending(path: "testfile-\(UUID().uuidString)", directoryHint: .notDirectory)
36-
#else
37-
FileManager.default.temporaryDirectory.appendingPathComponent("testfile-\(UUID().uuidString)")
38-
#endif
39-
}
40-
4133
func generateTestData(count: Int) -> Data {
4234
let memory = malloc(count)!
4335
let ptr = memory.bindMemory(to: UInt8.self, capacity: count)
@@ -51,18 +43,29 @@ func generateTestData(count: Int) -> Data {
5143
return Data(bytesNoCopy: ptr, count: count, deallocator: .free)
5244
}
5345

54-
func cleanup(at path: URL) {
55-
try? FileManager.default.removeItem(at: path)
46+
func cleanupTestPath() {
47+
try? FileManager.default.removeItem(at: testPath)
5648
// Ignore any errors
5749
}
5850

5951
// 16 MB file, big enough to trigger things like chunking
6052
let data = generateTestData(count: 1 << 24)
61-
let readMe = testPath()
53+
#if compiler(>=6)
54+
let testPath = FileManager.default.temporaryDirectory.appending(path: "testfile-\(UUID().uuidString)", directoryHint: .notDirectory)
55+
#else
56+
let testPath = FileManager.default.temporaryDirectory.appendingPathComponent("testfile-\(UUID().uuidString)")
57+
#endif
58+
let nonExistentPath = URL(filePath: "/does-not-exist", directoryHint: .notDirectory)
6259

6360
let base64Data = generateTestData(count: 1024 * 1024)
6461
let base64DataString = base64Data.base64EncodedString()
6562

63+
extension Benchmark.Configuration {
64+
fileprivate static var cleanupTestPathConfig: Self {
65+
.init(teardown: cleanupTestPath)
66+
}
67+
}
68+
6669
let benchmarks = {
6770
Benchmark.defaultConfiguration.maxIterations = 1_000_000_000
6871
Benchmark.defaultConfiguration.maxDuration = .seconds(3)
@@ -75,44 +78,78 @@ let benchmarks = {
7578
Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .mallocCountTotal, .throughput]
7679
#endif
7780

78-
Benchmark("read-write-emptyFile") { benchmark in
79-
let path = testPath()
81+
Benchmark("read-write-emptyFile", configuration: .cleanupTestPathConfig) { benchmark in
8082
let data = Data()
81-
try data.write(to: path)
82-
let read = try Data(contentsOf: path, options: [])
83-
cleanup(at: path)
83+
try data.write(to: testPath)
84+
let read = try Data(contentsOf: testPath, options: [])
8485
}
8586

86-
Benchmark("write-regularFile") { benchmark in
87-
let path = testPath()
88-
try data.write(to: path)
89-
cleanup(at: path)
87+
Benchmark("write-regularFile", configuration: .cleanupTestPathConfig) { benchmark in
88+
try data.write(to: testPath)
89+
}
90+
91+
Benchmark("write-regularFile-atomic", configuration: .cleanupTestPathConfig) { benchmark in
92+
try data.write(to: testPath, options: .atomic)
93+
}
94+
95+
Benchmark("write-regularFile-alreadyExists",
96+
configuration: .init(
97+
setup: {
98+
try! Data().write(to: testPath)
99+
},
100+
teardown: cleanupTestPath
101+
)
102+
) { benchmark in
103+
try? data.write(to: testPath)
104+
}
105+
106+
Benchmark("write-regularFile-alreadyExists-atomic",
107+
configuration: .init(
108+
setup: {
109+
try! Data().write(to: testPath)
110+
},
111+
teardown: cleanupTestPath
112+
)
113+
) { benchmark in
114+
try? data.write(to: testPath, options: .atomic)
90115
}
91116

92117
Benchmark("read-regularFile",
93118
configuration: .init(
94119
setup: {
95-
try! data.write(to: readMe)
120+
try! data.write(to: testPath)
96121
},
97-
teardown: {
98-
cleanup(at: readMe)
99-
}
122+
teardown: cleanupTestPath
100123
)
101124
) { benchmark in
102-
blackHole(try Data(contentsOf: readMe))
125+
blackHole(try Data(contentsOf: testPath))
126+
}
127+
128+
Benchmark("read-nonExistentFile") { benchmark in
129+
for _ in benchmark.scaledIterations {
130+
blackHole(try? Data(contentsOf: nonExistentPath))
131+
}
132+
}
133+
134+
Benchmark("read-nonExistentFile-userInfo") { benchmark in
135+
for _ in benchmark.scaledIterations {
136+
do {
137+
blackHole(try Data(contentsOf: nonExistentPath))
138+
} catch {
139+
blackHole((error as? CocoaError)?.userInfo["NSURLErrorKey"])
140+
}
141+
}
103142
}
104143

105144
Benchmark("read-hugeFile",
106145
configuration: .init(
107146
setup: {
108-
try! generateTestData(count: 1 << 30).write(to: readMe)
147+
try! generateTestData(count: 1 << 30).write(to: testPath)
109148
},
110-
teardown: {
111-
cleanup(at: readMe)
112-
}
149+
teardown: cleanupTestPath
113150
)
114151
) { benchmark in
115-
blackHole(try Data(contentsOf: readMe))
152+
blackHole(try Data(contentsOf: testPath))
116153
}
117154

118155
// MARK: base64

Sources/FoundationEssentials/Data/Data+Writing.swift

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,18 @@ private func writeToFileDescriptorWithProgress(_ fd: Int32, buffer: UnsafeRawBuf
125125

126126
private func cleanupTemporaryDirectory(at inPath: String?) {
127127
guard let inPath else { return }
128+
#if canImport(Darwin) || os(Linux)
129+
// Since we expect the directory to be empty at this point, try rmdir which is much faster than Darwin's removefile(3) for known empty directories
130+
if inPath.withFileSystemRepresentation({ $0.flatMap(rmdir) }) == 0 {
131+
return
132+
}
133+
#endif
128134
// Attempt to use FileManager, ignore error
129135
try? FileManager.default.removeItem(atPath: inPath)
130136
}
131137

132138
/// Caller is responsible for calling `close` on the `Int32` file descriptor.
133-
private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, prefix: String, options: Data.WritingOptions) throws -> (Int32, String) {
139+
private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, prefix: String, options: Data.WritingOptions, variant: String? = nil) throws -> (Int32, String) {
134140
#if os(WASI)
135141
// WASI does not have temp directories
136142
throw CocoaError(.featureUnsupported)
@@ -154,12 +160,12 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,
154160
// Furthermore, we can't compatibly switch to mkstemp() until we have the ability to set fchmod correctly, which requires the ability to query the current umask, which we don't have. (22033100)
155161
#if os(Windows)
156162
guard _mktemp_s(templateFileSystemRep, template.count + 1) == 0 else {
157-
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false)
163+
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant)
158164
}
159165
let flags: CInt = _O_BINARY | _O_CREAT | _O_EXCL | _O_RDWR
160166
#else
161167
guard mktemp(templateFileSystemRep) != nil else {
162-
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false)
168+
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant)
163169
}
164170
let flags: CInt = O_CREAT | O_EXCL | O_RDWR
165171
#endif
@@ -172,7 +178,7 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,
172178

173179
// If the file exists, we repeat. Otherwise throw the error.
174180
if errno != EEXIST {
175-
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false)
181+
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant)
176182
}
177183

178184
// Try again
@@ -194,12 +200,25 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,
194200

195201
/// Returns `(file descriptor, temporary file path, temporary directory path)`
196202
/// Caller is responsible for calling `close` on the `Int32` file descriptor and calling `cleanupTemporaryDirectory` on the temporary directory path. The temporary directory path may be nil, if it does not need to be cleaned up.
197-
private func createProtectedTemporaryFile(at destinationPath: String, inPath: PathOrURL, options: Data.WritingOptions) throws -> (Int32, String, String?) {
203+
private func createProtectedTemporaryFile(at destinationPath: String, inPath: PathOrURL, options: Data.WritingOptions, variant: String? = nil) throws -> (Int32, String, String?) {
198204
#if FOUNDATION_FRAMEWORK
199205
if _foundation_sandbox_check(getpid(), nil) != 0 {
200206
// Convert the path back into a string
201207
let url = URL(fileURLWithPath: destinationPath, isDirectory: false)
202-
let temporaryDirectoryPath = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: url, create: true).path(percentEncoded: false)
208+
var temporaryDirectoryPath: String
209+
do {
210+
temporaryDirectoryPath = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: url, create: true).path(percentEncoded: false)
211+
} catch {
212+
if let variant, let cocoaError = error as? CocoaError {
213+
let code = cocoaError.code
214+
var userInfo = cocoaError.userInfo
215+
userInfo[NSUserStringVariantErrorKey] = variant
216+
217+
throw CocoaError(code, userInfo: userInfo)
218+
} else {
219+
throw error
220+
}
221+
}
203222

204223
let auxFile = temporaryDirectoryPath.appendingPathComponent(destinationPath.lastPathComponent)
205224
return try auxFile.withFileSystemRepresentation { auxFileFileSystemRep in
@@ -212,14 +231,14 @@ private func createProtectedTemporaryFile(at destinationPath: String, inPath: Pa
212231
} else {
213232
let savedErrno = errno
214233
cleanupTemporaryDirectory(at: temporaryDirectoryPath)
215-
throw CocoaError.errorWithFilePath(inPath, errno: savedErrno, reading: false)
234+
throw CocoaError.errorWithFilePath(inPath, errno: savedErrno, reading: false, variant: variant)
216235
}
217236
}
218237
}
219238
#endif
220239

221240
let temporaryDirectoryPath = destinationPath.deletingLastPathComponent()
222-
let (fd, auxFile) = try createTemporaryFile(at: temporaryDirectoryPath, inPath: inPath, prefix: ".dat.nosync", options: options)
241+
let (fd, auxFile) = try createTemporaryFile(at: temporaryDirectoryPath, inPath: inPath, prefix: ".dat.nosync", options: options, variant: variant)
223242
return (fd, auxFile, nil)
224243
}
225244

@@ -310,25 +329,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
310329

311330
#if os(Windows)
312331
try inPath.path.withNTPathRepresentation { pwszPath in
313-
var fd: CInt
314-
var auxPath: String?
315-
var temporaryDirectoryPath: String?
316-
317-
do {
318-
(fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: inPath.path, inPath: inPath, options: options)
319-
} catch {
320-
if let cocoaError = error as? CocoaError {
321-
// Extract code and userInfo, then re-create it with an additional userInfo key.
322-
let code = cocoaError.code
323-
var userInfo = cocoaError.userInfo
324-
userInfo[NSUserStringVariantErrorKey] = "Folder"
325-
326-
throw CocoaError(code, userInfo: userInfo)
327-
} else {
328-
// These should all be CocoaErrors, but just in case we re-throw the original one here.
329-
throw error
330-
}
331-
}
332+
var (fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: inPath.path, inPath: inPath, options: options, variant: "Folder")
332333

333334
// Cleanup temporary directory
334335
defer { cleanupTemporaryDirectory(at: temporaryDirectoryPath) }
@@ -344,10 +345,8 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
344345
do {
345346
try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: callback)
346347
} catch {
347-
if let auxPath {
348-
try auxPath.withNTPathRepresentation { pwszAuxPath in
349-
_ = DeleteFileW(pwszAuxPath)
350-
}
348+
try auxPath.withNTPathRepresentation { pwszAuxPath in
349+
_ = DeleteFileW(pwszAuxPath)
351350
}
352351

353352
if callback?.isCancelled ?? false {
@@ -359,9 +358,6 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
359358

360359
writeExtendedAttributes(fd: fd, attributes: attributes)
361360

362-
// We're done now
363-
guard let auxPath else { return }
364-
365361
_close(fd)
366362
fd = -1
367363

@@ -379,10 +375,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
379375
throw CocoaError(.fileWriteInvalidFileName)
380376
}
381377

382-
let fd: Int32
383378
var mode: mode_t?
384-
var temporaryDirectoryPath: String?
385-
var auxPath: String?
386379

387380
#if FOUNDATION_FRAMEWORK
388381
var newPath = inPath.path
@@ -410,21 +403,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
410403
let newPath = inPath.path
411404
#endif
412405

413-
do {
414-
(fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: newPath, inPath: inPath, options: options)
415-
} catch {
416-
if let cocoaError = error as? CocoaError {
417-
// Extract code and userInfo, then re-create it with an additional userInfo key.
418-
let code = cocoaError.code
419-
var userInfo = cocoaError.userInfo
420-
userInfo[NSUserStringVariantErrorKey] = "Folder"
421-
422-
throw CocoaError(code, userInfo: userInfo)
423-
} else {
424-
// These should all be CocoaErrors, but just in case we re-throw the original one here.
425-
throw error
426-
}
427-
}
406+
var (fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: newPath, inPath: inPath, options: options, variant: "Folder")
428407

429408
guard fd >= 0 else {
430409
let savedErrno = errno
@@ -442,11 +421,9 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
442421
} catch {
443422
let savedError = errno
444423

445-
if let auxPath {
446-
auxPath.withFileSystemRepresentation { pathFileSystemRep in
447-
guard let pathFileSystemRep else { return }
448-
unlink(pathFileSystemRep)
449-
}
424+
auxPath.withFileSystemRepresentation { pathFileSystemRep in
425+
guard let pathFileSystemRep else { return }
426+
unlink(pathFileSystemRep)
450427
}
451428
cleanupTemporaryDirectory(at: temporaryDirectoryPath)
452429

@@ -458,11 +435,6 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint
458435
}
459436

460437
writeExtendedAttributes(fd: fd, attributes: attributes)
461-
462-
guard let auxPath else {
463-
// We're done now
464-
return
465-
}
466438

467439
try auxPath.withFileSystemRepresentation { auxPathFileSystemRep in
468440
guard let auxPathFileSystemRep else {

0 commit comments

Comments
 (0)