PureYAML/Tests/PerformancePhaseTests.swift
Mihaela Mihaljevic 208b892019 feat(parser): harden diagnostics and streaming
Add structured validation diagnostics, JSON and Yams comparison gates, and lazy token/event parsing so production behavior is measured without retaining full intermediate parser arrays.
2026-06-06 07:57:48 +02:00

299 lines
9.5 KiB
Swift

import Foundation
@testable import PureYAML
import Testing
@Suite("Performance Phases")
struct PerformancePhaseTests {
@Test("Profiles parser phases for representative slow corpus seeds")
func test_profilesParserPhasesForRepresentativeSeeds() throws {
guard ProcessInfo.processInfo.environment["PUREYAML_RUN_PHASE_PROFILER"] == "1" else {
return
}
let measurements = try PerformancePhaseSeed.all.map(profile)
let slowestLazyParse = measurements.map(\.lazyParse.seconds).max() ?? 0
let slowestMaterializedPipeline = measurements.map(\.materializedPipelineSeconds).max() ?? 0
let report: [String: Any] = [
"summary": [
"seeds": measurements.count,
"slowestLazyParseSeconds": slowestLazyParse,
"slowestMaterializedPipelineSeconds": slowestMaterializedPipeline,
],
"measurements": measurements.map(\.object),
]
let artifactURL = try performancePhaseArtifactURL()
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
try data.write(to: artifactURL)
print("PureYAML phase profiler seeds: \(measurements.count)")
for measurement in measurements {
print(measurement.summaryLine)
}
print("Report: \(artifactURL.path)")
}
}
private struct PerformancePhaseSeed {
var id: String
var localPath: String
var iterations: Int
static let all = [
PerformancePhaseSeed(
id: "cert-manager-crd",
localPath: "cert-manager-certificate-crd.yaml",
iterations: 20,
),
PerformancePhaseSeed(
id: "stripe-yaml",
localPath: "api-guru-stripe-openapi.yaml",
iterations: 2,
),
PerformancePhaseSeed(
id: "zoom-yaml",
localPath: "api-guru-zoom-openapi.yaml",
iterations: 2,
),
]
}
private struct PhaseTiming {
var seconds: TimeInterval
var units: Int
var object: [String: Any] {
[
"seconds": seconds,
"units": units,
]
}
}
private struct PhaseMeasurement {
var seed: PerformancePhaseSeed
var bytes: Int
var scanner: PhaseTiming
var eventParser: PhaseTiming
var composer: PhaseTiming
var lazyParse: PhaseTiming
var validation: PhaseTiming
var dumping: PhaseTiming
var materializedPipelineSeconds: TimeInterval {
scanner.seconds + eventParser.seconds + composer.seconds
}
var object: [String: Any] {
[
"id": seed.id,
"localPath": seed.localPath,
"bytes": bytes,
"iterations": seed.iterations,
"scanner": scanner.object,
"eventParser": eventParser.object,
"composer": composer.object,
"lazyParse": lazyParse.object,
"validation": validation.object,
"dumping": dumping.object,
"materializedPipelineSeconds": materializedPipelineSeconds,
]
}
var summaryLine: String {
[
seed.id,
"bytes=\(bytes)",
"iter=\(seed.iterations)",
"scan=\(format(scanner.seconds))s",
"events=\(format(eventParser.seconds))s",
"compose=\(format(composer.seconds))s",
"lazy=\(format(lazyParse.seconds))s",
"validate=\(format(validation.seconds))s",
"dump=\(format(dumping.seconds))s",
].joined(separator: " ")
}
}
private struct WarmedPerformanceInputs {
var tokens: [PureYAML.Parsing.Token]
var events: [PureYAML.Parsing.Event]
var documents: [PureYAML.Stream.Document]
}
private func profile(_ seed: PerformancePhaseSeed) throws -> PhaseMeasurement {
let yaml = try String(contentsOf: fixtureURL(for: seed), encoding: .utf8)
let parser = PureYAML.Parsing.Parser()
let warmed = try warmup(yaml: yaml, parser: parser)
let scanner = try timeScanner(yaml: yaml, iterations: seed.iterations)
let eventParser = try timeEventParser(tokens: warmed.tokens, parser: parser, iterations: seed.iterations)
let composer = try timeComposer(events: warmed.events, parser: parser, iterations: seed.iterations)
let lazyParse = try timeLazyParse(yaml: yaml, iterations: seed.iterations)
let validation = timeValidation(documents: warmed.documents, iterations: seed.iterations)
let dumping = timeDumping(documents: warmed.documents, iterations: seed.iterations)
return PhaseMeasurement(
seed: seed,
bytes: yaml.utf8.count,
scanner: .init(seconds: scanner.seconds, units: scanner.value),
eventParser: .init(seconds: eventParser.seconds, units: eventParser.value),
composer: .init(seconds: composer.seconds, units: composer.value),
lazyParse: .init(seconds: lazyParse.seconds, units: lazyParse.value),
validation: .init(seconds: validation.seconds, units: validation.value),
dumping: .init(seconds: dumping.seconds, units: dumping.value),
)
}
private func warmup(
yaml: String,
parser: PureYAML.Parsing.Parser,
) throws -> WarmedPerformanceInputs {
let tokens = try PureYAML.Parsing.Scanner().scan(yaml)
var eventParser = PureYAML.Parsing.TokenEventParser(tokens: tokens, scalarParser: parser)
let events = try eventParser.parse()
var composer = PureYAML.Parsing.EventComposer(events: events, scalarParser: parser)
let documents = try composer.composeStream()
_ = PureYAML.Validation.Validator().collect(documents)
_ = PureYAML.dump(documents)
_ = try PureYAML.parseStream(yaml)
return WarmedPerformanceInputs(tokens: tokens, events: events, documents: documents)
}
private func timeScanner(
yaml: String,
iterations: Int,
) throws -> (seconds: TimeInterval, value: Int) {
try timed {
var units = 0
for _ in 0 ..< iterations {
units += try PureYAML.Parsing.Scanner().scan(yaml).count
}
return units
}
}
private func timeEventParser(
tokens: [PureYAML.Parsing.Token],
parser: PureYAML.Parsing.Parser,
iterations: Int,
) throws -> (seconds: TimeInterval, value: Int) {
try timed {
var units = 0
for _ in 0 ..< iterations {
var eventParser = PureYAML.Parsing.TokenEventParser(tokens: tokens, scalarParser: parser)
units += try eventParser.parse().count
}
return units
}
}
private func timeComposer(
events: [PureYAML.Parsing.Event],
parser: PureYAML.Parsing.Parser,
iterations: Int,
) throws -> (seconds: TimeInterval, value: Int) {
try timed {
var units = 0
for _ in 0 ..< iterations {
var composer = PureYAML.Parsing.EventComposer(events: events, scalarParser: parser)
units += try composer.composeStream().count
}
return units
}
}
private func timeLazyParse(
yaml: String,
iterations: Int,
) throws -> (seconds: TimeInterval, value: Int) {
try timed {
var units = 0
for _ in 0 ..< iterations {
units += try PureYAML.parseStream(yaml).count
}
return units
}
}
private func timeValidation(
documents: [PureYAML.Stream.Document],
iterations: Int,
) -> (seconds: TimeInterval, value: Int) {
timed {
var units = 0
for _ in 0 ..< iterations {
units += PureYAML.Validation.Validator().collect(documents).issues.count
}
return units
}
}
private func timeDumping(
documents: [PureYAML.Stream.Document],
iterations: Int,
) -> (seconds: TimeInterval, value: Int) {
timed {
var units = 0
for _ in 0 ..< iterations {
units += PureYAML.dump(documents).utf8.count
}
return units
}
}
private func fixtureURL(for seed: PerformancePhaseSeed) throws -> URL {
try #require(Bundle.module.url(
forResource: seed.resourceName,
withExtension: seed.resourceExtension,
subdirectory: "Fixtures/real-yaml",
))
}
private func performancePhaseArtifactURL() throws -> URL {
if let path = ProcessInfo.processInfo.environment["PUREYAML_PHASE_PROFILER_ARTIFACT"] {
let url = URL(fileURLWithPath: path)
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true,
)
return url
}
let url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
.appendingPathComponent(".build")
.appendingPathComponent("pureyaml-artifacts")
.appendingPathComponent("performance")
.appendingPathComponent("phase-profile.json")
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true,
)
return url
}
private func timed(_ body: () throws -> Int) rethrows -> (seconds: TimeInterval, value: Int) {
let start = DispatchTime.now().uptimeNanoseconds
let value = try body()
let end = DispatchTime.now().uptimeNanoseconds
return (Double(end - start) / 1_000_000_000, value)
}
private func format(_ value: Double) -> String {
String(format: "%.3f", value)
}
private extension PerformancePhaseSeed {
var resourceName: String {
guard let dotIndex = localPath.lastIndex(of: ".") else {
return localPath
}
return String(localPath[..<dotIndex])
}
var resourceExtension: String {
guard let dotIndex = localPath.lastIndex(of: ".") else {
return ""
}
return String(localPath[localPath.index(after: dotIndex)...])
}
}