├── .gitignore ├── .project ├── .pydevproject ├── COPYING ├── MANIFEST.in ├── README.md ├── changelog.txt ├── examples ├── MegaRAID.txt ├── compare.txt ├── intel320.txt └── mdadmRAID.txt ├── scripts ├── tkperf └── tkperf-cmp ├── setup.py └── src ├── fio ├── FioJob.py └── __init__.py ├── perfTest ├── DeviceTests.py ├── Devices.py ├── Options.py ├── PerfTest.py ├── StdyState.py └── __init__.py ├── plots ├── __init__.py ├── compPlots.py └── genPlots.py ├── reports ├── RstReport.py ├── XmlReport.py ├── __init__.py └── pics │ └── TKperf_logo.png └── system ├── Mail.py ├── OS.py └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | TKperf 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.dltk.core.scriptbuilder 10 | 11 | 12 | 13 | 14 | org.python.pydev.PyDevBuilder 15 | 16 | 17 | 18 | 19 | 20 | org.python.pydev.pythonNature 21 | net.sourceforge.shelled.core.nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /TKperf/src 5 | 6 | python 2.7 7 | Default 8 | 9 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include examples *.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TKperf performance test tool for SSDs, HDDs and RAID devices 2 | 3 | ## Disclaimer 4 | * **ATTENTION: All data on the tested device is lost!** 5 | * The given device becomes overwritten multiple times! 6 | * For SSDs a secure erase is carried out, too. 7 | * For RAID devices each device becomes overwritten or secure erased 8 | 9 | ## Requirements 10 | * The following python packages are required: 11 | * logging 12 | * json 13 | * lxml 14 | * subprocess 15 | * datetime 16 | * argparse 17 | * copy 18 | * collections 19 | * numpy 20 | * matplotlib 21 | * os 22 | * sys 23 | * makedirs, path, walk 24 | * zipfile 25 | * smtplib 26 | * email 27 | * ABCMeta, abstractmethod 28 | * string.split 29 | * lstat 30 | * stat.S_ISBLK 31 | * string.split 32 | * time.sleep 33 | * setuptools 34 | * Under Ubuntu it is normally sufficient to install: 35 | $ sudo apt-get install python3-matplotlib \ 36 | python3-lxml python3-numpy python3-simplejson python3-setuptools 37 | * The following software is required: 38 | * FIO version 2.0.3 or higher. 39 | Please note that older FIO versions are not supported due to an 40 | incompatibility in the terse output of FIO. The tests will not produce any 41 | valuable information if FIO is older than 2.0.3. 42 | * hdparm 43 | * sg3-utils (for testing SAS devices) 44 | * nvme-cli (for testing nvme devices) 45 | * E.g. https://github.com/linux-nvme/nvme-cli.git 46 | * rst2pdf 47 | 48 | ## Setup 49 | * TKperf checks if FIO is found with: 50 | $ which fio 51 | Please ensure that FIO is in your path and executable. 52 | * To install FIO visit 53 | http://freecode.com/projects/fio 54 | * To fetch FIO per git use: 55 | $ git clone git://git.kernel.dk/fio.git 56 | * An installation howto (currently only in German): 57 | http://www.thomas-krenn.com/de/wiki/Fio_kompilieren 58 | 59 | ## Installation 60 | * To install TKperf simply call 61 | $ sudo python setup.py install 62 | * If the source is updated via git ensure to reinstall TKperf. 63 | * An installation howto (in German): 64 | https://www.thomas-krenn.com/de/wiki/TKperf 65 | 66 | ## Hardware Prerequisites 67 | * To carry out a Secure Erase it is necessary that the SSD is NOT in the 68 | frozen state. 69 | (http://www.thomas-krenn.com/en/wiki/SSD_Secure_Erase#Step_1:_NOT_Frozen) 70 | If the SSD is frozen un- and replug the device while the host is on. 71 | 72 | ## Running a test 73 | * The main script file is called tkperf: 74 | $ which tkperf 75 | /usr/local/bin/tkperf 76 | * Call tkperf with the command line options described below. 77 | To get the help output use: 78 | $ tkperf -h 79 | 80 | ### RAID Tests 81 | * To run a RAID test you have to create a RAID config file (cf. 'examples'). 82 | The config file specifies RAID type, devices and level. For Avago/LSI use 83 | 'hw_lsi' as type, for mdadm 'sw_mdadm'. hw_lsi uses enclosure IDs for the 84 | devices, you can get them with: 85 | $ sudo storcli64 /c0 /eall /sall show 86 | mdadm uses normal device paths like '/dev/sda', '/dev/sdb' etc. 87 | Example config: 88 | { 89 | "devices": [ 90 | "252:0", 91 | "252:1" 92 | ], 93 | "raidlevel": 1, 94 | "type": "hw_lsi" 95 | } 96 | 97 | ## Log File 98 | * The log file is named after the given test name (e.g. 'intel320' in the 99 | example below). Inspect the log from time to time to ensure that no errors 100 | occur. In the log all calls to FIO are stated out. Also the results from the 101 | steady rounds are written to the log file. If you have any doubts that the 102 | results in the pdf report are correct check the log what performance values 103 | FIO returned. As the log file contains the FIO output in terse version, read 104 | the FIO HOWTO to find out how to interpret it. 105 | If a report is generated from the xml file a seperate log is created. This log 106 | gets '.xml' appended. 107 | 108 | ## Examples 109 | * To run a test on a remote machine and ensure it is not stopped if the 110 | connection closes it is good practice to start the script in a screen session. 111 | See 'man screen' for more information. Basically it is sufficient to call 112 | 'screen', start the script and then detach with 'Ctrl-a d'. 113 | * As root or with 'sudo': 114 | $ sudo tkperf ssd intel320 /dev/sde -nj 2 -iod 16 -rfb 115 | * If hdparm does not output any valuable information for your device, 116 | use a description file to provide the device informations: 117 | $ sudo tkperf ssd intel320 /dev/sde -nj 2 -iod 16 -rfb -dsc intel320.dsc 118 | * Also nohup could be used to start a test. To disable the warning that the 119 | device will be erased ('-ft', '--force-test'). Note that with nohup it is 120 | difficult to use sudo and redirect stdout/stderr to output files. 121 | $ sudo nohup tkperf ssd intel320 /dev/sde -ft -nj 2 \ 122 | -iod 16 -rfb 1>runTest.out 2>runTest.err & 123 | 124 | ### RAID Examples 125 | * To deal with Avago RAID devices, you have to install storcli 126 | * To deal with software RAID devices, you have to install mdadm 127 | * First, create the RAID device manually, this has only to be done the first 128 | time. Afterwards the RAID is re-created automatically: 129 | $ sudo storcli64 /c0 add vd type=raid1 drives=252:0,252:1 130 | Then check with e.g. 'lsblk' which device was created and start the test 131 | with: 132 | $ sudo tkperf raid LSI-I3500-R5-4 /dev/sdb -c raid5.cfg -nj 2 -iod 16 133 | 134 | ## SSD Compression 135 | * If the SSD controller uses compression use the '-rfb' switch to ensure that 136 | data buffers used by FIO are completely random. This option enables the 137 | 'refill_buffers' from FIO. 138 | 139 | ## Description File 140 | * If 'hdparm -I' does not provide any valuable information about a drive, you 141 | have to provide a so called 'description file'. This file just contains some 142 | meta information for the pdf report and so it can be a simple text file 143 | describing the tested device. When calling the script provide the path to the 144 | description file with '-dsc PATH_TO_FILE'. 145 | 146 | ## Generate a PDF report 147 | * To generate a pdf from the rst use rst2pdf: 148 | $ rst2pdf test.rst test.pdf 149 | 150 | ## Loading from XML 151 | * To load an already carried out test from the generated xml file, use 152 | the '-xml' option: 153 | $ sudo tkperf ssd intel520 none -xml 154 | * In this case as tested device 'none' is used, it is there just as a 155 | placeholder. 156 | * Loading from an xml file is useful if something has changed in the plotting 157 | methods and you want to re-plot the results. This is mainly required during a 158 | development process or if a major update/bugfix has been made to tkperf. 159 | 160 | ## Creating compare plots 161 | * With the help of the generated xml files multiple devices can be compared (up 162 | to seven devices). The script 'tkperf-cmp' generates the compare plots for 163 | write saturation, throughput, IOPS and latency. To generate the plots use: 164 | $ tkperf-cmp ssd Samsung840PRO-256GB.xml Samsung840EVO-250GB.xml 165 | * Guide (in German) 166 | https://www.thomas-krenn.com/de/wiki/SSD_Performance_mit_TKperf_vergleichen 167 | 168 | ## Further information 169 | * To get more information about how the SSD tests are carried out, visit 170 | http://www.snia.org/tech_activities/standards/curr_standards/pts for the 171 | standard these tests are based on. 172 | 173 | ## Help Text 174 | 175 | ### tkperf 176 | ``` 177 | usage: tkperf [-h] [-v] [-d] [-q] [-nj NUMJOBS] [-iod IODEPTH] [-rt RUNTIME] 178 | [-i {sas,nvme,fusion}] [-xml] [-rfb] [-dsc DESC_FILE] 179 | [-c CONFIG] [-ft] [-fm FEATURE_MATRIX] [-hddt {iops,tp}] 180 | [-ssdt {iops,lat,tp,writesat}] [-m MAIL] [-s SMTP] 181 | [-g GEN_REPORT] 182 | {hdd,ssd,raid} testname device 183 | 184 | positional arguments: 185 | {hdd,ssd,raid} specify the test mode for the device 186 | testname name of the performance tests, corresponds to the 187 | result output filenames 188 | device device to run fio test on 189 | 190 | optional arguments: 191 | -h, --help show this help message and exit 192 | -v, --version get the version information 193 | -d, --debug get detailed debug information 194 | -q, --quiet turn off logging of info messages 195 | -nj NUMJOBS, --numjobs NUMJOBS 196 | specify number of jobs for fio 197 | -iod IODEPTH, --iodepth IODEPTH 198 | specify iodepth for libaio used by fio 199 | -rt RUNTIME, --runtime RUNTIME 200 | specify the fio runtime of one test round, if not set 201 | this is 60 seconds 202 | -i {sas,nvme,fusion}, --interface {sas,nvme,fusion} 203 | specify optional device interface 204 | -xml, --fromxml don't run tests but load test objects from xml file 205 | -rfb, --refill_buffers 206 | use Fio's refill buffers option to circumvent any 207 | compression of devices 208 | -dsc DESC_FILE, --desc_file DESC_FILE 209 | use a description file for the tested device if hdparm 210 | doesn't work correctly 211 | -c CONFIG, --config CONFIG 212 | specify the config file for a raid device 213 | -ft, --force_test skip checks if the used device is mounted, don't print 214 | warnings and force starting the test 215 | -fm FEATURE_MATRIX, --feature_matrix FEATURE_MATRIX 216 | add a feature matrix of the given device to the report 217 | -hddt {iops,tp}, --hdd_type {iops,tp} 218 | choose which tests are run 219 | -ssdt {iops,lat,tp,writesat}, --ssd_type {iops,lat,tp,writesat} 220 | choose which tests are run 221 | -m MAIL, --mail MAIL Send reports or errors to mail address, needs -s to be 222 | set 223 | -s SMTP, --smtp SMTP Use the specified smtp server to send mails, uses port 224 | 25 to connect 225 | -g GEN_REPORT, --gen_report GEN_REPORT 226 | Set and specify command to generate pdf report, e.g. 227 | rst2pdf 228 | ``` 229 | 230 | ### tkperf-cmp 231 | ``` 232 | $ tkperf-cmp -h 233 | usage: tkperf-cmp [-h] [-v] [-d] [-q] [-f FOLDER] [-z] 234 | {hdd,ssd,raid} xmls [xmls ...] 235 | 236 | positional arguments: 237 | {hdd,ssd,raid} specify the test mode for the device 238 | xmls XML files to read from 239 | 240 | optional arguments: 241 | -h, --help show this help message and exit 242 | -v, --version get the version information 243 | -d, --debug get detailed debug information 244 | -q, --quiet turn off logging of info messages 245 | -f FOLDER, --folder FOLDER 246 | store compare plots in a subfolder, specify name of 247 | folder 248 | -z, --zip store compare plots in a zip archive, requires '-f' as 249 | zip is created from subfolder 250 | ``` 251 | 252 | ## Copyright (C) 2015-2018 Thomas-Krenn.AG 253 | This program is free software; you can redistribute it and/or modify it under 254 | the terms of the GNU General Public License as published by the Free Software 255 | Foundation; either version 3 of the License, or (at your option) any later 256 | version. 257 | 258 | This program is distributed in the hope that it will be useful, but WITHOUT 259 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 260 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 261 | details. 262 | 263 | You should have received a copy of the GNU General Public License along with 264 | this program; if not, see . 265 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | Changelog for TKperf, performance tests for ssds, hdds and RAIDs 3 | #################################################################### 4 | 5 | Visit https://github.com/thomas-krenn/TKperf to get current development 6 | versions of TKperf and detailed changes. 7 | 8 | Version 2.2 20180926 9 | * Add timestamps to log messages 10 | * Add read and write policies for megaraid devices 11 | * Use markdown for README 12 | * Set auto background initialization to off for megaraid devices 13 | * Fix device number to 2 if megaraid RAID10 devices are created 14 | * Add strip size for megaraid devices 15 | * NVME: use correct lbaf for nvme format command 16 | * Set correct StdyState if device did not reach steady state 17 | * NVME: use correct interface with NVME RAID devices 18 | 19 | Version 2.1 20151016 20 | * Fix max rounds bug for SSD write saturation 21 | * If the maximum rounds 1440 was reached for an SSD the number of 22 | rounds has not been set correctly. 23 | * Add option for specifying test round runtime 24 | * Add 'fusion' to valid interface names, secure erasion FusionIO cards 25 | * Use nvme format for devices with nvme interface 26 | 27 | Version 2.0 20150121 28 | * Refactor performance tests and devices with abstract classes 29 | * Devices implementing the Device class can be used as test device 30 | * Add a system package for interaction with OS command line tools like 31 | storcli or mdadm 32 | * Added new RAID tests, RAIDs are created automatically 33 | * Avago or LSI devices can be tested with a specific RAID config file 34 | * Preconditioning is done automatically on all devices in the RAID set 35 | * Software RAID with mdadm can be tested with a specific RAID config file 36 | * Preconditioning is done automatically on all devices in the RAID set 37 | * Secure erase is done automatically on all devices in the RAID set 38 | * SAS devices are tested automatically with sf_format and sg_info 39 | * Added new TKperf logo 40 | * Remove tkperf_dialog.sh as it is obsolete 41 | * Fix error in tkperf-comp LAT plot 42 | * Enhance tkperf-cmp 43 | * Plots can be put into a subfolder and be compressed with zip 44 | * If -g is used a PDF report is generated automatically 45 | * Reports can be sent to an email automatically, use -m and -s 46 | 47 | Version 1.3 20140804 48 | * Support 3D plots also with matplotlib < 1.0 49 | * Added a sleep operation after each secure erase 50 | * Added tkperf-cmp, a script to generate compare plots 51 | 52 | Version 1.2 20131017 53 | * Return an error if hdparm doesn't work for HDD 54 | 55 | Version 1.1 20130117 56 | * Checking for correct FIO version 57 | * Added more hdparm informations about a device (e.g. write-cache) 58 | * Description files to provide informations for devices that 59 | cannot be checked with hdparm 60 | * Loading from Xml is possible if plotting or reporting functions 61 | changed and the report should be generated again 62 | * With setup.py an installations script is provided to ease TKperf 63 | installation 64 | * FIO version is included in the report 65 | * Date of test run is included in the report 66 | * Number of jobs and number of outstanding I/Os can be changed 67 | * Added option "refill_buffers" to circumvent any compression of 68 | SSD controllers 69 | * OS distribution information is included in the report 70 | * tkperf_dialog.sh is a shell script to start a simple test script 71 | via answering interactive questions 72 | 73 | Version 1.0-dev 20121123 74 | * SSD 75 | * Tests 76 | * Iops 77 | * Latency 78 | * Write Saturation 79 | * Throughput 80 | * Plots and measurement tables required by SNIA spec 81 | * Logging all FIO calls to log file 82 | * Xml file with test results 83 | * Rst file with restructured text 84 | * Rst file can be converted to pdf with rst2pdf 85 | * HDD 86 | * Tests 87 | * Iops 88 | * Throughput 89 | * Measurement plots 90 | 91 | List of contributors to version 2.* branch: 92 | Georg Schönberger, Thomas-Krenn.AG (author) 93 | ################################################################################ 94 | -------------------------------------------------------------------------------- /examples/MegaRAID.txt: -------------------------------------------------------------------------------- 1 | { 2 | "devices": [ 3 | "252:0", 4 | "252:1" 5 | ], 6 | "raidlevel": 1, 7 | "type": "hw_lsi", 8 | "readpolicy": "nora", 9 | "writepolicy": "wt", 10 | "stripsize": "256" 11 | } 12 | sudo tkperf raid MEGARAID-R1 /dev/sda -c raid1.cfg -nj 2 -iod 16 13 | -------------------------------------------------------------------------------- /examples/compare.txt: -------------------------------------------------------------------------------- 1 | tkperf-cmp ssd Samsung840PRO-256GB.xml Samsung840EVO-250GB.xml 2 | -------------------------------------------------------------------------------- /examples/intel320.txt: -------------------------------------------------------------------------------- 1 | sudo tkperf ssd intel320 /dev/sdb -nj 2 -iod 16 -rfb 2 | -------------------------------------------------------------------------------- /examples/mdadmRAID.txt: -------------------------------------------------------------------------------- 1 | { 2 | "devices": [ 3 | "/dev/sdb", 4 | "/dev/sdc", 5 | "/dev/sdd" 6 | ], 7 | "raidlevel": 5, 8 | "type": "sw_mdadm" 9 | } 10 | sudo tkperf raid SW-I3500-R5-3 /dev/md0 -c raid.cfg -nj 2 -iod 16 -------------------------------------------------------------------------------- /scripts/tkperf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Created on 27.06.2012 4 | 5 | @author: gschoenb 6 | @version: 2.2 7 | ''' 8 | import argparse 9 | import logging 10 | import sys 11 | 12 | from perfTest.Devices import SSD 13 | from perfTest.Devices import HDD 14 | from perfTest.Devices import RAID 15 | from perfTest.Options import Options 16 | from perfTest.PerfTest import SsdPerfTest 17 | from perfTest.PerfTest import HddPerfTest 18 | import perfTest.PerfTest as pT 19 | from system.Mail import Mail 20 | from email.errors import MessageError 21 | 22 | if __name__ == '__main__': 23 | tkPerfVersion = "TKperf Version: " + pT.__version__ 24 | 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument("mode", help="specify the test mode for the device", choices=["hdd","ssd","raid"]) 27 | parser.add_argument("testname",help="name of the performance tests, corresponds to the result output filenames") 28 | parser.add_argument("device",help="device to run fio test on") 29 | 30 | parser.add_argument("-v","--version", help="get the version information", action='version',version=tkPerfVersion) 31 | parser.add_argument("-d","--debug", help="get detailed debug information",action ='store_true') 32 | parser.add_argument("-q","--quiet", help="turn off logging of info messages",action ='store_true') 33 | parser.add_argument("-nj","--numjobs",help="specify number of jobs for fio",type=int) 34 | parser.add_argument("-iod","--iodepth",help="specify iodepth for libaio used by fio",type=int) 35 | parser.add_argument("-rt","--runtime",help="specify the fio runtime of one test round, if not set this is 60 seconds",type=int) 36 | parser.add_argument("-i","--interface",help="specify optional device interface",choices=["sas","nvme","fusion","usb","sdcard","compactflash"]) 37 | parser.add_argument("-xml","--fromxml",help="don't run tests but load test objects from xml file", 38 | action='store_true') 39 | parser.add_argument("-rfb","--refill_buffers",help="use Fio's refill buffers option to circumvent any compression of devices", 40 | action='store_true') 41 | parser.add_argument("-dsc","--desc_file",help="use a description file for the tested device if hdparm doesn't work correctly", 42 | type=argparse.FileType('r')) 43 | parser.add_argument("-c","--config",help="specify the config file for a raid device", 44 | type=argparse.FileType('r')) 45 | parser.add_argument("-ft","--force_test",help="skip checks if the used device is mounted, don't print warnings and force starting the test", 46 | action='store_true') 47 | parser.add_argument("-fm","--feature_matrix",help="add a feature matrix of the given device to the report", 48 | type=argparse.FileType('r')) 49 | parser.add_argument("-hddt","--hdd_type",help="choose which tests are run", 50 | choices=['iops','tp'],action='append',dest='hddt') 51 | parser.add_argument("-ssdt","--ssd_type",help="choose which tests are run", 52 | choices=['iops','lat','tp','writesat'],action='append',dest='ssdt') 53 | parser.add_argument("-m","--mail",help="Send reports or errors to mail address, needs -s to be set") 54 | parser.add_argument("-s","--smtp",help="Use the specified smtp server to send mails, uses port 25 to connect") 55 | parser.add_argument("-g","--gen_report",help="Set and specify command to generate pdf report, e.g. rst2pdf") 56 | args = parser.parse_args() 57 | # Configure logging levels 58 | logformat = '%(asctime)s %(name)-8s %(levelname)-8s %(message)s' 59 | logdatefmt = '%Y%m%d %H:%M' 60 | if args.fromxml == False: 61 | logfile = args.testname+'.log' 62 | else: 63 | logfile = args.testname+'.xml.log' 64 | if args.debug == True: 65 | logging.basicConfig(filename=logfile,level=logging.DEBUG,format=logformat,datefmt=logdatefmt) 66 | if args.quiet == True: 67 | logging.basicConfig(filename=logfile,level=logging.WARNING,format=logformat,datefmt=logdatefmt) 68 | else: 69 | logging.basicConfig(filename=logfile,level=logging.INFO,format=logformat,datefmt=logdatefmt) 70 | # Create objects, a device and given options 71 | if args.mode == "ssd": 72 | devToTest = SSD(args.mode,args.device,args.testname) 73 | if args.mode == "hdd": 74 | devToTest = HDD(args.mode,args.device,args.testname) 75 | if args.mode == "raid": 76 | devToTest = RAID(args.mode,args.device,args.testname) 77 | if args.interface != None: 78 | devToTest.setInterface(args.interface) 79 | options = Options() 80 | if args.numjobs != None: 81 | options.setNj(args.numjobs) 82 | if args.iodepth != None: 83 | options.setIod(args.iodepth) 84 | if args.runtime != None: 85 | options.setRuntime(args.runtime) 86 | if args.refill_buffers == True: 87 | xargs = ['refill_buffers'] 88 | options.setXargs(xargs) 89 | # Create performance test objects, don't yet init them 90 | if args.mode == "ssd" or args.mode == "raid": 91 | if args.ssdt != None: 92 | SsdPerfTest.testKeys = args.ssdt 93 | myTest = SsdPerfTest(args.testname, devToTest,options) 94 | if args.mode == "hdd": 95 | if args.hddt != None: 96 | HddPerfTest.testKeys = args.hddt 97 | myTest = HddPerfTest(args.testname, devToTest,options) 98 | # First check if we are loading values from a given xml 99 | if args.fromxml == True: 100 | print("Loading from xml file...") 101 | myTest.fromXml() 102 | myTest.genPlots() 103 | myTest.toRst() 104 | exit(0) 105 | # Start a real performance test 106 | try: 107 | # A raid test needs a raid config 108 | if args.mode == "raid": 109 | devToTest.setConfig(args.config) 110 | myTest.initialize() 111 | except RuntimeError: 112 | print("### Error! ###") 113 | print("Test initialization failed, please inspect the log file!") 114 | exit(1) 115 | # Keep used command line arguments 116 | myTest.readCmdLineArgs(sys.argv) 117 | # Check if a correct setup is given 118 | if (not devToTest.isInitialized()) and args.desc_file == None: 119 | print("### Error! ###") 120 | print("Please use a description file for the current device.") 121 | print("The information via hdparm -I is not reliable.") 122 | print("Use -dsc DESC_FILE to provide the information") 123 | exit(1) 124 | if args.desc_file != None: 125 | devToTest.readDevInfoFile(args.desc_file) 126 | if args.feature_matrix != None: 127 | devToTest.readFeatureFile(args.feature_matrix) 128 | # Don't print a warning if force test is given 129 | if args.force_test == False: 130 | if devToTest.isMounted(): 131 | print("!!!WARNING!!!") 132 | print("You are testing a mounted device, this is highly dangerous!") 133 | exit(0) 134 | if devToTest.isAvailable(): 135 | print("!!!Attention!!!") 136 | print("All data on " + args.device + " will be lost!") 137 | print("Are you sure you want to continue? (In case you really know what you are doing.)") 138 | print("Press 'y' to continue, any key to stop:") 139 | key = input() 140 | if key != 'y': 141 | exit(0) 142 | else: 143 | print("You are not using a valid device or partition!") 144 | exit(1) 145 | print("Starting "+args.mode+" mode...") 146 | print("Testing device:") 147 | print(devToTest.getDevInfo()) 148 | try: 149 | myTest.run() 150 | if args.gen_report != None: 151 | try: 152 | myTest.getRstReport().toPDF(args.gen_report) 153 | except RuntimeError: 154 | print("### Error! ###") 155 | print("Generating PDF failed.") 156 | if args.mail != None and args.smtp != None: 157 | try: 158 | mail = Mail('TKperf message', 'root@tkperf.local', args.mail, args.smtp) 159 | mail.addMsg('Please find your TKperf report as attachment!') 160 | if args.gen_report != None: 161 | mail.addPDFAttachment(args.testname+'.pdf') 162 | mail.addTextAttachment(args.testname+'.rst') 163 | mail.addXMLAttachment(args.testname+'.xml') 164 | mail.addTextAttachment(logfile) 165 | mail.send() 166 | except MessageError: 167 | print("### Error! ###") 168 | print("Creating and sending mail failed.") 169 | except RuntimeError: 170 | print("### Error! ###") 171 | print("Running the tests failed, please inspect the log file!") 172 | if args.mail != None and args.smtp != None: 173 | try: 174 | mail = Mail('TKperf message', 'root@tkperf.local', args.mail, args.smtp) 175 | mail.addMsg('A TKperf error occurred, please inspect the log file!') 176 | mail.addTextAttachment(logfile) 177 | mail.send() 178 | except MessageError: 179 | print("### Error! ###") 180 | print("Creating and sending mail failed.") 181 | exit(1) 182 | -------------------------------------------------------------------------------- /scripts/tkperf-cmp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Created on 27.06.2012 4 | 5 | @author: gschoenb 6 | ''' 7 | import argparse 8 | import logging 9 | from os import makedirs 10 | from os import path 11 | from os import walk 12 | import zipfile 13 | 14 | from perfTest.Devices import SSD 15 | from perfTest.Devices import HDD 16 | from perfTest.Options import Options 17 | from perfTest.PerfTest import SsdPerfTest 18 | from perfTest.PerfTest import HddPerfTest 19 | import perfTest.PerfTest as pT 20 | import plots.compPlots as pcp 21 | 22 | if __name__ == '__main__': 23 | tkPerfVersion = "TKperf Version: " + pT.__version__ 24 | parser = argparse.ArgumentParser() 25 | 26 | parser.add_argument("mode", help="specify the test mode for the device", choices=["hdd","ssd","raid"]) 27 | parser.add_argument("xmls", help="XML files to read from", type=str, nargs='+') 28 | 29 | parser.add_argument("-v","--version", help="get the version information", action='version',version=tkPerfVersion) 30 | parser.add_argument("-d","--debug", help="get detailed debug information",action ='store_true') 31 | parser.add_argument("-q","--quiet", help="turn off logging of info messages",action ='store_true') 32 | parser.add_argument("-f","--folder", help="store compare plots in a subfolder, specify name of folder",type=str) 33 | parser.add_argument("-z","--zip",help="store compare plots in a zip archive, requires '-f' as zip is created from subfolder", 34 | action ='store_true') 35 | 36 | args = parser.parse_args() 37 | if args.debug == True: 38 | logging.basicConfig(filename='tkperf-cmp.log',level=logging.DEBUG) 39 | if args.quiet == True: 40 | logging.basicConfig(filename='tkperf-cmp.log',level=logging.WARNING) 41 | else: 42 | logging.basicConfig(filename='tkperf-cmp.log',level=logging.INFO) 43 | 44 | # Strip the filename suffix as it is appended automatically 45 | for i,file in enumerate(args.xmls): 46 | if file.endswith('.xml'): 47 | file = file[:-4] 48 | args.xmls[i] = file 49 | # Check if plots go into a subfolder 50 | if args.folder != None: 51 | try: 52 | makedirs(args.folder) 53 | except OSError: 54 | if not path.isdir(args.folder): 55 | raise 56 | # Generate objects and plots 57 | toCompare = [] 58 | for file in args.xmls: 59 | if args.mode == 'ssd' or args.mode == 'raid': 60 | options = Options() 61 | dummyDev = SSD('ssd',None,file) 62 | myTest = SsdPerfTest(file, dummyDev, options) 63 | if args.mode == 'hdd': 64 | dummyDev = HDD('hdd',None,file) 65 | myTest = HddPerfTest(file, dummyDev, options) 66 | myTest.fromXml() 67 | toCompare.append(myTest) 68 | 69 | pcp.compWriteSatIOPSPlt(toCompare, args.folder) 70 | pcp.compILPlt(toCompare, 'IOPS', args.folder) 71 | pcp.compILPlt(toCompare, 'LAT', args.folder) 72 | pcp.compTPPlt(toCompare, args.folder) 73 | # Check if a zip archive should be created 74 | if args.zip: 75 | if args.folder == None: 76 | print("### Error! ###") 77 | print("Please use Option '-f' in conjunction with '-z'.") 78 | else: 79 | zfile = zipfile.ZipFile(args.folder+'.zip', 'w') 80 | for root, dirs, files in walk(args.folder): 81 | for file in files: 82 | zfile.write(path.join(root, file)) 83 | zfile.close() 84 | exit(0) 85 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='TKperf', 6 | version='2.2', 7 | description='TKperf - Thomas Krenn IO Performance Tests', 8 | author='Georg Schoenberger', 9 | author_email='g.schoenberger@xortex.com', 10 | url='https://github.com/thomas-krenn/TKperf.git', 11 | package_dir = {'': 'src'}, 12 | packages = ['fio', 'perfTest','plots','reports','system'], 13 | package_data = {'reports':['pics/TKperf_logo.png']}, 14 | scripts = ["scripts/tkperf","scripts/tkperf-cmp"], 15 | license = 'GPL' 16 | ) 17 | -------------------------------------------------------------------------------- /src/fio/FioJob.py: -------------------------------------------------------------------------------- 1 | ''' @package FioJob 2 | A module realizing a fio job run. 3 | ''' 4 | import subprocess 5 | import logging 6 | import re 7 | import json 8 | from lxml import etree 9 | 10 | class FioJob(object): 11 | ''' 12 | A class configuring the fio job. 13 | ''' 14 | ## Position of read IOPS in the fio terse output. 15 | terseIOPSReadPos = 7 16 | 17 | ## Position of write IOPS in the fio terse output. 18 | terseIOPSWritePos = 48 19 | 20 | ## Position of write total IO in the fio terse output 21 | terseTotIOWritePos = 46 22 | 23 | ## Start Position of write latencies in fio terse output 24 | terseLatStartWritePos = 78 25 | 26 | ## Start Position of read latencies in fio terse output 27 | terseLatStartReadPos = 37 28 | 29 | ## Postion of total read throughput. 30 | terseTPReadPos = 6 31 | 32 | ## Postion of total write throughput. 33 | terseTPWritePos = 47 34 | 35 | def __init__(self): 36 | ''' The constructor ''' 37 | ## Fio path 38 | self.__fioPath = None 39 | ## Fio version 40 | self.__fioVersion = None 41 | ## Key value arguments e.g. name="test" 42 | self.__fioKVArgs = {} 43 | ## Single arguments e.g. group_reporting 44 | self.__fioSglArgs = [] 45 | 46 | def __str__(self): 47 | ''' Return a string representation of the fio executable. ''' 48 | res = "fio: " + self.__fioPath + ", " + self.__fioVersion 49 | return res 50 | 51 | def initialize(self): 52 | ''' Initialize Fio path and version. ''' 53 | fio = subprocess.Popen(['which', 'fio'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 54 | stdout = fio.communicate()[0] 55 | if fio.returncode != 0: 56 | logging.error("# Error: command 'which fio' returned an error code.") 57 | raise RuntimeError("which fio command error") 58 | 59 | self.__fioPath = stdout.rstrip("\n"); 60 | fio = subprocess.Popen(['fio','--version'],stdout=subprocess.PIPE,universal_newlines=True) 61 | self.__fioVersion = fio.communicate()[0] 62 | 63 | def getFioVersion(self): 64 | ''' Return the current Fio version string. ''' 65 | return self.__fioVersion 66 | 67 | def setFioVersion(self,fioStr): 68 | ''' Set the used Fio version (useful if loading from xml). ''' 69 | self.__fioVersion = fioStr 70 | 71 | def checkFioVersion(self): 72 | ''' Check if the Fio version is high enough. ''' 73 | if self.__fioVersion != None: 74 | match = re.search(r'[\d\.]+',self.__fioVersion) 75 | if match == None: 76 | logging.error("# Error: checking fio version returned a none string.") 77 | raise RuntimeError("fio version string error") 78 | version = match.group().split('.') 79 | if int(version[0]) < 2: 80 | logging.error("# Error: the fio version is to old, ensure to use > 2.0.3.") 81 | raise RuntimeError("fio version to old error") 82 | if int(version[0]) >= 2: 83 | if int(version[1]) == 0: 84 | if int(version[2]) < 3: 85 | logging.error("# Error: the fio version is to old, ensure to use > 2.0.3.") 86 | raise RuntimeError("fio version to old error") 87 | 88 | def appendXml(self,root): 89 | ''' 90 | Append the information about Fio to a XML node. 91 | @param root The xml root tag to append the new elements to. 92 | ''' 93 | data = json.dumps(self.__fioVersion) 94 | e = etree.SubElement(root,'fioversion') 95 | e.text = data 96 | 97 | def fromXml(self,root): 98 | ''' 99 | Loads the information about Fio from XML. 100 | @param root The given element containing the information about 101 | the object to be initialized. 102 | ''' 103 | if root.findtext('fioversion'): 104 | self.__fioVersion = json.loads(root.findtext('fioversion')) 105 | else: 106 | self.__fioVersion = 'n.a' 107 | logging.info("# Loading Fio version from xml") 108 | 109 | def getKVArgs(self): 110 | ''' Return the current configured Fio key value arguments. ''' 111 | return self.__fioKVArgs 112 | 113 | def getSglArgs(self): 114 | ''' Return the current configured Fio single key arguments. ''' 115 | return self.__fioSglArgs 116 | 117 | def addKVArg(self,key,value): 118 | ''' Add a key value pair as an argument to fio. 119 | @param key Name of the option for Fio. 120 | @param value Value for the given Fio option. 121 | ''' 122 | self.__fioKVArgs[key] = value 123 | 124 | def addSglArg(self,key): 125 | ''' Add a single value option to fio argument list. 126 | @param key Name of the option being added. 127 | ''' 128 | self.__fioSglArgs.append(key) 129 | 130 | def prepKVArgs(self): 131 | ''' Generate an argument list out of the dictionary suited for fio. ''' 132 | argList = [self.__fioPath] 133 | for k,v in self.__fioKVArgs.items(): 134 | argList.append('--' + k + '=' + v) 135 | return argList 136 | 137 | def prepSglArgs(self,argList): 138 | ''' Generate an argument list out of the single key arguments. ''' 139 | for k in self.__fioSglArgs: 140 | argList.append('--' + k) 141 | return argList 142 | 143 | def start(self): 144 | ''' Start a Fio job with its argument list. 145 | The argument list defines the parameters given to Fio. 146 | @return [True,standard output] of the Fio test or [False,0] on error. 147 | ''' 148 | args = self.prepKVArgs() 149 | args = self.prepSglArgs(args) 150 | logging.info('%s',args) 151 | if len(args) == 0: 152 | logging.error("Error: Fio argument list is empty.") 153 | exit(1) 154 | out = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 155 | (stdout,stderr) = out.communicate() 156 | if stderr != '': 157 | logging.error("Fio encountered an error: " + stderr) 158 | return [False,''] 159 | else: 160 | return [True,stdout] 161 | 162 | def getIOPS(self,fioOut): 163 | ''' 164 | Parses the average IOPS out of the Fio result output. 165 | @param fioOut The output of the Fio performance test. 166 | @return Sum of read IOPS and write IOPS. 167 | ''' 168 | #index 7 iops read 169 | #index 48 iops write 170 | fioTerse = fioOut.split(';') 171 | return int(fioTerse[FioJob.terseIOPSReadPos]) + int(fioTerse[FioJob.terseIOPSWritePos]) 172 | 173 | def getIOPSRead(self,fioOut): 174 | ''' 175 | Parses the average read IOPS out of the fio result output. 176 | @param fioOut The output of the fio performance test. 177 | @return Read IOPS 178 | ''' 179 | #index 7 iops read 180 | fioTerse = fioOut.split(';') 181 | return int(fioTerse[FioJob.terseIOPSReadPos]) 182 | 183 | def getIOPSWrite(self,fioOut): 184 | ''' 185 | Parses the average write IOPS out of the Fio result output. 186 | @param fioOut The output of the Fio performance test. 187 | @return Write IOPS 188 | ''' 189 | #index 48 iops write 190 | fioTerse = fioOut.split(';') 191 | return int(fioTerse[FioJob.terseIOPSWritePos]) 192 | 193 | def getTotIOWrite(self,fioOut): 194 | ''' 195 | Parses the write total IO out of the Fio result output. 196 | @param fioOut The output of the Fio performance test. 197 | @return Write total IO in KB. 198 | ''' 199 | #index 46 write total IO 200 | fioTerse = fioOut.split(';') 201 | return int(fioTerse[FioJob.terseTotIOWritePos]) 202 | 203 | def getWriteLats(self,fioOut): 204 | ''' 205 | Parses the write total latencies out of the Fio result output. 206 | @param fioOut The output of the Fio performance test. 207 | @return [min,max,mean] total write latencies in microseconds. 208 | ''' 209 | #index 78 write total latency 210 | fioTerse = fioOut.split(';') 211 | return [float(fioTerse[FioJob.terseLatStartWritePos]), 212 | float(fioTerse[FioJob.terseLatStartWritePos + 1]), 213 | float(fioTerse[FioJob.terseLatStartWritePos + 2])] 214 | 215 | def getReadLats(self,fioOut): 216 | ''' 217 | Parses the read total latencies out of the Fio result output. 218 | @param fioOut The output of the Fio performance test. 219 | @return [min,max,mean] total read latencies in microseconds. 220 | ''' 221 | #index 78 write total latency 222 | fioTerse = fioOut.split(';') 223 | return [float(fioTerse[FioJob.terseLatStartReadPos]), 224 | float(fioTerse[FioJob.terseLatStartReadPos + 1]), 225 | float(fioTerse[FioJob.terseLatStartReadPos + 2])] 226 | 227 | def getTotLats(self,fioOut): 228 | ''' 229 | Parses the read+write total latencies out of the Fio result output. 230 | @param fioOut The output of the Fio performance test. 231 | @return [min,max,mean] total latencies in microseconds. 232 | ''' 233 | #index 78 write total latency 234 | fioTerse = fioOut.split(';') 235 | return [float(fioTerse[FioJob.terseLatStartReadPos]) + 236 | float(fioTerse[FioJob.terseLatStartWritePos]), 237 | float(fioTerse[FioJob.terseLatStartReadPos + 1]) + 238 | float(fioTerse[FioJob.terseLatStartWritePos + 1]), 239 | float(fioTerse[FioJob.terseLatStartReadPos + 2]) + 240 | float(fioTerse[FioJob.terseLatStartWritePos + 2])] 241 | 242 | def getTPRead(self,fioOut): 243 | ''' 244 | Parses the read bandwidth of the Fio result output. 245 | @param fioOut The output of the Fio performance test. 246 | @return Read total bandwidth. 247 | ''' 248 | #index 6 write total IO 249 | fioTerse = fioOut.split(';') 250 | return int(fioTerse[FioJob.terseTPReadPos]) 251 | 252 | def getTPWrite(self,fioOut): 253 | ''' 254 | Parses the write bandwidth of the Fio result output. 255 | @param fioOut The output of the Fio performance test. 256 | @return Write total bandwidth. 257 | ''' 258 | #index 47 write total IO 259 | fioTerse = fioOut.split(';') 260 | return int(fioTerse[FioJob.terseTPWritePos]) -------------------------------------------------------------------------------- /src/fio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-krenn/TKperf/638b0837b97b32337314993feb52b5111aae0979/src/fio/__init__.py -------------------------------------------------------------------------------- /src/perfTest/Options.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 26, 2014 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | import logging 8 | import json 9 | from lxml import etree 10 | 11 | class Options(object): 12 | ''' 13 | A class holding user defined options on command line. 14 | ''' 15 | 16 | def __init__(self, nj=1, iod=1, runtime=60, xargs=None): 17 | ''' 18 | Constructor 19 | @param nj Number of jobs 20 | @param iod Number for io depth 21 | @param xargs Further argument as list for all fio jobs in tests 22 | ''' 23 | ## Number of jobs for fio. 24 | self.__nj = nj 25 | ## Number of iodepth for fio. 26 | self.__iod = iod 27 | ## Runtime of one test round for fio. 28 | self.__runtime = runtime 29 | ## Further single arguments as list for fio. 30 | self.__xargs = xargs 31 | 32 | def getNj(self): return self.__nj 33 | def getIod(self): return self.__iod 34 | def getRuntime(self): return self.__runtime 35 | def getXargs(self): return self.__xargs 36 | def setNj(self,nj): self.__nj = nj 37 | def setIod(self,iod): self.__iod = iod 38 | def setRuntime(self,rt): self.__runtime = rt 39 | def setXargs(self,xargs): self.__xargs = xargs 40 | 41 | def appendXml(self,r): 42 | ''' 43 | Append the information about options to a XML node. 44 | @param root The xml root tag to append the new elements to 45 | ''' 46 | data = json.dumps(self.__nj) 47 | e = etree.SubElement(r,'numjobs') 48 | e.text = data 49 | 50 | data = json.dumps(self.__iod) 51 | e = etree.SubElement(r,'iodepth') 52 | e.text = data 53 | 54 | data = json.dumps(self.__runtime) 55 | e = etree.SubElement(r,'runtime') 56 | e.text = data 57 | 58 | if self.__xargs != None: 59 | data = json.dumps(list(self.__xargs)) 60 | e = etree.SubElement(r,'xargs') 61 | e.text = data 62 | 63 | def fromXml(self,root): 64 | ''' 65 | Loads the information about options from XML. 66 | @param root The given element containing the information about 67 | the object to be initialized. 68 | ''' 69 | if root.findtext('numjobs'): 70 | self.__nj = json.loads(root.findtext('numjobs')) 71 | if root.findtext('iodepth'): 72 | self.__iod = json.loads(root.findtext('iodepth')) 73 | if root.findtext('runtime'): 74 | self.__runtime = json.loads(root.findtext('runtime')) 75 | if root.findtext('xargs'): 76 | self.__xargs = json.loads(root.findtext('xargs')) 77 | logging.info("# Loading options from xml") 78 | logging.info("# Options nj:"+str(self.__nj)) 79 | logging.info("# Options iod: "+str(self.__iod)) 80 | if self.__xargs != None: 81 | logging.info("# Options xargs:") 82 | logging.info(self.__xargs) -------------------------------------------------------------------------------- /src/perfTest/PerfTest.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 07.08.2012 3 | 4 | @author: gschoenb 5 | ''' 6 | __version__ = '2.2' 7 | 8 | from abc import ABCMeta, abstractmethod 9 | import logging 10 | import subprocess 11 | from lxml import etree 12 | import json 13 | import datetime 14 | import os 15 | import time 16 | 17 | import perfTest.DeviceTests as dt 18 | from perfTest.Devices import SSD 19 | from perfTest.Devices import HDD 20 | from perfTest.Options import Options 21 | from reports.XmlReport import XmlReport 22 | from reports.RstReport import RstReport 23 | 24 | class PerfTest(object): 25 | ''' 26 | A performance test, consists of multiple Device Tests. 27 | ''' 28 | 29 | def __init__(self,testname,device): 30 | ''' 31 | A performance test has several reports and plots. 32 | @param testname Name of the performance test. 33 | @param device A Device object, the device to run tests on. 34 | ''' 35 | ## The output file for the fio job test results. 36 | self.__testname = testname 37 | 38 | ## The device object to run test on. 39 | self.__device = device 40 | 41 | ## Xml file to write test results to 42 | self.__xmlReport = XmlReport(self.__testname) 43 | 44 | ## Rst file to generate pdf of 45 | self.__rstReport = RstReport(self.__testname) 46 | 47 | ## Dictionary of tests to carry out 48 | self.__tests = {} 49 | 50 | ## Date the test has been carried out 51 | self.__testDate = None 52 | 53 | ## Per default use the version from main module 54 | self.__IOPerfVersion = __version__ 55 | 56 | ## Information about the used operating system 57 | self.__OSInfo = {} 58 | self.collOSInfos() 59 | 60 | ## Hold the command line used to call the test 61 | self.__cmdLineArgs = None 62 | 63 | def getTestname(self): return self.__testname 64 | def getDevice(self): return self.__device 65 | def getTestDate(self): return self.__testDate 66 | def getIOPerfVersion(self): return self.__IOPerfVersion 67 | def getCmdLineArgs(self): return self.__cmdLineArgs 68 | def getOSInfo(self): return self.__OSInfo 69 | def getTests(self): return self.__tests 70 | def getXmlReport(self): return self.__xmlReport 71 | def getRstReport(self): return self.__rstReport 72 | 73 | def collOSInfos(self): 74 | ''' 75 | Collects some information about the current OS in use. 76 | @return True if all infos are present, False on error. 77 | ''' 78 | out = subprocess.Popen(['uname','-r'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 79 | (stdout,stderr) = out.communicate() 80 | if stderr != '': 81 | logging.error("uname -r encountered an error: " + stderr) 82 | return False 83 | else: 84 | self.__OSInfo['kernel'] = stdout 85 | #Check if we are on red hat based distributions 86 | if os.path.isfile('/etc/redhat-release'): 87 | out = subprocess.Popen(['cat','/etc/redhat-release'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 88 | else: 89 | out = subprocess.Popen(['lsb_release','-d'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 90 | (stdout,stderr) = out.communicate() 91 | if stderr != '': 92 | logging.error("getting OS information encountered an error: " + stderr) 93 | return False 94 | else: 95 | self.__OSInfo['lsb'] = stdout 96 | return True 97 | 98 | def readCmdLineArgs(self,argv): 99 | ''' 100 | Reads the command line argument list argv and sets it as 101 | __cmdLineArgs. 102 | @param argv The command line argument list. 103 | ''' 104 | self.__cmdLineArgs = '' 105 | for arg in argv: 106 | self.__cmdLineArgs += (arg + ' ') 107 | self.__cmdLineArgs = self.__cmdLineArgs.rstrip() 108 | 109 | def setOSInfo(self,key,value): 110 | ''' 111 | Sets the current OS information. 112 | @param key A key to be used in the OS info dict. 113 | @param value The value being assigned to the key. 114 | ''' 115 | if value != None: 116 | self.__OSInfo[key] = value 117 | 118 | def setTestDate(self,dateStr): 119 | ''' 120 | Sets the date the test has been carried out. 121 | @param dateStr The date string. 122 | ''' 123 | self.__testDate = dateStr 124 | 125 | def setIOPerfVersion(self,verStr): 126 | ''' 127 | Sets the current io perf version. 128 | @param verStr The version string. 129 | ''' 130 | self.__IOPerfVersion = verStr 131 | 132 | def setCmdLineArgs(self,cmdLineStr): 133 | ''' 134 | Sets the command line arg string. 135 | @param cmdLineStr The command line string. 136 | ''' 137 | self.__cmdLineArgs = cmdLineStr 138 | 139 | def addTest(self,key,test): 140 | ''' 141 | Add a test to the test dictionary. 142 | @param key The key for the test in the dictionary. 143 | @param test The test object to be added. 144 | ''' 145 | self.__tests[key] = test 146 | 147 | def resetTests(self): 148 | ''' 149 | Clear the dictionary containing the tests. 150 | ''' 151 | self.__tests.clear() 152 | 153 | def initialize(self): 154 | ''' 155 | Initialize the given tests, this sets the device and Fio 156 | init params for all tests. 157 | ''' 158 | sorted(self.__tests.items()) 159 | for k,v in list(self.__tests.items()): 160 | logging.info("# Initialiazing test "+k) 161 | v.initialize() 162 | 163 | def runTests(self): 164 | ''' 165 | Call the run method of every test in the test dictionary. The run method 166 | of a test is its core function where the performance test is carried out. 167 | ''' 168 | #sort per key to ensure tests have the same order 169 | sorted(self.__tests.items()) 170 | for k,v in list(self.__tests.items()): 171 | print("Starting test: " + k) 172 | #before each test sleep, to ensure device operations of previous 173 | #tests are finished 174 | logging.info("# Sleeping for 5 seconds...") 175 | time.sleep(5) 176 | v.run() 177 | 178 | def genPlots(self): 179 | ''' 180 | Generate the plots/charts for each specific test in the dictionary. 181 | ''' 182 | sorted(self.__tests.items()) 183 | for k,v in list(self.__tests.items()): 184 | logging.info("# Generating plots for "+k+" test") 185 | v.genPlots() 186 | 187 | def toXml(self): 188 | ''' 189 | First the device information is written to the xml file. 190 | Calls for every test in the test dictionary the toXMl method 191 | and writes the results to the xml file. 192 | ''' 193 | tests = self.getTests() 194 | e = self.getXmlReport().getXml() 195 | # Add the current date to the xml 196 | if self.__testDate != None: 197 | dev = etree.SubElement(e,'testdate') 198 | dev.text = json.dumps(self.__testDate) 199 | # Add the device information to the xml file 200 | self.getDevice().toXml(e) 201 | # Add OS information 202 | if self.__OSInfo != None: 203 | if 'kernel' in self.__OSInfo: 204 | dev = etree.SubElement(e,'kernel') 205 | dev.text = json.dumps(self.__OSInfo['kernel']) 206 | if 'lsb' in self.__OSInfo: 207 | dev = etree.SubElement(e,'lsb') 208 | dev.text = json.dumps(self.__OSInfo['lsb']) 209 | # Add the current test suite version to the xml file 210 | dev = etree.SubElement(e,'ioperfversion') 211 | dev.text = json.dumps(self.__IOPerfVersion) 212 | # Add the command line to xml 213 | if self.__cmdLineArgs != None: 214 | dev = etree.SubElement(e,'cmdline') 215 | dev.text = json.dumps(self.__cmdLineArgs) 216 | # Call the xml function for every test in the dictionary 217 | sorted(self.__tests.items()) 218 | for k,v in tests.items(): 219 | e.append(v.toXml(k)) 220 | self.getXmlReport().xmlToFile(self.getTestname()) 221 | 222 | def fromXml(self): 223 | ''' 224 | Reads out the xml file name 'testname.xml' and initializes the test 225 | specified with xml. The valid tags are "iops,lat,tp,writesat" for ssd, 226 | "iops, tp" for hdd. But there isn't always every test run, so xml can 227 | miss a test. 228 | ''' 229 | self.getXmlReport().fileToXml(self.getTestname()) 230 | self.resetTests() 231 | root = self.getXmlReport().getXml() 232 | 233 | if(root.findtext('testdate')): 234 | self.setTestDate(json.loads(root.findtext('testdate'))) 235 | else: 236 | self.setTestDate('n.a.') 237 | # Read the operating system information 238 | if(root.findtext('kernel')): 239 | self.setOSInfo('kernel',json.loads(root.findtext('kernel'))) 240 | else: 241 | self.setOSInfo('kernel','n.a.') 242 | if(root.findtext('lsb')): 243 | self.setOSInfo('lsb',json.loads(root.findtext('lsb'))) 244 | else: 245 | self.setOSInfo('lsb','n.a.') 246 | # Read version and command line 247 | if(root.findtext('ioperfversion')): 248 | self.setIOPerfVersion(json.loads(root.findtext('ioperfversion'))) 249 | if(root.findtext('cmdline')): 250 | self.setCmdLineArgs(json.loads(root.findtext('cmdline'))) 251 | else: 252 | self.setCmdLineArgs('n.a.') 253 | # Create an empty options object, it is initialized in fromXml 254 | options = Options(None,None) 255 | # Initialize device and performance tests 256 | if isinstance(self, SsdPerfTest): 257 | device = SSD('ssd',None,self.getTestname()) 258 | device.fromXml(root) 259 | for tag in SsdPerfTest.testKeys: 260 | #check which test tags are in the xml file 261 | for elem in root.iterfind(tag): 262 | test = None 263 | if elem.tag == SsdPerfTest.iopsKey: 264 | test = dt.SsdIopsTest(self.getTestname(),device,options) 265 | if elem.tag == SsdPerfTest.latKey: 266 | test = dt.SsdLatencyTest(self.getTestname(),device,options) 267 | if elem.tag == SsdPerfTest.tpKey: 268 | test = dt.SsdTPTest(self.getTestname(),device,options) 269 | if elem.tag == SsdPerfTest.wrKey: 270 | test = dt.SsdWriteSatTest(self.getTestname(),device,options) 271 | #we found a tag in the xml file, now we can read the data from xml 272 | if test != None: 273 | test.fromXml(elem) 274 | self.addTest(tag, test) 275 | elif isinstance(self, HddPerfTest): 276 | device = HDD('hdd',None,self.getTestname()) 277 | device.fromXml(root) 278 | for tag in HddPerfTest.testKeys: 279 | for elem in root.iterfind(tag): 280 | test = None 281 | if elem.tag == HddPerfTest.iopsKey: 282 | test = dt.HddIopsTest(self.getTestname(),device,options) 283 | if elem.tag == HddPerfTest.tpKey: 284 | test = dt.HddTPTest(self.getTestname(),device,options) 285 | if test != None: 286 | test.fromXml(elem) 287 | self.addTest(tag, test) 288 | 289 | @abstractmethod 290 | def toRst(self): 291 | ''' Convert tests to restructured text ''' 292 | 293 | def run(self): 294 | ''' The main run method, runs tests, generates plots and rst report. ''' 295 | self.runTests() 296 | self.toXml() 297 | self.genPlots() 298 | self.toRst() 299 | 300 | class SsdPerfTest(PerfTest): 301 | ''' 302 | A performance test for ssds consists of all ssd tests. 303 | ''' 304 | ## Keys valid for tests 305 | iopsKey = 'iops' 306 | latKey = 'lat' 307 | tpKey = 'tp' 308 | wrKey = 'writesat' 309 | ## Keys for the tests carried out 310 | testKeys = [iopsKey,latKey,tpKey,wrKey] 311 | 312 | def __init__(self,testname,device,options=None): 313 | ''' 314 | Cf. super constructor. 315 | ''' 316 | PerfTest.__init__(self, testname, device) 317 | #Add current date to test 318 | now = datetime.datetime.now() 319 | self.setTestDate(now.strftime("%Y-%m-%d")) 320 | #Add every test to the performance test 321 | for testType in SsdPerfTest.testKeys: 322 | if testType == SsdPerfTest.iopsKey: 323 | test = dt.SsdIopsTest(testname,device,options) 324 | if testType == SsdPerfTest.latKey: 325 | test = dt.SsdLatencyTest(testname,device,options) 326 | if testType == SsdPerfTest.tpKey: 327 | test = dt.SsdTPTest(testname,device,options) 328 | if testType == SsdPerfTest.wrKey: 329 | test = dt.SsdWriteSatTest(testname,device,options) 330 | #Add the test to the key/value structure 331 | self.addTest(testType, test) 332 | 333 | def toRst(self): 334 | ''' 335 | Generate the rst report file, used to convert other report 336 | formats from. The file is a simple text file as restructured text. 337 | ''' 338 | tests = self.getTests() 339 | rst = self.getRstReport() 340 | 341 | rst.addFooter() 342 | rst.addTitle() 343 | #add the device information and the feature matrix for one device 344 | for keys in tests.keys(): 345 | rst.addDevInfo(tests[keys].getDevice().getDevInfo(),tests[keys].getDevice().getFeatureMatrix()) 346 | break 347 | rst.addCmdLine(self.getCmdLineArgs()) 348 | 349 | #add the fio version, nj, iod and general info of one test to the report 350 | for keys in tests.keys(): 351 | if keys != 'lat': 352 | rst.addSetupInfo(self.getIOPerfVersion(),tests[keys].getFioJob().getFioVersion(), 353 | self.getTestDate()) 354 | rst.addFioJobInfo(tests[keys].getOptions().getNj(), tests[keys].getOptions().getIod()) 355 | rst.addOSInfo(self.getOSInfo()) 356 | rst.addGeneralInfo('ssd') 357 | break 358 | 359 | if SsdPerfTest.iopsKey in tests: 360 | rst.addChapter("IOPS") 361 | rst.addTestInfo('ssd','iops',tests['iops']) 362 | rst.addSection("Measurement Plots") 363 | for i,fig in enumerate(tests['iops'].getFigures()): 364 | rst.addFigure(fig,'ssd','iops',i) 365 | rst.addSection("Measurement Window Summary Table") 366 | rst.addTable(tests['iops'].getTables()[0],tests['iops'].getBsLabels(),'iops') 367 | if SsdPerfTest.tpKey in tests: 368 | rst.addChapter("Throughput") 369 | rst.addTestInfo('ssd','tp',tests['tp']) 370 | rst.addSection("Measurement Plots") 371 | for i,fig in enumerate(tests['tp'].getFigures()): 372 | rst.addFigure(fig,'ssd','tp',i) 373 | rst.addSection("Measurement Window Summary Table") 374 | rst.addTable(tests['tp'].getTables()[0],tests['tp'].getBsLabels(),'tp') 375 | if SsdPerfTest.latKey in tests: 376 | rst.addChapter("Latency") 377 | rst.addTestInfo('ssd','lat',tests['lat']) 378 | rst.addSection("Measurement Plots") 379 | for i,fig in enumerate(tests['lat'].getFigures()): 380 | #index 2 and 3 are 2D measurement plots that are not required 381 | #but we need them to generate the measurement overview table 382 | if i == 2 or i == 3: continue 383 | rst.addFigure(fig,'ssd','lat',i) 384 | rst.addSection("Measurement Window Summary Table") 385 | rst.addTable(tests['lat'].getTables()[0],tests['lat'].getBsLabels(),'avg-lat')#avg lat 386 | rst.addTable(tests['lat'].getTables()[1],tests['lat'].getBsLabels(),'max-lat')#max lat 387 | if SsdPerfTest.wrKey in tests: 388 | rst.addChapter("Write Saturation") 389 | rst.addTestInfo('ssd','writesat',tests['writesat']) 390 | rst.addSection("Measurement Plots") 391 | for i,fig in enumerate(tests['writesat'].getFigures()): 392 | rst.addFigure(fig,'ssd','writesat',i) 393 | 394 | rst.toRstFile() 395 | 396 | class HddPerfTest(PerfTest): 397 | ''' 398 | A performance test for hdds consists of all hdd tests. 399 | ''' 400 | ## Keys valid for tests 401 | iopsKey = 'iops' 402 | tpKey = 'tp' 403 | ## Keys valid for test dictionary and xml file 404 | testKeys = [iopsKey,tpKey] 405 | 406 | def __init__(self,testname,device,options=None): 407 | ''' 408 | Cf. super constructor. 409 | ''' 410 | PerfTest.__init__(self, testname, device) 411 | #Add current date 412 | now = datetime.datetime.now() 413 | self.setTestDate(now.strftime("%Y-%m-%d")) 414 | #Add every test to the performance test 415 | for testType in HddPerfTest.testKeys: 416 | if testType == HddPerfTest.iopsKey: 417 | test = dt.HddIopsTest(testname,device,options) 418 | if testType == HddPerfTest.tpKey: 419 | test = dt.HddTPTest(testname,device,options) 420 | #add the test to the key/value structure 421 | self.addTest(testType, test) 422 | 423 | def toRst(self): 424 | ''' 425 | Generate the rst report file, used to convert other report 426 | formats from. The file is a simple text file as restructured text. 427 | ''' 428 | tests = self.getTests() 429 | rst = self.getRstReport() 430 | rst.addFooter() 431 | rst.addTitle() 432 | #add the device information and the feature matrix for one device 433 | for keys in tests.keys(): 434 | rst.addDevInfo(tests[keys].getDevice().getDevInfo(),tests[keys].getDevice().getFeatureMatrix()) 435 | break 436 | rst.addCmdLine(self.getCmdLineArgs()) 437 | 438 | #Setup and OS infos are the same for all tests, just take one 439 | for keys in tests.keys(): 440 | rst.addSetupInfo(self.getIOPerfVersion(),tests[keys].getFioJob().getFioVersion(), 441 | self.getTestDate()) 442 | rst.addFioJobInfo(tests[keys].getOptions().getNj(), tests[keys].getOptions().getIod()) 443 | rst.addOSInfo(self.getOSInfo()) 444 | rst.addGeneralInfo('hdd') 445 | break 446 | if HddPerfTest.iopsKey in tests: 447 | rst.addChapter("IOPS") 448 | rst.addTestInfo('hdd','iops',tests['iops']) 449 | rst.addSection("Measurement Plots") 450 | for i,fig in enumerate(tests['iops'].getFigures()): 451 | rst.addFigure(fig,'hdd','iops',i) 452 | if HddPerfTest.tpKey in tests: 453 | rst.addChapter("Throughput") 454 | rst.addTestInfo('hdd','tp',tests['tp']) 455 | rst.addSection("Measurement Plots") 456 | for i,fig in enumerate(tests['tp'].getFigures()): 457 | rst.addFigure(fig,'hdd','tp',i) 458 | rst.toRstFile() 459 | -------------------------------------------------------------------------------- /src/perfTest/StdyState.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 26, 2014 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | import logging 8 | import numpy as np 9 | import json 10 | from lxml import etree 11 | 12 | class StdyState(object): 13 | ''' 14 | Used to define a stable state of a device 15 | ''' 16 | ## Max number of carried out test rounds. 17 | testRnds = 25 18 | ## Always use a sliding window of 4 to measure performance values. 19 | testMesWindow = 4 20 | 21 | def __init__(self): 22 | ''' 23 | Constructor 24 | ''' 25 | ## Number of rounds until steady state has been reached 26 | self.__rounds = 0 27 | ## Number of round where steady state has been reached. 28 | self.__stdyRnds = [] 29 | ## Dependent variable to detect steady state. 30 | self.__stdyValues = [] 31 | ##Measured average in measurement window 32 | self.__stdyAvg = 0 33 | ##Slope of steady regression line. 34 | self.__stdySlope = [] 35 | ##States if the steady state has been reached or not 36 | self.__reachStdyState = None 37 | 38 | def getRnds(self): return self.__rounds 39 | def getStdyRnds(self): return self.__stdyRnds 40 | def getStdyAvg(self): return self.__stdyAvg 41 | def getStdyValues(self): return self.__stdyValues 42 | def getStdySlope(self): return self.__stdySlope 43 | 44 | def setReachStdyState(self,s): self.__reachStdyState = s 45 | 46 | def isSteady(self): 47 | ''' 48 | Return if the current state is steady. 49 | @return: True if yes, False if no 50 | @exception RuntimeError if state is not set (None) 51 | ''' 52 | if self.__reachStdyState == None: 53 | raise RuntimeError("steady state is none") 54 | return self.__reachStdyState 55 | 56 | def checkSteadyState(self,xs,ys,rounds): 57 | ''' 58 | Checks if the steady is reached for the given values. 59 | The steady state is defined by the allowed data excursion from the average (+-10%), and 60 | the allowed slope excursion of the linear regression best fit line (+-5%). 61 | @param xs Values on x axis 62 | @param ys Corresponding values for xs on y axis 63 | @param rounds Number of carried out rounds 64 | @return True (k*x+d is slope line) if steady state is reached, False if not 65 | ''' 66 | stdyState = True 67 | maxY = max(ys) 68 | minY = min(ys) 69 | avg = sum(ys)/len(ys)#calc average of values 70 | #allow max excursion of 20% of average 71 | avgRange = avg * 0.20 72 | if (maxY - minY) > avgRange: 73 | stdyState = False 74 | 75 | #do linear regression to calculate slope of linear best fit 76 | y = np.array(ys) 77 | x = np.array(xs) 78 | A = np.vstack([x, np.ones(len(x))]).T 79 | #calculate k*x+d 80 | k, d = np.linalg.lstsq(A, y, rcond=-1)[0] 81 | 82 | #as we have a measurement window of 4, we calculate 83 | #the slope excursion in the window 84 | slopeExc = k * self.testMesWindow 85 | if slopeExc < 0: 86 | slopeExc *= -1 87 | maxSlopeExc = avg * 0.10 #allowed are 10% of avg 88 | if slopeExc > maxSlopeExc: 89 | stdyState = False 90 | 91 | self.__rounds = rounds 92 | self.__stdyRnds = xs 93 | self.__stdyValues = ys 94 | self.__stdyAvg = avg 95 | # Clear previous slope 96 | self.__stdySlope = [] 97 | self.__stdySlope.extend([k,d]) 98 | self.__reachStdyState = stdyState 99 | return stdyState 100 | 101 | def appendXml(self,r): 102 | ''' 103 | Append the information about a steady state test to a XML node. 104 | @param root The xml root tag to append the new elements to 105 | ''' 106 | data = json.dumps(list(self.__stdyRnds)) 107 | e = etree.SubElement(r,'stdyrounds') 108 | e.text = data 109 | 110 | data = json.dumps(list(self.__stdyValues)) 111 | e = etree.SubElement(r,'stdyvalues') 112 | e.text = data 113 | 114 | data = json.dumps(self.__stdySlope) 115 | e = etree.SubElement(r,'stdyslope') 116 | e.text = data 117 | 118 | data = json.dumps(self.__stdyAvg) 119 | e = etree.SubElement(r,'stdyavg') 120 | e.text = data 121 | 122 | data = json.dumps(self.__reachStdyState) 123 | e = etree.SubElement(r,'reachstdystate') 124 | e.text = data 125 | 126 | data = json.dumps(self.__rounds) 127 | e = etree.SubElement(r,'rndnr') 128 | e.text = data 129 | 130 | def fromXml(self,root): 131 | ''' 132 | Loads the information about a steady state from XML. 133 | @param root The given element containing the information about 134 | the object to be initialized. 135 | ''' 136 | self.__stdyRnds = json.loads(root.findtext('stdyrounds')) 137 | self.__stdyValues = json.loads(root.findtext('stdyvalues')) 138 | self.__stdySlope = json.loads(root.findtext('stdyslope')) 139 | self.__stdyAvg = json.loads(root.findtext('stdyavg')) 140 | self.__reachStdyState = json.loads(root.findtext('reachstdystate')) 141 | self.__rounds = json.loads(root.findtext('rndnr')) 142 | logging.info("########### Loading steady state from xml ###########") 143 | self.toLog() 144 | 145 | def toLog(self): 146 | ''' 147 | Log information about the steady state and how it 148 | has been reached. 149 | ''' 150 | logging.info("Rounds of steady state:") 151 | logging.info(self.__stdyRnds) 152 | logging.info("Steady values:") 153 | logging.info(self.__stdyValues) 154 | logging.info("K and d of steady best fit slope:") 155 | logging.info(self.__stdySlope) 156 | logging.info("Steady average:") 157 | logging.info(self.__stdyAvg) 158 | logging.info("Stopped after round number:") 159 | logging.info(self.__rounds) 160 | logging.info("Reached steady state:") 161 | logging.info(self.__reachStdyState) 162 | -------------------------------------------------------------------------------- /src/perfTest/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/plots/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-krenn/TKperf/638b0837b97b32337314993feb52b5111aae0979/src/plots/__init__.py -------------------------------------------------------------------------------- /src/plots/compPlots.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 3, 2013 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | import plots.genPlots as pgp 8 | import matplotlib.pyplot as plt 9 | 10 | __colorTable__ = ['#0000FF','#32cd32','#ffff00','#00ffff','#b22222','#9932cc','#ff4500'] 11 | 12 | def compWriteSatIOPSPlt(testsToPlot, subfolder=None): 13 | """ 14 | Compare multiple tests and create a write saturation IOPS plot. 15 | All test objects in testsToPlot are plotted. 16 | 17 | Keyword arguments: 18 | testsToPlot -- an array of perfTest objects 19 | """ 20 | plt.clf()#clear plot 21 | min_y = 0 22 | max_y = 0 23 | max_x = 0 24 | for i,tests in enumerate(testsToPlot): 25 | test = tests.getTests()['writesat'] 26 | rnds = test.getRnds() 27 | x = list(range(rnds + 1)) 28 | #first elem in matrix are iops 29 | iops_l = test.getRndMatrices()[0] 30 | plt.plot(x,iops_l,'-',label=test.getTestname(), color = __colorTable__[i]) 31 | #fetch new min and max from current test values 32 | min_y,max_y = pgp.getMinMax(iops_l, min_y, max_y) 33 | if max(x) > max_x: 34 | max_x = max(x) 35 | 36 | plt.ylim(min_y * 0.75, max_y * 1.25) 37 | #every 50 rounds print the round number 38 | x = list(range(0, max_x, 50)) 39 | plt.xticks(x) 40 | plt.suptitle("Write Saturation Test",fontweight='bold') 41 | plt.xlabel("Round #") 42 | plt.ylabel("Avg. IOPS") 43 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 44 | ncol=3, fancybox=True, shadow=True,prop={'size':10}) 45 | if subfolder == None: 46 | plt.savefig('compWriteSatIOPSPlt.png',dpi=300) 47 | else: 48 | plt.savefig(subfolder+'/compWriteSatIOPSPlt.png',dpi=300) 49 | 50 | def compILPlt(testsToPlot, mode, subfolder=None): 51 | """ 52 | Compare multiple tests and create an IOPS or Latency plot. 53 | All test objects in testsToPlot are plotted. 54 | 55 | Keyword arguments: 56 | testsToPlot -- an array of perfTest objects 57 | mode -- the desired test mode (IOPS or Latency) 58 | """ 59 | plt.clf()#clear plot 60 | x = list(range(3)) 61 | width = 1/len(testsToPlot) 62 | max_y = 0 63 | for i in range(len(x)): 64 | x[i] = x[i] + (i * width) 65 | for i,tests in enumerate(testsToPlot): 66 | if mode == "IOPS": 67 | test = tests.getTests()['iops'] 68 | pgp.calcMsmtTable(test, 'IOPS') 69 | if mode == "LAT": 70 | test = tests.getTests()['lat'] 71 | pgp.calcMsmtTable(test, 'avg-LAT') 72 | mixWLds = test.getTables()[0] 73 | if mode == "IOPS": 74 | testVal = [mixWLds[0][6],mixWLds[3][6],mixWLds[6][6]] 75 | if mode == "LAT": 76 | testVal = [mixWLds[0][1],mixWLds[1][1],mixWLds[2][1]] 77 | plt.bar(x, testVal, width,label=test.getTestname(),color = __colorTable__[i]) 78 | x = [v + width for v in x] 79 | if max(testVal) > max_y: 80 | max_y = max(testVal) 81 | ticksx = [(len(testsToPlot)/2) * width, 1 + width + 0.5, 2 + (2 * width) + 0.5 ] 82 | if mode == "IOPS": 83 | labelsx = ['Read','50/50','Write'] 84 | if mode == "LAT": 85 | labelsx = ['Read','65/35','Write'] 86 | plt.xticks(ticksx, labelsx) 87 | plt.ylim(0, max_y * 1.15) 88 | if mode == "IOPS": 89 | title = "IOPS" 90 | plt.ylabel("Avg. " + title + " at 4k Block Size") 91 | if mode == "LAT": 92 | title = "LAT" 93 | plt.ylabel("Avg. "+ title + " (ms) at 4k Block Size") 94 | plt.suptitle(title + " Measurement Test",fontweight='bold') 95 | plt.xlabel("R/W Workload") 96 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 97 | ncol=3,fancybox=True, shadow=True,prop={'size':10}) 98 | if subfolder == None: 99 | plt.savefig('comp'+title+'Plt.png',dpi=300) 100 | else: 101 | plt.savefig(subfolder+'/comp'+title+'Plt.png',dpi=300) 102 | 103 | def compTPPlt(testsToPlot, subfolder=None): 104 | """ 105 | Compare multiple tests and create a throughput plot. 106 | All test objects in testsToPlot are plotted. 107 | 108 | Keyword arguments: 109 | testsToPlot -- an array of perfTest objects 110 | """ 111 | plt.clf()#clear plot 112 | height = 1/len(testsToPlot) 113 | ticksy = [(len(testsToPlot)/2) * height, 1 + height + 0.5, 2 + (2 * height) + 0.5 ] 114 | labelsy = ['8k','64k','1024k'] 115 | max_x = 0 116 | fig = plt.figure() 117 | # Plot read throughput 118 | ax = fig.add_subplot(2, 1, 2) 119 | y = list(range(3)) 120 | for i in range(len(y)): 121 | y[i] = y[i] + (i * height) 122 | for i,tests in enumerate(testsToPlot): 123 | test = tests.getTests()['tp'] 124 | pgp.calcMsmtTPTable(test) 125 | wlds = test.getTables()[0] 126 | testRTP = [wlds[0][2],wlds[0][1],wlds[0][0]] 127 | ax.barh(y, testRTP, height, label=test.getTestname(),color = __colorTable__[i]) 128 | y = [v + height for v in y] 129 | if max(testRTP) > max_x: 130 | max_x = max(testRTP) 131 | plt.xlabel("Read Bandwidth (MB/s)") 132 | plt.xlim(0,max_x*1.05) 133 | plt.yticks(ticksy, labelsy) 134 | plt.ylabel("Block Size (Byte)") 135 | plt.grid() 136 | 137 | # Plot write throughput 138 | ax = fig.add_subplot(2, 1, 1) 139 | y = list(range(3)) 140 | for i in range(len(y)): 141 | y[i] = y[i] + (i * height) 142 | for i,tests in enumerate(testsToPlot): 143 | test = tests.getTests()['tp'] 144 | wlds = test.getTables()[0] 145 | testRTP = [wlds[1][2],wlds[1][1],wlds[1][0]] 146 | ax.barh(y, testRTP, height, label=test.getTestname(),color = __colorTable__[i]) 147 | y = [v + height for v in y] 148 | plt.xlabel("Write Bandwidth (MB/s)") 149 | plt.xlim(0,max_x*1.05) 150 | plt.yticks(ticksy, labelsy) 151 | plt.ylabel("Block Size (Byte)") 152 | plt.grid() 153 | plt.legend() 154 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.18), 155 | ncol=3,fancybox=True, shadow=True,prop={'size':9}) 156 | plt.suptitle("TP R/W Measurement Test",fontweight='bold') 157 | if subfolder == None: 158 | plt.savefig('compTPPlt.png',dpi=300) 159 | else: 160 | plt.savefig(subfolder+'/compTPPlt.png',dpi=300) 161 | -------------------------------------------------------------------------------- /src/plots/genPlots.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 09.07.2012 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | import matplotlib 8 | matplotlib.use('Agg') 9 | import matplotlib.pyplot as plt 10 | import matplotlib.ticker as ticker 11 | import matplotlib.colors as pltm 12 | from mpl_toolkits.mplot3d import Axes3D 13 | 14 | import numpy as np 15 | from copy import deepcopy 16 | 17 | import perfTest.DeviceTests as dt 18 | 19 | __matplotVersion__=float('.'.join(matplotlib.__version__.split('.')[0:2])) 20 | 21 | def stdyStVerPlt(toPlot,mode): 22 | ''' 23 | Generate a steady state verification plot. 24 | The plot includes: 25 | -Measured IOPS|Latencies|Throughput of rounds in which steady state was reached 26 | -Average IOPS|Latencies|Throughput in steady state rounds 27 | -Slope of best fit line 28 | -Top and Bottom limits: +-10% percent of average 29 | The figure is saved as SsdTest.Testname-stdyStVerPlt.png. 30 | @param toPlot A SsdTest object. 31 | @param mode A string representing the test mode (IOPS|LAT|TP) 32 | ''' 33 | x = np.array(toPlot.getStdyState().getStdyRnds()) 34 | #calculate average and its top and bottom limit 35 | av = [] 36 | avT = [] 37 | avB = [] 38 | av.append(toPlot.getStdyState().getStdyAvg()) 39 | avTop = toPlot.getStdyState().getStdyAvg() * 1.10 40 | avBot = toPlot.getStdyState().getStdyAvg() * 0.9 41 | avT.append(avTop) 42 | avB.append(avBot) 43 | av = av * len(x) 44 | avT = avT * len(x) 45 | avB = avB * len(x) 46 | 47 | plt.clf()#clear 48 | plt.plot(x,toPlot.getStdyState().getStdyValues(),'o', label=mode, markersize=10) 49 | plt.plot(x, toPlot.getStdyState().getStdySlope()[0]*x + toPlot.getStdyState().getStdySlope()[1], 'r', label='Slope') 50 | plt.plot(x, av, '-', color='black',label='Average') 51 | plt.plot(x, avT, '--', color='black',label='Top') 52 | plt.plot(x, avB, '--', color='black',label='Bottom') 53 | 54 | #set the y axes to start at 3/4 of mininum 55 | plt.ylim(min(toPlot.getStdyState().getStdyValues())*0.75,max(toPlot.getStdyState().getStdyValues())*1.25) 56 | plt.xticks(x) 57 | title = mode + " Steady State Verification Plot" 58 | plt.suptitle(title,fontweight='bold') 59 | plt.xlabel("Round #") 60 | if mode == "LAT": 61 | plt.ylabel("Latency (us)") 62 | if mode == "TP": 63 | plt.ylabel("Bandwidth (KB/s)") 64 | if mode == "IOPS": 65 | plt.ylabel(mode) 66 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 67 | ncol=3, fancybox=True, shadow=True,prop={'size':12}) 68 | plt.savefig(toPlot.getTestname()+'-'+mode+'-stdyStVerPlt.png',dpi=300) 69 | toPlot.addFigure(toPlot.getTestname()+'-'+mode+'-stdyStVerPlt.png') 70 | 71 | def stdyStConvPlt(toPlot,mode): 72 | ''' 73 | Generate a steady state convergence plot. 74 | The plot consists of: 75 | IOPS: 76 | -Measured IOPS of pure random write 77 | LAT: 78 | -Avg latency of read, mixed, write 79 | -All lines are the different block sizes 80 | -IOPS/Latencies of all the rounds are plotted 81 | The figure is saved as SsdTest.Testname-stdyStConvPlt.png. 82 | @param toPlot A SsdTest object. 83 | @param mode A string representing the test mode (IOPS|LAT) 84 | ''' 85 | rnds = toPlot.getStdyState().getRnds() 86 | matrices = toPlot.getRndMatrices() 87 | bsLens = len(matrices[0][-1])#fetch the number of bs of the first matrix 88 | 89 | #initialize matrix for plotting 90 | if mode == "IOPS": 91 | lines = [] 92 | for i in range(bsLens): 93 | lines.append([]) 94 | for rndMat in matrices: 95 | row = rndMat[-1]#last row is random write 96 | for i in range(len(row)): 97 | lines[i].append(row[i])#switch from row to column wise ordering of values 98 | 99 | if mode == "LAT": 100 | readLines = [] 101 | writeLines = [] 102 | mixLines = [] 103 | for i in range(bsLens): 104 | readLines.append([]) 105 | writeLines.append([]) 106 | mixLines.append([]) 107 | for rndMat in matrices: 108 | #take read mat len, every elem has the same len 109 | for i in range(len(rndMat[0])): 110 | #also convert it from us to ms 111 | readLines[i].append((rndMat[0][i][2]) / 1000)#mean latency 112 | mixLines[i].append((rndMat[1][i][2]) / 1000)#mean latency 113 | writeLines[i].append((rndMat[2][i][2]) / 1000)#mean latency 114 | 115 | plt.clf()#clear 116 | 117 | #fetch number of rounds, we want to include all rounds 118 | x = list(range(rnds + 1)) 119 | max_y = 0 120 | min_y = 0 121 | if mode == "IOPS": 122 | for i in range(len(lines)): 123 | min_y,max_y = getMinMax(lines[i], min_y, max_y) 124 | plt.plot(x,lines[i],'o-',label='bs='+toPlot.getBsLabels()[i]) 125 | if mode == "LAT": 126 | for i in range(len(readLines)): 127 | min_y,max_y = getMinMax(readLines[i], min_y, max_y) 128 | plt.plot(x,readLines[i],'s-',label='bs='+toPlot.getBsLabels()[i]+' read') 129 | for i in range(len(mixLines)): 130 | min_y,max_y = getMinMax(mixLines[i], min_y, max_y) 131 | plt.plot(x,mixLines[i],'^-',label='bs='+toPlot.getBsLabels()[i]+' mixed') 132 | for i in range(len(writeLines)): 133 | min_y,max_y = getMinMax(writeLines[i], min_y, max_y) 134 | plt.plot(x,writeLines[i],'o-',label='bs='+toPlot.getBsLabels()[i]+' write') 135 | 136 | plt.xticks(x) 137 | plt.suptitle(mode+" Steady State Convergence Plot",fontweight='bold') 138 | plt.xlabel("Round #") 139 | plt.ylim((min_y*0.75,max_y*1.25)) 140 | if mode == "LAT": 141 | plt.ylabel("Latency (ms)") 142 | if mode == "IOPS": 143 | plt.ylabel(mode) 144 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 145 | ncol=3, fancybox=True, shadow=True,prop={'size':12}) 146 | plt.savefig(toPlot.getTestname()+'-'+mode+'-stdyStConvPlt.png',dpi=300) 147 | toPlot.addFigure(toPlot.getTestname()+'-'+mode+'-stdyStConvPlt.png') 148 | 149 | def mes2DPlt(toPlot,mode): 150 | ''' 151 | Generate a measurement 2D plot and the measurement overview table. 152 | The plot includes: 153 | -Lines of the workloads 154 | -Each line consists of the average of IOPS/Latencies per round 155 | for each block size in the measurement window! 156 | Therefore the x axes are the block sizes, the plotted lines 157 | are the different workloads (from 100% read to 100% write). The 158 | y axes are the IOPS/Latencies over the measurement window for each block sizes 159 | and workload. 160 | The figure is saved as SsdTest.Testname-mode-mes2DPlt.png. 161 | @param toPlot A SsdTest object. 162 | @param mode A string representing the test mode (IOPS|max-LAT|avg-LAT) 163 | ''' 164 | # Generate the measurement table 165 | calcMsmtTable(toPlot, mode) 166 | if mode == "IOPS": 167 | wlds = dt.SsdIopsTest.mixWlds 168 | bsLabels = toPlot.getBsLabels() 169 | mixWLds = toPlot.getTables()[0] 170 | if mode == "avg-LAT" or mode == "max-LAT": 171 | wlds = dt.SsdLatencyTest.mixWlds 172 | bsLabels = toPlot.getBsLabels() 173 | if mode == "avg-LAT": 174 | mixWLds = toPlot.getTables()[0] 175 | if mode == "max-LAT": 176 | mixWLds = toPlot.getTables()[1] 177 | 178 | plt.clf()#clear plot 179 | if mode == "IOPS": 180 | x = getBS(toPlot.getBsLabels()) 181 | if mode == "avg-LAT" or mode == "max-LAT": 182 | x = getBS(toPlot.getBsLabels()) 183 | 184 | max_y = 0 185 | min_y = 0 186 | for i in range(len(mixWLds)): 187 | min_y,max_y = getMinMax(mixWLds[i], min_y, max_y) 188 | #the labels are r/w percentage of mixed workload 189 | plt.plot(x,mixWLds[i],'o-', 190 | label=str(wlds[i])+'/'+str(100-wlds[i])) 191 | if mode == 'IOPS': 192 | plt.yscale('log') 193 | plt.xscale('log') 194 | plt.ylabel(mode) 195 | plt.legend(prop={'size':12}) 196 | if mode == "avg-LAT" or mode == "max-LAT": 197 | plt.ylabel("Latency (ms)") 198 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 199 | ncol=3, fancybox=True, shadow=True,prop={'size':12}) 200 | 201 | plt.xlabel("Block Size (Byte)") 202 | #scale axis to min and max 203 | plt.ylim((min_y*0.75,max_y*1.15)) 204 | plt.xticks(x,bsLabels) 205 | plt.suptitle(mode+" Measurement Plot",fontweight='bold') 206 | plt.savefig(toPlot.getTestname()+'-'+mode+'-mes2DPlt.png',dpi=300) 207 | toPlot.addFigure(toPlot.getTestname()+'-'+mode+'-mes2DPlt.png') 208 | 209 | def mes3DPlt(toPlot,mode): 210 | ''' 211 | Generate a measurement 3D plot. This plot depends on the 212 | mes2DPlt as there the measurement overview table is calculated. 213 | @param toPlot A SsdTest object. 214 | @param mode A string representing the test mode (IOPS) 215 | ''' 216 | colorTable = ['#0000FF','#008080','#00FFFF','#FFFF00','#00FF00','#FF00FF','#800000'] 217 | if mode == 'IOPS': 218 | #Iops have only one measurement table 219 | matrix = deepcopy(toPlot.getTables()[0]) 220 | #reverse to start with 0/100 221 | matrix.reverse() 222 | #reverse the block size in each table row, to start with 512B 223 | for row in matrix: 224 | row.reverse() 225 | bsLabels = list(toPlot.getBsLabels()) 226 | mixWlds = list(dt.SsdIopsTest.mixWlds) 227 | 228 | #define positions for bars 229 | ypos = np.array([0.25] * len(bsLabels)) 230 | xpos = np.arange(0.25, len(bsLabels)+0.25, 1) 231 | zpos = np.array([0] * len(bsLabels)) 232 | 233 | #define widht and height (x,y) of bars 234 | # z will be the measured values 235 | dx = np.array([0.5] * len(bsLabels)) 236 | dy = np.array([0.5] * len(bsLabels)) 237 | 238 | plt.clf 239 | fig = plt.figure() 240 | if __matplotVersion__ >= 1.0: 241 | ax = fig.gca(projection='3d') 242 | else: 243 | ax = Axes3D(fig) 244 | for j,wl in enumerate(matrix): 245 | ax.bar3d(xpos,ypos,zpos, dx, dy, wl, color=pltm.colorConverter.to_rgba_array(colorTable[j])) 246 | for pos in range(len(ypos)): 247 | ypos[pos] += 1 248 | 249 | ticksx = np.arange(0.5, len(bsLabels), 1) 250 | bsLabels.reverse() 251 | ticksy = np.arange(0.5, len(mixWlds), 1) 252 | mixWlds.reverse() 253 | if __matplotVersion__ >= 1.0: 254 | plt.yticks(ticksy,mixWlds) 255 | plt.xticks(ticksx, bsLabels) 256 | else: 257 | ax.w_xaxis.set_major_locator(ticker.FixedLocator(ticksx)) 258 | ax.w_xaxis.set_ticklabels(bsLabels) 259 | ax.w_yaxis.set_major_locator(ticker.FixedLocator(ticksy)) 260 | ax.w_yaxis.set_ticklabels(mixWlds) 261 | 262 | plt.suptitle(mode+" 3D Measurement Plot",fontweight='bold') 263 | ax.set_xlabel('Block Size (Byte)') 264 | ax.set_ylabel('R/W Mix%') 265 | if mode == 'IOPS': 266 | ax.set_zlabel('IOPS',rotation='vertical') 267 | plt.savefig(toPlot.getTestname()+'-'+mode+'-mes3DPlt.png',dpi=300) 268 | toPlot.addFigure(toPlot.getTestname()+'-'+mode+'-mes3DPlt.png') 269 | 270 | def latMes3DPlt(toPlot): 271 | ''' 272 | Generate a measurement 3D plot for latency. This plot depends on the 273 | mes2DPlt as there the measurement overview table is calculated. 274 | @param toPlot A SsdTest object. 275 | ''' 276 | colorTable = ['#0000FF','#008080','#00FFFF'] 277 | mixWlds = list(dt.SsdLatencyTest.mixWlds) 278 | bsLabels = list(toPlot.getBsLabels()) 279 | 280 | avgMatrix = deepcopy(toPlot.getTables()[0]) 281 | maxMatrix = deepcopy(toPlot.getTables()[1]) 282 | 283 | #define positions for bars 284 | ypos = np.array([0.25] * len(bsLabels)) 285 | xpos = np.arange(0.25, len(bsLabels)+0.25, 1) 286 | zpos = np.array([0] * len(bsLabels)) 287 | 288 | #define widht and height (x,y) of bars 289 | # z will be the measured values 290 | dx = np.array([0.5] * len(bsLabels)) 291 | dy = np.array([0.5] * len(bsLabels)) 292 | 293 | plt.clf() 294 | fig = plt.figure() 295 | if __matplotVersion__ >= 1.0: 296 | ax = fig.add_subplot(2, 1, 1, projection='3d') 297 | else: 298 | rect = fig.add_subplot(2, 1, 1).get_position() 299 | ax = Axes3D(fig, rect) 300 | for j,wl in enumerate(avgMatrix): 301 | ax.bar3d(xpos,ypos,zpos, dx, dy, wl, color=pltm.colorConverter.to_rgba_array(colorTable[j])) 302 | for pos in range(len(ypos)): 303 | ypos[pos] += 1 304 | ax.xaxis.set_ticks([]) 305 | ax.yaxis.set_ticks([]) 306 | ax.set_zlabel('Latency (ms)',rotation='vertical') 307 | 308 | #Second subplot 309 | if __matplotVersion__ >= 1.0: 310 | ax = fig.add_subplot(2,1,2, projection='3d') 311 | else: 312 | rect = fig.add_subplot(2, 1, 2).get_position() 313 | ax = Axes3D(fig, rect) 314 | #reset ypos 315 | ypos = np.array([0.25] * len(bsLabels)) 316 | for j,wl in enumerate(maxMatrix): 317 | ax.bar3d(xpos,ypos,zpos, dx, dy, wl, color=pltm.colorConverter.to_rgba_array(colorTable[j])) 318 | for pos in range(len(ypos)): 319 | ypos[pos] += 1 320 | 321 | ticksx = np.arange(0.5, len(bsLabels), 1) 322 | ticksy = np.arange(0.5, len(mixWlds), 1) 323 | if __matplotVersion__ >= 1.0: 324 | plt.xticks(ticksx, bsLabels) 325 | plt.yticks(ticksy,mixWlds) 326 | else: 327 | ax.w_xaxis.set_major_locator(ticker.FixedLocator(ticksx)) 328 | ax.w_xaxis.set_ticklabels(bsLabels) 329 | ax.w_yaxis.set_major_locator(ticker.FixedLocator(ticksy)) 330 | ax.w_yaxis.set_ticklabels(mixWlds) 331 | 332 | plt.suptitle("LAT 3D Measurement Plot",fontweight='bold') 333 | #ax.set_xlabel('Block Size (Byte)') 334 | ax.set_ylabel('R/W Mix%') 335 | ax.set_zlabel('Latency (ms)',rotation='vertical') 336 | plt.savefig(toPlot.getTestname()+'-LAT-mes3DPlt.png',dpi=300) 337 | toPlot.addFigure(toPlot.getTestname()+'-LAT-mes3DPlt.png') 338 | 339 | def writeSatIOPSPlt(toPlot): 340 | #fetch number of rounds, we want to include all rounds 341 | #as stdy state was reached at rnds, it must be included 342 | rnds = toPlot.getRnds() 343 | x = list(range(rnds + 1)) 344 | 345 | iops_l = toPlot.getRndMatrices()[0]#first elem in matrix are iops 346 | 347 | plt.clf()#clear plot 348 | plt.plot(x,iops_l,'-',label='Avg IOPS') 349 | plt.ylim(min(iops_l)*0.75,max(iops_l)*1.25) 350 | #every 10 rounds print the round number 351 | x = list(range(0,rnds + 1,50)) 352 | plt.xticks(x) 353 | plt.suptitle("Write Saturation Test",fontweight='bold') 354 | plt.xlabel("Round #") 355 | plt.ylabel("IOPS") 356 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 357 | ncol=1, fancybox=True, shadow=True,prop={'size':12}) 358 | plt.savefig(toPlot.getTestname()+'-writeSatIOPSPlt.png',dpi=300) 359 | toPlot.addFigure(toPlot.getTestname()+'-writeSatIOPSPlt.png') 360 | 361 | def writeSatLatPlt(toPlot): 362 | rnds = toPlot.getRnds() 363 | x = list(range(rnds + 1)) 364 | 365 | lats_l = toPlot.getRndMatrices()[1]#second elem in matrix are latencies 366 | 367 | #get the average latencies from the lat list (last elem) 368 | av_lats = [] 369 | for i in lats_l: 370 | #convert from us to ms 371 | av_lats.append((i[2]) / 1000) 372 | 373 | plt.clf()#clear plot 374 | plt.plot(x,av_lats,'-',label='Avg latency') 375 | #set the y axes to start at 3/4 of mininum 376 | plt.ylim(min(av_lats)*0.75,max(av_lats)*1.25) 377 | #every 50 rounds print the round number 378 | x = list(range(0,rnds + 1,50)) 379 | plt.xticks(x) 380 | plt.suptitle("Write Saturation Test",fontweight='bold') 381 | plt.xlabel("Round #") 382 | plt.ylabel("Latency (ms)") 383 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 384 | ncol=1, fancybox=True, shadow=True,prop={'size':12}) 385 | plt.savefig(toPlot.getTestname()+'-writeSatLatPlt.png',dpi=300) 386 | toPlot.addFigure(toPlot.getTestname()+'-writeSatLatPlt.png') 387 | 388 | def tpRWStdyStConvPlt(toPlot): 389 | ''' 390 | Generate one steady state convergence plot for throughput read and write measurements. 391 | The plot consists of: 392 | -Measured bandwidths per round per block size 393 | -All lines are the different block sizes 394 | -x axes is the number of all rounds 395 | -y axes is the bw of the corresponding round 396 | The top plot is write, below read 397 | The figure is saved as SsdTest.Testname-TP-RW-stdyStConvPlt.png. 398 | @param toPlot A SsdTest object. 399 | ''' 400 | matrices = deepcopy(toPlot.getRndMatrices()) 401 | rnds = toPlot.getStdyState().getRnds()#fetch the number of total rounds 402 | bsLens = len(matrices)#fetch the number of bs, each row is a bs in the matrix 403 | bsLabels = toPlot.getBsLabels() 404 | 405 | #initialize matrix for plotting 406 | lines = [] 407 | for i in range(bsLens): 408 | lines.append([]) 409 | 410 | #values for scaling the axes 411 | max_y = 0 412 | min_y = 0 413 | 414 | plt.clf()#clear 415 | x = list(range(rnds+1))#ensure to include all rounds 416 | 417 | plt.clf 418 | fig = plt.figure() 419 | ax = fig.add_subplot(2, 1, 1) 420 | for i,rndMat in enumerate(matrices): 421 | row = rndMat[1]#plot the write row 422 | #convert to MB/s 423 | for v in range(len(row)): 424 | row[v] = row[v] / 1024 425 | #calc min,man to scale axes 426 | min_y,max_y = getMinMax(row, min_y, max_y) 427 | ax.plot(x,row,'o-',label='bs='+bsLabels[i]) 428 | plt.xticks(x) 429 | plt.ylim((0,max_y*1.15)) 430 | plt.legend() 431 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.10), 432 | ncol=5, fancybox=True, shadow=True,prop={'size':11}) 433 | plt.ylabel("Write TP (MB/s)") 434 | 435 | ax = fig.add_subplot(2, 1, 2) 436 | for i,rndMat in enumerate(matrices): 437 | row = rndMat[0]#plot the read row 438 | #convert to MB/s 439 | for v in range(len(row)): 440 | row[v] = row[v] / 1024 441 | #calc min,man to scale axes 442 | min_y,max_y = getMinMax(row, min_y, max_y) 443 | ax.plot(x,row,'o-',label='bs='+bsLabels[i]) 444 | plt.xticks(x) 445 | plt.ylim((0,max_y*1.15)) 446 | plt.ylabel("Read TP (MB/s)") 447 | 448 | plt.xlabel("Round #") 449 | plt.suptitle("TP R/W Steady State Convergence Plot",fontweight='bold') 450 | 451 | plt.savefig(toPlot.getTestname()+'-TP-RW-stdyStConvPlt.png',dpi=300) 452 | toPlot.addFigure(toPlot.getTestname()+'-TP-RW-stdyStConvPlt.png') 453 | 454 | def tpMes2DPlt(toPlot): 455 | ''' 456 | Generate a measurement 2D plot and the measurement overview table for throughput. 457 | The plot includes: 458 | -One line for read, one line for write 459 | -Each line consists of the average of IOPS per round 460 | for each block size in the measurement window! 461 | Therefore the x axes are the block sizes, the y axes is 462 | the average over the measurement window for each block size. 463 | The figure is saved as SsdTest.Testname-bw-mes2DPlt.png. 464 | @param toPlot A SsdTest object. 465 | ''' 466 | calcMsmtTPTable(toPlot) 467 | wlds = toPlot.getTables()[0] 468 | #start plotting 469 | plt.clf()#clear 470 | x = getBS(toPlot.getBsLabels()) 471 | for i in range(len(wlds)): 472 | if i == 0: 473 | label = "read" 474 | else: 475 | label = "write" 476 | plt.plot(x,wlds[i],'o-',label=label) 477 | 478 | plt.xscale('log') 479 | plt.suptitle("TP Measurement Plot",fontweight='bold') 480 | plt.xlabel("Block Size (Byte)") 481 | plt.ylabel("Bandwidth (MB/s)") 482 | plt.xticks(x,toPlot.getBsLabels()) 483 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 484 | ncol=3, fancybox=True, shadow=True,prop={'size':12}) 485 | plt.savefig(toPlot.getTestname()+'-TP-mes2DPlt.png',dpi=300) 486 | toPlot.addFigure(toPlot.getTestname()+'-TP-mes2DPlt.png') 487 | 488 | ######### PLOTS FOR HDD TESTS ######### 489 | def TPplot(toPlot): 490 | ''' 491 | Generate a R/W throughput measurement plot for the hdd round results. 492 | The plot consists of: 493 | -Measured bandwidths per round, per block size for read and write 494 | -x axes is the number of carried out rounds 495 | -y axes is the bandwidth of the corresponding round 496 | The figure is saved as TPTest.Testname-TP-RW-Plt.png. 497 | @param toPlot A hdd TPTest object. 498 | ''' 499 | #As the values are converted to KB, copy the matrices 500 | matrices = deepcopy(toPlot.getRndMatrices()) 501 | rnds = dt.HddTPTest.maxRnds 502 | bsLabels = toPlot.getBsLabels() 503 | 504 | #values for scaling the axes 505 | max_y = 0 506 | min_y = 0 507 | 508 | plt.clf()#clear 509 | x = list(range(rnds)) 510 | for i,rndMat in enumerate(matrices): 511 | #convert to MB/S 512 | for v in range(len(rndMat[0])): 513 | rndMat[0][v] = (rndMat[0][v]) / 1024 514 | #plot the read row for current BS 515 | min_y,max_y = getMinMax(rndMat[0], min_y, max_y) 516 | plt.plot(x,rndMat[0],'o-',label='read bs='+bsLabels[i]) 517 | #plot the write row for current BS 518 | for v in range(len(rndMat[1])): 519 | rndMat[1][v] = (rndMat[1][v]) / 1024 520 | min_y,max_y = getMinMax(rndMat[1], min_y, max_y) 521 | plt.plot(x,rndMat[1],'o-',label='write bs='+bsLabels[i]) 522 | 523 | x = list(range(0,rnds+1,16)) 524 | plt.xticks(x) 525 | plt.suptitle("TP Measurement Plot",fontweight='bold') 526 | plt.xlabel("Area of Device (in rounds)") 527 | plt.ylabel("Bandwidth (MB/s)") 528 | #scale axis to min and max +- 15% 529 | plt.ylim((min_y*0.6,max_y*1.15)) 530 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 531 | ncol=2, fancybox=True, shadow=True,prop={'size':12}) 532 | plt.savefig(toPlot.getTestname()+'-TP-RW-Plt.png',dpi=300) 533 | toPlot.addFigure(toPlot.getTestname()+'-TP-RW-Plt.png') 534 | 535 | def IOPSplot(toPlot): 536 | ''' 537 | Generate the IOPS plot for a hdd performance test. The plot consists 538 | of plotting the IOPS results from the 128 rounds that have been carried 539 | out. In each round the mixed workloads and all block sizes are plotted. 540 | @param toPlot An hdd IopsTest object. 541 | ''' 542 | rnds = dt.HddIopsTest.maxRnds 543 | matrices = toPlot.getRndMatrices() 544 | 545 | wlds = dt.HddIopsTest.mixWlds 546 | bsLabels = toPlot.getBsLabels() 547 | 548 | #each row will be a workload percentage 549 | mixWLds = [] 550 | for i in range(len(wlds)): 551 | mixWLds.append([]) 552 | #in each row will be the different block sizes 553 | for bs in range(len(bsLabels)): 554 | mixWLds[i].append([]) 555 | 556 | for rnd in matrices: 557 | #each round has its workloads 558 | for i,row in enumerate(rnd): 559 | #each workload has its block sizes 560 | for j,bs in enumerate(row): 561 | mixWLds[i][j].append(bs) 562 | 563 | plt.clf()#clear 564 | x = list(range(rnds)) 565 | max_y = 0 566 | min_y = 0 567 | for i in range(len(mixWLds)): 568 | if i == 0: 569 | lc = 'blue' 570 | if i == 1: 571 | lc = 'green' 572 | if i == 2: 573 | lc = 'red' 574 | for j in range(len(mixWLds[i])): 575 | if j == 0: 576 | ls = 's-' 577 | if j == 1: 578 | ls = 'o-' 579 | if j == 2: 580 | ls = '^-' 581 | min_y,max_y = getMinMax(mixWLds[i][j], min_y, max_y) 582 | plt.plot(x,mixWLds[i][j],ls,color=lc, 583 | label=str(wlds[i])+'/bs=' + bsLabels[j]) 584 | x = list(range(0,rnds + 1,16)) 585 | plt.xticks(x) 586 | plt.suptitle("IOPS Measurement Plot",fontweight='bold') 587 | plt.xlabel("Area of Device (in rounds)") 588 | plt.ylabel("IOPS") 589 | #plt.yscale('log') 590 | plt.ylim((min_y*0.75,max_y*1.25)) 591 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.07), 592 | ncol=3, fancybox=True, shadow=True,prop={'size':12}) 593 | plt.savefig(toPlot.getTestname()+'-IOPSPlt.png',dpi=300) 594 | toPlot.addFigure(toPlot.getTestname()+'-IOPSPlt.png') 595 | 596 | def TPBoxPlot(toPlot): 597 | ''' 598 | Generate a R/W throughput box plot for the hdd round results. 599 | The plot consists of: 600 | -Measured bandwidths per round, per block size for read and write 601 | -x axes is the number of carried out rounds 602 | -y axes is the bandwidth of the corresponding round 603 | The figure is saved as TPTest.Testname-TP-RW-Plt.png. 604 | @param toPlot A hdd TPTest object. 605 | ''' 606 | #As the values are converted to KB, copy the matrices 607 | matrices = deepcopy(toPlot.getRndMatrices()) 608 | bsLabels = toPlot.getBsLabels() 609 | 610 | plt.clf()#clear 611 | boxes = [] 612 | min_y = 0 613 | max_y = 0 614 | for bsRows in matrices: 615 | #For each BS we have read and write, both rows have equal length 616 | for v in range(len(bsRows[0])): 617 | bsRows[0][v] = (bsRows[0][v]) / 1024 618 | bsRows[1][v] = (bsRows[1][v]) / 1024 619 | boxes.append(bsRows[0]) 620 | min_y,max_y = getMinMax(bsRows[0], min_y, max_y) 621 | boxes.append(bsRows[1]) 622 | min_y,max_y = getMinMax(bsRows[1], min_y, max_y) 623 | #Length of BS per R/W 624 | pos = list(range(len(bsLabels) * 2)) 625 | plt.boxplot(boxes,positions=pos) 626 | labels = [] 627 | for l in bsLabels: 628 | labels.append(l + ' (R)') 629 | labels.append(l + ' (W)') 630 | plt.xticks(pos,labels) 631 | plt.xlabel('BS (Mode)') 632 | plt.suptitle("TP Boxplot",fontweight='bold') 633 | plt.ylabel("Bandwidth (MB/s)") 634 | #scale axis to min and max +- 15% 635 | plt.ylim((min_y*0.7,max_y*1.10)) 636 | #Draw some fake data for legend 637 | hB, = plt.plot([1,1],'b-') 638 | hR, = plt.plot([1,1],'r-') 639 | plt.legend((hB, hR),('Quartiles', 'Median'),loc='upper center', bbox_to_anchor=(0.5, 1.07), 640 | ncol=2, fancybox=True, shadow=True,prop={'size':12}) 641 | hB.set_visible(False) 642 | hR.set_visible(False) 643 | plt.savefig(toPlot.getTestname()+'-TP-Boxplt.png',dpi=300) 644 | toPlot.addFigure(toPlot.getTestname()+'-TP-Boxplt.png') 645 | 646 | ######### HELPER FUNCTIONS TO GENERATE PLOTS ######### 647 | def calcMsmtTable(toPlot,mode): 648 | ''' 649 | Generate the measurement overview table for IOPS and Latency. The table is 650 | an overview over the average values in the measurement window. For latency 651 | the values are converted from us to ms also. 652 | @param toPlot A SsdTest object. 653 | @param mode A string representing the test mode (IOPS|max-LAT|avg-LAT) 654 | ''' 655 | mixWLds = [] 656 | mesWin = toPlot.getStdyState().getStdyRnds() #get measurement window, only include these values 657 | if mode == "IOPS": 658 | wlds = dt.SsdIopsTest.mixWlds 659 | bsLabels = toPlot.getBsLabels() 660 | if mode == "avg-LAT" or mode == "max-LAT": 661 | wlds = dt.SsdLatencyTest.mixWlds 662 | bsLabels = toPlot.getBsLabels() 663 | 664 | #each row will be a workload percentage 665 | for i in range(len(wlds)): 666 | mixWLds.append([]) 667 | #in each row will be the different block sizes 668 | for bs in range(len(bsLabels)): 669 | mixWLds[i].append(0) 670 | matrices = toPlot.getRndMatrices() 671 | 672 | #as j does not necessarily start from 0, we need k 673 | #to calculate the average iteratively 674 | k = 0 675 | #limit the matrices to the measurement window 676 | for j in mesWin: 677 | rndMat = matrices[j] 678 | #each row is a percentage of a workload 679 | for i,row in enumerate(rndMat): 680 | #in each row are the different block sizes 681 | for bs in range(len(row)): 682 | #calculate average iteratively 683 | if mixWLds[i][bs] != 0: 684 | #calculate max latency or continue with average 685 | if mode == "max-LAT": 686 | if row[bs][1] > mixWLds[i][bs]: 687 | mixWLds[i][bs] = row[bs][1]#max latency 688 | else: 689 | mixWLds[i][bs] *= k 690 | if mode == "IOPS": 691 | mixWLds[i][bs] += row[bs]#IOPS 692 | if mode == "avg-LAT": 693 | mixWLds[i][bs] += row[bs][2]#mean latency 694 | mixWLds[i][bs] = (mixWLds[i][bs]) / (k+1) 695 | else: 696 | if mode == "IOPS": 697 | mixWLds[i][bs] = row[bs]#IOPS 698 | if mode == "max-LAT": 699 | mixWLds[i][bs] = row[bs][1]#max latency 700 | if mode == "avg-LAT": 701 | mixWLds[i][bs] = row[bs][2]#mean latency 702 | k += 1 703 | #for latency convert to ms 704 | for i in range(len(mixWLds)): 705 | if mode == "avg-LAT" or mode == "max-LAT": 706 | for v in range(len(mixWLds[i])): 707 | mixWLds[i][v] = (mixWLds[i][v]) / 1000 708 | toPlot.addTable(mixWLds) 709 | 710 | def calcMsmtTPTable(toPlot): 711 | ''' 712 | Generate the measurement overview table for Throughput. The table is 713 | an overview over the average values in the measurement window. 714 | @param toPlot A SsdTest object. 715 | ''' 716 | wlds = [] #read and write workload mode 717 | #one row for read, one for write 718 | for i in range(2): 719 | wlds.append([]) 720 | #in each row will be the different block sizes 721 | for bs in range(len(toPlot.getBsLabels())): 722 | wlds[i].append(0) 723 | matrices = deepcopy(toPlot.getRndMatrices()) 724 | #each row of the matrix is a block size 725 | for j,bs in enumerate(matrices): 726 | #each block size has read and write 727 | for i,row in enumerate(bs): 728 | #as rnd does not need to start at 0 729 | #we need k to calculate average 730 | k = 0 731 | #read and write have their round values 732 | for rnd in toPlot.getStdyState().getStdyRnds(): 733 | #calculate average iteratively 734 | if wlds[i][j] != 0: 735 | wlds[i][j] *= k 736 | wlds[i][j] += row[rnd] 737 | wlds[i][j] = (wlds[i][j]) / (k+1) 738 | else: 739 | wlds[i][j] = row[rnd] 740 | k += 1 741 | #Convert to MB/s 742 | for i in range(len(wlds)): 743 | for v in range(len(wlds[i])): 744 | wlds[i][v] = (wlds[i][v]) / 1024 745 | toPlot.addTable(wlds) 746 | 747 | def getBS(bsLabels): 748 | ''' 749 | Convert a list of string block size labels to a list of integers. 750 | This can be handy if the block sizes are needed to be plotted at 751 | the x axis. 752 | @return A list of integer block sizes. 753 | ''' 754 | bs = [] 755 | for b in bsLabels: 756 | if b == "512": 757 | bs.append(0.5) 758 | continue 759 | s = b[0:-1] 760 | bs.append(int(s)) 761 | return bs 762 | 763 | def getMinMax(values, currMin, currMax): 764 | ''' 765 | Returns the minimum and maximum of a sequence of values. 766 | @param values Squence to calculate min and max for 767 | @param currMin Current minimum to compare to. 768 | @param currMax Current maximum to compare to. 769 | @return [newMin,newMax] if newMin is smaller than currMin, 770 | newMax if it is greater than currMax. 771 | ''' 772 | #TODO Testing for 0 can be a problem if minimum is really 0 773 | #This should not happen for performance tests under normal circumstances 774 | newMin = 0 775 | newMax = 0 776 | curr = max(values) 777 | if curr > currMax: 778 | newMax = curr 779 | else: 780 | newMax = currMax 781 | curr = min(values) 782 | if currMin == 0: 783 | newMin = curr 784 | else: 785 | if curr < currMin: 786 | newMin = curr 787 | else: 788 | newMin = currMin 789 | return [newMin,newMax] 790 | -------------------------------------------------------------------------------- /src/reports/RstReport.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 9 Aug 2012 3 | 4 | @author: gschoenb 5 | ''' 6 | from io import StringIO 7 | from copy import deepcopy 8 | import os 9 | import inspect 10 | import subprocess 11 | import logging 12 | 13 | import perfTest.DeviceTests as dt 14 | from perfTest.StdyState import StdyState 15 | 16 | class RstReport(object): 17 | ''' 18 | A report as restructured text. 19 | ''' 20 | 21 | def __init__(self,testname): 22 | ''' 23 | @param testname Name of the test, also the filename. 24 | ''' 25 | self.__testname = testname 26 | self.__rst = StringIO() 27 | 28 | def getRst(self): 29 | return self.__rst 30 | 31 | def addTitle(self): 32 | print("====================", file=self.__rst) 33 | print("TKperf Test Report", file=self.__rst) 34 | print("====================\n", file=self.__rst) 35 | print(".. contents::", file=self.__rst) 36 | print(".. sectnum::", file=self.__rst) 37 | print(".. include:: ", file=self.__rst) 38 | print(".. raw:: pdf\n", file=self.__rst) 39 | print("\tPageBreak\n", file=self.__rst) 40 | 41 | def addFooter(self): 42 | print(".. |logo| image:: " + os.path.dirname(inspect.getfile(RstReport)) + "/pics/TKperf_logo.png", file=self.__rst) 43 | print("\t:height: 90px", file=self.__rst) 44 | print(".. footer::", file=self.__rst) 45 | print("\t |logo| A `Thomas-Krenn `_ project, Page ###Page### of ###Total###\n", file=self.__rst) 46 | 47 | def addChapter(self,chap): 48 | print(chap, file=self.__rst) 49 | line = "=" 50 | for i in chap: 51 | line += "=" 52 | print(line+'\n', file=self.__rst) 53 | 54 | def addSection(self,sec): 55 | print(sec, file=self.__rst) 56 | line = "-" 57 | for i in sec: 58 | line += "-" 59 | print(line+'\n', file=self.__rst) 60 | 61 | def addString(self,str): 62 | if str[-1] != '\n': 63 | str += '\n' 64 | print(str, file=self.__rst) 65 | 66 | def addFigure(self,filename,testtype,perftype,index): 67 | ''' 68 | Adds a figure to the restructured text. 69 | @param filename The filename of the figure. 70 | @param testtype The type of the performance test (ssd,hdd) 71 | @param type The type of the test (iops,tp etc) 72 | @param index The index of the caption to insert after the figure. 73 | ''' 74 | print(".. figure:: "+filename, file=self.__rst) 75 | print("\t:scale: 65%", file=self.__rst) 76 | print("\t:figwidth: 85%\n", file=self.__rst) 77 | caption = '' 78 | if testtype == 'ssd': 79 | if perftype == 'iops': 80 | if index == 0: 81 | caption= "\tThe Steady State Convergence Plot shows the reached IOPS for " 82 | caption += "all block sizes of random writes over all rounds." 83 | if index == 1: 84 | caption= "\tThe Steady State Verification Plot shows the measured IOPS of 4k " 85 | caption += "random writes, the 20% average window and the slope of the linear best fit line " 86 | caption += "in the measurement window." 87 | if index == 2: 88 | caption= "\tThe Measurement Plot shows the average of IOPS in the measurement window. For every " 89 | caption += "workload the IOPS of all block sizes are plotted." 90 | if index == 3: 91 | caption= "\tThe Measurement 3D Plot shows the average of IOPS in the measurement window. For every " 92 | caption += "workload the IOPS of all block sizes are plotted." 93 | if perftype == 'tp': 94 | if index == 0: 95 | caption= "\tThe Read/Write Steady State Convergence Plot shows the bandwidth for " 96 | caption += "all block sizes of seq. reads over all rounds. On the top the write throughput is plotted, below " 97 | caption += "the throughput for read." 98 | if index == 1: 99 | caption= "\tThe Steady State Verification Plot shows the bandwidth of 1024k " 100 | caption += "seq. writes, the 20% average window and the slope of the linear best fit line " 101 | caption += "in the measurement window." 102 | if index == 2: 103 | caption= "\tThe Measurement Plot shows the average bandwidth of reads and writes in the measurement window. " 104 | caption += "For all block sizes the seq. read and write bandwidth is plotted." 105 | if perftype == 'lat': 106 | if index == 0: 107 | caption= "\tThe Steady State Convergence Plot shows the mean latency for " 108 | caption += "all block sizes of random read, mixed workload and write." 109 | if index == 1: 110 | caption= "\tThe Steady State Verification Plot shows the mean latency of 4k " 111 | caption += "random writes, the 20% average window and the slope of the linear best fit line " 112 | caption += "in the measurement window." 113 | if index == 4: 114 | caption = "\tThe Latency Measurement 3D Plot shows the average latency on top and the max latency below it. " 115 | caption += "For the measurement window every workload including all block sizes is plotted." 116 | if perftype == 'writesat': 117 | if index == 0: 118 | caption= "\tThe Write Saturation IOPS Plot shows the average IOPS of 4k random " 119 | caption += "writes over all rounds." 120 | if index == 1: 121 | caption= "\tThe Write Saturation Latency Plot shows the mean latency of 4k random " 122 | caption += "writes over all rounds." 123 | if testtype == 'hdd': 124 | if perftype == 'iops': 125 | if index == 0: 126 | caption= "\tThe Measurement Plot shows the IOPS of each one-128th part of the disk. For every " 127 | caption += "workload the IOPS of all block sizes are plotted." 128 | if perftype == 'tp': 129 | if index == 0: 130 | caption= "\tThe Measurement Plot shows the bandwidth of reads and writes in each one-128th part " 131 | caption += "of the disk. For all block sizes the seq. read and write bandwidth is plotted." 132 | if index == 1: 133 | caption= "\tThe Boxplot shows minimum, lower quartile, median, upper quartile and maximum. " 134 | caption += "For all block sizes the seq. read and write data is plotted." 135 | 136 | self.addString(caption) 137 | 138 | def addTable(self,table,labels,perftype): 139 | ''' 140 | Adds a table to the restructured text. 141 | @param table The table to insert into the report. 142 | @param type The type of performance test. 143 | ''' 144 | #copy labels and values, don't want to change them 145 | l = list(labels) 146 | t = deepcopy(table) 147 | 148 | if perftype == 'iops': 149 | val = StringIO() 150 | print(".. csv-table:: Average IOPS vs. Block Size and R/W Mix %", file=self.__rst) 151 | print("\t:header: \"Block Size\ |darr|\", \"Wld. |rarr| \" 100/0, 95/5, 65/35, 50/50, 35/65, 5/95, 0/100\n", file=self.__rst) 152 | #reverse the block size in each table row, to start with 512B 153 | for row in t: 154 | row.reverse() 155 | #also reverse labels 156 | l.reverse() 157 | if perftype == 'tp': 158 | val = StringIO() 159 | print(".. csv-table:: Average MB/s vs. Block Size and R/W", file=self.__rst) 160 | print("\t:header: \"Block Size\ |darr|\", \"Read\", \"Write\"\n", file=self.__rst) 161 | 162 | if perftype == 'avg-lat': 163 | val = StringIO() 164 | print(".. csv-table:: Average Latency (ms) vs. Block Size and R/W Mix %", file=self.__rst) 165 | print("\t:header: \"Block Size\ |darr|\", \"Wld. |rarr| \" 0/100, 65/35, 100/0\n", file=self.__rst) 166 | #reverse to start with 0/100 167 | t.reverse() 168 | 169 | if perftype == 'max-lat': 170 | val = StringIO() 171 | print(".. csv-table:: Max Latency (ms) vs. Block Size and R/W Mix %", file=self.__rst) 172 | print("\t:header: \"Block Size\ |darr|\", \"Wld. |rarr| \" 0/100, 65/35, 100/0\n", file=self.__rst) 173 | #reverse to start with 0/100 174 | t.reverse() 175 | 176 | for i in range(len(l)): 177 | val.write("\t") 178 | val.write(l[i] + ', ') 179 | #access the matrix column wise, round the numbers to 3 after 180 | for j,elem in enumerate(row[i] for row in t): 181 | if j != 0: 182 | val.write(", ") 183 | val.write(str(round(elem,3))) 184 | val.write("\n") 185 | self.addString(val.getvalue()) 186 | val.close() 187 | 188 | 189 | def toRstFile(self): 190 | f = open(self.__testname+'.rst','w') 191 | f.write(self.__rst.getvalue()) 192 | self.__rst.close() 193 | f.close() 194 | 195 | def toPDF(self,pdfgen): 196 | pdf = subprocess.Popen([pdfgen, self.__testname+'.rst'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 197 | stderr = pdf.communicate()[1] 198 | if pdf.returncode != 0: 199 | logging.error("generating the PDF encountered an error: " + stderr) 200 | raise RuntimeError("PDF gen command error") 201 | 202 | def addDevInfo(self,devStr,featMat): 203 | ''' 204 | Add info about the tested device to the report. 205 | @param devStr The device information from hdparm or the dsc file. 206 | @param featMat The extra feature matrix given via a csv table. 207 | ''' 208 | self.addChapter("Setup Information") 209 | print("Tested Device:", file=self.__rst) 210 | if devStr[-1] == '\n': 211 | devStr = devStr[:-1] 212 | for line in devStr.split('\n'): 213 | print(" - " + line, file=self.__rst) 214 | print('\n', file=self.__rst) 215 | 216 | if featMat != None: 217 | print("Feature Matrix:", file=self.__rst) 218 | print(featMat + "\n", file=self.__rst) 219 | 220 | def addCmdLine(self,cmdLineStr): 221 | print("Used command line:", file=self.__rst) 222 | print(" - " + cmdLineStr, file=self.__rst) 223 | 224 | def addSetupInfo(self,ioVer,fioVer,dateStr): 225 | ''' 226 | Add info about the version of Fio to the report. 227 | @param setupStr The Fio version string, fetched via str-method of a FioJob. 228 | @param dateStr The date string the test was carried out. 229 | ''' 230 | print("Performance System:", file=self.__rst) 231 | print(" - TKperf Version: " + ioVer, file=self.__rst) 232 | print(" - Fio Version: " + fioVer, file=self.__rst) 233 | print(" - Date of test run: " + dateStr, file=self.__rst) 234 | 235 | def addFioJobInfo(self,nj,iod): 236 | ''' 237 | Write information about Fio number of jobs and iodepth to report. 238 | @param nj The number of jobs. 239 | @param iod The number of outstanding ios (iodepth). 240 | ''' 241 | info = StringIO() 242 | info.write(" - Number of jobs: " + str(nj) + "\n") 243 | info.write(" - Number of outstanding IOs (iodepth): " + str(iod)) 244 | self.addString(info.getvalue()) 245 | info.close() 246 | 247 | def addOSInfo(self,OSDict): 248 | if OSDict != None: 249 | print("Operating System:", file=self.__rst) 250 | if 'kernel' in OSDict: 251 | print(" - Kernel Version: " + OSDict['kernel'], file=self.__rst) 252 | if 'lsb' in OSDict: 253 | print(" - " + OSDict['lsb'], file=self.__rst) 254 | 255 | def addGeneralInfo(self,testtype): 256 | ''' 257 | Defines some general used words. 258 | @param testtype The type of the performance test (ssd,hdd) 259 | ''' 260 | info = StringIO() 261 | self.addChapter("General Information") 262 | info.write("- *workloads*: The percentage of read operations in the random mixed workload. In the plots the ") 263 | info.write("term \"100/00\" means 100% read and 0% write, \"95/5\" 95% read and 5% write, and so on.\n") 264 | info.write("- *block sizes*: The block size of Fio to be used for IO operations.\n") 265 | if testtype == 'ssd': 266 | info.write("- *measurement window*: Those rounds, where the dependence variable became stable.\n") 267 | info.write("- *dependence variable*: A specific type of test variable to determine the steady state.\n") 268 | if testtype == 'hdd': 269 | info.write("- *round*: As an hdd's performance is different at outer and inner side, the ") 270 | info.write("device is divided into equal size parts. In every round one part of the device is tested") 271 | self.addString(info.getvalue()) 272 | info.close() 273 | if testtype == 'ssd': 274 | info = StringIO() 275 | self.addSection("Steady State") 276 | info.write("The Steady State is to determine if a test has reached a steady performance level. ") 277 | info.write("Each test has a different dependence variable to check if the state has already been reached. ") 278 | info.write("To check for the steady state the performance values of a test measurement window are taken (the last 5 rounds).\n") 279 | info.write("The steady state is reached if:\n\n") 280 | info.write("- The maximum data excursion is less than 20% of the average in the measurement window.\n") 281 | info.write("- The slope of the linear best fit line is less than 10% of the average in the measurement window\n\n") 282 | 283 | info.write("If these two conditions are met the steady state has been reach for the specific dependence variable. ") 284 | info.write("Therefore the test can be stopped and the performance values of the measurement window can be taken ") 285 | info.write("for the measurement plots. If the steady state has not been reached after a maximum number of rounds the test ") 286 | info.write("can be stopped as well. The numbers for these two variables are:\n\n") 287 | print("- Measurement Window: " + str(StdyState.testMesWindow), file=info) 288 | print("- Max. number of rounds: " + str(StdyState.testRnds) + '\n', file=info) 289 | self.addString(info.getvalue()) 290 | info.close() 291 | 292 | def addSteadyInfo(self,test): 293 | ''' 294 | Adds information about the steady state to the rst report. 295 | @param test The corresponding test object. 296 | ''' 297 | self.addSection("Steady State Information") 298 | 299 | stdyStr = StringIO() 300 | stdyStr.write("Steady State has been reached:\n") 301 | stdyStr.write(" - ") 302 | print(test.getStdyState().isSteady(), file=stdyStr) 303 | 304 | stdyStr.write("Steady State has been reached in rounds :\n") 305 | stdyStr.write(" - ") 306 | print(test.getStdyState().getStdyRnds(), file=stdyStr) 307 | 308 | stdyStr.write("Values in stdy measurement window:\n") 309 | stdyStr.write(" - ") 310 | print(test.getStdyState().getStdyValues(), file=stdyStr) 311 | 312 | stdyStr.write("Average in stdy measurement window:\n") 313 | stdyStr.write(" - ") 314 | print(test.getStdyState().getStdyAvg(), file=stdyStr) 315 | 316 | self.addString(stdyStr.getvalue()) 317 | stdyStr.close() 318 | 319 | def addTestInfo(self,testtype,testname,test): 320 | ''' 321 | Add information about a test to the rst report. 322 | This part is the main information about a test, it describes how 323 | a test has been carried out. 324 | @param testtype Type of performance test (hdd,ssd) 325 | @param testname Type name of a test. 326 | @param test The specific test object 327 | ''' 328 | if testtype == 'ssd': 329 | if testname == 'iops': 330 | desc = StringIO() 331 | desc.write("The IOPS test consists of looping over the following parameters:\n") 332 | desc.write('\n::\n\n\t') 333 | print("Make Secure Erase", file=desc) 334 | print("\tWorkload Ind. Preconditioning", file=desc) 335 | print("\tWhile not Steady State", file=desc) 336 | print("\t\tFor workloads ", end=' ', file=desc) 337 | print(dt.SsdIopsTest.mixWlds, file=desc) 338 | desc.write('\t\t\t') 339 | print("For block sizes", end=' ', file=desc) 340 | print(test.getBsLabels(), file=desc) 341 | desc.write("\nEach combination of workload and block size is carried out for 60 seconds using direct IO. ") 342 | desc.write("The average number of read and write IOPS is measured and summed up, therefore 56 values are ") 343 | desc.write("the result of the two loops.\n") 344 | desc.write("After these loops are finished one test round has been carried out. To detect the steady state ") 345 | desc.write("the IOPS of 4k random write are taken.\n\n") 346 | print("- Dependent Variable: 4k block size, random write", file=desc) 347 | self.addString(desc.getvalue()) 348 | desc.close() 349 | self.addSteadyInfo(test) 350 | if testname == 'tp': 351 | desc = StringIO() 352 | desc.write("The throughput test consists of looping over the following parameters:\n") 353 | desc.write('\n::\n\n\t') 354 | print("For block sizes ", end=' ', file=desc) 355 | print(test.getBsLabels(), file=desc) 356 | desc.write('\t\t') 357 | print("Make Secure Erase", file=desc) 358 | desc.write('\t\t') 359 | print("While not Steady State", file=desc) 360 | desc.write('\t\t\t') 361 | print("Sequential read", file=desc) 362 | desc.write('\t\t\t') 363 | print("Sequential write", file=desc) 364 | desc.write("\nFor each block size sequential read and write is carried out for 60 seconds using direct IO. ") 365 | desc.write("The number of kilobytes for read and write is measured, therefore 2 values are ") 366 | desc.write("the result of one round.\n") 367 | desc.write("To detect the steady state the throughput of 1024k sequential write is taken.\n\n") 368 | print("- Dependent Variable: 1024k block size, sequential write", file=desc) 369 | self.addString(desc.getvalue()) 370 | desc.close() 371 | self.addSteadyInfo(test) 372 | if testname == 'lat': 373 | desc = StringIO() 374 | desc.write("The latency test consists of looping over the following parameters:\n") 375 | desc.write('\n::\n\n\t') 376 | print("Make Secure Erase", file=desc) 377 | print("\tWorkload Ind. Preconditioning", file=desc) 378 | print("\tWhile not Steady State", file=desc) 379 | print("\t\tFor workloads ", end=' ', file=desc) 380 | print(dt.SsdLatencyTest.mixWlds, file=desc) 381 | desc.write('\t\t\t') 382 | print("For block sizes", end=' ', file=desc) 383 | print(test.getBsLabels(), file=desc) 384 | desc.write("\nFor all block sizes random read, a 65/35 read/write mixed workload and random write is carried out for 60 ") 385 | desc.write("seconds using direct IO. ") 386 | desc.write("For every combination the Min, Max and Mean Latency is measured. ") 387 | desc.write("After these loops are finished one test round has been carried out. To detect the steady state ") 388 | desc.write("the mean latency of 4k random write is taken.\n\n") 389 | print("- Dependent Variable: 4k block size, random write mean latency", file=desc) 390 | self.addString(desc.getvalue()) 391 | desc.close() 392 | self.addSteadyInfo(test) 393 | if testname == 'writesat': 394 | desc = StringIO() 395 | desc.write("The write saturation test consists of looping over the following parameters:\n") 396 | desc.write('\n::\n\n\t') 397 | print("Make Secure Erase", file=desc) 398 | print("\tWhile not written 4x User Capacity or 24h", file=desc) 399 | print("\t\tCarry out random write, 4k block size for 1 minute.", file=desc) 400 | desc.write("\nFor 4k block size random write is carried out for 60 ") 401 | desc.write("seconds using direct IO. ") 402 | desc.write("For each round (60 second window) the write IOPS and latencies are measured. Also the total written ") 403 | desc.write("IO is measured to check if 4x capacity has been written.\n\n") 404 | desc.write("As no steady state detection is necessary there is no dependence variable.\n\n") 405 | self.addString(desc.getvalue()) 406 | desc.close() 407 | 408 | if testtype == 'hdd': 409 | if testname == 'iops': 410 | desc = StringIO() 411 | desc.write("The IOPS test consists of looping over the following parameters:\n") 412 | desc.write('\n::\n\n\t') 413 | print("Divide device in " + str(dt.HddIopsTest.maxRnds) + " parts", file=desc) 414 | print("\tFor range(" + str(dt.HddIopsTest.maxRnds) + ")", file=desc) 415 | print("\t\tFor workloads ", end=' ', file=desc) 416 | print(dt.HddIopsTest.mixWlds, file=desc) 417 | desc.write('\t\t\t') 418 | print("For block sizes", end=' ', file=desc) 419 | print(test.getBsLabels(), file=desc) 420 | desc.write("\nEach combination of workload and block size is carried out for 60 seconds using direct IO. ") 421 | desc.write("The IOPS of one round are an indicator for the random performance of the corresponding area.") 422 | self.addString(desc.getvalue()) 423 | desc.close() 424 | if testname == 'tp': 425 | desc = StringIO() 426 | desc.write("The throughput test consists of looping over the following parameters:\n") 427 | desc.write('\n::\n\n\t') 428 | print("For block sizes ", end=' ', file=desc) 429 | print(test.getBsLabels(), file=desc) 430 | desc.write('\t\t') 431 | print("For range(" + str(dt.HddTPTest.maxRnds) + ")", file=desc) 432 | desc.write('\t\t\t') 433 | print("Sequential read", file=desc) 434 | desc.write('\t\t\t') 435 | print("Sequential write", file=desc) 436 | desc.write("\nFor each block size, every area of the device (this are the rounds) is tested ") 437 | desc.write("with sequential read and write using direct IO. ") 438 | self.addString(desc.getvalue()) 439 | desc.close() 440 | -------------------------------------------------------------------------------- /src/reports/XmlReport.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 03.08.2012 3 | 4 | @author: gschoenb 5 | ''' 6 | from lxml import etree 7 | 8 | class XmlReport(object): 9 | ''' 10 | Creates an xml file with the test results of a device test. 11 | ''' 12 | 13 | 14 | def __init__(self,testname): 15 | ''' 16 | Constructor 17 | @param testname The name of the root tag, equals the name 18 | of the performance test. 19 | ''' 20 | 21 | ## The root tag of the xml file. 22 | self.__xml = etree.Element(testname) 23 | 24 | def getXml(self): 25 | return self.__xml 26 | 27 | def printXml(self): 28 | print((etree.tostring(self.__xml, xml_declaration=True,pretty_print=True))) 29 | 30 | def xmlToFile(self,testname): 31 | et = etree.ElementTree(self.__xml) 32 | et.write(testname + '.xml') 33 | 34 | def fileToXml(self,testname): 35 | et = etree.parse(testname + '.xml') 36 | self.__xml = et.getroot() 37 | -------------------------------------------------------------------------------- /src/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-krenn/TKperf/638b0837b97b32337314993feb52b5111aae0979/src/reports/__init__.py -------------------------------------------------------------------------------- /src/reports/pics/TKperf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-krenn/TKperf/638b0837b97b32337314993feb52b5111aae0979/src/reports/pics/TKperf_logo.png -------------------------------------------------------------------------------- /src/system/Mail.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Sep 26, 2014 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | import smtplib 8 | from email.mime.multipart import MIMEMultipart 9 | from email.mime.application import MIMEApplication 10 | from email.mime.text import MIMEText 11 | from os import path 12 | 13 | class Mail(object): 14 | ''' 15 | Sending emails via local smtp. 16 | ''' 17 | 18 | def __init__(self, subj, sender, rcpt, smtp): 19 | ''' 20 | Constructor 21 | ''' 22 | self.__sender = sender 23 | self.__rcpt = rcpt 24 | self.__msg = MIMEMultipart() 25 | self.__msg['Subject'] = subj 26 | self.__msg['From'] = sender 27 | self.__msg['To'] = rcpt 28 | self.__smtp = smtplib.SMTP(smtp) 29 | 30 | def addMsg(self, msg): 31 | msgText = MIMEText(msg, 'plain') 32 | self.__msg.attach(msgText) 33 | 34 | def addPDFAttachment(self,filename): 35 | fp=open(filename,'rb') 36 | att = MIMEApplication(fp.read(),_subtype="pdf") 37 | fp.close() 38 | name = path.basename(path.normpath(filename)) 39 | att.add_header('Content-Disposition','attachment',filename=name) 40 | self.__msg.attach(att) 41 | 42 | def addXMLAttachment(self,filename): 43 | fp=open(filename,'rb') 44 | att = MIMEApplication(fp.read(),_subtype="xml") 45 | fp.close() 46 | name = path.basename(path.normpath(filename)) 47 | att.add_header('Content-Disposition','attachment',filename=name) 48 | self.__msg.attach(att) 49 | 50 | def addTextAttachment(self,filename): 51 | fp=open(filename,'rb') 52 | att = MIMEApplication(fp.read(),_subtype="txt") 53 | fp.close() 54 | name = path.basename(path.normpath(filename)) 55 | att.add_header('Content-Disposition','attachment',filename=name) 56 | self.__msg.attach(att) 57 | 58 | def send(self): 59 | self.__smtp.sendmail(self.__sender, self.__rcpt, self.__msg.as_string()) 60 | self.__smtp.quit() -------------------------------------------------------------------------------- /src/system/OS.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Sep 24, 2014 3 | 4 | @author: gschoenb 5 | ''' 6 | 7 | from abc import ABCMeta, abstractmethod 8 | import subprocess 9 | import logging 10 | import re 11 | from os import lstat 12 | from stat import S_ISBLK 13 | from time import sleep 14 | 15 | class RAIDtec(object, metaclass=ABCMeta): 16 | ''' 17 | Representing a RAID technology, used from the OS. 18 | ''' 19 | 20 | def __init__(self, path, level, devices): 21 | ## Path of the RAID utils 22 | self.__util = None 23 | ## Path of the raid device 24 | self.__path = path 25 | ## RAID level 26 | self.__level = level 27 | ## List of devices 28 | self.__devices = devices 29 | ## List of block Devices in OS 30 | self.__blockdevs = None 31 | 32 | def getUtil(self): return self.__util 33 | def getDevPath(self): return self.__path 34 | def getLevel(self): return self.__level 35 | def getDevices(self): return self.__devices 36 | def getBlockDevs(self): return self.__blockdevs 37 | 38 | def setUtil(self, u): 39 | self.__util = u 40 | 41 | def checkBlockDevs(self): 42 | ''' 43 | Checks the current available block devices. 44 | Sets blockdevs of OS. 45 | ''' 46 | out = subprocess.Popen(['lsblk', '-l', '-n', '-e', '7', '-e', '1', '-o', 'NAME'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,universal_newlines=True) 47 | (stdout, stderr) = out.communicate() 48 | if stderr != '': 49 | logging.error("lsblk encountered an error: " + stderr) 50 | raise RuntimeError("lsblk command error") 51 | else: 52 | self.__blockdevs = stdout.splitlines() 53 | logging.info("# Got the following BDs: ") 54 | logging.info(self.getBlockDevs()) 55 | 56 | @abstractmethod 57 | def initialize(self): 58 | ''' Initialize the specific RAID technology. ''' 59 | @abstractmethod 60 | def checkRaidPath(self): 61 | ''' Checks if the virtual drive exists. ''' 62 | @abstractmethod 63 | def checkVDs(self): 64 | ''' Check which virtual drives are configured. ''' 65 | @abstractmethod 66 | def createVD(self): 67 | ''' Create a virtual drive. ''' 68 | @abstractmethod 69 | def deleteVD(self): 70 | ''' Delete a virtual drive. ''' 71 | @abstractmethod 72 | def isReady(self): 73 | ''' Check if a virtual drive is ready. ''' 74 | 75 | class Mdadm(RAIDtec): 76 | ''' 77 | Represents a linux software RAID technology. 78 | ''' 79 | 80 | def initialize(self): 81 | ''' 82 | Checks for mdadm and sets the util path. 83 | ''' 84 | mdadm = subprocess.Popen(['which', 'mdadm'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 85 | stdout = mdadm.communicate()[0] 86 | if mdadm.returncode != 0: 87 | logging.error("# Error: command 'which mdadm' returned an error code.") 88 | raise RuntimeError("which mdadm command error") 89 | else: 90 | self.setUtil(stdout.rstrip("\n")) 91 | 92 | def checkRaidPath(self): 93 | logging.info("# Checking for device "+self.getDevPath()) 94 | try: 95 | mode = lstat(self.getDevPath()).st_mode 96 | except OSError: 97 | return False 98 | else: 99 | return S_ISBLK(mode) 100 | 101 | def checkVDs(self): 102 | pass 103 | 104 | def createVD(self): 105 | self.getDevPath() 106 | args = [self.getUtil(), "--create", self.getDevPath(), "--quiet", "--metadata=default", str("--level=" + str(self.getLevel())), str("--raid-devices=" + str(len(self.getDevices())))] 107 | for dev in self.getDevices(): 108 | args.append(dev) 109 | logging.info("# Creating raid device "+self.getDevPath()) 110 | logging.info("# Command line: "+subprocess.list2cmdline(args)) 111 | mdadm = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 112 | stderr = mdadm.communicate()[1] 113 | if mdadm.returncode != 0: 114 | logging.error("mdadm encountered an error: " + stderr) 115 | raise RuntimeError("mdadm command error") 116 | 117 | def deleteVD(self): 118 | logging.info("# Deleting raid device "+self.getDevPath()) 119 | mdadm = subprocess.Popen([self.getUtil(), "--stop", self.getDevPath()], stdout=subprocess.PIPE, stderr=subprocess.PIPE,universal_newlines=True) 120 | stderr = mdadm.communicate()[1] 121 | if mdadm.returncode != 0: 122 | logging.error("mdadm encountered an error: " + stderr) 123 | raise RuntimeError("mdadm command error") 124 | # Reset all devices in the Raid 125 | # If the raid device was overwritten completely before (precondition), zero-superblock can fail 126 | for dev in self.getDevices(): 127 | logging.info("# Deleting superblock for device "+dev) 128 | mdadm = subprocess.Popen([self.getUtil(), "--zero-superblock", dev], stdout=subprocess.PIPE, stderr=subprocess.PIPE,universal_newlines=True) 129 | mdadm.communicate() 130 | 131 | def isReady(self): 132 | logging.info("# Checking if raid device "+self.getDevPath()+" is ready...") 133 | process = subprocess.Popen(["cat", "/proc/mdstat"], stdout=subprocess.PIPE, stderr=subprocess.PIPE,universal_newlines=True) 134 | (stdout, stderr) = process.communicate() 135 | if stderr != '': 136 | logging.error("cat mdstat encountered an error: " + stderr) 137 | raise RuntimeError("cat mdstat command error") 138 | else: 139 | # Remove the Personalities line 140 | stdout = stdout.partition("\n")[2] 141 | # Split in single devices 142 | mds = stdout.split("\n\n") 143 | # Search devices for our device 144 | match = re.search('^/dev/(.*)$', self.getDevPath()) 145 | mdName = match.group(1) 146 | for md in mds: 147 | if md.startswith(mdName): 148 | # Check if a task is running) 149 | if md.find("finish") != -1: 150 | return False 151 | else: 152 | return True 153 | 154 | class Storcli(RAIDtec): 155 | ''' 156 | Represents a storcli based RAID technology. 157 | ''' 158 | 159 | def __init__(self, path, level, devices, readpolicy, writepolicy, stripsize): 160 | ''' 161 | Constructor 162 | ''' 163 | super(Storcli, self).__init__(path, level, devices) 164 | ## The virtual drive of the raid controller 165 | self.__vd = None 166 | ## List of current RAID virtual drives 167 | self.__vds = None 168 | ## Read policy of the virtual drive 169 | self.__readpolicy = readpolicy 170 | ## Write policy of the virtual drive 171 | self.__writepolicy = writepolicy 172 | ## Strip size of the virtual drive 173 | self.__stripsize = stripsize 174 | 175 | def getVD(self): return self.__vd 176 | def getVDs(self): return self.__vds 177 | def getREADPOLICY(self): return self.__readpolicy 178 | def getWRITEPOLICY(self): return self.__writepolicy 179 | def getSTRIPSIZE(self): return self.__stripsize 180 | def setVD(self,v): self.__vd = v 181 | def setVDs(self, v): self.__vds = v 182 | 183 | def initialize(self): 184 | ''' 185 | Checks for the storcli executable and sets the path of storcli. 186 | ''' 187 | storcli = subprocess.Popen(['which', 'storcli'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 188 | stdout = storcli.communicate()[0] 189 | if storcli.returncode != 0: 190 | storcli = subprocess.Popen(['which', 'storcli64'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 191 | stdout = storcli.communicate()[0] 192 | if storcli.returncode != 0: 193 | logging.error("# Error: command 'which storcli' returned an error code.") 194 | raise RuntimeError("which storcli command error") 195 | else: 196 | self.setUtil(stdout.rstrip("\n")) 197 | 198 | def checkRaidPath(self): 199 | ''' 200 | Checks if the virtual drive of the RAID controller is available. 201 | @return True if yes, False if not 202 | ''' 203 | if self.getVD() != None: 204 | logging.info("# Checking for virtual drive "+self.getVD()) 205 | match = re.search('^[0-9]\/([0-9]+)',self.getVD()) 206 | vdNum = match.group(1) 207 | storcli = subprocess.Popen([self.getUtil(),'/c0/v'+vdNum, 'show', 'all'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 208 | (stdout,stderr) = storcli.communicate() 209 | if storcli.returncode != 0: 210 | logging.error("storcli encountered an error: " + stderr) 211 | raise RuntimeError("storcli command error") 212 | else: 213 | vdCheck = None 214 | for line in stdout.splitlines(): 215 | match = re.search('^Description = (\w+)$',line) 216 | if match != None: 217 | if match.group(1) == 'No VDs have been configured': 218 | vdCheck = False 219 | else: 220 | vdCheck = True 221 | match = re.search('^Status = (\w+)$',line) 222 | if match != None: 223 | if match.group(1) == 'Failure': 224 | vdCheck = False 225 | else: 226 | vdCheck = True 227 | return vdCheck 228 | else: 229 | logging.info("# VD not set, checking for PDs: ") 230 | logging.info(self.getDevices()) 231 | storcli = subprocess.Popen([self.getUtil(),'/c0/vall', 'show', 'all'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 232 | (stdout,stderr) = storcli.communicate() 233 | if storcli.returncode != 0: 234 | logging.error("storcli encountered an error: " + stderr) 235 | raise RuntimeError("storcli command error") 236 | else: 237 | vdCheck = None 238 | for line in stdout.splitlines(): 239 | match = re.search('^Description = (\w+)$',line) 240 | if match != None: 241 | if match.group(1) == 'No VDs have been configured': 242 | logging.info("# No VDs configured!") 243 | vdCheck = False 244 | break 245 | match = re.search('^PDs for VD ([0-9]+) \:$',line) 246 | if match != None: 247 | vdNum = match.group(1) 248 | PDs = self.getPDsFromVD(vdNum) 249 | if set(self.getDevices()) == set(PDs): 250 | self.setVD('0/'+vdNum) 251 | logging.info("# Set VD as "+self.getVD()) 252 | vdCheck = True 253 | break 254 | else: 255 | vdCheck = False 256 | return vdCheck 257 | 258 | def getPDsFromVD(self,vdNum): 259 | ''' 260 | Returns a list of PDs for a given VD. 261 | @param vdNum Number of VD to check for 262 | @return A list of enclosure:device IDs 263 | ''' 264 | storcli = subprocess.Popen([self.getUtil(),'/c0/v'+vdNum, 'show', 'all'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 265 | (stdout,stderr) = storcli.communicate() 266 | if storcli.returncode != 0: 267 | logging.error("storcli encountered an error: " + stderr) 268 | raise RuntimeError("storcli command error") 269 | else: 270 | PDs = [] 271 | for line in stdout.splitlines(): 272 | match = re.search('^([0-9]+\:[0-9]+).*$',line) 273 | if match != None: 274 | PDs.append(match.group(1)) 275 | logging.info("# Found PDs for VD "+ vdNum +":") 276 | logging.info(PDs) 277 | return PDs 278 | 279 | def checkVDs(self): 280 | ''' 281 | Checks which virtual drives are configured. 282 | Sets VDs as a list of virtual drives. 283 | ''' 284 | process1 = subprocess.Popen([self.getUtil(), '/call', '/vall', 'show'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,universal_newlines=True) 285 | process2 = subprocess.Popen(['awk', 'BEGIN{RS=ORS=\"\\n\\n\";FS=OFS=\"\\n\\n\"}/TYPE /'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=process1.stdout,universal_newlines=True) 286 | process3 = subprocess.Popen(['awk', '/^[0-9]/{print $1}'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=process2.stdout,universal_newlines=True) 287 | process1.stdout.close() 288 | process2.stdout.close() 289 | (stdout, stderr) = process3.communicate() 290 | if process3.returncode != 0: 291 | logging.error("storcli encountered an error: " + stderr) 292 | raise RuntimeError("storcli command error") 293 | else: 294 | self.setVDs(stdout.splitlines()) 295 | logging.info("# Got the following VDs: ") 296 | logging.info(self.getVDs()) 297 | 298 | def createVD(self): 299 | ''' 300 | Creates a virtual drive from a given raid level and a list of 301 | enclosure:drive IDs. self.__devices must be a list of raid devices as 302 | strings, e.g. ['e252:1','e252:2']. 303 | ''' 304 | encid = self.getDevices()[0].split(":")[0] 305 | args = [self.getUtil(), '/c0', 'add', 'vd', str('type=r' + str(self.getLevel()))] 306 | devicearg = "drives=" + encid + ":" 307 | for dev in self.getDevices(): 308 | devicearg += dev.split(":")[1] + "," 309 | args.append(devicearg.rstrip(",")) 310 | if str(self.getLevel()) == "10": 311 | args.append(str('PDperArray=2')) 312 | if self.getREADPOLICY(): 313 | args.append(self.getREADPOLICY()) 314 | if self.getWRITEPOLICY(): 315 | args.append(self.getWRITEPOLICY()) 316 | if self.getSTRIPSIZE(): 317 | args.append(str('strip=' + str(self.getSTRIPSIZE()))) 318 | logging.info("# Creating raid device with storcli") 319 | logging.info("# Command line: "+subprocess.list2cmdline(args)) 320 | # Fetch VDs before creating the new one 321 | # Wait for update of lsblk 322 | sleep(5) 323 | self.checkVDs() 324 | self.checkBlockDevs() 325 | VDsbefore = self.getVDs() 326 | BDsbefore = self.getBlockDevs() 327 | process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 328 | stderr = process.communicate()[1] 329 | if process.returncode != 0: 330 | logging.error("storcli encountered an error: " + str(stderr)) 331 | raise RuntimeError("storcli command error") 332 | else: 333 | # Wait for update of lsblk 334 | sleep(5) 335 | # Fetch VDs after creating the new one 336 | self.checkVDs() 337 | self.checkBlockDevs() 338 | VDsafter = self.getVDs() 339 | BDsafter = self.getBlockDevs() 340 | vd = [x for x in VDsafter if x not in VDsbefore] 341 | if self.getVD() != None: 342 | if vd[0] != self.getVD(): 343 | logging.info("# The VD changed, the new on is: " + vd[0]) 344 | self.setVD(vd[0]) 345 | bd = [x for x in BDsafter if x not in BDsbefore] 346 | if (len(bd) != 1) or (('/dev/'+bd[0]) != self.getDevPath()): 347 | logging.info("Got BD: " + bd[0]) 348 | logging.error("# Error: The new block device doesn't match the tested device path!") 349 | raise RuntimeError("New block dev doesn't match tested dev error") 350 | # Set MegaRAID's automatic background initialization (autobgi) to 351 | # off to prevent performance influences caused by autobgi 352 | match = re.search('^[0-9]\/([0-9]+)', self.getVD()) 353 | vdNum = match.group(1) 354 | storclibgi = subprocess.Popen([self.getUtil(),'/c0/v' + vdNum, 'set autobgi=off'], stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 355 | stderr = storclibgi.communicate()[1] 356 | if storclibgi.returncode != 0: 357 | logging.error("storcli encountered an error: " + stderr) 358 | raise RuntimeError("storcli command error") 359 | else: 360 | logging.info("# Set autobgi=off for VD " + vdNum) 361 | # Log information about the created VD 362 | logging.info("# Created VD " + self.getVD()) 363 | logging.info("# Using block device " + bd[0]) 364 | 365 | def deleteVD(self): 366 | ''' 367 | Deletes a virtual drive, self.__vd must be a string like 0/0 specifying 368 | the virtual drive. 369 | ''' 370 | match = re.search('^[0-9]\/([0-9]+)',self.getVD()) 371 | vdNum = match.group(1) 372 | storcli = subprocess.Popen([self.getUtil(),'/c0/v'+vdNum, 'del', 'force'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 373 | stderr = storcli.communicate()[1] 374 | if storcli.returncode != 0: 375 | logging.error("storcli encountered an error: " + stderr) 376 | raise RuntimeError("storcli command error") 377 | else: 378 | logging.info("# Deleting raid device VD "+vdNum) 379 | 380 | def isReady(self): 381 | ''' 382 | Checks if a virtual device is ready, i.e. if no rebuild on any PDs is running 383 | and if not initializarion process is going on. 384 | @return True if VD is ready, False if not 385 | ''' 386 | ready = None 387 | storcli = subprocess.Popen([self.getUtil(),'/c0/eall/sall', 'show', 'rebuild'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 388 | (stdout, stderr) = storcli.communicate() 389 | if storcli.returncode != 0: 390 | logging.error("storcli encountered an error: " + stderr) 391 | raise RuntimeError("storcli command error") 392 | else: 393 | for line in stdout.splitlines(): 394 | match = re.search('^\/c0\/e([0-9]+\/s[0-9]+).*$',line) 395 | if match != None: 396 | for d in self.getDevices(): 397 | d = d.replace(':','/s') 398 | if d == match.group(1): 399 | logging.info(line) 400 | status = re.search('Not in progress',line) 401 | if status != None: 402 | ready = True 403 | else: 404 | ready = False 405 | match = re.search('^[0-9]\/([0-9]+)',self.getVD()) 406 | vdNum = match.group(1) 407 | storcli = subprocess.Popen([self.getUtil(),'/call', '/v'+vdNum, 'show', 'init'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True) 408 | (stdout, stderr) = storcli.communicate() 409 | if storcli.returncode != 0: 410 | logging.error("storcli encountered an error: " + stderr) 411 | raise RuntimeError("storcli command error") 412 | else: 413 | for line in stdout.splitlines(): 414 | match = re.search(vdNum+' INIT',line) 415 | if match != None: 416 | logging.info(line) 417 | status = re.search('Not in progress',line) 418 | if status != None: 419 | ready = True 420 | else: 421 | ready = False 422 | return ready 423 | -------------------------------------------------------------------------------- /src/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-krenn/TKperf/638b0837b97b32337314993feb52b5111aae0979/src/system/__init__.py --------------------------------------------------------------------------------