Skip to content

Commit 97f2778

Browse files
authored
Give the tests a bit of a sprucing up (vapor#91)
* Move the performance tests to their own file and predicate them on an environment variable secondary to the "is debug build" check. Skip them using XCTSkipUnless() instead of manually returning. * When testing, if initial connection to a database completely fails (such as because auth failure), fail with a proper error thrown instead of crashing because the connection wasn't shut down properly. * Modernize several tests a bit. * Allow the one (very) long-running test to be enabled via an environment variable, and use XCTSkipUnless() to skip it. * Rename perf test case per PR feedback
1 parent 4871046 commit 97f2778

File tree

3 files changed

+370
-348
lines changed

3 files changed

+370
-348
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import Logging
2+
import PostgresNIO
3+
import XCTest
4+
import NIOTestUtils
5+
6+
final class PerformanceTests: XCTestCase {
7+
private var group: EventLoopGroup!
8+
9+
private var eventLoop: EventLoop { self.group.next() }
10+
11+
override func setUpWithError() throws {
12+
try super.setUpWithError()
13+
try XCTSkipUnless(Self.shouldRunPerformanceTests)
14+
15+
XCTAssertTrue(isLoggingConfigured)
16+
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
17+
}
18+
19+
override func tearDownWithError() throws {
20+
try self.group?.syncShutdownGracefully()
21+
self.group = nil
22+
try super.tearDownWithError()
23+
}
24+
25+
26+
// MARK: Performance
27+
28+
func testPerformanceRangeSelectDecodePerformance() throws {
29+
struct Series: Decodable {
30+
var num: Int
31+
}
32+
33+
let conn = try PostgresConnection.test(on: eventLoop).wait()
34+
defer { try! conn.close().wait() }
35+
measure {
36+
do {
37+
for _ in 0..<5 {
38+
try conn.query("SELECT * FROM generate_series(1, 10000) num") { row in
39+
_ = row.column("num")?.int
40+
}.wait()
41+
}
42+
} catch {
43+
XCTFail("\(error)")
44+
}
45+
}
46+
}
47+
48+
func testPerformanceSelectTinyModel() throws {
49+
let conn = try PostgresConnection.test(on: eventLoop).wait()
50+
defer { try! conn.close().wait() }
51+
52+
try prepareTableToMeasureSelectPerformance(
53+
rowCount: 300_000, batchSize: 5_000,
54+
schema:
55+
"""
56+
"int" int8,
57+
""",
58+
fixtureData: [PostgresData(int: 1234)],
59+
on: self.eventLoop
60+
)
61+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
62+
63+
measure {
64+
do {
65+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
66+
_ = row.column("int")?.int
67+
}.wait()
68+
} catch {
69+
XCTFail("\(error)")
70+
}
71+
}
72+
}
73+
74+
func testPerformanceSelectMediumModel() throws {
75+
let conn = try PostgresConnection.test(on: eventLoop).wait()
76+
defer { try! conn.close().wait() }
77+
78+
let now = Date()
79+
let uuid = UUID()
80+
try prepareTableToMeasureSelectPerformance(
81+
rowCount: 300_000,
82+
schema:
83+
// TODO: Also add a `Double` and a `Data` field to this performance test.
84+
"""
85+
"string" text,
86+
"int" int8,
87+
"date" timestamptz,
88+
"uuid" uuid,
89+
""",
90+
fixtureData: [
91+
PostgresData(string: "foo"),
92+
PostgresData(int: 0),
93+
now.postgresData!,
94+
PostgresData(uuid: uuid)
95+
],
96+
on: self.eventLoop
97+
)
98+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
99+
100+
measure {
101+
do {
102+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
103+
_ = row.column("id")?.int
104+
_ = row.column("string")?.string
105+
_ = row.column("int")?.int
106+
_ = row.column("date")?.date
107+
_ = row.column("uuid")?.uuid
108+
}.wait()
109+
} catch {
110+
XCTFail("\(error)")
111+
}
112+
}
113+
}
114+
115+
func testPerformanceSelectLargeModel() throws {
116+
let conn = try PostgresConnection.test(on: eventLoop).wait()
117+
defer { try! conn.close().wait() }
118+
119+
let now = Date()
120+
let uuid = UUID()
121+
try prepareTableToMeasureSelectPerformance(
122+
rowCount: 100_000,
123+
schema:
124+
// TODO: Also add `Double` and `Data` fields to this performance test.
125+
"""
126+
"string1" text,
127+
"string2" text,
128+
"string3" text,
129+
"string4" text,
130+
"string5" text,
131+
"int1" int8,
132+
"int2" int8,
133+
"int3" int8,
134+
"int4" int8,
135+
"int5" int8,
136+
"date1" timestamptz,
137+
"date2" timestamptz,
138+
"date3" timestamptz,
139+
"date4" timestamptz,
140+
"date5" timestamptz,
141+
"uuid1" uuid,
142+
"uuid2" uuid,
143+
"uuid3" uuid,
144+
"uuid4" uuid,
145+
"uuid5" uuid,
146+
""",
147+
fixtureData: [
148+
PostgresData(string: "string1"),
149+
PostgresData(string: "string2"),
150+
PostgresData(string: "string3"),
151+
PostgresData(string: "string4"),
152+
PostgresData(string: "string5"),
153+
PostgresData(int: 1),
154+
PostgresData(int: 2),
155+
PostgresData(int: 3),
156+
PostgresData(int: 4),
157+
PostgresData(int: 5),
158+
now.postgresData!,
159+
now.postgresData!,
160+
now.postgresData!,
161+
now.postgresData!,
162+
now.postgresData!,
163+
PostgresData(uuid: uuid),
164+
PostgresData(uuid: uuid),
165+
PostgresData(uuid: uuid),
166+
PostgresData(uuid: uuid),
167+
PostgresData(uuid: uuid)
168+
],
169+
on: self.eventLoop
170+
)
171+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
172+
173+
measure {
174+
do {
175+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
176+
_ = row.column("id")?.int
177+
_ = row.column("string1")?.string
178+
_ = row.column("string2")?.string
179+
_ = row.column("string3")?.string
180+
_ = row.column("string4")?.string
181+
_ = row.column("string5")?.string
182+
_ = row.column("int1")?.int
183+
_ = row.column("int2")?.int
184+
_ = row.column("int3")?.int
185+
_ = row.column("int4")?.int
186+
_ = row.column("int5")?.int
187+
_ = row.column("date1")?.date
188+
_ = row.column("date2")?.date
189+
_ = row.column("date3")?.date
190+
_ = row.column("date4")?.date
191+
_ = row.column("date5")?.date
192+
_ = row.column("uuid1")?.uuid
193+
_ = row.column("uuid2")?.uuid
194+
_ = row.column("uuid3")?.uuid
195+
_ = row.column("uuid4")?.uuid
196+
_ = row.column("uuid5")?.uuid
197+
}.wait()
198+
} catch {
199+
XCTFail("\(error)")
200+
}
201+
}
202+
}
203+
204+
func testPerformanceSelectLargeModelWithLongFieldNames() throws {
205+
let conn = try PostgresConnection.test(on: eventLoop).wait()
206+
defer { try! conn.close().wait() }
207+
208+
let fieldIndices = Array(1...20)
209+
let fieldNames = fieldIndices.map { "veryLongFieldNameVeryLongFieldName\($0)" }
210+
try prepareTableToMeasureSelectPerformance(
211+
rowCount: 50_000, batchSize: 200,
212+
schema: fieldNames.map { "\"\($0)\" int8" }.joined(separator: ", ") + ",",
213+
fixtureData: fieldIndices.map { PostgresData(int: $0) },
214+
on: self.eventLoop
215+
)
216+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
217+
218+
measure {
219+
do {
220+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
221+
_ = row.column("id")?.int
222+
for fieldName in fieldNames {
223+
_ = row.column(fieldName)?.int
224+
}
225+
}.wait()
226+
} catch {
227+
XCTFail("\(error)")
228+
}
229+
}
230+
}
231+
232+
func testPerformanceSelectHugeModel() throws {
233+
let conn = try PostgresConnection.test(on: eventLoop).wait()
234+
defer { try! conn.close().wait() }
235+
236+
let fieldIndices = Array(1...100)
237+
let fieldNames = fieldIndices.map { "int\($0)" }
238+
try prepareTableToMeasureSelectPerformance(
239+
rowCount: 10_000, batchSize: 200,
240+
schema: fieldNames.map { "\"\($0)\" int8" }.joined(separator: ", ") + ",",
241+
fixtureData: fieldIndices.map { PostgresData(int: $0) },
242+
on: self.eventLoop
243+
)
244+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
245+
246+
measure {
247+
do {
248+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
249+
_ = row.column("id")?.int
250+
for fieldName in fieldNames {
251+
_ = row.column(fieldName)?.int
252+
}
253+
}.wait()
254+
} catch {
255+
XCTFail("\(error)")
256+
}
257+
}
258+
}
259+
260+
}
261+
262+
private func prepareTableToMeasureSelectPerformance(
263+
rowCount: Int,
264+
batchSize: Int = 1_000,
265+
schema: String,
266+
fixtureData: [PostgresData],
267+
on eventLoop: EventLoop,
268+
file: StaticString = #file,
269+
line: UInt = #line
270+
) throws {
271+
XCTAssertEqual(rowCount % batchSize, 0, "`rowCount` must be a multiple of `batchSize`", file: file, line: line)
272+
let conn = try PostgresConnection.test(on: eventLoop).wait()
273+
defer { try! conn.close().wait() }
274+
275+
_ = try conn.simpleQuery("DROP TABLE IF EXISTS \"measureSelectPerformance\"").wait()
276+
_ = try conn.simpleQuery("""
277+
CREATE TABLE "measureSelectPerformance" (
278+
"id" int8 NOT NULL,
279+
\(schema)
280+
PRIMARY KEY ("id")
281+
);
282+
""").wait()
283+
284+
// Batch `batchSize` inserts into one for better insert performance.
285+
let totalArgumentsPerRow = fixtureData.count + 1
286+
let insertArgumentsPlaceholder = (0..<batchSize).map { indexInBatch in
287+
"("
288+
+ (0..<totalArgumentsPerRow).map { argumentIndex in "$\(indexInBatch * totalArgumentsPerRow + argumentIndex + 1)" }
289+
.joined(separator: ", ")
290+
+ ")"
291+
}.joined(separator: ", ")
292+
let insertQuery = "INSERT INTO \"measureSelectPerformance\" VALUES \(insertArgumentsPlaceholder)"
293+
var batchedFixtureData = Array(repeating: [PostgresData(int: 0)] + fixtureData, count: batchSize).flatMap { $0 }
294+
for batchIndex in 0..<(rowCount / batchSize) {
295+
for indexInBatch in 0..<batchSize {
296+
let rowIndex = batchIndex * batchSize + indexInBatch
297+
batchedFixtureData[indexInBatch * totalArgumentsPerRow] = PostgresData(int: rowIndex)
298+
}
299+
_ = try conn.query(insertQuery, batchedFixtureData).wait()
300+
}
301+
}
302+

0 commit comments

Comments
 (0)