如何在 XCTest 中提取 measureBlock 测量的性能指标
How to extract performance metrics measured by measureBlock in XCTest
我有一个简单的测试功能,它会点击一个按钮来测量性能。我正在使用 XCTest。在 measureBlock returns 之后,我可以在控制台上看到一堆性能指标。我想在测试程序中获取它,以便我可以通过编程方式在其他地方填充数据。在测试控制台上查看测试数据被证明很慢,因为我有很多测试用例。
- (void)testUseMeasureBlock {
XCUIElement *launchTest1Button = [[XCUIApplication alloc] init].buttons[@"Launch Test 1"];
void (^blockToMeasure)(void) = ^void(void) {
[launchTest1Button tap];
};
// Run once to warm up any potential caching properties
@autoreleasepool {
blockToMeasure();
}
// Now measure the block
[self measureBlock:blockToMeasure];
/// Collect the measured metrics and send somewhere.
当我们 运行 测试时它打印:
measured [Time, seconds] average: 0.594, relative standard deviation: 0.517%, values: [0.602709, 0.593631, 0.593004, 0.592350, 0.596199, 0.593807, 0.591444, 0.593460, 0.592648, 0.592769],
如果我能得到平均时间,那现在就足够了。
由于没有 API 来获取此数据,您可以通过管道传输 stderr
流并解析测试日志以获取所需的信息,例如平均时间。例如,您可以使用下一种方法:
@interface MeasureParser : NSObject
@property (nonatomic) NSPipe* pipe;
@property (nonatomic) NSRegularExpression* regex;
@property (nonatomic) NSMutableDictionary* results;
@end
@implementation MeasureParser
- (instancetype)init {
self = [super self];
if (self) {
self.pipe = NSPipe.pipe;
self.results = [NSMutableDictionary new];
let pattern = [NSString stringWithFormat:@"[^']+'\S+\s([^\]]+)\]'\smeasured\s\[Time,\sseconds\]\saverage:\s([^,]+)"];
NSError* error = nil;
self.regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
if (error) {
return nil;
}
}
return self;
}
- (void)capture:(void (^)(void))block {
// Save original output
int original = dup(STDERR_FILENO);
setvbuf(stderr, nil, _IONBF, 0);
dup2(self.pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);
__weak let wself = self;
self.pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) {
var *str = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
let firstMatch = [wself.regex firstMatchInString:str options:NSMatchingReportCompletion range:NSMakeRange(0, str.length)];
if (firstMatch) {
let name = [str substringWithRange:[firstMatch rangeAtIndex:1]];
let average = [str substringWithRange:[firstMatch rangeAtIndex:2]];
wself.results[name] = average;
}
// Print to stdout because stderr is piped
printf("%s", [str cStringUsingEncoding:NSUTF8StringEncoding]);
};
block();
// Revert
fflush(stderr);
dup2(original, STDERR_FILENO);
close(original);
}
@end
使用方法:
- (void)testPerformanceExample {
let measureParser = [MeasureParser new];
[measureParser capture:^{
[self measureBlock:^{
// Put the code you want to measure the time of here.
sleep(1);
}];
}];
NSLog(@"%@", measureParser.results);
}
// Outputs
{
testPerformanceExample = "1.001";
}
Swift 5 个版本
final class MeasureParser {
let pipe: Pipe = Pipe()
let regex: NSRegularExpression?
let results: NSMutableDictionary = NSMutableDictionary()
init() {
self.regex = try? NSRegularExpression(
pattern: "\[(Clock Monotonic Time|CPU Time|Memory Peak Physical|Memory Physical|CPU Instructions Retired|Disk Logical Writes|CPU Cycles), (s|kB|kI|kC)\] average: ([0-9\.]*),",
options: .caseInsensitive)
}
func capture(completion: @escaping () -> Void) {
let original = dup(STDERR_FILENO)
setvbuf(stderr, nil, _IONBF, 0)
dup2(self.pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
self.pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
guard self != nil else { return }
let data = handle.availableData
let str = String(data: data, encoding: .utf8) ?? "<Non-ascii data of size\(data.count)>\n"
self!.fetchAndSaveMetrics(str)
// Print to stdout because stderr is piped
if let copy = (str as NSString?)?.cString(using: String.Encoding.utf8.rawValue) {
print("\(copy)")
}
}
completion()
fflush(stderr)
dup2(original, STDERR_FILENO)
close(original)
}
private func fetchAndSaveMetrics(_ str: String) {
guard let mRegex = self.regex else { return }
let matches = mRegex.matches(in: str, options: .reportCompletion, range: NSRange(location: 0, length: str.count))
matches.forEach {
let nameIndex = Range([=10=].range(at: 1), in: str)
let averageIndex = Range([=10=].range(at: 3), in: str)
if nameIndex != nil && averageIndex != nil {
let name = str[nameIndex!]
let average = str[averageIndex!]
self.results[name] = average
}
}
}
}
使用方法:
import XCTest
final class MyUiTests: XCTestCase {
var app: XCUIApplication!
let measureParser = MeasureParser()
// MARK: - XCTestCase
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
override func tearDown() {
//FIXME: Just for debugging
print(self.measureParser.results)
print(self.measureParser.results["CPU Cycles"])
print(self.measureParser.results["CPU Instructions Retired"])
print(self.measureParser.results["CPU Time"])
print(self.measureParser.results["Clock Monotonic Time"])
print(self.measureParser.results["Disk Logical Writes"])
print(self.measureParser.results["Memory Peak Physical"])
print(self.measureParser.results["Memory Physical"])
}
// MARK: - Tests
func testListing() {
self.measureParser.capture { [weak self] in
guard let self = self else { return }
self.measureListingScroll()
}
}
// MARK: XCTest measures
private func measureListingScroll() {
measure(metrics: [XCTCPUMetric(), XCTClockMetric(), XCTMemoryMetric(), XCTStorageMetric()]) {
self.app.swipeUp()
self.app.swipeUp()
self.app.swipeUp()
}
}
}
我有一个简单的测试功能,它会点击一个按钮来测量性能。我正在使用 XCTest。在 measureBlock returns 之后,我可以在控制台上看到一堆性能指标。我想在测试程序中获取它,以便我可以通过编程方式在其他地方填充数据。在测试控制台上查看测试数据被证明很慢,因为我有很多测试用例。
- (void)testUseMeasureBlock {
XCUIElement *launchTest1Button = [[XCUIApplication alloc] init].buttons[@"Launch Test 1"];
void (^blockToMeasure)(void) = ^void(void) {
[launchTest1Button tap];
};
// Run once to warm up any potential caching properties
@autoreleasepool {
blockToMeasure();
}
// Now measure the block
[self measureBlock:blockToMeasure];
/// Collect the measured metrics and send somewhere.
当我们 运行 测试时它打印:
measured [Time, seconds] average: 0.594, relative standard deviation: 0.517%, values: [0.602709, 0.593631, 0.593004, 0.592350, 0.596199, 0.593807, 0.591444, 0.593460, 0.592648, 0.592769],
如果我能得到平均时间,那现在就足够了。
由于没有 API 来获取此数据,您可以通过管道传输 stderr
流并解析测试日志以获取所需的信息,例如平均时间。例如,您可以使用下一种方法:
@interface MeasureParser : NSObject
@property (nonatomic) NSPipe* pipe;
@property (nonatomic) NSRegularExpression* regex;
@property (nonatomic) NSMutableDictionary* results;
@end
@implementation MeasureParser
- (instancetype)init {
self = [super self];
if (self) {
self.pipe = NSPipe.pipe;
self.results = [NSMutableDictionary new];
let pattern = [NSString stringWithFormat:@"[^']+'\S+\s([^\]]+)\]'\smeasured\s\[Time,\sseconds\]\saverage:\s([^,]+)"];
NSError* error = nil;
self.regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
if (error) {
return nil;
}
}
return self;
}
- (void)capture:(void (^)(void))block {
// Save original output
int original = dup(STDERR_FILENO);
setvbuf(stderr, nil, _IONBF, 0);
dup2(self.pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);
__weak let wself = self;
self.pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) {
var *str = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
let firstMatch = [wself.regex firstMatchInString:str options:NSMatchingReportCompletion range:NSMakeRange(0, str.length)];
if (firstMatch) {
let name = [str substringWithRange:[firstMatch rangeAtIndex:1]];
let average = [str substringWithRange:[firstMatch rangeAtIndex:2]];
wself.results[name] = average;
}
// Print to stdout because stderr is piped
printf("%s", [str cStringUsingEncoding:NSUTF8StringEncoding]);
};
block();
// Revert
fflush(stderr);
dup2(original, STDERR_FILENO);
close(original);
}
@end
使用方法:
- (void)testPerformanceExample {
let measureParser = [MeasureParser new];
[measureParser capture:^{
[self measureBlock:^{
// Put the code you want to measure the time of here.
sleep(1);
}];
}];
NSLog(@"%@", measureParser.results);
}
// Outputs
{
testPerformanceExample = "1.001";
}
Swift 5 个版本
final class MeasureParser {
let pipe: Pipe = Pipe()
let regex: NSRegularExpression?
let results: NSMutableDictionary = NSMutableDictionary()
init() {
self.regex = try? NSRegularExpression(
pattern: "\[(Clock Monotonic Time|CPU Time|Memory Peak Physical|Memory Physical|CPU Instructions Retired|Disk Logical Writes|CPU Cycles), (s|kB|kI|kC)\] average: ([0-9\.]*),",
options: .caseInsensitive)
}
func capture(completion: @escaping () -> Void) {
let original = dup(STDERR_FILENO)
setvbuf(stderr, nil, _IONBF, 0)
dup2(self.pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
self.pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
guard self != nil else { return }
let data = handle.availableData
let str = String(data: data, encoding: .utf8) ?? "<Non-ascii data of size\(data.count)>\n"
self!.fetchAndSaveMetrics(str)
// Print to stdout because stderr is piped
if let copy = (str as NSString?)?.cString(using: String.Encoding.utf8.rawValue) {
print("\(copy)")
}
}
completion()
fflush(stderr)
dup2(original, STDERR_FILENO)
close(original)
}
private func fetchAndSaveMetrics(_ str: String) {
guard let mRegex = self.regex else { return }
let matches = mRegex.matches(in: str, options: .reportCompletion, range: NSRange(location: 0, length: str.count))
matches.forEach {
let nameIndex = Range([=10=].range(at: 1), in: str)
let averageIndex = Range([=10=].range(at: 3), in: str)
if nameIndex != nil && averageIndex != nil {
let name = str[nameIndex!]
let average = str[averageIndex!]
self.results[name] = average
}
}
}
}
使用方法:
import XCTest
final class MyUiTests: XCTestCase {
var app: XCUIApplication!
let measureParser = MeasureParser()
// MARK: - XCTestCase
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
override func tearDown() {
//FIXME: Just for debugging
print(self.measureParser.results)
print(self.measureParser.results["CPU Cycles"])
print(self.measureParser.results["CPU Instructions Retired"])
print(self.measureParser.results["CPU Time"])
print(self.measureParser.results["Clock Monotonic Time"])
print(self.measureParser.results["Disk Logical Writes"])
print(self.measureParser.results["Memory Peak Physical"])
print(self.measureParser.results["Memory Physical"])
}
// MARK: - Tests
func testListing() {
self.measureParser.capture { [weak self] in
guard let self = self else { return }
self.measureListingScroll()
}
}
// MARK: XCTest measures
private func measureListingScroll() {
measure(metrics: [XCTCPUMetric(), XCTClockMetric(), XCTMemoryMetric(), XCTStorageMetric()]) {
self.app.swipeUp()
self.app.swipeUp()
self.app.swipeUp()
}
}
}