Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
michael@0 | 1 | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
michael@0 | 2 | * |
michael@0 | 3 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include <stdio.h> |
michael@0 | 8 | #include <stdlib.h> |
michael@0 | 9 | #include <string.h> |
michael@0 | 10 | #include <time.h> |
michael@0 | 11 | #include <ctype.h> |
michael@0 | 12 | #include <errno.h> |
michael@0 | 13 | #include <math.h> |
michael@0 | 14 | |
michael@0 | 15 | #include "nspr.h" |
michael@0 | 16 | #include "tmreader.h" |
michael@0 | 17 | |
michael@0 | 18 | |
michael@0 | 19 | #define ERROR_REPORT(num, val, msg) fprintf(stderr, "error(%d):\t\"%s\"\t%s\n", (num), (val), (msg)); |
michael@0 | 20 | #define CLEANUP(ptr) do { if(NULL != ptr) { free(ptr); ptr = NULL; } } while(0) |
michael@0 | 21 | |
michael@0 | 22 | |
michael@0 | 23 | #define ticks2msec(reader, ticks) ticks2xsec((reader), (ticks), 1000) |
michael@0 | 24 | #define ticks2usec(reader, ticks) ticks2xsec((reader), (ticks), 1000000) |
michael@0 | 25 | #define TICK_RESOLUTION 1000 |
michael@0 | 26 | #define TICK_PRINTABLE(timeval) ((double)(timeval) / (double)ST_TIMEVAL_RESOLUTION) |
michael@0 | 27 | |
michael@0 | 28 | |
michael@0 | 29 | typedef struct __struct_Options |
michael@0 | 30 | /* |
michael@0 | 31 | ** Options to control how we perform. |
michael@0 | 32 | ** |
michael@0 | 33 | ** mProgramName Used in help text. |
michael@0 | 34 | ** mInputName Name of the file. |
michael@0 | 35 | ** mOutput Output file, append. |
michael@0 | 36 | ** Default is stdout. |
michael@0 | 37 | ** mOutputName Name of the file. |
michael@0 | 38 | ** mHelp Whether or not help should be shown. |
michael@0 | 39 | ** mOverhead How much overhead an allocation will have. |
michael@0 | 40 | ** mAlignment What boundry will the end of an allocation line up on. |
michael@0 | 41 | ** mPageSize Controls the page size. A page containing only fragments |
michael@0 | 42 | ** is not fragmented. A page containing any life memory |
michael@0 | 43 | ** costs mPageSize in bytes. |
michael@0 | 44 | */ |
michael@0 | 45 | { |
michael@0 | 46 | const char* mProgramName; |
michael@0 | 47 | char* mInputName; |
michael@0 | 48 | FILE* mOutput; |
michael@0 | 49 | char* mOutputName; |
michael@0 | 50 | int mHelp; |
michael@0 | 51 | unsigned mOverhead; |
michael@0 | 52 | unsigned mAlignment; |
michael@0 | 53 | unsigned mPageSize; |
michael@0 | 54 | } |
michael@0 | 55 | Options; |
michael@0 | 56 | |
michael@0 | 57 | |
michael@0 | 58 | typedef struct __struct_Switch |
michael@0 | 59 | /* |
michael@0 | 60 | ** Command line options. |
michael@0 | 61 | */ |
michael@0 | 62 | { |
michael@0 | 63 | const char* mLongName; |
michael@0 | 64 | const char* mShortName; |
michael@0 | 65 | int mHasValue; |
michael@0 | 66 | const char* mValue; |
michael@0 | 67 | const char* mDescription; |
michael@0 | 68 | } |
michael@0 | 69 | Switch; |
michael@0 | 70 | |
michael@0 | 71 | #define DESC_NEWLINE "\n\t\t" |
michael@0 | 72 | |
michael@0 | 73 | static Switch gInputSwitch = {"--input", "-i", 1, NULL, "Specify input file." DESC_NEWLINE "stdin is default."}; |
michael@0 | 74 | static Switch gOutputSwitch = {"--output", "-o", 1, NULL, "Specify output file." DESC_NEWLINE "Appends if file exists." DESC_NEWLINE "stdout is default."}; |
michael@0 | 75 | static Switch gHelpSwitch = {"--help", "-h", 0, NULL, "Information on usage."}; |
michael@0 | 76 | static Switch gAlignmentSwitch = {"--alignment", "-al", 1, NULL, "All allocation sizes are made to be a multiple of this number." DESC_NEWLINE "Closer to actual heap conditions; set to 1 for true sizes." DESC_NEWLINE "Default value is 16."}; |
michael@0 | 77 | static Switch gOverheadSwitch = {"--overhead", "-ov", 1, NULL, "After alignment, all allocations are made to increase by this number." DESC_NEWLINE "Closer to actual heap conditions; set to 0 for true sizes." DESC_NEWLINE "Default value is 8."}; |
michael@0 | 78 | static Switch gPageSizeSwitch = {"--page-size", "-ps", 1, NULL, "Sets the page size which aids the identification of fragmentation." DESC_NEWLINE "Closer to actual heap conditions; set to 4294967295 for true sizes." DESC_NEWLINE "Default value is 4096."}; |
michael@0 | 79 | |
michael@0 | 80 | static Switch* gSwitches[] = { |
michael@0 | 81 | &gInputSwitch, |
michael@0 | 82 | &gOutputSwitch, |
michael@0 | 83 | &gAlignmentSwitch, |
michael@0 | 84 | &gOverheadSwitch, |
michael@0 | 85 | &gPageSizeSwitch, |
michael@0 | 86 | &gHelpSwitch |
michael@0 | 87 | }; |
michael@0 | 88 | |
michael@0 | 89 | |
michael@0 | 90 | typedef struct __struct_AnyArray |
michael@0 | 91 | /* |
michael@0 | 92 | ** Variable sized item array. |
michael@0 | 93 | ** |
michael@0 | 94 | ** mItems The void pointer items. |
michael@0 | 95 | ** mItemSize Size of each different item. |
michael@0 | 96 | ** mCount The number of items in the array. |
michael@0 | 97 | ** mCapacity How many more items we can hold before reallocing. |
michael@0 | 98 | ** mGrowBy How many items we allocate when we grow. |
michael@0 | 99 | */ |
michael@0 | 100 | { |
michael@0 | 101 | void* mItems; |
michael@0 | 102 | unsigned mItemSize; |
michael@0 | 103 | unsigned mCount; |
michael@0 | 104 | unsigned mCapacity; |
michael@0 | 105 | unsigned mGrowBy; |
michael@0 | 106 | } |
michael@0 | 107 | AnyArray; |
michael@0 | 108 | |
michael@0 | 109 | |
michael@0 | 110 | typedef int (*arrayMatchFunc)(void* inContext, AnyArray* inArray, void* inItem, unsigned inItemIndex) |
michael@0 | 111 | /* |
michael@0 | 112 | ** Callback function for the arrayIndexFn function. |
michael@0 | 113 | ** Used to determine an item match by customizable criteria. |
michael@0 | 114 | ** |
michael@0 | 115 | ** inContext The criteria and state of the search. |
michael@0 | 116 | ** User specified/created. |
michael@0 | 117 | ** inArray The array the item is in. |
michael@0 | 118 | ** inItem The item to evaluate for match. |
michael@0 | 119 | ** inItemIndex The index of this particular item in the array. |
michael@0 | 120 | ** |
michael@0 | 121 | ** return int 0 to specify a match. |
michael@0 | 122 | ** !0 to continue the search performed by arrayIndexFn. |
michael@0 | 123 | */ |
michael@0 | 124 | ; |
michael@0 | 125 | |
michael@0 | 126 | |
michael@0 | 127 | typedef enum __enum_HeapEventType |
michael@0 | 128 | /* |
michael@0 | 129 | ** Simple heap events are really one of two things. |
michael@0 | 130 | */ |
michael@0 | 131 | { |
michael@0 | 132 | FREE, |
michael@0 | 133 | ALLOC |
michael@0 | 134 | } |
michael@0 | 135 | HeapEventType; |
michael@0 | 136 | |
michael@0 | 137 | |
michael@0 | 138 | typedef enum __enum_HeapObjectType |
michael@0 | 139 | /* |
michael@0 | 140 | ** The various types of heap objects we track. |
michael@0 | 141 | */ |
michael@0 | 142 | { |
michael@0 | 143 | ALLOCATION, |
michael@0 | 144 | FRAGMENT |
michael@0 | 145 | } |
michael@0 | 146 | HeapObjectType; |
michael@0 | 147 | |
michael@0 | 148 | |
michael@0 | 149 | typedef struct __struct_HeapObject HeapObject; |
michael@0 | 150 | typedef struct __struct_HeapHistory |
michael@0 | 151 | /* |
michael@0 | 152 | ** A marker as to what has happened. |
michael@0 | 153 | ** |
michael@0 | 154 | ** mTimestamp When history occurred. |
michael@0 | 155 | ** mTMRSerial The historical state as known to the tmreader. |
michael@0 | 156 | ** mObjectIndex Index to the object that was before or after this event. |
michael@0 | 157 | ** The index as in the index according to all heap objects |
michael@0 | 158 | ** kept in the TMState structure. |
michael@0 | 159 | ** We use an index instead of a pointer as the array of |
michael@0 | 160 | ** objects can change location in the heap. |
michael@0 | 161 | */ |
michael@0 | 162 | { |
michael@0 | 163 | unsigned mTimestamp; |
michael@0 | 164 | unsigned mTMRSerial; |
michael@0 | 165 | unsigned mObjectIndex; |
michael@0 | 166 | } |
michael@0 | 167 | HeapHistory; |
michael@0 | 168 | |
michael@0 | 169 | |
michael@0 | 170 | struct __struct_HeapObject |
michael@0 | 171 | /* |
michael@0 | 172 | ** An object in the heap. |
michael@0 | 173 | ** |
michael@0 | 174 | ** A special case should be noted here. If either the birth or death |
michael@0 | 175 | ** history leads to an object of the same type, then this object |
michael@0 | 176 | ** is the same as that object, but was modified somehow. |
michael@0 | 177 | ** Also note that multiple objects may have the same birth object, |
michael@0 | 178 | ** as well as the same death object. |
michael@0 | 179 | ** |
michael@0 | 180 | ** mUniqueID Each object is unique. |
michael@0 | 181 | ** mType Either allocation or fragment. |
michael@0 | 182 | ** mHeapOffset Where in the heap the object is. |
michael@0 | 183 | ** mSize How much of the heap the object takes. |
michael@0 | 184 | ** mBirth History about the birth event. |
michael@0 | 185 | ** mDeath History about the death event. |
michael@0 | 186 | */ |
michael@0 | 187 | { |
michael@0 | 188 | unsigned mUniqueID; |
michael@0 | 189 | |
michael@0 | 190 | HeapObjectType mType; |
michael@0 | 191 | unsigned mHeapOffset; |
michael@0 | 192 | unsigned mSize; |
michael@0 | 193 | |
michael@0 | 194 | HeapHistory mBirth; |
michael@0 | 195 | HeapHistory mDeath; |
michael@0 | 196 | }; |
michael@0 | 197 | |
michael@0 | 198 | |
michael@0 | 199 | typedef struct __struct_TMState |
michael@0 | 200 | /* |
michael@0 | 201 | ** State of our current operation. |
michael@0 | 202 | ** Stats we are trying to calculate. |
michael@0 | 203 | ** |
michael@0 | 204 | ** mOptions Obilgatory options pointer. |
michael@0 | 205 | ** mTMR The tmreader, used in tmreader API calls. |
michael@0 | 206 | ** mLoopExitTMR Set to non zero in order to quickly exit from tmreader |
michael@0 | 207 | ** input loop. This will also result in an error. |
michael@0 | 208 | ** uMinTicks Start of run, milliseconds. |
michael@0 | 209 | ** uMaxTicks End of run, milliseconds. |
michael@0 | 210 | */ |
michael@0 | 211 | { |
michael@0 | 212 | Options* mOptions; |
michael@0 | 213 | tmreader* mTMR; |
michael@0 | 214 | |
michael@0 | 215 | int mLoopExitTMR; |
michael@0 | 216 | |
michael@0 | 217 | unsigned uMinTicks; |
michael@0 | 218 | unsigned uMaxTicks; |
michael@0 | 219 | } |
michael@0 | 220 | TMState; |
michael@0 | 221 | |
michael@0 | 222 | |
michael@0 | 223 | int initOptions(Options* outOptions, int inArgc, char** inArgv) |
michael@0 | 224 | /* |
michael@0 | 225 | ** returns int 0 if successful. |
michael@0 | 226 | */ |
michael@0 | 227 | { |
michael@0 | 228 | int retval = 0; |
michael@0 | 229 | int loop = 0; |
michael@0 | 230 | int switchLoop = 0; |
michael@0 | 231 | int match = 0; |
michael@0 | 232 | const int switchCount = sizeof(gSwitches) / sizeof(gSwitches[0]); |
michael@0 | 233 | Switch* current = NULL; |
michael@0 | 234 | |
michael@0 | 235 | /* |
michael@0 | 236 | ** Set any defaults. |
michael@0 | 237 | */ |
michael@0 | 238 | memset(outOptions, 0, sizeof(Options)); |
michael@0 | 239 | outOptions->mProgramName = inArgv[0]; |
michael@0 | 240 | outOptions->mInputName = strdup("-"); |
michael@0 | 241 | outOptions->mOutput = stdout; |
michael@0 | 242 | outOptions->mOutputName = strdup("stdout"); |
michael@0 | 243 | outOptions->mAlignment = 16; |
michael@0 | 244 | outOptions->mOverhead = 8; |
michael@0 | 245 | |
michael@0 | 246 | if(NULL == outOptions->mOutputName || NULL == outOptions->mInputName) |
michael@0 | 247 | { |
michael@0 | 248 | retval = __LINE__; |
michael@0 | 249 | ERROR_REPORT(retval, "stdin/stdout", "Unable to strdup."); |
michael@0 | 250 | } |
michael@0 | 251 | |
michael@0 | 252 | /* |
michael@0 | 253 | ** Go through and attempt to do the right thing. |
michael@0 | 254 | */ |
michael@0 | 255 | for(loop = 1; loop < inArgc && 0 == retval; loop++) |
michael@0 | 256 | { |
michael@0 | 257 | match = 0; |
michael@0 | 258 | current = NULL; |
michael@0 | 259 | |
michael@0 | 260 | for(switchLoop = 0; switchLoop < switchCount && 0 == retval; switchLoop++) |
michael@0 | 261 | { |
michael@0 | 262 | if(0 == strcmp(gSwitches[switchLoop]->mLongName, inArgv[loop])) |
michael@0 | 263 | { |
michael@0 | 264 | match = __LINE__; |
michael@0 | 265 | } |
michael@0 | 266 | else if(0 == strcmp(gSwitches[switchLoop]->mShortName, inArgv[loop])) |
michael@0 | 267 | { |
michael@0 | 268 | match = __LINE__; |
michael@0 | 269 | } |
michael@0 | 270 | |
michael@0 | 271 | if(match) |
michael@0 | 272 | { |
michael@0 | 273 | if(gSwitches[switchLoop]->mHasValue) |
michael@0 | 274 | { |
michael@0 | 275 | /* |
michael@0 | 276 | ** Attempt to absorb next option to fullfill value. |
michael@0 | 277 | */ |
michael@0 | 278 | if(loop + 1 < inArgc) |
michael@0 | 279 | { |
michael@0 | 280 | loop++; |
michael@0 | 281 | |
michael@0 | 282 | current = gSwitches[switchLoop]; |
michael@0 | 283 | current->mValue = inArgv[loop]; |
michael@0 | 284 | } |
michael@0 | 285 | } |
michael@0 | 286 | else |
michael@0 | 287 | { |
michael@0 | 288 | current = gSwitches[switchLoop]; |
michael@0 | 289 | } |
michael@0 | 290 | |
michael@0 | 291 | break; |
michael@0 | 292 | } |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | if(0 == match) |
michael@0 | 296 | { |
michael@0 | 297 | outOptions->mHelp = __LINE__; |
michael@0 | 298 | retval = __LINE__; |
michael@0 | 299 | ERROR_REPORT(retval, inArgv[loop], "Unknown command line switch."); |
michael@0 | 300 | } |
michael@0 | 301 | else if(NULL == current) |
michael@0 | 302 | { |
michael@0 | 303 | outOptions->mHelp = __LINE__; |
michael@0 | 304 | retval = __LINE__; |
michael@0 | 305 | ERROR_REPORT(retval, inArgv[loop], "Command line switch requires a value."); |
michael@0 | 306 | } |
michael@0 | 307 | else |
michael@0 | 308 | { |
michael@0 | 309 | /* |
michael@0 | 310 | ** Do something based on address/swtich. |
michael@0 | 311 | */ |
michael@0 | 312 | if(current == &gInputSwitch) |
michael@0 | 313 | { |
michael@0 | 314 | CLEANUP(outOptions->mInputName); |
michael@0 | 315 | outOptions->mInputName = strdup(current->mValue); |
michael@0 | 316 | if(NULL == outOptions->mInputName) |
michael@0 | 317 | { |
michael@0 | 318 | retval = __LINE__; |
michael@0 | 319 | ERROR_REPORT(retval, current->mValue, "Unable to strdup."); |
michael@0 | 320 | } |
michael@0 | 321 | } |
michael@0 | 322 | else if(current == &gOutputSwitch) |
michael@0 | 323 | { |
michael@0 | 324 | CLEANUP(outOptions->mOutputName); |
michael@0 | 325 | if(NULL != outOptions->mOutput && stdout != outOptions->mOutput) |
michael@0 | 326 | { |
michael@0 | 327 | fclose(outOptions->mOutput); |
michael@0 | 328 | outOptions->mOutput = NULL; |
michael@0 | 329 | } |
michael@0 | 330 | |
michael@0 | 331 | outOptions->mOutput = fopen(current->mValue, "a"); |
michael@0 | 332 | if(NULL == outOptions->mOutput) |
michael@0 | 333 | { |
michael@0 | 334 | retval = __LINE__; |
michael@0 | 335 | ERROR_REPORT(retval, current->mValue, "Unable to open output file."); |
michael@0 | 336 | } |
michael@0 | 337 | else |
michael@0 | 338 | { |
michael@0 | 339 | outOptions->mOutputName = strdup(current->mValue); |
michael@0 | 340 | if(NULL == outOptions->mOutputName) |
michael@0 | 341 | { |
michael@0 | 342 | retval = __LINE__; |
michael@0 | 343 | ERROR_REPORT(retval, current->mValue, "Unable to strdup."); |
michael@0 | 344 | } |
michael@0 | 345 | } |
michael@0 | 346 | } |
michael@0 | 347 | else if(current == &gHelpSwitch) |
michael@0 | 348 | { |
michael@0 | 349 | outOptions->mHelp = __LINE__; |
michael@0 | 350 | } |
michael@0 | 351 | else if(current == &gAlignmentSwitch) |
michael@0 | 352 | { |
michael@0 | 353 | unsigned arg = 0; |
michael@0 | 354 | char* endScan = NULL; |
michael@0 | 355 | |
michael@0 | 356 | errno = 0; |
michael@0 | 357 | arg = strtoul(current->mValue, &endScan, 0); |
michael@0 | 358 | if(0 == errno && endScan != current->mValue) |
michael@0 | 359 | { |
michael@0 | 360 | outOptions->mAlignment = arg; |
michael@0 | 361 | } |
michael@0 | 362 | else |
michael@0 | 363 | { |
michael@0 | 364 | retval = __LINE__; |
michael@0 | 365 | ERROR_REPORT(retval, current->mValue, "Unable to convert to a number."); |
michael@0 | 366 | } |
michael@0 | 367 | } |
michael@0 | 368 | else if(current == &gOverheadSwitch) |
michael@0 | 369 | { |
michael@0 | 370 | unsigned arg = 0; |
michael@0 | 371 | char* endScan = NULL; |
michael@0 | 372 | |
michael@0 | 373 | errno = 0; |
michael@0 | 374 | arg = strtoul(current->mValue, &endScan, 0); |
michael@0 | 375 | if(0 == errno && endScan != current->mValue) |
michael@0 | 376 | { |
michael@0 | 377 | outOptions->mOverhead = arg; |
michael@0 | 378 | } |
michael@0 | 379 | else |
michael@0 | 380 | { |
michael@0 | 381 | retval = __LINE__; |
michael@0 | 382 | ERROR_REPORT(retval, current->mValue, "Unable to convert to a number."); |
michael@0 | 383 | } |
michael@0 | 384 | } |
michael@0 | 385 | else if(current == &gPageSizeSwitch) |
michael@0 | 386 | { |
michael@0 | 387 | unsigned arg = 0; |
michael@0 | 388 | char* endScan = NULL; |
michael@0 | 389 | |
michael@0 | 390 | errno = 0; |
michael@0 | 391 | arg = strtoul(current->mValue, &endScan, 0); |
michael@0 | 392 | if(0 == errno && endScan != current->mValue) |
michael@0 | 393 | { |
michael@0 | 394 | outOptions->mPageSize = arg; |
michael@0 | 395 | } |
michael@0 | 396 | else |
michael@0 | 397 | { |
michael@0 | 398 | retval = __LINE__; |
michael@0 | 399 | ERROR_REPORT(retval, current->mValue, "Unable to convert to a number."); |
michael@0 | 400 | } |
michael@0 | 401 | } |
michael@0 | 402 | else |
michael@0 | 403 | { |
michael@0 | 404 | retval = __LINE__; |
michael@0 | 405 | ERROR_REPORT(retval, current->mLongName, "No handler for command line switch."); |
michael@0 | 406 | } |
michael@0 | 407 | } |
michael@0 | 408 | } |
michael@0 | 409 | |
michael@0 | 410 | return retval; |
michael@0 | 411 | } |
michael@0 | 412 | |
michael@0 | 413 | |
michael@0 | 414 | uint32_t ticks2xsec(tmreader* aReader, uint32_t aTicks, uint32_t aResolution) |
michael@0 | 415 | /* |
michael@0 | 416 | ** Convert platform specific ticks to second units |
michael@0 | 417 | */ |
michael@0 | 418 | { |
michael@0 | 419 | return (uint32)((aResolution * aTicks) / aReader->ticksPerSec); |
michael@0 | 420 | } |
michael@0 | 421 | |
michael@0 | 422 | |
michael@0 | 423 | void cleanOptions(Options* inOptions) |
michael@0 | 424 | /* |
michael@0 | 425 | ** Clean up any open handles. |
michael@0 | 426 | */ |
michael@0 | 427 | { |
michael@0 | 428 | unsigned loop = 0; |
michael@0 | 429 | |
michael@0 | 430 | CLEANUP(inOptions->mInputName); |
michael@0 | 431 | CLEANUP(inOptions->mOutputName); |
michael@0 | 432 | if(NULL != inOptions->mOutput && stdout != inOptions->mOutput) |
michael@0 | 433 | { |
michael@0 | 434 | fclose(inOptions->mOutput); |
michael@0 | 435 | } |
michael@0 | 436 | |
michael@0 | 437 | memset(inOptions, 0, sizeof(Options)); |
michael@0 | 438 | } |
michael@0 | 439 | |
michael@0 | 440 | |
michael@0 | 441 | void showHelp(Options* inOptions) |
michael@0 | 442 | /* |
michael@0 | 443 | ** Show some simple help text on usage. |
michael@0 | 444 | */ |
michael@0 | 445 | { |
michael@0 | 446 | int loop = 0; |
michael@0 | 447 | const int switchCount = sizeof(gSwitches) / sizeof(gSwitches[0]); |
michael@0 | 448 | const char* valueText = NULL; |
michael@0 | 449 | |
michael@0 | 450 | printf("usage:\t%s [arguments]\n", inOptions->mProgramName); |
michael@0 | 451 | printf("\n"); |
michael@0 | 452 | printf("arguments:\n"); |
michael@0 | 453 | |
michael@0 | 454 | for(loop = 0; loop < switchCount; loop++) |
michael@0 | 455 | { |
michael@0 | 456 | if(gSwitches[loop]->mHasValue) |
michael@0 | 457 | { |
michael@0 | 458 | valueText = " <value>"; |
michael@0 | 459 | } |
michael@0 | 460 | else |
michael@0 | 461 | { |
michael@0 | 462 | valueText = ""; |
michael@0 | 463 | } |
michael@0 | 464 | |
michael@0 | 465 | printf("\t%s%s\n", gSwitches[loop]->mLongName, valueText); |
michael@0 | 466 | printf("\t %s%s", gSwitches[loop]->mShortName, valueText); |
michael@0 | 467 | printf(DESC_NEWLINE "%s\n\n", gSwitches[loop]->mDescription); |
michael@0 | 468 | } |
michael@0 | 469 | |
michael@0 | 470 | printf("This tool reports heap fragmentation stats from a trace-malloc log.\n"); |
michael@0 | 471 | } |
michael@0 | 472 | |
michael@0 | 473 | |
michael@0 | 474 | AnyArray* arrayCreate(unsigned inItemSize, unsigned inGrowBy) |
michael@0 | 475 | /* |
michael@0 | 476 | ** Create an array container object. |
michael@0 | 477 | */ |
michael@0 | 478 | { |
michael@0 | 479 | AnyArray* retval = NULL; |
michael@0 | 480 | |
michael@0 | 481 | if(0 != inGrowBy && 0 != inItemSize) |
michael@0 | 482 | { |
michael@0 | 483 | retval = (AnyArray*)calloc(1, sizeof(AnyArray)); |
michael@0 | 484 | retval->mItemSize = inItemSize; |
michael@0 | 485 | retval->mGrowBy = inGrowBy; |
michael@0 | 486 | } |
michael@0 | 487 | |
michael@0 | 488 | return retval; |
michael@0 | 489 | } |
michael@0 | 490 | |
michael@0 | 491 | |
michael@0 | 492 | void arrayDestroy(AnyArray* inArray) |
michael@0 | 493 | /* |
michael@0 | 494 | ** Release the memory the array contains. |
michael@0 | 495 | ** This will release the items as well. |
michael@0 | 496 | */ |
michael@0 | 497 | { |
michael@0 | 498 | if(NULL != inArray) |
michael@0 | 499 | { |
michael@0 | 500 | if(NULL != inArray->mItems) |
michael@0 | 501 | { |
michael@0 | 502 | free(inArray->mItems); |
michael@0 | 503 | } |
michael@0 | 504 | free(inArray); |
michael@0 | 505 | } |
michael@0 | 506 | } |
michael@0 | 507 | |
michael@0 | 508 | |
michael@0 | 509 | unsigned arrayAlloc(AnyArray* inArray, unsigned inItems) |
michael@0 | 510 | /* |
michael@0 | 511 | ** Resize the item array capcity to a specific number of items. |
michael@0 | 512 | ** This could possibly truncate the array, so handle that as well. |
michael@0 | 513 | ** |
michael@0 | 514 | ** returns unsigned <= inArray->mCapacity on success. |
michael@0 | 515 | */ |
michael@0 | 516 | { |
michael@0 | 517 | unsigned retval = (unsigned)-1; |
michael@0 | 518 | |
michael@0 | 519 | if(NULL != inArray) |
michael@0 | 520 | { |
michael@0 | 521 | void* moved = NULL; |
michael@0 | 522 | |
michael@0 | 523 | moved = realloc(inArray->mItems, inItems * inArray->mItemSize); |
michael@0 | 524 | if(NULL != moved) |
michael@0 | 525 | { |
michael@0 | 526 | inArray->mItems = moved; |
michael@0 | 527 | inArray->mCapacity = inItems; |
michael@0 | 528 | if(inArray->mCount > inItems) |
michael@0 | 529 | { |
michael@0 | 530 | inArray->mCount = inItems; |
michael@0 | 531 | } |
michael@0 | 532 | |
michael@0 | 533 | retval = inItems; |
michael@0 | 534 | } |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | return retval; |
michael@0 | 538 | } |
michael@0 | 539 | |
michael@0 | 540 | |
michael@0 | 541 | void* arrayItem(AnyArray* inArray, unsigned inIndex) |
michael@0 | 542 | /* |
michael@0 | 543 | ** Return the array item at said index. |
michael@0 | 544 | ** Zero based index. |
michael@0 | 545 | ** |
michael@0 | 546 | ** returns void* NULL on failure. |
michael@0 | 547 | */ |
michael@0 | 548 | { |
michael@0 | 549 | void* retval = NULL; |
michael@0 | 550 | |
michael@0 | 551 | if(NULL != inArray && inIndex < inArray->mCount) |
michael@0 | 552 | { |
michael@0 | 553 | retval = (void*)((char*)inArray->mItems + (inArray->mItemSize * inIndex)); |
michael@0 | 554 | } |
michael@0 | 555 | |
michael@0 | 556 | return retval; |
michael@0 | 557 | } |
michael@0 | 558 | |
michael@0 | 559 | |
michael@0 | 560 | unsigned arrayIndex(AnyArray* inArray, void* inItem, unsigned inStartIndex) |
michael@0 | 561 | /* |
michael@0 | 562 | ** Go through the array from the index specified looking for an item |
michael@0 | 563 | ** match based on byte for byte comparison. |
michael@0 | 564 | ** We allow specifying the start index in order to handle arrays with |
michael@0 | 565 | ** duplicate items. |
michael@0 | 566 | ** |
michael@0 | 567 | ** returns unsigned >= inArray->mCount on failure. |
michael@0 | 568 | */ |
michael@0 | 569 | { |
michael@0 | 570 | unsigned retval = (unsigned)-1; |
michael@0 | 571 | |
michael@0 | 572 | if(NULL != inArray && NULL != inItem && inStartIndex < inArray->mCount) |
michael@0 | 573 | { |
michael@0 | 574 | void* curItem = NULL; |
michael@0 | 575 | |
michael@0 | 576 | for(retval = inStartIndex; retval < inArray->mCount; retval++) |
michael@0 | 577 | { |
michael@0 | 578 | curItem = arrayItem(inArray, retval); |
michael@0 | 579 | if(0 == memcmp(inItem, curItem, inArray->mItemSize)) |
michael@0 | 580 | { |
michael@0 | 581 | break; |
michael@0 | 582 | } |
michael@0 | 583 | } |
michael@0 | 584 | } |
michael@0 | 585 | |
michael@0 | 586 | |
michael@0 | 587 | return retval; |
michael@0 | 588 | } |
michael@0 | 589 | |
michael@0 | 590 | |
michael@0 | 591 | unsigned arrayIndexFn(AnyArray* inArray, arrayMatchFunc inFunc, void* inFuncContext, unsigned inStartIndex) |
michael@0 | 592 | /* |
michael@0 | 593 | ** Go through the array from the index specified looking for an item |
michael@0 | 594 | ** match based upon the return value of inFunc (0, Zero, is a match). |
michael@0 | 595 | ** We allow specifying the start index in order to facilitate looping over |
michael@0 | 596 | ** the array which could have multiple matches. |
michael@0 | 597 | ** |
michael@0 | 598 | ** returns unsigned >= inArray->mCount on failure. |
michael@0 | 599 | */ |
michael@0 | 600 | { |
michael@0 | 601 | unsigned retval = (unsigned)-1; |
michael@0 | 602 | |
michael@0 | 603 | if(NULL != inArray && NULL != inFunc && inStartIndex < inArray->mCount) |
michael@0 | 604 | { |
michael@0 | 605 | void* curItem = NULL; |
michael@0 | 606 | |
michael@0 | 607 | for(retval = inStartIndex; retval < inArray->mCount; retval++) |
michael@0 | 608 | { |
michael@0 | 609 | curItem = arrayItem(inArray, retval); |
michael@0 | 610 | if(0 == inFunc(inFuncContext, inArray, curItem, retval)) |
michael@0 | 611 | { |
michael@0 | 612 | break; |
michael@0 | 613 | } |
michael@0 | 614 | } |
michael@0 | 615 | } |
michael@0 | 616 | |
michael@0 | 617 | return retval; |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | |
michael@0 | 621 | unsigned arrayAddItem(AnyArray* inArray, void* inItem) |
michael@0 | 622 | /* |
michael@0 | 623 | ** Add a new item to the array. |
michael@0 | 624 | ** This is done by copying the item. |
michael@0 | 625 | ** |
michael@0 | 626 | ** returns unsigned < inArray->mCount on success. |
michael@0 | 627 | */ |
michael@0 | 628 | { |
michael@0 | 629 | unsigned retval = (unsigned)-1; |
michael@0 | 630 | |
michael@0 | 631 | if(NULL != inArray && NULL != inItem) |
michael@0 | 632 | { |
michael@0 | 633 | int noCopy = 0; |
michael@0 | 634 | |
michael@0 | 635 | /* |
michael@0 | 636 | ** See if the array should grow. |
michael@0 | 637 | */ |
michael@0 | 638 | if(inArray->mCount == inArray->mCapacity) |
michael@0 | 639 | { |
michael@0 | 640 | unsigned allocRes = 0; |
michael@0 | 641 | |
michael@0 | 642 | allocRes = arrayAlloc(inArray, inArray->mCapacity + inArray->mGrowBy); |
michael@0 | 643 | if(allocRes > inArray->mCapacity) |
michael@0 | 644 | { |
michael@0 | 645 | noCopy = __LINE__; |
michael@0 | 646 | } |
michael@0 | 647 | } |
michael@0 | 648 | |
michael@0 | 649 | if(0 == noCopy) |
michael@0 | 650 | { |
michael@0 | 651 | retval = inArray->mCount; |
michael@0 | 652 | |
michael@0 | 653 | inArray->mCount++; |
michael@0 | 654 | memcpy(arrayItem(inArray, retval), inItem, inArray->mItemSize); |
michael@0 | 655 | } |
michael@0 | 656 | } |
michael@0 | 657 | |
michael@0 | 658 | return retval; |
michael@0 | 659 | } |
michael@0 | 660 | |
michael@0 | 661 | |
michael@0 | 662 | HeapObject* initHeapObject(HeapObject* inObject) |
michael@0 | 663 | /* |
michael@0 | 664 | ** Function to init the heap object just right. |
michael@0 | 665 | ** Sets the unique ID to something unique. |
michael@0 | 666 | */ |
michael@0 | 667 | { |
michael@0 | 668 | HeapObject* retval = inObject; |
michael@0 | 669 | |
michael@0 | 670 | if(NULL != inObject) |
michael@0 | 671 | { |
michael@0 | 672 | static unsigned uniqueGenerator = 0; |
michael@0 | 673 | |
michael@0 | 674 | memset(inObject, -1, sizeof(HeapObject)); |
michael@0 | 675 | |
michael@0 | 676 | inObject->mUniqueID = uniqueGenerator; |
michael@0 | 677 | uniqueGenerator++; |
michael@0 | 678 | } |
michael@0 | 679 | |
michael@0 | 680 | return retval; |
michael@0 | 681 | } |
michael@0 | 682 | |
michael@0 | 683 | |
michael@0 | 684 | int simpleHeapEvent(TMState* inStats, HeapEventType inType, unsigned mTimestamp, unsigned inSerial, unsigned inHeapID, unsigned inSize) |
michael@0 | 685 | /* |
michael@0 | 686 | ** A new heap event will cause the creation of a new heap object. |
michael@0 | 687 | ** The new heap object will displace, or replace, a heap object of a different type. |
michael@0 | 688 | */ |
michael@0 | 689 | { |
michael@0 | 690 | int retval = 0; |
michael@0 | 691 | HeapObject newObject; |
michael@0 | 692 | |
michael@0 | 693 | /* |
michael@0 | 694 | ** Set the most basic object details. |
michael@0 | 695 | */ |
michael@0 | 696 | initHeapObject(&newObject); |
michael@0 | 697 | newObject.mHeapOffset = inHeapID; |
michael@0 | 698 | newObject.mSize = inSize; |
michael@0 | 699 | if(FREE == inType) |
michael@0 | 700 | { |
michael@0 | 701 | newObject.mType = FRAGMENT; |
michael@0 | 702 | } |
michael@0 | 703 | else if(ALLOC == inType) |
michael@0 | 704 | { |
michael@0 | 705 | newObject.mType = ALLOCATION; |
michael@0 | 706 | } |
michael@0 | 707 | |
michael@0 | 708 | /* |
michael@0 | 709 | ** Add it to the heap object array. |
michael@0 | 710 | */ |
michael@0 | 711 | |
michael@0 | 712 | /* |
michael@0 | 713 | ** TODO GAB |
michael@0 | 714 | ** |
michael@0 | 715 | ** First thing to do is to add the new object to the heap in order to |
michael@0 | 716 | ** obtain a valid index. |
michael@0 | 717 | ** |
michael@0 | 718 | ** Next, find all matches to this range of heap memory that this event |
michael@0 | 719 | ** refers to, that are alive during this timestamp (no death yet). |
michael@0 | 720 | ** Fill in the death event of those objects. |
michael@0 | 721 | ** If the objects contain some portions outside of the range, then |
michael@0 | 722 | ** new objects for those ranges need to be created that carry on |
michael@0 | 723 | ** the same object type, have the index of the old object for birth, |
michael@0 | 724 | ** and the serial of the old object, new timestamp of course. |
michael@0 | 725 | ** The old object's death points to the new object, which tells why the |
michael@0 | 726 | ** fragmentation took place. |
michael@0 | 727 | ** The new object birth points to the old object only if a fragment. |
michael@0 | 728 | ** An allocation only has a birth object when it is a realloc (complex) |
michael@0 | 729 | ** heap event. |
michael@0 | 730 | ** |
michael@0 | 731 | ** I believe this give us enough information to look up particular |
michael@0 | 732 | ** details of the heap at any given time. |
michael@0 | 733 | */ |
michael@0 | 734 | |
michael@0 | 735 | return retval; |
michael@0 | 736 | } |
michael@0 | 737 | |
michael@0 | 738 | |
michael@0 | 739 | int complexHeapEvent(TMState* inStats, unsigned mTimestamp, unsigned inOldSerial, unsigned inOldHeapID, unsigned inOSize, unsigned inNewSerial, unsigned inNewHeapID, unsigned inNewSize) |
michael@0 | 740 | /* |
michael@0 | 741 | ** Generally, this event intends to chain one old heap object to a newer heap object. |
michael@0 | 742 | ** Otherwise, the functionality should recognizable ala simpleHeapEvent. |
michael@0 | 743 | */ |
michael@0 | 744 | { |
michael@0 | 745 | int retval = 0; |
michael@0 | 746 | |
michael@0 | 747 | /* |
michael@0 | 748 | ** TODO GAB |
michael@0 | 749 | */ |
michael@0 | 750 | |
michael@0 | 751 | return retval; |
michael@0 | 752 | } |
michael@0 | 753 | |
michael@0 | 754 | |
michael@0 | 755 | unsigned actualByteSize(Options* inOptions, unsigned retval) |
michael@0 | 756 | /* |
michael@0 | 757 | ** Apply alignment and overhead to size to figure out actual byte size. |
michael@0 | 758 | ** This by default mimics spacetrace with default options (msvc crt heap). |
michael@0 | 759 | */ |
michael@0 | 760 | { |
michael@0 | 761 | if(0 != retval) |
michael@0 | 762 | { |
michael@0 | 763 | unsigned eval = 0; |
michael@0 | 764 | unsigned over = 0; |
michael@0 | 765 | |
michael@0 | 766 | eval = retval - 1; |
michael@0 | 767 | if(0 != inOptions->mAlignment) |
michael@0 | 768 | { |
michael@0 | 769 | over = eval % inOptions->mAlignment; |
michael@0 | 770 | } |
michael@0 | 771 | retval = eval + inOptions->mOverhead + inOptions->mAlignment - over; |
michael@0 | 772 | } |
michael@0 | 773 | |
michael@0 | 774 | return retval; |
michael@0 | 775 | } |
michael@0 | 776 | |
michael@0 | 777 | |
michael@0 | 778 | void tmEventHandler(tmreader* inReader, tmevent* inEvent) |
michael@0 | 779 | /* |
michael@0 | 780 | ** Callback from the tmreader_eventloop. |
michael@0 | 781 | ** Build up our fragmentation information herein. |
michael@0 | 782 | */ |
michael@0 | 783 | { |
michael@0 | 784 | char type = inEvent->type; |
michael@0 | 785 | TMState* stats = (TMState*)inReader->data; |
michael@0 | 786 | |
michael@0 | 787 | /* |
michael@0 | 788 | ** Only intersted in handling events of a particular type. |
michael@0 | 789 | */ |
michael@0 | 790 | switch(type) |
michael@0 | 791 | { |
michael@0 | 792 | default: |
michael@0 | 793 | return; |
michael@0 | 794 | |
michael@0 | 795 | case TM_EVENT_MALLOC: |
michael@0 | 796 | case TM_EVENT_CALLOC: |
michael@0 | 797 | case TM_EVENT_REALLOC: |
michael@0 | 798 | case TM_EVENT_FREE: |
michael@0 | 799 | break; |
michael@0 | 800 | } |
michael@0 | 801 | |
michael@0 | 802 | /* |
michael@0 | 803 | ** Should we even try to look? |
michael@0 | 804 | ** Set mLoopExitTMR to non-zero to abort the read loop faster. |
michael@0 | 805 | */ |
michael@0 | 806 | if(0 == stats->mLoopExitTMR) |
michael@0 | 807 | { |
michael@0 | 808 | Options* options = (Options*)stats->mOptions; |
michael@0 | 809 | unsigned timestamp = ticks2msec(stats->mTMR, inEvent->u.alloc.interval); |
michael@0 | 810 | unsigned actualSize = actualByteSize(options, inEvent->u.alloc.size); |
michael@0 | 811 | unsigned heapID = inEvent->u.alloc.ptr; |
michael@0 | 812 | unsigned serial = inEvent->serial; |
michael@0 | 813 | |
michael@0 | 814 | /* |
michael@0 | 815 | ** Check the timestamp range of our overall state. |
michael@0 | 816 | */ |
michael@0 | 817 | if(stats->uMinTicks > timestamp) |
michael@0 | 818 | { |
michael@0 | 819 | stats->uMinTicks = timestamp; |
michael@0 | 820 | } |
michael@0 | 821 | if(stats->uMaxTicks < timestamp) |
michael@0 | 822 | { |
michael@0 | 823 | stats->uMaxTicks = timestamp; |
michael@0 | 824 | } |
michael@0 | 825 | |
michael@0 | 826 | /* |
michael@0 | 827 | ** Realloc in general deserves some special attention if dealing |
michael@0 | 828 | ** with an old allocation (not new memory). |
michael@0 | 829 | */ |
michael@0 | 830 | if(TM_EVENT_REALLOC == type && 0 != inEvent->u.alloc.oldserial) |
michael@0 | 831 | { |
michael@0 | 832 | unsigned oldActualSize = actualByteSize(options, inEvent->u.alloc.oldsize); |
michael@0 | 833 | unsigned oldHeapID = inEvent->u.alloc.oldptr; |
michael@0 | 834 | unsigned oldSerial = inEvent->u.alloc.oldserial; |
michael@0 | 835 | |
michael@0 | 836 | if(0 == actualSize) |
michael@0 | 837 | { |
michael@0 | 838 | /* |
michael@0 | 839 | ** Reallocs of size zero are to become free events. |
michael@0 | 840 | */ |
michael@0 | 841 | stats->mLoopExitTMR = simpleHeapEvent(stats, FREE, timestamp, serial, oldHeapID, oldActualSize); |
michael@0 | 842 | } |
michael@0 | 843 | else if(heapID != oldHeapID || actualSize != oldActualSize) |
michael@0 | 844 | { |
michael@0 | 845 | /* |
michael@0 | 846 | ** Reallocs which moved generate two events. |
michael@0 | 847 | ** Reallocs which changed size generate two events. |
michael@0 | 848 | ** |
michael@0 | 849 | ** One event to free the old memory area. |
michael@0 | 850 | ** Another event to allocate the new memory area. |
michael@0 | 851 | ** They are to be linked to one another, so the history |
michael@0 | 852 | ** and true origin can be tracked. |
michael@0 | 853 | */ |
michael@0 | 854 | stats->mLoopExitTMR = complexHeapEvent(stats, timestamp, oldSerial, oldHeapID, oldActualSize, serial, heapID, actualSize); |
michael@0 | 855 | } |
michael@0 | 856 | else |
michael@0 | 857 | { |
michael@0 | 858 | /* |
michael@0 | 859 | ** The realloc is not considered an operation and is skipped. |
michael@0 | 860 | ** It is not an operation, because it did not move or change |
michael@0 | 861 | ** size; this can happen if a realloc falls within the |
michael@0 | 862 | ** alignment of an allocation. |
michael@0 | 863 | ** Say if you realloc a 1 byte allocation to 2 bytes, it will |
michael@0 | 864 | ** not really change heap impact unless you have 1 set as |
michael@0 | 865 | ** the alignment of your allocations. |
michael@0 | 866 | */ |
michael@0 | 867 | } |
michael@0 | 868 | } |
michael@0 | 869 | else if(TM_EVENT_FREE == type) |
michael@0 | 870 | { |
michael@0 | 871 | /* |
michael@0 | 872 | ** Generate a free event to create a fragment. |
michael@0 | 873 | */ |
michael@0 | 874 | stats->mLoopExitTMR = simpleHeapEvent(stats, FREE, timestamp, serial, heapID, actualSize); |
michael@0 | 875 | } |
michael@0 | 876 | else |
michael@0 | 877 | { |
michael@0 | 878 | /* |
michael@0 | 879 | ** Generate an allocation event to clear fragments. |
michael@0 | 880 | */ |
michael@0 | 881 | stats->mLoopExitTMR = simpleHeapEvent(stats, ALLOC, timestamp, serial, heapID, actualSize); |
michael@0 | 882 | } |
michael@0 | 883 | } |
michael@0 | 884 | } |
michael@0 | 885 | |
michael@0 | 886 | |
michael@0 | 887 | int tmfrags(Options* inOptions) |
michael@0 | 888 | /* |
michael@0 | 889 | ** Load the input file and report stats. |
michael@0 | 890 | */ |
michael@0 | 891 | { |
michael@0 | 892 | int retval = 0; |
michael@0 | 893 | TMState stats; |
michael@0 | 894 | |
michael@0 | 895 | memset(&stats, 0, sizeof(stats)); |
michael@0 | 896 | stats.mOptions = inOptions; |
michael@0 | 897 | stats.uMinTicks = 0xFFFFFFFFU; |
michael@0 | 898 | |
michael@0 | 899 | /* |
michael@0 | 900 | ** Need a tmreader. |
michael@0 | 901 | */ |
michael@0 | 902 | stats.mTMR = tmreader_new(inOptions->mProgramName, &stats); |
michael@0 | 903 | if(NULL != stats.mTMR) |
michael@0 | 904 | { |
michael@0 | 905 | int tmResult = 0; |
michael@0 | 906 | |
michael@0 | 907 | tmResult = tmreader_eventloop(stats.mTMR, inOptions->mInputName, tmEventHandler); |
michael@0 | 908 | if(0 == tmResult) |
michael@0 | 909 | { |
michael@0 | 910 | retval = __LINE__; |
michael@0 | 911 | ERROR_REPORT(retval, inOptions->mInputName, "Problem reading trace-malloc data."); |
michael@0 | 912 | } |
michael@0 | 913 | if(0 != stats.mLoopExitTMR) |
michael@0 | 914 | { |
michael@0 | 915 | retval = stats.mLoopExitTMR; |
michael@0 | 916 | ERROR_REPORT(retval, inOptions->mInputName, "Aborted trace-malloc input loop."); |
michael@0 | 917 | } |
michael@0 | 918 | |
michael@0 | 919 | tmreader_destroy(stats.mTMR); |
michael@0 | 920 | stats.mTMR = NULL; |
michael@0 | 921 | } |
michael@0 | 922 | else |
michael@0 | 923 | { |
michael@0 | 924 | retval = __LINE__; |
michael@0 | 925 | ERROR_REPORT(retval, inOptions->mProgramName, "Unable to obtain tmreader."); |
michael@0 | 926 | } |
michael@0 | 927 | |
michael@0 | 928 | return retval; |
michael@0 | 929 | } |
michael@0 | 930 | |
michael@0 | 931 | |
michael@0 | 932 | int main(int inArgc, char** inArgv) |
michael@0 | 933 | { |
michael@0 | 934 | int retval = 0; |
michael@0 | 935 | Options options; |
michael@0 | 936 | |
michael@0 | 937 | retval = initOptions(&options, inArgc, inArgv); |
michael@0 | 938 | if(options.mHelp) |
michael@0 | 939 | { |
michael@0 | 940 | showHelp(&options); |
michael@0 | 941 | } |
michael@0 | 942 | else if(0 == retval) |
michael@0 | 943 | { |
michael@0 | 944 | retval = tmfrags(&options); |
michael@0 | 945 | } |
michael@0 | 946 | |
michael@0 | 947 | cleanOptions(&options); |
michael@0 | 948 | return retval; |
michael@0 | 949 | } |
michael@0 | 950 |