[
  {
    "path": "COPYING",
    "content": "\n                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README",
    "content": "ezFIO V1.0\r\n(C) Copyright 2015-18 HGST\r\nearle.philhower.iii@hgst.com\r\n\r\n------------------------------------------------------------------------\r\nezFIO is free software: you can redistribute it and/or modify\r\nit under the terms of the GNU General Public License as published by\r\nthe Free Software Foundation, either version 2 of the License, or\r\n(at your option) any later version.\r\n\r\nezFIO is distributed in the hope that it will be useful,\r\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\r\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\nGNU General Public License for more details.\r\n\r\nYou should have received a copy of the GNU General Public License\r\nalong with ezFIO.  If not, see <https://www.gnu.org/licenses/>.\r\n------------------------------------------------------------------------\r\n\r\nThis test script is intended to give a block-level based overview of\r\nSSD performance (SATA, SAS, and NVME) under real-world conditions by\r\nfocusing on sustained performance at different block sizes and queue\r\ndepths.  Both text-mode Linux and GUI and text-mode Windows versions\r\nare included.\r\n\r\nThe results of multiple tests are summarized into a single OpenDoc format\r\nspreadsheet, readable under OpenOffice, LibreOffice, or Microsoft Excel.\r\n\r\nFIO is required to perform the actual IO tests.  Please ensure the latest\r\nversion is installed, either from your operating system's repository or\r\nsources available at https://github.com/axboe/fio or precompiled for\r\nWindows at https://ci.appveyor.com/project/axboe/fio (for the GIT latest)\r\nor from https://www.bluestop.org/fio/ .\r\n\r\n(There seems to be an issue with FIO 3.1 under Windows that is not present\r\nunder earlier or later builds.  In a nutshell, the 1200 second sustained\r\nperformance test ends up running, under this version, for over 12 hours!\r\nWhile the final results are still good and the script continues, it does\r\nwaste a large amount of time and so I recommend avoiding the BlueStop 3.1\r\nbuild.  The CI.appveyor.com link above can be used to get current FIO\r\nhead builds instead.)\r\n\r\n\r\n------------------------------------------------------------------------\r\n\r\nA new --cluster option allows for running multiple clients in parallel,\r\nto allow testing performance of shared storage systems like SANs or\r\nAFAs.\r\n\r\nStart a \"fio --server\" job on all clients, then on one of them run\r\n./ezfio.py --cluster --drive host1:/dev/dr1,host2:/dev/dr2/... ...\r\n\r\nBasically add \"--cluster\" to the command line before the drive\r\noption, and in the drive option make a comma separated list of\r\nhostname:/path/to/storage .\r\n\r\nThe first host in the list must be the one you're currently running\r\nezfio from.  ezfio will try using the local system to collect\r\nappropriate system info on the first drive.\r\n\r\nIn the current implementation, all nodes/drives must be identical in\r\nsize.  There are no provisions for having volumes of differing sizes.\r\n\r\nAll other graphs and results should be the aggregate of the entire\r\ncluster, as reported by fio.\r\n\r\nex:\r\n\r\nStart up FIO servers on all systems to be tested\r\n(on host 1):\r\n  # fio --server &\r\n(on host 2):\r\n  # fio --server &\r\n(on host 3):\r\n  # fio --server &\r\n\r\nRun a benchmark run:\r\n(on host 1)\r\n  # ./ezfio.py --cluster --drive host1:/dev/nvme1n1,host2:/dev/nvme1n1,host3:/dev/nvme4n1\r\n\r\n------------------------------------------------------------------------\r\n\r\nezFIO got where it is today through the help of many users who filed\r\nbugs when things didn't work, or submitted patches to support new CPUs.\r\nPlease feel free to open issues or drop me a line if you have questions.\r\n\r\nSpecial thanks to @coolrecep (Recep Baltaş) who has spent literally days\r\ntracking down Windows issues.\r\n"
  },
  {
    "path": "combine.py",
    "content": "#!/usr/bin/python\n\n# ezfio 1.0\n# earle.philhower.iii@hgst.com\n#\n# ------------------------------------------------------------------------\n# ezfio is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 2 of the License, or\n# (at your option) any later version.\n#\n# ezfio is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with ezfio.  If not, see <http://www.gnu.org/licenses/>.\n# ------------------------------------------------------------------------\n#\n# Usage:   ./append.py --source <old.ods> --append <new.ods> --suffix <_new> --color <223344> --output <combined.ods>\n\n\nimport argparse\nimport base64\nimport datetime\nimport json\nimport os\nimport platform\nimport pwd\nimport re\nimport shutil\nimport socket\nimport subprocess\nimport sys\nimport threading\nimport time\nimport zipfile\n\ndef ParseArgs():\n    \"\"\"Parse command line options into globals.\"\"\"\n    global sourceODS, appendODS, destODS, suffix, color\n    parser = argparse.ArgumentParser(\n                 formatter_class=argparse.RawDescriptionHelpFormatter,\n    description=\"A tool to add a dataset to an existing ezFIO ODS file.\",\n    epilog=\"\")\n    parser.add_argument(\"--source\", \"-s\", dest = \"sourceODS\",\n        help=\"First ODS file with 1 or more test runs included\", required=True)\n    parser.add_argument(\"--append\", \"-a\", dest=\"appendODS\",\n        help=\"ODS file with tests to append to source\", required=True)\n    parser.add_argument(\"--suffix\", \"-x\", dest=\"suffix\",\n        help=\"Suffix to append to data tables from appended ODS\", required=True)\n    parser.add_argument(\"--color\", \"-c\", dest=\"color\",\n        help=\"Color to use for graphed data in appended ODS (rrggbb format)\", required=True)\n    parser.add_argument(\"--output\", \"-o\", dest=\"destODS\",\n        help=\"Location where results should be saved\", required=True)\n    args = parser.parse_args()\n    sourceODS = args.sourceODS\n    appendODS = args.appendODS\n    destODS = args.destODS\n    suffix = args.suffix\n    color = args.color\n\ndef GenerateCombinedODS():\n    \"\"\"Builds a new ODS spreadsheet w/graphs from generated test CSV files.\"\"\"\n\n    def GetContentXMLFromODS( odssrc ):\n        \"\"\"Extract content.xml from an ODS file, where the sheet lives.\"\"\"\n        ziparchive = zipfile.ZipFile( odssrc )\n        content = ziparchive.read(\"content.xml\")\n        content = content.replace(\"\\n\", \"\")\n        return content\n\n    def CSVtoXMLSheet(sheetName, csvName):\n        \"\"\"Replace a named sheet with the contents of a CSV file.\"\"\"\n        newt  = '<table:table table:name='\n        newt += '\"' + sheetName + '\"' + ' table:style-name=\"ta1\" > '\n        newt += '<table:table-column table:style-name=\"co1\" '\n        newt += 'table:default-cell-style-name=\"Default\"/>'\n        # Insert the rows, one entry at a time\n        with open(csvName) as f:\n            for line in f:\n                line = line.rstrip()\n                newt += '<table:table-row table:style-name=\"ro1\">'\n                for val in line.split(','):\n                    try:\n                        cell  = '<table:table-cell office:value-type=\"float\" '\n                        cell += 'office:value=\"' + str(float(val))\n                        cell += '\"><text:p>'\n                        cell += str(float(val)) + '</text:p></table:table-cell>'\n                    except: # It's not a float, so let's call it a string\n                        cell  = '<table:table-cell office:value-type=\"string\" '\n                        cell += '><text:p>'\n                        cell += str(val) + '</text:p></table:table-cell>'\n                    newt += cell\n                newt += '</table:table-row>'\n            f.close()\n        # Close the tags\n        newt += '</table:table>'\n        return newt\n\n    def AppendSheetFromCSV(sheetName, csvName, xmltext):\n        \"\"\"Add a new sheet to the XML from the CSV file.\"\"\"\n        newt = CSVtoXMLSheet(sheetName, csvName)\n\n        # Replace the XML using lazy string matching\n        searchstr  = '<table:named-expressions/>'\n        return re.sub(searchstr, newt + searchstr, xmltext)\n\n    def UpdateContentXMLToODS_text( odssrc, odsdest, xmltext ):\n        \"\"\"Replace content.xml in an ODS w/an in-memory copy and write new.\n\n        Replace content.xml in an ODS file with in-memory, modified copy and\n        write new ODS. Can't just copy source.zip and replace one file, the\n        output ZIP file is not correct in many cases (opens in Excel but fails\n        ODF validation and LibreOffice fails to load under Windows).\n\n        Also strips out any binary versions of objects and the thumbnail,\n        since they are no longer valid once we've changed the data in the\n        sheet.\n        \"\"\"\n        global suffix\n\n        if os.path.exists(odsdest):\n            os.unlink(odsdest)\n\n        # Windows ZipArchive will not use \"Store\" even with \"no compression\"\n        # so we need to have a mimetype.zip file encoded below to match spec:\n        mimetypezip = \"\"\"\nUEsDBAoAAAAAAOKbNUiFbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz\naXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsBAj8ACgAAAAAA4ps1SIVsOYouAAAALgAAAAgA\nJAAAAAAAAACAAAAAAAAAAG1pbWV0eXBlCgAgAAAAAAABABgAAAyCUsVU0QFH/eNMmlTRAUf940ya\nVNEBUEsFBgAAAAABAAEAWgAAAFQAAAAAAA==\n\"\"\"\n        zipbytes = base64.b64decode( mimetypezip )\n        with open(odsdest, 'wb') as f:\n            f.write(zipbytes)\n\n        zasrc = zipfile.ZipFile(odssrc, 'r')\n        zadst = zipfile.ZipFile(odsdest, 'a', zipfile.ZIP_DEFLATED)\n        for entry in zasrc.namelist():\n            if entry == \"mimetype\":\n                continue\n            elif entry.endswith('/') or entry.endswith('\\\\'):\n                continue\n            elif entry == \"content.xml\":\n                zadst.writestr( \"content.xml\", xmltext)\n            elif (\"Object\" in entry) and (\"content.xml\" in entry):\n                # Remove <table:table table:name=\"local-table\"> table\n                rdbytes = zasrc.read(entry)\n                outbytes = re.sub('<table:table table:name=\"local-table\">.*</table:table>', \"\", rdbytes)\n                # Add in extra chart series following existing format...\n                searchStr = '<chart:series .*</chart:series>'\n                match = re.search(searchStr, outbytes);\n                addl = \"\"\n                if match:\n                    fmt = match.group(0)\n                    addl = fmt;\n                    for sheet in [ \"Tests\", \"Timeseries\", \"Exceedance\"]:\n                        addl = re.sub( sheet, sheet+suffix, addl )\n                    # Remove any existing label and add updated one\n                    addl = re.sub(\"loext:label-string=\\\".*?\\\"\" , \"\", addl );\n                    addl = re.sub (\"<chart:series \", \"<chart:series \" + \"loext:label-string=\\\"\"+suffix+\"\\\" \", addl) \n                    styleMatch = re.search(\"chart:style-name=\\\"(.)*?\\\"\", fmt)\n                    if styleMatch:\n                        styleName = re.sub(\"chart:style-name=\\\"\", \"\", styleMatch.group(0) )\n                        styleName = re.sub(\"\\\".*\", \"\", styleName)\n                        # Change the style requested in new one...\n                        addl = re.sub( \"\\\"\" + styleName + \"\\\"\", \"\\\"\" + styleName + suffix + \"\\\"\", addl )\n                        # And patch in the new chart:series entry\n                        outbytes = re.sub ( searchStr, fmt + addl, outbytes )\n                        # Now make the new style...\n                        oldStyleMatch = re.search( \"<style:style style:name=\\\"\" + styleName + \".*?</style:style>\" , outbytes )\n                        if oldStyleMatch:\n                            oldStyle = oldStyleMatch.group(0)\n                            newStyle = re.sub( \"\\\"\" + styleName + \"\\\"\", \"\\\"\" + styleName + suffix + \"\\\"\", oldStyle)\n                            # Change the embedded color:\n                            newStyle = re.sub( \"svg:stroke-color=\\\"#.*?\\\"\", \"svg:stroke-color=\\\"#\" + color + \"\\\"\", newStyle )\n                            # Add in the new style...\n                            outbytes = re.sub ( oldStyle, oldStyle + newStyle, outbytes )\n                        # Add legend if it doesn't exist\n                        legendMatch = re.search(\"<chart:legend .*?/>\", outbytes)\n                        if not legendMatch:\n                            # Put in hardcoded one...looks like junk, but can be tweaked by user in application\n                            legend = \"<chart:legend chart:legend-position=\\\"bottom\\\" svg:x=\\\"0.000cm\\\" svg:y=\\\"0.000cm\\\" style:legend-expansion=\\\"wide\\\" chart:style-name=\\\"ch3\\\"/>\";\n                            outbytes = re.sub (\"</chart:title>\", \"</chart:title>\" + legend, outbytes )\n                zadst.writestr(entry, outbytes)\n            elif entry == \"META-INF/manifest.xml\":\n                # Remove ObjectReplacements from the list\n                rdbytes = zasrc.read(entry)\n                outbytes = \"\"\n                lines = rdbytes.split(\"\\n\")\n                for line in lines:\n                    if not ( (\"ObjectReplacement\" in line) or (\"Thumbnails\" in line) ):\n                        outbytes = outbytes + line + \"\\n\"\n                zadst.writestr(entry, outbytes)\n            elif (\"Thumbnails\" in entry) or (\"ObjectReplacement\" in entry):\n                # Skip binary versions\n                continue\n            else:\n                rdbytes = zasrc.read(entry)\n                zadst.writestr(entry, rdbytes)\n        zasrc.close()\n        zadst.close()\n\n\n    global sourceODS, appendODS, destODS\n    \n    # First rename and append the extra data sheets\n    xmlsrc = GetContentXMLFromODS( sourceODS )\n    xmlapp = GetContentXMLFromODS( appendODS )\n    for tableName in [ \"Tests\", \"Timeseries\", \"Exceedance\" ]:\n        searchStr = '<table:table table:name=\"' + tableName + '\".*?</table:table>'\n        sheetMatch = re.search(searchStr, xmlapp);\n        if sheetMatch:\n            sheet = sheetMatch.group(0)\n            # Rename the table\n            sheet = re.sub( '\"' + tableName + '\"', '\"' + tableName + suffix + '\"', sheet);\n            # Stick it right before the end of the list\n            searchStr  = '<table:named-expressions/>'\n            xmlsrc = re.sub(searchStr, sheet + searchStr, xmlsrc)\n    UpdateContentXMLToODS_text( sourceODS, destODS, xmlsrc )\n\nsourceODS = \"\"\nappendODS = \"\"\ndestODS = \"\"\nsuffix = \"\"\ncolor = \"\"\n\nif __name__ == \"__main__\":\n    ParseArgs()\n    GenerateCombinedODS()\n\n"
  },
  {
    "path": "ezfio.bat",
    "content": "@echo off\r\nREM Start EZFIO.PS1 in an elevated PowerShell interpreter.\r\nREM Here be dragons.\r\nREM First start a standard powershell and use it's Start-Process cmdlet\r\nREM to start *another* powershell, this one as administrator, to interpret\r\nREM the script.  Care must be taken to properly quote the path to the script.\r\n\r\nset GO='%cd%\\ezfio.ps1'\r\npowershell -Command \"$p = new-object System.Diagnostics.ProcessStartInfo 'PowerShell'; $p.Arguments = {-WindowStyle hidden -Command \". %GO%\"}; $p.Verb = 'RunAs'; [System.Diagnostics.Process]::Start($p) | out-null;\"\r\n"
  },
  {
    "path": "ezfio.ps1",
    "content": "# ezfio 1.0\r\n# earle.philhower.iii@hgst.com\r\n#\r\n# ------------------------------------------------------------------------\r\n# ezfio is free software: you can redistribute it and/or modify\r\n# it under the terms of the GNU General Public License as published by\r\n# the Free Software Foundation, either version 2 of the License, or\r\n# (at your option) any later version.\r\n#\r\n# ezfio is distributed in the hope that it will be useful,\r\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n# GNU General Public License for more details.\r\n#\r\n# You should have received a copy of the GNU General Public License\r\n# along with ezfio.  If not, see <http://www.gnu.org/licenses/>.\r\n# ------------------------------------------------------------------------\r\n#\r\n# Usage:   ezfio.ps1 -drive {physicaldrive number}\r\n# Example: ezfio.ps1 -drive 3\r\n#\r\n# When no parameters are specified, the script will provide usage info\r\n# as well as a list of attached PhysicalDrives\r\n#\r\n# This script requires Administrator privileges so must be run from\r\n# a PowerShell session started with \"Run as Administrator.\"\r\n#\r\n# If Windows errors with, \"...cannot be loaded because running scripts is\r\n# disabled on this system....\" you need to run the following line to enable\r\n# execution of local PowerShell scripts:\r\n#       Set-ExecutionPolicy -scope CurrentUser RemoteSigned\r\n#\r\n# Please be sure to have FIO installed, or you will be prompted to install\r\n# and re-run the script.\r\n\r\n\r\nparam (\r\n    [string]$drive = \"none\",\r\n    [string]$outDir = \"none\",\r\n    [int]$util = 100,\r\n    [switch]$help,\r\n    [switch]$yes,\r\n    [switch]$nullio,\r\n    [switch]$fastprecond,\r\n    [switch]$quickie\r\n)\r\n\r\n\r\nAdd-Type -Assembly System.IO.Compression\r\nAdd-Type -Assembly System.IO.Compression.FileSystem\r\nAdd-Type -AssemblyName PresentationFramework, System.Windows.Forms\r\nAdd-Type -AssemblyName PresentationCore\r\n\r\nChdir (Split-Path $script:MyInvocation.MyCommand.Path)\r\n\r\nfunction WindowFromXAML( $xaml, $prefix )\r\n{\r\n    # Create a WPF window from XAML from DevStudio\r\n    $xaml = $xaml -replace 'mc:Ignorable=\"d\"', ''\r\n    $xaml = $xaml -replace \"x:N\", 'N'\r\n    $xaml = $xaml -replace '^<Win.*', '<Window'\r\n    $xml = [xml]$xaml\r\n    $reader = (New-Object System.Xml.XmlNodeReader $xml)\r\n    try{ $window = [Windows.Markup.XamlReader]::Load( $reader ) }\r\n    catch { Write-Host \"Unable to load XAML for window.\"; return $null; }\r\n    # Set the variables locally in the calling function.  Please forgive me.\r\n    $xml.SelectNodes(\"//*[@Name]\") |\r\n      %{Set-Variable -Name \"$($prefix)_$($_.Name)\" -Value $window.FindName($_.Name) -Scope 1}\r\n    return $window\r\n}\r\n\r\nfunction CheckAdmin()\r\n{\r\n    # Check that we have root privileges for disk access, abort if not.\r\n    if ( -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] \"Administrator\") ) {\r\n        [System.Windows.Forms.MessageBox]::Show( \"Administrator privileges are required for low-level disk access.`nPlease restart this script as Administrator to continue.\", \"Fatal Error\", 0, 48 ) | Out-Null\r\n        exit\r\n    }\r\n}\r\n\r\nfunction FindFIO()\r\n{\r\n    # Try the path to the FIO executable, return path or exit.\r\n    if ( -not (Get-Command fio.exe) ) {\r\n        $ret = [System.Windows.Forms.MessageBox]::Show( \"FIO is required to run IO tests. Would you like to install?\", \"FIO Not Detected\", 4, 32 )\r\n        if ($ret -eq \"yes\" ) {\r\n            Start-Process \"https://www.bluestop.org/fio/\"\r\n        }\r\n        exit\r\n    } else {\r\n        $global:fio = (Get-Command fio.exe).Path\r\n    }\r\n}\r\n\r\nfunction CheckFIOVersion()\r\n{\r\n    # Check we have a version of FIO we can use.\r\n    try {\r\n        $global:fioVerString = ( . $global:fio \"--version\" )\r\n        $fiov = ( . $global:fio \"--version\" ).Split('-')[1].Split('.')[0]\r\n        if ([int]$fiov -lt 2) {\r\n            $err = \"ERROR! FIO version \" + (. $global:fio \"--version\") + \" is unsupported. Version 2.0 or later is required\"\r\n            if ($global:testmode -eq \"gui\") {\r\n                [System.Windows.Forms.MessageBox]::Show( $err, \"Fatal Error\", 0, 48 ) | Out-Null\r\n            } else {\r\n                Write-Error $err\r\n            }\r\n            exit 1\r\n        }\r\n    } catch {\r\n        $err = \"ERROR! Unable to determine FIO version.  Version 2.0 or later is required.\"\r\n        if ($global:testmode -eq \"gui\") {\r\n            [System.Windows.Forms.MessageBox]::Show( $err, \"Fatal Error\", 0, 48 ) | Out-Null\r\n        } else {\r\n            Write-Error $err\r\n        }\r\n        exit 1\r\n    }\r\n\r\n    try {\r\n        $out = (. $global:fio \"--parse-only\" \"--output-format=json+\")\r\n        if ($LastExitCode -eq 0 ) {\r\n            $global:fioOutputFormat = \"json+\"\r\n        }\r\n    } catch {\r\n        # Nothing, we can't make exceedance\r\n    }\r\n}\r\n\r\nfunction ParseArgs()\r\n{\r\n    # Set the global values to the param() values, so that Parse() can see them\r\n\r\n    function IntroDialog()\r\n    {\r\n        # Gets user test parameters if not specified on the command line\r\n        $xaml = @'\r\n<Window x:Class=\"Window2\"\r\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\r\n    Title=\"ezFIO Drive Selection\" ResizeMode=\"NoResize\" Height=\"281\" Width=\"456\">\r\n    <Grid>\r\n        <Label Content=\"Drive to test:\" HorizontalAlignment=\"Left\" Margin=\"24,16,0,0\" VerticalAlignment=\"Top\"/>\r\n        <ComboBox x:Name=\"driveList\" HorizontalAlignment=\"Left\" Margin=\"115,20,0,0\" VerticalAlignment=\"Top\" Width=\"218\"/>\r\n        <Button x:Name=\"startTest\" Content=\"Start Test\" HorizontalAlignment=\"Left\" Margin=\"358,79,0,0\" VerticalAlignment=\"Top\" Width=\"75\"/>\r\n        <Button x:Name=\"exit\" Content=\"Exit\" HorizontalAlignment=\"Left\" Margin=\"358,125,0,0\" VerticalAlignment=\"Top\" Width=\"75\"/>\r\n        <GroupBox Header=\"Information\" HorizontalAlignment=\"Left\" Margin=\"24,55,0,0\" VerticalAlignment=\"Top\" Height=\"109\" Width=\"309\">\r\n            <Grid>\r\n                <Label Content=\"Model:\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Top\" Margin=\"2,0,0,0\"/>\r\n                <Label x:Name=\"modelName\" Content=\"Happy NVME\" HorizontalAlignment=\"Left\" Margin=\"50,0,0,0\" VerticalAlignment=\"Top\"/>\r\n                <Label Content=\"Serial:\" HorizontalAlignment=\"Left\" Margin=\"8,25,0,0\" VerticalAlignment=\"Top\"/>\r\n                <Label x:Name=\"serial\" Content=\"NVME001\" HorizontalAlignment=\"Left\" Margin=\"50,25,0,0\" VerticalAlignment=\"Top\"/>\r\n                <Label Content=\"Size:\" HorizontalAlignment=\"Left\" Margin=\"15,50,0,0\" VerticalAlignment=\"Top\"/>\r\n                <Label x:Name=\"sizeGB\" Content=\"100GB\" HorizontalAlignment=\"Left\" Margin=\"50,50,0,0\" VerticalAlignment=\"Top\"/>\r\n            </Grid>\r\n        </GroupBox>\r\n        <GroupBox Header=\"WARNING! WARNING! WARNING!\" HorizontalAlignment=\"Left\" Margin=\"24,176,0,0\" VerticalAlignment=\"Top\" Width=\"398\">\r\n            <Label >\r\n                <TextBlock TextWrapping=\"WrapWithOverflow\" Width=\"376\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\">All data on the selected drive will be destroyed by the test.  Please make sure there are no mounted filesystems or data on the drive.</TextBlock>\r\n            </Label>\r\n        </GroupBox>\r\n    </Grid>\r\n</Window>\r\n'@\r\n\r\n        $intro = WindowFromXAML $xaml 'intro'\r\n        $intro.Icon = $global:iconBitmap\r\n\r\n        $pd = @{}\r\n\r\n        $intro.add_Loaded( {\r\n            $intro.Activate()\r\n            $intro_driveList.Focus()\r\n\r\n            $global:physDrive = $null\r\n            # Populate the physicaldrive list\r\n            $drives = Get-WmiObject -query \"SELECT * from Win32_DiskDrive\" | Sort-Object\r\n            foreach ( $drive in $drives ) {\r\n                $idx = $intro_driveList.Items.Add( $drive.DeviceID )\r\n                $pd.Add( $idx, $drive )\r\n            }\r\n            $intro_driveList.SelectedIndex = 0\r\n            $drive = $pd.Get_Item( 0 )\r\n            $intro_modelName.Content = $drive.Model.Trim()\r\n            if ($drive.SerialNumber -ne $null) { $intro_serial.Content = $drive.SerialNumber.Trim() }\r\n            else { $intro_serial.Content = \"UNKNOWN\" }\r\n            $intro_sizeGB.Content = [string]::Format( \"{0} GB\", [int]($drive.Size/1000000000) )\r\n\r\n            $intro_driveList.add_SelectionChanged( {\r\n                $idx = $intro_driveList.SelectedIndex\r\n                $drive = $pd.Get_Item( $idx )\r\n                $intro_modelName.Content = $drive.Model.Trim()\r\n                if ($drive.SerialNumber -ne $null) { $intro_serial.Content = $drive.SerialNumber.Trim() }\r\n                else { $intro_serial.Content = \"UNKNOWN\" }\r\n                $intro_sizeGB.Content = [string]::Format( \"{0} GB\", [int]($drive.Size/1000000000) )\r\n            } )\r\n\r\n            $intro_startTest.add_Click( {\r\n                $idx = $intro_driveList.SelectedIndex\r\n                $drive = $pd.Get_Item( $idx )\r\n                $global:physDrive = $drive.DeviceID\r\n                $intro.dialogResult = $true\r\n                $intro.Close()\r\n            } )\r\n\r\n            $intro_exit.add_Click( { $intro.Close() } )\r\n\r\n        } )\r\n\r\n        $intro.ShowDialog()\r\n    }\r\n\r\n    # Parse command line options into globals.\r\n    function usage()\r\n    {\r\n        # How to use the script, and some handy info on current drives\r\n        $scriptname = split-path $global:scriptName -Leaf\r\n        \"ezfio, an in-depth IO tester for NVME devices\"\r\n        \"WARNING: All data on any tested device will be destroyed!`n\"\r\n        \"Usage: \"\r\n        [string]::Format(\"    .\\{0} -drive <PhysicalDiskNumber> [-util <1..100>] [-outDir <path>] [-nullIO]\", $scriptname)\r\n        [string]::Format(\"EX: .\\{0} -drive 2 -util 100`n\", $scriptname)\r\n        \"PhysDrive is the ID number of the \\\\PhysicalDrive to test\"\r\n        \"Usage is the percent of total size to test (100%=default)`n\"\r\n\r\n        \"`nPhysical disks:\"\r\n        $drives=Get-WmiObject -query \"SELECT * from Win32_DiskDrive\" | Sort-Object\r\n        foreach ( $drive in $drives ) {\r\n            if ($drive.SerialNumber -ne $null) {\r\n                [string]::Format( \"{0}. {1}, Serial: {2}, Size: {3}GB\",\r\n                    $drive.DeviceID.substring(17), $drive.Model.Trim(),\r\n                    $drive.SerialNumber.Trim(), [int]($drive.Size/1000000000) )\r\n            } else {\r\n                [string]::Format( \"{0}. {1}, Size: {2}GB\",\r\n                    $drive.DeviceID.substring(17), $drive.Model.Trim(),\r\n                    [int]($drive.Size/1000000000) )\r\n            }\r\n        }\r\n        exit\r\n    }\r\n\r\n    if ($help) { usage }\r\n\r\n    if (($util -lt 1) -or ($util -gt 100)) {\r\n        \"ERROR: Utilization must be between 1 and 100.`n\"\r\n        usage\r\n    } else {\r\n        $global:utilization = $util\r\n    }\r\n\r\n    if ( $outDir -eq \"none\" ) {\r\n        $global:outDir = \"${PWD}\"\r\n    } else {\r\n        $global:outDir = \"$outDir\"\r\n    }\r\n\r\n    if ( $drive -ne \"none\" ) {\r\n        $global:testMode = \"cli\"\r\n        if ( $drive -notin (Get-Disk).Number ){\r\n            Write-Error \"The drive number `\"$drive`\" you entered does not exist.`n`n\"\r\n            usage\r\n        }\r\n        $global:physDrive = \"\\\\.\\PhysicalDrive$drive\"\r\n    } else {\r\n        $global:testmode = \"gui\"\r\n        $ok = IntroDialog\r\n        if (-not ($ok) ) {\r\n            exit\r\n        }\r\n    }\r\n\r\n    $global:yes = $yes\r\n    if ( -not $nullio ) {\r\n        $global:ioengine = \"windowsaio\"\r\n    } else {\r\n        $global:ioengine = \"null\"\r\n    }\r\n    $global:quickie = $quickie\r\n    $global:fastPrecond = $fastprecond\r\n\r\n    # Do a sanity check that the selected drive does not show as a local drive letter\r\n    Get-WMIObject Win32_LogicalDisk | Foreach-Object {\r\n        $did = (Get-WmiObject -Query \"Associators of {Win32_LogicalDisk.DeviceID='$($_.DeviceID)'} WHERE ResultRole=Antecedent\").Path\r\n        $dl = $_.DeviceID\r\n        if ($did.RelativePath) {\r\n            $part = $did.RelativePath.Split('\"')[1]\r\n            $pd = $part.split(',')[0].split('#')[1]\r\n            if ($global:physDrive.ToLower() -eq \"\\\\.\\physicaldrive$pd\") {\r\n                if ($global:testmode -eq \"cli\") {\r\n                    Write-Error \"ERROR! Drive '$global:physdrive' is mounted as drive '$dl'!\"\r\n                    Write-Error \"Aborting run, cannot run on mounted filesystem.\"\r\n                    exit\r\n                } else {\r\n                    [System.Windows.Forms.MessageBox]::Show( \"ERROR! Drive '$global:physdrive' is mounted as drive '$dl'!`nAborting run, cannot run on mounted filesystem.\", \"Fatal Error\", 0, 48 ) | Out-Null\r\n                    exit\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n}\r\n\r\n\r\nfunction CollectSystemInfo()\r\n{\r\n    # Collect some OS and CPU information.\r\n\r\n    # May want to put a window up while this happens.  GWMI is very slow\r\n\r\n    $procs = [array](Get-WmiObject -class win32_processor) # Single-socket gives object, so coerce into array to match multisocket\r\n    $global:cpu = $procs[0].Name.Trim()\r\n    $cpuCount = ($procs[0].NumberOfCores).Count\r\n    $cpuCores = ($procs[0] | Where DeviceID -eq \"CPU0\" ).NumberOfLogicalProcessors\r\n    $global:cpuCores = $cpuCores * $cpuCount\r\n    $global:cpuFreqMHz = ($procs[0] | Where DeviceID -eq \"CPU0\").MaxClockSpeed\r\n    $os = Get-WmiObject Win32_OperatingSystem\r\n    $global:uname = $os.Caption.Trim() + \" - Build \" + $os.BuildNumber.Trim() + \" - ServicePack \" + $os.ServicePackMajorVersion + \".\" + $os.ServicePackMinorVersion\r\n\r\n    # Check if we're running in high-performance mode\r\n    $plan = Get-WmiObject -Class win32_powerplan -Namespace root\\cimv2\\power -Filter \"IsActive=True\"\r\n    if (-not ($plan.InstanceID -like \"*8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c*\")) {\r\n        function SetHighPerformance() {\r\n            \"Setting High Performance power scheme via POWERCFG\"\r\n            powercfg /setactive \"8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c\"\r\n        }\r\n        if ($global:yes) {\r\n            SetHighPerformance\r\n        } elseif ($testmode -ne \"gui\") {\r\n            \"-\" * 75\r\n            \"Power mode is not currently set to High Performance.\"\r\n            \"This may result in lowered test results.\"\r\n            $cont = Read-Host \"Would you like to enable this power setting now? (y/n)\"\r\n            \"-\" * 75\r\n            if ($cont -ne \"\") {\r\n                if ($cont.Substring(0, 1).ToLower() -eq \"y\" ) {\r\n                    SetHighPerformance\r\n                }\r\n            }\r\n        } else {\r\n            $ret = [System.Windows.Forms.MessageBox]::Show(\r\n                \"System power mode not set to High Performance.`nThis may result in lowered test results.`nWould you like to enable High Performance Mode now?\",\r\n                \"Verify Performance Mode\", 4, 32)\r\n            if ($ret -eq\"yes\" ) {\r\n                SetHighPerformance\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n\r\nfunction VerifyContinue()\r\n{\r\n    # User's last chance to abort the test.  Exit if they don't agree.\r\n    if ( -not $global:yes ) {\r\n        if ($testMode -ne \"gui\") { # text-mode prompt since we're running with command line options\r\n            \"-\" * 75\r\n            \"WARNING! \" * 9\r\n            \"THIS TEST WILL DESTROY ANY DATA AND FILESYSTEMS ON $global:physDrive\"\r\n            $cont = Read-Host \"Please type the word `\"yes`\" and hit return to continue, or anything else to abort\"\r\n            \"-\" * 75\r\n            if ( $cont -ne \"yes\" ) {\r\n                \"Performance test aborted, drive is untouched.\"\r\n                exit\r\n            }\r\n        } else {\r\n            # Do it in a messagebox since we're running GUI-wise\r\n            $ret = [System.Windows.Forms.MessageBox]::Show(\r\n                \"Test selected to run on $global:physDrive.`nALL DATA WILL BE ERASED ON THIS DRIVE`nContinue with testing?\",\r\n                \"Verify the device to test\", 4)\r\n            if ($ret -ne \"yes\" ) {\r\n                \"Performance test aborted, drive is untouched.\"\r\n                exit\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\nfunction CollectDriveInfo()\r\n{\r\n    # Get important device information, exit if not possible.\r\n    # We absolutely need this information\r\n    $global:physDriveBase = ([io.fileinfo]$global:physDrive).BaseName\r\n    $global:physDriveNo = $global:physDrive.Substring(17)\r\n    $global:physDriveBytes=(GET-WMIOBJECT win32_diskdrive | where DeviceID -eq $global:physDrive).Size\r\n    $global:physDriveGB=[long]($global:physDriveBytes/(1000*1000*1000))\r\n    $global:physDriveGiB=[long]($global:physDriveBytes/(1024*1024*1024))\r\n    $global:testcapacity = [long](($global:physDriveGiB * $global:utilization) / 100)\r\n    # This is just nice to have\r\n    $drive = (Get-Disk | Where-Object { $_.Number -eq $global:physDriveNo })\r\n    $global:model = $drive.Model.ToString().Trim()\r\n    if ($drive.SerialNumber -ne $null ) { $global:serial = $drive.SerialNumber.ToString().Trim() }\r\n    else { $global:serial = \"UNKNOWN\" }\r\n}\r\n\r\n\r\n# Set up names for all output/input files, place headers on CSVs.\r\nfunction CSVInfoHeader {\r\n    if ($global:fastPrecond -eq $false) { $prefix = \"\" }\r\n    else { $prefix = \"FASTPRECOND-\" }\r\n\r\n    #Headers to the CSV file (ending up in the ODS at the test end)\r\n    \"Drive,$prefix$global:physDrive\"\r\n    \"Model,$prefix$global:model\"\r\n    \"Serial,$prefix$global:serial\"\r\n    \"AvailCapacity,$prefix$global:physDriveGiB,GiB\"\r\n    \"TestedCapacity,$prefix$global:testcapacity,GiB\"\r\n    \"CPU,$prefix$global:cpu\"\r\n    \"Cores,$prefix$global:cpuCores\"\r\n    \"Frequency,$prefix$global:cpuFreqMHz\"\r\n    \"OS,$prefix$global:uname\"\r\n    \"FIOVersion,$prefix$global:fioVerString\"\r\n}\r\n\r\nfunction SetupFiles()\r\n{\r\n    # Datestamp for run output files\r\n    $global:ds=(Get-Date).ToString(\"yyyy-MM-dd_HH-mm-ss\")\r\n\r\n    # The unique suffix we generate for all output files\r\n    $suffix=\"${global:physDriveGB}GB_${global:cpuCores}cores_${global:cpuFreqMHz}MHz_${global:physDriveBase}_${env:computername}_${global:ds}\"\r\n\r\n    # Need to worry about normalizing passed in directory names, or else non-absolute output paths will resolve to c:\\windows\\system32\\...\r\n    if ( -not ( Test-Path -Path $global:outDir ) ) {\r\n        # New-Item used PWD, so we're OK here\r\n        New-Item -ItemType directory -Path $global:outDir | Out-Null\r\n    }\r\n    # Now resolve to c:\\... path and put back to global for sanity.\r\n    $global:outDir = Resolve-Path $global:outDir\r\n\r\n    # The \"details\" directory contains the raw output of each FIO run\r\n    $global:details = \"${global:outDir}\\details_${suffix}\"\r\n    # The \"details\" directory contains the raw output of each FIO run\r\n    if ( Test-Path -Path $global:details ) {\r\n        Remove-Item -Recurse -Force $global:details | Out-Null\r\n    }\r\n    New-Item -ItemType directory -Path $global:details | Out-Null\r\n\r\n    # Copy this script into it for posterity\r\n    Copy-Item $scriptName $global:details\r\n\r\n    # Files we're going to generate, encode some system info in the names\r\n    # If the output files already exist, erase them\r\n    $global:testcsv = \"${global:details}\\ezfio_tests_${suffix}.csv\"\r\n    if (Test-Path $global:testcsv) { Remove-Item $global:testcsv }\r\n    CSVInfoHeader > $global:testcsv\r\n    \"Type,Write %,Block Size,Threads,Queue Depth/Thread,IOPS,Bandwidth (MB/s),Read Latency (us),Write Latency (us)\" >> $global:testcsv\r\n    $global:timeseriescsv =\"${global:details}\\ezfio_timeseries_${suffix}.csv\"\r\n    $global:timeseriesclatcsv =\"${global:details}\\ezfio_timeseriesclat_${suffix}.csv\"\r\n    $global:timeseriesslatcsv =\"${global:details}\\ezfio_timeseriesslat_${suffix}.csv\"\r\n    if (Test-Path $global:timeseriescsv) { Remove-Item $global:timeseriescsv }\r\n    if (Test-Path $global:timeseriesclatcsv) { Remove-Item $global:timeseriesclatcsv }\r\n    if (Test-Path $global:timeseriesslatcsv) { Remove-Item $global:timeseriesslatcsv }\r\n    CSVInfoHeader > $global:timeseriescsv\r\n    CSVInfoHeader > $global:timeseriesclatcsv\r\n    CSVInfoHeader > $global:timeseriesslatcsv\r\n    \"IOPS\" >> $global:timeseriescsv  # Add IOPS header\r\n    \"CLAT-read,CLAT-write\" >> $global:timeseriesclatcsv\r\n    \"SLAT-read,SLAT-write\" >> $global:timeseriesslatcsv\r\n\r\n    # ODS input and output files\r\n    $global:odssrc = \"${PWD}\\original.ods\"\r\n    $global:odsdest = \"${global:outDir}\\ezfio_results_${suffix}.ods\"\r\n    if (Test-Path $global:odsdest) { Remove-Item $global:odsdest }\r\n}\r\n\r\n\r\nfunction TestName ($seqrand, $wmix, $bs, $threads, $iodepth)\r\n{\r\n    # Return full path and filename prefix for test of specified params\r\n    $testfile  = $global:details + \"\\Test\" + $seqrand + \"_w\" + [string]$wmix\r\n    $testfile += \"_bs\" + [string]$bs + \"_threads\" + [string]$threads + \"_iodepth\"\r\n    $testfile += [string]$iodepth + \"_\" + $global:physDriveBase + \".out\"\r\n    return $testfile\r\n}\r\n\r\n# The actual functions that run FIO, in a string so that we can do a Start-Job using it.\r\n$global:jobutils = @'\r\n\r\nfunction TestName ($seqrand, $wmix, $bs, $threads, $iodepth)\r\n{\r\n    # Return full path and filename prefix for test of specified params\r\n    $testfile  = $details + \"\\Test\" + $seqrand + \"_w\" + [string]$wmix\r\n    $testfile += \"_bs\" + [string]$bs + \"_threads\" + [string]$threads + \"_iodepth\"\r\n    $testfile += [string]$iodepth + \"_\" + $physDriveBase + \".out\"\r\n    return $testfile\r\n}\r\n\r\nfunction SequentialConditioning\r\n{\r\n    # Sequentially fill the complete capacity of the drive once.\r\n    # Note that we can't use regular test runner because this test needs\r\n    # to run for a specified # of bytes, not a specified # of seconds.\r\n    if ( $quickie ) {\r\n        $size = \"1G\"\r\n    } else {\r\n        $size = \"${testcapacity}G\"\r\n    }\r\n    . $fio \"--name=SeqCond\" \"--readwrite=write\" \"--bs=128k\" \"--ioengine=$ioengine\" \"--iodepth=64\" \"--direct=1\" \"--filename=$physDrive\" \"--size=$size\" \"--thread\" | Out-Null\r\n    if ( $LastExitCode -ne 0 ) {\r\n        Write-Output \"ERROR\" \"ERROR\" \"ERROR\"\r\n    } else {\r\n        Write-Output \"DONE\" \"DONE\" \"DONE\"\r\n    }\r\n}\r\n\r\nfunction RandomConditioning\r\n{\r\n    # Randomly write entire device for the full capacity\r\n    # Note that we can't use regular test runner because this test needs\r\n    # to run for a specified # of bytes, not a specified # of seconds.\r\n    if ( $quickie ) {\r\n        $size = \"1G\"\r\n    } else {\r\n        $size = \"${testcapacity}G\"\r\n    }\r\n    . $fio \"--name=RandCond\" \"--readwrite=randwrite\" \"--bs=4k\" \"--invalidate=1\" \"--end_fsync=0\" \"--group_reporting\" \"--direct=1\" \"--filename=$physDrive\"  \"--size=$size\" \"--ioengine=$ioengine\" \"--iodepth=256\" \"--norandommap\" \"--randrepeat=0\" \"--thread\" | Out-Null\r\n    if ( $LastExitCode -ne 0 ) {\r\n        Write-Output \"ERROR\" \"ERROR\" \"ERROR\"\r\n    } else {\r\n        Write-Output \"DONE\" \"DONE\" \"DONE\"\r\n    }\r\n}\r\n\r\n# Taken from fio_latency2csv.py\r\nfunction plat_idx_to_val( $idx, $FIO_IO_U_PLAT_BITS, $FIO_IO_U_PLAT_VAL )\r\n{\r\n    # MSB <= (FIO_IO_U_PLAT_BITS-1), cannot be rounded off. Use\r\n    # all bits of the sample as index\r\n    if ($idx -lt ($FIO_IO_U_PLAT_VAL -shl 1)) {\r\n        return $idx\r\n    }\r\n    # Find the group and compute the minimum value of that group\r\n    $error_bits = ($idx -shr $FIO_IO_U_PLAT_BITS) - 1\r\n    $base = 1 -shl ($error_bits + $FIO_IO_U_PLAT_BITS)\r\n    # Find its bucket number of the group\r\n    $k = $idx % $FIO_IO_U_PLAT_VAL\r\n    # Return the mean of the range of the bucket\r\n    return ($base + (($k + 0.5) * (1 -shl $error_bits)))\r\n}\r\n\r\nfunction WriteExceedance($j, $rdwr, $outfile)\r\n{\r\n    # Generate an exceedance CSV for read or write from JSON output.\r\n    if ($fioOutputFormat -eq \"json\") {\r\n        return # This data not present in JSON format, only JSON+\r\n    }\r\n    $ios = $j.jobs[0].$rdwr.total_ios\r\n    if ( $ios -gt 0 ) {\r\n        $runttl = 0;\r\n        # FIO 2.99 changed this to use saner latency bucketing, no semi-log needed\r\n        if ($j.jobs[0].$rdwr.clat_ns) {\r\n            # This is very inefficient, but need to convert from object.property's to sorted ints...\r\n            $lat_ns = @()\r\n            foreach ($n in ((Get-Member -inputObject $j.jobs[0].$rdwr.clat_ns.bins -MemberType Properties).name) ) {\r\n                $lat_ns += [long]$n\r\n            }\r\n            foreach ($b in ($lat_ns | sort-object)) {\r\n                $lat_us = [float]($b) / 1000.0\r\n                $cnt = [int]$j.jobs[0].$rdwr.clat_ns.bins.$b\r\n                $runttl += $cnt\r\n                $pctile = 1.0 - [float]$runttl / [float]$ios;\r\n                if ( $cnt -gt 0 ) {\r\n                    \"$lat_us,$pctile\" >> $outfile\r\n                }\r\n            }\r\n        } else {\r\n            $plat_bits = $j.jobs[0].$rdwr.clat.bins.FIO_IO_U_PLAT_BITS\r\n            $plat_val = $j.jobs[0].$rdwr.clat.bins.FIO_IO_U_PLAT_VAL\r\n            foreach ($b in 0..[int]$j.jobs[0].$rdwr.clat.bins.FIO_IO_U_PLAT_NR) {\r\n                $cnt = [int]$j.jobs[0].$rdwr.clat.bins.$b\r\n                $runttl += $cnt\r\n                $pctile = 1.0 - [float]$runttl / [float]$ios\r\n                if ( $cnt -gt 0 ) {\r\n                    $p2idx = plat_idx_to_val $b $plat_bits $plat_val\r\n                    \"${p2idx},${pctile}\" >> $outfile\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\nfunction CombineThreadOutputs($suffix, $outcsv, $lat, $runtime, $extra_runtime)\r\n{\r\n    # Merge all FIO iops/lat logs across all servers\"\"\"\r\n    # The lists may be called \"iops\" but the same works for clat/slat\r\n    $testtime = $runtime + $extra_runtime\r\n    $iops = New-Object 'float[]' $testtime\r\n    # For latencies, need to keep the _w and _r separate\r\n    $iops_w = New-Object 'float[]' $testtime\r\n    $filecnt = 0\r\n    $fileglob = \"$testfile$suffix.*log\"\r\n    Get-ChildItem $fileglob | ForEach-Object {\r\n        $filename = $_.FullName\r\n        $filecnt++\r\n    $csvhdr = 'timestamp', 'value', 'wr', 'ign'\r\n    $lines = Import-Csv -Path $filename -Header $csvhdr\r\n    $lineidx = 0\r\n        # Set time 0 IOPS to first values\r\n        $riops = [float]0.0\r\n        $wiops = [float]0.0\r\n        $nexttime = [float]0.0\r\n        for ($x=0; $x -lt $testtime; $x++) {\r\n            if ( -not $lat ) {\r\n                $iops[$x] = [float]$iops[$x] + [float]$riops + [float]$wiops\r\n            } else {\r\n                $iops[$x] = [float]$iops[$x] + [float]$riops\r\n                $iops_w[$x] = [float]$iops_w[$x] + [float]$wiops\r\n            }\r\n            while (($lineidx -lt $lines.Count) -and ($nexttime -lt $x)) {\r\n                $nexttime = $lines[$lineidx].timestamp / 1000.0\r\n                if ( $lines[$lineidx].wr -eq 1 ) {\r\n                    $wiops = [int]$lines[$lineidx].value\r\n                } else {\r\n                    $riops = [int]$lines[$lineidx].value\r\n                }\r\n                $lineidx++\r\n            }\r\n        }\r\n    }\r\n\r\n    # Generate the combined CSV\r\n    for ($x=[int]($extra_runtime / 2); $x -lt ($runtime + $extra_runtime); $x++) {\r\n        if ( $lat ) {\r\n            $a = [float]$iops[$x] / [float]$filecnt\r\n            $b = [float]$iops_w[$x] / [float]$filecnt\r\n            \"{0:f1},{1:f1}\" -f $a, $b >> $outcsv\r\n        } else {\r\n        $a = $iops[$x]\r\n            \"{0:f0}\" -f $a >> $outcsv\r\n        }\r\n    }\r\n}\r\n\r\n\r\nfunction RunTest\r\n{\r\n    # Runs the specified test, generates output CSV lines.\r\n\r\n    # Output file names\r\n    $testfile = TestName $seqrand $wmix $bs $threads $iodepth\r\n\r\n    if ( $seqrand -eq \"Seq\" ) { $rw = \"rw\" }\r\n    else { $rw = \"randrw\" }\r\n\r\n    if ( $iops_log ) {\r\n        $extra_runtime = 10\r\n    } else {\r\n        $extra_runtime = 0\r\n    }\r\n    $testtime = $runtime + $extra_runtime\r\n\r\n    $cmd  = (\"--name=test\", \"--readwrite=$rw\", \"--rwmixwrite=$wmix\")\r\n    $cmd += (\"--bs=$bs\", \"--invalidate=1\", \"--end_fsync=0\")\r\n    $cmd += (\"--group_reporting\", \"--direct=1\", \"--filename=$physDrive\")\r\n    $cmd += (\"--size=${testcapacity}G\", \"--time_based\", \"--runtime=$testtime\")\r\n    $cmd += (\"--ioengine=$ioengine\", \"--numjobs=$threads\")\r\n    $cmd += (\"--iodepth=$iodepth\", \"--norandommap\", \"--randrepeat=0\")\r\n    if ( $iops_log ) {\r\n        $cmd += (\"--write_iops_log=$testfile\")\r\n        $cmd += (\"--write_lat_log=$testfile\")\r\n        $cmd += (\"--log_avg_msec=1000\")\r\n        $cmd += (\"--log_unix_epoch=0\")\r\n    }\r\n    $cmd += (\"--thread\", \"--output-format=$fioOutputFormat\", \"--exitall\")\r\n    $fio + \" \" + [string]::Join(\" \", $cmd) | Out-File $testfile\r\n\r\n    # Check that the IO size is usable.  Some SSDs are only 4K logical sectors\r\n    $minblock = (Get-Disk | Where-Object { $_.Number -eq $global:physDriveNo }).LogicalSectorSize\r\n    if ( $bs -lt $minblock ) {\r\n        \"Test not run because block size $bs below minimum size $minblock\" | Out-File -Append $testfile\r\n        \"3;\" + \"0;\" * 100 | Out-File -Append $testfile # Bogus 0-filled result line\r\n        \"1,1\" | Out-File \"${testfile}.exc.read.csv\"\r\n        \"1,1\" | Out-File \"${testfile}.exc.write.csv\"\r\n        \"$seqrand,$wmix,$bs,$threads,$iodepth,0,0,0,0\" | Out-File -Append $testcsv\r\n        Write-Output \"SKIP\" \"SKIP\" \"SKIP\"\r\n        return\r\n    }\r\n\r\n    . $fio @cmd | Out-File -Append $testfile\r\n\r\n    if ( $LastExitCode -ne 0 ) {\r\n        Write-Output \"ERROR\" \"ERROR\" \"ERROR\"\r\n        return # Don't process this one, it was error'd out!\r\n    }\r\n\r\n    if ( $iops_log ) {\r\n        CombineThreadOutputs '_iops' $timeseriescsv $false $runtime $extra_runtime\r\n        CombineThreadOutputs '_clat' $timeseriesclatcsv $true $runtime $extra_runtime\r\n        CombineThreadOutputs '_slat' $timeseriesslatcsv $true $runtime $extra_runtime\r\n    }\r\n\r\n    # Thanks to @BryanTuttle.  Skip any FIO output before the JSON open-bracket\r\n    $LineSkip=0\r\n    foreach ($line in Get-Content $testfile) {\r\n\t    if ($line -match '^{') { break }\r\n\t\telse {$LineSkip++}\r\n    }\r\n\r\n    $j = ConvertFrom-Json \"$(Get-Content $testfile | select -Skip $LineSkip)\"\r\n    $rdiops = [float]($j.jobs[0].read.iops);\r\n    $wriops = [float]($j.jobs[0].write.iops);\r\n    $rlat = [float]($j.jobs[0].read.lat_ns.mean) / 1000.0;\r\n    if ($rlat -le 0.0001) { $rlat = [float]($j.jobs[0].read.lat.mean); }\r\n    $wlat = [float]($j.jobs[0].write.lat_ns.mean) / 1000.0;\r\n    if ($wlat -le 0.0001) { $wlat = [float]($j.jobs[0].wlat.lat.mean); }\r\n    $iops = \"{0:F0}\" -f ($rdiops + $wriops)\r\n    # Locale output is not wanted here, manually make a decimal string.  Ugh\r\n    $lat = \"{0:F1}\" -f ([math]::Max($rlat, $wlat))\r\n    $mbpsfloat = (( ($rdiops+$wriops) * $bs ) / ( 1024.0 * 1024.0 ))\r\n    \"{0:f1}\" -f $mbpsfloat | Set-Variable mbps\r\n    $lat = \"{0:F1}\" -f ([math]::Max($rlat, $wlat)) # This is just displayed, use native locale\r\n    \"$seqrand,$wmix,$bs,$threads,$iodepth,$iops,$mbps,$rlat,$wlat\" | Out-File -Append $testcsv\r\n\r\n    WriteExceedance $j \"read\" \"${testfile}.exc.read.csv\"\r\n    WriteExceedance $j \"write\" \"${testfile}.exc.write.csv\"\r\n\r\n    Write-Output $iops $mbps $lat\r\n}\r\n'@\r\n\r\n\r\nfunction DefineTests {\r\n    # Generate the work list for the main worker into OC.\r\n\r\n    # What we're shmoo-ing across\r\n    $bslist = (512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072)\r\n    $qdlist = (1, 2, 4, 8, 16, 32, 64, 128, 256)\r\n    $threadslist = (1, 2, 4, 8, 16, 32, 64, 128, 256)\r\n    $shorttime = 120 # Runtime of point tests\r\n    $longtime = 1200 # Runtime of long-running tests\r\n    if ( $quickie ) {\r\n        $shorttime = [int]($shorttime / 10)\r\n        $longtime = [int]($longtime / 10)\r\n    }\r\n    function AddTest( $name, $seqrand, $writepct, $blocksize, $threads, $qdperthread, $desc, $cmdline ) {\r\n        if ($threads -eq \"\") { $qd = '' } else { $qd = ([int]$threads) * ([int]$qdperthread) }\r\n        if ($blocksize -ne \"\") { if ($blocksize -lt 1024) { $bsstr = \"${blocksize}b\" } else { $bsstr = \"{0:N0}K\" -f ([int]$blocksize/1024) } }\r\n        if ($writepct -ne \"\" ) { $writepct = [string]$writepct + \"%\" }\r\n        $dat = New-Object psobject -Property @{ name=$name; seqrand=$seqrand; writepct=$writepct\r\n            bs=$bsstr; qd = $qd; qdperthread = $qdperthread; bw = ''; iops= ''; lat = ''; desc = $desc;\r\n            cmdline = $cmdline }\r\n        $global:oc.Add( $dat )\r\n    }\r\n\r\n    function DoAddTest {\r\n        AddTest $testname $seqrand $wmix $bs $threads $iodepth $desc \"$global:globals; $global:jobutils; `$iops_log=$iops_log; `$seqrand=`\"$seqrand`\"; `$wmix=$wmix; `$bs=$bs; `$threads=$threads; `$iodepth=$iodepth; `$runtime=$runtime; RunTest\"\r\n    }\r\n\r\n    function AddTestBSShmoo {\r\n        AddTest $testname 'Preparation' '' '' '' '' '' \"$global:globals; `\"$testname`\" >> `\"$global:testcsv`\"; Write-Output ' ' ' ' ' '\"\r\n        foreach ($bs in $bslist ) { $desc = \"$testname, BS=$bs\"; DoAddTest }\r\n    }\r\n\r\n    function AddTestQDShmoo {\r\n        AddTest $testname 'Preparation' '' '' '' '' '' \"$global:globals; `\"$testname`\" >> `\"$global:testcsv`\"; Write-Output ' ' ' ' ' '\"\r\n        foreach ($iodepth in $qdlist ) { $desc = \"$testname, QD=$iodepth\"; DoAddTest }\r\n    }\r\n\r\n    function AddTestThreadsShmoo {\r\n        AddTest $testname 'Preparation' '' '' '' '' '' \"$global:globals; `\"$testname`\" >> `\"$global:testcsv`\"; Write-Output ' ' ' ' ' '\"\r\n        foreach ($threads in $threadslist) { $desc = \"$testname, Threads=$threads\"; DoAddTest }\r\n    }\r\n\r\n    AddTest 'Sequential Preconditioning' 'Seq Pass 1' '100' '131072' '1' '256' 'Sequential Preconditioning' \"$global:globals; $global:jobutils; SequentialConditioning;\"\r\n    if ($global:fastPrecond -ne $true) {\r\n\t    AddTest 'Sequential Preconditioning' 'Seq Pass 2' '100' '131072' '1' '256' 'Sequential Preconditioning' \"$global:globals; $global:jobutils; SequentialConditioning;\"\r\n    }\r\n\r\n    $testname = \"Sustained Multi-Threaded Sequential Read Tests by Block Size\"\r\n    $seqrand = \"Seq\"; $wmix=0; $threads=1; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=256\r\n    AddTestBSShmoo\r\n\r\n    $testname = \"Sustained Multi-Threaded Random Read Tests by Block Size\"\r\n    $seqrand = \"Rand\"; $wmix=0; $threads=16; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=16\r\n    AddTestBSShmoo\r\n\r\n    $testname = \"Sequential Write Tests with Queue Depth=1 by Block Size\"\r\n    $seqrand = \"Seq\"; $wmix=100; $threads=1; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=1\r\n    AddTestBSShmoo\r\n\r\n    if ($global:fastPrecond -ne $true) {\r\n        AddTest 'Random Preconditioning' 'Rand Pass 1' '100' '4096' '1' '256' 'Random Preconditioning' \"$global:globals; $global:jobutils; RandomConditioning;\"\r\n        AddTest 'Random Preconditioning' 'Rand Pass 2' '100' '4096' '1' '256' 'Random Preconditioning' \"$global:globals; $global:jobutils; RandomConditioning;\"\r\n    }\r\n\r\n    $testname = \"Sustained 4KB Random Read Tests by Number of Threads\"\r\n    $seqrand = \"Rand\"; $wmix=0; $bs=4096; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=1\r\n    AddTestThreadsShmoo\r\n\r\n    $testname = \"Sustained 4KB Random mixed 30% Write Tests by Number Threads\"\r\n    $seqrand = \"Rand\"; $wmix=30; $bs=4096; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=1\r\n    AddTestThreadsShmoo\r\n\r\n    $testname = \"Sustained Perf Stability Test - 4KB Random 30% Write for 20 minutes\"\r\n    $desc = $testname\r\n    AddTest $testname 'Preparation' '' '' '' '' '' \"$global:globals; `\"$testname`\" >> `\"$global:testcsv`\"; Write-Output ' ' ' ' ' '\"\r\n    $seqrand = \"Rand\"; $wmix=30; $bs=4096; $runtime=$longtime; $iops_log=\"`$true\"; $iodepth=1; $threads=256\r\n    DoAddTest\r\n\r\n    $testname = \"Sustained 4KB Random Write Tests by Number of Threads\"\r\n    $seqrand = \"Rand\"; $wmix=100; $bs=4096; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=1\r\n    AddTestThreadsShmoo\r\n\r\n    $testname = \"Sustained Multi-Threaded Random Write Tests by Block Size\"\r\n    $seqrand = \"Rand\"; $wmix=100; $runtime=$shorttime; $iops_log=\"`$false\"; $iodepth=16; $threads=16\r\n    AddTestBSShmoo\r\n}\r\n\r\n\r\nfunction RunAllTests()\r\n{\r\n    # Iterate through the OC work queue and run each job, show progress.\r\n\r\n    function UpdateView {\r\n        # Updates the grid to reflect new data, scrolls to selection\r\n        $t_testList.ItemsSource.Refresh()\r\n        $t_testList.UpdateLayout()\r\n        $t_testList.ScrollIntoView($t_testList.SelectedItem)\r\n    }\r\n\r\n    function NotifyIcon {\r\n        # NotifyIcon needs to run as separate Powerhell process because\r\n        # a WPF form will block other events (like the notify-clicked) until\r\n        # it returns control to PowerShell\r\n\r\n        # Pass destination into the block through the child's environment\r\n        [System.Environment]::SetEnvironmentVariable(\"ods\", $global:odsdest)\r\n        $proc = Start-Process -PassThru (Get-Command powershell.exe) -WindowStyle Hidden -ArgumentList ( \"-Command\", {\r\n            Add-Type -AssemblyName PresentationFramework, System.Windows.Forms\r\n            echo ([System.Environment]::GetEnvironmentVariable(\"'ods'\"))\r\n            # Add a NotifyIcon that, when clicked, will open the results spreadsheet\r\n            $global:notify = New-Object System.Windows.Forms.NotifyIcon\r\n            $global:notify.Icon = [System.Drawing.SystemIcons]::Information\r\n            $global:notify.BalloonTipIcon = \"'Info'\"\r\n            $global:notify.BalloonTipText = \"'The ezFIO test series has completed and result spreadsheet may be opened.'\"\r\n            $global:notify.Text = \"'Click to open the ezFIOresult spreadsheet'\"\r\n            $global:notify.BalloonTipTitle = \"'ezFIO Test Completion'\"\r\n            $global:notify.Visible = $True\r\n            # Using the add_BalloonTipClicked() seemed to fault every time\r\n            Unregister-Event -SourceIdentifier click_event -ErrorAction SilentlyContinue\r\n            Register-ObjectEvent $notify Click -sourceIdentifier click_event -Action {\r\n                Invoke-Item ([System.Environment]::GetEnvironmentVariable(\"'ods'\"))\r\n                $global:notify.Dispose()\r\n                $global:notify = $null\r\n                } | Out-Null\r\n            Unregister-Event -SourceIdentifier balloonclick_event -ErrorAction SilentlyContinue\r\n            Register-ObjectEvent $notify BalloonTipClicked -SourceIdentifier balloonclick_event -Action {\r\n                Invoke-Item ([System.Environment]::GetEnvironmentVariable(\"'ods'\"))\r\n                $global:notify.Dispose()\r\n                $global:notify = $null\r\n            } | Out-Null\r\n            $notify.ShowBalloonTip(10000)\r\n            while ( $global:notify -ne $null ) { sleep 1 }\r\n        } )\r\n        return $proc\r\n    }\r\n\r\n    $xaml = @'\r\n<Window x:Class=\"Window3\"\r\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\r\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\r\n    Title=\"ezFIO Test Progress\" Height=\"562.084\" Width=\"682.007\">\r\n    <Window.Resources>\r\n        <Style x:Key=\"CellRightAlign\">\r\n            <Setter Property=\"Control.HorizontalAlignment\" Value=\"Right\" />\r\n        </Style>\r\n        <Style x:Key=\"CellCenterAlign\">\r\n            <Setter Property=\"Control.HorizontalAlignment\" Value=\"Center\" />\r\n        </Style>\r\n    </Window.Resources>\r\n    <Grid>\r\n        <Label Content=\"Testing Drive:\" HorizontalAlignment=\"Left\" Margin=\"83,23,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label x:Name=\"testingDrive\" Content=\"\\\\physicaldrive0\" HorizontalAlignment=\"Left\" Margin=\"167,23,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label Content=\"Current Test Runtime:\" HorizontalAlignment=\"Left\" Margin=\"40,75,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label x:Name=\"testRuntime\" Content=\"00:00:00\" HorizontalAlignment=\"Left\" Margin=\"167,75,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label Content=\"Current Test:\" HorizontalAlignment=\"Left\" Margin=\"88,49,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label x:Name=\"currentTest\" Content=\"BS 4K, QD 32, WR 100%\" HorizontalAlignment=\"Left\" Margin=\"167,49,0,0\" VerticalAlignment=\"Top\"/>\r\n        <DataGrid x:Name=\"testList\" HorizontalAlignment=\"Left\" Margin=\"36,146,0,0\" VerticalAlignment=\"Top\" Height=\"321\" Width=\"600\" GridLinesVisibility=\"None\" HeadersVisibility=\"Column\">\r\n            <DataGrid.GroupStyle>\r\n                <GroupStyle>\r\n                    <GroupStyle.HeaderTemplate>\r\n                        <DataTemplate>\r\n                            <TextBlock Text=\"{Binding Items[0].name}\"/>\r\n                        </DataTemplate>\r\n                    </GroupStyle.HeaderTemplate>\r\n                </GroupStyle>\r\n            </DataGrid.GroupStyle>\r\n            <DataGrid.Columns>\r\n                <DataGridTextColumn Header=\"Access Pattern\" Binding=\"{Binding seqrand}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\"/>\r\n                <DataGridTextColumn Header=\"Write %\" Binding=\"{Binding writepct}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"Block Size\" Binding=\"{Binding bs}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"Queue Depth\" Binding=\"{Binding qd}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"        \" Binding=\"{Binding blank}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"IOPS\" Binding=\"{Binding iops}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"Bandwidth (MB/s)\" Binding=\"{Binding bw}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n                <DataGridTextColumn Header=\"Latency (us)\" Binding=\"{Binding lat}\" CanUserSort=\"false\" CanUserReorder=\"false\" IsReadOnly=\"true\" ElementStyle=\"{StaticResource CellRightAlign}\"/>\r\n            </DataGrid.Columns>\r\n        </DataGrid>\r\n        <Button x:Name=\"openSpreadsheet\" Content=\"Open Graphs Spreadsheet\" HorizontalAlignment=\"Left\" Margin=\"236,486,0,0\" Width=\"200\" Height=\"27\" VerticalAlignment=\"Top\"/>\r\n        <Label Content=\"Total Test Runtime:\" HorizontalAlignment=\"Left\" Margin=\"53,102,0,0\" VerticalAlignment=\"Top\"/>\r\n        <Label x:Name=\"totalRuntime\" Content=\"00:00:00\" HorizontalAlignment=\"Left\" Margin=\"167,102,0,0\" VerticalAlignment=\"Top\"/>\r\n    </Grid>\r\n</Window>\r\n'@\r\n\r\n\r\n    # The test window\r\n    $t = WindowFromXAML $xaml 't'\r\n    $t.Icon = $global:iconBitmap\r\n\r\n    $t.add_Loaded( {\r\n        $t.Activate()\r\n        $t_testList.Focus()\r\n\r\n        $global:step = -1 # Which test we're on\r\n        $global:curjob = $null # Which process is running\r\n        $global:totalStarttime = Get-Date\r\n\r\n        # The NotifyIcon process info\r\n        $global:notifyProc = $null\r\n\r\n        $t_testingDrive.Content = [string]::Format(\"{0}, {1}({2}), {3}GB\", $global:physDrive, $global:model, $global:serial, $global:testcapacity )\r\n        $t_openSpreadsheet.IsEnabled = $false\r\n        $t_currentTest.Content = \"Starting up...\"\r\n\r\n        $t_testList.CanUserAddRows = $false\r\n        $t_testList.AutoGenerateColumns = $false\r\n        $t_testList.ItemsSource = $null\r\n\r\n        $lview = [System.Windows.Data.ListCollectionView]$global:oc\r\n        $lview.GroupDescriptions.Add((new-object System.Windows.Data.PropertyGroupDescription \"name\"))\r\n        $t_testList.ItemsSource = $lview\r\n\r\n        # Poor man's threading/event driven\r\n        $global:timer = new-object System.Windows.Threading.DispatcherTimer\r\n        $global:timer.Interval = [TimeSpan]\"0:0:1.00\"\r\n        $global:timer.Add_Tick(\r\n        {\r\n            # If there's a running job, update the runtime if not done, and capture the results if finished\r\n            if ($global:curjob -ne $null)\r\n            {\r\n                if ( $global:curjob.State -match 'running' )\r\n                {\r\n                    $now = Get-Date\r\n                    $delta = $now - $global:starttime\r\n                    $ts = [timespan]::FromTicks($delta.Ticks)\r\n                    $t_testRuntime.Content = $ts.ToString(\"hh\\:mm\\:ss\")\r\n                    $delta = $now - $global:totalstarttime\r\n                    $ts = [timespan]::FromTicks($delta.Ticks)\r\n                    $t_totalRuntime.Content = $ts.ToString(\"hh\\:mm\\:ss\")\r\n                } else {\r\n                    # Job just finished, let's read out answers\r\n                    $q = Receive-Job $global:curjob\r\n                    $global:oc[$global:step].iops = $q[0]\r\n                    $global:oc[$global:step].bw = $q[1]\r\n                    $global:oc[$global:step].lat = $q[2]\r\n                    $ign = 0.0\r\n                    if ([float]::TryParse($global:oc[$global:step].iops, [ref]$ign)) { $global:oc[$global:step].iops = [string]::Format(\"{0:N0}\", [float]$global:oc[$global:step].iops) }\r\n                    if ([float]::TryParse($global:oc[$global:step].bw, [ref]$ign)) { $global:oc[$global:step].bw = [string]::Format(\"{0:N1}\", [float]$global:oc[$global:step].bw) }\r\n                    $t_testList.SelectedIndex = $global:step\r\n                    UpdateView\r\n                    if ($global:oc[$global:step].iops -eq \"ERROR\") {\r\n                        $global:step = 9999 # Skip all the other tests\r\n                        [System.Windows.Forms.MessageBox]::Show( \"ERROR!  FIO job did not complete successfully.  Aborting further runs.\", \"Fatal Error\", 0, 48 ) | Out-Null\r\n                    }\r\n                }\r\n            }\r\n            # If there's no running job (last one finished), start a new one\r\n            if ( ($global:curjob -eq $null) -or ( -not ($global:curjob.State -match 'running' ) ) ){\r\n                $global:step = $global:step + 1\r\n                if ($global:step -lt $t_testList.Items.Count) {\r\n                    $t_testList.SelectedIndex = $global:step\r\n                    $global:cmdline = $t_testList.Items[$global:step].cmdline\r\n                    $t_currentTest.Content = $t_testList.Items[$global:step].desc\r\n                    # Powershell won't have the $globals in the Start-Job context, so expand here\r\n                    $fullcmd = \"Start-Job { $global:cmdline }\"\r\n                    $global:curjob = Invoke-Expression $fullcmd\r\n                    $global:starttime = Get-Date\r\n                    $global:oc[$global:step].iops = \"Running\"\r\n                    $global:oc[$global:step].bw = \"Running\"\r\n                    $global:oc[$global:step].lat = \"Running\"\r\n                    $t_testList.SelectedIndex = $global:step\r\n                    UpdateView\r\n                } else {\r\n                    $global:timer.Stop()\r\n                    $global:curjob = $null\r\n                    $t_testList.SelectedIndex = $null\r\n                    if ($global:step -lt 9999) {\r\n                        $t_currentTest.Content = \"Completed\"\r\n                        GenerateResultODS\r\n                        $t_openSpreadsheet.IsEnabled = $true\r\n                        $global:notifyProc = NotifyIcon\r\n                    } else {\r\n                        $t_currentTest.Content = \"ERROR\"\r\n                    }\r\n                }\r\n            }\r\n        } )\r\n        $global:timer.Start()\r\n    } )\r\n\r\n    $t.add_Closing( {\r\n        $global:timer.Stop()\r\n    } )\r\n\r\n    $t_openSpreadsheet.add_Click( {\r\n        # Just open the file using default application\r\n        Invoke-Item $global:odsdest\r\n        $t.close()\r\n    } )\r\n\r\n    $t.ShowDialog() | Out-Null\r\n\r\n    # Clean up the notifyicon process\r\n    if ($global:notifyProc -ne $null) {\r\n        if (-not ($global:notifyProc.HasExited)) { $global:notifyProc.Kill() }\r\n    }\r\n}\r\n\r\n\r\nfunction RunAllTestsCLI()\r\n{\r\n    # CLI mode will short-circuit, run much simpler path and output only text\r\n\r\n    # Determine some column widths to make format specifiers for CLI mode outputs\r\n    $maxlen = 0\r\n    foreach ($o in $global:oc) {\r\n        $maxlen = [math]::max($maxlen, $o.desc.length)\r\n    }\r\n    $descfmt = \"{0,-\" + [string]$maxlen + \"}\"\r\n    $resfmt = \"{1,8} {2,9} {3,8}\"\r\n    $fmtstr = $descfmt + \" \" + $resfmt\r\n\r\n    \"*\" * [string]::format( $fmtstr, \"\", \"\", \"\", \"\").length\r\n    \"ezFio test parameters:\"\r\n\r\n    $fmtinfo=\"{0,-20}: {1}\"\r\n    [string]::format( $fmtinfo, \"Drive\", $global:physDrive )\r\n    [string]::format( $fmtinfo, \"Model\", $global:model )\r\n    [string]::format( $fmtinfo, \"Serial\", $global:serial )\r\n    [string]::format( $fmtinfo, \"AvailCapacity\", [string]$global:physDriveGiB + \" GiB\")\r\n    [string]::format( $fmtinfo, \"TestedCapacity\", [string]$global:testcapacity + \" GiB\")\r\n    [string]::format( $fmtinfo, \"CPU\", $global:cpu)\r\n    [string]::format( $fmtinfo, \"Cores\", $global:cpuCores)\r\n    [string]::format( $fmtinfo, \"Frequency\", $global:cpuFreqMHz)\r\n    [string]::format( $fmtinfo, \"FIO Version\", $global:fioVerString)\r\n\r\n    \"\"\r\n    [string]::format( $fmtstr, \"Test Description\", \"BW(MB/s)\", \"IOPS\", \"Lat(us)\")\r\n    [string]::format( $fmtstr, \"-\"*$maxlen, \"-\"*8, \"-\"*9, \"-\"*8)\r\n\r\n    foreach ($o in $global:oc) {\r\n        if ( $o.desc -eq \"\" ) {\r\n            # This is a header-printing job, don't thread out\r\n            [string]::format( $fmtstr, \"---\" + $o.name + \"---\", \"\", \"\", \"\")\r\n            [Console]::Out.Flush()\r\n            Invoke-Expression $o.cmdline | Out-Null\r\n        } else {\r\n            # This is a real test job, print some stuff, execute, then print output\r\n            Write-Host -NoNewline ([string]::format($descfmt, $o.desc))\r\n            [Console]::Out.Flush()\r\n            $q = Invoke-Expression $o.cmdline\r\n            $iops = $q[0]\r\n            $mbps = $q[1]\r\n            $lat = $q[2]\r\n            Write-Host ([string]::format($resfmt, \"\", $mbps, $iops, $lat))\r\n            if ($mbps -eq \"ERROR\") {\r\n                \"ERROR!  FIO job did not complete successfully.  Aborting further runs.\"\r\n                return\r\n            }\r\n            [Console]::Out.Flush()\r\n        }\r\n    }\r\n    GenerateResultODS\r\n    \"`nCOMPLETED!  Output file: $global:odsdest\"\r\n    return\r\n}\r\n\r\n\r\nfunction GenerateResultODS()\r\n{\r\n    # Builds a new ODS spreadsheet w/graphs from generated test CSV files.\r\n\r\n    function GetContentXMLFromODS( $odssrc )\r\n    {\r\n        # Extract content.xml from an ODS file, where the sheet lives.\r\n        $ziparchive = [System.IO.Compression.ZipFile]::Open( $odssrc, [System.IO.Compression.ZipArchiveMode]::Read )\r\n        $zipentry = $ziparchive.GetEntry(\"content.xml\")\r\n        $reader = New-Object System.IO.StreamReader( $zipentry.Open() )\r\n        $contentobj = $reader.ReadToEnd()\r\n        $reader.Close()\r\n        $ziparchive.Dispose()\r\n        return $contentobj -replace \"`n\",\"\" -replace \"`r\",\"\"\r\n    }\r\n\r\n    function ReplaceSheetWithCSV_regex($sheetName, $csvName, $xmltext)\r\n    {\r\n        # Replace a named sheet with the contents of a CSV file.\r\n        $newt = \"<table:table table:name=\"\r\n        $newt = $newt + \"`\"$sheetName`\"\" + ' table:style-name=\"ta1\" > <table:table-column table:style-name=\"co1\" table:default-cell-style-name=\"Default\"/>'\r\n        Get-Content $csvName | ForEach-Object {\r\n            $newt = $newt + \"<table:table-row table:style-name=`\"ro1`\">\"\r\n            foreach ($val in ($_.Split(','))) {\r\n                $dbl = 0.0\r\n                if ( [System.Double]::TryParse( $val, [ref]$dbl ) ) {\r\n                    $newt = $newt + \"<table:table-cell office:value-type=`\"float`\" office:value=`\"$val`\"><text:p>$val</text:p></table:table-cell>\"\r\n                } else {\r\n                    $newt = $newt + \"<table:table-cell office:value-type=`\"string`\"><text:p>$val</text:p></table:table-cell>\"\r\n                }\r\n            }\r\n            $newt = $newt + \"</table:table-row>\"\r\n        }\r\n        $newt = $newt + \"</table:table>\"\r\n        $searchstr = \"<table:table table:name=`\"$sheetName`\".*?</table:table>\"\r\n        $xmltext -replace $searchstr, $newt\r\n    }\r\n\r\n    function CombineExceedanceCSV( $qdList, $testType, $testWpct, $testBS, $testIOdepth, $suffix )\r\n    {\r\n        # Merge multiple exceedance CSVs into a single output file.\r\n        # Column merge multiple CSV files into a single one.  Complicated by\r\n        # the fact that the number of columns in each may vary.\r\n\r\n        $csv = $global:details + \"/ezfio_exceedance_\" + $suffix + \".csv\"\r\n        if ( Test-Path -Path $csv ) {\r\n            Remove-Item -Recurse -Force $csv | Out-Null\r\n        }\r\n        CSVInfoHeader > $csv\r\n        $line1 = \"\"\r\n        $line2 = \"\"\r\n        foreach ($qd in $qdList) {\r\n            $line1 = $line1 + \"QD${qd} Read Exceedance,,QD${qd} Write Exceedance,,,\"\r\n            $line2 = $line2 + \"rdusec,rdpct,wrusec,wrpct,,\"\r\n        }\r\n        $line1 >> $csv;\r\n        $line2 >> $csv;\r\n\r\n        $files = @()\r\n        foreach ($qd in $qdList) {\r\n            $testname = TestName $testType $testWpct $testBS $qd $testIOdepth\r\n            if ( Test-Path -Path \"${testname}.exc.read.csv\") {\r\n                $r = [System.IO.File]::OpenText( \"${testname}.exc.read.csv\" )\r\n            } else {\r\n                $r = $null\r\n            }\r\n            if ( Test-Path -Path \"${testname}.exc.write.csv\") {\r\n                $w = [System.IO.File]::OpenText( \"${testname}.exc.write.csv\" )\r\n            } else {\r\n                $w = $null\r\n            }\r\n            $files += , @( $r, $w )\r\n        }\r\n        do {\r\n            $all_empty = $true\r\n            $l = \"\"\r\n            foreach ($fset in $files) {\r\n                if (($fset[0] -eq $null) -or ($fset[0].EndOfStream)) {\r\n                    $a = \",\"\r\n                } else {\r\n                    $a = $fset[0].ReadLine().Trim()\r\n                    $all_empty = $false\r\n                }\r\n                if (($fset[1] -eq $null) -or ($fset[1].EndOfStream)) {\r\n                    $b = \",\"\r\n                } else {\r\n                    $b = $fset[1].ReadLine().Trim()\r\n                    $all_empty = $false\r\n                }\r\n                $l += \"${a},${b},,\"\r\n            }\r\n            $l >> $csv\r\n        } while (-not $all_empty)\r\n        foreach ($fset in $files) {\r\n            if ($fset[0] -ne $null) {\r\n                $fset[0].Close()\r\n            }\r\n            if ($fset[1] -ne $null) {\r\n                $fset[1].Close()\r\n            }\r\n        }\r\n        return $csv\r\n    }\r\n\r\n    function UpdateContentXMLToODS_text( $odssrc, $odsdest, $xmltext )\r\n    {\r\n        # Replace content.xml in an ODS file with in-memory, modified copy and\r\n        # write new ODS. Can't just copy source.zip and replace one file, the\r\n        # output ZIP file is not correct in many cases (opens in Excel but fails\r\n        # ODF validation and LibreOffice fails to load under Windows)\r\n\r\n        if (test-path $odsdest) { Remove-Item $odsdest }\r\n\r\n        # Windows ZipArchive will not use \"Store\" even if we select no compression\r\n        # so we need to have a mimetype.zip file encoded below to match ODF spec:\r\n        $mimetypezip = @'\r\nUEsDBBQAAAgAAICyN0+FbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz\r\naXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsBAhQAFAAACAAAgLI3T4VsOYouAAAALgAAAAgA\r\nAAAAAAAAAAAAAAAAAAAAAG1pbWV0eXBlUEsFBgAAAAABAAEANgAAAFQAAAAAAA==\r\n'@\r\n        $bytes = [System.Convert]::FromBase64String( $mimetypezip )\r\n        [io.file]::WriteAllBytes( $odsdest, $bytes )\r\n\r\n        $zasrc = [System.IO.Compression.ZipFile]::Open( $odssrc, [System.IO.Compression.ZipArchiveMode]::Read )\r\n        $zadst = [System.IO.Compression.ZipFile]::Open( $odsdest, [System.IO.Compression.ZipArchiveMode]::Update )\r\n        foreach ($entry in $zasrc.Entries) {\r\n            if (($entry.FullName -eq \"mimetype\") -or $entry.FullName.StartsWith(\"Thumbnails\") -or $entry.FullName.StartsWith(\"ObjectReplacement\")) {\r\n                # Skip binary versions, and the copied-over mimetype\r\n                continue\r\n            }\r\n            $newentry = $zadst.CreateEntry( $entry )\r\n            if ($entry.FullName.EndsWith(\"/\") -or $entry.FullName.EndsWith(\"\\\")) {\r\n                # Directory, don't copy anything\r\n            } elseif ($entry.FullName -eq \"content.xml\") {\r\n                # Copying data for content.xml from new data\r\n                $wr = New-Object System.IO.StreamWriter( $newentry.Open() )\r\n                $wr.Write( $xmltext )\r\n                $wr.Close()\r\n            } elseif ($entry.FullName -like \"Object */content.xml\") {\r\n                # Remove <table:table table:name=\"local-table\"> table\r\n                $rd = New-Object System.IO.StreamReader( $entry.Open() )\r\n                $rdbytes = $rd.ReadToEnd()\r\n                $wr = New-Object System.IO.StreamWriter( $newentry.Open() )\r\n                $wrbytes = $rdbytes -replace \"<table:table table:name=`\"local-table`\">.*</table:table>\", \"\"\r\n                $wr.write( $wrbytes )\r\n                $wr.Close()\r\n                $rd.Close()\r\n            } elseif ($entry.FullName -eq \"META-INF/manifest.xml\") {\r\n                # Remove ObjectReplacements from the list\r\n                $rd = New-Object System.IO.StreamReader( $entry.Open() )\r\n                $wr = New-Object System.IO.StreamWriter( $newentry.Open() )\r\n                $rdbytes = $rd.ReadToEnd()\r\n                $lines = $rdbytes.Split(\"`n\")\r\n                foreach ($line in $lines) {\r\n                    if ( -not ( ($line -contains \"ObjectReplacement\") -or ($line -contains \"Thumbnails\") ) ) {\r\n                        $wr.Write($line)\r\n                        $wr.Write(\"`n\")\r\n                    }\r\n                }\r\n                $wr.Close()\r\n                $rd.Close()\r\n            } else {\r\n                # Copying data for from the source ZIP\r\n                $wr = New-Object System.IO.StreamWriter( $newentry.Open() )\r\n                $rd = New-Object System.IO.StreamReader( $entry.Open() )\r\n                $wr.Write( $rd.ReadToEnd() )\r\n                $wr.Close()\r\n                $rd.Close()\r\n            }\r\n        }\r\n        $zadst.Dispose()\r\n        $zasrc.Dispose()\r\n    }\r\n\r\n    # Use text magic and not XML editing as the XML processor doesn't seem to\r\n    # escape special characters in the same way that OpenOffice does, leading to\r\n    # occasional problems.  Also allows same logic to run under Linux w/sed\r\n    [string]$xmlsrc = GetContentXMLFromODS $global:odssrc\r\n    $xmlsrc = ReplaceSheetWithCSV_regex Timeseries $global:timeseriescsv $xmlsrc\r\n    $xmlsrc = ReplaceSheetWithCSV_regex TimeseriesCLAT $global:timeseriesclatcsv $xmlsrc\r\n    $xmlsrc = ReplaceSheetWithCSV_regex TimeseriesSLAT $global:timeseriesslatcsv $xmlsrc\r\n    $xmlsrc = ReplaceSheetWithCSV_regex Tests $global:testcsv $xmlsrc\r\n    # Potentially add exceedance data if we have it\r\n    if ($global:fioOutputFormat -eq \"json+\") {\r\n        $csv = CombineExceedanceCSV @(1, 4, 16, 32) \"Rand\" 30 4096 1 \"exceedance30\"\r\n        $xmlsrc = ReplaceSheetWithCSV_regex Exceedance $csv $xmlsrc\r\n    }\r\n    # Remove draw:image references to deleted binary previews\r\n    $xmlsrc = $xmlsrc -replace \"<draw:image.*?/>\",\"\"\r\n    $xmlsrc = $xmlsrc -replace \"_DRIVE\",$global:physDrive -replace \"_TESTCAP\",$global:testcapacity -replace \"_MODEL\",$global:model -replace \"_SERIAL\",$global:serial -replace \"_OS\",$global:os -replace \"_FIO\",$global:fioVerString\r\n    UpdateContentXMLToODS_text $global:odssrc $global:odsdest $xmlsrc\r\n}\r\n\r\n\r\n\r\nfunction CreateIcon()\r\n{\r\n    $iconb64 = @'\r\nAAABAAEAICAAAAEAIABDAgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p6\r\n9AAAAgpJREFUWIXtlz9rVEEUxX+zTqW9gn8Sm6ikErTQJPoB3E4UIogQ3WiiqIh/CGJhsYkG\r\ngmCjxqwaYqO9H0DcqI1gJ2YNSLTKB9BCcmcs4oxv3s7ukpDNFHrgwbwz975z5s5w33vKWgvA\r\n4bH3ReAisBfYQnuwCHwEym9uHnwLoKy19JVn7wAjbRJthJHZW33jquf26yLwap3FAQQ4oI2R\r\nCwnEATYAV7UR2ZfIAECvtkY2JzSwQxtZSqgP2hhJa8CmNmAktYF/vgKrOQOfHp6q47qHZzzf\r\nPTzTNC4LtXvwqV2pgc+PB5aTlQp492JTSgXj/Pyes888F5yBWqXkx7tKlTouyzt0nZlqaTgf\r\nMzd12nMFK4IV8ULOca1SworQNTAZrOLLk8FgRbVKycc2gtOIcQVjhGwVXJkA5qeHMEai5XT3\r\n7pqfHmpoIK+R5bQxYSvOi+Tnd5683/Q+bqC+3TtOx0rXeeKeH399fikwFitlK8RiHOcPYUf/\r\nBN9eXAu2oKN/4m/CHz7LLa8kbiD2vOxCXF7BiOCu7cfHg339/vJ6Uw4glu/4fK6b23bsrs9R\r\nW4+OrrgPrCXSt+LkL6P/3wPJt8DI0iLt+xVrhQVtjXwAiokMVLURGQOOAKpV9BrjF1BW1lo2\r\nHTp/AxgF9DqKX/5RffBIuV69sefcfuAK0At0tkl4AagC5Z/vJucAfgOSfC+wPSfmJAAAAABJ\r\nRU5ErkJggg==\r\n'@\r\n    # Load the icon as a bitmap for user\r\n    $iconBitmap = New-Object System.Windows.Media.Imaging.BitmapImage\r\n    $iconBitmap.BeginInit()\r\n    $iconBitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($iconb64)\r\n    $iconBitmap.EndInit()\r\n    $iconBitmap.Freeze()\r\n    return $iconBitmap\r\n}\r\n\r\n\r\n\r\n$global:fio = \"\"          # FIO executable\r\n$global:fioVerString = \"\" # FIO self-reported version\r\n$global:fioOutputFormat = \"json\" # Can we make exceedance charts using JSON+ output?\r\n$global:physDrive = \"\"    # Device path to test\r\n$global:utilization = \"\"  # Device utilization % 1..100\r\n$global:yes = $false      # Skip user verification\r\n$global:nullio = $false   # Use the null IO engine, no real transfers done\r\n$global:fastPrecond = $false  # Only do one sequential fill, no other preconditioning\r\n$global:ioengine = \"windowsaio\"   # FIO engine to use for simplicity\r\n$global:quickie = $false   # Do short shadown test, non-standard\r\n\r\n$global:cpu = \"\"         # CPU model\r\n$global:cpuCores = \"\"    # # of cores (including virtual)\r\n$global:cpuFreqMHz = \"\"  # \"Nominal\" speed of CPU\r\n$global:uname = \"\"       # Kernel name/info\r\n\r\n$global:physDriveGiB = \"\"  # Disk size in GiB (2^n)\r\n$global:physDriveGB = \"\"   # Disk size in GB (10^n)\r\n$global:physDriveBase = \"\" # Basename (ex: nvme0n1)\r\n$global:testcapacity = \"\"  # Total GiB to test\r\n$global:model = \"\"         # Drive model name\r\n$global:serial = \"\"        # Drive serial number\r\n\r\n$global:ds = \"\"  # Datestamp to appent to files/directories to uniquify\r\n\r\n$global:details = \"\"       # Test details directory\r\n$global:testcsv = \"\"       # Intermediate test output CSV file\r\n$global:timeseriescsv = \"\" # Intermediate iostat output CSV file\r\n$global:timeseriesclatcsv = \"\" # Intermediate iostat output CSV file\r\n$global:timeseriesslatcsv = \"\" # Intermediate iostat output CSV file\r\n\r\n$global:odssrc = \"\"  # Original ODS spreadsheet file\r\n$global:odsdest = \"\" # Generated results ODS spreadsheet file\r\n\r\n$global:oc = New-Object System.Collections.ObjectModel.ObservableCollection[Object] # The list of tests to run\r\n\r\n$global:iconBitmap = CreateIcon\r\n$global:scriptName = $MyInvocation.MyCommand.Name\r\n\r\nCheckAdmin\r\nParseArgs\r\nFindFIO\r\nCheckFIOVersion\r\nCollectSystemInfo\r\nCollectDriveInfo\r\nVerifyContinue\r\nSetupFiles\r\n\r\n# $globals == The \"global\" variables to pass into the FIO runner script\r\n$global:globals  = \"`$fio = `\"$global:fio`\";\"\r\n$global:globals += \"`$fioOutputFormat = `\"$global:fioOutputFormat`\";\"\r\n$global:globals += \"`$physDrive = `\"$global:physDrive`\";\"\r\n$global:globals += \"`$testcapacity = `\"$global:testcapacity`\";\"\r\n$global:globals += \"`$timeseriescsv = `\"$global:timeseriescsv`\";\"\r\n$global:globals += \"`$timeseriesclatcsv = `\"$global:timeseriesclatcsv`\";\"\r\n$global:globals += \"`$timeseriesslatcsv = `\"$global:timeseriesslatcsv`\";\"\r\n$global:globals += \"`$testcsv = `\"$global:testcsv`\";\"\r\n$global:globals += \"`$physDriveBase = `\"$global:physDriveBase`\";\"\r\n$global:globals += \"`$physDriveNo = `\"$global:physDriveNo`\";\"\r\n$global:globals += \"`$details= `\"$global:details`\";\"\r\n$global:globals += \"`$ds = `\"$global:ds`\";\"\r\n$global:globals += \"`$ioengine = `\"$global:ioengine`\";\"\r\nif ( $globals:quickie ) {\r\n    $global:globals += \"`$quickie = 1;\"\r\n} else {\r\n    $global:globals += \"`$quickie = 0;\"\r\n}\r\n\r\n\r\nDefineTests\r\nif ($global:testmode -eq \"cli\") { RunAllTestsCLI }\r\nelse { RunAllTests }\r\n# GenerateResultODS # Done in the RunAllTests function\r\n"
  },
  {
    "path": "ezfio.py",
    "content": "#!/usr/bin/python3\n\n\"\"\"ezfio 1.9\nearlephilhower@yahoo.com\n\n------------------------------------------------------------------------\nezfio is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or\n(at your option) any later version.\n\nezfio is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with ezfio.  If not, see <http://www.gnu.org/licenses/>.\n------------------------------------------------------------------------\n\nUsage:   ./ezfio.py -d </dev/node> [-u <100..1>]\nExample: ./ezfio.py -d /dev/nvme0n1 -u 100\n\nThis script requires root privileges so must be run as \"root\" or\nvia \"sudo ./ezfio.py\"\n\nPlease be sure to have FIO installed, or you will be prompted to install\nand re-run the script.\"\"\"\n\nfrom __future__ import print_function\nimport argparse\nimport base64\nfrom collections import OrderedDict\nimport datetime\nimport glob\nimport json\nimport os\nimport platform\nimport pwd\nimport re\nimport shutil\nimport socket\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nimport zipfile\n\n\ndef AppendFile(text, filename):\n    \"\"\"Equivalent to >> in BASH, append a line to a text file.\"\"\"\n    with open(filename, \"a\") as f:\n        f.write(text)\n        f.write(\"\\n\")\n\n\ndef Run(cmd):\n    \"\"\"Run a cmd[], return the exit code, stdout, and stderr.\"\"\"\n    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,\n                            stderr=subprocess.PIPE)\n    out = proc.stdout.read()\n    err = proc.stderr.read()\n    code = proc.wait()\n    return int(code), out.decode('UTF-8'), err.decode('UTF-8')\n\n\ndef CheckAdmin():\n    \"\"\"Check that we have root privileges for disk access, abort if not.\"\"\"\n    if os.geteuid() != 0:\n        sys.stderr.write(\"Root privileges are required for low-level disk \")\n        sys.stderr.write(\"access.\\nPlease restart this script as root \")\n        sys.stderr.write(\"(sudo) to continue.\\n\")\n        sys.exit(1)\n\n\ndef FindFIO():\n    \"\"\"Try the path and the CWD for a FIO executable, return path or exit.\"\"\"\n    # Determine if FIO is in path or CWD\n    try:\n        ret, out, err = Run([\"fio\", \"-v\"])\n        if ret == 0:\n            return \"fio\"\n    except:\n        try:\n            ret, out, err = Run(['./fio', '-v'])\n            if ret == 0:\n                return \"./fio\"\n        except:\n            sys.stderr.write(\"FIO is required to run IO tests.\\n\")\n            sys.stderr.write(\"The latest versions can be found at \")\n            sys.stderr.write(\"https://github.com/axboe/fio.\\n\")\n            sys.exit(1)\n\n\ndef CheckFIOVersion():\n    \"\"\"Check that we have a version of FIO installed that we can use.\"\"\"\n    global fio, fioVerString, fioOutputFormat\n    code, out, err = Run([fio, '--version'])\n    try:\n        fioVerString = out.split('\\n')[0].rstrip()\n        ver = out.split('\\n')[0].rstrip().split('-')[1].split('.')[0]\n        if int(ver) < 2:\n            sys.stderr.write(\"ERROR: FIO version \" + ver + \" unsupported, \")\n            sys.stderr.write(\"version 2.0 or later required.  Exiting.\\n\")\n            sys.exit(2)\n    except:\n        sys.stderr.write(\"ERROR: Unable to determine version of fio \" +\n                          \"installed.  Exiting.\\n\")\n        sys.exit(2)\n    # Now see if we can make exceedance charts\n    # Can't just try --output-format=json+ because the FIO in Ubuntu 16.04\n    # repo doesn't understand it and *silently ignores ir*.  Instead, use\n    # the help output to see if \"json+\" exists at all...\n    try:\n        code, out, err = Run([fio, '--help'])\n        if (code == 0) and (\"json+\" in out):\n            fioOutputFormat = \"json+\"\n    except:\n        pass\n\n\ndef CheckAIOLimits():\n    \"\"\"Ensure kernel AIO max transactions is large enough to run test.\"\"\"\n    global aioNeeded\n    # If anything fails, silently continue.  FIO will give error if it\n    # can't run due to the AIO setting later on.\n    try:\n        code, out, err = Run(['cat', '/proc/sys/fs/aio-max-nr'])\n        if code == 0:\n            aiomaxnr = int(out.split(\"\\n\")[0].rstrip())\n            if aiomaxnr < int(aioNeeded):\n                sys.stderr.write(\n                    \"ERROR: The kernel's maximum outstanding async IO\" +\n                    \"setting (aio-max-nr) is too\\n\")\n                sys.stderr.write(\"       low to complete the test run.  Required value is \" + str(\n                    aioNeeded) + \", current is \" + str(aiomaxnr) + \"\\n\")\n                sys.stderr.write(\n                    \"       To fix this temporarially, please execute the following command:\\n\")\n                sys.stderr.write(\n                    \"            sudo sysctl -w fs.aio-max-nr=\" + str(aioNeeded) + \"\\n\")\n                sys.stderr.write(\"Unable to continue.  Exiting.\\n\")\n                sys.exit(2)\n    except:\n        pass\n\n\ndef ParseArgs():\n    \"\"\"Parse command line options into globals.\"\"\"\n    global physDrive, physDriveDict, physDriveTxt, utilization, nullio, isFile\n    global outputDest, offset, cluster, yes, quickie, verify, fastPrecond\n    global readOnly, compressPct\n\n    parser = argparse.ArgumentParser(\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        description=\"A tool to easily run FIO to benchmark sustained \"\n        \"performance of NVME\\nand other types of SSD.\",\n        epilog=\"\"\"\nRequirements:\\n\n* Root access (log in as root, or sudo {prog})\n* No filesytems or data on target device\n* FIO IO tester (available https://github.com/axboe/fio)\n* sdparm to identify the NVME device and serial number\n\nWARNING: All data on the target device will be DESTROYED by this test.\"\"\")\n    parser.add_argument(\"--cluster\", dest=\"cluster\", action='store_true',\n                        help=\"Run the test on a cluster (--drive in \"+\n                        \"host1:/dev/p1,host2:/dev/ps,...)\", required=False)\n    parser.add_argument(\"--verify\", dest=\"verify\", action='store_true',\n                        help=\"Have FIO perform data verifications on reads.\"+\n                        \" May impact performance\", required=False)\n    parser.add_argument(\"--drive\", \"-d\", dest=\"physDrive\",\n                        help=\"Device to test (ex: /dev/nvme0n1)\", required=True)\n    parser.add_argument(\"--utilization\", \"-u\", dest=\"utilization\",\n                        help=\"Amount of drive to test (in percent), 1...100\",\n                        default=\"100\", type=int, required=False)\n    parser.add_argument(\"--offset\", \"-s\", dest=\"offset\",\n                        help=\"offset from start (in percent), 0...99\", default=\"0\",\n                        type=int, required=False)\n    parser.add_argument(\"--output\", \"-o\", dest=\"outputDest\",\n                        help=\"Location where results should be saved\", required=False)\n    parser.add_argument(\"--yes\", dest=\"yes\", action='store_true',\n                        help=\"Skip the final warning prompt (for scripted tests)\",\n                        required=False)\n    parser.add_argument(\"--fast-precondition\", dest='fastpre', action='store_true',\n                        help=\"Only do a single sequential write to precondition drive\",\n                        required=False)\n    parser.add_argument(\"--quickie\", dest=\"quickie\", help=argparse.SUPPRESS,\n                        action='store_true', required=False)\n    parser.add_argument(\"--file\", dest=\"file\", help=\"Test using a regular file, not a device\",\n                        action='store_true', required=False)\n    parser.add_argument(\"--nullio\", dest=\"nullio\", help=argparse.SUPPRESS,\n                        action='store_true', required=False)\n    parser.add_argument(\"--readonly\", dest=\"readonly\", help=\"Only run read-only tests, don't write to device\",\n                        action='store_true', required=False)\n    parser.add_argument(\"--compress_percentage\", dest=\"compresspct\", help=\"Set the target data compressibility\",\n                        default=\"100\", type=int, required=False)\n    args = parser.parse_args()\n\n    physDrive = args.physDrive\n    physDriveTxt = physDrive\n    utilization = args.utilization\n    outputDest = args.outputDest\n    offset = args.offset\n    yes = args.yes\n    quickie = args.quickie\n    nullio = args.nullio\n    verify = args.verify\n    fastPrecond = args.fastpre\n    cluster = args.cluster\n    isFile = args.file\n    readOnly = args.readonly\n    compressPct = args.compresspct\n\n    # For cluster mode, we add a new physDriveList dict and fake physDrive\n    if cluster:\n        nodes = physDrive.split(\",\")\n        for node in nodes:\n            physDriveDict[node.split(\":\")[0]] = node.split(\":\")[1]\n        physDrive = nodes[0].split(\":\")[1]\n\n    if (utilization < 1) or (utilization > 100):\n        print(\"ERROR:  Utilization must be between 1...100\")\n        parser.print_help()\n        sys.exit(1)\n\n    if (offset < 0) or (offset > 99) or (offset+utilization > 100):\n        print(\"ERROR:  offset must be between 0...99 while offset + utilization <= 100\")\n        parser.print_help()\n        sys.exit(1)\n    # Sanity check that the selected drive is not mounted by parsing mounts\n    # This is not guaranteed to catch all as there's just too many different\n    # naming conventions out there.  Let's cover simple HDD/SSD/NVME patterns\n    pdispart = (re.match('.*p?[1-9][0-9]*$', physDrive) and\n                not re.match('.*/nvme[0-9]+n[1-9][0-9]*$', physDrive))\n    hit = \"\"\n    with open(\"/proc/mounts\", \"r\") as f:\n        mounts = f.readlines()\n    for l in mounts:\n        dev = l.split()[0]\n        mnt = l.split()[1]\n        if dev == physDrive:\n            hit = dev + \" on \" + mnt  # Obvious exact match\n        if pdispart:\n            chkdev = dev\n        else:\n            # /dev/sdp# is special case, don't remove the \"p\"\n            if re.match('^/dev/sdp.*$', dev):\n                chkdev = re.sub('[1-9][0-9]*$', '', dev)\n            else:\n                # Need to see if mounted partition is on a raw device being tested\n                chkdev = re.sub('p?[1-9][0-9]*$', '', dev)\n        if chkdev == physDrive:\n            hit = dev + \" on \" + mnt\n    if hit != \"\":\n        print(\"ERROR:  Mounted volume '\" + str(hit) + \"' is on same device\" +\n              \"as tested device '\" + str(physDrive) + \"'.  ABORTING.\")\n        sys.exit(2)\n\n\ndef grep(inlist, regex):\n    \"\"\"Implement grep in a non-Pythonic way to make it comprehensible to humans\"\"\"\n    out = []\n    for i in inlist:\n        if re.search(regex, i):\n            out = out + [i]\n    return out\n\n\ndef CollectSystemInfo():\n    \"\"\"Collect some OS and CPU information.\"\"\"\n    global cpu, cpuCores, cpuFreqMHz, uname\n    uname = \" \".join(platform.uname())\n    code, cpuinfo, err = Run(['cat', '/proc/cpuinfo'])\n    cpuinfo = cpuinfo.split(\"\\n\")\n    if 'aarch64' in uname:\n        code, cpuinfo, err = Run(['lscpu'])\n        cpuinfo = cpuinfo.split(\"\\n\")\n        cpu = grep(cpuinfo, r'Model name')[0].split(':')[1].lstrip()\n        cpuCores = grep(cpuinfo, r'CPU')[1].split(':')[1].lstrip()\n        try:\n            code, dmidecode, err = Run(['dmidecode', '--type', 'processor'])\n            cpuFreqMHz = int(round(float(grep(dmidecode.split(\"\\n\"), r'Current Speed')[0].rstrip().lstrip().split(\" \")[2])))\n        except:\n            cpuFreqMHz = grep(cpuinfo, r'max')[0].split(':')[1].lstrip()\n    elif 'ppc64' in uname:\n        # Implement grep and sed in Python...\n        cpu = grep(cpuinfo, r'model')[0].split(': ')[1].replace('(R)', '').replace('(TM)', '')\n        cpuCores = len(grep(cpuinfo, r'processor'))\n        try:\n            code, dmidecode, err = Run(['dmidecode', '--type', 'processor'])\n            cpuFreqMHz = int(round(float(grep(dmidecode.split(\"\\n\"), r'Current Speed')[0].rstrip().lstrip().split(\" \")[2])))\n        except:\n            cpuFreqMHz = int(round(float(grep(cpuinfo, r'clock')[0].split(': ')[1][:-3])))\n    else:\n        model_names = grep(cpuinfo, r'model name')\n        cpu = model_names[0].split(': ')[1].replace('(R)', '').replace('(TM)', '')\n        cpuCores = len(model_names)\n        try:\n            code, dmidecode, err = Run(['dmidecode', '--type', 'processor'])\n            cpuFreqMHz = int(round(float(grep(dmidecode.split(\"\\n\"), r'Current Speed')[0].rstrip().lstrip().split(\" \")[2])))\n        except:\n            cpuFreqMHz = int(round(float(grep(cpuinfo, r'cpu MHz')[0].split(': ')[1])))\n\n\ndef VerifyContinue():\n    \"\"\"User's last chance to abort the test.  Exit if they don't agree.\"\"\"\n    if not yes:\n        print(\"-\" * 75)\n        print(\"WARNING! \" * 9)\n        print(\"THIS TEST WILL DESTROY ANY DATA AND FILESYSTEMS ON \" + physDrive)\n        cont = input(\"Please type the word \\\"yes\\\" and hit return to \" +\n                         \"continue, or anything else to abort.\")\n        print(\"-\" * 75 + \"\\n\")\n        if cont != \"yes\":\n            print(\"Performance test aborted, drive is untouched.\")\n            sys.exit(1)\n\n\ndef CollectDriveInfo():\n    \"\"\"Get important device information, exit if not possible.\"\"\"\n    global physDriveGiB, physDriveGB, physDriveBase, testcapacity, testoffset\n    global model, serial, physDrive, isFile\n    # We absolutely need this information\n    pd = physDrive.split(',')[0]\n    try:\n        if isFile:\n            physDriveBase = os.path.basename(pd)\n            physDriveBytes = str(os.stat(pd).st_size) + \"\\n\"\n        else:\n            physDriveBase = os.path.basename(pd)\n            code, physDriveBytes, err = Run(['blockdev', '--getsize64', pd])\n            if code != 0:\n                raise Exception(\"Can't get drive size for \" + pd)\n        physDriveBytes = physDriveBytes.split('\\n')[0]\n        physDriveBytes = int(physDriveBytes)\n        physDriveGB = int(physDriveBytes / (1000 * 1000 * 1000))\n        physDriveGiB = int(physDriveBytes / (1024 * 1024 * 1024))\n        testcapacity = int((physDriveGiB * utilization) / 100)\n        testoffset = int((physDriveGiB * offset) / 100)\n    except:\n        print(\"ERROR: Can't get '\" + pd + \"' size. Incorrect device name?\")\n        sys.exit(1)\n    # These are nice to have, but we can run without it\n    model = \"UNKNOWN\"\n    serial = \"UNKNOWN\"\n    try:\n        nvmeclicmd = ['nvme', 'list', '--output-format=json']\n        code, nvmecli, err = Run(nvmeclicmd)\n        if code == 0:\n            j = json.loads(nvmecli)\n            for drive in j['Devices']:\n                if drive['DevicePath'] == pd:\n                    model = drive['ModelNumber']\n                    serial = drive['SerialNumber']\n                    return\n    except:\n        pass  # An error in nvme is not a problem\n    try:\n        sdparmcmd = ['sdparm', '--page', 'sn', '--inquiry', '--long', pd]\n        code, sdparm, err = Run(sdparmcmd)\n        lines = sdparm.split(\"\\n\")\n        if len(lines) == 4:\n            model = re.sub(\n                r'\\s+', \" \", lines[0].split(\":\")[1].lstrip().rstrip())\n            serial = re.sub(r'\\s+', \" \", lines[2].lstrip().rstrip())\n        else:\n            print(\"Unable to identify drive using sdparm. Continuing.\")\n    except:\n        print(\"Install sdparm to allow model/serial extraction. Continuing.\")\n\n\ndef CSVInfoHeader(f):\n    \"\"\"Headers to the CSV file (ending up in the ODS at the test end).\"\"\"\n    global physDriveTxt, model, serial, physDriveGiB, testcapacity, testoffset\n    global cpu, cpuCores, cpuFreqMHz, uname, quickie, fastPrecond\n    if quickie:\n        prefix = \"QUICKIE-INVALID-RESULTS-\"\n    else:\n        prefix = \"\"\n    if fastPrecond:\n        prefix = \"FASTPRECOND-\" + prefix\n    AppendFile(\"Drive,\" + prefix + str(physDriveTxt).replace(\",\", \" \"), f)\n    AppendFile(\"Model,\" + prefix + str(model), f)\n    AppendFile(\"Serial,\" + prefix + str(serial), f)\n    AppendFile(\"AvailCapacity,\" + prefix + str(physDriveGiB) + \",GiB\", f)\n    if offset == 0:\n        testcap = str(testcapacity)\n    else:\n        testcap = str(testcapacity) + \" @ \" + str(testoffset)\n    AppendFile(\"TestedCapacity,\" + prefix + str(testcap) + \",GiB\", f)\n    AppendFile(\"CPU,\" + prefix + str(cpu), f)\n    AppendFile(\"Cores,\" + prefix + str(cpuCores), f)\n    AppendFile(\"Frequency,\" + prefix + str(cpuFreqMHz), f)\n    AppendFile(\"OS,\" + prefix + str(uname), f)\n    AppendFile(\"FIOVersion,\" + prefix + str(fioVerString), f)\n\n\ndef SetupFiles():\n    \"\"\"Set up names for all output/input files, place headers on CSVs.\"\"\"\n    global ds, details, testcsv, timeseriescsv, odssrc, odsdest\n    global physDriveBase, fioVerString, outputDest, timeseriesclatcsv\n    global timeseriesslatcsv\n\n    # Datestamp for run output files\n    ds = datetime.datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\")\n\n    # The unique suffix we generate for all output files\n    suffix = str(physDriveGB) + \"GB_\" + str(cpuCores) + \"cores_\"\n    suffix += str(cpuFreqMHz) + \"MHz_\" + physDriveBase + \"_\"\n    suffix += socket.gethostname() + \"_\" + ds\n\n    if not outputDest:\n        outputDest = os.getcwd()\n    # The \"details\" directory contains the raw output of each FIO run\n    details = outputDest + \"/details_\" + suffix\n    if os.path.exists(details):\n        shutil.rmtree(details)\n    os.makedirs(details)\n    # Copy this script into it for posterity\n    shutil.copyfile(__file__, details + \"/\" + os.path.basename(__file__))\n\n    # Files we're going to generate, encode some system info in the names\n    # If the output files already exist, erase them\n    testcsv = details + \"/ezfio_tests_\"+suffix+\".csv\"\n    if os.path.exists(testcsv):\n        os.unlink(testcsv)\n    CSVInfoHeader(testcsv)\n    AppendFile(\"Type,Write %,Block Size,Threads,Queue Depth/Thread,IOPS,\" +\n               \"Bandwidth (MB/s),Read Latency (us),Write Latency (us),\" +\n               \"System CPU,User CPU\", testcsv)\n    timeseriescsv = details + \"/ezfio_timeseries_\"+suffix+\".csv\"\n    timeseriesclatcsv = details + \"/ezfio_timeseriesclat_\"+suffix+\".csv\"\n    timeseriesslatcsv = details + \"/ezfio_timeseriesslat_\"+suffix+\".csv\"\n    for f in [timeseriescsv, timeseriesclatcsv, timeseriesslatcsv]:\n        if os.path.exists(f):\n            os.unlink(f)\n        CSVInfoHeader(f)\n    AppendFile(\",\".join([\"IOPS\"] + list(physDriveDict.keys())),\n               timeseriescsv)  # Add IOPS header\n    hdr = \"\"\n    for host in physDriveDict.keys():\n        hdr = hdr + ',' + host + \"-read\"\n        hdr = hdr + ',' + host + \"-write\"\n    AppendFile('CLAT-read,CLAT-write' + hdr,\n               timeseriesclatcsv)  # Add IOPS header\n    AppendFile('SLAT-read,SLAT-write' + hdr,\n               timeseriesslatcsv)  # Add IOPS header\n\n    # ODS input and output files\n    odssrc = os.path.dirname(os.path.realpath(__file__)) + \"/original.ods\"\n    if not os.path.exists(odssrc):\n        print(\"ERROR: Can't find original ODS spreadsheet '\" + odssrc + \"'.\")\n        sys.exit(1)\n    odsdest = outputDest + \"/ezfio_results_\"+suffix+\".ods\"\n    if os.path.exists(odsdest):\n        os.unlink(odsdest)\n\n\nclass FIOError(Exception):\n    \"\"\"Exception generated when FIO returns a non-success value\n\n    Attributes:\n        cmdline -- The FIO command that was executed\n        code    -- Error code FIO returned\n        stderr  -- STDERR output from FIO\n        stdout  -- STDOUT output from FIO\n    \"\"\"\n\n    def __init__(self, cmdline, code, stderr, stdout):\n        super(FIOError, self).__init__()\n        self.cmdline = cmdline\n        self.code = code\n        self.stderr = stderr\n        self.stdout = stdout\n\n\ndef TestName(seqrand, wmix, bs, threads, iodepth):\n    \"\"\"Return full path and filename prefix for test of specified params\"\"\"\n    global details, physDriveBase\n    testfile = str(details) + \"/Test\" + str(seqrand) + \"_w\" + str(wmix)\n    testfile += \"_bs\" + str(bs) + \"_threads\" + str(threads) + \"_iodepth\"\n    testfile += str(iodepth) + \"_\" + str(physDriveBase) + \".out\"\n    return testfile\n\n\ndef SequentialConditioning():\n    \"\"\"Sequentially fill the complete capacity of the drive once.\"\"\"\n    global quickie, fastPrecond, nullio, readOnly, compressPct\n\n    def GenerateJobfile(drive, testcapacity, testoffset):\n        \"\"\"Write the sequential jobfile for a single server\"\"\"\n        jobfile = tempfile.NamedTemporaryFile(delete=False, mode='w')\n        for dr in drive.split(','):\n            jobfile.write(\"[SeqCond-\" + dr + \"]\\n\")\n            # Note that we can't use regular test runner because this test needs\n            # to run for a specified # of bytes, not a specified # of seconds.\n            jobfile.write(\"readwrite=write\\n\")\n            jobfile.write(\"bs=128k\\n\")\n            if nullio:\n                jobfile.write(\"ioengine=null\\n\")\n            else:\n                jobfile.write(\"ioengine=libaio\\n\")\n            jobfile.write(\"iodepth=64\\n\")\n            jobfile.write(\"direct=1\\n\")\n            jobfile.write(\"filename=\" + str(dr) + \"\\n\")\n            if quickie:\n                jobfile.write(\"size=1G\\n\")\n            else:\n                jobfile.write(\"size=\" + str(testcapacity) + \"G\\n\")\n            jobfile.write(\"thread=1\\n\")\n            jobfile.write(\"offset=\" + str(testoffset) + \"G\\n\")\n            if compressPct != 100:\n                jobfile.write(\"buffer_compress_percentage=\" + str(compressPct) + \"\\n\")\n        jobfile.close()\n        return jobfile\n\n    cmdline = [fio]\n    if not cluster:\n        jobfile = GenerateJobfile(physDrive, testcapacity, testoffset)\n        cmdline = cmdline + [jobfile.name]\n    else:\n        jobfile = []\n        for host in physDriveDict.keys():\n            newjob = GenerateJobfile(\n                physDriveDict[host], testcapacity, testoffset)\n            cmdline = cmdline + ['--client=' + str(host), str(newjob.name)]\n            jobfile = jobfile + [newjob]\n    cmdline = cmdline + ['--output-format=' + str(fioOutputFormat)]\n\n    if not readOnly:\n        code, out, err = Run(cmdline)\n    else:\n        code = 0\n\n    if cluster:\n        for job in jobfile:\n            os.unlink(job.name)\n    else:\n        os.unlink(jobfile.name)\n\n    if code != 0:\n        raise FIOError(\" \".join(cmdline), code, err, out)\n    else:\n        return \"DONE\", \"DONE\", \"DONE\"\n\n\ndef RandomConditioning():\n    \"\"\"Randomly write entire device for the full capacity\"\"\"\n    global quickie, nullio, readOnly, compressPct\n\n    def GenerateJobfile(drive, testcapacity, testoffset):\n        \"\"\"Write the random jobfile\"\"\"\n        jobfile = tempfile.NamedTemporaryFile(delete=False, mode='w')\n        for dr in drive.split(','):\n            jobfile.write(\"[RandCond-\" + dr + \"]\\n\")\n            # Note that we can't use regular test runner because this test needs\n            # to run for a specified # of bytes, not a specified # of seconds.\n            jobfile.write(\"readwrite=randwrite\\n\")\n            jobfile.write(\"bs=4k\\n\")\n            jobfile.write(\"invalidate=1\\n\")\n            jobfile.write(\"end_fsync=0\\n\")\n            jobfile.write(\"group_reporting=1\\n\")\n            jobfile.write(\"direct=1\\n\")\n            jobfile.write(\"filename=\" + str(dr) + \"\\n\")\n            if quickie:\n                jobfile.write(\"size=1G\\n\")\n            else:\n                jobfile.write(\"size=\" + str(testcapacity) + \"G\\n\")\n            if nullio:\n                jobfile.write(\"ioengine=null\\n\")\n            else:\n                jobfile.write(\"ioengine=libaio\\n\")\n            jobfile.write(\"iodepth=256\\n\")\n            jobfile.write(\"norandommap\\n\")\n            jobfile.write(\"randrepeat=0\\n\")\n            jobfile.write(\"thread=1\\n\")\n            jobfile.write(\"offset=\" + str(testoffset) + \"G\\n\")\n            if compressPct != 100:\n                jobfile.write(\"buffer_compress_percentage=\" + str(compressPct) + \"\\n\")\n        jobfile.close()\n        return jobfile\n\n    cmdline = [fio]\n    if not cluster:\n        jobfile = GenerateJobfile(physDrive, testcapacity, testoffset)\n        cmdline = cmdline + [jobfile.name]\n    else:\n        jobfile = []\n        for host in physDriveDict.keys():\n            newjob = GenerateJobfile(\n                physDriveDict[host], testcapacity, testoffset)\n            cmdline = cmdline + ['--client=' + str(host), str(newjob.name)]\n            jobfile = jobfile + [newjob]\n    cmdline = cmdline + ['--output-format=' + str(fioOutputFormat)]\n\n    if not readOnly:\n        code, out, err = Run(cmdline)\n    else:\n        code = 0\n\n    if cluster:\n        for job in jobfile:\n            os.unlink(job.name)\n    else:\n        os.unlink(jobfile.name)\n\n    if code != 0:\n        raise FIOError(\" \".join(cmdline), code, err, out)\n    else:\n        return \"DONE\", \"DONE\", \"DONE\"\n\n\ndef RunTest(iops_log, seqrand, wmix, bs, threads, iodepth, runtime):\n    \"\"\"Runs the specified test, generates output CSV lines.\"\"\"\n    global cluster, physDriveDict, compressPct\n\n    # Taken from fio_latency2csv.py - needed to convert funky semi-log to normal latencies\n    def plat_idx_to_val(idx, FIO_IO_U_PLAT_BITS=6, FIO_IO_U_PLAT_VAL=64):\n        \"\"\"Convert from lat bucket to real value, for obsolete FIO revisions\"\"\"\n        # MSB <= (FIO_IO_U_PLAT_BITS-1), cannot be rounded off. Use\n        # all bits of the sample as index\n        if idx < (FIO_IO_U_PLAT_VAL << 1):\n            return idx\n        # Find the group and compute the minimum value of that group\n        error_bits = (idx >> FIO_IO_U_PLAT_BITS) - 1\n        base = 1 << (error_bits + FIO_IO_U_PLAT_BITS)\n        # Find its bucket number of the group\n        k = idx % FIO_IO_U_PLAT_VAL\n        # Return the mean of the range of the bucket\n        return base + ((k + 0.5) * (1 << error_bits))\n\n    def WriteExceedance(j, rdwr, outfile):\n        \"\"\"Generate an exceedance CSV for read or write from JSON output.\"\"\"\n        global fioOutputFormat\n        if fioOutputFormat == \"json\":\n            return  # This data not present in JSON format, only JSON+\n        # Generate a dict of combined bins, either for jobs[0] or client_stats[]\n        bins = {}\n        ios = 0\n        try:\n            # Non-cluster case will have jobs, only a single one needed\n            ios = j['jobs'][0][rdwr]['total_ios']\n            if ('N' in j['jobs'][0][rdwr]['clat_ns']) and (j['jobs'][0][rdwr]['clat_ns']['N'] > 0): \n                bins = j['jobs'][0][rdwr]['clat_ns']['bins']\n            else:\n                bins = {}\n        except:\n            # Cluster case will have client_stats to combine\n            for client_stats in j['client_stats']:\n                if client_stats['jobname'] == 'All clients':\n                    # Don't bother looking at combined, bins doesn't exist there\n                    continue\n                if client_stats[rdwr]['total_ios']:\n                    ios = ios + client_stats[rdwr]['total_ios']\n                    for k in client_stats[rdwr]['clat_ns']['bins'].keys():\n                        try:\n                            bins[k] = bins[k] + client_stats[rdwr]['clat_ns']['bins'][k]\n                        except:\n                            bins[k] = client_stats[rdwr]['clat_ns']['bins'][k]\n        #ios = client[rdwr]['total_ios']\n        #bins = client[rdwr]['clat_ns']['bins']\n        if ios:\n            runttl = 0\n            # This was changed in 2.99 to be in nanoseconds and to discard the crazy _bits magic\n            if float(fioVerString.split('-')[1]) >= 2.99:\n                lat_ns = []\n                # JSON dict has keys of type string, need a sorted integer list for our work...\n                for entry in bins:\n                    lat_ns.append(int(entry))\n                for entry in sorted(lat_ns):\n                    lat_us = float(entry) / 1000.0\n                    cnt = int(bins[str(entry)])\n                    runttl += cnt\n                    pctile = 1.0 - float(runttl) / float(ios)\n                    if cnt > 0:\n                        AppendFile(\n                            \",\".join((str(lat_us), str(pctile))), outfile)\n            else:\n                plat_bits = client[rdwr]['clat']['bins']['FIO_IO_U_PLAT_BITS']\n                plat_val = client[rdwr]['clat']['bins']['FIO_IO_U_PLAT_VAL']\n                for b in range(0, int(client[rdwr]['clat']['bins']['FIO_IO_U_PLAT_NR'])):\n                    cnt = int(client[rdwr]['clat']['bins'][str(b)])\n                    runttl += cnt\n                    pctile = 1.0 - float(runttl) / float(ios)\n                    if cnt > 0:\n                        AppendFile(\n                            \",\".join((str(plat_idx_to_val(b, plat_bits, plat_val)),\n                                      str(pctile))), outfile)\n\n    def GenerateJobfile(rw, wmix, bs, drive, testcapacity, runtime, threads, iodepth, testoffset):\n        \"\"\"Make a jobfile for the specified test parameters\"\"\"\n        global verify, nullio\n        jobfile = tempfile.NamedTemporaryFile(delete=False, mode='w')\n        for dr in drive.split(\",\"):\n            jobfile.write(\"[test-\" + dr + \"]\\n\")\n            jobfile.write(\"readwrite=\" + str(rw) + \"\\n\")\n            jobfile.write(\"rwmixwrite=\" + str(wmix) + \"\\n\")\n            jobfile.write(\"bs=\" + str(bs) + \"\\n\")\n            jobfile.write(\"invalidate=1\\n\")\n            jobfile.write(\"end_fsync=0\\n\")\n            jobfile.write(\"group_reporting=1\\n\")\n            jobfile.write(\"direct=1\\n\")\n            jobfile.write(\"filename=\" + str(dr) + \"\\n\")\n            jobfile.write(\"size=\" + str(testcapacity) + \"G\\n\")\n            jobfile.write(\"time_based=1\\n\")\n            jobfile.write(\"runtime=\" + str(runtime) + \"\\n\")\n            if nullio:\n                jobfile.write(\"ioengine=null\\n\")\n            else:\n                jobfile.write(\"ioengine=libaio\\n\")\n            jobfile.write(\"numjobs=\" + str(threads) + \"\\n\")\n            jobfile.write(\"iodepth=\" + str(iodepth) + \"\\n\")\n            jobfile.write(\"norandommap=1\\n\")\n            jobfile.write(\"randrepeat=0\\n\")\n            jobfile.write(\"thread=1\\n\")\n            jobfile.write(\"exitall=1\\n\")\n            if verify:\n                jobfile.write(\"verify=crc32c\\n\")\n                jobfile.write(\"random_generator=lfsr\\n\")\n            jobfile.write(\"offset=\" + str(testoffset) + \"G\\n\")\n            if compressPct != 100:\n                jobfile.write(\"buffer_compress_percentage=\" + str(compressPct) + \"\\n\")\n        jobfile.close()\n        return jobfile\n\n    def CombineThreadOutputs(suffix, outcsv, lat):\n        \"\"\"Merge all FIO iops/lat logs across all servers\"\"\"\n        # The lists may be called \"iops\" but the same works for clat/slat\n        iops = [0] * (runtime + extra_runtime)\n        # For latencies, need to keep the _w and _r separate\n        iops_w = [0] * (runtime + extra_runtime)\n        host_iops = OrderedDict()\n        host_iops_w = OrderedDict()\n        filecnt = 0\n        if not cluster:\n            pdd = OrderedDict()\n            pdd['localhost'] = 1 # Just the single host, faked here\n        else:\n            pdd = physDriveDict\n        for host in pdd.keys():\n            host_iops[host] = [0] * (runtime + extra_runtime)\n            host_iops_w[host] = [0] * (runtime + extra_runtime)\n            if not cluster:\n                fileglob = testfile + str(suffix) + '.*log'\n            else:\n                fileglob = testfile + str(suffix) + '.*.log.' + host\n            for filename in glob.glob(fileglob):\n                filecnt = filecnt + 1\n                catcmdline = ['cat', filename]\n                catcode, catout, caterr = Run(catcmdline)\n                if catcode != 0:\n                    AppendFile(\"ERROR\", testcsv)\n                    raise FIOError(\" \".join(catcmdline),\n                                   catcode, caterr, catout)\n                lines = catout.split(\"\\n\")\n                # Set time 0 IOPS to first values\n                riops = 0\n                wiops = 0\n                nexttime = 0\n                for x in range(0, runtime + extra_runtime):\n                    if not lat:\n                        iops[x] = iops[x] + riops + wiops\n                        host_iops[host][x] = host_iops[host][x] + riops + wiops\n                    else:\n                        iops[x] = iops[x] + riops\n                        iops_w[x] = iops_w[x] + wiops\n                        host_iops[host][x] = host_iops[host][x] + riops\n                        host_iops_w[host][x] = host_iops_w[host][x] + wiops\n                    while len(lines) > 1 and (nexttime < x):\n                        parts = lines[0].split(\",\")\n                        nexttime = float(parts[0]) / 1000.0\n                        if int(lines[0].split(\",\")[2]) == 1:\n                            wiops = int(parts[1])\n                        else:\n                            riops = int(parts[1])\n                        lines = lines[1:]\n\n        # Generate the combined CSV\n        with open(outcsv, 'a') as f:\n            for cnt in range(int(extra_runtime/2), runtime + extra_runtime):\n                if filecnt > 0 and lat:\n                    line = str(float(iops[cnt])/float(filecnt))\n                    line = line + ',' + str(float(iops_w[cnt])/float(filecnt))\n                else:\n                    line = str(iops[cnt])\n                if len(pdd.keys()) > 1:\n                    for host in pdd.keys():\n                        if filecnt > 0 and lat:\n                            line = line + ',' + \\\n                                str(float(host_iops[host][cnt])/float(filecnt))\n                            line = line + ',' + \\\n                                str(float(host_iops_w[host]\n                                          [cnt])/float(filecnt))\n                        else:\n                            line = line + \",\" + str(host_iops[host][cnt])\n                f.write(line + \"\\n\")\n\n    # Output file names\n    testfile = TestName(seqrand, wmix, bs, threads, iodepth)\n\n    if seqrand == \"Seq\":\n        rw = \"rw\"\n    else:\n        rw = \"randrw\"\n\n    if iops_log:\n        extra_runtime = 10\n    else:\n        extra_runtime = 0\n\n    cmdline = [fio]\n    if not cluster:\n        jobfile = GenerateJobfile(rw, wmix, bs, physDrive, testcapacity,\n                                  runtime + extra_runtime, threads, iodepth, testoffset)\n        cmdline = cmdline + [jobfile.name]\n        AppendFile(\"[JOBFILE]\", testfile)\n        with open(jobfile.name, 'r') as of:\n            txt = of.read()\n            AppendFile(txt, testfile)\n        if iops_log:\n            AppendFile(\"write_iops_log=\" + testfile, jobfile.name)\n            AppendFile(\"write_lat_log=\" + testfile, jobfile.name)\n            AppendFile(\"log_avg_msec=1000\", jobfile.name)\n            AppendFile(\"log_unix_epoch=0\", jobfile.name)\n    else:\n        jobfile = []\n        for host in physDriveDict.keys():\n            newjob = GenerateJobfile(rw, wmix, bs, physDriveDict[host], testcapacity,\n                                     runtime + extra_runtime, threads, iodepth, testoffset)\n            cmdline = cmdline + ['--client=' + str(host), str(newjob.name)]\n            AppendFile('[JOBFILE-' + str(host) + \"]\", testfile)\n            with open(newjob.name, 'r') as of:\n                txt = of.read()\n                AppendFile(txt, testfile)\n            jobfile = jobfile + [newjob]\n            if iops_log:\n                AppendFile(\"write_iops_log=\" + testfile, newjob.name)\n                AppendFile(\"write_lat_log=\" + testfile, newjob.name)\n                AppendFile(\"log_avg_msec=1000\", newjob.name)\n                AppendFile(\"log_unix_epoch=0\", newjob.name)\n\n    cmdline = cmdline + ['--output-format=' + str(fioOutputFormat)]\n\n    # There are some NVME drives with 4k physical and logical out there.\n    # Check that we can actually do this size IO, OTW return 0 for all\n    skiptest = False\n    code, out, err = Run(['blockdev', '--getpbsz', str(physDrive.split(',')[0])])\n    if code == 0:\n        iomin = int(out.split(\"\\n\")[0])\n        if int(bs) < iomin:\n            skiptest = True\n\n    if readOnly and wmix != 0:\n        skiptest = True \n\n    # Silently ignore failure to return min block size, FIO will fail and\n    # we'll catch that a little later.\n    if skiptest:\n        code = 0\n        out = \"Test not run because block size \" + str(bs)\n        out += \" below iominsize \" + str(iomin) + \"\\n\"\n        out += \"3;\" + \"0;\" * 100 + \"\\n\"  # Bogus 0-filled resulte line\n        err = \"\"\n    else:\n        code, out, err = Run(cmdline)\n    AppendFile(\"[STDOUT]\", testfile)\n    AppendFile(out, testfile)\n    AppendFile(\"[STDERR]\", testfile)\n    AppendFile(err, testfile)\n\n    if cluster:\n        for job in jobfile:\n            os.unlink(job.name)\n    else:\n        os.unlink(jobfile.name)\n\n    # Make sure we had successful completion, else note and abort run\n    if code != 0:\n        AppendFile(\"ERROR\", testcsv)\n        raise FIOError(\" \".join(cmdline), code, err, out)\n\n    if iops_log:\n        CombineThreadOutputs('_iops', timeseriescsv, False)\n        CombineThreadOutputs('_clat', timeseriesclatcsv, True)\n        CombineThreadOutputs('_slat', timeseriesslatcsv, True)\n\n    rdiops = 0\n    wriops = 0\n    rlat = 0\n    wlat = 0\n    syscpu = 0\n    usrcpu = 0\n    if not skiptest:\n        # Chomp anything before the json.\n        for i in range(0, len(out)):\n            if out[i] == '{':\n                out = out[i:]\n                break\n        j = json.loads(out)\n\n        if cluster and len(physDriveDict.keys()) == 1:\n            client = j['client_stats'][0]\n        elif cluster:\n            for res in j['client_stats']:\n                if res['jobname'] == \"All clients\":\n                    client = res\n                    break\n        else:\n            client = j['jobs'][0]\n\n        syscpu = float(client['sys_cpu'])\n        usrcpu = float(client['usr_cpu'])\n\n        rdiops = float(client['read']['iops'])\n        wriops = float(client['write']['iops'])\n\n        # 'lat' goes to 'lat_ns' in newest FIO JSON formats...ugh\n        try:\n            rlat = float(client['read']['lat_ns']['mean']) / 1000  # ns->us\n        except:\n            rlat = float(client['read']['lat']['mean'])\n        try:\n            wlat = float(client['write']['lat_ns']['mean']) / 1000  # ns->us\n        except:\n            wlat = float(client['write']['lat']['mean'])\n\n    iops = \"{0:0.0f}\".format(rdiops + wriops)\n    mbps = \"{0:0.2f}\".format((float((rdiops+wriops) * bs) /\n                              (1024.0 * 1024.0)))\n    lat = \"{0:0.1f}\".format(max(rlat, wlat))\n\n    AppendFile(\",\".join((str(seqrand), str(wmix), str(bs), str(threads),\n                         str(iodepth), str(iops), str(mbps), str(rlat),\n                         str(wlat), str(syscpu), str(usrcpu))), testcsv)\n\n    if skiptest:\n        AppendFile(\"1,1\\n\", testfile + \".exc.read.csv\")\n        AppendFile(\"1,1\\n\", testfile + \".exc.write.csv\")\n    else:\n        WriteExceedance(j, 'read', testfile + \".exc.read.csv\")\n        WriteExceedance(j, 'write', testfile + \".exc.write.csv\")\n\n    return iops, mbps, lat\n\n\ndef DefineTests():\n    \"\"\"Generate the work list for the main worker into OC.\"\"\"\n    global oc, quickie, fastPrecond\n    # What we're shmoo-ing across\n    bslist = (512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072)\n    qdlist = (1, 2, 4, 8, 16, 32, 64, 128, 256)\n    threadslist = (1, 2, 4, 8, 16, 32, 64, 128, 256)\n\n    shorttime = 120  # Runtime of point tests\n    longtime = 1200  # Runtime of long-running tests\n    if quickie:\n        shorttime = int(shorttime / 10)\n        longtime = int(longtime / 10)\n\n    def AddTest(name, seqrand, writepct, blocksize, threads, qdperthread,\n                iops_log, runtime, desc, cmdline):\n        \"\"\"Bare usage add a test to the list to execute\"\"\"\n        if threads != \"\":\n            qd = int(threads) * int(qdperthread)\n        else:\n            qd = 0\n        dat = {}\n        dat['name'] = name\n        dat['seqrand'] = seqrand\n        dat['wmix'] = writepct\n        dat['bs'] = blocksize\n        dat['qd'] = qd\n        dat['qdperthread'] = qdperthread\n        dat['threads'] = threads\n        dat['bw'] = ''\n        dat['iops'] = ''\n        dat['lat'] = ''\n        dat['desc'] = desc\n        dat['iops_log'] = iops_log\n        dat['runtime'] = runtime\n        dat['cmdline'] = cmdline\n        oc.append(dat)\n\n    def DoAddTest(testname, seqrand, wmix, bs, threads, iodepth, desc,\n                  iops_log, runtime):\n        \"\"\"Add an individual run to the list of tests to execute\"\"\"\n        AddTest(testname, seqrand, wmix, bs, threads, iodepth, iops_log,\n                runtime, desc, lambda o: {RunTest(o['iops_log'],\n                                                  o['seqrand'], o['wmix'],\n                                                  o['bs'], o['threads'],\n                                                  o['qdperthread'],\n                                                  o['runtime'])})\n\n    def AddTestBSShmoo():\n        \"\"\"Add a sequence of tests varying the block size\"\"\"\n        AddTest(testname, 'Preparation', '', '', '', '', '', '', '',\n                lambda o: {AppendFile(o['name'], testcsv)})\n        for bs in bslist:\n            desc = testname + \", BS=\" + str(bs)\n            DoAddTest(testname, seqrand, wmix, bs, threads, iodepth, desc,\n                      iops_log, runtime)\n\n    def AddTestQDShmoo():\n        \"\"\"Add a sequence of tests varying the queue depth\"\"\"\n        AddTest(testname, 'Preparation', '', '', '', '', '', '', '',\n                lambda o: {AppendFile(o['name'], testcsv)})\n        for iodepth in qdlist:\n            desc = testname + \", QD=\" + str(iodepth)\n            DoAddTest(testname, seqrand, wmix, bs, threads, iodepth, desc,\n                      iops_log, runtime)\n\n    def AddTestThreadsShmoo():\n        \"\"\"Add a sequence of tests varying the number of threads\"\"\"\n        AddTest(testname, 'Preparation', '', '', '', '', '', '', '',\n                lambda o: {AppendFile(o['name'], testcsv)})\n        for threads in threadslist:\n            desc = testname + \", Threads=\" + str(threads)\n            DoAddTest(testname, seqrand, wmix, bs, threads, iodepth, desc,\n                      iops_log, runtime)\n\n    AddTest('Sequential Preconditioning', 'Preparation', '', '', '', '', '',\n            '', '', lambda o: {})  # Only for display on-screen\n    AddTest('Sequential Preconditioning', 'Seq Pass 1', '100', '131072', '1',\n            '256', False, '', 'Sequential Preconditioning Pass 1',\n            lambda o: {SequentialConditioning()})\n    if not fastPrecond:\n        AddTest('Sequential Preconditioning', 'Seq Pass 2', '100', '131072', '1',\n                '256', False, '', 'Sequential Preconditioning Pass 2',\n                lambda o: {SequentialConditioning()})\n\n    testname = \"Sustained Multi-Threaded Sequential Read Tests by Block Size\"\n    seqrand = \"Seq\"\n    wmix = 0\n    threads = 1\n    runtime = shorttime\n    iops_log = False\n    iodepth = 256\n    AddTestBSShmoo()\n\n    testname = \"Sustained Multi-Threaded Random Read Tests by Block Size\"\n    seqrand = \"Rand\"\n    wmix = 0\n    threads = 16\n    runtime = shorttime\n    iops_log = False\n    iodepth = 16\n    AddTestBSShmoo()\n\n    testname = \"Sequential Write Tests with Queue Depth=1 by Block Size\"\n    seqrand = \"Seq\"\n    wmix = 100\n    threads = 1\n    runtime = shorttime\n    iops_log = False\n    iodepth = 1\n    AddTestBSShmoo()\n\n    if not fastPrecond:\n        AddTest('Random Preconditioning', 'Preparation', '', '', '', '', '', '',\n                '', lambda o: {})  # Only for display on-screen\n        AddTest('Random Preconditioning', 'Rand Pass 1', '100', '4096', '1',\n                '256', False, '', 'Random Preconditioning',\n                lambda o: {RandomConditioning()})\n        AddTest('Random Preconditioning', 'Rand Pass 2', '100', '4096', '1',\n                '256', False, '', 'Random Preconditioning',\n                lambda o: {RandomConditioning()})\n\n    testname = \"Sustained 4KB Random Read Tests by Number of Threads\"\n    seqrand = \"Rand\"\n    wmix = 0\n    bs = 4096\n    runtime = shorttime\n    iops_log = False\n    iodepth = 1\n    AddTestThreadsShmoo()\n\n    testname = \"Sustained 4KB Random mixed 30% Write Tests by Threads\"\n    seqrand = \"Rand\"\n    wmix = 30\n    bs = 4096\n    runtime = shorttime\n    iops_log = False\n    iodepth = 1\n    AddTestThreadsShmoo()\n\n    testname = \"Sustained Perf Stability Test - 4KB Random 30% Write\"\n    AddTest(testname, 'Preparation', '', '', '', '', '', '', '',\n            lambda o: {AppendFile(o['name'], testcsv)})\n    seqrand = \"Rand\"\n    wmix = 30\n    bs = 4096\n    runtime = longtime\n    iops_log = True\n    iodepth = 1\n    threads = 256\n    DoAddTest(testname, seqrand, wmix, bs, threads, iodepth, testname,\n              iops_log, runtime)\n\n    testname = \"Sustained 4KB Random Write Tests by Number of Threads\"\n    seqrand = \"Rand\"\n    wmix = 100\n    bs = 4096\n    runtime = shorttime\n    iops_log = False\n    iodepth = 1\n    AddTestThreadsShmoo()\n\n    testname = \"Sustained Multi-Threaded Random Write Tests by Block Size\"\n    seqrand = \"Rand\"\n    wmix = 100\n    runtime = shorttime\n    iops_log = False\n    iodepth = 16\n    threads = 16\n    AddTestBSShmoo()\n\n\ndef RunAllTests():\n    \"\"\"Iterate through the OC work queue and run each job, show progress.\"\"\"\n    global ret_iops, ret_mbps, ret_lat, fioVerString\n\n    # Determine some column widths to make format specifiers\n    maxlen = 0\n    for o in oc:\n        maxlen = max(maxlen, len(o['desc']))\n    descfmt = \"{0:\" + str(maxlen) + \"}\"\n    resfmt = \"{1: >8} {2: >9} {3: >8}\"\n    fmtstr = descfmt + \" \" + resfmt\n\n    def JobWrapper(**kwargs):\n        \"\"\"Thread wrapper to store return values for parent to read later.\"\"\"\n        global ret_iops, ret_mbps, ret_lat, oc\n        # Until we know it's succeeded, we're in error\n        ret_iops = \"ERROR\"\n        ret_mbps = \"ERROR\"\n        ret_lat = \"ERROR\"\n        try:\n            val = o['cmdline'](o)\n            ret_iops = list(val)[0][0]\n            ret_mbps = list(val)[0][1]\n            ret_lat = list(val)[0][2]\n        except FIOError as e:\n            print(\"\\nFIO Error!\\n\" + e.cmdline + \"\\nSTDOUT:\\n\" + e.stdout)\n            print(\"STDERR:\\n\" + e.stderr)\n            raise\n        except:\n            print(\"\\nUnexpected error while running FIO job.\")\n            raise\n\n    print(\"*\" * len(fmtstr.format(\"\", \"\", \"\", \"\")))\n    print(\"ezFio test parameters:\\n\")\n\n    fmtinfo = \"{0: >20}: {1}\"\n    print(fmtinfo.format(\"Drive\", str(physDriveTxt)))\n    print(fmtinfo.format(\"Model\", str(model)))\n    print(fmtinfo.format(\"Serial\", str(serial)))\n    print(fmtinfo.format(\"AvailCapacity\", str(physDriveGiB) + \" GiB\"))\n    print(fmtinfo.format(\"TestedCapacity\", str(testcapacity) + \" GiB\"))\n    print(fmtinfo.format(\"TestedOffset\", str(testoffset) + \" GiB\"))\n    print(fmtinfo.format(\"CPU\", str(cpu)))\n    print(fmtinfo.format(\"Cores\", str(cpuCores)))\n    print(fmtinfo.format(\"Frequency\", str(cpuFreqMHz)))\n    print(fmtinfo.format(\"FIO Version\", str(fioVerString)))\n\n    print(\"\\n\")\n    print(fmtstr.format(\"Test Description\", \"BW(MB/s)\", \"IOPS\", \"Lat(us)\"))\n    print(fmtstr.format(\"-\"*maxlen, \"-\"*8, \"-\"*9, \"-\"*8))\n    for o in oc:\n        if o['desc'] == \"\":\n            # This is a header-printing job, don't thread out\n            print(\"\\n\" + fmtstr.format(\"---\"+o['name']+\"---\", \"\", \"\", \"\"))\n            sys.stdout.flush()\n            o['cmdline'](o)\n        else:\n            # This is a real test job, run it in a thread\n            if sys.stdout.isatty():\n                print(fmtstr.format(o['desc'], \"Runtime\", \"00:00:00\", \"...\"), end='\\r')\n            else:\n                print(descfmt.format(o['desc']), end='')\n            sys.stdout.flush()\n            starttime = datetime.datetime.now()\n            job = threading.Thread(target=JobWrapper, kwargs=(o))\n            job.start()\n            while job.is_alive():\n                now = datetime.datetime.now()\n                delta = now - starttime\n                dstr = \"{0:02}:{1:02}:{2:02}\".format(int(delta.seconds / 3600),\n                                                     int((delta.seconds % 3600)/60),\n                                                     int(delta.seconds % 60))\n                if sys.stdout.isatty():\n                    # Blink runtime to make it obvious stuff is happening\n                    if (delta.seconds % 2) != 0:\n                        print(fmtstr.format(o['desc'], \"Runtime\", dstr, \"...\"), end='\\r')\n                    else:\n                        print(fmtstr.format(o['desc'], \"\", dstr, \"\"), end='\\r')\n                sys.stdout.flush()\n                time.sleep(1)\n            job.join()\n            # Pretty-print with grouping, if possible\n            try:\n                ret_iops = \"{:,}\".format(int(ret_iops))\n                ret_mbps = \"{:0,.2f}\".format(float(ret_mbps))\n            except:\n                pass\n            if sys.stdout.isatty():\n                print(fmtstr.format(o['desc'], ret_mbps, ret_iops, ret_lat))\n            else:\n                print(\" \" + resfmt.format(o['desc'],\n                                          ret_mbps, ret_iops, ret_lat))\n            sys.stdout.flush()\n            # On any error abort the test, all future results could be invalid\n            if ret_mbps == \"ERROR\":\n                print(\"ERROR DETECTED, ABORTING TEST RUN.\")\n                sys.exit(2)\n\n\ndef GenerateResultODS():\n    \"\"\"Builds a new ODS spreadsheet w/graphs from generated test CSV files.\"\"\"\n\n    def GetContentXMLFromODS(odssrc):\n        \"\"\"Extract content.xml from an ODS file, where the sheet lives.\"\"\"\n        ziparchive = zipfile.ZipFile(odssrc)\n        content = ziparchive.read(\"content.xml\").decode('UTF-8')\n        content = content.replace(\"\\n\", \"\")\n        return content\n\n    def CSVtoXMLSheet(sheetName, csvName):\n        \"\"\"Replace a named sheet with the contents of a CSV file.\"\"\"\n        newt = '<table:table table:name='\n        newt += '\"' + sheetName + '\"' + ' table:style-name=\"ta1\" > '\n        newt += '<table:table-column table:style-name=\"co1\" '\n        newt += 'table:default-cell-style-name=\"Default\"/>'\n        # Insert the rows, one entry at a time\n        with open(csvName, 'r') as f:\n            for line in f:\n                line = line.rstrip()\n                newt += '<table:table-row table:style-name=\"ro1\">'\n                for val in line.split(','):\n                    try:\n                        cell = '<table:table-cell office:value-type=\"float\" '\n                        cell += 'office:value=\"' + str(float(val))\n                        cell += '\"><text:p>'\n                        cell += str(float(val)) + \\\n                            '</text:p></table:table-cell>'\n                    except:  # It's not a float, so let's call it a string\n                        cell = '<table:table-cell office:value-type=\"string\" '\n                        cell += '><text:p>'\n                        cell += str(val) + '</text:p></table:table-cell>'\n                    newt += cell\n                newt += '</table:table-row>'\n            f.close()\n        # Close the tags\n        newt += '</table:table>'\n        return newt\n\n    def ReplaceSheetWithCSV_regex(sheetName, csvName, xmltext):\n        \"\"\"Replace a named sheet with the contents of a CSV file.\"\"\"\n        newt = CSVtoXMLSheet(sheetName, csvName)\n\n        # Replace the XML using lazy string matching\n        searchstr = '<table:table table:name=\"' + sheetName\n        searchstr += '\".*?</table:table>'\n        return re.sub(searchstr, newt, xmltext, flags=re.DOTALL)\n\n    def AppendSheetFromCSV(sheetName, csvName, xmltext):\n        \"\"\"Add a new sheet to the XML from the CSV file.\"\"\"\n        newt = CSVtoXMLSheet(sheetName, csvName)\n\n        # Replace the XML using lazy string matching\n        searchstr = '<table:named-expressions/>'\n        return re.sub(searchstr, newt + searchstr, xmltext, flags=re.DOTALL)\n\n    def UpdateContentXMLToODS_text(odssrc, odsdest, xmltext):\n        \"\"\"Replace content.xml in an ODS w/an in-memory copy and write new.\n\n        Replace content.xml in an ODS file with in-memory, modified copy and\n        write new ODS. Can't just copy source.zip and replace one file, the\n        output ZIP file is not correct in many cases (opens in Excel but fails\n        ODF validation and LibreOffice fails to load under Windows).\n\n        Also strips out any binary versions of objects and the thumbnail,\n        since they are no longer valid once we've changed the data in the\n        sheet.\n        \"\"\"\n        if os.path.exists(odsdest):\n            os.unlink(odsdest)\n\n        # Windows ZipArchive will not use \"Store\" even with \"no compression\"\n        # so we need to have a mimetype.zip file encoded below to match spec:\n        mimetypezip = \"\"\"\nUEsDBBQAAAgAAICyN0+FbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz\naXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsBAhQAFAAACAAAgLI3T4VsOYouAAAALgAAAAgA\nAAAAAAAAAAAAAAAAAAAAAG1pbWV0eXBlUEsFBgAAAAABAAEANgAAAFQAAAAAAA==\n\"\"\"\n        zipbytes = base64.b64decode(mimetypezip)\n        with open(odsdest, 'wb') as f:\n            f.write(zipbytes)\n\n        zasrc = zipfile.ZipFile(odssrc, 'r')\n        zadst = zipfile.ZipFile(odsdest, 'a', zipfile.ZIP_DEFLATED)\n        for entry in zasrc.namelist():\n            if entry == \"mimetype\":\n                continue\n            elif entry.endswith('/') or entry.endswith('\\\\'):\n                continue\n            elif entry == \"content.xml\":\n                zadst.writestr(\"content.xml\", xmltext)\n            elif (\"Object\" in entry) and (\"content.xml\" in entry):\n                # Remove <table:table table:name=\"local-table\"> table\n                rdbytes = zasrc.read(entry).decode('UTF-8')\n                outbytes = re.sub(\n                    '<table:table table:name=\"local-table\">.*</table:table>', \"\", rdbytes, flags=re.DOTALL)\n                zadst.writestr(entry, outbytes)\n            elif entry == \"META-INF/manifest.xml\":\n                # Remove ObjectReplacements from the list\n                rdbytes = zasrc.read(entry).decode('UTF-8')\n                outbytes = \"\"\n                lines = rdbytes.split(\"\\n\")\n                for line in lines:\n                    if not ((\"ObjectReplacement\" in line) or (\"Thumbnails\" in line)):\n                        outbytes = outbytes + line + \"\\n\"\n                zadst.writestr(entry, outbytes)\n            elif (\"Thumbnails\" in entry) or (\"ObjectReplacement\" in entry):\n                # Skip binary versions\n                continue\n            else:\n                rdbytes = zasrc.read(entry)\n                zadst.writestr(entry, rdbytes)\n        zasrc.close()\n        zadst.close()\n\n    def CombineExceedanceCSV(qdList, testType, testWpct, testBS, testIOdepth, suffix):\n        \"\"\"Merge multiple exceedance CSVs into a single output file.\n\n        Column merge multiple CSV files into a single one.  Complicated by\n        the fact that the number of columns in each may vary.\n        \"\"\"\n        csv = details + \"/ezfio_exceedance_\"+suffix+\".csv\"\n        if os.path.exists(csv):\n            os.unlink(csv)\n        CSVInfoHeader(csv)\n        line1 = \"\"\n        line2 = \"\"\n        for qd in qdList:\n            line1 = line1 + \\\n                (\"QD%d Read Exceedance,,QD%d Write Exceedance,,,\" % (qd, qd))\n            line2 = line2 + \"rdusec,rdpct,wrusec,wrpct,,\"\n        AppendFile(line1, csv)\n        AppendFile(line2, csv)\n\n        files = []\n        for qd in qdList:\n            try:\n                r = open(TestName(testType, testWpct, testBS,\n                                  qd, testIOdepth) + \".exc.read.csv\")\n            except:\n                r = None\n            try:\n                w = open(TestName(testType, testWpct, testBS,\n                                  qd, testIOdepth) + \".exc.write.csv\")\n            except:\n                w = None\n            files.append([r, w])\n        while True:\n            all_empty = True\n            l = \"\"\n            for fset in files:\n                if fset[0] is None:\n                    a = \"\"\n                else:\n                    a = fset[0].readline().strip()\n                if fset[1] is None:\n                    b = \"\"\n                else:\n                    b = fset[1].readline().strip()\n                l += (a + \",\", \",,\")[not a]\n                l += (b + \",\", \",,\")[not b]\n                l += ','\n                all_empty = all_empty and (not a) and (not b)\n            AppendFile(l, csv)\n            if all_empty:\n                break\n        return csv\n\n    global odssrc, timeseriescsv, testcsv, physDrive, testcapacity, model, testoffset\n    global serial, uname, fioVerString, odsdest, timeseriesclatcsv, timeseriesslatcsv\n\n    xmlsrc = GetContentXMLFromODS(odssrc)\n    xmlsrc = ReplaceSheetWithCSV_regex(\"Timeseries\", timeseriescsv, xmlsrc)\n    xmlsrc = ReplaceSheetWithCSV_regex(\n        \"TimeseriesCLAT\", timeseriesclatcsv, xmlsrc)\n    xmlsrc = ReplaceSheetWithCSV_regex(\n        \"TimeseriesSLAT\", timeseriesslatcsv, xmlsrc)\n    xmlsrc = ReplaceSheetWithCSV_regex(\"Tests\", testcsv, xmlsrc)\n    # Potentially add exceedance data if we have it\n    if fioOutputFormat == \"json+\":\n        csv = CombineExceedanceCSV(\n            [1, 4, 16, 32], \"Rand\", 30, 4096, 1, \"exceedance30\")\n        xmlsrc = ReplaceSheetWithCSV_regex(\"Exceedance\", csv, xmlsrc)\n    # Remove draw:image references to deleted binary previews\n    xmlsrc = re.sub(\"<draw:image.*?/>\", \"\", xmlsrc, flags=re.DOTALL)\n    # OpenOffice doesn't recalculate these cells on load?!\n    xmlsrc = xmlsrc.replace(\"_DRIVE\", str(physDrive))\n    xmlsrc = xmlsrc.replace(\"_TESTCAP\", str(testcapacity))\n    xmlsrc = xmlsrc.replace(\"_MODEL\", str(model))\n    xmlsrc = xmlsrc.replace(\"_SERIAL\", str(serial))\n    xmlsrc = xmlsrc.replace(\"_OS\", str(uname))\n    xmlsrc = xmlsrc.replace(\"_FIO\", str(fioVerString))\n    UpdateContentXMLToODS_text(odssrc, odsdest, xmlsrc)\n\n\nfio = \"\"          # FIO executable\nfioVerString = \"\"  # FIO self-reported version\nfioOutputFormat = \"json\"  # Can we make exceedance charts using JSON+ output?\ncluster = False   # Running multiple jobs in a cluster using fio --server\nphysDrive = \"\"    # Device path to test\nphysDriveTxt = \"\"  # Unadulterated drive line\nphysDriveDict = OrderedDict()  # Device path to test\nutilization = \"\"  # Device utilization % 1..100\noffset = \"\"       # Test region offset % 0..99\nyes = False       # Skip user verification\nquickie = False   # Flag to indicate short runs, only for ezfio debugging!\nnullio = False    # Flag to do no IO at all, use nullio instead\nfastPrecond = False  # Only do 1x sequential write for preconditioning (no random)\nverify = False    # Use built-in FIO data verification\nreadOnly = False  # Only run read-only tests\n\ncpu = \"\"         # CPU model\ncpuCores = \"\"    # # of cores (including virtual)\ncpuFreqMHz = \"\"  # \"Nominal\" speed of CPU\nuname = \"\"       # Kernel name/info\n\nphysDriveGiB = \"\"  # Disk size in GiB (2^n)\nphysDriveGB = \"\"   # Disk size in GB (10^n)\nphysDriveBase = \"\"  # Basename (ex: nvme0n1)\ntestcapacity = \"\"  # Total GiB to test\ntestoffset = \"\"    # test region offset in GiB\nmodel = \"\"         # Drive model name\nserial = \"\"        # Drive serial number\n\nds = \"\"  # Datestamp to appent to files/directories to uniquify\npwd = \"\"  # $CWD\n\ndetails = \"\"       # Test details directory\ntestcsv = \"\"       # Intermediate test output CSV file\ntimeseriescsv = \"\"  # Intermediate iostat output CSV file\ntimeseriesclatcsv = \"\"  # Intermediate iostat output CSV file\ntimeseriesslatcsv = \"\"  # Intermediate iostat output CSV file\nexceedancecsv = \"\"  # Intermediate exceedance output CSV\n\nodssrc = \"\"  # Original ODS spreadsheet file\nodsdest = \"\"  # Generated results ODS spreadsheet file\n\noc = []  # The list of tests to run\naioNeeded = 4096  # Minimum AIO kernel setting to run all tests\n\n# These globals are used to return the output results of the test thread\n# Required because it's difficult to pass back values from a threading.().\nret_iops = 0  # Last test IOPS\nret_mbps = 0  # Last test MBPs\nret_lat = 0  # Last test in microseconds\n\nif __name__ == \"__main__\":\n    ParseArgs()\n    CheckAdmin()\n    fio = FindFIO()\n    CheckFIOVersion()\n    CheckAIOLimits()\n    CollectSystemInfo()\n    CollectDriveInfo()\n    VerifyContinue()\n    SetupFiles()\n    DefineTests()\n    RunAllTests()\n    GenerateResultODS()\n\n    print(\"\\nCOMPLETED!\\nSpreadsheet file: \" + odsdest)\n"
  }
]