mirror of
https://github.com/CocoaLumberjack/CocoaLumberjack.git
synced 2026-05-07 20:12:46 +00:00
428 lines
14 KiB
Objective-C
428 lines
14 KiB
Objective-C
//
|
|
// PerformanceTesting.m
|
|
// Benchmarking
|
|
//
|
|
// CocoaLumberjack Benchmarking
|
|
//
|
|
|
|
#import "PerformanceTesting.h"
|
|
#import <CocoaLumberjack/CocoaLumberjack.h>
|
|
|
|
#import "BaseNSLogging.h"
|
|
#import "StaticLogging.h"
|
|
#import "DynamicLogging.h"
|
|
|
|
// Define the number of times each test is performed.
|
|
// Due to various factors, the execution time of each test run may vary quite a bit.
|
|
// Each test should be executed several times in order to arrive at a stable average.
|
|
#define NUMBER_OF_RUNS 20
|
|
|
|
/**
|
|
* The idea behind the benchmark tests is simple:
|
|
* How does the logging framework compare to basic NSLog statements?
|
|
*
|
|
* However, due to the complexity of the logging framework and its various configuration options,
|
|
* it is more complicated than a single test. Thus the testing is broken up as follows:
|
|
*
|
|
* - 3 Suites, each representing a different configuration of the logging framework
|
|
* - 5 Tests, run within each suite.
|
|
*
|
|
* The suites are described below in the configureLoggingForSuiteX methods.
|
|
* The tests are described in the various logging files, such as StaticLogging or DynamicLogging.
|
|
* Notice that these file are almost exactly the same.
|
|
*
|
|
* BaseNSLogging defines the log methods to use NSLog (the base we are comparing against).
|
|
* StaticLogging uses a 'const' log level, meaning the compiler will prune log statements (in release mode).
|
|
* DynamicLogging use a non-const log level, meaning each log statement will incur an integer comparison penalty.
|
|
**/
|
|
|
|
@implementation PerformanceTesting
|
|
|
|
static NSTimeInterval base[5][3]; // [test][min,avg,max]
|
|
|
|
static NSTimeInterval fmwk[3][2][5][3]; // [suite][file][test][min,avg,max]
|
|
|
|
static DDFileLogger *fileLogger = nil;
|
|
|
|
+ (void)initialize
|
|
{
|
|
bzero(&base, sizeof(base));
|
|
bzero(&fmwk, sizeof(fmwk));
|
|
}
|
|
|
|
+ (DDFileLogger *)fileLogger
|
|
{
|
|
if (fileLogger == nil)
|
|
{
|
|
fileLogger = [[DDFileLogger alloc] init];
|
|
|
|
fileLogger.maximumFileSize = (1024 * 1024 * 1); // 1 MB
|
|
fileLogger.rollingFrequency = (60 * 60 * 24); // 24 Hours
|
|
|
|
fileLogger.logFileManager.maximumNumberOfLogFiles = 4;
|
|
}
|
|
|
|
return fileLogger;
|
|
}
|
|
|
|
+ (id<DDLogger>)ttyLogger
|
|
{
|
|
return [DDTTYLogger sharedInstance];
|
|
}
|
|
|
|
/**
|
|
* Suite 1 - Logging to Console only.
|
|
**/
|
|
+ (void)configureLoggingForSuite1
|
|
{
|
|
[DDLog removeAllLoggers];
|
|
|
|
[DDLog addLogger:[DDOSLogger sharedInstance]];
|
|
[DDLog addLogger:[self ttyLogger]];
|
|
}
|
|
|
|
/**
|
|
* Suite 2 - Logging to File only.
|
|
*
|
|
* We attempt to configure the logging so it will be forced to roll the log files during the test.
|
|
* Rolling the log files requires creating and opening a new file.
|
|
* This could be a performance hit, so we want our benchmark to take this into account.
|
|
**/
|
|
+ (void)configureLoggingForSuite2
|
|
{
|
|
[DDLog removeAllLoggers];
|
|
|
|
[DDLog addLogger:[self fileLogger]];
|
|
}
|
|
|
|
/**
|
|
* Suite 3 - Logging to Console & File.
|
|
**/
|
|
+ (void)configureLoggingForSuite3
|
|
{
|
|
[DDLog removeAllLoggers];
|
|
|
|
[DDLog addLogger:[DDOSLogger sharedInstance]];
|
|
[DDLog addLogger:[self ttyLogger]];
|
|
[DDLog addLogger:[self fileLogger]];
|
|
}
|
|
|
|
+ (void)executeTestsWithBase:(BOOL)exeBase framework:(BOOL)exeFramework frameworkSuite:(int)suiteNum
|
|
{
|
|
if (!exeBase && !exeFramework) return;
|
|
|
|
int sn = suiteNum - 1; // Zero-indexed for array
|
|
|
|
int i, j, k;
|
|
|
|
int start = exeBase ? 0 : 1;
|
|
int finish = exeFramework ? 3 : 1;
|
|
|
|
for (i = start; i < finish; i++)
|
|
{
|
|
Class class;
|
|
switch (i)
|
|
{
|
|
case 0 : class = [BaseNSLogging class]; break;
|
|
case 1 : class = [StaticLogging class]; break;
|
|
default : class = [DynamicLogging class]; break;
|
|
}
|
|
|
|
for (j = 0; j < 5; j++)
|
|
{
|
|
NSTimeInterval min = DBL_MAX;
|
|
NSTimeInterval max = DBL_MIN;
|
|
|
|
NSTimeInterval total = 0.0;
|
|
|
|
for (k = 0; k < NUMBER_OF_RUNS; k++)
|
|
{
|
|
@autoreleasepool {
|
|
|
|
NSDate *startDate = [NSDate date];
|
|
|
|
switch (j)
|
|
{
|
|
case 0 : [class performSelector:@selector(speedTest0)]; break;
|
|
case 1 : [class performSelector:@selector(speedTest1)]; break;
|
|
case 2 : [class performSelector:@selector(speedTest2)]; break;
|
|
case 3 : [class performSelector:@selector(speedTest3)]; break;
|
|
default : [class performSelector:@selector(speedTest4)]; break;
|
|
}
|
|
|
|
NSTimeInterval result = [startDate timeIntervalSinceNow] * -1.0;
|
|
|
|
min = MIN(min, result);
|
|
max = MAX(max, result);
|
|
|
|
total += result;
|
|
|
|
}
|
|
[DDLog flushLog];
|
|
}
|
|
|
|
if (i == 0)
|
|
{
|
|
// Base
|
|
base[j][0] = min;
|
|
base[j][1] = total / (double)NUMBER_OF_RUNS;
|
|
base[j][2] = max;
|
|
}
|
|
else
|
|
{
|
|
// Framework
|
|
fmwk[sn][i-1][j][0] = min;
|
|
fmwk[sn][i-1][j][1] = total / (double)NUMBER_OF_RUNS;
|
|
fmwk[sn][i-1][j][2] = max;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (NSString *)printableResultsForSuite:(int)suiteNum
|
|
{
|
|
int sn = suiteNum - 1; // Zero-indexed for array
|
|
|
|
NSMutableString *str = [NSMutableString stringWithCapacity:2000];
|
|
|
|
[str appendFormat:@"Results are given as [min][avg][max] calculated over the course of %i runs.", NUMBER_OF_RUNS];
|
|
[str appendString:@"\n\n"];
|
|
|
|
[str appendString:@"Test 0:\n"];
|
|
[str appendFormat:@"Execute %i log statements.\n", SPEED_TEST_0_COUNT];
|
|
[str appendString:@"The log statement is above the log level threshold, and will not execute.\n"];
|
|
[str appendString:@"The StaticLogging class will compile it out (in release mode).\n"];
|
|
[str appendString:@"The DynamicLogging class will require a single integer comparison.\n"];
|
|
[str appendString:@"\n"];
|
|
[str appendFormat:@"BaseNSLogging :[%.4f][%.4f][%.4f]\n", base[0][0], base[0][1], base[0][2]];
|
|
[str appendFormat:@"StaticLogging :[%.4f][%.4f][%.4f]\n", fmwk[sn][0][0][0], fmwk[sn][0][0][1], fmwk[sn][0][0][2]];
|
|
[str appendFormat:@"DynamicLogging:[%.4f][%.4f][%.4f]\n", fmwk[sn][1][0][0], fmwk[sn][1][0][1], fmwk[sn][1][0][2]];
|
|
[str appendString:@"\n\n\n"];
|
|
|
|
[str appendString:@"Test 1:\n"];
|
|
[str appendFormat:@"Execute %i log statements.\n", SPEED_TEST_1_COUNT];
|
|
[str appendString:@"The log statement is at or below the log level threshold, and will execute.\n"];
|
|
[str appendString:@"The logging framework will execute the statements Asynchronously.\n"];
|
|
[str appendString:@"\n"];
|
|
[str appendFormat:@"BaseNSLogging :[%.4f][%.4f][%.4f]\n", base[1][0], base[1][1], base[1][2]];
|
|
[str appendFormat:@"StaticLogging :[%.4f][%.4f][%.4f]\n", fmwk[sn][0][1][0], fmwk[sn][0][1][1], fmwk[sn][0][1][2]];
|
|
[str appendFormat:@"DynamicLogging:[%.4f][%.4f][%.4f]\n", fmwk[sn][1][1][0], fmwk[sn][1][1][1], fmwk[sn][1][1][2]];
|
|
[str appendString:@"\n\n\n"];
|
|
|
|
[str appendString:@"Test 2:\n"];
|
|
[str appendFormat:@"Execute %i log statements.\n", SPEED_TEST_2_COUNT];
|
|
[str appendString:@"The log statement is at or below the log level threshold, and will execute.\n"];
|
|
[str appendString:@"The logging framework will execute the statements Synchronously.\n"];
|
|
[str appendString:@"\n"];
|
|
[str appendFormat:@"BaseNSLogging :[%.4f][%.4f][%.4f]\n", base[2][0], base[2][1], base[2][2]];
|
|
[str appendFormat:@"StaticLogging :[%.4f][%.4f][%.4f]\n", fmwk[sn][0][2][0], fmwk[sn][0][2][1], fmwk[sn][0][2][2]];
|
|
[str appendFormat:@"DynamicLogging:[%.4f][%.4f][%.4f]\n", fmwk[sn][1][2][0], fmwk[sn][1][2][1], fmwk[sn][1][2][2]];
|
|
[str appendString:@"\n\n\n"];
|
|
|
|
[str appendString:@"Test 3:"];
|
|
[str appendFormat:@"Execute %i log statements per level.\n", SPEED_TEST_3_COUNT];
|
|
[str appendString:@"This is designed to mimic what might happen in a regular application.\n"];
|
|
[str appendString:@"25% will be above log level threshold and will be filtered out.\n"];
|
|
[str appendString:@"50% will execute Asynchronously.\n"];
|
|
[str appendString:@"25% will execute Synchronously.\n"];
|
|
[str appendString:@"\n"];
|
|
[str appendFormat:@"BaseNSLogging :[%.4f][%.4f][%.4f]\n", base[3][0], base[3][1], base[3][2]];
|
|
[str appendFormat:@"StaticLogging :[%.4f][%.4f][%.4f]\n", fmwk[sn][0][3][0], fmwk[sn][0][3][1], fmwk[sn][0][3][2]];
|
|
[str appendFormat:@"DynamicLogging:[%.4f][%.4f][%.4f]\n", fmwk[sn][1][3][0], fmwk[sn][1][3][1], fmwk[sn][1][3][2]];
|
|
[str appendString:@"\n\n\n"];
|
|
|
|
float total = 0.0F;
|
|
total += SPEED_TEST_4_VERBOSE_COUNT;
|
|
total += SPEED_TEST_4_INFO_COUNT;
|
|
total += SPEED_TEST_4_WARN_COUNT;
|
|
total += SPEED_TEST_4_ERROR_COUNT;
|
|
|
|
float verbose = (float)SPEED_TEST_4_VERBOSE_COUNT / total * 100.0F;
|
|
float info = (float)SPEED_TEST_4_INFO_COUNT / total * 100.0F;
|
|
float warn = (float)SPEED_TEST_4_WARN_COUNT / total * 100.0F;
|
|
float error = (float)SPEED_TEST_4_ERROR_COUNT / total * 100.0F;
|
|
|
|
[str appendString:@"Test 4:\n"];
|
|
[str appendString:@"Similar to test 3, designed to mimic a real application\n"];
|
|
[str appendFormat:@"Execute %i log statements in total.\n", (int)total];
|
|
[str appendFormat:@"%04.1f%% will be above log level threshold and will be filtered out.\n", (double)verbose];
|
|
[str appendFormat:@"%04.1f%% will execute Asynchronously.\n", (double)(info + warn)];
|
|
[str appendFormat:@"%04.1f%% will execute Synchronously.\n", (double)error];
|
|
[str appendString:@"\n"];
|
|
[str appendFormat:@"BaseNSLogging :[%.4f][%.4f][%.4f]\n", base[4][0], base[4][1], base[4][2]];
|
|
[str appendFormat:@"StaticLogging :[%.4f][%.4f][%.4f]\n", fmwk[sn][0][4][0], fmwk[sn][0][4][1], fmwk[sn][0][4][2]];
|
|
[str appendFormat:@"DynamicLogging:[%.4f][%.4f][%.4f]\n", fmwk[sn][1][4][0], fmwk[sn][1][4][1], fmwk[sn][1][4][2]];
|
|
[str appendString:@"\n\n\n"];
|
|
|
|
return str;
|
|
}
|
|
|
|
+ (NSString *)csvResults
|
|
{
|
|
NSMutableString *str = [NSMutableString stringWithCapacity:1000];
|
|
|
|
// What are we trying to do here?
|
|
//
|
|
// What we ultimately want is to compare the performance of the framework against the baseline.
|
|
// This means we want to see the performance of the baseline for test 1,
|
|
// and then right next to it we want to see the performance of the framework with each various configuration.
|
|
//
|
|
// So we want it to kinda look like this for Test 1:
|
|
//
|
|
// Base, [min], [avg], [max]
|
|
// Suite 1 - Static, [min], [avg], [max]
|
|
// Suite 1 - Dynamic, [min], [avg], [max]
|
|
// Suite 2 - Static, [min], [avg], [max]
|
|
// Suite 2 - Dynamic, [min], [avg], [max]
|
|
// Suite 3 - Static, [min], [avg], [max]
|
|
// Suite 3 - Dynamic, [min], [avg], [max]
|
|
//
|
|
// This will import into Excel just fine.
|
|
// However, I couldn't get Excel to make a decent looking graph with the data.
|
|
// Perhaps I'm just not familiar enough with Excel.
|
|
// But I was able to download OmniGraphSketcher,
|
|
// and figure out how to create an awesome looking graph in less than 15 minutes.
|
|
// And thus OmniGraphSketcher wins for me.
|
|
// The only catch is that it wants to import the data with numbers instead of names.
|
|
// So I need to convert the output to look like this:
|
|
//
|
|
// 0, [min], [avg], [max]
|
|
// 1, [min], [avg], [max]
|
|
// 2, [min], [avg], [max]
|
|
// 3, [min], [avg], [max]
|
|
// 4, [min], [avg], [max]
|
|
// 5, [min], [avg], [max]
|
|
// 6, [min], [avg], [max]
|
|
//
|
|
// I can then import the data into OmniGraphSketcher, and rename the X-axis points.
|
|
|
|
// static NSTimeInterval base[5][3]; // [test][min,avg,max]
|
|
//
|
|
// static NSTimeInterval fmwk[3][2][5][3]; // [suite][file][test][min,avg,max]
|
|
|
|
int row = 0;
|
|
int suite, file, test;
|
|
|
|
for (test = 0; test < 5; test++)
|
|
{
|
|
[str appendFormat:@"%i, %.4f, %.4f, %.4f\n", row++, base[test][0], base[test][1], base[test][2]];
|
|
|
|
for (suite = 0; suite < 3; suite++)
|
|
{
|
|
for (file = 0; file < 2; file++)
|
|
{
|
|
[str appendFormat:@"%i, %.4f, %.4f, %.4f\n", row++,
|
|
fmwk[suite][file][test][0], fmwk[suite][file][test][1], fmwk[suite][file][test][2]];
|
|
}
|
|
}
|
|
|
|
row += 3;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
+ (void)startPerformanceTests
|
|
{
|
|
BOOL runBase = YES;
|
|
BOOL runSuite1 = YES;
|
|
BOOL runSuite2 = YES;
|
|
BOOL runSuite3 = YES;
|
|
|
|
if (!runBase && !runSuite1 && !runSuite2 && !runSuite3)
|
|
{
|
|
// Nothing to do, all suites disabled
|
|
return;
|
|
}
|
|
|
|
NSLog(@"Preparing to start performance tests...");
|
|
NSLog(@"The results will be printed nicely when all logging has completed.\n\n");
|
|
|
|
[NSThread sleepForTimeInterval:3.0];
|
|
|
|
if (runBase)
|
|
{
|
|
[self executeTestsWithBase:YES framework:NO frameworkSuite:0];
|
|
}
|
|
|
|
NSString *printableResults1 = nil;
|
|
NSString *printableResults2 = nil;
|
|
NSString *printableResults3 = nil;
|
|
|
|
if (runSuite1)
|
|
{
|
|
[self configureLoggingForSuite1];
|
|
[self executeTestsWithBase:NO framework:YES frameworkSuite:1];
|
|
|
|
printableResults1 = [self printableResultsForSuite:1];
|
|
|
|
NSLog(@"\n\n\n\n");
|
|
}
|
|
if (runSuite2)
|
|
{
|
|
[self configureLoggingForSuite2];
|
|
[self executeTestsWithBase:NO framework:YES frameworkSuite:2];
|
|
|
|
printableResults2 = [self printableResultsForSuite:2];
|
|
|
|
NSLog(@"\n\n\n\n");
|
|
}
|
|
if (runSuite3)
|
|
{
|
|
[self configureLoggingForSuite3];
|
|
[self executeTestsWithBase:NO framework:YES frameworkSuite:3];
|
|
|
|
printableResults3 = [self printableResultsForSuite:3];
|
|
|
|
NSLog(@"\n\n\n\n");
|
|
}
|
|
|
|
if (runSuite1)
|
|
{
|
|
NSLog(@"======================================================================");
|
|
NSLog(@"Benchmark Suite 1:");
|
|
NSLog(@"Logging framework configured to log to console only.");
|
|
NSLog(@"\n\n%@", printableResults1);
|
|
NSLog(@"======================================================================");
|
|
}
|
|
if (runSuite2)
|
|
{
|
|
NSLog(@"======================================================================");
|
|
NSLog(@"Benchmark Suite 2:");
|
|
NSLog(@"Logging framework configured to log to file only.");
|
|
NSLog(@"\n\n%@", printableResults2);
|
|
NSLog(@"======================================================================");
|
|
}
|
|
if (runSuite3)
|
|
{
|
|
NSLog(@"======================================================================");
|
|
NSLog(@"Benchmark Suite 3:");
|
|
NSLog(@"Logging framework configured to log to console & file.");
|
|
NSLog(@"\n\n%@", printableResults3);
|
|
NSLog(@"======================================================================");
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE
|
|
NSString *csvResultsPath = [@"~/Documents/LumberjackBenchmark.csv" stringByExpandingTildeInPath];
|
|
#else
|
|
NSString *csvResultsPath = [@"~/Desktop/LumberjackBenchmark.csv" stringByExpandingTildeInPath];
|
|
#endif
|
|
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:csvResultsPath])
|
|
{
|
|
[[NSFileManager defaultManager] createFileAtPath:csvResultsPath contents:nil attributes:nil];
|
|
}
|
|
|
|
NSFileHandle *csvResultsFile = [NSFileHandle fileHandleForWritingAtPath:csvResultsPath];
|
|
|
|
NSData *csvResults = [[self csvResults] dataUsingEncoding:NSUTF8StringEncoding];
|
|
if (csvResults != nil)
|
|
{
|
|
[csvResultsFile writeData:csvResults];
|
|
}
|
|
|
|
NSLog(@"CSV results file written to:\n%@", csvResultsPath);
|
|
}
|
|
|
|
@end
|