Skip to content

Commit 46a8390

Browse files
authored
Merge pull request #18124 from palimondo/fluctuation-of-the-pupil
[benchmark] Measure memory with rusage and a TON of gardening
2 parents 149437b + 362f925 commit 46a8390

File tree

5 files changed

+618
-359
lines changed

5 files changed

+618
-359
lines changed

benchmark/utils/ArgParse.swift

Lines changed: 213 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,67 +12,227 @@
1212

1313
import Foundation
1414

15-
public struct Arguments {
16-
public var progName: String
17-
public var positionalArgs: [String]
18-
public var optionalArgsMap: [String : String]
19-
20-
init(_ pName: String, _ posArgs: [String], _ optArgsMap: [String : String]) {
21-
progName = pName
22-
positionalArgs = posArgs
23-
optionalArgsMap = optArgsMap
15+
enum ArgumentError: Error {
16+
case missingValue(String)
17+
case invalidType(value: String, type: String, argument: String?)
18+
case unsupportedArgument(String)
19+
}
20+
21+
extension ArgumentError: CustomStringConvertible {
22+
public var description: String {
23+
switch self {
24+
case let .missingValue(key):
25+
return "missing value for '\(key)'"
26+
case let .invalidType(value, type, argument):
27+
return (argument == nil)
28+
? "'\(value)' is not a valid '\(type)'"
29+
: "'\(value)' is not a valid '\(type)' for '\(argument!)'"
30+
case let .unsupportedArgument(argument):
31+
return "unsupported argument '\(argument)'"
32+
}
2433
}
2534
}
2635

27-
/// Using CommandLine.arguments, returns an Arguments struct describing
28-
/// the arguments to this program. If we fail to parse arguments, we
29-
/// return nil.
36+
/// Type-checked parsing of the argument value.
3037
///
31-
/// We assume that optional switch args are of the form:
38+
/// - Returns: Typed value of the argument converted using the `parse` function.
3239
///
33-
/// --opt-name[=opt-value]
34-
/// -opt-name[=opt-value]
35-
///
36-
/// with opt-name and opt-value not containing any '=' signs. Any
37-
/// other option passed in is assumed to be a positional argument.
38-
public func parseArgs(_ validOptions: [String]? = nil)
39-
-> Arguments? {
40-
let progName = CommandLine.arguments[0]
41-
var positionalArgs = [String]()
42-
var optionalArgsMap = [String : String]()
43-
44-
// For each argument we are passed...
45-
var passThroughArgs = false
46-
for arg in CommandLine.arguments[1..<CommandLine.arguments.count] {
47-
// If the argument doesn't match the optional argument pattern. Add
48-
// it to the positional argument list and continue...
49-
if passThroughArgs || !arg.starts(with: "-") {
50-
positionalArgs.append(arg)
51-
continue
40+
/// - Throws: `ArgumentError.invalidType` when the conversion fails.
41+
func checked<T>(
42+
_ parse: (String) throws -> T?,
43+
_ value: String,
44+
argument: String? = nil
45+
) throws -> T {
46+
if let t = try parse(value) { return t }
47+
var type = "\(T.self)"
48+
if type.starts(with: "Optional<") {
49+
let s = type.index(after: type.index(of:"<")!)
50+
let e = type.index(before: type.endIndex) // ">"
51+
type = String(type[s ..< e]) // strip Optional< >
52+
}
53+
throw ArgumentError.invalidType(
54+
value: value, type: type, argument: argument)
55+
}
56+
57+
/// Parser that converts the program's command line arguments to typed values
58+
/// according to the parser's configuration, storing them in the provided
59+
/// instance of a value-holding type.
60+
class ArgumentParser<U> {
61+
private var result: U
62+
private var validOptions: [String] {
63+
return arguments.compactMap { $0.name }
64+
}
65+
private var arguments: [Argument] = []
66+
private let programName: String = {
67+
// Strip full path from the program name.
68+
let r = CommandLine.arguments[0].reversed()
69+
let ss = r[r.startIndex ..< (r.index(of:"/") ?? r.endIndex)]
70+
return String(ss.reversed())
71+
}()
72+
private var positionalArgs = [String]()
73+
private var optionalArgsMap = [String : String]()
74+
75+
/// Argument holds the name of the command line parameter, its help
76+
/// desciption and a rule that's applied to process it.
77+
///
78+
/// The the rule is typically a value processing closure used to convert it
79+
/// into given type and storing it in the parsing result.
80+
///
81+
/// See also: addArgument, parseArgument
82+
struct Argument {
83+
let name: String?
84+
let help: String?
85+
let apply: () throws -> ()
86+
}
87+
88+
/// ArgumentParser is initialized with an instance of a type that holds
89+
/// the results of the parsing of the individual command line arguments.
90+
init(into result: U) {
91+
self.result = result
92+
self.arguments += [
93+
Argument(name: "--help", help: "show this help message and exit",
94+
apply: printUsage)
95+
]
5296
}
53-
if arg == "--" {
54-
passThroughArgs = true
55-
continue
97+
98+
private func printUsage() {
99+
guard let _ = optionalArgsMap["--help"] else { return }
100+
let space = " "
101+
let maxLength = arguments.compactMap({ $0.name?.count }).max()!
102+
let padded = { (s: String) in
103+
" \(s)\(String(repeating:space, count: maxLength - s.count)) " }
104+
let f: (String, String) -> String = {
105+
"\(padded($0))\($1)"
106+
.split(separator: "\n")
107+
.joined(separator: "\n" + padded(""))
108+
}
109+
let positional = f("TEST", "name or number of the benchmark to measure")
110+
let optional = arguments.filter { $0.name != nil }
111+
.map { f($0.name!, $0.help ?? "") }
112+
.joined(separator: "\n")
113+
print(
114+
"""
115+
usage: \(programName) [--argument=VALUE] [TEST [TEST ...]]
116+
117+
positional arguments:
118+
\(positional)
119+
120+
optional arguments:
121+
\(optional)
122+
""")
123+
exit(0)
56124
}
57-
// Attempt to split it into two components separated by an equals sign.
58-
let components = arg.split(separator: "=")
59-
let optionName = String(components[0])
60-
if validOptions != nil && !validOptions!.contains(optionName) {
61-
print("Invalid option: \(arg)")
62-
return nil
125+
126+
/// Parses the command line arguments, returning the result filled with
127+
/// specified argument values or report errors and exit the program if
128+
/// the parsing fails.
129+
public func parse() -> U {
130+
do {
131+
try parseArgs() // parse the argument syntax
132+
try arguments.forEach { try $0.apply() } // type-check and store values
133+
return result
134+
} catch let error as ArgumentError {
135+
fputs("error: \(error)\n", stderr)
136+
exit(1)
137+
} catch {
138+
fflush(stdout)
139+
fatalError("\(error)")
140+
}
63141
}
64-
var optionVal : String
65-
switch components.count {
66-
case 1: optionVal = ""
67-
case 2: optionVal = String(components[1])
68-
default:
69-
// If we do not have two components at this point, we can not have
70-
// an option switch. This is an invalid argument. Bail!
71-
print("Invalid option: \(arg)")
72-
return nil
142+
143+
/// Using CommandLine.arguments, parses the structure of optional and
144+
/// positional arguments of this program.
145+
///
146+
/// We assume that optional switch args are of the form:
147+
///
148+
/// --opt-name[=opt-value]
149+
/// -opt-name[=opt-value]
150+
///
151+
/// with `opt-name` and `opt-value` not containing any '=' signs. Any
152+
/// other option passed in is assumed to be a positional argument.
153+
///
154+
/// - Throws: `ArgumentError.unsupportedArgument` on failure to parse
155+
/// the supported argument syntax.
156+
private func parseArgs() throws {
157+
158+
// For each argument we are passed...
159+
for arg in CommandLine.arguments[1..<CommandLine.arguments.count] {
160+
// If the argument doesn't match the optional argument pattern. Add
161+
// it to the positional argument list and continue...
162+
if !arg.starts(with: "-") {
163+
positionalArgs.append(arg)
164+
continue
165+
}
166+
// Attempt to split it into two components separated by an equals sign.
167+
let components = arg.split(separator: "=")
168+
let optionName = String(components[0])
169+
guard validOptions.contains(optionName) else {
170+
throw ArgumentError.unsupportedArgument(arg)
171+
}
172+
var optionVal : String
173+
switch components.count {
174+
case 1: optionVal = ""
175+
case 2: optionVal = String(components[1])
176+
default:
177+
// If we do not have two components at this point, we can not have
178+
// an option switch. This is an invalid argument. Bail!
179+
throw ArgumentError.unsupportedArgument(arg)
180+
}
181+
optionalArgsMap[optionName] = optionVal
182+
}
183+
}
184+
185+
/// Add a rule for parsing the specified argument.
186+
///
187+
/// Stores the type-erased invocation of the `parseArgument` in `Argument`.
188+
///
189+
/// Parameters:
190+
/// - name: Name of the command line argument. E.g.: `--opt-arg`.
191+
/// `nil` denotes positional arguments.
192+
/// - property: Property on the `result`, to store the value into.
193+
/// - defaultValue: Value used when the command line argument doesn't
194+
/// provide one.
195+
/// - help: Argument's description used when printing usage with `--help`.
196+
/// - parser: Function that converts the argument value to given type `T`.
197+
public func addArgument<T>(
198+
_ name: String?,
199+
_ property: WritableKeyPath<U, T>,
200+
defaultValue: T? = nil,
201+
help: String? = nil,
202+
parser: @escaping (String) throws -> T? = { _ in nil }
203+
) {
204+
arguments.append(Argument(name: name, help: help)
205+
{ try self.parseArgument(name, property, defaultValue, parser) })
73206
}
74-
optionalArgsMap[optionName] = optionVal
75-
}
76207

77-
return Arguments(progName, positionalArgs, optionalArgsMap)
208+
/// Process the specified command line argument.
209+
///
210+
/// For optional arguments that have a value we attempt to convert it into
211+
/// given type using the supplied parser, performing the type-checking with
212+
/// the `checked` function.
213+
/// If the value is empty the `defaultValue` is used instead.
214+
/// The typed value is finally stored in the `result` into the specified
215+
/// `property`.
216+
///
217+
/// For the optional positional arguments, the [String] is simply assigned
218+
/// to the specified property without any conversion.
219+
///
220+
/// See `addArgument` for detailed parameter descriptions.
221+
private func parseArgument<T>(
222+
_ name: String?,
223+
_ property: WritableKeyPath<U, T>,
224+
_ defaultValue: T?,
225+
_ parse: (String) throws -> T?
226+
) throws {
227+
if let name = name, let value = optionalArgsMap[name] {
228+
guard !value.isEmpty || defaultValue != nil
229+
else { throw ArgumentError.missingValue(name) }
230+
231+
result[keyPath: property] = (value.isEmpty)
232+
? defaultValue!
233+
: try checked(parse, value, argument: name)
234+
} else if name == nil {
235+
result[keyPath: property] = positionalArgs as! T
236+
}
237+
}
78238
}

0 commit comments

Comments
 (0)