diff --git a/specifelse/benchtest/.gitignore b/specifelse/benchtest/.gitignore new file mode 100644 index 0000000..b18fd87 --- /dev/null +++ b/specifelse/benchtest/.gitignore @@ -0,0 +1 @@ +jomtSettings/ diff --git a/specifelse/benchtest/.tasks b/specifelse/benchtest/.tasks index 5df553a..2db69b2 100644 --- a/specifelse/benchtest/.tasks +++ b/specifelse/benchtest/.tasks @@ -1,4 +1,4 @@ [+] build_type=Release build_target=demo0 -run_args=--benchmark_out=build/$(VIM:build_target).json +run_target="$(VIM:build_dir)/$(VIM:build_target)" --benchmark_out="$(VIM:build_dir)/$(VIM:build_target).json" diff --git a/specifelse/benchtest/jomt/.gitignore b/specifelse/benchtest/jomt/.gitignore new file mode 100644 index 0000000..a0c0092 --- /dev/null +++ b/specifelse/benchtest/jomt/.gitignore @@ -0,0 +1,35 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Misc +CMakeLists.txt.user diff --git a/specifelse/benchtest/jomt/.tasks b/specifelse/benchtest/jomt/.tasks new file mode 100644 index 0000000..be088d7 --- /dev/null +++ b/specifelse/benchtest/jomt/.tasks @@ -0,0 +1,4 @@ +[+] +build_type=Release +build_target=JOMT +run_target="$(VIM:build_dir)/src/$(VIM:build_target)" diff --git a/specifelse/benchtest/jomt/CMakeLists.txt b/specifelse/benchtest/jomt/CMakeLists.txt new file mode 100644 index 0000000..cb4698b --- /dev/null +++ b/specifelse/benchtest/jomt/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) + +project(JOMT LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if (MSVC) + add_compile_options(/W3) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2 /DNDEBUG /DQT_NO_DEBUG_OUTPUT") +else() + add_compile_options(-Wall -Wextra -pedantic) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -DNDEBUG -DQT_NO_DEBUG_OUTPUT -march=native -mtune=native") +endif() + +# Build the target +add_subdirectory(src) diff --git a/specifelse/benchtest/jomt/LICENSE b/specifelse/benchtest/jomt/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/specifelse/benchtest/jomt/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/specifelse/benchtest/jomt/README.md b/specifelse/benchtest/jomt/README.md new file mode 100644 index 0000000..4652a1c --- /dev/null +++ b/specifelse/benchtest/jomt/README.md @@ -0,0 +1,57 @@ +Benchmark visualizer cloned from https://github.com/gaujay/jomt by @archibate. + +# JOMT + +Visualization tool for Google benchmark results. + +Built upon Qt5 Charts and DataVisualization modules. + +### Features + +- Parse Google benchmark results as json files +- Support old naming format and aggregate data (min, median, mean, stddev/cv) +- Multiple 2D and 3D chart types +- Benchmarks and axes selection +- Plotting options (theme, ranges, logarithm, labels, units, ...) +- Auto-reload and preferences saving + +### Command line + +Direct chart plotting with parameters is available through command line options. + +``` +Options: + -?, -h, --help Displays this help. + -v, --version Displays version information. + --ct, --chart-type Chart type (e.g. Lines, Boxes, 3DBars) + --cx, --chart-x Chart X-axis (e.g. a1, t2) + --cy, --chart-y Chart Y-axis (e.g. CPUTime, Bytes, + RealMeanTime, ItemsMin) + --cz, --chart-z Chart Z-axis (e.g. auto, a2, t1) + --ap, --append Files to append by renaming (uses ';' as + separator) + --ow, --overwrite Files to append by overwriting (uses ';' as + separator) + +Arguments: + file Benchmark results file in json to parse. +``` + +### Building + +Supports GCC/MinGW and MSVC builds through CMake. + +You may need to install Qt dev libraries, if not already available. +See : https://doc.qt.io/qt-5/gettingstarted.html#installing-qt + +Then just open 'CMakeLists.txt' with a compatible IDE (like QtCreator) or use command line: + + $ cd jomt + $ mkdir build + $ cd build + $ cmake .. + $ make -j + +### License + +As the Qt modules it uses, this application is licensed under *GNU GPL-3.0-or-later*. diff --git a/specifelse/benchtest/jomt/src/CMakeLists.txt b/specifelse/benchtest/jomt/src/CMakeLists.txt new file mode 100644 index 0000000..ff0ce76 --- /dev/null +++ b/specifelse/benchtest/jomt/src/CMakeLists.txt @@ -0,0 +1,58 @@ + +find_package(Qt5 COMPONENTS Widgets Charts DataVisualization REQUIRED) + +# Files +set(HEADERS + include/mainwindow.h + include/benchmark_results.h + include/result_parser.h + include/plot_parameters.h + include/commandline_handler.h + include/result_selector.h + include/plotter_linechart.h + include/plotter_barchart.h + include/plotter_boxchart.h + include/plotter_3dbars.h + include/plotter_3dsurface.h + include/series_dialog.h +) +set(SOURCES + main.cpp + mainwindow.cpp + benchmark_results.cpp + result_parser.cpp + plot_parameters.cpp + commandline_handler.cpp + result_selector.cpp + plotter_linechart.cpp + plotter_barchart.cpp + plotter_boxchart.cpp + plotter_3dbars.cpp + plotter_3dsurface.cpp + series_dialog.cpp +) +set(FORMS + ui/mainwindow.ui + ui/result_selector.ui + ui/plotter_linechart.ui + ui/plotter_barchart.ui + ui/plotter_boxchart.ui + ui/plotter_3dbars.ui + ui/plotter_3dsurface.ui + ui/series_dialog.ui +) +set(RESOURCES + resource/resource.qrc +) +set(CMAKE_AUTOUIC_SEARCH_PATHS ui) + +# Target +add_executable(JOMT + ${HEADERS} + ${SOURCES} + ${FORMS} + ${RESOURCES} +) +target_include_directories(JOMT PRIVATE include) + +target_link_libraries(JOMT PRIVATE Qt5::Widgets Qt5::Charts Qt5::DataVisualization) diff --git a/specifelse/benchtest/jomt/src/benchmark_results.cpp b/specifelse/benchtest/jomt/src/benchmark_results.cpp new file mode 100644 index 0000000..4b326f7 --- /dev/null +++ b/specifelse/benchtest/jomt/src/benchmark_results.cpp @@ -0,0 +1,616 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "benchmark_results.h" + +#include + +#define BCHRES_DEBUG false +#include + + +/************************************************************************************************** +* +* Static functions +* +**************************************************************************************************/ + +QString BenchResults::extractData(const BenchData &data, int argIdx, int tpltIdx, const QString &glyph, + int argIdx2, int tpltIdx2, const QString &glyph2) +{ + int ttlArgs = data.arguments.size(); + int ttlTplts = data.templates.size(); + + // Check indexes validity + if (argIdx+1 > ttlArgs) { + if (BCHRES_DEBUG) qDebug() << "extractData(): no argument at index" + << argIdx << "of:" << data.run_name; + return ""; + } + if (argIdx2+1 > ttlArgs) { + if (BCHRES_DEBUG) qDebug() << "extractData(): no argument at index" + << argIdx2 << "of:" << data.run_name; + return ""; + } + if (tpltIdx+1 > ttlTplts) { + if (BCHRES_DEBUG) qDebug() << "extractData(): no template at index" + << tpltIdx << "of:" << data.run_name; + return ""; + } + if (tpltIdx2+1 > ttlTplts) { + if (BCHRES_DEBUG) qDebug() << "extractData(): no template at index" + << tpltIdx2 << "of:" << data.run_name; + return ""; + } + + QString sRes = data.base_name; + bool hasBoth = argIdx2 >= 0 || tpltIdx2 >= 0; + bool lastArgs = ttlArgs >= 2 + && (argIdx == ttlArgs-1 || argIdx == ttlArgs-2) + && (argIdx2 == ttlArgs-1 || argIdx2 == ttlArgs-2); + + // Templates + QString sTemplates; + for (int idx=0; idx"; + + // Arguments + for (int idx=0; idx BenchResults::convertCustomDataSize(const QString &tplt) +{ + QPair res(0., ""); + + double mult = 0.; + if ( tplt.startsWith("data8") ) mult = 1.; + else if ( tplt.startsWith("data16") ) mult = 2.; + else if ( tplt.startsWith("data32") ) mult = 4.; + else if ( tplt.startsWith("data64") ) mult = 8.; + + if (mult > 0.) + { + int in = tplt.indexOf('<'); + int out = tplt.lastIndexOf('>'); + + if (in > 0 && out > in) + { + QString val = tplt.mid(in+1, out-in-1); + bool ok = false; + res.first = val.toDouble(&ok); + if (ok) { + res.first *= mult; + res.second = "Data (bytes)"; + } + } + } + + return res; +} + +/**************************************************************************************************/ + +double BenchResults::getParamValue(const QString &name, QString &custDataName, bool &custDataAxis, double &fallbackIdx) +{ + bool ok = false; + double val = name.toDouble(&ok); + if (!ok) + { + if (custDataAxis) { + // Check if custom data size template + const auto &custData = convertCustomDataSize(name); + if ( !custData.second.isEmpty() ) { + val = custData.first; + if (custDataName.isEmpty()) + custDataName = custData.second; + } + else { + val = fallbackIdx++; + custDataAxis = false; + } + } + else + val = fallbackIdx++; + } + else + custDataAxis = false; + + return val; +} + +/************************************************************************************************** +* +* Member functions +* +**************************************************************************************************/ + +QVector BenchResults::segmentAll() const +{ + QVector allRes; + allRes.reserve( benchmarks.size() ); + + for (int idx = 0; idx < benchmarks.size(); ++idx) { + allRes.push_back(idx); + } + + return allRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentEach() const +{ + QVector eachRes; + eachRes.reserve( benchmarks.size() ); + + for (int idx = 0; idx < benchmarks.size(); ++idx) + { + const BenchData &bchData = benchmarks[idx]; + eachRes.push_back( BenchSubset(bchData.name, idx) ); + } + + return eachRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentFamilies() const +{ + QVector famRes; + QMap famMap; + + for (int idx = 0; idx < benchmarks.size(); ++idx) + { + const BenchData &bchData = benchmarks[idx]; + + if ( !famMap.contains(bchData.family) ) + { + famMap[bchData.family] = famRes.size(); + famRes.push_back( BenchSubset(bchData.family) ); + } + // Append to associated entry + famRes[famMap[bchData.family]].idxs.push_back(idx); + } + + return famRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentFamilies(const QVector &subset) const +{ + QVector famRes; + QMap famMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + if ( !famMap.contains(bchData.family) ) + { + famMap[bchData.family] = famRes.size(); + famRes.push_back( BenchSubset(bchData.family) ); + } + // Append to associated entry + famRes[famMap[bchData.family]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(famRes)) + if (BCHRES_DEBUG) qDebug() << "familySub:" << sub.name << "->" << sub.idxs; + + return famRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentContainers(const QVector &subset) const +{ + QVector ctnRes; + QMap ctnMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + if ( !ctnMap.contains(bchData.container) ) + { + ctnMap[bchData.container] = ctnRes.size(); + ctnRes.push_back( BenchSubset(bchData.container) ); + } + // Append to associated entry + ctnRes[ctnMap[bchData.container]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(ctnRes)) + if (BCHRES_DEBUG) qDebug() << "containerSub:" << sub.name << "->" << sub.idxs; + + return ctnRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentBaseNames() const +{ + QVector nameRes; + QMap nameMap; + + for (int idx = 0; idx < benchmarks.size(); ++idx) + { + const BenchData &bchData = benchmarks[idx]; + + if ( !nameMap.contains(bchData.base_name) ) + { + nameMap[bchData.base_name] = nameRes.size(); + nameRes.push_back( BenchSubset(bchData.base_name) ); + } + // Append to associated entry + nameRes[nameMap[bchData.base_name]].idxs.push_back(idx); + } + + return nameRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentBaseNames(const QVector &subset) const +{ + QVector nameRes; + QMap nameMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + if ( !nameMap.contains(bchData.base_name) ) + { + nameMap[bchData.base_name] = nameRes.size(); + nameRes.push_back( BenchSubset(bchData.base_name) ); + } + // Append to associated entry + nameRes[nameMap[bchData.base_name]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(nameRes)) + if (BCHRES_DEBUG) qDebug() << "nameSub:" << sub.name << "->" << sub.idxs; + + return nameRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segment2DNames(const QVector &subset, + bool isArg1, int idx1, bool isArg2, int idx2) const +{ + QVector nameRes; + QMap nameMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + QString difName; + if (isArg1) { + if (isArg2) + difName = extractData(bchData, idx1, -1, "X", idx2, -1, "Z"); + else + difName = extractData(bchData, idx1, -1, "X", -1, idx2, "Z"); + } + else { + if (isArg2) + difName = extractData(bchData, -1, idx1, "X", idx2, -1, "Z"); + else + difName = extractData(bchData, -1, idx1, "X", -1, idx2, "Z"); + } + + if ( !nameMap.contains(difName) ) + { + nameMap[difName] = nameRes.size(); + nameRes.push_back( BenchSubset(difName) ); + } + // Append to associated entry + nameRes[nameMap[difName]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(nameRes)) + if (BCHRES_DEBUG) qDebug() << "nameSub:" << sub.name << "->" << sub.idxs; + + return nameRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentArguments(const QVector &subset, int argIdx) const +{ + QVector argRes; + QMap argMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + if (bchData.arguments.size() <= argIdx) continue; + + const QString ¶m = bchData.arguments[argIdx]; + if ( !argMap.contains(param) ) + { + argMap[param] = argRes.size(); + argRes.push_back( BenchSubset(param) ); //Full name but param + } + // Append to associated entry + argRes[argMap[param]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(argRes)) + if (BCHRES_DEBUG) qDebug() << "argSub:" << sub.name << "->" << sub.idxs; + + return argRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentTemplates(const QVector &subset, int tpltIdx) const +{ + QVector tpltRes; + QMap tpltMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + if (bchData.templates.size() <= tpltIdx) continue; + + const QString ¶m = bchData.templates[tpltIdx]; + if ( !tpltMap.contains(param) ) + { + tpltMap[param] = tpltRes.size(); + tpltRes.push_back( BenchSubset(param) ); + } + // Append to associated entry + tpltRes[tpltMap[param]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(tpltRes)) + if (BCHRES_DEBUG) qDebug() << "templateSub:" << sub.name << "->" << sub.idxs; + + return tpltRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::segmentParam(bool isArgument, const QVector &subset, int idx) const +{ + if (isArgument) + return segmentArguments(subset, idx); + + return segmentTemplates(subset, idx); +} + +/**************************************************************************************************/ +/**************************************************************************************************/ + +QVector BenchResults::groupArgument(const QVector &subset, + int argIdx, const QString &argGlyph) const +{ + QVector argRes; + QMap argMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + const QString &bchID = extractArgument(bchData, argIdx, argGlyph); + if ( bchID.isEmpty() ) + continue; //Ignore if incompatible + + if ( !argMap.contains(bchID) ) + { + argMap[bchID] = argRes.size(); //Add ID to map + argRes.push_back( BenchSubset(bchID) ); + } + // Append to associated entry + argRes[argMap[bchID]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(argRes)) + if (BCHRES_DEBUG) qDebug() << "argGSub:" << sub.name << "->" << sub.idxs; + + return argRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::groupTemplate(const QVector &subset, + int tpltIdx, const QString &tpltGlyph) const +{ + QVector tpltRes; + QMap tpltMap; + + for (int idx : subset) + { + if (idx >= benchmarks.size()) + continue; //No longer exists + + const BenchData &bchData = benchmarks[idx]; + const QString &bchID = extractTemplate(bchData, tpltIdx, tpltGlyph); + if ( bchID.isEmpty() ) + continue; //Ignore if incompatible + + if ( !tpltMap.contains(bchID) ) + { + tpltMap[bchID] = tpltRes.size(); //Add ID to map + tpltRes.push_back( BenchSubset(bchID) ); + } + // Append to associated entry + tpltRes[tpltMap[bchID]].idxs.push_back(idx); + } + for (const BenchSubset& sub : qAsConst(tpltRes)) + if (BCHRES_DEBUG) qDebug() << "tpltGSub:" << sub.name << "->" << sub.idxs; + + return tpltRes; +} + +/**************************************************************************************************/ + +QVector BenchResults::groupParam(bool isArgument, const QVector &subset, + int idx, const QString &glyph) const +{ + if (isArgument) + return groupArgument(subset, idx, glyph); + + return groupTemplate(subset, idx, glyph); +} + +/**************************************************************************************************/ +/**************************************************************************************************/ + +QString BenchResults::getBenchName(int index) const +{ + Q_ASSERT(index >= 0 && index < benchmarks.size()); + return benchmarks[index].name; +} + +QString BenchResults::getParamName(bool isArgument, int benchIdx, int paramIdx) const +{ + if (paramIdx < 0) + return ""; + + // Argument + if (isArgument) { + Q_ASSERT(benchmarks[benchIdx].arguments.size() > paramIdx); + return benchmarks[benchIdx].arguments[paramIdx]; + } + // Template + Q_ASSERT(benchmarks[benchIdx].templates.size() > paramIdx); + return benchmarks[benchIdx].templates[paramIdx]; +} + +/**************************************************************************************************/ +/**************************************************************************************************/ + +void BenchResults::appendResults(const BenchResults &bchRes) +{ + // Benchmarks + for (const auto& newBench : qAsConst(bchRes.benchmarks)) + { + // Rename if needed + QString tempName = newBench.name; + int suffix = 1; + + int idx; + do { + idx = -1; + for (int i=0; idx<0 && ibenchmarks.size(); ++i) + if (this->benchmarks[i].name == tempName) idx = i; + + if (idx >= 0) { + tempName = newBench.name; + tempName.insert(newBench.base_name.size(), "_" + QString::number(++suffix)); + } + } while (idx >= 0); + + // Apply and append + if (newBench.name == tempName) + this->benchmarks.append(newBench); + else { + BenchData cpyBench = newBench; + cpyBench.name = tempName; + cpyBench.run_name = tempName; + cpyBench.base_name += "_" + QString::number(suffix); + + this->benchmarks.append(cpyBench); + } + if (BCHRES_DEBUG) qDebug() << "newBench:" << newBench.name << "|" << tempName; + } + + // Meta + if (this->meta.maxArguments < bchRes.meta.maxArguments) + this->meta.maxArguments = bchRes.meta.maxArguments; + if (this->meta.maxTemplates < bchRes.meta.maxTemplates) + this->meta.maxTemplates = bchRes.meta.maxTemplates; + if (this->meta.time_unit != bchRes.meta.time_unit) + this->meta.time_unit = "us"; + + this->meta.hasAggregate |= bchRes.meta.hasAggregate; + this->meta.onlyAggregate &= bchRes.meta.onlyAggregate; + this->meta.hasCv |= bchRes.meta.hasCv; + this->meta.hasBytesSec |= bchRes.meta.hasBytesSec; + this->meta.hasItemsSec |= bchRes.meta.hasItemsSec; +} + +/**************************************************************************************************/ + +void BenchResults::overwriteResults(const BenchResults &bchRes) +{ + // Benchmarks + for (const auto& newBench : qAsConst(bchRes.benchmarks)) + { + int idx = -1; + for (int i=0; idx<0 && ibenchmarks.size(); ++i) + if (this->benchmarks[i].name == newBench.name) idx = i; + + if (idx < 0) + this->benchmarks.append(newBench); + else + this->benchmarks[idx] = newBench; + } + + // Meta + if (this->meta.maxArguments < bchRes.meta.maxArguments) + this->meta.maxArguments = bchRes.meta.maxArguments; + if (this->meta.maxTemplates < bchRes.meta.maxTemplates) + this->meta.maxTemplates = bchRes.meta.maxTemplates; + if (this->meta.time_unit != bchRes.meta.time_unit) + this->meta.time_unit = "us"; + + this->meta.hasAggregate |= bchRes.meta.hasAggregate; + this->meta.onlyAggregate &= bchRes.meta.onlyAggregate; + this->meta.hasCv |= bchRes.meta.hasCv; + this->meta.hasBytesSec |= bchRes.meta.hasBytesSec; + this->meta.hasItemsSec |= bchRes.meta.hasItemsSec; +} + +/**************************************************************************************************/ diff --git a/specifelse/benchtest/jomt/src/commandline_handler.cpp b/specifelse/benchtest/jomt/src/commandline_handler.cpp new file mode 100644 index 0000000..e55cff6 --- /dev/null +++ b/specifelse/benchtest/jomt/src/commandline_handler.cpp @@ -0,0 +1,334 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "commandline_handler.h" + +#include "plot_parameters.h" +#include "benchmark_results.h" +#include "result_parser.h" + +#include "plotter_linechart.h" +#include "plotter_barchart.h" +#include "plotter_boxchart.h" +#include "plotter_3dbars.h" +#include "plotter_3dsurface.h" + +#include +#include +#include + +const char* ct_name = "chart-type"; +const char* cx_name = "chart-x"; +const char* cy_name = "chart-y"; +const char* cz_name = "chart-z"; +const char* fa_name = "append"; +const char* fo_name = "overwrite"; + + +CommandLineHandler::CommandLineHandler() +{ + // Parser configuration + mParser.setApplicationDescription("JOMT - Help"); + mParser.addHelpOption(); + mParser.addVersionOption(); + mParser.addPositionalArgument("file", "Benchmark results file in json to parse.", "[file]"); + + QCommandLineOption chartTypeOption(QStringList() << "ct" << ct_name, + "Chart type (e.g. Lines, Boxes, 3DBars)", "chart_type", "Lines"); + mParser.addOption(chartTypeOption); + + QCommandLineOption chartXOption(QStringList() << "cx" << cx_name, + "Chart X-axis (e.g. a1, t2)", "chart_x", "a1"); + mParser.addOption(chartXOption); + + QCommandLineOption chartYOption(QStringList() << "cy" << cy_name, + "Chart Y-axis (e.g. CPUTime, Bytes, RealMeanTime, ItemsMin)", "chart_y", "RealTime"); + mParser.addOption(chartYOption); + + QCommandLineOption chartZOption(QStringList() << "cz" << cz_name, + "Chart Z-axis (e.g. auto, a2, t1)", "chart_z", "auto"); + mParser.addOption(chartZOption); + + QCommandLineOption appendOption(QStringList() << "ap" << fa_name, + "Files to append by renaming (uses ';' as separator)", "files..."); + mParser.addOption(appendOption); + + QCommandLineOption overwriteOption(QStringList() << "ow" << fo_name, + "Files to append by overwriting (uses ';' as separator)", "files..."); + mParser.addOption(overwriteOption); +} + +bool CommandLineHandler::process(const QApplication& app) +{ + // Process + mParser.process(app); + + const QStringList args = mParser.positionalArguments(); + + if ( args.empty() ) + return false; // Not handled + else if ( args.size() > 1) + qWarning() << "[CmdLine] Ignoring additional arguments after first one"; + + // Parse results + QString errorMsg; + BenchResults bchResults = ResultParser::parseJsonFile( args[0], errorMsg); + + if ( bchResults.benchmarks.isEmpty() ) { + qCritical() << "[CmdLine] Error parsing file: " << args[0] << " -> " << errorMsg; + return true; + } + + // Get params + QString chartType = mParser.value(ct_name).toLower(); + QString chartX = mParser.value(cx_name).toLower(); + QString chartY = mParser.value(cy_name).toLower(); + QString chartZ = mParser.value(cz_name).toLower(); + QString apFiles = mParser.value(fa_name); + QString owFiles = mParser.value(fo_name); + + // + // Parse params + PlotParams plotParams; + + // Append files + bool multiFiles = false; + QVector addFilenames; + if ( !apFiles.isEmpty() ) + { + QStringList apList = apFiles.split(';', Qt::SkipEmptyParts); + for (const auto& fileName : qAsConst(apList)) + { + if ( QFile::exists(fileName) ) + { + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(fileName, errorMsg); + if (newResults.benchmarks.size() <= 0) { + qCritical() << "[CmdLine] Error parsing append file: " << fileName << " -> " << errorMsg; + return true; + } + bchResults.appendResults(newResults); + multiFiles = true; + addFilenames.append( {fileName, true} ); + } + } + } + // Overwrite files + if ( !owFiles.isEmpty() ) + { + QStringList owList = owFiles.split(';', Qt::SkipEmptyParts); + for (const auto& fileName : qAsConst(owList)) + { + if ( QFile::exists(fileName) ) + { + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(fileName, errorMsg); + if (newResults.benchmarks.size() <= 0) { + qCritical() << "[CmdLine] Error parsing overwrite file: " << fileName << " -> " << errorMsg; + return true; + } + bchResults.overwriteResults(newResults); + multiFiles = true; + addFilenames.append( {fileName, false} ); + } + } + } + + + // Chart-type + if (chartType == "lines") plotParams.type = ChartLineType; + else if (chartType == "splines") plotParams.type = ChartSplineType; + else if (chartType == "bars") plotParams.type = ChartBarType; + else if (chartType == "hbars") plotParams.type = ChartHBarType; + else if (chartType == "boxes") plotParams.type = ChartBoxType; + else if (chartType == "3dbars") plotParams.type = Chart3DBarsType; + else if (chartType == "3dsurface") plotParams.type = Chart3DSurfaceType; + else { + plotParams.type = ChartLineType; + qWarning() << "[CmdLine] Unknown chart-type:" << chartType; + } + + // X-axis + if (chartX.size() >= 2 && (chartX.startsWith("a") || chartX.startsWith("t"))) + { + if (chartX.startsWith("a")) plotParams.xType = PlotArgumentType; + else plotParams.xType = PlotTemplateType; + // Index + chartX = chartX.mid(1); + bool ok; + int idx = chartX.toInt(&ok); + if (ok && idx >= 1) + plotParams.xIdx = idx - 1; + else { + plotParams.xIdx = 0; + qWarning() << "[CmdLine] Unknown chart-x index:" << chartX; + } + // Invalid + if ( (plotParams.xType == PlotArgumentType && plotParams.xIdx >= bchResults.meta.maxArguments) + || (plotParams.xType == PlotTemplateType && plotParams.xIdx >= bchResults.meta.maxTemplates) ) + { + if ( (plotParams.xType == PlotArgumentType && bchResults.meta.maxArguments == 0) + || (plotParams.xType == PlotTemplateType && bchResults.meta.maxTemplates == 0) ) + { + plotParams.xType = PlotEmptyType; + plotParams.xIdx = -1; + } + else + plotParams.xIdx = 0; + qWarning() << "[CmdLine] Chart-x index greater than number of parameters:" << chartX; + } + } + else { + plotParams.xType = PlotArgumentType; + plotParams.xIdx = 0; + qWarning() << "[CmdLine] Unknown chart-x:" << chartX; + } + + // Y-axis + if (chartY == "cputime") plotParams.yType = CpuTimeType; + else if (chartY == "realtime") plotParams.yType = RealTimeType; + else if (chartY == "iterations") plotParams.yType = IterationsType; + else if (chartY == "bytes" && bchResults.meta.hasBytesSec) plotParams.yType = BytesType; + else if (chartY == "items" && bchResults.meta.hasItemsSec) plotParams.yType = ItemsType; + + else if (bchResults.meta.hasAggregate) + { + if (chartY == "cpumintime") plotParams.yType = CpuTimeMinType; + else if (chartY == "cpumeantime") plotParams.yType = CpuTimeMeanType; + else if (chartY == "cpumediantime") plotParams.yType = CpuTimeMedianType; + else if (chartY == "realmintime") plotParams.yType = RealTimeMinType; + else if (chartY == "realmeantime") plotParams.yType = RealTimeMeanType; + else if (chartY == "realmediantime") plotParams.yType = RealTimeMedianType; + else if (bchResults.meta.hasBytesSec) { + if (chartY == "bytesmin") plotParams.yType = BytesMinType; + else if (chartY == "bytesmean") plotParams.yType = BytesMeanType; + else if (chartY == "bytesmedian") plotParams.yType = BytesMedianType; + } + else if (bchResults.meta.hasItemsSec) { + if (chartY == "itemsmin") plotParams.yType = ItemsMinType; + else if (chartY == "itemsmean") plotParams.yType = ItemsMeanType; + else if (chartY == "itemsmedian") plotParams.yType = ItemsMedianType; + } + else { + plotParams.yType = RealTimeType; + qWarning() << "[CmdLine] Unknown chart-y:" << chartY; + } + } + else { + plotParams.yType = RealTimeType; + qWarning() << "[CmdLine] Unknown chart-y:" << chartY; + } + + // Z-axis + if (chartZ.size() >= 2 + && (chartZ == "auto" || chartZ.startsWith("a") || chartZ.startsWith("t"))) + { + if (chartZ == "auto") { + plotParams.zType = PlotEmptyType; + plotParams.zIdx = 0; + } + else + { + if (chartZ.startsWith("a")) plotParams.zType = PlotArgumentType; + else plotParams.zType = PlotTemplateType; + // Index + chartZ = chartZ.mid(1); + bool ok; + int idx = chartZ.toInt(&ok); + if (ok && idx >= 1) + plotParams.zIdx = idx - 1; + else { + plotParams.zIdx = 0; + qWarning() << "[CmdLine] Unknown chart-z index:" << chartZ; + } + // Invalid + if ( (plotParams.zType == PlotArgumentType && plotParams.zIdx >= bchResults.meta.maxArguments) + || (plotParams.zType == PlotTemplateType && plotParams.zIdx >= bchResults.meta.maxTemplates)) + { + if ( (plotParams.zType == PlotArgumentType && bchResults.meta.maxArguments == 0) + || (plotParams.zType == PlotTemplateType && bchResults.meta.maxTemplates == 0) ) + { + plotParams.zType = PlotEmptyType; + plotParams.zIdx = -1; + } + else + plotParams.zIdx = 0; + qWarning() << "[CmdLine] Chart-z index greater than number of parameters:" << chartZ; + } + else if (plotParams.zType == plotParams.xType && plotParams.zIdx == plotParams.xIdx) { + qCritical() << "[CmdLine] Chart-z cannot be the same as chart-x"; + return true; + } + } + } + else { + plotParams.zType = PlotEmptyType; + plotParams.zIdx = 0; + qWarning() << "[CmdLine] Unknown chart-z:" << chartZ; + } + + + // + // Call plotter + QFileInfo fileInfo( args[0] ); + QString fileName = fileInfo.fileName(); + if (multiFiles) fileName += " + ..."; + + const auto& bchIdxs = bchResults.segmentAll(); + + switch (plotParams.type) + { + case ChartLineType: + case ChartSplineType: + { + PlotterLineChart *plotLines = new PlotterLineChart(bchResults, bchIdxs, + plotParams, fileName, addFilenames); + plotLines->show(); + break; + } + case ChartBarType: + case ChartHBarType: + { + PlotterBarChart *plotBars = new PlotterBarChart(bchResults, bchIdxs, + plotParams, fileName, addFilenames); + plotBars->show(); + break; + } + case ChartBoxType: + { + PlotterBoxChart *plotBoxes = new PlotterBoxChart(bchResults, bchIdxs, + plotParams, fileName, addFilenames); + plotBoxes->show(); + break; + } + case Chart3DBarsType: + { + Plotter3DBars *plot3DBars = new Plotter3DBars(bchResults, bchIdxs, + plotParams, fileName, addFilenames); + plot3DBars->show(); + break; + } + case Chart3DSurfaceType: + { + Plotter3DSurface *plot3DSurface = new Plotter3DSurface(bchResults, bchIdxs, + plotParams, fileName, addFilenames); + plot3DSurface->show(); + break; + } + } + + // Handled + return true; +} diff --git a/specifelse/benchtest/jomt/src/include/benchmark_results.h b/specifelse/benchtest/jomt/src/include/benchmark_results.h new file mode 100644 index 0000000..afcb4b7 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/benchmark_results.h @@ -0,0 +1,211 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef BENCHMARK_DATA_H +#define BENCHMARK_DATA_H + +#include +#include +#include + + +/* + * Structures + */ +// Additional file +struct FileReload { + QString filename; + bool isAppend; +}; +inline bool operator==(const FileReload& lhs, const FileReload& rhs) { + return (lhs.isAppend == rhs.isAppend && lhs.filename == rhs.filename); +} + +// Context Cache +struct BenchCache { + QString type; + int level; + int64_t size; + int num_sharing; +}; + +// Benchmarks Context +struct BenchContext { + QString date; + QString host_name; + QString executable; + int num_cpus; + int mhz_per_cpu; + bool cpu_scaling_enabled; + //"load_avg": [], // ? + QString build_type; + QVector caches; +}; + +// Benchmark Data +struct BenchData { + // Iterations + QString name; + QString run_name; + QString run_type; + int repetitions = 0; + int repetition_index; + int threads; + int iterations; + QString time_unit; + QVector real_time; // One per iteration + QVector cpu_time; + QVector kbytes_sec; + QVector kitems_sec; + + // Aggregate (all durations in us/cv in %) + bool hasAggregate = false; + double min_real, min_cpu, min_kbytes, min_kitems; + double max_real, max_cpu, max_kbytes, max_kitems; + double mean_real, mean_cpu, mean_kbytes, mean_kitems; + double median_real, median_cpu, median_kbytes, median_kitems; + double stddev_real, stddev_cpu, stddev_kbytes, stddev_kitems; + double cv_real = -1, cv_cpu = -1, cv_kbytes = -1, cv_kitems = -1; + + // Meta + // Note: JOMT format = "JOMT_FamilyName_ContainerName/params + QString base_name; // run_name without template/param/JOMT prefix + QString family; // family/algo name (empty if not JOMT) + QString container; // container name (empty if not JOMT) + QStringList arguments; // benchmark arguments + QStringList templates; // template parameters + + // Default (use associated 'min' values if has aggregate) + double real_time_us, cpu_time_us; // in us + double kbytes_sec_dflt = 0, kitems_sec_dflt = 0; +}; + +// Benchmark Subset +struct BenchSubset { + BenchSubset() {} + BenchSubset(const QString &name_) : name(name_) {} + BenchSubset(const QString &name_, int idx) : name(name_) + { idxs.push_back(idx); } + + // Data + QString name; + QVector idxs; +}; + +// Benchmark Meta +struct BenchMeta { + // Data + bool hasAggregate = false, onlyAggregate = true, hasCv = false; + bool hasBytesSec = false, hasItemsSec = false; + int maxArguments = 0, maxTemplates = 0; + QString time_unit; // if same for all, otherwise "us" as default +}; + +// +// BenchResults +struct BenchResults { + /* + * Data + */ + BenchMeta meta; + BenchContext context; + QVector benchmarks; + + + /* + * Static functions + */ + // Replace argument and/or template with glyph in BenchData name + static QString extractData(const BenchData &data, int argIdx, int tpltIdx, const QString &glyph = "", + int argIdx2 = -1, int tpltIdx2 = -1, const QString &glyph2 = ""); + // Replace argument with glyph in BenchData name + static inline QString extractArgument(const BenchData &data, int argIdx, const QString &argGlyph) + { + return extractData(data, argIdx, -1, argGlyph); + } + // Replace template with glyph in BenchData name + static inline QString extractTemplate(const BenchData &data, int tpltIdx, const QString &tpltGlyph) + { + return extractData(data, -1, tpltIdx, tpltGlyph); + } + // Try to extract special name+value from template name (JOMT specific) + static QPair convertCustomDataSize(const QString &tplt); + + // Convert parameter name to value (check special names, use incremented fallback if all else fail) + static double getParamValue(const QString &name, QString &custDataName, + bool &custDataAxis, double &fallbackIdx); + + /* + * Member functions + */ + // Ordered vector of all BenchData indexes + QVector segmentAll() const; + + // Each BenchData in its own subset + QVector segmentEach() const; + + // Each Family in its own subset + QVector segmentFamilies() const; + + // Each Family from index vector in its own subset + QVector segmentFamilies(const QVector &subset) const; + + // Each Container from index vector in its own subset + QVector segmentContainers(const QVector &subset) const; + + // Each BaseName in its own subset + QVector segmentBaseNames() const; + + // Each BaseName from index vector in its own subset + QVector segmentBaseNames(const QVector &subset) const; + + // Each 'full name % param1 % param2' from index vector in its own subset + QVector segment2DNames(const QVector &subset, + bool isArg1, int idx1, bool isArg2, int idx2) const; + // Each Argument from index vector in its own subset + QVector segmentArguments(const QVector &subset, int argIdx) const; + + // Each Template from index vector in its own subset + QVector segmentTemplates(const QVector &subset, int tpltIdx) const; + + // Each Argument/Template from index vector in its own subset + QVector segmentParam(bool isArgument, const QVector &subset, int idx) const; + + // + // Each Benchmark from vector in its own subset % Argument + QVector groupArgument(const QVector &subset, + int argIdx, const QString &argGlyph) const; + // Each Benchmark from vector in its own subset % Template + QVector groupTemplate(const QVector &subset, + int tpltIdx, const QString &tpltGlyph) const; + // Each Benchmark from vector in its own subset % Argument/Template + QVector groupParam(bool isArgument, const QVector &subset, + int idx, const QString &glyph) const; + + // Get Benchmark full name + QString getBenchName(int index) const; + // Get Argument/Template name + QString getParamName(bool isArgument, int benchIdx, int paramIdx) const; + + // + // Merge results (rename BenchData if already exists) + void appendResults(const BenchResults &bchRes); + // Merge results (overwrite BenchData if already exists) + void overwriteResults(const BenchResults &bchRes); + +}; + + +#endif // BENCHMARK_DATA_H diff --git a/specifelse/benchtest/jomt/src/include/commandline_handler.h b/specifelse/benchtest/jomt/src/include/commandline_handler.h new file mode 100644 index 0000000..bf3e68b --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/commandline_handler.h @@ -0,0 +1,36 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef COMMANDLINEHANDLER_H +#define COMMANDLINEHANDLER_H + +#include + +class QApplication; + + +class CommandLineHandler +{ +public: + CommandLineHandler(); + + bool process(const QApplication& app); + +private: + QCommandLineParser mParser; +}; + + +#endif // COMMANDLINEHANDLER_H diff --git a/specifelse/benchtest/jomt/src/include/mainwindow.h b/specifelse/benchtest/jomt/src/include/mainwindow.h new file mode 100644 index 0000000..6f21152 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/mainwindow.h @@ -0,0 +1,39 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; +}; + + +#endif // MAINWINDOW_H diff --git a/specifelse/benchtest/jomt/src/include/plot_parameters.h b/specifelse/benchtest/jomt/src/include/plot_parameters.h new file mode 100644 index 0000000..941be28 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plot_parameters.h @@ -0,0 +1,102 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOT_PARAMETERS_H +#define PLOT_PARAMETERS_H + +#include "benchmark_results.h" + +#include +#include +#include + +extern const char* config_folder; + + +// Chart types +enum PlotChartType { + ChartLineType, + ChartSplineType, + ChartBarType, + ChartHBarType, + ChartBoxType, + Chart3DBarsType, + Chart3DSurfaceType +}; + +// Parameter types +enum PlotParamType { + PlotEmptyType, + PlotArgumentType, + PlotTemplateType +}; + +// Y-value types +enum PlotValueType { + CpuTimeType, CpuTimeMinType, CpuTimeMeanType, CpuTimeMedianType, CpuTimeStddevType, CpuTimeCvType, + RealTimeType, RealTimeMinType, RealTimeMeanType, RealTimeMedianType, RealTimeStddevType, RealTimeCvType, + IterationsType, + BytesType, BytesMinType, BytesMeanType, BytesMedianType, BytesStddevType, BytesCvType, + ItemsType, ItemsMinType, ItemsMeanType, ItemsMedianType, ItemsStddevType, ItemsCvType +}; + +// Y-value stats +struct BenchYStats { + double min, max; + double median; + double lowQuart, uppQuart; +}; + +// Plot parameters +struct PlotParams { + PlotChartType type; + PlotParamType xType; + int xIdx; + PlotValueType yType; + PlotParamType zType; + int zIdx; +}; + + +/* + * Helpers + */ +// Get Y-value according to type +double getYPlotValue(const BenchData &bchData, PlotValueType yType); + +// Get Y-name according to type +QString getYPlotName(PlotValueType yType, QString timeUnit = "us"); + +// Convert time value to micro-seconds +double normalizeTimeUs(const BenchData &bchData, double value); + +// Check Y-value type is time-based +bool isYTimeBased(PlotValueType yType); + +// Find median in vector subpart +double findMedian(QVector sorted, int begin, int end); + +// Get Y-value statistics (for Box chart) +BenchYStats getYPlotStats(BenchData &bchData, PlotValueType yType); + +// Compare first common elements of string lists +bool commonPartEqual(const QStringList &listA, const QStringList &listB); + +// Check benchmark results have same origin files +bool sameResultsFiles(const QString &fileA, const QString &fileB, + const QVector &addFilesA, const QVector &addFilesB); + + +#endif // PLOT_PARAMETERS_H diff --git a/specifelse/benchtest/jomt/src/include/plotter_3dbars.h b/specifelse/benchtest/jomt/src/include/plotter_3dbars.h new file mode 100644 index 0000000..5150300 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plotter_3dbars.h @@ -0,0 +1,124 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOTTER_3DBARS_H +#define PLOTTER_3DBARS_H + +#include "plot_parameters.h" +#include "series_dialog.h" + +#include +#include +#include +#include + +namespace Ui { +class Plotter3DBars; +} +namespace QtDataVisualization { +class Q3DBars; +} +struct BenchResults; +struct FileReload; + + +class Plotter3DBars : public QWidget +{ + Q_OBJECT + +public: + explicit Plotter3DBars(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &filename, + const QVector& addFilenames, QWidget *parent = nullptr); + ~Plotter3DBars(); + +private: + void connectUI(); + void setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init = true); + void setupOptions(bool init = true); + void loadConfig(bool init); + void saveConfig(); + +public slots: + void onComboThemeChanged(int index); + + void onComboGradientChanged(int index); + void onSpinThicknessChanged(double d); + void onSpinFloorChanged(double d); + void onSpinSpaceXChanged(double d); + void onSpinSpaceZChanged(double d); + void onSeriesEditClicked(); + void onComboTimeUnitChanged(int index); + + void onComboAxisChanged(int index); + void onCheckAxisRotate(int state); + void onCheckTitleVisible(int state); + void onCheckLog(int state); + void onSpinLogBaseChanged(int i); + void onEditTitleChanged(const QString& text); + void onEditTitleChanged2(const QString& text, int iAxis); + void onEditFormatChanged(const QString& text); + void onSpinMinChanged(double d); + void onSpinMaxChanged(double d); + void onComboMinChanged(int index); + void onComboMaxChanged(int index); + void onSpinTicksChanged(int i); + void onSpinMTicksChanged(int i); + + void onCheckAutoReload(int state); + void onAutoReload(const QString &path); + void onReloadClicked(); + void onSnapshotClicked(); + + +private: + struct AxisParam { + AxisParam() : rotate(false), title(false), minIdx(0), maxIdx(0) {} + void reset() + { + rotate = false; + title = false; + minIdx = 0; + maxIdx = 0; + titleText.clear(); + range.clear(); + } + + bool rotate, title; + QString titleText; + QStringList range; + int minIdx, maxIdx; + }; + void setupGradients(); + + Ui::Plotter3DBars *ui; + QtDataVisualization::Q3DBars *mBars; + + QVector mBenchIdxs; + const PlotParams mPlotParams; + const QString mOrigFilename; + const QVector mAddFilenames; + const bool mAllIndexes; + + QFileSystemWatcher mWatcher; + SeriesMapping mSeriesMapping; + double mCurrentTimeFactor; // from us + AxisParam mAxesParams[3]; + QVector mGrads; + bool mIgnoreEvents = false; +}; + + +#endif // PLOTTER_3DBARS_H diff --git a/specifelse/benchtest/jomt/src/include/plotter_3dsurface.h b/specifelse/benchtest/jomt/src/include/plotter_3dsurface.h new file mode 100644 index 0000000..e88eb88 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plotter_3dsurface.h @@ -0,0 +1,121 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOTTER_3DSURFACE_H +#define PLOTTER_3DSURFACE_H + +#include "plot_parameters.h" +#include "series_dialog.h" + +#include +#include +#include +#include + +namespace Ui { +class Plotter3DSurface; +} +namespace QtDataVisualization { +class Q3DSurface; +} +struct BenchResults; +struct FileReload; + + +class Plotter3DSurface : public QWidget +{ + Q_OBJECT + +public: + explicit Plotter3DSurface(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &filename, + const QVector& addFilenames, QWidget *parent = nullptr); + ~Plotter3DSurface(); + +private: + void connectUI(); + void setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init = true); + void setupOptions(bool init = true); + void loadConfig(bool init); + void saveConfig(); + +public slots: + void onComboThemeChanged(int index); + + void onCheckFlip(int state); + void onComboGradientChanged(int index); + void onSeriesEditClicked(); + void onComboTimeUnitChanged(int index); + + void onComboAxisChanged(int index); + void onCheckAxisRotate(int state); + void onCheckTitleVisible(int state); + void onCheckLog(int state); + void onSpinLogBaseChanged(int i); + void onEditTitleChanged(const QString& text); + void onEditTitleChanged2(const QString& text, int iAxis); + void onEditFormatChanged(const QString& text); + void onSpinMinChanged(double d); + void onSpinMinChanged2(double d, int iAxis); + void onSpinMaxChanged(double d); + void onSpinMaxChanged2(double d, int iAxis); + void onSpinTicksChanged(int i); + void onSpinMTicksChanged(int i); + + void onCheckAutoReload(int state); + void onAutoReload(const QString &path); + void onReloadClicked(); + void onSnapshotClicked(); + + +private: + struct ValAxisParam { + ValAxisParam() : rotate(false), title(false), log(false), logBase(10) {} + void reset() + { + rotate = false; + title = false; + log = false; + logBase = 10; + titleText.clear(); + labelFormat.clear(); + } + + bool rotate, title, log; + QString titleText, labelFormat; + double min, max; + int ticks, mticks, logBase; + }; + void setupGradients(); + + Ui::Plotter3DSurface *ui; + QtDataVisualization::Q3DSurface *mSurface; + + QVector mBenchIdxs; + const PlotParams mPlotParams; + const QString mOrigFilename; + const QVector mAddFilenames; + const bool mAllIndexes; + + QFileSystemWatcher mWatcher; + SeriesMapping mSeriesMapping; + double mCurrentTimeFactor; // from us + ValAxisParam mAxesParams[3]; + QVector mGrads; + bool mIgnoreEvents = false; +}; + + +#endif // PLOTTER_3DSURFACE_H diff --git a/specifelse/benchtest/jomt/src/include/plotter_barchart.h b/specifelse/benchtest/jomt/src/include/plotter_barchart.h new file mode 100644 index 0000000..c791b31 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plotter_barchart.h @@ -0,0 +1,119 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOTTER_BARCHART_H +#define PLOTTER_BARCHART_H + +#include "plot_parameters.h" +#include "series_dialog.h" + +#include +#include +#include +#include + +namespace Ui { +class PlotterBarChart; +} +namespace QtCharts { +class QChartView; +} +struct BenchResults; +struct FileReload; + + +class PlotterBarChart : public QWidget +{ + Q_OBJECT + +public: + explicit PlotterBarChart(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &filename, + const QVector& addFilenames, QWidget *parent = nullptr); + ~PlotterBarChart(); + +private: + void connectUI(); + void setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init = true); + void setupOptions(bool init = true); + void loadConfig(bool init); + void saveConfig(); + +public slots: + void onComboThemeChanged(int index); + + void onCheckLegendVisible(int state); + void onComboLegendAlignChanged(int index); + void onSpinLegendFontSizeChanged(int i); + void onSeriesEditClicked(); + void onComboTimeUnitChanged(int index); + + void onComboAxisChanged(int index); + void onCheckAxisVisible(int state); + void onCheckTitleVisible(int state); + void onCheckLog(int state); + void onSpinLogBaseChanged(int i); + void onEditTitleChanged(const QString& text); + void onEditTitleChanged2(const QString& text, int iAxis); + void onSpinTitleSizeChanged(int i); + void onSpinTitleSizeChanged2(int i, int iAxis); + void onEditFormatChanged(const QString& text); + void onComboValuePositionChanged(int index); + void onComboValueAngleChanged(int index); + void onSpinLabelSizeChanged(int i); + void onSpinLabelSizeChanged2(int i, int iAxis); + void onSpinMinChanged(double d); + void onSpinMinChanged2(double d, int iAxis); + void onSpinMaxChanged(double d); + void onSpinMaxChanged2(double d, int iAxis); + void onComboMinChanged(int index); + void onComboMaxChanged(int index); + void onSpinTicksChanged(int i); + void onSpinMTicksChanged(int i); + + void onCheckAutoReload(int state); + void onAutoReload(const QString &path); + void onReloadClicked(); + void onSnapshotClicked(); + + +private: + struct AxisParam { + AxisParam() : visible(true), title(true) {} + + bool visible, title; + QString titleText; + int titleSize, labelSize; + }; + + Ui::PlotterBarChart *ui; + QtCharts::QChartView *mChartView = nullptr; + + QVector mBenchIdxs; + const PlotParams mPlotParams; + const QString mOrigFilename; + const QVector mAddFilenames; + const bool mAllIndexes; + + QFileSystemWatcher mWatcher; + SeriesMapping mSeriesMapping; + double mCurrentTimeFactor; // from us + AxisParam mAxesParams[2]; + const bool mIsVert; + bool mIgnoreEvents = false; +}; + + +#endif // PLOTTER_BARCHART_H diff --git a/specifelse/benchtest/jomt/src/include/plotter_boxchart.h b/specifelse/benchtest/jomt/src/include/plotter_boxchart.h new file mode 100644 index 0000000..37ee7c8 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plotter_boxchart.h @@ -0,0 +1,116 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOTTER_BOXCHART_H +#define PLOTTER_BOXCHART_H + +#include "plot_parameters.h" +#include "series_dialog.h" + +#include +#include +#include +#include + +namespace Ui { +class PlotterBoxChart; +} +namespace QtCharts { +class QChartView; +} +struct BenchResults; +struct FileReload; + + +class PlotterBoxChart : public QWidget +{ + Q_OBJECT + +public: + explicit PlotterBoxChart(BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &filename, + const QVector& addFilenames, QWidget *parent = nullptr); + ~PlotterBoxChart(); + +private: + void connectUI(); + void setupChart(BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init = true); + void setupOptions(bool init = true); + void loadConfig(bool init); + void saveConfig(); + +public slots: + void onComboThemeChanged(int index); + + void onCheckLegendVisible(int state); + void onComboLegendAlignChanged(int index); + void onSpinLegendFontSizeChanged(int i); + void onSeriesEditClicked(); + void onComboTimeUnitChanged(int index); + + void onComboAxisChanged(int index); + void onCheckAxisVisible(int state); + void onCheckTitleVisible(int state); + void onCheckLog(int state); + void onSpinLogBaseChanged(int i); + void onEditTitleChanged(const QString& text); + void onEditTitleChanged2(const QString& text, int iAxis); + void onSpinTitleSizeChanged(int i); + void onSpinTitleSizeChanged2(int i, int iAxis); + void onEditFormatChanged(const QString& text); + void onSpinLabelSizeChanged(int i); + void onSpinLabelSizeChanged2(int i, int iAxis); + void onSpinMinChanged(double d); + void onSpinMinChanged2(double d, int iAxis); + void onSpinMaxChanged(double d); + void onSpinMaxChanged2(double d, int iAxis); + void onComboMinChanged(int index); + void onComboMaxChanged(int index); + void onSpinTicksChanged(int i); + void onSpinMTicksChanged(int i); + + void onCheckAutoReload(int state); + void onAutoReload(const QString &path); + void onReloadClicked(); + void onSnapshotClicked(); + + +private: + struct AxisParam { + AxisParam() : visible(true), title(true) {} + + bool visible, title; + QString titleText; + int titleSize, labelSize; + }; + + Ui::PlotterBoxChart *ui; + QtCharts::QChartView *mChartView = nullptr; + + QVector mBenchIdxs; + const PlotParams mPlotParams; + const QString mOrigFilename; + const QVector mAddFilenames; + const bool mAllIndexes; + + QFileSystemWatcher mWatcher; + SeriesMapping mSeriesMapping; + double mCurrentTimeFactor; // from us + AxisParam mAxesParams[2]; + bool mIgnoreEvents = false; +}; + + +#endif // PLOTTER_BOXCHART_H diff --git a/specifelse/benchtest/jomt/src/include/plotter_linechart.h b/specifelse/benchtest/jomt/src/include/plotter_linechart.h new file mode 100644 index 0000000..fca6c48 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/plotter_linechart.h @@ -0,0 +1,116 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef PLOTTER_LINECHART_H +#define PLOTTER_LINECHART_H + +#include "plot_parameters.h" +#include "series_dialog.h" + +#include +#include +#include +#include + +namespace Ui { +class PlotterLineChart; +} +namespace QtCharts { +class QChartView; +} +struct BenchResults; +struct FileReload; + + +class PlotterLineChart : public QWidget +{ + Q_OBJECT + +public: + explicit PlotterLineChart(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &filename, + const QVector& addFilenames, QWidget *parent = nullptr); + ~PlotterLineChart(); + +private: + void connectUI(); + void setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init = true); + void setupOptions(bool init = true); + void loadConfig(bool init); + void saveConfig(); + +public slots: + void onComboThemeChanged(int index); + + void onCheckLegendVisible(int state); + void onComboLegendAlignChanged(int index); + void onSpinLegendFontSizeChanged(int i); + void onSeriesEditClicked(); + void onComboTimeUnitChanged(int index); + + void onComboAxisChanged(int index); + void onCheckAxisVisible(int state); + void onCheckTitleVisible(int state); + void onCheckLog(int state); + void onSpinLogBaseChanged(int i); + void onEditTitleChanged(const QString& text); + void onEditTitleChanged2(const QString& text, int iAxis); + void onSpinTitleSizeChanged(int i); + void onSpinTitleSizeChanged2(int i, int iAxis); + void onEditFormatChanged(const QString& text); + void onSpinLabelSizeChanged(int i); + void onSpinLabelSizeChanged2(int i, int iAxis); + void onSpinMinChanged(double d); + void onSpinMinChanged2(double d, int iAxis); + void onSpinMaxChanged(double d); + void onSpinMaxChanged2(double d, int iAxis); + void onSpinTicksChanged(int i); + void onSpinMTicksChanged(int i); + + void onCheckAutoReload(int state); + void onAutoReload(const QString &path); + void onReloadClicked(); + void onSnapshotClicked(); + + +private: + struct ValAxisParam { + ValAxisParam() : visible(true), title(true), log(false), logBase(10) {} + + bool visible, title, log; + QString titleText, labelFormat; + int titleSize, labelSize; + double min, max; + int ticks, mticks, logBase; + }; + + Ui::PlotterLineChart *ui; + QtCharts::QChartView *mChartView = nullptr; + + QVector mBenchIdxs; + const PlotParams mPlotParams; + const QString mOrigFilename; + const QVector mAddFilenames; + const bool mAllIndexes; + + QFileSystemWatcher mWatcher; + SeriesMapping mSeriesMapping; + double mCurrentTimeFactor; // from us + ValAxisParam mAxesParams[2]; + bool mIgnoreEvents = false; +}; + + +#endif // PLOTTER_LINECHART_H diff --git a/specifelse/benchtest/jomt/src/include/result_parser.h b/specifelse/benchtest/jomt/src/include/result_parser.h new file mode 100644 index 0000000..2c84fe1 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/result_parser.h @@ -0,0 +1,29 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef RESULTPARSER_H +#define RESULTPARSER_H + +#include "benchmark_results.h" + + +class ResultParser +{ +public: + static BenchResults parseJsonFile(const QString &filename, QString& errorMsg); +}; + + +#endif // RESULTPARSER_H diff --git a/specifelse/benchtest/jomt/src/include/result_selector.h b/specifelse/benchtest/jomt/src/include/result_selector.h new file mode 100644 index 0000000..9750194 --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/result_selector.h @@ -0,0 +1,82 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef RESULT_SELECTOR_H +#define RESULT_SELECTOR_H + +#include "benchmark_results.h" + +#include +#include +#include +#include +#include + +namespace Ui { +class ResultSelector; +} +class QTreeWidgetItem; + + +class ResultSelector : public QWidget +{ + Q_OBJECT + +public: + explicit ResultSelector(QWidget *parent = nullptr); + explicit ResultSelector(const BenchResults &bchResults, const QString &fileName, QWidget *parent = nullptr); + ~ResultSelector(); + +private: + void connectUI(); + void loadConfig(); + void saveConfig(); + void updateComboBoxY(); + void updateResults(bool clear, const QSet unselected = {}); + +public slots: + void onItemChanged(QTreeWidgetItem *item, int column); + + void onComboTypeChanged(int index); + void onComboXChanged(int index); + void onComboZChanged(int index); + + void onAutoReload(const QString &path); + void updateReloadWatchList(); + void onCheckAutoReload(int state); + void onReloadClicked(); + + void onNewClicked(); + void onAppendClicked(); + void onOverwriteClicked(); + + void onSelectAllClicked(); + void onSelectNoneClicked(); + + void onPlotClicked(); + +private: + Ui::ResultSelector *ui; + + BenchResults mBchResults; + QString mOrigFilename; + QVector mAddFilenames; + + QString mWorkingDir; + QFileSystemWatcher mWatcher; +}; + + +#endif // RESULT_SELECTOR_H diff --git a/specifelse/benchtest/jomt/src/include/series_dialog.h b/specifelse/benchtest/jomt/src/include/series_dialog.h new file mode 100644 index 0000000..0d0fbdd --- /dev/null +++ b/specifelse/benchtest/jomt/src/include/series_dialog.h @@ -0,0 +1,64 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef SERIES_DIALOG_H +#define SERIES_DIALOG_H + +#include +#include +#include +#include + +namespace Ui { +class SeriesDialog; +} + +struct SeriesConfig { + SeriesConfig(const QString &oldName_, const QString &newName_) + : oldName(oldName_) + , newName(newName_) + {} + + QString oldName, newName; + QColor oldColor, newColor; +}; +inline bool operator==(const SeriesConfig& lhs, const SeriesConfig& rhs) { + return (lhs.oldName == rhs.oldName); +} +typedef QVector SeriesMapping; + + +class SeriesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SeriesDialog(const SeriesMapping &mapping, QWidget *parent = nullptr); + ~SeriesDialog(); + + const SeriesMapping& getMapping() { return mMapping; } + +public slots: + virtual void accept(); + void onRestoreClicked(); + +private: + Ui::SeriesDialog *ui; + + SeriesMapping mMapping; +}; + + +#endif // SERIES_DIALOG_H diff --git a/specifelse/benchtest/jomt/src/main.cpp b/specifelse/benchtest/jomt/src/main.cpp new file mode 100644 index 0000000..e01b7cf --- /dev/null +++ b/specifelse/benchtest/jomt/src/main.cpp @@ -0,0 +1,82 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "benchmark_results.h" +#include "result_parser.h" +#include "plot_parameters.h" +#include "commandline_handler.h" +#include "result_selector.h" + +#include +#include +#include +#include +#include + +#define APP_NAME "JOMT" +#define APP_VER "1.0b" +#define APP_ICON ":/jomt_icon.png" + +// Debug +#define DEFAULT_DIR "" +#define DEFAULT_FILE "" + + +int main(int argc, char *argv[]) +{ + // Init + QApplication app(argc, argv); + QCoreApplication::setApplicationName(APP_NAME); + QCoreApplication::setApplicationVersion(APP_VER); + QApplication::setWindowIcon( QIcon(APP_ICON) ); + + QDir configDir(config_folder); + if (!configDir.exists()) + configDir.mkpath("."); + + // + // Command line options + CommandLineHandler cmdHandler; + bool isCmd = cmdHandler.process(app); + + QScopedPointer resultSelector; + if (!isCmd) + { + // Debug test + QString fileName(DEFAULT_FILE); + if (!QString(DEFAULT_DIR).isEmpty() && !fileName.isEmpty()) + { + QDir jmtDir(DEFAULT_DIR); + + QString errorMsg; + BenchResults bchResults = ResultParser::parseJsonFile( jmtDir.filePath(fileName), errorMsg ); + + if ( bchResults.benchmarks.isEmpty() ) { + qCritical() << "Error parsing file: " << fileName << " -> " << errorMsg; + return 1; + } + // Selector Test + resultSelector.reset(new ResultSelector(bchResults, jmtDir.filePath(fileName))); + } + else + // Show empty selector + resultSelector.reset(new ResultSelector()); + resultSelector->show(); + } + + // + // Execute + return app.exec(); +} diff --git a/specifelse/benchtest/jomt/src/mainwindow.cpp b/specifelse/benchtest/jomt/src/mainwindow.cpp new file mode 100644 index 0000000..97280d1 --- /dev/null +++ b/specifelse/benchtest/jomt/src/mainwindow.cpp @@ -0,0 +1,31 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "mainwindow.h" +#include "./ui_mainwindow.h" + + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + diff --git a/specifelse/benchtest/jomt/src/plot_parameters.cpp b/specifelse/benchtest/jomt/src/plot_parameters.cpp new file mode 100644 index 0000000..846ff17 --- /dev/null +++ b/specifelse/benchtest/jomt/src/plot_parameters.cpp @@ -0,0 +1,364 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "plot_parameters.h" + +#include + +const char* config_folder = "jomtSettings/"; + + +double getYPlotValue(const BenchData &bchData, PlotValueType yType) +{ + switch (yType) + { + // CPU time + case CpuTimeType: { + return bchData.cpu_time_us; + } + case CpuTimeMinType: { + return bchData.min_cpu; + } + case CpuTimeMeanType: { + return bchData.mean_cpu; + } + case CpuTimeMedianType: { + return bchData.median_cpu; + } + case CpuTimeStddevType: { + return bchData.stddev_cpu; + } + case CpuTimeCvType: { + return bchData.cv_cpu; + } + + // Real time + case RealTimeType: { + return bchData.real_time_us; + } + case RealTimeMinType: { + return bchData.min_real; + } + case RealTimeMeanType: { + return bchData.mean_real; + } + case RealTimeMedianType: { + return bchData.median_real; + } + case RealTimeStddevType: { + return bchData.stddev_real; + } + case RealTimeCvType: { + return bchData.cv_real; + } + + // Iterations + case IterationsType: { + return bchData.iterations; + } + + // Bytes/s + case BytesType: { + return bchData.kbytes_sec_dflt; + } + case BytesMinType: { + return bchData.min_kbytes; + } + case BytesMeanType: { + return bchData.mean_kbytes; + } + case BytesMedianType: { + return bchData.median_kbytes; + } + case BytesStddevType: { + return bchData.stddev_kbytes; + } + case BytesCvType: { + return bchData.cv_kbytes; + } + + // Items/s + case ItemsType: { + return bchData.kitems_sec_dflt; + } + case ItemsMinType: { + return bchData.min_kitems; + } + case ItemsMeanType: { + return bchData.mean_kitems; + } + case ItemsMedianType: { + return bchData.median_kitems; + } + case ItemsStddevType: { + return bchData.stddev_kitems; + } + case ItemsCvType: { + return bchData.cv_kitems; + } + } + + return -1; +} + +QString getYPlotName(PlotValueType yType, QString timeUnit) +{ + if (!timeUnit.isEmpty()) + timeUnit = " (" + timeUnit + ")"; + + switch (yType) + { + // CPU time + case CpuTimeType: { + return "CPU time" + timeUnit; + } + case CpuTimeMinType: { + return "CPU min time" + timeUnit; + } + case CpuTimeMeanType: { + return "CPU mean time" + timeUnit; + } + case CpuTimeMedianType: { + return "CPU median time" + timeUnit; + } + case CpuTimeStddevType: { + return "CPU stddev time" + timeUnit; + } + case CpuTimeCvType: { + return "CPU cv (%)"; + } + + // Real time + case RealTimeType: { + return "Real time" + timeUnit; + } + case RealTimeMinType: { + return "Real min time" + timeUnit; + } + case RealTimeMeanType: { + return "Real mean time" + timeUnit; + } + case RealTimeMedianType: { + return "Real median time" + timeUnit; + } + case RealTimeStddevType: { + return "Real stddev time" + timeUnit; + } + case RealTimeCvType: { + return "Real cv (%)"; + } + + // Iterations + case IterationsType: { + return "Iterations"; + } + + // Bytes/s + case BytesType: { + return "Bytes/s (k)"; + } + case BytesMinType: { + return "Bytes/s min (k)"; + } + case BytesMeanType: { + return "Bytes/s mean (k)"; + } + case BytesMedianType: { + return "Bytes/s median (k)"; + } + case BytesStddevType: { + return "Bytes/s stddev (k)"; + } + case BytesCvType: { + return "Bytes/s cv (%)"; + } + + // Items/s + case ItemsType: { + return "Items/s (k)"; + } + case ItemsMinType: { + return "Items/s min (k)"; + } + case ItemsMeanType: { + return "Items/s mean (k)"; + } + case ItemsMedianType: { + return "Items/s median (k)"; + } + case ItemsStddevType: { + return "Items/s stddev (k)"; + } + case ItemsCvType: { + return "Items/s cv (%)"; + } + } + + return "Unknown"; +} + +double normalizeTimeUs(const BenchData &bchData, double value) +{ + double timeFactor = 1.; + if (bchData.time_unit == "ns") timeFactor = 0.001; + else if (bchData.time_unit == "ms") timeFactor = 1000.; + return value * timeFactor; +} + +bool isYTimeBased(PlotValueType yType) +{ + if ( yType != PlotValueType::RealTimeType && yType != PlotValueType::CpuTimeType + && yType != PlotValueType::RealTimeMinType && yType != PlotValueType::CpuTimeMinType + && yType != PlotValueType::RealTimeMeanType && yType != PlotValueType::CpuTimeMeanType + && yType != PlotValueType::RealTimeMedianType && yType != PlotValueType::CpuTimeMedianType + && yType != PlotValueType::RealTimeStddevType && yType != PlotValueType::CpuTimeStddevType ) + return false; + + return true; +} + +double findMedian(QVector sorted, int begin, int end) +{ + int count = end - begin; + if (count <= 0) return 0.; + + if (count % 2) { + return sorted.at(count / 2 + begin); + } else { + qreal right = sorted.at(count / 2 + begin); + qreal left = sorted.at(count / 2 - 1 + begin); + return (right + left) / 2.0; + } +} + +BenchYStats getYPlotStats(BenchData &bchData, PlotValueType yType) +{ + BenchYStats statRes; + + // No statistics + if (!bchData.hasAggregate) { + statRes.min = 0.; + statRes.max = 0.; + statRes.median = 0.; + statRes.lowQuart = 0.; + statRes.uppQuart = 0.; + + return statRes; + } + + switch (yType) + { + // CPU time + case CpuTimeType: + case CpuTimeMinType: case CpuTimeMeanType: case CpuTimeMedianType: case CpuTimeStddevType: + { + statRes.min = bchData.min_cpu; + statRes.max = bchData.max_cpu; + statRes.median = bchData.median_cpu; + + std::sort(bchData.cpu_time.begin(), bchData.cpu_time.end()); + int count = bchData.cpu_time.count(); + statRes.lowQuart = normalizeTimeUs(bchData, findMedian(bchData.cpu_time, 0, count/2)); + statRes.uppQuart = normalizeTimeUs(bchData, findMedian(bchData.cpu_time, count/2 + (count%2), count)); + + break; + } + // Real time + case RealTimeType: + case RealTimeMinType: case RealTimeMeanType: case RealTimeMedianType: case RealTimeStddevType: + { + statRes.min = bchData.min_real; + statRes.max = bchData.max_real; + statRes.median = bchData.median_real; + + std::sort(bchData.real_time.begin(), bchData.real_time.end()); + int count = bchData.real_time.count(); + statRes.lowQuart = normalizeTimeUs(bchData, findMedian(bchData.real_time, 0, count/2)); + statRes.uppQuart = normalizeTimeUs(bchData, findMedian(bchData.real_time, count/2 + (count%2), count)); + + break; + } + // Bytes/s + case BytesType: + case BytesMinType: case BytesMeanType: case BytesMedianType: case BytesStddevType: + { + statRes.min = bchData.min_kbytes; + statRes.max = bchData.max_kbytes; + statRes.median = bchData.median_kbytes; + + std::sort(bchData.kbytes_sec.begin(), bchData.kbytes_sec.end()); + int count = bchData.kbytes_sec.count(); + statRes.lowQuart = findMedian(bchData.kbytes_sec, 0, count/2); + statRes.uppQuart = findMedian(bchData.kbytes_sec, count/2 + (count%2), count); + + break; + } + // Items/s + case ItemsType: + case ItemsMinType: case ItemsMeanType: case ItemsMedianType: case ItemsStddevType: + { + statRes.min = bchData.min_kitems; + statRes.max = bchData.max_kitems; + statRes.median = bchData.median_kitems; + + std::sort(bchData.kitems_sec.begin(), bchData.kitems_sec.end()); + int count = bchData.kitems_sec.count(); + statRes.lowQuart = findMedian(bchData.kitems_sec, 0, count/2); + statRes.uppQuart = findMedian(bchData.kitems_sec, count/2 + (count%2), count); + + break; + } + default: //Error + { + statRes.min = 0.; + statRes.max = 0.; + statRes.median = 0.; + statRes.lowQuart = 0.; + statRes.uppQuart = 0.; + + break; + } + } + + return statRes; +} + +bool commonPartEqual(const QStringList &listA, const QStringList &listB) +{ + bool isEqual = true; + int maxIdx = std::min(listA.size(), listB.size()); + if (maxIdx <= 0) return false; + + for (int idx=0; isEqual && idx &addFilesA, const QVector &addFilesB) +{ + if (fileA != fileB) + return false; + if (addFilesA.size() != addFilesB.size()) + return false; + + for (int i=0; i. + +#include "plotter_3dbars.h" +#include "ui_plotter_3dbars.h" + +#include "benchmark_results.h" +#include "result_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace QtDataVisualization; + +static const char* config_file = "config_3dbars.json"; +static const bool force_config = false; + + +Plotter3DBars::Plotter3DBars(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &origFilename, + const QVector& addFilenames, QWidget *parent) + : QWidget(parent) + , ui(new Ui::Plotter3DBars) + , mBenchIdxs(bchIdxs) + , mPlotParams(plotParams) + , mOrigFilename(origFilename) + , mAddFilenames(addFilenames) + , mAllIndexes(bchIdxs.size() == bchResults.benchmarks.size()) + , mWatcher(parent) +{ + // UI + ui->setupUi(this); + this->setAttribute(Qt::WA_DeleteOnClose); + + QFileInfo fileInfo(origFilename); + this->setWindowTitle("3D Bars - " + fileInfo.fileName()); + + connectUI(); + + // Init + setupChart(bchResults, bchIdxs, plotParams); + setupOptions(); + + // Show + QWidget *container = QWidget::createWindowContainer(mBars); + ui->horizontalLayout->insertWidget(0, container, 1); +} + +Plotter3DBars::~Plotter3DBars() +{ + // Save options to file + saveConfig(); + + delete ui; +} + +void Plotter3DBars::connectUI() +{ + // Theme + ui->comboBoxTheme->addItem("Primary Colors", Q3DTheme::ThemePrimaryColors); + ui->comboBoxTheme->addItem("Digia", Q3DTheme::ThemeDigia); + ui->comboBoxTheme->addItem("StoneMoss", Q3DTheme::ThemeStoneMoss); + ui->comboBoxTheme->addItem("ArmyBlue", Q3DTheme::ThemeArmyBlue); + ui->comboBoxTheme->addItem("Retro", Q3DTheme::ThemeRetro); + ui->comboBoxTheme->addItem("Ebony", Q3DTheme::ThemeEbony); + ui->comboBoxTheme->addItem("Isabelle", Q3DTheme::ThemeIsabelle); + ui->comboBoxTheme->addItem("Qt", Q3DTheme::ThemeQt); + connect(ui->comboBoxTheme, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboThemeChanged); + + // Bars + setupGradients(); + connect(ui->comboBoxGradient, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboGradientChanged); + + connect(ui->doubleSpinBoxThickness, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinThicknessChanged); + connect(ui->doubleSpinBoxFloor, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinFloorChanged); + connect(ui->doubleSpinBoxSpacingX, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinSpaceXChanged); + connect(ui->doubleSpinBoxSpacingZ, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinSpaceZChanged); + connect(ui->pushButtonSeries, &QPushButton::clicked, this, &Plotter3DBars::onSeriesEditClicked); + + if (!isYTimeBased(mPlotParams.yType)) + ui->comboBoxTimeUnit->setEnabled(false); + else + { + ui->comboBoxTimeUnit->addItem("ns", 1000.); + ui->comboBoxTimeUnit->addItem("us", 1.); + ui->comboBoxTimeUnit->addItem("ms", 0.001); + connect(ui->comboBoxTimeUnit, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboTimeUnitChanged); + } + + // Axes + ui->comboBoxAxis->addItem("X-Axis"); + ui->comboBoxAxis->addItem("Y-Axis"); + ui->comboBoxAxis->addItem("Z-Axis"); + connect(ui->comboBoxAxis, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboAxisChanged); + + connect(ui->checkBoxAxisRotate, &QCheckBox::stateChanged, this, &Plotter3DBars::onCheckAxisRotate); + connect(ui->checkBoxTitle, &QCheckBox::stateChanged, this, &Plotter3DBars::onCheckTitleVisible); + connect(ui->checkBoxLog, &QCheckBox::stateChanged, this, &Plotter3DBars::onCheckLog); + connect(ui->spinBoxLogBase, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DBars::onSpinLogBaseChanged); + connect(ui->lineEditTitle, &QLineEdit::textChanged, this, &Plotter3DBars::onEditTitleChanged); + connect(ui->lineEditFormat, &QLineEdit::textChanged, this, &Plotter3DBars::onEditFormatChanged); + connect(ui->doubleSpinBoxMin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinMinChanged); + connect(ui->doubleSpinBoxMax, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DBars::onSpinMaxChanged); + connect(ui->comboBoxMin, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboMinChanged); + connect(ui->comboBoxMax, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DBars::onComboMaxChanged); + connect(ui->spinBoxTicks, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DBars::onSpinTicksChanged); + connect(ui->spinBoxMTicks, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DBars::onSpinMTicksChanged); + + // Actions + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &Plotter3DBars::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &Plotter3DBars::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &Plotter3DBars::onReloadClicked); + connect(ui->pushButtonSnapshot, &QPushButton::clicked, this, &Plotter3DBars::onSnapshotClicked); +} + +void Plotter3DBars::setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init) +{ + QScopedPointer scopedBars; + Q3DBars* bars = nullptr; + if (init) { + scopedBars.reset( new Q3DBars() ); + bars = scopedBars.get(); + } + else { // Re-init + bars = mBars; + const auto seriesList = bars->seriesList(); + for (const auto barSeries : seriesList) + bars->removeSeries(barSeries); + const auto barsAxes = bars->axes(); + for (const auto axis : barsAxes) + bars->releaseAxis(axis); + mSeriesMapping.clear(); + } + Q_ASSERT(bars); + + // Time unit + mCurrentTimeFactor = 1.; + if ( isYTimeBased(mPlotParams.yType) ) { + if ( bchResults.meta.time_unit == "ns") mCurrentTimeFactor = 1000.; + else if (bchResults.meta.time_unit == "ms") mCurrentTimeFactor = 0.001; + } + + + // 3D + // X: argumentA or templateB + // Y: time/iter/bytes/items (not name dependent) + // Z: argumentC or templateD (with C!=A, D!=B) + bool hasZParam = plotParams.zType != PlotEmptyType; + + // + // No Z-param -> one row per benchmark type + if (!hasZParam) + { + // Single series (i.e. color) + QScopedPointer series(new QBar3DSeries); + + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchIdxs, plotParams.xIdx, "X"); + bool firstCol = true; + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + // One row per benchmark * X-group + QScopedPointer data(new QBarDataRow); + + const QString & subsetName = bchSubset.name; +// qDebug() << "subsetName:" << subsetName; +// qDebug() << "subsetIdxs:" << bchSubset.idxs; + + QStringList colLabels; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + colLabels.append(xName); + + // Add column + data->append( static_cast(getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor) ); + } + // Add benchmark row + series->dataProxy()->addRow(data.take(), subsetName); + + // Set column labels (only if no collision, empty otherwise) + if (firstCol) // init + series->dataProxy()->setColumnLabels(colLabels); + else if ( commonPartEqual(series->dataProxy()->columnLabels(), colLabels) ) { + if (series->dataProxy()->columnLabels().size() < colLabels.size()) // replace by longest + series->dataProxy()->setColumnLabels(colLabels); + } + else { // collision + series->dataProxy()->setColumnLabels( QStringList("") ); + } + firstCol = false; +// qDebug() << "[Multi-NoZ] colLabels:" << colLabels << "|" << series->dataProxy()->columnLabels(); + } + // Add series + series->setItemLabelFormat(QStringLiteral("@rowLabel [X=@colLabel]: @valueLabel")); + series->setMesh(QAbstract3DSeries::MeshBevelBar); + series->setMeshSmooth(false); + mSeriesMapping.push_back({"", ""}); // color set later + + bars->addSeries(series.take()); + } + // + // Z-param -> one series per benchmark, one row per Z, one column per X + else + { + // Initial segmentation by 'full name % param1 % param2' (group benchmarks) + const auto bchNames = bchResults.segment2DNames(bchIdxs, + plotParams.xType == PlotArgumentType, plotParams.xIdx, + plotParams.zType == PlotArgumentType, plotParams.zIdx); + QStringList prevRowLabels, prevColLabels; + bool sameRowLabels = true, sameColLabels = true; + for (const auto& bchName : bchNames) + { + // One series (i.e. color) per 2D name + QScopedPointer series(new QBar3DSeries); +// qDebug() << "bchName" << bchName.name << "|" << bchName.idxs; + + // Segment: one sub per Z-param from 2D names + QVector bchZSubs = bchResults.segmentParam(plotParams.zType == PlotArgumentType, + bchName.idxs, plotParams.zIdx); + QStringList curRowLabels; + for (const auto& bchZSub : qAsConst(bchZSubs)) + { +// qDebug() << "bchZSub" << bchZSub.name << "|" << bchZSub.idxs; + curRowLabels.append(bchZSub.name); + + // One row per Z-param from 2D names + QScopedPointer data(new QBarDataRow); + + // Group: one column per X-param + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchZSub.idxs, plotParams.xIdx, "X"); + Q_ASSERT(bchSubsets.size() == 1); + if (bchSubsets.empty()) { + qWarning() << "Missing X-parameter subset for Z-row: " << bchZSub.name; + break; + } + const auto& bchSubset = bchSubsets[0]; + + QStringList curColLabels; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + curColLabels.append(xName); + + // Y-values on row + data->append( static_cast(getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor) ); + } + // Add benchmark row + series->dataProxy()->addRow(data.take()); + + // Check column labels collisions + if (sameColLabels) { + if ( prevColLabels.isEmpty() ) // init + prevColLabels = curColLabels; + else { + if ( commonPartEqual(prevColLabels, curColLabels) ) { + if (prevColLabels.size() < curColLabels.size()) // replace by longest + prevColLabels = curColLabels; + } + else sameColLabels = false; + } + } +// qDebug() << "[Multi-Z] curColLabels:" << curColLabels << "|" << prevColLabels; + } + // Check row labels collisions + if (sameRowLabels) { + if ( prevRowLabels.isEmpty() ) // init + prevRowLabels = curRowLabels; + else { + if ( commonPartEqual(prevRowLabels, curRowLabels) ) { + if (prevRowLabels.size() < curRowLabels.size()) // replace by longest + prevRowLabels = curRowLabels; + } + else sameRowLabels = false; + } + } +// qDebug() << "[Multi-Z] curRowLabels:" << curRowLabels << "|" << prevRowLabels; + // + // Add series + series->setName( bchName.name ); + mSeriesMapping.push_back({bchName.name, bchName.name}); // color set later + series->setItemLabelFormat(QStringLiteral("@seriesName [@colLabel, @rowLabel]: @valueLabel")); + series->setMesh(QAbstract3DSeries::MeshBevelBar); + series->setMeshSmooth(false); + + bars->addSeries(series.take()); + } + // Set row/column labels (empty if collisions) + if ( !bars->seriesList().isEmpty() && bars->seriesList().constFirst()->dataProxy()->rowCount() > 0) + { + for (auto &series : bars->seriesList()) { + series->dataProxy()->setColumnLabels(sameColLabels ? prevColLabels : QStringList("")); + series->dataProxy()->setRowLabels( sameRowLabels ? prevRowLabels : QStringList("")); + } + } + } + + // Axes + if ( !bars->seriesList().isEmpty() && bars->seriesList().constFirst()->dataProxy()->rowCount() > 0) + { + // General + bars->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium); + + // X-axis + QCategory3DAxis *colAxis = bars->columnAxis(); + if (plotParams.xType == PlotArgumentType) + colAxis->setTitle("Argument " + QString::number(plotParams.xIdx+1)); + else if (plotParams.xType == PlotTemplateType) + colAxis->setTitle("Template " + QString::number(plotParams.xIdx+1)); + if (plotParams.xType != PlotEmptyType) + colAxis->setTitleVisible(true); + + // Y-axis + QValue3DAxis *valAxis = bars->valueAxis(); + valAxis->setTitle( getYPlotName(plotParams.yType, bchResults.meta.time_unit) ); + valAxis->setTitleVisible(true); + + // Z-axis + if (plotParams.zType != PlotEmptyType) + { + QCategory3DAxis *rowAxis = bars->rowAxis(); + if (plotParams.zType == PlotArgumentType) + rowAxis->setTitle("Argument " + QString::number(plotParams.zIdx+1)); + else + rowAxis->setTitle("Template " + QString::number(plotParams.zIdx+1)); + rowAxis->setTitleVisible(true); + } + } + else { + // Title-like + QCategory3DAxis *colAxis = bars->columnAxis(); + colAxis->setTitle("No compatible series to display"); + colAxis->setTitleVisible(true); + } + + if (init) + { + // Take + mBars = scopedBars.take(); + } +} + +void Plotter3DBars::setupOptions(bool init) +{ + // General + if (init) { + mBars->activeTheme()->setType(Q3DTheme::ThemePrimaryColors); + } + + mIgnoreEvents = true; + int prevAxisIdx = ui->comboBoxAxis->currentIndex(); + + if (!init) // Re-init + { + ui->comboBoxAxis->setCurrentIndex(0); + for (auto &axisParams : mAxesParams) + axisParams.reset(); + ui->comboBoxMin->clear(); + ui->comboBoxMax->clear(); + ui->checkBoxAxisRotate->setChecked(false); + ui->checkBoxTitle->setChecked(true); + ui->checkBoxLog->setChecked(false); + ui->comboBoxGradient->setCurrentIndex(0); + } + + // Time unit + if (mCurrentTimeFactor > 1.) ui->comboBoxTimeUnit->setCurrentIndex(0); // ns + else if (mCurrentTimeFactor < 1.) ui->comboBoxTimeUnit->setCurrentIndex(2); // ms + else ui->comboBoxTimeUnit->setCurrentIndex(1); // us + + // Axes + // X-axis + QCategory3DAxis *colAxis = mBars->columnAxis(); + if (colAxis) + { + auto& axisParams = mAxesParams[0]; + + axisParams.titleText = colAxis->title(); + axisParams.title = !axisParams.titleText.isEmpty(); + + ui->doubleSpinBoxMin->setVisible(false); + ui->doubleSpinBoxMax->setVisible(false); + + ui->lineEditTitle->setText( axisParams.titleText ); + ui->lineEditTitle->setCursorPosition(0); + if ( !colAxis->labels().isEmpty() && !colAxis->labels().constFirst().isEmpty() ) { + axisParams.range = colAxis->labels(); + } + else if ( !mBars->seriesList().isEmpty() ) { + int maxCol = 0; + const auto& seriesList = mBars->seriesList(); + for (const auto& series : seriesList) + for (int iR=0; iR < series->dataProxy()->rowCount(); ++iR) + if (maxCol < series->dataProxy()->rowAt(iR)->size()) + maxCol = series->dataProxy()->rowAt(iR)->size(); + for (int i=0; icomboBoxMin->addItems( axisParams.range ); + ui->comboBoxMax->addItems( axisParams.range ); + ui->comboBoxMax->setCurrentIndex(ui->comboBoxMax->count() - 1); + axisParams.maxIdx = ui->comboBoxMax->count() - 1; + } + // Y-axis + QValue3DAxis *valAxis = mBars->valueAxis(); + if (valAxis) + { + auto& axisParams = mAxesParams[1]; + + axisParams.titleText = valAxis->title(); + axisParams.title = !axisParams.titleText.isEmpty(); + + ui->doubleSpinBoxFloor->setMinimum( valAxis->min() ); + ui->doubleSpinBoxFloor->setMaximum( valAxis->max() ); + ui->lineEditFormat->setText( valAxis->labelFormat() ); + ui->lineEditFormat->setCursorPosition(0); + ui->doubleSpinBoxMin->setValue( valAxis->min() ); + ui->doubleSpinBoxMax->setValue( valAxis->max() ); + ui->spinBoxTicks->setValue( valAxis->segmentCount() ); + ui->spinBoxMTicks->setValue( valAxis->subSegmentCount() ); + } + // Z-axis + QCategory3DAxis *rowAxis = mBars->rowAxis(); + if (rowAxis) + { + auto& axisParams = mAxesParams[2]; + + axisParams.titleText = rowAxis->title(); + axisParams.title = !axisParams.titleText.isEmpty(); + if ( !rowAxis->labels().isEmpty() && !rowAxis->labels().constFirst().isEmpty() ) { + axisParams.range = rowAxis->labels(); + } + else if ( !mBars->seriesList().isEmpty() ) { + int maxRow = 0; + const auto& seriesList = mBars->seriesList(); + for (const auto& series : seriesList) + if (maxRow < series->dataProxy()->rowCount()) + maxRow = series->dataProxy()->rowCount(); + for (int i=0; icheckBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); + + // Update series color config + const auto& chartSeries = mBars->seriesList(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = chartSeries.at(idx); + + config.oldColor = series->baseColor(); + if (!config.newColor.isValid()) + config.newColor = series->baseColor(); // init + else + series->setBaseColor(config.newColor); // apply + + if (config.newName != config.oldName) + series->setName( config.newName ); + } + + // Restore selected axis + if (!init) + ui->comboBoxAxis->setCurrentIndex(prevAxisIdx); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() +")"); +} + +void Plotter3DBars::loadConfig(bool init) +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // Theme + if (json.contains("theme") && json["theme"].isString()) + ui->comboBoxTheme->setCurrentText( json["theme"].toString() ); + + // Bars + if (json.contains("bars.gradient") && json["bars.gradient"].isString()) + ui->comboBoxGradient->setCurrentText( json["bars.gradient"].toString() ); + if (json.contains("bars.thick") && json["bars.thick"].isDouble()) + ui->doubleSpinBoxThickness->setValue( json["bars.thick"].toDouble() ); + if (json.contains("bars.floor") && json["bars.floor"].isDouble()) + ui->doubleSpinBoxFloor->setValue( json["bars.floor"].toDouble() ); + if (json.contains("bars.spacing.x") && json["bars.spacing.x"].isDouble()) + ui->doubleSpinBoxSpacingX->setValue( json["bars.spacing.x"].toDouble() ); + if (json.contains("bars.spacing.z") && json["bars.spacing.z"].isDouble()) + ui->doubleSpinBoxSpacingZ->setValue( json["bars.spacing.z"].toDouble() ); + + // Series + if (json.contains("series") && json["series"].isArray()) + { + auto series = json["series"].toArray(); + for (int idx = 0; idx < series.size(); ++idx) { + QJsonObject config = series[idx].toObject(); + if ( config.contains("oldName") && config["oldName"].isString() + && config.contains("newName") && config["newName"].isString() + && config.contains("newColor") && config["newColor"].isString() + && QColor::isValidColor(config["newColor"].toString()) ) + { + SeriesConfig savedConfig(config["oldName"].toString(), ""); + int iCfg = mSeriesMapping.indexOf(savedConfig); + if (iCfg >= 0) { + mSeriesMapping[iCfg].newName = config["newName"].toString(); + mSeriesMapping[iCfg].newColor.setNamedColor( config["newColor"].toString() ); + } + } + } + } + + // Time + if (!init) { + if (json.contains("timeUnit") && json["timeUnit"].isString()) + ui->comboBoxTimeUnit->setCurrentText( json["timeUnit"].toString() ); + } + + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 3; ++idx) + { + auto& axis = mAxesParams[idx]; + + if (json.contains(prefix + ".rotate") && json[prefix + ".rotate"].isBool()) { + axis.rotate = json[prefix + ".rotate"].toBool(); + ui->checkBoxAxisRotate->setChecked( axis.rotate ); + } + if (json.contains(prefix + ".title") && json[prefix + ".title"].isBool()) { + axis.title = json[prefix + ".title"].toBool(); + ui->checkBoxTitle->setChecked( axis.title ); + } + if (!init) + { + if (json.contains(prefix + ".titleText") && json[prefix + ".titleText"].isString()) { + axis.titleText = json[prefix + ".titleText"].toString(); + ui->lineEditTitle->setText( axis.titleText ); + ui->lineEditTitle->setCursorPosition(0); + } + } + // x or z-axis + if (idx == 0 || idx == 2) + { + if (force_config) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isString()) { + ui->comboBoxMin->setCurrentText( json[prefix + ".min"].toString() ); + axis.minIdx = ui->comboBoxMin->currentIndex(); + } + if (json.contains(prefix + ".max") && json[prefix + ".max"].isString()) { + ui->comboBoxMax->setCurrentText( json[prefix + ".max"].toString() ); + axis.maxIdx = ui->comboBoxMax->currentIndex(); + } + } + if (idx == 0) + { + prefix = "axis.y"; + ui->comboBoxAxis->setCurrentIndex(1); + } + } + else // y-axis + { + if (json.contains(prefix + ".log") && json[prefix + ".log"].isBool()) + ui->checkBoxLog->setChecked( json[prefix + ".log"].toBool() ); + if (json.contains(prefix + ".logBase") && json[prefix + ".logBase"].isDouble()) + ui->spinBoxLogBase->setValue( json[prefix + ".logBase"].toInt(10) ); + if (json.contains(prefix + ".labelFormat") && json[prefix + ".labelFormat"].isString()) { + ui->lineEditFormat->setText( json[prefix + ".labelFormat"].toString() ); + ui->lineEditFormat->setCursorPosition(0); + } + if (json.contains(prefix + ".ticks") && json[prefix + ".ticks"].isDouble()) + ui->spinBoxTicks->setValue( json[prefix + ".ticks"].toInt(5) ); + if (json.contains(prefix + ".mticks") && json[prefix + ".mticks"].isDouble()) + ui->spinBoxMTicks->setValue( json[prefix + ".mticks"].toInt(1) ); + if (!init) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isDouble()) + ui->doubleSpinBoxMin->setValue( json[prefix + ".min"].toDouble() ); + if (json.contains(prefix + ".max") && json[prefix + ".max"].isDouble()) + ui->doubleSpinBoxMax->setValue( json[prefix + ".max"].toDouble() ); + } + prefix = "axis.z"; + ui->comboBoxAxis->setCurrentIndex(2); + } + } + ui->comboBoxAxis->setCurrentIndex(0); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } +} + +void Plotter3DBars::saveConfig() +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // Theme + json["theme"] = ui->comboBoxTheme->currentText(); + // Bars + json["bars.gradient"] = ui->comboBoxGradient->currentText(); + json["bars.thick"] = ui->doubleSpinBoxThickness->value(); + json["bars.floor"] = ui->doubleSpinBoxFloor->value(); + json["bars.spacing.x"] = ui->doubleSpinBoxSpacingX->value(); + json["bars.spacing.z"] = ui->doubleSpinBoxSpacingZ->value(); + // Series + QJsonArray series; + for (const auto& seriesConfig : qAsConst(mSeriesMapping)) { + QJsonObject config; + config["oldName"] = seriesConfig.oldName; + config["newName"] = seriesConfig.newName; + config["newColor"] = seriesConfig.newColor.name(); + series.append(config); + } + if (!series.empty()) + json["series"] = series; + // Time + json["timeUnit"] = ui->comboBoxTimeUnit->currentText(); + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 3; ++idx) + { + const auto& axis = mAxesParams[idx]; + + json[prefix + ".rotate"] = axis.rotate; + json[prefix + ".title"] = axis.title; + json[prefix + ".titleText"] = axis.titleText; + // x or z-axis + if (idx == 0 || idx == 2) + { + if ( axis.minIdx >= 0 && axis.minIdx < axis.range.size() + && axis.maxIdx >= 0 && axis.maxIdx < axis.range.size() ) { + json[prefix + ".min"] = axis.range[axis.minIdx]; + json[prefix + ".max"] = axis.range[axis.maxIdx]; + } + prefix = "axis.y"; + } + else // y-axis + { + json[prefix + ".log"] = ui->checkBoxLog->isChecked(); + json[prefix + ".logBase"] = ui->spinBoxLogBase->value(); + json[prefix + ".labelFormat"] = ui->lineEditFormat->text(); + json[prefix + ".min"] = ui->doubleSpinBoxMin->value(); + json[prefix + ".max"] = ui->doubleSpinBoxMax->value(); + json[prefix + ".ticks"] = ui->spinBoxTicks->value(); + json[prefix + ".mticks"] = ui->spinBoxMTicks->value(); + + prefix = "axis.z"; + } + } + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +void Plotter3DBars::setupGradients() +{ + ui->comboBoxGradient->addItem("No gradient"); + + ui->comboBoxGradient->addItem("Deep volcano"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(0.33, Qt::blue); + gr.setColorAt(0.67, Qt::red); gr.setColorAt(1.0, Qt::yellow); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Jungle heat"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkGreen); gr.setColorAt(0.5, Qt::yellow); + gr.setColorAt(0.8, Qt::red); gr.setColorAt(1.0, Qt::darkRed); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Spectral redux"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::blue); gr.setColorAt(0.33, Qt::green); + gr.setColorAt(0.5, Qt::yellow); gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Spectral extended"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::magenta); gr.setColorAt(0.25, Qt::blue); + gr.setColorAt(0.5, Qt::cyan); + gr.setColorAt(0.67, Qt::green); gr.setColorAt(0.83, Qt::yellow); + gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Reddish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkRed); gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Greenish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkGreen); gr.setColorAt(1.0, Qt::green); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Bluish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkCyan); gr.setColorAt(1.0, Qt::cyan); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(1.0, Qt::white); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray inverted"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::white); gr.setColorAt(1.0, Qt::black); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray centered"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(0.5, Qt::white); + gr.setColorAt(1.0, Qt::black); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray inv-centered"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::white); gr.setColorAt(0.5, Qt::black); + gr.setColorAt(1.0, Qt::white); + mGrads.push_back(gr); + } +} + +// +// Theme +void Plotter3DBars::onComboThemeChanged(int index) +{ + Q3DTheme::Theme theme = static_cast( + ui->comboBoxTheme->itemData(index).toInt()); + mBars->activeTheme()->setType(theme); + + onComboGradientChanged( ui->comboBoxGradient->currentIndex() ); + + // Update series color + const auto& chartSeries = mBars->seriesList(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = chartSeries.at(idx); + auto prevColor = config.oldColor; + + config.oldColor = series->baseColor(); + if (config.newColor != prevColor) + series->setBaseColor(config.newColor); // re-apply config + else + config.newColor = config.oldColor; // sync with theme + } +} + +// +// Bars +void Plotter3DBars::onComboGradientChanged(int idx) +{ + if (idx == 0) + { + for (auto& series : mBars->seriesList()) + series->setColorStyle(Q3DTheme::ColorStyleUniform); + } + else + { + for (auto& series : mBars->seriesList()) { + series->setBaseGradient( mGrads[idx-1] ); + series->setColorStyle(Q3DTheme::ColorStyleRangeGradient); + } + } +} + +void Plotter3DBars::onSpinThicknessChanged(double d) +{ + mBars->setBarThickness(d); +} + +void Plotter3DBars::onSpinFloorChanged(double d) +{ + mBars->setFloorLevel(d); +} + +void Plotter3DBars::onSpinSpaceXChanged(double d) +{ + QSizeF barSpacing = mBars->barSpacing(); + barSpacing.setWidth(d); + + mBars->setBarSpacing(barSpacing); +} + +void Plotter3DBars::onSpinSpaceZChanged(double d) +{ + QSizeF barSpacing = mBars->barSpacing(); + barSpacing.setHeight(d); + + mBars->setBarSpacing(barSpacing); +} + +void Plotter3DBars::onSeriesEditClicked() +{ + SeriesDialog seriesDialog(mSeriesMapping, this); + auto res = seriesDialog.exec(); + if (res == QDialog::Accepted) + { + const auto& chartSeries = mBars->seriesList(); + const auto& newMapping = seriesDialog.getMapping(); + for (int idx = 0; idx < newMapping.size(); ++idx) + { + const auto& newPair = newMapping[idx]; + const auto& oldPair = mSeriesMapping[idx]; + auto series = chartSeries.at(idx); + if (newPair.newName != oldPair.newName) { + series->setName( newPair.newName ); + } + if (newPair.newColor != oldPair.newColor) { + series->setBaseColor(newPair.newColor); + } + } + mSeriesMapping = newMapping; + } +} + +void Plotter3DBars::onComboTimeUnitChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + + // Update data + double unitFactor = ui->comboBoxTimeUnit->currentData().toDouble(); + double updateFactor = unitFactor / mCurrentTimeFactor; // can cause precision loss + auto chartSeries = mBars->seriesList(); + if (chartSeries.empty()) + return; + + for (auto& series : chartSeries) + { + const auto& dataProxy = series->dataProxy(); + for (int iR = 0; iR < dataProxy->rowCount(); ++iR) + { + auto row = dataProxy->rowAt(iR); + for (int iC = 0; iC < row->size(); ++iC) + { + auto item = dataProxy->itemAt(iR, iC); + dataProxy->setItem(iR, iC, + QBarDataItem( static_cast(item->value() * updateFactor) )); + } + } + } + + // Update axis title + QString oldUnitName = "(us)"; + if (mCurrentTimeFactor > 1.) oldUnitName = "(ns)"; + else if (mCurrentTimeFactor < 1.) oldUnitName = "(ms)"; + + auto yAxis = mBars->valueAxis(); + if (yAxis) { + QString axisTitle = yAxis->title(); + if (axisTitle.endsWith(oldUnitName)) { + QString unitName = ui->comboBoxTimeUnit->currentText(); + onEditTitleChanged2(axisTitle.replace(axisTitle.size() - 3, 2, unitName), 1); + } + } + // Update range + if (updateFactor > 1.) { // enforce proper order + ui->doubleSpinBoxMax->setValue(ui->doubleSpinBoxMax->value() * updateFactor); + ui->doubleSpinBoxMin->setValue(ui->doubleSpinBoxMin->value() * updateFactor); + } + else { + ui->doubleSpinBoxMin->setValue(ui->doubleSpinBoxMin->value() * updateFactor); + ui->doubleSpinBoxMax->setValue(ui->doubleSpinBoxMax->value() * updateFactor); + } + + mCurrentTimeFactor = unitFactor; +} + +// +// Axes +void Plotter3DBars::onComboAxisChanged(int idx) +{ + // Update UI + bool wasIgnoring = mIgnoreEvents; + mIgnoreEvents = true; + + ui->checkBoxAxisRotate->setChecked( mAxesParams[idx].rotate ); + ui->checkBoxTitle->setChecked( mAxesParams[idx].title ); + ui->checkBoxLog->setEnabled( idx == 1 ); + ui->spinBoxLogBase->setEnabled( ui->checkBoxLog->isEnabled() && ui->checkBoxLog->isChecked() ); + ui->lineEditTitle->setText( mAxesParams[idx].titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->lineEditFormat->setEnabled( idx == 1 ); + // Force visibility order + if (idx == 1) { + ui->comboBoxMin->setVisible(false); + ui->comboBoxMax->setVisible(false); + ui->doubleSpinBoxMin->setVisible(true); + ui->doubleSpinBoxMax->setVisible(true); + } + else { + ui->doubleSpinBoxMin->setVisible(false); + ui->doubleSpinBoxMax->setVisible(false); + ui->comboBoxMin->setVisible(true); + ui->comboBoxMax->setVisible(true); + + ui->comboBoxMin->clear(); + ui->comboBoxMax->clear(); + ui->comboBoxMin->addItems( mAxesParams[idx].range ); + ui->comboBoxMax->addItems( mAxesParams[idx].range ); + ui->comboBoxMin->setCurrentIndex( mAxesParams[idx].minIdx ); + ui->comboBoxMax->setCurrentIndex( mAxesParams[idx].maxIdx ); + } + ui->spinBoxTicks->setEnabled( idx == 1 && !ui->checkBoxLog->isChecked() ); + ui->spinBoxMTicks->setEnabled( idx == 1 && !ui->checkBoxLog->isChecked() ); + + mIgnoreEvents = wasIgnoring; +} + +void Plotter3DBars::onCheckAxisRotate(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QAbstract3DAxis* axis; + if (iAxis == 0) axis = mBars->columnAxis(); + else if (iAxis == 1) axis = mBars->valueAxis(); + else axis = mBars->rowAxis(); + + if (axis) { + axis->setTitleFixed(state != Qt::Checked); + axis->setLabelAutoRotation(state == Qt::Checked ? 90 : 0); + mAxesParams[iAxis].rotate = state == Qt::Checked; + } +} + +void Plotter3DBars::onCheckTitleVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QAbstract3DAxis* axis; + if (iAxis == 0) axis = mBars->columnAxis(); + else if (iAxis == 1) axis = mBars->valueAxis(); + else axis = mBars->rowAxis(); + + if (axis) { + axis->setTitleVisible(state == Qt::Checked); + mAxesParams[iAxis].title = state == Qt::Checked; + } +} + +void Plotter3DBars::onCheckLog(int state) +{ + if (mIgnoreEvents) return; + Q_ASSERT(ui->comboBoxAxis->currentIndex() == 1); + QValue3DAxis* axis = mBars->valueAxis(); + + if (axis) + { + if (state == Qt::Checked) { + axis->setFormatter(new QLogValue3DAxisFormatter()); + ui->doubleSpinBoxMin->setMinimum(0.001); + } + else { + axis->setFormatter(new QValue3DAxisFormatter()); + ui->doubleSpinBoxMin->setMinimum(0.); + } + ui->spinBoxTicks->setEnabled( state != Qt::Checked); + ui->spinBoxMTicks->setEnabled( state != Qt::Checked); + ui->spinBoxLogBase->setEnabled(state == Qt::Checked); + } +} + +void Plotter3DBars::onSpinLogBaseChanged(int i) +{ + if (mIgnoreEvents) return; + Q_ASSERT(ui->comboBoxAxis->currentIndex() == 1); + QValue3DAxis* axis = mBars->valueAxis(); + + if (axis) + { + QLogValue3DAxisFormatter* formatter = (QLogValue3DAxisFormatter*)axis->formatter(); + if (formatter) + formatter->setBase(i); + } +} + +void Plotter3DBars::onEditTitleChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onEditTitleChanged2(text, iAxis); +} + +void Plotter3DBars::onEditTitleChanged2(const QString& text, int iAxis) +{ + QAbstract3DAxis* axis; + if (iAxis == 0) axis = mBars->columnAxis(); + else if (iAxis == 1) axis = mBars->valueAxis(); + else axis = mBars->rowAxis(); + + if (axis) { + axis->setTitle(text); + mAxesParams[iAxis].titleText = text; + } +} + +void Plotter3DBars::onEditFormatChanged(const QString& text) +{ + if (mIgnoreEvents) return; + Q_ASSERT(ui->comboBoxAxis->currentIndex() == 1); + QValue3DAxis* axis = mBars->valueAxis(); + + if (axis) { + axis->setLabelFormat(text); + } +} + +void Plotter3DBars::onSpinMinChanged(double d) +{ + if (mIgnoreEvents) return; + QAbstract3DAxis* axis = mBars->valueAxis(); + + if (axis) { + axis->setMin(d); + } +} + +void Plotter3DBars::onSpinMaxChanged(double d) +{ + if (mIgnoreEvents) return; + QAbstract3DAxis* axis = mBars->valueAxis(); + + if (axis) { + axis->setMax(d); + } +} + +void Plotter3DBars::onComboMinChanged(int index) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QAbstract3DAxis* axis; + if (iAxis == 0) axis = mBars->columnAxis(); + else axis = mBars->rowAxis(); + + if (axis) { + axis->setMin(index); + mAxesParams[iAxis].minIdx = index; + } +} + +void Plotter3DBars::onComboMaxChanged(int index) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QAbstract3DAxis* axis; + if (iAxis == 0) axis = mBars->columnAxis(); + else axis = mBars->rowAxis(); + + if (axis) { + axis->setMax(index); + mAxesParams[iAxis].maxIdx = index; + } +} + +void Plotter3DBars::onSpinTicksChanged(int i) +{ + if (mIgnoreEvents) return; + Q_ASSERT(ui->comboBoxAxis->currentIndex() == 1); + QValue3DAxis* axis = mBars->valueAxis(); + + if (axis) { + axis->setSegmentCount(i); + } +} + +void Plotter3DBars::onSpinMTicksChanged(int i) +{ + if (mIgnoreEvents) return; + Q_ASSERT(ui->comboBoxAxis->currentIndex() == 1); + QValue3DAxis* axis = mBars->valueAxis(); + + if (axis) { + axis->setSubSegmentCount(i); + } +} + +// +// Actions +void Plotter3DBars::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : mAddFilenames) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void Plotter3DBars::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void Plotter3DBars::onReloadClicked() +{ + // Load new results + QString errorMsg; + BenchResults newBchResults = ResultParser::parseJsonFile( mOrigFilename, errorMsg ); + + if ( newBchResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing original file: " + mOrigFilename + " -> " + errorMsg); + return; + } + + for (const auto& addFile : qAsConst(mAddFilenames)) + { + errorMsg.clear(); + BenchResults newAddResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if ( newAddResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing additional file: " + addFile.filename + " -> " + errorMsg); + return; + } + + if (addFile.isAppend) + newBchResults.appendResults(newAddResults); + else + newBchResults.overwriteResults(newAddResults); + } + + // Check compatibility with previous + errorMsg.clear(); + if (mBenchIdxs.size() != newBchResults.benchmarks.size()) + { + errorMsg = "Number of series/points is different"; + if (mAllIndexes) + { + mBenchIdxs.clear(); + for (int i=0; iseriesList(); + if (oldBarsSeries.size() != 1) { + errorMsg = "No single series originally"; + break; + } + const auto oldSeries = oldBarsSeries[0]; + const auto oldDataProxy = oldSeries->dataProxy(); + + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + mBenchIdxs, mPlotParams.xIdx, "X"); + if (newBchSubsets.size() != oldDataProxy->rowCount()) { + errorMsg = "Number of single series rows is different"; + break; + } + + int newRowsIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + const auto& oldRowLabel = oldDataProxy->rowLabels().at(newRowsIdx); + const QString& subsetName = bchSubset.name; + if (subsetName != oldRowLabel) + { + errorMsg = "Series row has different name"; + break; + } + const auto& oldRow = oldDataProxy->rowAt(newRowsIdx); + if (bchSubset.idxs.size() != oldRow->size()) + { + errorMsg = "Number of series columns is different"; + break; + } + ++newRowsIdx; + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + newRowsIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + int newColsIdx = 0; + for (int idx : bchSubset.idxs) + { + // Update item + oldDataProxy->setItem(newRowsIdx, newColsIdx, + QBarDataItem( static_cast(getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor) )); + ++newColsIdx; + } + ++newRowsIdx; + } + } + } + else + { + // Check compatibility with previous + const auto& oldBarsSeries = mBars->seriesList(); + if (oldBarsSeries.empty()) { + errorMsg = "No series originally"; + break; + } + + const auto newBchNames = newBchResults.segment2DNames(mBenchIdxs, + mPlotParams.xType == PlotArgumentType, mPlotParams.xIdx, + mPlotParams.zType == PlotArgumentType, mPlotParams.zIdx); + if (newBchNames.size() != oldBarsSeries.size()) { + errorMsg = "Number of series is different"; + break; + } + + int newSeriesIdx = 0; + for (const auto& bchName : newBchNames) + { + const auto& oldSeries = oldBarsSeries.at(newSeriesIdx); + const auto& oldDataProxy = oldSeries->dataProxy(); + if (bchName.name != mSeriesMapping[newSeriesIdx].oldName) + { + errorMsg = "Series has different name"; + break; + } + + QVector newBchZSubs = newBchResults.segmentParam(mPlotParams.zType == PlotArgumentType, + bchName.idxs, mPlotParams.zIdx); + if (newBchZSubs.size() != oldDataProxy->rowCount()) { + errorMsg = "Number of series rows is different"; + break; + } + + int newRowsIdx = 0; + for (const auto& bchZSub : qAsConst(newBchZSubs)) + { + const auto& oldRowLabel = oldDataProxy->rowLabels().size() < newRowsIdx ? + oldDataProxy->rowLabels().at(newRowsIdx) : ""; + const QString& subsetName = bchZSub.name; + if (subsetName != oldRowLabel && !oldRowLabel.isEmpty()) + { + errorMsg = "Series row has different name"; + break; + } + + const auto& oldRow = oldDataProxy->rowAt(newRowsIdx); + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + bchZSub.idxs, mPlotParams.xIdx, "X"); + Q_ASSERT(newBchSubsets.size() == 1); + if (newBchSubsets.empty()) { + qWarning() << "Missing X-parameter subset for Z-row: " << bchZSub.name; + break; + } + if (newBchSubsets[0].idxs.size() != oldRow->size()) + { + errorMsg = "Number of series columns is different"; + break; + } + ++newRowsIdx; + } + ++newSeriesIdx; + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + newSeriesIdx = 0; + for (const auto& bchName : newBchNames) + { + const auto& oldSeries = oldBarsSeries.at(newSeriesIdx); + const auto& oldDataProxy = oldSeries->dataProxy(); + QVector newBchZSubs = newBchResults.segmentParam(mPlotParams.zType == PlotArgumentType, + bchName.idxs, mPlotParams.zIdx); + int newRowsIdx = 0; + for (const auto& bchZSub : qAsConst(newBchZSubs)) + { + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + bchZSub.idxs, mPlotParams.xIdx, "X"); + if (newBchSubsets.empty()) + break; + const auto& bchSubset = newBchSubsets[0]; + + int newColsIdx = 0; + for (int idx : bchSubset.idxs) + { + // Update item + oldDataProxy->setItem(newRowsIdx, newColsIdx, + QBarDataItem( static_cast(getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor) )); + ++newColsIdx; + } + ++newRowsIdx; + } + ++newSeriesIdx; + } + } + } + + break; // once + } + + if ( !errorMsg.isEmpty() ) + { + // Reset update if all benchmarks + if (mAllIndexes) + { + saveConfig(); + setupChart(newBchResults, mBenchIdxs, mPlotParams, false); + setupOptions(false); + } + else + { + QMessageBox::critical(this, "Chart reload", errorMsg); + return; + } + } + + // Restore Y-range + onSpinMinChanged( ui->doubleSpinBoxMin->value() ); // force update + onSpinMaxChanged( ui->doubleSpinBoxMax->value() ); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() +")"); +} + +void Plotter3DBars::onSnapshotClicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save snapshot"), "", tr("Images (*.png)")); + + if ( !fileName.isEmpty() ) + { + QImage image = mBars->renderToImage(8); + + bool ok = image.save(fileName, "PNG"); + if (!ok) + QMessageBox::warning(this, "Chart snapshot", "Error saving snapshot file."); + } +} diff --git a/specifelse/benchtest/jomt/src/plotter_3dsurface.cpp b/specifelse/benchtest/jomt/src/plotter_3dsurface.cpp new file mode 100644 index 0000000..ea03a40 --- /dev/null +++ b/specifelse/benchtest/jomt/src/plotter_3dsurface.cpp @@ -0,0 +1,1486 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "plotter_3dsurface.h" +#include "ui_plotter_3dsurface.h" + +#include "benchmark_results.h" +#include "result_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace QtDataVisualization; + +static const char* config_file = "config_3dsurface.json"; +static const bool force_config = false; + + +Plotter3DSurface::Plotter3DSurface(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &origFilename, + const QVector& addFilenames, QWidget *parent) + : QWidget(parent) + , ui(new Ui::Plotter3DSurface) + , mBenchIdxs(bchIdxs) + , mPlotParams(plotParams) + , mOrigFilename(origFilename) + , mAddFilenames(addFilenames) + , mAllIndexes(bchIdxs.size() == bchResults.benchmarks.size()) + , mWatcher(parent) +{ + // UI + ui->setupUi(this); + this->setAttribute(Qt::WA_DeleteOnClose); + + QFileInfo fileInfo(origFilename); + this->setWindowTitle("3D Surface - " + fileInfo.fileName()); + + connectUI(); + + // Init + setupChart(bchResults, bchIdxs, plotParams); + setupOptions(); + + // Show + QWidget *container = QWidget::createWindowContainer(mSurface); + ui->horizontalLayout->insertWidget(0, container, 1); +} + +Plotter3DSurface::~Plotter3DSurface() +{ + // Save options to file + saveConfig(); + + delete ui; +} + +void Plotter3DSurface::connectUI() +{ + // Theme + ui->comboBoxTheme->addItem("Primary Colors", Q3DTheme::ThemePrimaryColors); + ui->comboBoxTheme->addItem("Digia", Q3DTheme::ThemeDigia); + ui->comboBoxTheme->addItem("StoneMoss", Q3DTheme::ThemeStoneMoss); + ui->comboBoxTheme->addItem("ArmyBlue", Q3DTheme::ThemeArmyBlue); + ui->comboBoxTheme->addItem("Retro", Q3DTheme::ThemeRetro); + ui->comboBoxTheme->addItem("Ebony", Q3DTheme::ThemeEbony); + ui->comboBoxTheme->addItem("Isabelle", Q3DTheme::ThemeIsabelle); + ui->comboBoxTheme->addItem("Qt", Q3DTheme::ThemeQt); + connect(ui->comboBoxTheme, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DSurface::onComboThemeChanged); + + // Surface + connect(ui->checkBoxFlip, &QCheckBox::stateChanged, this, &Plotter3DSurface::onCheckFlip); + + setupGradients(); + connect(ui->comboBoxGradient, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DSurface::onComboGradientChanged); + connect(ui->pushButtonSeries, &QPushButton::clicked, this, &Plotter3DSurface::onSeriesEditClicked); + + if (!isYTimeBased(mPlotParams.yType)) + ui->comboBoxTimeUnit->setEnabled(false); + else + { + ui->comboBoxTimeUnit->addItem("ns", 1000.); + ui->comboBoxTimeUnit->addItem("us", 1.); + ui->comboBoxTimeUnit->addItem("ms", 0.001); + connect(ui->comboBoxTimeUnit, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DSurface::onComboTimeUnitChanged); + } + + // Axes + ui->comboBoxAxis->addItem("X-Axis"); + ui->comboBoxAxis->addItem("Y-Axis"); + ui->comboBoxAxis->addItem("Z-Axis"); + connect(ui->comboBoxAxis, QOverload::of(&QComboBox::currentIndexChanged), this, &Plotter3DSurface::onComboAxisChanged); + + connect(ui->checkBoxAxisRotate, &QCheckBox::stateChanged, this, &Plotter3DSurface::onCheckAxisRotate); + connect(ui->checkBoxTitle, &QCheckBox::stateChanged, this, &Plotter3DSurface::onCheckTitleVisible); + connect(ui->checkBoxLog, &QCheckBox::stateChanged, this, &Plotter3DSurface::onCheckLog); + connect(ui->spinBoxLogBase, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DSurface::onSpinLogBaseChanged); + connect(ui->lineEditTitle, &QLineEdit::textChanged, this, &Plotter3DSurface::onEditTitleChanged); + connect(ui->lineEditFormat, &QLineEdit::textChanged, this, &Plotter3DSurface::onEditFormatChanged); + connect(ui->doubleSpinBoxMin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DSurface::onSpinMinChanged); + connect(ui->doubleSpinBoxMax, QOverload::of(&QDoubleSpinBox::valueChanged), this, &Plotter3DSurface::onSpinMaxChanged); + connect(ui->spinBoxTicks, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DSurface::onSpinTicksChanged); + connect(ui->spinBoxMTicks, QOverload::of(&QSpinBox::valueChanged), this, &Plotter3DSurface::onSpinMTicksChanged); + + // Actions + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &Plotter3DSurface::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &Plotter3DSurface::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &Plotter3DSurface::onReloadClicked); + connect(ui->pushButtonSnapshot, &QPushButton::clicked, this, &Plotter3DSurface::onSnapshotClicked); +} + +void Plotter3DSurface::setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init) +{ + QScopedPointer scopedSurface; + Q3DSurface* surface = nullptr; + if (init) { + scopedSurface.reset( new Q3DSurface() ); + surface = scopedSurface.get(); + } + else { // Re-init + surface = mSurface; + const auto seriesList = surface->seriesList(); + for (const auto surfaceSeries : seriesList) + surface->removeSeries(surfaceSeries); + const auto surfaceAxes = surface->axes(); + for (const auto axis : surfaceAxes) + surface->releaseAxis(axis); + mSeriesMapping.clear(); + } + Q_ASSERT(surface); + + // Time unit + mCurrentTimeFactor = 1.; + if ( isYTimeBased(mPlotParams.yType) ) { + if ( bchResults.meta.time_unit == "ns") mCurrentTimeFactor = 1000.; + else if (bchResults.meta.time_unit == "ms") mCurrentTimeFactor = 0.001; + } + + + // 3D + // X: argumentA or templateB + // Y: time/iter/bytes/items (not name dependent) + // Z: argumentC or templateD (with C!=A, D!=B) + bool custXAxis = true, custZAxis = true; + QString custXName, custZName; + bool hasZParam = plotParams.zType != PlotEmptyType; + + // + // No Z-param -> one row per benchmark type + if (!hasZParam) + { + // Single series (i.e. color) + QSurfaceDataProxy *dataProxy = new QSurfaceDataProxy(); + QScopedPointer series(new QSurface3DSeries(dataProxy)); + QScopedPointer dataArray(new QSurfaceDataArray); + + // Segment per X-param + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchIdxs, plotParams.xIdx, "X"); + // Check subsets symmetry/min size + bool symBchOK = true, symOK = true, minOK = true; + QString culpritName; + int refSize = bchSubsets.empty() ? 0 : bchSubsets[0].idxs.size(); + for (int i = 0; symOK && minOK && i < bchSubsets.size(); ++i) { + symOK = bchSubsets[i].idxs.size() == refSize; + minOK = bchSubsets[i].idxs.size() >= 2; + if (!symOK || !minOK) + culpritName = bchSubsets[i].name; + } + // Ignore asymmetrical series + if (!symOK) { + qWarning() << "Inconsistent number of X-values between benchmarks to trace surface for: " << culpritName; + } + // Ignore single-row series + else if (!minOK) { + qWarning() << "Not enough X-values to trace surface for: " << culpritName; + } + else + { + int prevRowSize = 0; + double zFallback = 0.; + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + // Check inter benchmark consistency + if (prevRowSize > 0 && prevRowSize != bchSubset.idxs.size()) { + symBchOK = false; + qWarning() << "Inconsistent number of X-values between benchmarks to trace surface"; + break; + } + prevRowSize = bchSubset.idxs.size(); + + // One row per X-group + QScopedPointer newRow(new QSurfaceDataRow( bchSubset.idxs.size() )); + +// const QString & subsetName = bchSubset.name; +// qDebug() << "subsetName:" << subsetName; +// qDebug() << "subsetIdxs:" << bchSubset.idxs; + + int index = 0; + double xFallback = 0.; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custXName, custXAxis, xFallback); + + // Y val + double yVal = getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor; +// qDebug() << "-> [" << xVal << yVal << zFallback << "]"; + + // Add column + (*newRow)[index++].setPosition( QVector3D(xVal, yVal, zFallback) ); + } + // Add row + dataArray->append(newRow.take()); + + ++zFallback; + } + } + if (symBchOK && dataArray->size() > 0) + { + // Add series + dataProxy->resetArray(dataArray.take()); + + series->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe); + series->setFlatShadingEnabled(true); + series->setItemLabelFormat(QStringLiteral("[@xLabel, @zLabel]: @yLabel")); + mSeriesMapping.push_back({"", ""}); // color set later + + surface->addSeries(series.take()); + } + } + // + // Z-param -> one series per benchmark type + else + { + // Initial segmentation by 'full name % param1 % param2' (group benchmarks) + const auto bchNames = bchResults.segment2DNames(bchIdxs, + plotParams.xType == PlotArgumentType, plotParams.xIdx, + plotParams.zType == PlotArgumentType, plotParams.zIdx); + for (const auto& bchName : bchNames) + { + // One series (i.e. color) per 2D-name + QSurfaceDataProxy *dataProxy = new QSurfaceDataProxy(); + QScopedPointer series(new QSurface3DSeries(dataProxy)); + +// qDebug() << "bchName" << bchName.name << "|" << bchName.idxs; + + // One subset per Z-param from 2D-names + QVector bchZSubs = bchResults.segmentParam(plotParams.zType == PlotArgumentType, + bchName.idxs, plotParams.zIdx); + // Ignore incompatible series + if ( bchZSubs.isEmpty() ) { + qWarning() << "No Z-value to trace surface for other benchmarks"; + continue; + } + + // Check subsets symmetry/min size + bool symOK = true, minOK = true; + QString culpritName; + int refSize = bchZSubs[0].idxs.size(); + for (int i=0; symOK && minOK && i= 2; + if (!symOK || !minOK) + culpritName = bchZSubs[0].name; + } + // Ignore asymmetrical series + if (!symOK) { + qWarning() << "Inconsistent number of X-values between benchmarks to trace surface for: " + << bchName.name + " [Z=" + culpritName + "]"; + continue; + } + // Ignore single-row series + else if (!minOK) { + qWarning() << "Not enough X-values to trace surface for: " + << bchName.name + " [Z=" + culpritName + "]"; + continue; + } + + QScopedPointer dataArray(new QSurfaceDataArray); + double zFallback = 0.; + for (const auto& bchZSub : qAsConst(bchZSubs)) + { + QString zName = bchZSub.name; +// qDebug() << "bchZSub" << bchZSub.name << "|" << bchZSub.idxs; + + double zVal = BenchResults::getParamValue(zName, custZName, custZAxis, zFallback); + + // One row per Z-param from 2D-names + QScopedPointer newRow(new QSurfaceDataRow( bchZSub.idxs.size() )); + + // One subset per X-param from Z-Subset + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchZSub.idxs, plotParams.xIdx, "X"); + Q_ASSERT(bchSubsets.size() <= 1); + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + int index = 0; + double xFallback = 0.; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custXName, custXAxis, xFallback); + + // Y val + double yVal = getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor; +// qDebug() << "-> [" << xVal << yVal << zVal << "]"; + + // Add column + (*newRow)[index++].setPosition( QVector3D(xVal, yVal, zVal) ); + } + // Add row + dataArray->append(newRow.take()); + } + } + // Add series + dataProxy->resetArray(dataArray.take()); + + series->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe); + series->setFlatShadingEnabled(true); + series->setName(bchName.name); + mSeriesMapping.push_back({bchName.name, bchName.name}); // color set later + series->setItemLabelFormat(QStringLiteral("@seriesName [@xLabel, @zLabel]: @yLabel")); + + surface->addSeries(series.take()); + } + } + + // Axes + if ( !surface->seriesList().isEmpty() && surface->seriesList().constFirst()->dataProxy()->rowCount() > 0) + { + // General + surface->setHorizontalAspectRatio(1.0); + surface->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium); + + // X-axis + QValue3DAxis *xAxis = surface->axisX(); + if (plotParams.xType == PlotArgumentType) + xAxis->setTitle("Argument " + QString::number(plotParams.xIdx+1)); + else { // template + if ( !custXName.isEmpty() ) + xAxis->setTitle(custXName); + else + xAxis->setTitle("Template " + QString::number(plotParams.xIdx+1)); + } + xAxis->setTitleVisible(true); + xAxis->setSegmentCount(8); + + // Y-axis + QValue3DAxis *yAxis = surface->axisY(); + yAxis->setTitle( getYPlotName(plotParams.yType, bchResults.meta.time_unit) ); + yAxis->setTitleVisible(true); + + // Z-axis + QValue3DAxis *zAxis = surface->axisZ(); + if (plotParams.zType != PlotEmptyType) + { + if (plotParams.zType == PlotArgumentType) + zAxis->setTitle("Argument " + QString::number(plotParams.zIdx+1)); + else { // template + if ( !custZName.isEmpty() ) + zAxis->setTitle(custZName); + else + zAxis->setTitle("Template " + QString::number(plotParams.zIdx+1)); + } + zAxis->setTitleVisible(true); + } + zAxis->setSegmentCount(8); + } + else { + // Title-like + QValue3DAxis *yAxis = surface->axisY(); + yAxis->setTitle("No compatible series to display"); + yAxis->setTitleVisible(true); + + qWarning() << "No compatible series to display"; + } + + if (init) + { + // Take + mSurface = scopedSurface.take(); + } +} + +void Plotter3DSurface::setupOptions(bool init) +{ + // General + if (init) { + mSurface->activeTheme()->setType(Q3DTheme::ThemePrimaryColors); + } + + mIgnoreEvents = true; + int prevAxisIdx = ui->comboBoxAxis->currentIndex(); + + if (!init) // Re-init + { + ui->comboBoxAxis->setCurrentIndex(0); + for (auto &axisParams : mAxesParams) + axisParams.reset(); + ui->checkBoxAxisRotate->setChecked(false); + ui->checkBoxTitle->setChecked(true); + ui->checkBoxLog->setChecked(false); + ui->spinBoxLogBase->setValue(10); + ui->comboBoxGradient->setCurrentIndex(0); + } + + // Time unit + if (mCurrentTimeFactor > 1.) ui->comboBoxTimeUnit->setCurrentIndex(0); // ns + else if (mCurrentTimeFactor < 1.) ui->comboBoxTimeUnit->setCurrentIndex(2); // ms + else ui->comboBoxTimeUnit->setCurrentIndex(1); // us + + // Axes + // X-axis + QValue3DAxis *xAxis = mSurface->axisX(); + if (xAxis) + { + auto& axisParam = mAxesParams[0]; + + axisParam.titleText = xAxis->title(); + axisParam.title = !axisParam.titleText.isEmpty(); + axisParam.labelFormat = "%g"; + xAxis->setLabelFormat(axisParam.labelFormat); + axisParam.min = xAxis->min(); + axisParam.max = xAxis->max(); + axisParam.ticks = xAxis->segmentCount(); + axisParam.mticks = xAxis->subSegmentCount(); + + ui->checkBoxTitle->setChecked( axisParam.title ); + ui->lineEditTitle->setText( axisParam.titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->lineEditFormat->setText( axisParam.labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + ui->doubleSpinBoxMin->setValue( axisParam.min ); + ui->doubleSpinBoxMax->setValue( axisParam.max ); + ui->spinBoxTicks->setValue( axisParam.ticks ); + ui->spinBoxMTicks->setValue( axisParam.mticks ); + } + // Y-axis + QValue3DAxis *yAxis = mSurface->axisY(); + if (yAxis) + { + auto& axisParam = mAxesParams[1]; + + axisParam.titleText = yAxis->title(); + axisParam.title = !axisParam.titleText.isEmpty(); + axisParam.labelFormat = yAxis->labelFormat(); + axisParam.min = yAxis->min(); + axisParam.max = yAxis->max(); + axisParam.ticks = yAxis->segmentCount(); + axisParam.mticks = yAxis->subSegmentCount(); + } + // Z-axis + QValue3DAxis *zAxis = mSurface->axisZ(); + if (zAxis) + { + auto& axisParam = mAxesParams[2]; + + axisParam.titleText = zAxis->title(); + axisParam.title = !axisParam.titleText.isEmpty(); + axisParam.labelFormat = "%g"; + zAxis->setLabelFormat(axisParam.labelFormat); + axisParam.min = zAxis->min(); + axisParam.max = zAxis->max(); + axisParam.ticks = zAxis->segmentCount(); + axisParam.mticks = zAxis->subSegmentCount(); + } + mIgnoreEvents = false; + + + // Load options from file + loadConfig(init); + + + // Apply actions + if (ui->checkBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); + + // Update series color config + const auto& chartSeries = mSurface->seriesList(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = chartSeries.at(idx); + + config.oldColor = series->baseColor(); + if (!config.newColor.isValid()) + config.newColor = series->baseColor(); // init + else + series->setBaseColor(config.newColor); // apply + + if (config.newName != config.oldName) + series->setName( config.newName ); + } + + // Restore selected axis + if (!init) + ui->comboBoxAxis->setCurrentIndex(prevAxisIdx); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString()+ ")"); +} + +void Plotter3DSurface::loadConfig(bool init) +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // Theme + if (json.contains("theme") && json["theme"].isString()) + ui->comboBoxTheme->setCurrentText( json["theme"].toString() ); + + // Surface + if (json.contains("surface.flip") && json["surface.flip"].isBool()) + ui->checkBoxFlip->setChecked( json["surface.flip"].toBool() ); + if (json.contains("surface.gradient") && json["surface.gradient"].isString()) + ui->comboBoxGradient->setCurrentText( json["surface.gradient"].toString() ); + + // Series + if (json.contains("series") && json["series"].isArray()) + { + auto series = json["series"].toArray(); + for (int idx = 0; idx < series.size(); ++idx) { + QJsonObject config = series[idx].toObject(); + if ( config.contains("oldName") && config["oldName"].isString() + && config.contains("newName") && config["newName"].isString() + && config.contains("newColor") && config["newColor"].isString() + && QColor::isValidColor(config["newColor"].toString()) ) + { + SeriesConfig savedConfig(config["oldName"].toString(), ""); + int iCfg = mSeriesMapping.indexOf(savedConfig); + if (iCfg >= 0) { + mSeriesMapping[iCfg].newName = config["newName"].toString(); + mSeriesMapping[iCfg].newColor.setNamedColor( config["newColor"].toString() ); + } + } + } + } + + // Time + if (!init) { + if (json.contains("timeUnit") && json["timeUnit"].isString()) + ui->comboBoxTimeUnit->setCurrentText( json["timeUnit"].toString() ); + } + + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 3; ++idx) + { + auto& axis = mAxesParams[idx]; + + if (json.contains(prefix + ".rotate") && json[prefix + ".rotate"].isBool()) { + axis.rotate = json[prefix + ".rotate"].toBool(); + ui->checkBoxAxisRotate->setChecked( axis.rotate ); + } + if (json.contains(prefix + ".title") && json[prefix + ".title"].isBool()) { + axis.title = json[prefix + ".title"].toBool(); + ui->checkBoxTitle->setChecked( axis.title ); + } + if (json.contains(prefix + ".log") && json[prefix + ".log"].isBool()) { + axis.log = json[prefix + ".log"].toBool(); + ui->checkBoxLog->setChecked( axis.log ); + } + if (json.contains(prefix + ".logBase") && json[prefix + ".logBase"].isDouble()) { + axis.logBase = json[prefix + ".logBase"].toInt(10); + ui->spinBoxLogBase->setValue( axis.logBase ); + } + if (json.contains(prefix + ".labelFormat") && json[prefix + ".labelFormat"].isString()) { + axis.labelFormat = json[prefix + ".labelFormat"].toString(); + ui->lineEditFormat->setText( axis.labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + } + if (json.contains(prefix + ".ticks") && json[prefix + ".ticks"].isDouble()) { + axis.ticks = json[prefix + ".ticks"].toInt(idx == 1 ? 5 : 8); + ui->spinBoxTicks->setValue( axis.ticks ); + } + if (json.contains(prefix + ".mticks") && json[prefix + ".mticks"].isDouble()) { + axis.mticks = json[prefix + ".mticks"].toInt(1); + ui->spinBoxMTicks->setValue( axis.mticks ); + } + if (!init) + { + if (json.contains(prefix + ".titleText") && json[prefix + ".titleText"].isString()) { + axis.titleText = json[prefix + ".titleText"].toString(); + ui->lineEditTitle->setText( axis.titleText ); + ui->lineEditTitle->setCursorPosition(0); + } + if (idx == 1 || force_config) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isDouble()) { + axis.min = json[prefix + ".min"].toDouble(); + ui->doubleSpinBoxMin->setValue( axis.min ); + } + if (json.contains(prefix + ".max") && json[prefix + ".max"].isDouble()) { + axis.max = json[prefix + ".max"].toDouble(); + ui->doubleSpinBoxMax->setValue( axis.max ); + } + } + } + if (idx == 0) + { + prefix = "axis.y"; + ui->comboBoxAxis->setCurrentIndex(1); + } + else if (idx == 1) + { + prefix = "axis.z"; + ui->comboBoxAxis->setCurrentIndex(2); + } + } + ui->comboBoxAxis->setCurrentIndex(0); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } +} + +void Plotter3DSurface::saveConfig() +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // Theme + json["theme"] = ui->comboBoxTheme->currentText(); + // Surface + json["surface.flip"] = ui->checkBoxFlip->isChecked(); + json["surface.gradient"] = ui->comboBoxGradient->currentText(); + // Series + QJsonArray series; + for (const auto& seriesConfig : qAsConst(mSeriesMapping)) { + QJsonObject config; + config["oldName"] = seriesConfig.oldName; + config["newName"] = seriesConfig.newName; + config["newColor"] = seriesConfig.newColor.name(); + series.append(config); + } + if (!series.empty()) + json["series"] = series; + // Time + json["timeUnit"] = ui->comboBoxTimeUnit->currentText(); + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 3; ++idx) + { + const auto& axis = mAxesParams[idx]; + + json[prefix + ".rotate"] = axis.rotate; + json[prefix + ".title"] = axis.title; + json[prefix + ".log"] = axis.log; + json[prefix + ".logBase"] = axis.logBase; + json[prefix + ".titleText"] = axis.titleText; + json[prefix + ".labelFormat"] = axis.labelFormat; + json[prefix + ".min"] = axis.min; + json[prefix + ".max"] = axis.max; + json[prefix + ".ticks"] = axis.ticks; + json[prefix + ".mticks"] = axis.mticks; + + if (idx == 0) + prefix = "axis.y"; + else if (idx == 1) + prefix = "axis.z"; + } + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +void Plotter3DSurface::setupGradients() +{ + ui->comboBoxGradient->addItem("No gradient"); + + ui->comboBoxGradient->addItem("Deep volcano"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(0.33, Qt::blue); + gr.setColorAt(0.67, Qt::red); gr.setColorAt(1.0, Qt::yellow); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Jungle heat"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkGreen); gr.setColorAt(0.5, Qt::yellow); + gr.setColorAt(0.8, Qt::red); gr.setColorAt(1.0, Qt::darkRed); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Spectral redux"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::blue); gr.setColorAt(0.33, Qt::green); + gr.setColorAt(0.5, Qt::yellow); gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Spectral extended"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::magenta); gr.setColorAt(0.25, Qt::blue); + gr.setColorAt(0.5, Qt::cyan); + gr.setColorAt(0.67, Qt::green); gr.setColorAt(0.83, Qt::yellow); + gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Reddish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkRed); gr.setColorAt(1.0, Qt::red); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Greenish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkGreen); gr.setColorAt(1.0, Qt::green); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Bluish"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::darkCyan); gr.setColorAt(1.0, Qt::cyan); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(1.0, Qt::white); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray inverted"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::white); gr.setColorAt(1.0, Qt::black); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray centered"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::black); gr.setColorAt(0.5, Qt::white); + gr.setColorAt(1.0, Qt::black); + mGrads.push_back(gr); + } + + ui->comboBoxGradient->addItem("Gray inv-centered"); { + QLinearGradient gr; + gr.setColorAt(0.0, Qt::white); gr.setColorAt(0.5, Qt::black); + gr.setColorAt(1.0, Qt::white); + mGrads.push_back(gr); + } +} + +// +// Theme +void Plotter3DSurface::onComboThemeChanged(int index) +{ + Q3DTheme::Theme theme = static_cast( + ui->comboBoxTheme->itemData(index).toInt()); + mSurface->activeTheme()->setType(theme); + + onComboGradientChanged( ui->comboBoxGradient->currentIndex() ); + + // Update series color + const auto& chartSeries = mSurface->seriesList(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = chartSeries.at(idx); + auto prevColor = config.oldColor; + + config.oldColor = series->baseColor(); + if (config.newColor != prevColor) + series->setBaseColor(config.newColor); // re-apply config + else + config.newColor = config.oldColor; // sync with theme + } +} + +// +// Surface +void Plotter3DSurface::onCheckFlip(int state) +{ + mSurface->setFlipHorizontalGrid(state == Qt::Checked); +} + +void Plotter3DSurface::onComboGradientChanged(int idx) +{ + if (idx == 0) + { + for (auto& series : mSurface->seriesList()) + series->setColorStyle(Q3DTheme::ColorStyleUniform); + } + else + { + for (auto& series : mSurface->seriesList()) { + series->setBaseGradient( mGrads[idx-1] ); + series->setColorStyle(Q3DTheme::ColorStyleRangeGradient); + } + } +} + +void Plotter3DSurface::onSeriesEditClicked() +{ + SeriesDialog seriesDialog(mSeriesMapping, this); + auto res = seriesDialog.exec(); + if (res == QDialog::Accepted) + { + const auto& chartSeries = mSurface->seriesList(); + const auto& newMapping = seriesDialog.getMapping(); + for (int idx = 0; idx < newMapping.size(); ++idx) + { + const auto& newPair = newMapping[idx]; + const auto& oldPair = mSeriesMapping[idx]; + auto series = chartSeries.at(idx); + if (newPair.newName != oldPair.newName) { + series->setName( newPair.newName ); + } + if (newPair.newColor != oldPair.newColor) { + series->setBaseColor(newPair.newColor); + } + } + mSeriesMapping = newMapping; + } +} + +void Plotter3DSurface::onComboTimeUnitChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + + // Update data + double unitFactor = ui->comboBoxTimeUnit->currentData().toDouble(); + double updateFactor = unitFactor / mCurrentTimeFactor; // can cause precision loss + auto chartSeries = mSurface->seriesList(); + if (chartSeries.empty()) + return; + + for (auto& series : chartSeries) + { + const auto& dataProxy = series->dataProxy(); + for (int iR = 0; iR < dataProxy->rowCount(); ++iR) + { + for (int iC = 0; iC < dataProxy->columnCount(); ++iC) + { + auto item = dataProxy->itemAt(iR, iC); + dataProxy->setItem(iR, iC, + QSurfaceDataItem( QVector3D(item->x(), item->y() * updateFactor, item->z()) )); + } + } + } + + // Update axis title + QString oldUnitName = "(us)"; + if (mCurrentTimeFactor > 1.) oldUnitName = "(ns)"; + else if (mCurrentTimeFactor < 1.) oldUnitName = "(ms)"; + + auto yAxis = mSurface->axisY(); + if (yAxis) { + QString axisTitle = yAxis->title(); + if (axisTitle.endsWith(oldUnitName)) { + QString unitName = ui->comboBoxTimeUnit->currentText(); + onEditTitleChanged2(axisTitle.replace(axisTitle.size() - 3, 2, unitName), 1); + } + } + // Update range + if (ui->comboBoxAxis->currentIndex() == 1) + { + if (updateFactor > 1.) { // enforce proper order + ui->doubleSpinBoxMax->setValue(mAxesParams[1].max * updateFactor); + ui->doubleSpinBoxMin->setValue(mAxesParams[1].min * updateFactor); + } + else { + ui->doubleSpinBoxMin->setValue(mAxesParams[1].min * updateFactor); + ui->doubleSpinBoxMax->setValue(mAxesParams[1].max * updateFactor); + } + } + else { + if (updateFactor > 1.) { // enforce proper order + onSpinMaxChanged2(mAxesParams[1].max * updateFactor, 1); + onSpinMinChanged2(mAxesParams[1].min * updateFactor, 1); + } + else { + onSpinMinChanged2(mAxesParams[1].min * updateFactor, 1); + onSpinMaxChanged2(mAxesParams[1].max * updateFactor, 1); + } + } + + mCurrentTimeFactor = unitFactor; +} + +// +// Axes +void Plotter3DSurface::onComboAxisChanged(int idx) +{ + // Update UI + bool wasIgnoring = mIgnoreEvents; + mIgnoreEvents = true; + + ui->checkBoxAxisRotate->setChecked( mAxesParams[idx].rotate ); + ui->checkBoxTitle->setChecked( mAxesParams[idx].title ); + ui->checkBoxLog->setChecked( mAxesParams[idx].log ); + ui->spinBoxLogBase->setValue( mAxesParams[idx].logBase ); + ui->spinBoxLogBase->setEnabled( ui->checkBoxLog->isChecked() ); + ui->lineEditTitle->setText( mAxesParams[idx].titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->lineEditFormat->setText( mAxesParams[idx].labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + ui->doubleSpinBoxMin->setDecimals(idx == 1 ? 6 : 3); + ui->doubleSpinBoxMax->setDecimals(idx == 1 ? 6 : 3); + ui->doubleSpinBoxMin->setValue( mAxesParams[idx].min ); + ui->doubleSpinBoxMax->setValue( mAxesParams[idx].max ); + ui->doubleSpinBoxMin->setSingleStep(idx == 1 ? 0.1 : 1.0); + ui->doubleSpinBoxMax->setSingleStep(idx == 1 ? 0.1 : 1.0); + ui->spinBoxTicks->setValue( mAxesParams[idx].ticks ); + ui->spinBoxTicks->setEnabled( !ui->checkBoxLog->isChecked() ); + ui->spinBoxMTicks->setValue( mAxesParams[idx].mticks ); + ui->spinBoxMTicks->setEnabled( !ui->checkBoxLog->isChecked() ); + + mIgnoreEvents = wasIgnoring; +} + +void Plotter3DSurface::onCheckAxisRotate(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setTitleFixed(state != Qt::Checked); + axis->setLabelAutoRotation(state == Qt::Checked ? 90 : 0); + mAxesParams[iAxis].rotate = state == Qt::Checked; + } +} + +void Plotter3DSurface::onCheckTitleVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setTitleVisible(state == Qt::Checked); + mAxesParams[iAxis].title = state == Qt::Checked; + } +} + +void Plotter3DSurface::onCheckLog(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) + { + if (state == Qt::Checked) { + axis->setFormatter(new QLogValue3DAxisFormatter()); + ui->doubleSpinBoxMin->setMinimum( 0.001 ); + mAxesParams[iAxis].min = axis->min(); + } + else { + axis->setFormatter(new QValue3DAxisFormatter()); + ui->doubleSpinBoxMin->setMinimum( 0. ); + mAxesParams[iAxis].max = axis->max(); + } + mAxesParams[iAxis].log = state == Qt::Checked; + ui->spinBoxTicks->setEnabled( state != Qt::Checked); + ui->spinBoxMTicks->setEnabled( state != Qt::Checked); + ui->spinBoxLogBase->setEnabled(state == Qt::Checked); + } +} + +void Plotter3DSurface::onSpinLogBaseChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) + { + QLogValue3DAxisFormatter* formatter = (QLogValue3DAxisFormatter*)axis->formatter(); + if (formatter) { + formatter->setBase(i); + mAxesParams[iAxis].logBase = i; + } + } +} + +void Plotter3DSurface::onEditTitleChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onEditTitleChanged2(text, iAxis); +} + +void Plotter3DSurface::onEditTitleChanged2(const QString& text, int iAxis) +{ + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setTitle(text); + mAxesParams[iAxis].titleText = text; + } +} + +void Plotter3DSurface::onEditFormatChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setLabelFormat(text); + mAxesParams[iAxis].labelFormat = text; + } +} + +void Plotter3DSurface::onSpinMinChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMinChanged2(d, iAxis); +} + +void Plotter3DSurface::onSpinMinChanged2(double d, int iAxis) +{ + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setMin(d); + mAxesParams[iAxis].min = d; + } +} + +void Plotter3DSurface::onSpinMaxChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMaxChanged2(d, iAxis); +} + +void Plotter3DSurface::onSpinMaxChanged2(double d, int iAxis) +{ + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setMax(d); + mAxesParams[iAxis].max = d; + } +} + +void Plotter3DSurface::onSpinTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setSegmentCount(i); + mAxesParams[iAxis].ticks = i; + } +} + +void Plotter3DSurface::onSpinMTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + QValue3DAxis* axis; + if (iAxis == 0) axis = mSurface->axisX(); + else if (iAxis == 1) axis = mSurface->axisY(); + else axis = mSurface->axisZ(); + + if (axis) { + axis->setSubSegmentCount(i); + mAxesParams[iAxis].mticks = i; + } +} + +// +// Actions +void Plotter3DSurface::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : mAddFilenames) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void Plotter3DSurface::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void Plotter3DSurface::onReloadClicked() +{ + // Load new results + QString errorMsg; + BenchResults newBchResults = ResultParser::parseJsonFile( mOrigFilename, errorMsg ); + + if ( newBchResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing original file: " + mOrigFilename + " -> " + errorMsg); + return; + } + + for (const auto& addFile : qAsConst(mAddFilenames)) + { + errorMsg.clear(); + BenchResults newAddResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if ( newAddResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing additional file: " + addFile.filename + " -> " + errorMsg); + return; + } + + if (addFile.isAppend) + newBchResults.appendResults(newAddResults); + else + newBchResults.overwriteResults(newAddResults); + } + + // Check compatibility with previous + errorMsg.clear(); + if (mBenchIdxs.size() != newBchResults.benchmarks.size()) + { + errorMsg = "Number of series/points is different"; + if (mAllIndexes) + { + mBenchIdxs.clear(); + for (int i=0; iseriesList(); + if (oldSurfaceSeries.size() != 1) { + errorMsg = "No single series originally"; + break; + } + const auto& oldSeries = oldSurfaceSeries[0]; + const auto& oldDataProxy = oldSeries->dataProxy(); + const auto& oldDataArray = oldDataProxy->array(); + + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + mBenchIdxs, mPlotParams.xIdx, "X"); + // Check subsets symmetry/min size + bool symOK = true, minOK = true; + QString culpritName; + Q_ASSERT(!newBchSubsets.empty()); + int refSize = newBchSubsets.empty() ? 0 : newBchSubsets[0].idxs.size(); + for (int i = 0; symOK && minOK && i < newBchSubsets.size(); ++i) { + symOK = newBchSubsets[i].idxs.size() == refSize; + minOK = newBchSubsets[i].idxs.size() >= 2; + if (!symOK || !minOK) + culpritName = newBchSubsets[i].name; + } + if (!symOK) { + errorMsg = "Inconsistent number of X-values between benchmarks to trace surface for: " + culpritName; + break; + } + else if (!minOK) { + errorMsg = "Not enough X-values to trace surface for: " + culpritName; + break; + } + // Check rows + if (newBchSubsets.size() != oldDataProxy->rowCount()) { + errorMsg = "Number of single series rows is different"; + break; + } + + int prevRowSize = 0; + int newRowsIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Check inter benchmark consistency + if (prevRowSize > 0 && prevRowSize != bchSubset.idxs.size()) { + errorMsg = "Inconsistent number of X-values between benchmarks to trace surface"; + break; + } + prevRowSize = bchSubset.idxs.size(); + + const auto& oldRow = oldDataArray->at(newRowsIdx); + if (bchSubset.idxs.size() != oldRow->size()) + { + errorMsg = "Number of series columns is different"; + break; + } + ++newRowsIdx; + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + bool custXAxis = true; + QString custXName; + double zFallback = 0.; + + newRowsIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + double xFallback = 0.; + int newColsIdx = 0; + for (int idx : bchSubset.idxs) + { + // Update item + QString xName = newBchResults.getParamName(mPlotParams.xType == PlotArgumentType, + idx, mPlotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custXName, custXAxis, xFallback); + double yVal = getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor; + + oldDataProxy->setItem(newRowsIdx, newColsIdx, + QSurfaceDataItem( QVector3D(xVal, yVal, zFallback) )); + ++newColsIdx; + } + ++zFallback; + ++newRowsIdx; + } + } + } + else + { + // Check compatibility with previous + const auto& oldSurfaceSeries = mSurface->seriesList(); + if (oldSurfaceSeries.empty()) { + errorMsg = "No series originally"; + break; + } + const auto newBchNames = newBchResults.segment2DNames(mBenchIdxs, + mPlotParams.xType == PlotArgumentType, mPlotParams.xIdx, + mPlotParams.zType == PlotArgumentType, mPlotParams.zIdx); + if (newBchNames.size() < oldSurfaceSeries.size()) { + errorMsg = "Number of series is different"; + break; + } + + int newSeriesIdx = 0; + for (const auto& bchName : newBchNames) + { + QVector newBchZSubs = newBchResults.segmentParam(mPlotParams.zType == PlotArgumentType, + bchName.idxs, mPlotParams.zIdx); + // Ignore incompatible series + if ( newBchZSubs.isEmpty() ) { + qWarning() << "No Z-value to trace surface for other benchmarks"; + continue; + } + + // Check subsets symmetry/min size + bool symOK = true, minOK = true; + QString culpritName; + int refSize = newBchZSubs[0].idxs.size(); + for (int i=0; symOK && minOK && i= 2; + if (!symOK || !minOK) + culpritName = newBchZSubs[i].name; + } + if (!symOK) { + qWarning() << "Inconsistent number of X-values between benchmarks to trace surface for:" + << bchName.name + "[Z=" + culpritName + "]"; + continue; + } + else if (!minOK) { + qWarning() << "Not enough X-values to trace surface for:" + << bchName.name + "[Z=" + culpritName + "]"; + continue; + } + + const auto& oldSeries = oldSurfaceSeries.at(newSeriesIdx); + const auto& oldDataProxy = oldSeries->dataProxy(); + const auto& oldDataArray = oldDataProxy->array(); + if (bchName.name != mSeriesMapping[newSeriesIdx].oldName) + { + errorMsg = "Series has different name"; + break; + } + if (newBchZSubs.size() != oldDataProxy->rowCount()) { + errorMsg = "Number of single series rows is different"; + break; + } + + int newRowsIdx = 0; + for (const auto& bchZSub : qAsConst(newBchZSubs)) + { + const auto& oldRow = oldDataArray->at(newRowsIdx); + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + bchZSub.idxs, mPlotParams.xIdx, "X"); + Q_ASSERT(newBchSubsets.size() <= 1); + if (newBchSubsets.empty() || newBchSubsets[0].idxs.size() != oldRow->size()) + { + errorMsg = "Number of series columns is different"; + break; + } + ++newRowsIdx; + } + ++newSeriesIdx; + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + bool custXAxis = true, custZAxis = true; + QString custXName, custZName; + + newSeriesIdx = 0; + for (const auto& bchName : newBchNames) + { + QVector newBchZSubs = newBchResults.segmentParam(mPlotParams.zType == PlotArgumentType, + bchName.idxs, mPlotParams.zIdx); + // Ignore incompatible series + if ( newBchZSubs.isEmpty() ) + continue; + + // Check subsets symmetry/min size + bool symOK = true, minOK = true; + QString culpritName; + int refSize = newBchZSubs[0].idxs.size(); + for (int i=0; symOK && minOK && i= 2; + if (!symOK || !minOK) + culpritName = newBchZSubs[i].name; + } + if (!symOK || !minOK) + continue; + + const auto& oldSeries = oldSurfaceSeries.at(newSeriesIdx); + const auto& oldDataProxy = oldSeries->dataProxy(); + + double zFallback = 0.; + int newRowsIdx = 0; + for (const auto& bchZSub : qAsConst(newBchZSubs)) + { + const QString zName = bchZSub.name; + double zVal = BenchResults::getParamValue(zName, custZName, custZAxis, zFallback); + + QVector newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + bchZSub.idxs, mPlotParams.xIdx, "X"); + Q_ASSERT(newBchSubsets.size() == 1); + const auto& bchSubset = newBchSubsets[0]; + + double xFallback = 0.; + int newColsIdx = 0; + for (int idx : bchSubset.idxs) + { + // Update item + QString xName = newBchResults.getParamName(mPlotParams.xType == PlotArgumentType, + idx, mPlotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custXName, custXAxis, xFallback); + double yVal = getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor; + + oldDataProxy->setItem(newRowsIdx, newColsIdx, + QSurfaceDataItem( QVector3D(xVal, yVal, zVal) )); + ++newColsIdx; + } + ++newRowsIdx; + } + ++newSeriesIdx; + } + } + } + + break; // once + } + + if ( !errorMsg.isEmpty() ) + { + // Reset update if all benchmarks + if (mAllIndexes) + { + saveConfig(); + setupChart(newBchResults, mBenchIdxs, mPlotParams, false); + setupOptions(false); + } + else + { + QMessageBox::critical(this, "Chart reload", errorMsg); + return; + } + } + + // Restore Y-range + QValue3DAxis* axisY = mSurface->axisY(); + if (axisY) + { + axisY->setMin(mAxesParams[1].min); + axisY->setMax(mAxesParams[1].max); + } + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() +")"); +} + +void Plotter3DSurface::onSnapshotClicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save snapshot"), "", tr("Images (*.png)")); + + if ( !fileName.isEmpty() ) + { + QImage image = mSurface->renderToImage(8); + + bool ok = image.save(fileName, "PNG"); + if (!ok) + QMessageBox::warning(this, "Chart snapshot", "Error saving snapshot file."); + } +} diff --git a/specifelse/benchtest/jomt/src/plotter_barchart.cpp b/specifelse/benchtest/jomt/src/plotter_barchart.cpp new file mode 100644 index 0000000..2141c64 --- /dev/null +++ b/specifelse/benchtest/jomt/src/plotter_barchart.cpp @@ -0,0 +1,1264 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "plotter_barchart.h" +#include "ui_plotter_barchart.h" + +#include "benchmark_results.h" +#include "result_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace QtCharts; + +static const char* config_file = "config_bars.json"; +static const bool force_config = false; + + +PlotterBarChart::PlotterBarChart(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &origFilename, + const QVector& addFilenames, QWidget *parent) + : QWidget(parent) + , ui(new Ui::PlotterBarChart) + , mBenchIdxs(bchIdxs) + , mPlotParams(plotParams) + , mOrigFilename(origFilename) + , mAddFilenames(addFilenames) + , mAllIndexes(bchIdxs.size() == bchResults.benchmarks.size()) + , mWatcher(parent) + , mIsVert(plotParams.type == ChartBarType) +{ + // UI + ui->setupUi(this); + this->setAttribute(Qt::WA_DeleteOnClose); + + QFileInfo fileInfo(origFilename); + QString chartType = mIsVert ? "Bars - " : "HBars - "; + this->setWindowTitle(chartType + fileInfo.fileName()); + + connectUI(); + + // Init + setupChart(bchResults, bchIdxs, plotParams); + setupOptions(); + + // Show + ui->horizontalLayout->insertWidget(0, mChartView); +} + +PlotterBarChart::~PlotterBarChart() +{ + // Save options to file + saveConfig(); + + delete ui; +} + +void PlotterBarChart::connectUI() +{ + // Theme + ui->comboBoxTheme->addItem("Light", QChart::ChartThemeLight); + ui->comboBoxTheme->addItem("Blue Cerulean", QChart::ChartThemeBlueCerulean); + ui->comboBoxTheme->addItem("Dark", QChart::ChartThemeDark); + ui->comboBoxTheme->addItem("Brown Sand", QChart::ChartThemeBrownSand); + ui->comboBoxTheme->addItem("Blue Ncs", QChart::ChartThemeBlueNcs); + ui->comboBoxTheme->addItem("High Contrast", QChart::ChartThemeHighContrast); + ui->comboBoxTheme->addItem("Blue Icy", QChart::ChartThemeBlueIcy); + ui->comboBoxTheme->addItem("Qt", QChart::ChartThemeQt); + connect(ui->comboBoxTheme, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboThemeChanged); + + // Legend + connect(ui->checkBoxLegendVisible, &QCheckBox::stateChanged, this, &PlotterBarChart::onCheckLegendVisible); + + ui->comboBoxLegendAlign->addItem("Top", Qt::AlignTop); + ui->comboBoxLegendAlign->addItem("Bottom", Qt::AlignBottom); + ui->comboBoxLegendAlign->addItem("Left", Qt::AlignLeft); + ui->comboBoxLegendAlign->addItem("Right", Qt::AlignRight); + connect(ui->comboBoxLegendAlign, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboLegendAlignChanged); + + connect(ui->spinBoxLegendFontSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinLegendFontSizeChanged); + connect(ui->pushButtonSeries, &QPushButton::clicked, this, &PlotterBarChart::onSeriesEditClicked); + + if (!isYTimeBased(mPlotParams.yType)) + ui->comboBoxTimeUnit->setEnabled(false); + else + { + ui->comboBoxTimeUnit->addItem("ns", 1000.); + ui->comboBoxTimeUnit->addItem("us", 1.); + ui->comboBoxTimeUnit->addItem("ms", 0.001); + connect(ui->comboBoxTimeUnit, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboTimeUnitChanged); + } + + // Axes + ui->comboBoxAxis->addItem("X-Axis"); + ui->comboBoxAxis->addItem("Y-Axis"); + connect(ui->comboBoxAxis, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboAxisChanged); + + ui->comboBoxValuePosition->addItem("None", -1); + ui->comboBoxValuePosition->addItem("Center", QAbstractBarSeries::LabelsCenter); + ui->comboBoxValuePosition->addItem("InsideEnd", QAbstractBarSeries::LabelsInsideEnd); + ui->comboBoxValuePosition->addItem("InsideBase", QAbstractBarSeries::LabelsInsideBase); + ui->comboBoxValuePosition->addItem("OutsideEnd", QAbstractBarSeries::LabelsOutsideEnd); + connect(ui->comboBoxValuePosition, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboValuePositionChanged); + + ui->comboBoxValueAngle->addItem("Right", 360.); + ui->comboBoxValueAngle->addItem("Up", -90.); + ui->comboBoxValueAngle->addItem("Down", 90.); + connect(ui->comboBoxValueAngle, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboValueAngleChanged); + + connect(ui->checkBoxAxisVisible, &QCheckBox::stateChanged, this, &PlotterBarChart::onCheckAxisVisible); + connect(ui->checkBoxTitle, &QCheckBox::stateChanged, this, &PlotterBarChart::onCheckTitleVisible); + connect(ui->checkBoxLog, &QCheckBox::stateChanged, this, &PlotterBarChart::onCheckLog); + connect(ui->spinBoxLogBase, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinLogBaseChanged); + connect(ui->lineEditTitle, &QLineEdit::textChanged, this, &PlotterBarChart::onEditTitleChanged); + connect(ui->spinBoxTitleSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinTitleSizeChanged); + connect(ui->lineEditFormat, &QLineEdit::textChanged, this, &PlotterBarChart::onEditFormatChanged); + connect(ui->spinBoxLabelSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinLabelSizeChanged); + connect(ui->doubleSpinBoxMin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterBarChart::onSpinMinChanged); + connect(ui->doubleSpinBoxMax, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterBarChart::onSpinMaxChanged); + connect(ui->comboBoxMin, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboMinChanged); + connect(ui->comboBoxMax, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBarChart::onComboMaxChanged); + connect(ui->spinBoxTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinTicksChanged); + connect(ui->spinBoxMTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBarChart::onSpinMTicksChanged); + + // Actions + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &PlotterBarChart::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &PlotterBarChart::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &PlotterBarChart::onReloadClicked); + connect(ui->pushButtonSnapshot, &QPushButton::clicked, this, &PlotterBarChart::onSnapshotClicked); +} + +void PlotterBarChart::setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init) +{ + QScopedPointer scopedChart; + QChart* chart = nullptr; + if (init) { + scopedChart.reset( new QChart() ); + chart = scopedChart.get(); + } + else { // Re-init + chart = mChartView->chart(); + chart->setTitle(""); + chart->removeAllSeries(); + const auto xAxes = chart->axes(Qt::Horizontal); + if ( !xAxes.empty() ) + chart->removeAxis( xAxes.constFirst() ); + const auto yAxes = chart->axes(Qt::Vertical); + if ( !yAxes.empty() ) + chart->removeAxis( yAxes.constFirst() ); + mSeriesMapping.clear(); + } + Q_ASSERT(chart); + + // Time unit + mCurrentTimeFactor = 1.; + if ( isYTimeBased(mPlotParams.yType) ) { + if ( bchResults.meta.time_unit == "ns") mCurrentTimeFactor = 1000.; + else if (bchResults.meta.time_unit == "ms") mCurrentTimeFactor = 0.001; + } + + // Single series, one barset per benchmark type + QScopedPointer scopedSeries; + if (mIsVert) scopedSeries.reset(new QBarSeries()); + else scopedSeries.reset(new QHorizontalBarSeries()); + QAbstractBarSeries* series = scopedSeries.get(); + + + // 2D Bars + // X: argumentA or templateB + // Y: time/iter/bytes/items (not name dependent) + // Bar: one per benchmark % X-param + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchIdxs, plotParams.xIdx, "X"); + // Ignore empty series + if ( bchSubsets.isEmpty() ) { + qWarning() << "No compatible series to display"; + } + + bool firstCol = true; + QStringList prevColLabels; + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + // Ignore empty set + if ( bchSubset.idxs.isEmpty() ) { + qWarning() << "No X-value to trace bar for:" << bchSubset.name; + continue; + } + + const QString& subsetName = bchSubset.name; +// qDebug() << "subsetName:" << subsetName; +// qDebug() << "subsetIdxs:" << bchSubset.idxs; + + // X-row + QScopedPointer barSet(new QBarSet( subsetName.toHtmlEscaped() )); + mSeriesMapping.push_back({subsetName, subsetName}); // color set later + + QStringList colLabels; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + colLabels.append( xName.toHtmlEscaped() ); + + // Add column + barSet->append(getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor); + } + // Add set (i.e. color) + series->append(barSet.take()); + + // Set column labels (only if no collision, empty otherwise) + if (firstCol) // init + prevColLabels = colLabels; + else if ( commonPartEqual(prevColLabels, colLabels) ) { + if (prevColLabels.size() < colLabels.size()) // replace by longest + prevColLabels = colLabels; + } + else { // collision + prevColLabels = QStringList(""); + } + firstCol = false; + } + // Add the series + chart->addSeries(scopedSeries.take()); + + // Axes + if (series->count() > 0) + { + // Chart type + Qt::Alignment catAlign = mIsVert ? Qt::AlignBottom : Qt::AlignLeft; + Qt::Alignment valAlign = mIsVert ? Qt::AlignLeft : Qt::AlignBottom; + + // X-axis + QBarCategoryAxis* catAxis = new QBarCategoryAxis(); + catAxis->append(prevColLabels); + chart->addAxis(catAxis, catAlign); + series->attachAxis(catAxis); + if (plotParams.xType == PlotArgumentType) + catAxis->setTitleText("Argument " + QString::number(plotParams.xIdx+1)); + else if (plotParams.xType == PlotTemplateType) + catAxis->setTitleText("Template " + QString::number(plotParams.xIdx+1)); + + // Y-axis + QValueAxis* valAxis = new QValueAxis(); + chart->addAxis(valAxis, valAlign); + series->attachAxis(valAxis); + valAxis->applyNiceNumbers(); + valAxis->setTitleText( getYPlotName(plotParams.yType, bchResults.meta.time_unit) ); + } + else + chart->setTitle("No compatible series to display"); + + if (init) + { + // View + mChartView = new QChartView(scopedChart.take(), this); + mChartView->setRenderHint(QPainter::Antialiasing); + } +} + +void PlotterBarChart::setupOptions(bool init) +{ + auto chart = mChartView->chart(); + + // General + if (init) + { + chart->setTheme(QChart::ChartThemeLight); + chart->legend()->setAlignment(Qt::AlignTop); + chart->legend()->setShowToolTips(true); + } + ui->spinBoxLegendFontSize->setValue( chart->legend()->font().pointSize() ); + + mIgnoreEvents = true; + int prevAxisIdx = ui->comboBoxAxis->currentIndex(); + + if (!init) // Re-init + { + mAxesParams[1].visible = true; + mAxesParams[1].title = true; + ui->comboBoxAxis->setCurrentIndex(0); + ui->comboBoxMin->clear(); + ui->comboBoxMax->clear(); + ui->checkBoxAxisVisible->setChecked(true); + ui->checkBoxTitle->setChecked(true); + ui->checkBoxLog->setChecked(false); + ui->comboBoxValuePosition->setCurrentIndex(0); + ui->comboBoxValueAngle->setCurrentIndex(0); + } + + // Time unit + if (mCurrentTimeFactor > 1.) ui->comboBoxTimeUnit->setCurrentIndex(0); // ns + else if (mCurrentTimeFactor < 1.) ui->comboBoxTimeUnit->setCurrentIndex(2); // ms + else ui->comboBoxTimeUnit->setCurrentIndex(1); // us + + // Axes + Qt::Orientation xOrient = mIsVert ? Qt::Horizontal : Qt::Vertical; + const auto& xAxes = chart->axes(xOrient); + if ( !xAxes.isEmpty() ) + { + QBarCategoryAxis* xAxis = (QBarCategoryAxis*)(xAxes.first()); + auto& axisParam = mAxesParams[0]; + + axisParam.titleText = xAxis->titleText(); + axisParam.titleSize = xAxis->titleFont().pointSize(); + axisParam.labelSize = xAxis->labelsFont().pointSize(); + + ui->labelFormat->setVisible(false); + ui->lineEditFormat->setVisible(false); + ui->doubleSpinBoxMin->setVisible(false); + ui->doubleSpinBoxMax->setVisible(false); + + ui->lineEditTitle->setText( axisParam.titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( axisParam.titleSize ); + ui->spinBoxLabelSize->setValue( axisParam.labelSize ); + const auto categories = xAxis->categories(); + for (const auto& cat : categories) { + ui->comboBoxMin->addItem(cat); + ui->comboBoxMax->addItem(cat); + } + ui->comboBoxMax->setCurrentIndex( ui->comboBoxMax->count()-1 ); + + } + Qt::Orientation yOrient = mIsVert ? Qt::Vertical : Qt::Horizontal; + const auto& yAxes = chart->axes(yOrient); + if ( !yAxes.isEmpty() ) + { + QValueAxis* yAxis = (QValueAxis*)(yAxes.first()); + auto& axisParam = mAxesParams[1]; + + axisParam.titleText = yAxis->titleText(); + axisParam.titleSize = yAxis->titleFont().pointSize(); + axisParam.labelSize = yAxis->labelsFont().pointSize(); + + ui->lineEditFormat->setText( "%g" ); + ui->lineEditFormat->setCursorPosition(0); + yAxis->setLabelFormat( ui->lineEditFormat->text() ); + ui->doubleSpinBoxMin->setValue( yAxis->min() ); + ui->doubleSpinBoxMax->setValue( yAxis->max() ); + ui->spinBoxTicks->setValue( yAxis->tickCount() ); + ui->spinBoxMTicks->setValue( yAxis->minorTickCount() ); + } + mIgnoreEvents = false; + + + // Load options from file + loadConfig(init); + + + // Apply actions + if (ui->checkBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); + + // Update series color config + if (!chart->series().empty()) + { + const auto barSeries = (QAbstractBarSeries*)chart->series().at(0); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& barSet = barSeries->barSets().at(idx); + + config.oldColor = barSet->color(); + if (!config.newColor.isValid()) + config.newColor = barSet->color(); // init + else + barSet->setColor(config.newColor); // apply + + if (config.newName != config.oldName) + barSet->setLabel( config.newName.toHtmlEscaped() ); + } + } + + // Restore selected axis + if (!init) + ui->comboBoxAxis->setCurrentIndex(prevAxisIdx); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +void PlotterBarChart::loadConfig(bool init) +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // Theme + if (json.contains("theme") && json["theme"].isString()) + ui->comboBoxTheme->setCurrentText( json["theme"].toString() ); + + // Legend + if (json.contains("legend.visible") && json["legend.visible"].isBool()) + ui->checkBoxLegendVisible->setChecked( json["legend.visible"].toBool() ); + if (json.contains("legend.align") && json["legend.align"].isString()) + ui->comboBoxLegendAlign->setCurrentText( json["legend.align"].toString() ); + if (json.contains("legend.fontSize") && json["legend.fontSize"].isDouble()) + ui->spinBoxLegendFontSize->setValue( json["legend.fontSize"].toInt(8) ); + + // Series + if (json.contains("series") && json["series"].isArray()) + { + auto series = json["series"].toArray(); + for (int idx = 0; idx < series.size(); ++idx) { + QJsonObject config = series[idx].toObject(); + if ( config.contains("oldName") && config["oldName"].isString() + && config.contains("newName") && config["newName"].isString() + && config.contains("newColor") && config["newColor"].isString() + && QColor::isValidColor(config["newColor"].toString()) ) + { + SeriesConfig savedConfig(config["oldName"].toString(), ""); + int iCfg = mSeriesMapping.indexOf(savedConfig); + if (iCfg >= 0) { + mSeriesMapping[iCfg].newName = config["newName"].toString(); + mSeriesMapping[iCfg].newColor.setNamedColor( config["newColor"].toString() ); + } + } + } + } + + // Time + if (!init) { + if (json.contains("timeUnit") && json["timeUnit"].isString()) + ui->comboBoxTimeUnit->setCurrentText( json["timeUnit"].toString() ); + } + + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 2; ++idx) + { + auto& axis = mAxesParams[idx]; + + if (json.contains(prefix + ".visible") && json[prefix + ".visible"].isBool()) { + axis.visible = json[prefix + ".visible"].toBool(); + ui->checkBoxAxisVisible->setChecked( axis.visible ); + } + if (json.contains(prefix + ".title") && json[prefix + ".title"].isBool()) { + axis.title = json[prefix + ".title"].toBool(); + ui->checkBoxTitle->setChecked( axis.title ); + } + if (json.contains(prefix + ".titleSize") && json[prefix + ".titleSize"].isDouble()) { + axis.titleSize = json[prefix + ".titleSize"].toInt(8); + ui->spinBoxTitleSize->setValue( axis.titleSize ); + } + if (json.contains(prefix + ".labelSize") && json[prefix + ".labelSize"].isDouble()) { + axis.labelSize = json[prefix + ".labelSize"].toInt(8); + ui->spinBoxLabelSize->setValue( axis.labelSize ); + } + if (!init) + { + if (json.contains(prefix + ".titleText") && json[prefix + ".titleText"].isString()) { + axis.titleText = json[prefix + ".titleText"].toString(); + ui->lineEditTitle->setText( axis.titleText ); + ui->lineEditTitle->setCursorPosition(0); + } + } + // x-axis + if (idx == 0) + { + if (json.contains(prefix + ".value.position") && json[prefix + ".value.position"].isString()) + ui->comboBoxValuePosition->setCurrentText( json[prefix + ".value.position"].toString() ); + if (json.contains(prefix + ".value.angle") && json[prefix + ".value.angle"].isString()) + ui->comboBoxValueAngle->setCurrentText( json[prefix + ".value.angle"].toString() ); + if (force_config) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isString()) + ui->comboBoxMin->setCurrentText( json[prefix + ".min"].toString() ); + if (json.contains(prefix + ".max") && json[prefix + ".max"].isString()) + ui->comboBoxMax->setCurrentText( json[prefix + ".max"].toString() ); + } + } + else // y-axis + { + if (json.contains(prefix + ".log") && json[prefix + ".log"].isBool()) + ui->checkBoxLog->setChecked( json[prefix + ".log"].toBool() ); + if (json.contains(prefix + ".logBase") && json[prefix + ".logBase"].isDouble()) + ui->spinBoxLogBase->setValue( json[prefix + ".logBase"].toInt(10) ); + if (json.contains(prefix + ".labelFormat") && json[prefix + ".labelFormat"].isString()) { + ui->lineEditFormat->setText( json[prefix + ".labelFormat"].toString() ); + ui->lineEditFormat->setCursorPosition(0); + } + if (json.contains(prefix + ".ticks") && json[prefix + ".ticks"].isDouble()) + ui->spinBoxTicks->setValue( json[prefix + ".ticks"].toInt(5) ); + if (json.contains(prefix + ".mticks") && json[prefix + ".mticks"].isDouble()) + ui->spinBoxMTicks->setValue( json[prefix + ".mticks"].toInt(0) ); + if (!init) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isDouble()) + ui->doubleSpinBoxMin->setValue( json[prefix + ".min"].toDouble() ); + if (json.contains(prefix + ".max") && json[prefix + ".max"].isDouble()) + ui->doubleSpinBoxMax->setValue( json[prefix + ".max"].toDouble() ); + } + } + + prefix = "axis.y"; + ui->comboBoxAxis->setCurrentIndex(1); + } + ui->comboBoxAxis->setCurrentIndex(0); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } +} + +void PlotterBarChart::saveConfig() +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // Theme + json["theme"] = ui->comboBoxTheme->currentText(); + // Legend + json["legend.visible"] = ui->checkBoxLegendVisible->isChecked(); + json["legend.align"] = ui->comboBoxLegendAlign->currentText(); + json["legend.fontSize"] = ui->spinBoxLegendFontSize->value(); + // Series + QJsonArray series; + for (const auto& seriesConfig : qAsConst(mSeriesMapping)) { + QJsonObject config; + config["oldName"] = seriesConfig.oldName; + config["newName"] = seriesConfig.newName; + config["newColor"] = seriesConfig.newColor.name(); + series.append(config); + } + if (!series.empty()) + json["series"] = series; + // Time + json["timeUnit"] = ui->comboBoxTimeUnit->currentText(); + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 2; ++idx) + { + const auto& axis = mAxesParams[idx]; + + json[prefix + ".visible"] = axis.visible; + json[prefix + ".title"] = axis.title; + json[prefix + ".titleText"] = axis.titleText; + json[prefix + ".titleSize"] = axis.titleSize; + json[prefix + ".labelSize"] = axis.labelSize; + // x-axis + if (idx == 0) + { + json[prefix + ".value.position"] = ui->comboBoxValuePosition->currentText(); + json[prefix + ".value.angle"] = ui->comboBoxValueAngle->currentText(); + json[prefix + ".min"] = ui->comboBoxMin->currentText(); + json[prefix + ".max"] = ui->comboBoxMax->currentText(); + } + else // y-axis + { + json[prefix + ".log"] = ui->checkBoxLog->isChecked(); + json[prefix + ".logBase"] = ui->spinBoxLogBase->value(); + json[prefix + ".labelFormat"] = ui->lineEditFormat->text(); + json[prefix + ".min"] = ui->doubleSpinBoxMin->value(); + json[prefix + ".max"] = ui->doubleSpinBoxMax->value(); + json[prefix + ".ticks"] = ui->spinBoxTicks->value(); + json[prefix + ".mticks"] = ui->spinBoxMTicks->value(); + } + prefix = "axis.y"; + } + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +// +// Theme +void PlotterBarChart::onComboThemeChanged(int index) +{ + QChart::ChartTheme theme = static_cast( + ui->comboBoxTheme->itemData(index).toInt()); + mChartView->chart()->setTheme(theme); + + // Update series color + const auto& series = mChartView->chart()->series(); + if (!series.empty()) + { + const auto barSeries = (QAbstractBarSeries*)series.at(0); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& barSet = barSeries->barSets().at(idx); + auto prevColor = config.oldColor; + + config.oldColor = barSet->color(); + if (config.newColor != prevColor) + barSet->setColor(config.newColor); // re-apply config + else + config.newColor = config.oldColor; // sync with theme + } + } + + // Re-apply font sizes + onSpinLegendFontSizeChanged( ui->spinBoxLegendFontSize->value() ); + onSpinLabelSizeChanged2(mAxesParams[0].labelSize, 0); + onSpinLabelSizeChanged2(mAxesParams[1].labelSize, 1); + onSpinTitleSizeChanged2(mAxesParams[0].titleSize, 0); + onSpinTitleSizeChanged2(mAxesParams[1].titleSize, 1); +} + +// +// Legend +void PlotterBarChart::onCheckLegendVisible(int state) +{ + mChartView->chart()->legend()->setVisible(state == Qt::Checked); +} + +void PlotterBarChart::onComboLegendAlignChanged(int index) +{ + Qt::Alignment align = static_cast( + ui->comboBoxLegendAlign->itemData(index).toInt()); + mChartView->chart()->legend()->setAlignment(align); +} + +void PlotterBarChart::onSpinLegendFontSizeChanged(int i) +{ + QFont font = mChartView->chart()->legend()->font(); + font.setPointSize(i); + mChartView->chart()->legend()->setFont(font); +} + +void PlotterBarChart::onSeriesEditClicked() +{ + SeriesDialog seriesDialog(mSeriesMapping, this); + auto res = seriesDialog.exec(); + const auto& series = mChartView->chart()->series(); + if (res == QDialog::Accepted && !series.empty()) + { + const auto barSeries = (QAbstractBarSeries*)series.at(0); + const auto& newMapping = seriesDialog.getMapping(); + for (int idx = 0; idx < newMapping.size(); ++idx) + { + const auto& newPair = newMapping[idx]; + const auto& oldPair = mSeriesMapping[idx]; + const auto& barSet = barSeries->barSets().at(idx); + + if (newPair.newName != oldPair.newName) { + barSet->setLabel( newPair.newName.toHtmlEscaped() ); + } + if (newPair.newColor != oldPair.newColor) { + barSet->setColor(newPair.newColor); + } + } + mSeriesMapping = newMapping; + } +} + +void PlotterBarChart::onComboTimeUnitChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + + // Update data + double unitFactor = ui->comboBoxTimeUnit->currentData().toDouble(); + double updateFactor = unitFactor / mCurrentTimeFactor; // can cause precision loss + auto chartSeries = mChartView->chart()->series(); + if (chartSeries.empty()) + return; + + const QAbstractBarSeries* barSeries = (QAbstractBarSeries*)chartSeries[0]; + auto barSets = barSeries->barSets(); + for (int idx = 0; idx < barSets.size(); ++idx) + { + auto* barSet = barSets.at(idx); + for (int i = 0; i < barSet->count(); ++i) { + qreal val = barSet->at(i); + barSet->replace(i, val * updateFactor); + } + } + + // Update axis title + QString oldUnitName = "(us)"; + if (mCurrentTimeFactor > 1.) oldUnitName = "(ns)"; + else if (mCurrentTimeFactor < 1.) oldUnitName = "(ms)"; + + Qt::Orientation yOrient = mIsVert ? Qt::Vertical : Qt::Horizontal; + const auto& axes = mChartView->chart()->axes(yOrient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + QString axisTitle = axis->titleText(); + if (axisTitle.endsWith(oldUnitName)) { + QString unitName = ui->comboBoxTimeUnit->currentText(); + onEditTitleChanged2(axisTitle.replace(axisTitle.size() - 3, 2, unitName), 1); + } + } + // Update range + ui->doubleSpinBoxMin->setValue(ui->doubleSpinBoxMin->value() * updateFactor); + ui->doubleSpinBoxMax->setValue(ui->doubleSpinBoxMax->value() * updateFactor); + if (ui->comboBoxAxis->currentIndex() != 1 && !axes.isEmpty()) + { + QValueAxis* yAxis = (QValueAxis*)(axes.first()); + onSpinMinChanged2(yAxis->min() * updateFactor, 1); + onSpinMaxChanged2(yAxis->max() * updateFactor, 1); + } + + mCurrentTimeFactor = unitFactor; +} + +// +// Axes +void PlotterBarChart::onComboAxisChanged(int idx) +{ + // Update UI + bool wasIgnoring = mIgnoreEvents; + mIgnoreEvents = true; + + ui->checkBoxAxisVisible->setChecked( mAxesParams[idx].visible ); + ui->checkBoxTitle->setChecked( mAxesParams[idx].title ); + ui->checkBoxLog->setEnabled( idx == 1 ); + ui->spinBoxLogBase->setEnabled( ui->checkBoxLog->isEnabled() && ui->checkBoxLog->isChecked() ); + ui->lineEditTitle->setText( mAxesParams[idx].titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( mAxesParams[idx].titleSize ); + ui->labelFormat->setVisible( idx == 1 ); + ui->lineEditFormat->setVisible( idx == 1 ); + ui->labelValue->setVisible( idx == 0 ); + ui->comboBoxValuePosition->setVisible( idx == 0 ); + ui->comboBoxValueAngle->setVisible( idx == 0 ); + ui->spinBoxLabelSize->setValue( mAxesParams[idx].labelSize ); + ui->comboBoxMin->setVisible( idx == 0 ); + ui->comboBoxMax->setVisible( idx == 0 ); + ui->doubleSpinBoxMin->setVisible( idx == 1 ); + ui->doubleSpinBoxMax->setVisible( idx == 1 ); + ui->spinBoxTicks->setEnabled( idx == 1 && !ui->checkBoxLog->isChecked() ); + ui->spinBoxMTicks->setEnabled( idx == 1 ); + + mIgnoreEvents = wasIgnoring; +} + +void PlotterBarChart::onCheckAxisVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setVisible(state == Qt::Checked); + mAxesParams[iAxis].visible = state == Qt::Checked; + } +} + +void PlotterBarChart::onCheckTitleVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleVisible(state == Qt::Checked); + mAxesParams[iAxis].title = state == Qt::Checked; + } +} + +void PlotterBarChart::onCheckLog(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + Qt::Alignment align = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::AlignBottom : Qt::AlignLeft; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if (state == Qt::Checked) + { + QValueAxis* axis = (QValueAxis*)(axes.first()); + + QLogValueAxis* logAxis = new QLogValueAxis(); + logAxis->setVisible( axis->isVisible() ); + logAxis->setTitleVisible( axis->isTitleVisible() ); + logAxis->setTitleText( axis->titleText() ); + logAxis->setTitleFont( axis->titleFont() ); + logAxis->setLabelFormat( axis->labelFormat() ); + logAxis->setLabelsFont( axis->labelsFont() ); + + mChartView->chart()->removeAxis(axis); + mChartView->chart()->addAxis(logAxis, align); + const auto series = mChartView->chart()->series(); + for (const auto& series : series) + series->attachAxis(logAxis); + + logAxis->setBase( ui->spinBoxLogBase->value() ); + logAxis->setMin( ui->doubleSpinBoxMin->value() ); + logAxis->setMax( ui->doubleSpinBoxMax->value() ); + logAxis->setMinorTickCount( ui->spinBoxMTicks->value() ); + } + else + { + QLogValueAxis* logAxis = (QLogValueAxis*)(axes.first()); + + QValueAxis* axis = new QValueAxis(); + axis->setVisible( logAxis->isVisible() ); + axis->setTitleVisible( logAxis->isTitleVisible() ); + axis->setTitleText( logAxis->titleText() ); + axis->setTitleFont( logAxis->titleFont() ); + axis->setLabelFormat( logAxis->labelFormat() ); + axis->setLabelsFont( logAxis->labelsFont() ); + + mChartView->chart()->removeAxis(logAxis); + mChartView->chart()->addAxis(axis, align); + const auto chartSeries = mChartView->chart()->series(); + for (const auto& series : chartSeries) + series->attachAxis(axis); + + axis->setMin( ui->doubleSpinBoxMin->value() ); + axis->setMax( ui->doubleSpinBoxMax->value() ); + axis->setTickCount( ui->spinBoxTicks->value() ); + axis->setMinorTickCount( ui->spinBoxMTicks->value() ); + } + ui->spinBoxTicks->setEnabled( state != Qt::Checked); + ui->spinBoxLogBase->setEnabled(state == Qt::Checked); + } +} + +void PlotterBarChart::onSpinLogBaseChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() && ui->checkBoxLog->isChecked()) + { + QLogValueAxis* logAxis = (QLogValueAxis*)(axes.first()); + logAxis->setBase(i); + } +} + +void PlotterBarChart::onEditTitleChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onEditTitleChanged2(text, iAxis); +} + +void PlotterBarChart::onEditTitleChanged2(const QString& text, int iAxis) +{ + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleText(text); + mAxesParams[iAxis].titleText = text; + } +} + +void PlotterBarChart::onSpinTitleSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinTitleSizeChanged2(i, iAxis); +} + +void PlotterBarChart::onSpinTitleSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->titleFont(); + font.setPointSize(i); + axis->setTitleFont(font); + mAxesParams[iAxis].titleSize = i; + } +} + +void PlotterBarChart::onEditFormatChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + } +} + +void PlotterBarChart::onComboValuePositionChanged(int index) +{ + if (mIgnoreEvents) return; + const auto chartSeries = mChartView->chart()->series(); + for (auto series : chartSeries) + { + QAbstractBarSeries* barSeries = (QAbstractBarSeries*)(series); + if (index == 0) + barSeries->setLabelsVisible(false); + else { + barSeries->setLabelsVisible(true); + barSeries->setLabelsPosition( (QAbstractBarSeries::LabelsPosition) + (ui->comboBoxValuePosition->currentData().toInt()) ); + } + } +} + +void PlotterBarChart::onComboValueAngleChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + const auto chartSeries = mChartView->chart()->series(); + for (auto series : chartSeries) + { + QAbstractBarSeries* barSeries = (QAbstractBarSeries*)(series); + barSeries->setLabelsAngle( ui->comboBoxValueAngle->currentData().toDouble() ); + } +} + +void PlotterBarChart::onSpinLabelSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinLabelSizeChanged2(i, iAxis); +} + +void PlotterBarChart::onSpinLabelSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->labelsFont(); + font.setPointSize(i); + axis->setLabelsFont(font); + mAxesParams[iAxis].labelSize = i; + } +} + +void PlotterBarChart::onSpinMinChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMinChanged2(d, iAxis); +} + +void PlotterBarChart::onSpinMinChanged2(double d, int iAxis) +{ + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMin(d); + } +} + +void PlotterBarChart::onSpinMaxChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMaxChanged2(d, iAxis); +} + +void PlotterBarChart::onSpinMaxChanged2(double d, int iAxis) +{ + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMax(d); + } +} + +void PlotterBarChart::onComboMinChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QBarCategoryAxis* axis = (QBarCategoryAxis*)(axes.first()); + axis->setMin( ui->comboBoxMin->currentText() ); + } +} + +void PlotterBarChart::onComboMaxChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QBarCategoryAxis* axis = (QBarCategoryAxis*)(axes.first()); + axis->setMax( ui->comboBoxMax->currentText() ); + } +} + +void PlotterBarChart::onSpinTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setTickCount(i); + } + } +} + +void PlotterBarChart::onSpinMTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = (iAxis == 0 && mIsVert) || (iAxis == 1 && !mIsVert) + ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + + // Force update + const int base = (int)axis->base(); + axis->setBase(base + 1); + axis->setBase(base); + } + } +} + +// +// Actions +void PlotterBarChart::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : mAddFilenames) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void PlotterBarChart::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void PlotterBarChart::onReloadClicked() +{ + // Load new results + QString errorMsg; + BenchResults newBchResults = ResultParser::parseJsonFile( mOrigFilename, errorMsg ); + + if ( newBchResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing original file: " + mOrigFilename + " -> " + errorMsg); + return; + } + + for (const auto& addFile : qAsConst(mAddFilenames)) + { + errorMsg.clear(); + BenchResults newAddResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if ( newAddResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing additional file: " + addFile.filename + " -> " + errorMsg); + return; + } + + if (addFile.isAppend) + newBchResults.appendResults(newAddResults); + else + newBchResults.overwriteResults(newAddResults); + } + + // Check compatibility with previous + errorMsg.clear(); + if (mBenchIdxs.size() != newBchResults.benchmarks.size()) + { + errorMsg = "Number of series/points is different"; + if (mAllIndexes) + { + mBenchIdxs.clear(); + for (int i=0; i newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + mBenchIdxs, mPlotParams.xIdx, "X"); + int newBarSetIdx = 0; + const auto& oldChartSeries = mChartView->chart()->series(); + if ( newBchSubsets.isEmpty() ) { + errorMsg = "No compatible series to display"; // Ignore empty series + } + if (oldChartSeries.size() != 1) { + errorMsg = "No compatible series to display originally"; + } + + if (errorMsg.isEmpty()) + { + const QAbstractBarSeries* oldBarSeries = (QAbstractBarSeries*)oldChartSeries[0]; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Ignore empty set + if ( bchSubset.idxs.isEmpty() ) + continue; + if (newBarSetIdx >= oldBarSeries->count()) + break; + + const QString& subsetName = bchSubset.name; + if (subsetName != mSeriesMapping[newBarSetIdx].oldName) { + errorMsg = "Series has different name"; + break; + } + const auto barSet = oldBarSeries->barSets().at(newBarSetIdx); + if (bchSubset.idxs.size() != barSet->count()) + { + errorMsg = "Number of series bars is different"; + break; + } + ++newBarSetIdx; + } + if (newBarSetIdx != oldBarSeries->count()) { + errorMsg = "Number of series is different"; + } + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + newBarSetIdx = 0; + const QAbstractBarSeries* oldBarSeries = (QAbstractBarSeries*)oldChartSeries[0]; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Ignore empty set + if ( bchSubset.idxs.isEmpty() ) { + qWarning() << "No X-value to trace bar for:" << bchSubset.name; + continue; + } + + // Update points + const auto& barSet = oldBarSeries->barSets().at(newBarSetIdx); + barSet->remove(0, barSet->count()); + + for (int idx : bchSubset.idxs) { + // Add column + barSet->append(getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor); + } + ++newBarSetIdx; + } + } + // Reset update if all benchmarks + else if (mAllIndexes) + { + saveConfig(); + setupChart(newBchResults, mBenchIdxs, mPlotParams, false); + setupOptions(false); + } + else + { + QMessageBox::critical(this, "Chart reload", errorMsg); + return; + } + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() +")"); +} + +void PlotterBarChart::onSnapshotClicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save snapshot"), "", tr("Images (*.png)")); + + if ( !fileName.isEmpty() ) + { + QPixmap pixmap = mChartView->grab(); + + bool ok = pixmap.save(fileName, "PNG"); + if (!ok) + QMessageBox::warning(this, "Chart snapshot", "Error saving snapshot file."); + } +} diff --git a/specifelse/benchtest/jomt/src/plotter_boxchart.cpp b/specifelse/benchtest/jomt/src/plotter_boxchart.cpp new file mode 100644 index 0000000..f8a659d --- /dev/null +++ b/specifelse/benchtest/jomt/src/plotter_boxchart.cpp @@ -0,0 +1,1158 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "plotter_boxchart.h" +#include "ui_plotter_boxchart.h" + +#include "benchmark_results.h" +#include "result_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace QtCharts; + +static const char* config_file = "config_boxes.json"; +static const bool force_config = false; + + +PlotterBoxChart::PlotterBoxChart(BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &origFilename, + const QVector& addFilenames, QWidget *parent) + : QWidget(parent) + , ui(new Ui::PlotterBoxChart) + , mBenchIdxs(bchIdxs) + , mPlotParams(plotParams) + , mOrigFilename(origFilename) + , mAddFilenames(addFilenames) + , mAllIndexes(bchIdxs.size() == bchResults.benchmarks.size()) + , mWatcher(parent) +{ + // UI + ui->setupUi(this); + this->setAttribute(Qt::WA_DeleteOnClose); + + QFileInfo fileInfo(origFilename); + this->setWindowTitle("Boxes - " + fileInfo.fileName()); + + connectUI(); + + // Init + setupChart(bchResults, bchIdxs, plotParams); + setupOptions(); + + // Show + ui->horizontalLayout->insertWidget(0, mChartView); +} + +PlotterBoxChart::~PlotterBoxChart() +{ + // Save options to file + saveConfig(); + + delete ui; +} + +void PlotterBoxChart::connectUI() +{ + // Theme + ui->comboBoxTheme->addItem("Light", QChart::ChartThemeLight); + ui->comboBoxTheme->addItem("Blue Cerulean", QChart::ChartThemeBlueCerulean); + ui->comboBoxTheme->addItem("Dark", QChart::ChartThemeDark); + ui->comboBoxTheme->addItem("Brown Sand", QChart::ChartThemeBrownSand); + ui->comboBoxTheme->addItem("Blue Ncs", QChart::ChartThemeBlueNcs); + ui->comboBoxTheme->addItem("High Contrast", QChart::ChartThemeHighContrast); + ui->comboBoxTheme->addItem("Blue Icy", QChart::ChartThemeBlueIcy); + ui->comboBoxTheme->addItem("Qt", QChart::ChartThemeQt); + connect(ui->comboBoxTheme, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboThemeChanged); + + // Legend + connect(ui->checkBoxLegendVisible, &QCheckBox::stateChanged, this, &PlotterBoxChart::onCheckLegendVisible); + + ui->comboBoxLegendAlign->addItem("Top", Qt::AlignTop); + ui->comboBoxLegendAlign->addItem("Bottom", Qt::AlignBottom); + ui->comboBoxLegendAlign->addItem("Left", Qt::AlignLeft); + ui->comboBoxLegendAlign->addItem("Right", Qt::AlignRight); + connect(ui->comboBoxLegendAlign, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboLegendAlignChanged); + + connect(ui->spinBoxLegendFontSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinLegendFontSizeChanged); + connect(ui->pushButtonSeries, &QPushButton::clicked, this, &PlotterBoxChart::onSeriesEditClicked); + + if (!isYTimeBased(mPlotParams.yType)) + ui->comboBoxTimeUnit->setEnabled(false); + else + { + ui->comboBoxTimeUnit->addItem("ns", 1000.); + ui->comboBoxTimeUnit->addItem("us", 1.); + ui->comboBoxTimeUnit->addItem("ms", 0.001); + connect(ui->comboBoxTimeUnit, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboTimeUnitChanged); + } + + // Axes + ui->comboBoxAxis->addItem("X-Axis"); + ui->comboBoxAxis->addItem("Y-Axis"); + connect(ui->comboBoxAxis, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboAxisChanged); + + connect(ui->checkBoxAxisVisible, &QCheckBox::stateChanged, this, &PlotterBoxChart::onCheckAxisVisible); + connect(ui->checkBoxTitle, &QCheckBox::stateChanged, this, &PlotterBoxChart::onCheckTitleVisible); + connect(ui->checkBoxLog, &QCheckBox::stateChanged, this, &PlotterBoxChart::onCheckLog); + connect(ui->spinBoxLogBase, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinLogBaseChanged); + connect(ui->lineEditTitle, &QLineEdit::textChanged, this, &PlotterBoxChart::onEditTitleChanged); + connect(ui->spinBoxTitleSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinTitleSizeChanged); + connect(ui->lineEditFormat, &QLineEdit::textChanged, this, &PlotterBoxChart::onEditFormatChanged); + connect(ui->spinBoxLabelSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinLabelSizeChanged); + connect(ui->doubleSpinBoxMin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterBoxChart::onSpinMinChanged); + connect(ui->doubleSpinBoxMax, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterBoxChart::onSpinMaxChanged); + connect(ui->comboBoxMin, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboMinChanged); + connect(ui->comboBoxMax, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterBoxChart::onComboMaxChanged); + connect(ui->spinBoxTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinTicksChanged); + connect(ui->spinBoxMTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterBoxChart::onSpinMTicksChanged); + + // Actions + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &PlotterBoxChart::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &PlotterBoxChart::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &PlotterBoxChart::onReloadClicked); + connect(ui->pushButtonSnapshot, &QPushButton::clicked, this, &PlotterBoxChart::onSnapshotClicked); +} + +void PlotterBoxChart::setupChart(BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init) +{ +// QScopedPointer scopedChart(new QChart()); +// QChart* chart = scopedChart.get(); + QScopedPointer scopedChart; + QChart* chart = nullptr; + if (init) { + scopedChart.reset( new QChart() ); + chart = scopedChart.get(); + } + else { // Re-init + chart = mChartView->chart(); + chart->setTitle(""); + chart->removeAllSeries(); + const auto xAxes = chart->axes(Qt::Horizontal); + if ( !xAxes.empty() ) + chart->removeAxis( xAxes.constFirst() ); + const auto yAxes = chart->axes(Qt::Vertical); + if ( !yAxes.empty() ) + chart->removeAxis( yAxes.constFirst() ); + mSeriesMapping.clear(); + } + Q_ASSERT(chart); + + // Time unit + mCurrentTimeFactor = 1.; + if ( isYTimeBased(mPlotParams.yType) ) { + if ( bchResults.meta.time_unit == "ns") mCurrentTimeFactor = 1000.; + else if (bchResults.meta.time_unit == "ms") mCurrentTimeFactor = 0.001; + } + + + // 2D Boxes and whiskers + // X: argumentA or templateB + // Y: time/iter/bytes/items (not name dependent) + // Box: one per benchmark % X-param + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchIdxs, plotParams.xIdx, "X"); + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + // Series = benchmark % X-param + QScopedPointer series(new QBoxPlotSeries()); + + const QString & subsetName = bchSubset.name; +// qDebug() << "subsetName:" << subsetName; +// qDebug() << "subsetIdxs:" << bchSubset.idxs; + + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + BenchYStats yStats = getYPlotStats(bchResults.benchmarks[idx], plotParams.yType); + + // BoxSet + QScopedPointer box(new QBoxSet( xName.toHtmlEscaped() )); + box->setValue(QBoxSet::LowerExtreme, yStats.min * mCurrentTimeFactor); + box->setValue(QBoxSet::UpperExtreme, yStats.max * mCurrentTimeFactor); + box->setValue(QBoxSet::Median, yStats.median * mCurrentTimeFactor); + box->setValue(QBoxSet::LowerQuartile, yStats.lowQuart * mCurrentTimeFactor); + box->setValue(QBoxSet::UpperQuartile, yStats.uppQuart * mCurrentTimeFactor); + + series->append(box.take()); + } + // Add series + series->setName( subsetName.toHtmlEscaped() ); + mSeriesMapping.push_back({subsetName, subsetName}); // color set later + chart->addSeries(series.take()); + } + + // Axes + if ( !chart->series().isEmpty() ) + { + chart->legend()->setVisible(true); + chart->createDefaultAxes(); + + // X-axis + QBarCategoryAxis* xAxis = (QBarCategoryAxis*)(chart->axes(Qt::Horizontal).constFirst()); + if (plotParams.xType == PlotArgumentType) + xAxis->setTitleText("Argument " + QString::number(plotParams.xIdx+1)); + else if (plotParams.xType == PlotTemplateType) + xAxis->setTitleText("Template " + QString::number(plotParams.xIdx+1)); + if (plotParams.xType != PlotEmptyType) + xAxis->setTitleVisible(true); + + // Y-axis + QValueAxis* yAxis = (QValueAxis*)(chart->axes(Qt::Vertical).constFirst()); + yAxis->setTitleText( getYPlotName(plotParams.yType, bchResults.meta.time_unit) ); + yAxis->applyNiceNumbers(); + } + else + chart->setTitle("No compatible series to display"); + + if (init) + { + // View + mChartView = new QChartView(scopedChart.take(), this); + mChartView->setRenderHint(QPainter::Antialiasing); + } +} + +void PlotterBoxChart::setupOptions(bool init) +{ + auto chart = mChartView->chart(); + + // General + if (init) + { + chart->setTheme(QChart::ChartThemeLight); + chart->legend()->setAlignment(Qt::AlignTop); + chart->legend()->setShowToolTips(true); + } + ui->spinBoxLegendFontSize->setValue( chart->legend()->font().pointSize() ); + + mIgnoreEvents = true; + int prevAxisIdx = ui->comboBoxAxis->currentIndex(); + + if (!init) // Re-init + { + mAxesParams[1].visible = true; + mAxesParams[1].title = true; + ui->comboBoxAxis->setCurrentIndex(0); + ui->comboBoxMin->clear(); + ui->comboBoxMax->clear(); + ui->checkBoxAxisVisible->setChecked(true); + ui->checkBoxTitle->setChecked(true); + ui->checkBoxLog->setChecked(false); + } + + // Time unit + if (mCurrentTimeFactor > 1.) ui->comboBoxTimeUnit->setCurrentIndex(0); // ns + else if (mCurrentTimeFactor < 1.) ui->comboBoxTimeUnit->setCurrentIndex(2); // ms + else ui->comboBoxTimeUnit->setCurrentIndex(1); // us + + // Axes + const auto& xAxes = chart->axes(Qt::Horizontal); + if ( !xAxes.isEmpty() ) + { + QBarCategoryAxis* xAxis = (QBarCategoryAxis*)(xAxes.first()); + auto& axisParam = mAxesParams[0]; + + axisParam.titleText = xAxis->titleText(); + axisParam.titleSize = xAxis->titleFont().pointSize(); + axisParam.labelSize = xAxis->labelsFont().pointSize(); + + ui->doubleSpinBoxMin->setVisible(false); + ui->doubleSpinBoxMax->setVisible(false); + + ui->lineEditTitle->setText( axisParam.titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( axisParam.titleSize ); + ui->spinBoxLabelSize->setValue( axisParam.labelSize ); + const auto categories = xAxis->categories(); + for (const auto& cat : categories) { + ui->comboBoxMin->addItem(cat); + ui->comboBoxMax->addItem(cat); + } + ui->comboBoxMax->setCurrentIndex( ui->comboBoxMax->count()-1 ); + + } + const auto& yAxes = chart->axes(Qt::Vertical); + if ( !yAxes.isEmpty() ) + { + QValueAxis* yAxis = (QValueAxis*)(yAxes.first()); + auto& axisParam = mAxesParams[1]; + + axisParam.titleText = yAxis->titleText(); + axisParam.titleSize = yAxis->titleFont().pointSize(); + axisParam.labelSize = yAxis->labelsFont().pointSize(); + + ui->lineEditFormat->setText( "%g" ); + ui->lineEditFormat->setCursorPosition(0); + yAxis->setLabelFormat( ui->lineEditFormat->text() ); + ui->doubleSpinBoxMin->setValue( yAxis->min() ); + ui->doubleSpinBoxMax->setValue( yAxis->max() ); + ui->spinBoxTicks->setValue( yAxis->tickCount() ); + ui->spinBoxMTicks->setValue( yAxis->minorTickCount() ); + } + mIgnoreEvents = false; + + + // Load options from file + loadConfig(init); + + + // Apply actions + if (ui->checkBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); + + // Update series color config + const auto& chartSeries = chart->series(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = (QBoxPlotSeries*)chartSeries.at(idx); + + config.oldColor = series->brush().color(); + if (!config.newColor.isValid()) + config.newColor = series->brush().color(); // init + else { + auto brush = series->brush(); + brush.setColor(config.newColor); // apply + series->setBrush(brush); + } + + if (config.newName != config.oldName) + series->setName( config.newName.toHtmlEscaped() ); + } + + // Restore selected axis + if (!init) + ui->comboBoxAxis->setCurrentIndex(prevAxisIdx); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +void PlotterBoxChart::loadConfig(bool init) +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // Theme + if (json.contains("theme") && json["theme"].isString()) + ui->comboBoxTheme->setCurrentText( json["theme"].toString() ); + + // Legend + if (json.contains("legend.visible") && json["legend.visible"].isBool()) + ui->checkBoxLegendVisible->setChecked( json["legend.visible"].toBool() ); + if (json.contains("legend.align") && json["legend.align"].isString()) + ui->comboBoxLegendAlign->setCurrentText( json["legend.align"].toString() ); + if (json.contains("legend.fontSize") && json["legend.fontSize"].isDouble()) + ui->spinBoxLegendFontSize->setValue( json["legend.fontSize"].toInt(8) ); + + // Series + if (json.contains("series") && json["series"].isArray()) + { + auto series = json["series"].toArray(); + for (int idx = 0; idx < series.size(); ++idx) { + QJsonObject config = series[idx].toObject(); + if ( config.contains("oldName") && config["oldName"].isString() + && config.contains("newName") && config["newName"].isString() + && config.contains("newColor") && config["newColor"].isString() + && QColor::isValidColor(config["newColor"].toString()) ) + { + SeriesConfig savedConfig(config["oldName"].toString(), ""); + int iCfg = mSeriesMapping.indexOf(savedConfig); + if (iCfg >= 0) { + mSeriesMapping[iCfg].newName = config["newName"].toString(); + mSeriesMapping[iCfg].newColor.setNamedColor( config["newColor"].toString() ); + } + } + } + } + + // Time + if (!init) { + if (json.contains("timeUnit") && json["timeUnit"].isString()) + ui->comboBoxTimeUnit->setCurrentText( json["timeUnit"].toString() ); + } + + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 2; ++idx) + { + auto& axis = mAxesParams[idx]; + + if (json.contains(prefix + ".visible") && json[prefix + ".visible"].isBool()) { + axis.visible = json[prefix + ".visible"].toBool(); + ui->checkBoxAxisVisible->setChecked( axis.visible ); + } + if (json.contains(prefix + ".title") && json[prefix + ".title"].isBool()) { + axis.title = json[prefix + ".title"].toBool(); + ui->checkBoxTitle->setChecked( axis.title ); + } + if (json.contains(prefix + ".titleSize") && json[prefix + ".titleSize"].isDouble()) { + axis.titleSize = json[prefix + ".titleSize"].toInt(8); + ui->spinBoxTitleSize->setValue( axis.titleSize ); + } + if (json.contains(prefix + ".labelSize") && json[prefix + ".labelSize"].isDouble()) { + axis.labelSize = json[prefix + ".labelSize"].toInt(8); + ui->spinBoxLabelSize->setValue( axis.labelSize ); + } + if (!init) + { + if (json.contains(prefix + ".titleText") && json[prefix + ".titleText"].isString()) { + axis.titleText = json[prefix + ".titleText"].toString(); + ui->lineEditTitle->setText( axis.titleText ); + ui->lineEditTitle->setCursorPosition(0); + } + } + // x-axis + if (idx == 0) + { + if (force_config) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isString()) + ui->comboBoxMin->setCurrentText( json[prefix + ".min"].toString() ); + if (json.contains(prefix + ".max") && json[prefix + ".max"].isString()) + ui->comboBoxMax->setCurrentText( json[prefix + ".max"].toString() ); + } + } + else // y-axis + { + if (json.contains(prefix + ".log") && json[prefix + ".log"].isBool()) + ui->checkBoxLog->setChecked( json[prefix + ".log"].toBool() ); + if (json.contains(prefix + ".logBase") && json[prefix + ".logBase"].isDouble()) + ui->spinBoxLogBase->setValue( json[prefix + ".logBase"].toInt(10) ); + if (json.contains(prefix + ".labelFormat") && json[prefix + ".labelFormat"].isString()) { + ui->lineEditFormat->setText( json[prefix + ".labelFormat"].toString() ); + ui->lineEditFormat->setCursorPosition(0); + } + if (json.contains(prefix + ".ticks") && json[prefix + ".ticks"].isDouble()) + ui->spinBoxTicks->setValue( json[prefix + ".ticks"].toInt(5) ); + if (json.contains(prefix + ".mticks") && json[prefix + ".mticks"].isDouble()) + ui->spinBoxMTicks->setValue( json[prefix + ".mticks"].toInt(0) ); + if (!init) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isDouble()) + ui->doubleSpinBoxMin->setValue( json[prefix + ".min"].toDouble() ); + if (json.contains(prefix + ".max") && json[prefix + ".max"].isDouble()) + ui->doubleSpinBoxMax->setValue( json[prefix + ".max"].toDouble() ); + } + } + + prefix = "axis.y"; + ui->comboBoxAxis->setCurrentIndex(1); + } + ui->comboBoxAxis->setCurrentIndex(0); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } +} + +void PlotterBoxChart::saveConfig() +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // Theme + json["theme"] = ui->comboBoxTheme->currentText(); + // Legend + json["legend.visible"] = ui->checkBoxLegendVisible->isChecked(); + json["legend.align"] = ui->comboBoxLegendAlign->currentText(); + json["legend.fontSize"] = ui->spinBoxLegendFontSize->value(); + // Series + QJsonArray series; + for (const auto& seriesConfig : qAsConst(mSeriesMapping)) { + QJsonObject config; + config["oldName"] = seriesConfig.oldName; + config["newName"] = seriesConfig.newName; + config["newColor"] = seriesConfig.newColor.name(); + series.append(config); + } + if (!series.empty()) + json["series"] = series; + // Time + json["timeUnit"] = ui->comboBoxTimeUnit->currentText(); + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 2; ++idx) + { + const auto& axis = mAxesParams[idx]; + + json[prefix + ".visible"] = axis.visible; + json[prefix + ".title"] = axis.title; + json[prefix + ".titleText"] = axis.titleText; + json[prefix + ".titleSize"] = axis.titleSize; + json[prefix + ".labelSize"] = axis.labelSize; + // x-axis + if (idx == 0) + { + json[prefix + ".min"] = ui->comboBoxMin->currentText(); + json[prefix + ".max"] = ui->comboBoxMax->currentText(); + } + else // y-axis + { + json[prefix + ".log"] = ui->checkBoxLog->isChecked(); + json[prefix + ".logBase"] = ui->spinBoxLogBase->value(); + json[prefix + ".labelFormat"] = ui->lineEditFormat->text(); + json[prefix + ".min"] = ui->doubleSpinBoxMin->value(); + json[prefix + ".max"] = ui->doubleSpinBoxMax->value(); + json[prefix + ".ticks"] = ui->spinBoxTicks->value(); + json[prefix + ".mticks"] = ui->spinBoxMTicks->value(); + } + prefix = "axis.y"; + } + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +// +// Theme +void PlotterBoxChart::onComboThemeChanged(int index) +{ + QChart::ChartTheme theme = static_cast( + ui->comboBoxTheme->itemData(index).toInt()); + mChartView->chart()->setTheme(theme); + + // Update series color + const auto& chartSeries = mChartView->chart()->series(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + auto series = (QBoxPlotSeries*)chartSeries.at(idx); + auto prevColor = config.oldColor; + + auto brush = series->brush(); + config.oldColor = brush.color(); + if (config.newColor != prevColor) { + brush.setColor(config.newColor); + series->setBrush(brush); // re-apply config + } + else + config.newColor = config.oldColor; // sync with theme + } + + // Re-apply font sizes + onSpinLegendFontSizeChanged( ui->spinBoxLegendFontSize->value() ); + onSpinLabelSizeChanged2(mAxesParams[0].labelSize, 0); + onSpinLabelSizeChanged2(mAxesParams[1].labelSize, 1); + onSpinTitleSizeChanged2(mAxesParams[0].titleSize, 0); + onSpinTitleSizeChanged2(mAxesParams[1].titleSize, 1); +} + +// +// Legend +void PlotterBoxChart::onCheckLegendVisible(int state) +{ + mChartView->chart()->legend()->setVisible(state == Qt::Checked); +} + +void PlotterBoxChart::onComboLegendAlignChanged(int index) +{ + Qt::Alignment align = static_cast( + ui->comboBoxLegendAlign->itemData(index).toInt()); + mChartView->chart()->legend()->setAlignment(align); +} + +void PlotterBoxChart::onSpinLegendFontSizeChanged(int i) +{ + QFont font = mChartView->chart()->legend()->font(); + font.setPointSize(i); + mChartView->chart()->legend()->setFont(font); +} + +void PlotterBoxChart::onSeriesEditClicked() +{ + SeriesDialog seriesDialog(mSeriesMapping, this); + auto res = seriesDialog.exec(); + if (res == QDialog::Accepted) + { + const auto& newMapping = seriesDialog.getMapping(); + for (int idx = 0; idx < newMapping.size(); ++idx) + { + const auto& newPair = newMapping[idx]; + const auto& oldPair = mSeriesMapping[idx]; + auto series = (QBoxPlotSeries*)mChartView->chart()->series().at(idx); + if (newPair.newName != oldPair.newName) { + series->setName( newPair.newName.toHtmlEscaped() ); + } + if (newPair.newColor != oldPair.newColor) { + auto brush = series->brush(); + brush.setColor(newPair.newColor); + series->setBrush(brush); + } + } + mSeriesMapping = newMapping; + } +} + +void PlotterBoxChart::onComboTimeUnitChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + + // Update data + double unitFactor = ui->comboBoxTimeUnit->currentData().toDouble(); + double updateFactor = unitFactor / mCurrentTimeFactor; // can cause precision loss + auto chartSeries = mChartView->chart()->series(); + if (chartSeries.empty()) + return; + + for (auto& series : chartSeries) + { + QBoxPlotSeries* boxSeries = (QBoxPlotSeries*)series; + auto boxSets = boxSeries->boxSets(); + for (int idx = 0; idx < boxSets.size(); ++idx) + { + auto* box = boxSets.at(idx); + box->setValue(QBoxSet::LowerExtreme, box->at(QBoxSet::LowerExtreme) * updateFactor); + box->setValue(QBoxSet::UpperExtreme, box->at(QBoxSet::UpperExtreme) * updateFactor); + box->setValue(QBoxSet::Median, box->at(QBoxSet::Median) * updateFactor); + box->setValue(QBoxSet::LowerQuartile, box->at(QBoxSet::LowerQuartile) * updateFactor); + box->setValue(QBoxSet::UpperQuartile, box->at(QBoxSet::UpperQuartile) * updateFactor); + } + } + + // Update axis title + QString oldUnitName = "(us)"; + if (mCurrentTimeFactor > 1.) oldUnitName = "(ns)"; + else if (mCurrentTimeFactor < 1.) oldUnitName = "(ms)"; + + const auto& axes = mChartView->chart()->axes(Qt::Vertical); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + QString axisTitle = axis->titleText(); + if (axisTitle.endsWith(oldUnitName)) { + QString unitName = ui->comboBoxTimeUnit->currentText(); + onEditTitleChanged2(axisTitle.replace(axisTitle.size() - 3, 2, unitName), 1); + } + } + // Update range + ui->doubleSpinBoxMin->setValue(ui->doubleSpinBoxMin->value() * updateFactor); + ui->doubleSpinBoxMax->setValue(ui->doubleSpinBoxMax->value() * updateFactor); + if (ui->comboBoxAxis->currentIndex() != 1 && !axes.isEmpty()) + { + QValueAxis* yAxis = (QValueAxis*)(axes.first()); + onSpinMinChanged2(yAxis->min() * updateFactor, 1); + onSpinMaxChanged2(yAxis->max() * updateFactor, 1); + } + + mCurrentTimeFactor = unitFactor; +} + +// +// Axes +void PlotterBoxChart::onComboAxisChanged(int idx) +{ + // Update UI + bool wasIgnoring = mIgnoreEvents; + mIgnoreEvents = true; + + ui->checkBoxAxisVisible->setChecked( mAxesParams[idx].visible ); + ui->checkBoxTitle->setChecked( mAxesParams[idx].title ); + ui->checkBoxLog->setEnabled( idx == 1 ); + ui->spinBoxLogBase->setEnabled( ui->checkBoxLog->isEnabled() && ui->checkBoxLog->isChecked() ); + ui->lineEditTitle->setText( mAxesParams[idx].titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( mAxesParams[idx].titleSize ); + ui->lineEditFormat->setEnabled( idx == 1 ); + ui->spinBoxLabelSize->setValue( mAxesParams[idx].labelSize ); + ui->comboBoxMin->setVisible( idx == 0 ); + ui->comboBoxMax->setVisible( idx == 0 ); + ui->doubleSpinBoxMin->setVisible( idx == 1 ); + ui->doubleSpinBoxMax->setVisible( idx == 1 ); + ui->spinBoxTicks->setEnabled( idx == 1 && !ui->checkBoxLog->isChecked() ); + ui->spinBoxMTicks->setEnabled( idx == 1 ); + + mIgnoreEvents = wasIgnoring; +} + +void PlotterBoxChart::onCheckAxisVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setVisible(state == Qt::Checked); + mAxesParams[iAxis].visible = state == Qt::Checked; + } +} + +void PlotterBoxChart::onCheckTitleVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleVisible(state == Qt::Checked); + mAxesParams[iAxis].title = state == Qt::Checked; + } +} + +void PlotterBoxChart::onCheckLog(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + Qt::Alignment align = iAxis == 0 ? Qt::AlignBottom : Qt::AlignLeft; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if (state == Qt::Checked) + { + QValueAxis* axis = (QValueAxis*)(axes.first()); + + QLogValueAxis* logAxis = new QLogValueAxis(); + logAxis->setVisible( axis->isVisible() ); + logAxis->setTitleVisible( axis->isTitleVisible() ); + logAxis->setTitleText( axis->titleText() ); + logAxis->setTitleFont( axis->titleFont() ); + logAxis->setLabelFormat( axis->labelFormat() ); + logAxis->setLabelsFont( axis->labelsFont() ); + + mChartView->chart()->removeAxis(axis); + mChartView->chart()->addAxis(logAxis, align); + const auto chartSeries = mChartView->chart()->series(); + for (const auto& series : chartSeries) + series->attachAxis(logAxis); + + logAxis->setBase( ui->spinBoxLogBase->value() ); + logAxis->setMin( ui->doubleSpinBoxMin->value() ); + logAxis->setMax( ui->doubleSpinBoxMax->value() ); + logAxis->setMinorTickCount( ui->spinBoxMTicks->value() ); + } + else + { + QLogValueAxis* logAxis = (QLogValueAxis*)(axes.first()); + + QValueAxis* axis = new QValueAxis(); + axis->setVisible( logAxis->isVisible() ); + axis->setTitleVisible( logAxis->isTitleVisible() ); + axis->setTitleText( logAxis->titleText() ); + axis->setTitleFont( logAxis->titleFont() ); + axis->setLabelFormat( logAxis->labelFormat() ); + axis->setLabelsFont( logAxis->labelsFont() ); + + mChartView->chart()->removeAxis(logAxis); + mChartView->chart()->addAxis(axis, align); + const auto chartSeries = mChartView->chart()->series(); + for (const auto& series : chartSeries) + series->attachAxis(axis); + + axis->setMin( ui->doubleSpinBoxMin->value() ); + axis->setMax( ui->doubleSpinBoxMax->value() ); + axis->setTickCount( ui->spinBoxTicks->value() ); + axis->setMinorTickCount( ui->spinBoxMTicks->value() ); + } + ui->spinBoxTicks->setEnabled( state != Qt::Checked); + ui->spinBoxLogBase->setEnabled(state == Qt::Checked); + } +} + +void PlotterBoxChart::onSpinLogBaseChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() && ui->checkBoxLog->isChecked()) + { + QLogValueAxis* logAxis = (QLogValueAxis*)(axes.first()); + logAxis->setBase(i); + } +} + +void PlotterBoxChart::onEditTitleChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onEditTitleChanged2(text, iAxis); +} + +void PlotterBoxChart::onEditTitleChanged2(const QString& text, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleText(text); + mAxesParams[iAxis].titleText = text; + } +} + +void PlotterBoxChart::onSpinTitleSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinTitleSizeChanged2(i, iAxis); +} + +void PlotterBoxChart::onSpinTitleSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->titleFont(); + font.setPointSize(i); + axis->setTitleFont(font); + mAxesParams[iAxis].titleSize = i; + } +} + +void PlotterBoxChart::onEditFormatChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + } +} + +void PlotterBoxChart::onSpinLabelSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinLabelSizeChanged2(i, iAxis); +} + +void PlotterBoxChart::onSpinLabelSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->labelsFont(); + font.setPointSize(i); + axis->setLabelsFont(font); + mAxesParams[iAxis].labelSize = i; + } +} + +void PlotterBoxChart::onSpinMinChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMinChanged2(d, iAxis); +} + +void PlotterBoxChart::onSpinMinChanged2(double d, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMin(d); + } +} + +void PlotterBoxChart::onSpinMaxChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMaxChanged2(d, iAxis); +} + +void PlotterBoxChart::onSpinMaxChanged2(double d, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMax(d); + } +} + +void PlotterBoxChart::onComboMinChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QBarCategoryAxis* axis = (QBarCategoryAxis*)(axes.first()); + axis->setMin( ui->comboBoxMin->currentText() ); + } +} + +void PlotterBoxChart::onComboMaxChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QBarCategoryAxis* axis = (QBarCategoryAxis*)(axes.first()); + axis->setMax( ui->comboBoxMax->currentText() ); + } +} + +void PlotterBoxChart::onSpinTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setTickCount(i); + } + } +} + +void PlotterBoxChart::onSpinMTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + + // Force update + const int base = (int)axis->base(); + axis->setBase(base + 1); + axis->setBase(base); + } + } +} + +// +// Actions +void PlotterBoxChart::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : mAddFilenames) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void PlotterBoxChart::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void PlotterBoxChart::onReloadClicked() +{ + // Load new results + QString errorMsg; + BenchResults newBchResults = ResultParser::parseJsonFile( mOrigFilename, errorMsg ); + + if ( newBchResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing original file: " + mOrigFilename + " -> " + errorMsg); + return; + } + + for (const auto& addFile : qAsConst(mAddFilenames)) + { + errorMsg.clear(); + BenchResults newAddResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if ( newAddResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing additional file: " + addFile.filename + " -> " + errorMsg); + return; + } + + if (addFile.isAppend) + newBchResults.appendResults(newAddResults); + else + newBchResults.overwriteResults(newAddResults); + } + + // Check compatibility with previous + errorMsg.clear(); + if (mBenchIdxs.size() != newBchResults.benchmarks.size()) + { + errorMsg = "Number of series/points is different"; + if (mAllIndexes) + { + mBenchIdxs.clear(); + for (int i=0; i newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + mBenchIdxs, mPlotParams.xIdx, "X"); + const auto& oldChartSeries = mChartView->chart()->series(); + int newSeriesIdx = 0; + if (errorMsg.isEmpty()) + { + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + if (newSeriesIdx >= oldChartSeries.size()) + break; + + const QString& subsetName = bchSubset.name; + if (subsetName != mSeriesMapping[newSeriesIdx].oldName) + { + errorMsg = "Series has different name"; + break; + } + const auto boxSeries = (QBoxPlotSeries*)oldChartSeries[newSeriesIdx]; + if (bchSubset.idxs.size() != boxSeries->count()) + { + errorMsg = "Series has different number of points"; + break; + } + ++newSeriesIdx; + } + if (newSeriesIdx != oldChartSeries.size()) { + errorMsg = "Number of series is different"; + } + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + newSeriesIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Update points + QBoxPlotSeries* oldSeries = (QBoxPlotSeries*)oldChartSeries[newSeriesIdx]; + oldSeries->clear(); + + for (int idx : bchSubset.idxs) + { + QString xName = newBchResults.getParamName(mPlotParams.xType == PlotArgumentType, + idx, mPlotParams.xIdx); + BenchYStats yStats = getYPlotStats(newBchResults.benchmarks[idx], mPlotParams.yType); + + QScopedPointer box(new QBoxSet( xName.toHtmlEscaped() )); + box->setValue(QBoxSet::LowerExtreme, yStats.min * mCurrentTimeFactor); + box->setValue(QBoxSet::UpperExtreme, yStats.max * mCurrentTimeFactor); + box->setValue(QBoxSet::Median, yStats.median * mCurrentTimeFactor); + box->setValue(QBoxSet::LowerQuartile, yStats.lowQuart * mCurrentTimeFactor); + box->setValue(QBoxSet::UpperQuartile, yStats.uppQuart * mCurrentTimeFactor); + + oldSeries->append(box.take()); + } + ++newSeriesIdx; + } + } + // Reset update if all benchmarks + else if (mAllIndexes) + { + saveConfig(); + setupChart(newBchResults, mBenchIdxs, mPlotParams, false); + setupOptions(false); + } + else + { + QMessageBox::critical(this, "Chart reload", errorMsg); + return; + } + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +void PlotterBoxChart::onSnapshotClicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save snapshot"), "", tr("Images (*.png)")); + + if ( !fileName.isEmpty() ) + { + QPixmap pixmap = mChartView->grab(); + + bool ok = pixmap.save(fileName, "PNG"); + if (!ok) + QMessageBox::warning(this, "Chart snapshot", "Error saving snapshot file."); + } +} diff --git a/specifelse/benchtest/jomt/src/plotter_linechart.cpp b/specifelse/benchtest/jomt/src/plotter_linechart.cpp new file mode 100644 index 0000000..e6491f5 --- /dev/null +++ b/specifelse/benchtest/jomt/src/plotter_linechart.cpp @@ -0,0 +1,1126 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "plotter_linechart.h" +#include "ui_plotter_linechart.h" + +#include "benchmark_results.h" +#include "result_parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QtCharts; + +static const char* config_file = "config_lines.json"; + + +PlotterLineChart::PlotterLineChart(const BenchResults &bchResults, const QVector &bchIdxs, + const PlotParams &plotParams, const QString &origFilename, + const QVector& addFilenames, QWidget *parent) + : QWidget(parent) + , ui(new Ui::PlotterLineChart) + , mBenchIdxs(bchIdxs) + , mPlotParams(plotParams) + , mOrigFilename(origFilename) + , mAddFilenames(addFilenames) + , mAllIndexes(bchIdxs.size() == bchResults.benchmarks.size()) + , mWatcher(parent) +{ + // UI + ui->setupUi(this); + this->setAttribute(Qt::WA_DeleteOnClose); + + QFileInfo fileInfo(origFilename); + QString chartType = (plotParams.type == ChartLineType) ? "Lines - " : "Splines - "; + this->setWindowTitle(chartType + fileInfo.fileName()); + + connectUI(); + + //TODO: select points + //See: https://doc.qt.io/qt-5/qtcharts-callout-example.html + + // Init + setupChart(bchResults, bchIdxs, plotParams); + setupOptions(); + + // Show + ui->horizontalLayout->insertWidget(0, mChartView); +} + +PlotterLineChart::~PlotterLineChart() +{ + // Save options to file + saveConfig(); + + delete ui; +} + +void PlotterLineChart::connectUI() +{ + // Theme + ui->comboBoxTheme->addItem("Light", QChart::ChartThemeLight); + ui->comboBoxTheme->addItem("Blue Cerulean", QChart::ChartThemeBlueCerulean); + ui->comboBoxTheme->addItem("Dark", QChart::ChartThemeDark); + ui->comboBoxTheme->addItem("Brown Sand", QChart::ChartThemeBrownSand); + ui->comboBoxTheme->addItem("Blue Ncs", QChart::ChartThemeBlueNcs); + ui->comboBoxTheme->addItem("High Contrast", QChart::ChartThemeHighContrast); + ui->comboBoxTheme->addItem("Blue Icy", QChart::ChartThemeBlueIcy); + ui->comboBoxTheme->addItem("Qt", QChart::ChartThemeQt); + connect(ui->comboBoxTheme, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterLineChart::onComboThemeChanged); + + // Legend + connect(ui->checkBoxLegendVisible, &QCheckBox::stateChanged, this, &PlotterLineChart::onCheckLegendVisible); + + ui->comboBoxLegendAlign->addItem("Top", Qt::AlignTop); + ui->comboBoxLegendAlign->addItem("Bottom", Qt::AlignBottom); + ui->comboBoxLegendAlign->addItem("Left", Qt::AlignLeft); + ui->comboBoxLegendAlign->addItem("Right", Qt::AlignRight); + connect(ui->comboBoxLegendAlign, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterLineChart::onComboLegendAlignChanged); + + connect(ui->spinBoxLegendFontSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinLegendFontSizeChanged); + connect(ui->pushButtonSeries, &QPushButton::clicked, this, &PlotterLineChart::onSeriesEditClicked); + + if (!isYTimeBased(mPlotParams.yType)) + ui->comboBoxTimeUnit->setEnabled(false); + else + { + ui->comboBoxTimeUnit->addItem("ns", 1000.); + ui->comboBoxTimeUnit->addItem("us", 1.); + ui->comboBoxTimeUnit->addItem("ms", 0.001); + connect(ui->comboBoxTimeUnit, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterLineChart::onComboTimeUnitChanged); + } + + // Axes + ui->comboBoxAxis->addItem("X-Axis"); + ui->comboBoxAxis->addItem("Y-Axis"); + connect(ui->comboBoxAxis, QOverload::of(&QComboBox::currentIndexChanged), this, &PlotterLineChart::onComboAxisChanged); + + connect(ui->checkBoxAxisVisible, &QCheckBox::stateChanged, this, &PlotterLineChart::onCheckAxisVisible); + connect(ui->checkBoxTitle, &QCheckBox::stateChanged, this, &PlotterLineChart::onCheckTitleVisible); + connect(ui->checkBoxLog, &QCheckBox::stateChanged, this, &PlotterLineChart::onCheckLog); + connect(ui->spinBoxLogBase, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinLogBaseChanged); + connect(ui->lineEditTitle, &QLineEdit::textChanged, this, &PlotterLineChart::onEditTitleChanged); + connect(ui->spinBoxTitleSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinTitleSizeChanged); + connect(ui->lineEditFormat, &QLineEdit::textChanged, this, &PlotterLineChart::onEditFormatChanged); + connect(ui->spinBoxLabelSize, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinLabelSizeChanged); + connect(ui->doubleSpinBoxMin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterLineChart::onSpinMinChanged); + connect(ui->doubleSpinBoxMax, QOverload::of(&QDoubleSpinBox::valueChanged), this, &PlotterLineChart::onSpinMaxChanged); + connect(ui->spinBoxTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinTicksChanged); + connect(ui->spinBoxMTicks, QOverload::of(&QSpinBox::valueChanged), this, &PlotterLineChart::onSpinMTicksChanged); + + // Actions + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &PlotterLineChart::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &PlotterLineChart::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &PlotterLineChart::onReloadClicked); + connect(ui->pushButtonSnapshot, &QPushButton::clicked, this, &PlotterLineChart::onSnapshotClicked); +} + +void PlotterLineChart::setupChart(const BenchResults &bchResults, const QVector &bchIdxs, const PlotParams &plotParams, bool init) +{ + QScopedPointer scopedChart; + QChart* chart = nullptr; + if (init) { + scopedChart.reset( new QChart() ); + chart = scopedChart.get(); + } + else { // Re-init + chart = mChartView->chart(); + chart->setTitle(""); + chart->removeAllSeries(); + const auto xAxes = chart->axes(Qt::Horizontal); + if ( !xAxes.empty() ) + chart->removeAxis( xAxes.constFirst() ); + const auto yAxes = chart->axes(Qt::Vertical); + if ( !yAxes.empty() ) + chart->removeAxis( yAxes.constFirst() ); + mSeriesMapping.clear(); + } + Q_ASSERT(chart); + + // Time unit + mCurrentTimeFactor = 1.; + if ( isYTimeBased(mPlotParams.yType) ) { + if ( bchResults.meta.time_unit == "ns") mCurrentTimeFactor = 1000.; + else if (bchResults.meta.time_unit == "ms") mCurrentTimeFactor = 0.001; + } + + + // 2D Lines + // X: argumentA or templateB + // Y: time/iter/bytes/items (not name dependent) + // Line: one per benchmark % X-param + QVector bchSubsets = bchResults.groupParam(plotParams.xType == PlotArgumentType, + bchIdxs, plotParams.xIdx, "X"); + bool custDataAxis = true; + QString custDataName; + for (const auto& bchSubset : qAsConst(bchSubsets)) + { + // Ignore single point lines + if (bchSubset.idxs.size() < 2) { + qWarning() << "Not enough points to trace line for: " << bchSubset.name; + continue; + } + + // Chart type + QScopedPointer series; + if (plotParams.type == ChartLineType) series.reset(new QLineSeries()); + else series.reset(new QSplineSeries()); + + const QString& subsetName = bchSubset.name; +// qDebug() << "subsetName:" << subsetName; +// qDebug() << "subsetIdxs:" << bchSubset.idxs; + + double xFallback = 0.; + for (int idx : bchSubset.idxs) + { + QString xName = bchResults.getParamName(plotParams.xType == PlotArgumentType, + idx, plotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custDataName, custDataAxis, xFallback); + + // Add point + series->append(xVal, getYPlotValue(bchResults.benchmarks[idx], plotParams.yType) * mCurrentTimeFactor); + } + // Add series + series->setName( subsetName.toHtmlEscaped() ); + mSeriesMapping.push_back({subsetName, subsetName}); // color set later + chart->addSeries(series.take()); + } + + // + // Axes + if ( !chart->series().isEmpty() ) + { + chart->createDefaultAxes(); + + // X-axis + QValueAxis* xAxis = (QValueAxis*)(chart->axes(Qt::Horizontal).constFirst()); + if (plotParams.xType == PlotArgumentType) + xAxis->setTitleText("Argument " + QString::number(plotParams.xIdx+1)); + else { // template + if ( !custDataName.isEmpty() ) + xAxis->setTitleText(custDataName); + else + xAxis->setTitleText("Template " + QString::number(plotParams.xIdx+1)); + } + xAxis->setTickCount(9); + + // Y-axis + QValueAxis* yAxis = (QValueAxis*)(chart->axes(Qt::Vertical).constFirst()); + yAxis->setTitleText( getYPlotName(plotParams.yType, bchResults.meta.time_unit) ); + yAxis->applyNiceNumbers(); + } + else + chart->setTitle("No series with at least 2 points to display"); + + if (init) + { + // View + mChartView = new QChartView(scopedChart.take(), this); + mChartView->setRenderHint(QPainter::Antialiasing); + } +} + +void PlotterLineChart::setupOptions(bool init) +{ + auto chart = mChartView->chart(); + + // General + if (init) + { + chart->setTheme(QChart::ChartThemeLight); + chart->legend()->setAlignment(Qt::AlignTop); + chart->legend()->setShowToolTips(true); + } + ui->spinBoxLegendFontSize->setValue( chart->legend()->font().pointSize() ); + + mIgnoreEvents = true; + int prevAxisIdx = ui->comboBoxAxis->currentIndex(); + + if (!init) // Re-init + { + mAxesParams[0].log = false; + mAxesParams[1].log = false; + ui->comboBoxAxis->setCurrentIndex(0); + ui->checkBoxAxisVisible->setChecked(true); + ui->checkBoxTitle->setChecked(true); + ui->checkBoxLog->setChecked(false); + } + + // Time unit + if (mCurrentTimeFactor > 1.) ui->comboBoxTimeUnit->setCurrentIndex(0); // ns + else if (mCurrentTimeFactor < 1.) ui->comboBoxTimeUnit->setCurrentIndex(2); // ms + else ui->comboBoxTimeUnit->setCurrentIndex(1); // us + + // Axes + const auto& hAxes = chart->axes(Qt::Horizontal); + if ( !hAxes.isEmpty() ) + { + QValueAxis* xAxis = (QValueAxis*)(hAxes.first()); + auto& axisParam = mAxesParams[0]; + + axisParam.titleText = xAxis->titleText(); + axisParam.titleSize = xAxis->titleFont().pointSize(); + axisParam.labelFormat = "%g"; + xAxis->setLabelFormat(axisParam.labelFormat); + axisParam.labelSize = xAxis->labelsFont().pointSize(); + axisParam.min = xAxis->min(); + axisParam.max = xAxis->max(); + axisParam.ticks = xAxis->tickCount(); + axisParam.mticks = xAxis->minorTickCount(); + + ui->lineEditTitle->setText( axisParam.titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( axisParam.titleSize ); + ui->lineEditFormat->setText( axisParam.labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + ui->spinBoxLabelSize->setValue( axisParam.labelSize ); + ui->doubleSpinBoxMin->setValue( axisParam.min ); + ui->doubleSpinBoxMax->setValue( axisParam.max ); + ui->spinBoxTicks->setValue( axisParam.ticks ); + ui->spinBoxMTicks->setValue( axisParam.mticks ); + } + const auto& vAxes = chart->axes(Qt::Vertical); + if ( !vAxes.isEmpty() ) + { + QValueAxis* yAxis = (QValueAxis*)(vAxes.first()); + auto& axisParam = mAxesParams[1]; + + axisParam.titleText = yAxis->titleText(); + axisParam.titleSize = yAxis->titleFont().pointSize(); + axisParam.labelFormat = "%g"; + yAxis->setLabelFormat(axisParam.labelFormat); + axisParam.labelSize = yAxis->labelsFont().pointSize(); + axisParam.min = yAxis->min(); + axisParam.max = yAxis->max(); + axisParam.ticks = yAxis->tickCount(); + axisParam.mticks = yAxis->minorTickCount(); + } + mIgnoreEvents = false; + + + // Load options from file + loadConfig(init); + + + // Apply actions + if (ui->checkBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); + + // Update series color config + const auto& chartSeries = chart->series(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = (QXYSeries*)chartSeries.at(idx); + + config.oldColor = series->color(); + if (!config.newColor.isValid()) + config.newColor = series->color(); // init + else + series->setColor(config.newColor); // apply + + if (config.newName != config.oldName) + series->setName( config.newName.toHtmlEscaped() ); + } + + // Restore selected axis + if (!init) + ui->comboBoxAxis->setCurrentIndex(prevAxisIdx); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +void PlotterLineChart::loadConfig(bool init) +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // Theme + if (json.contains("theme") && json["theme"].isString()) + ui->comboBoxTheme->setCurrentText( json["theme"].toString() ); + + // Legend + if (json.contains("legend.visible") && json["legend.visible"].isBool()) + ui->checkBoxLegendVisible->setChecked( json["legend.visible"].toBool() ); + if (json.contains("legend.align") && json["legend.align"].isString()) + ui->comboBoxLegendAlign->setCurrentText( json["legend.align"].toString() ); + if (json.contains("legend.fontSize") && json["legend.fontSize"].isDouble()) + ui->spinBoxLegendFontSize->setValue( json["legend.fontSize"].toInt(8) ); + + // Series + if (json.contains("series") && json["series"].isArray()) + { + auto series = json["series"].toArray(); + for (int idx = 0; idx < series.size(); ++idx) { + QJsonObject config = series[idx].toObject(); + if ( config.contains("oldName") && config["oldName"].isString() + && config.contains("newName") && config["newName"].isString() + && config.contains("newColor") && config["newColor"].isString() + && QColor::isValidColor(config["newColor"].toString()) ) + { + SeriesConfig savedConfig(config["oldName"].toString(), ""); + int iCfg = mSeriesMapping.indexOf(savedConfig); + if (iCfg >= 0) { + mSeriesMapping[iCfg].newName = config["newName"].toString(); + mSeriesMapping[iCfg].newColor.setNamedColor( config["newColor"].toString() ); + } + } + } + } + + // Time + if (!init) { + if (json.contains("timeUnit") && json["timeUnit"].isString()) + ui->comboBoxTimeUnit->setCurrentText( json["timeUnit"].toString() ); + } + + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + + // Axes + QString prefix = "axis.x"; + for (int idx = 0; idx < 2; ++idx) + { + auto& axis = mAxesParams[idx]; + + if (json.contains(prefix + ".visible") && json[prefix + ".visible"].isBool()) { + axis.visible = json[prefix + ".visible"].toBool(); + ui->checkBoxAxisVisible->setChecked( axis.visible ); + } + if (json.contains(prefix + ".title") && json[prefix + ".title"].isBool()) { + axis.title = json[prefix + ".title"].toBool(); + ui->checkBoxTitle->setChecked( axis.title ); + } + if (json.contains(prefix + ".log") && json[prefix + ".log"].isBool()) { + axis.log = json[prefix + ".log"].toBool(); + ui->checkBoxLog->setChecked( axis.log ); + } + if (json.contains(prefix + ".logBase") && json[prefix + ".logBase"].isDouble()) { + axis.logBase = json[prefix + ".logBase"].toInt(10); + ui->spinBoxLogBase->setValue( axis.logBase ); + } + if (json.contains(prefix + ".titleSize") && json[prefix + ".titleSize"].isDouble()) { + axis.titleSize = json[prefix + ".titleSize"].toInt(8); + ui->spinBoxTitleSize->setValue( axis.titleSize ); + } + if (json.contains(prefix + ".labelFormat") && json[prefix + ".labelFormat"].isString()) { + axis.labelFormat = json[prefix + ".labelFormat"].toString(); + ui->lineEditFormat->setText( axis.labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + } + if (json.contains(prefix + ".labelSize") && json[prefix + ".labelSize"].isDouble()) { + axis.labelSize = json[prefix + ".labelSize"].toInt(8); + ui->spinBoxLabelSize->setValue( axis.labelSize ); + } + if (json.contains(prefix + ".ticks") && json[prefix + ".ticks"].isDouble()) { + axis.ticks = json[prefix + ".ticks"].toInt(idx == 0 ? 9 : 5); + ui->spinBoxTicks->setValue( axis.ticks ); + } + if (json.contains(prefix + ".mticks") && json[prefix + ".mticks"].isDouble()) { + axis.mticks = json[prefix + ".mticks"].toInt(0); + ui->spinBoxMTicks->setValue( axis.mticks ); + } + if (!init) + { + if (json.contains(prefix + ".titleText") && json[prefix + ".titleText"].isString()) { + axis.titleText = json[prefix + ".titleText"].toString(); + ui->lineEditTitle->setText( axis.titleText ); + ui->lineEditTitle->setCursorPosition(0); + } + if (idx == 1) + { + if (json.contains(prefix + ".min") && json[prefix + ".min"].isDouble()) { + axis.min = json[prefix + ".min"].toDouble(); + ui->doubleSpinBoxMin->setValue( axis.min ); + } + if (json.contains(prefix + ".max") && json[prefix + ".max"].isDouble()) { + axis.max = json[prefix + ".max"].toDouble(); + ui->doubleSpinBoxMax->setValue( axis.max ); + } + } + } + + prefix = "axis.y"; + ui->comboBoxAxis->setCurrentIndex(1); + } + ui->comboBoxAxis->setCurrentIndex(0); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } +} + +void PlotterLineChart::saveConfig() +{ + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // Theme + json["theme"] = ui->comboBoxTheme->currentText(); + // Legend + json["legend.visible"] = ui->checkBoxLegendVisible->isChecked(); + json["legend.align"] = ui->comboBoxLegendAlign->currentText(); + json["legend.fontSize"] = ui->spinBoxLegendFontSize->value(); + // Series + QJsonArray series; + for (const auto& seriesConfig : qAsConst(mSeriesMapping)) { + QJsonObject config; + config["oldName"] = seriesConfig.oldName; + config["newName"] = seriesConfig.newName; + config["newColor"] = seriesConfig.newColor.name(); + series.append(config); + } + if (!series.empty()) + json["series"] = series; + // Time + json["timeUnit"] = ui->comboBoxTimeUnit->currentText(); + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + // Axes + QString prefix = "axis.x"; + for (const auto& axis : mAxesParams) + { + json[prefix + ".visible"] = axis.visible; + json[prefix + ".title"] = axis.title; + json[prefix + ".log"] = axis.log; + json[prefix + ".logBase"] = axis.logBase; + json[prefix + ".titleText"] = axis.titleText; + json[prefix + ".titleSize"] = axis.titleSize; + json[prefix + ".labelFormat"] = axis.labelFormat; + json[prefix + ".labelSize"] = axis.labelSize; + json[prefix + ".min"] = axis.min; + json[prefix + ".max"] = axis.max; + json[prefix + ".ticks"] = axis.ticks; + json[prefix + ".mticks"] = axis.mticks; + + prefix = "axis.y"; + } + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +// +// Theme +void PlotterLineChart::onComboThemeChanged(int index) +{ + QChart::ChartTheme theme = static_cast( + ui->comboBoxTheme->itemData(index).toInt()); + mChartView->chart()->setTheme(theme); + + // Update series color + const auto& chartSeries = mChartView->chart()->series(); + for (int idx = 0 ; idx < mSeriesMapping.size(); ++idx) + { + auto& config = mSeriesMapping[idx]; + const auto& series = (QXYSeries*)chartSeries.at(idx); + auto prevColor = config.oldColor; + + config.oldColor = series->color(); + if (config.newColor != prevColor) + series->setColor(config.newColor); // re-apply config + else + config.newColor = config.oldColor; // sync with theme + } + + // Re-apply font sizes + onSpinLegendFontSizeChanged( ui->spinBoxLegendFontSize->value() ); + onSpinLabelSizeChanged2(mAxesParams[0].labelSize, 0); + onSpinLabelSizeChanged2(mAxesParams[1].labelSize, 1); + onSpinTitleSizeChanged2(mAxesParams[0].titleSize, 0); + onSpinTitleSizeChanged2(mAxesParams[1].titleSize, 1); +} + +// +// Legend +void PlotterLineChart::onCheckLegendVisible(int state) +{ + mChartView->chart()->legend()->setVisible(state == Qt::Checked); +} + +void PlotterLineChart::onComboLegendAlignChanged(int index) +{ + Qt::Alignment align = static_cast( + ui->comboBoxLegendAlign->itemData(index).toInt()); + mChartView->chart()->legend()->setAlignment(align); +} + +void PlotterLineChart::onSpinLegendFontSizeChanged(int i) +{ + QFont font = mChartView->chart()->legend()->font(); + font.setPointSize(i); + mChartView->chart()->legend()->setFont(font); +} + +void PlotterLineChart::onSeriesEditClicked() +{ + SeriesDialog seriesDialog(mSeriesMapping, this); + auto res = seriesDialog.exec(); + if (res == QDialog::Accepted) + { + const auto& newMapping = seriesDialog.getMapping(); + for (int idx = 0; idx < newMapping.size(); ++idx) + { + const auto& newPair = newMapping[idx]; + const auto& oldPair = mSeriesMapping[idx]; + auto series = (QXYSeries*)mChartView->chart()->series().at(idx); + if (newPair.newName != oldPair.newName) { + series->setName( newPair.newName.toHtmlEscaped() ); + } + if (newPair.newColor != oldPair.newColor) { + series->setColor(newPair.newColor); + } + } + mSeriesMapping = newMapping; + } +} + +void PlotterLineChart::onComboTimeUnitChanged(int /*index*/) +{ + if (mIgnoreEvents) return; + + // Update data + double unitFactor = ui->comboBoxTimeUnit->currentData().toDouble(); + double updateFactor = unitFactor / mCurrentTimeFactor; // can cause precision loss + auto chartSeries = mChartView->chart()->series(); + for (auto& series : chartSeries) + { + auto xySeries = (QXYSeries*)series; + auto points = xySeries->pointsVector(); + for (auto& point : points) { + point.setY(point.y() * updateFactor); + } + xySeries->replace(points); + } + + // Update axis title + QString oldUnitName = "(us)"; + if (mCurrentTimeFactor > 1.) oldUnitName = "(ns)"; + else if (mCurrentTimeFactor < 1.) oldUnitName = "(ms)"; + + const auto& axes = mChartView->chart()->axes(Qt::Vertical); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + QString axisTitle = axis->titleText(); + if (axisTitle.endsWith(oldUnitName)) { + QString unitName = ui->comboBoxTimeUnit->currentText(); + onEditTitleChanged2(axisTitle.replace(axisTitle.size() - 3, 2, unitName), 1); + } + } + // Update range + if (ui->comboBoxAxis->currentIndex() == 1) { + ui->doubleSpinBoxMin->setValue(mAxesParams[1].min * updateFactor); + ui->doubleSpinBoxMax->setValue(mAxesParams[1].max * updateFactor); + } + else { + onSpinMinChanged2(mAxesParams[1].min * updateFactor, 1); + onSpinMaxChanged2(mAxesParams[1].max * updateFactor, 1); + } + + mCurrentTimeFactor = unitFactor; +} + +// +// Axes +void PlotterLineChart::onComboAxisChanged(int idx) +{ + // Update UI + bool wasIgnoring = mIgnoreEvents; + mIgnoreEvents = true; + + ui->checkBoxAxisVisible->setChecked( mAxesParams[idx].visible ); + ui->checkBoxTitle->setChecked( mAxesParams[idx].title ); + ui->checkBoxLog->setChecked( mAxesParams[idx].log ); + ui->spinBoxLogBase->setValue( mAxesParams[idx].logBase ); + ui->lineEditTitle->setText( mAxesParams[idx].titleText ); + ui->lineEditTitle->setCursorPosition(0); + ui->spinBoxTitleSize->setValue( mAxesParams[idx].titleSize ); + ui->lineEditFormat->setText( mAxesParams[idx].labelFormat ); + ui->lineEditFormat->setCursorPosition(0); + ui->spinBoxLabelSize->setValue( mAxesParams[idx].labelSize ); + ui->doubleSpinBoxMin->setDecimals(idx == 1 ? 6 : 3); + ui->doubleSpinBoxMax->setDecimals(idx == 1 ? 6 : 3); + ui->doubleSpinBoxMin->setValue( mAxesParams[idx].min ); + ui->doubleSpinBoxMax->setValue( mAxesParams[idx].max ); + ui->doubleSpinBoxMin->setSingleStep(idx == 1 ? 0.1 : 1.0); + ui->doubleSpinBoxMax->setSingleStep(idx == 1 ? 0.1 : 1.0); + ui->spinBoxTicks->setValue( mAxesParams[idx].ticks ); + ui->spinBoxMTicks->setValue( mAxesParams[idx].mticks ); + + ui->spinBoxTicks->setEnabled( !mAxesParams[idx].log ); + ui->spinBoxLogBase->setEnabled( mAxesParams[idx].log ); + + mIgnoreEvents = wasIgnoring; +} + +void PlotterLineChart::onCheckAxisVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setVisible(state == Qt::Checked); + mAxesParams[iAxis].visible = state == Qt::Checked; + } +} + +void PlotterLineChart::onCheckTitleVisible(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleVisible(state == Qt::Checked); + mAxesParams[iAxis].title = state == Qt::Checked; + } +} + +void PlotterLineChart::onCheckLog(int state) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + Qt::Alignment align = iAxis == 0 ? Qt::AlignBottom : Qt::AlignLeft; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if (state == Qt::Checked) + { + QValueAxis* axis = (QValueAxis*)(axes.first()); + + QLogValueAxis* logAxis = new QLogValueAxis(); + logAxis->setVisible( axis->isVisible() ); + logAxis->setTitleVisible( axis->isTitleVisible() ); + logAxis->setTitleText( axis->titleText() ); + logAxis->setTitleFont( axis->titleFont() ); + logAxis->setLabelFormat( axis->labelFormat() ); + logAxis->setLabelsFont( axis->labelsFont() ); + + mChartView->chart()->removeAxis(axis); + mChartView->chart()->addAxis(logAxis, align); + const auto chartSeries = mChartView->chart()->series(); + for (const auto& series : chartSeries) + series->attachAxis(logAxis); + + logAxis->setBase( mAxesParams[iAxis].logBase ); + logAxis->setMin( mAxesParams[iAxis].min ); + logAxis->setMax( mAxesParams[iAxis].max ); + logAxis->setMinorTickCount( mAxesParams[iAxis].mticks ); + } + else + { + QLogValueAxis*logAxis = (QLogValueAxis*)(axes.first()); + + QValueAxis* axis = new QValueAxis(); + axis->setVisible( logAxis->isVisible() ); + axis->setTitleVisible( logAxis->isTitleVisible() ); + axis->setTitleText( logAxis->titleText() ); + axis->setTitleFont( logAxis->titleFont() ); + axis->setLabelFormat( logAxis->labelFormat() ); + axis->setLabelsFont( logAxis->labelsFont() ); + + mChartView->chart()->removeAxis(logAxis); + mChartView->chart()->addAxis(axis, align); + const auto chartSeries = mChartView->chart()->series(); + for (const auto& series : chartSeries) + series->attachAxis(axis); + + axis->setMin( mAxesParams[iAxis].min ); + axis->setMax( mAxesParams[iAxis].max ); + axis->setTickCount( mAxesParams[iAxis].ticks ); + axis->setMinorTickCount( mAxesParams[iAxis].mticks ); + } + ui->spinBoxTicks->setEnabled( state != Qt::Checked); + ui->spinBoxLogBase->setEnabled(state == Qt::Checked); + mAxesParams[iAxis].log = state == Qt::Checked; + } +} + +void PlotterLineChart::onSpinLogBaseChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() && ui->checkBoxLog->isChecked()) + { + QLogValueAxis*logAxis = (QLogValueAxis*)(axes.first()); + logAxis->setBase(i); + mAxesParams[iAxis].logBase = i; + } +} + +void PlotterLineChart::onEditTitleChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onEditTitleChanged2(text, iAxis); +} + +void PlotterLineChart::onEditTitleChanged2(const QString& text, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setTitleText(text); + mAxesParams[iAxis].titleText = text; + } +} + +void PlotterLineChart::onSpinTitleSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinTitleSizeChanged2(i, iAxis); +} + +void PlotterLineChart::onSpinTitleSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->titleFont(); + font.setPointSize(i); + axis->setTitleFont(font); + mAxesParams[iAxis].titleSize = i; + } +} + +void PlotterLineChart::onEditFormatChanged(const QString& text) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setLabelFormat(text); + } + mAxesParams[iAxis].labelFormat = text; + } +} + +void PlotterLineChart::onSpinLabelSizeChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinLabelSizeChanged2(i, iAxis); +} + +void PlotterLineChart::onSpinLabelSizeChanged2(int i, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + + QFont font = axis->labelsFont(); + font.setPointSize(i); + axis->setLabelsFont(font); + mAxesParams[iAxis].labelSize = i; + } +} + +void PlotterLineChart::onSpinMinChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMinChanged2(d, iAxis); +} + +void PlotterLineChart::onSpinMinChanged2(double d, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMin(d); + mAxesParams[iAxis].min = d; + } +} + +void PlotterLineChart::onSpinMaxChanged(double d) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + + onSpinMaxChanged2(d, iAxis); +} + +void PlotterLineChart::onSpinMaxChanged2(double d, int iAxis) +{ + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) { + QAbstractAxis* axis = axes.first(); + axis->setMax(d); + mAxesParams[iAxis].max = d; + } +} + +void PlotterLineChart::onSpinTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setTickCount(i); + mAxesParams[iAxis].ticks = i; + } + } +} + +void PlotterLineChart::onSpinMTicksChanged(int i) +{ + if (mIgnoreEvents) return; + int iAxis = ui->comboBoxAxis->currentIndex(); + Qt::Orientation orient = iAxis == 0 ? Qt::Horizontal : Qt::Vertical; + + const auto& axes = mChartView->chart()->axes(orient); + if ( !axes.isEmpty() ) + { + if ( !ui->checkBoxLog->isChecked() ) { + QValueAxis* axis = (QValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + } + else { + QLogValueAxis* axis = (QLogValueAxis*)(axes.first()); + axis->setMinorTickCount(i); + + // Force update + const int base = (int)axis->base(); + axis->setBase(base + 1); + axis->setBase(base); + } + mAxesParams[iAxis].mticks = i; + } +} + +// +// Actions +void PlotterLineChart::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : qAsConst(mAddFilenames)) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void PlotterLineChart::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void PlotterLineChart::onReloadClicked() +{ + // Load new results + QString errorMsg; + BenchResults newBchResults = ResultParser::parseJsonFile( mOrigFilename, errorMsg ); + + if ( newBchResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing original file: " + mOrigFilename + " -> " + errorMsg); + return; + } + for (const auto& addFile : qAsConst(mAddFilenames)) + { + errorMsg.clear(); + BenchResults newAddResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if ( newAddResults.benchmarks.isEmpty() ) { + QMessageBox::critical(this, "Chart reload", "Error parsing additional file: " + addFile.filename + " -> " + errorMsg); + return; + } + if (addFile.isAppend) + newBchResults.appendResults(newAddResults); + else + newBchResults.overwriteResults(newAddResults); + } + + // Check compatibility with previous + errorMsg.clear(); + if (mBenchIdxs.size() != newBchResults.benchmarks.size()) + { + errorMsg = "Number of series/points is different"; + if (mAllIndexes) + { + mBenchIdxs.clear(); + for (int i=0; i newBchSubsets = newBchResults.groupParam(mPlotParams.xType == PlotArgumentType, + mBenchIdxs, mPlotParams.xIdx, "X"); + const auto& oldChartSeries = mChartView->chart()->series(); + int newSeriesIdx = 0; + if (errorMsg.isEmpty()) + { + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Ignore single point lines + if (bchSubset.idxs.size() < 2) + continue; + if (newSeriesIdx >= oldChartSeries.size()) + break; + + const QString& subsetName = bchSubset.name; + if (subsetName != mSeriesMapping[newSeriesIdx].oldName) { + errorMsg = "Series has different name"; + break; + } + const auto lineSeries = (QLineSeries*)(oldChartSeries[newSeriesIdx]); + if (bchSubset.idxs.size() != lineSeries->count()) + { + errorMsg = "Series has different number of points"; + break; + } + ++newSeriesIdx; + } + if (newSeriesIdx != oldChartSeries.size()) { + errorMsg = "Number of series is different"; + } + } + + // Direct update if compatible + if ( errorMsg.isEmpty() ) + { + bool custDataAxis = true; + QString custDataName; + newSeriesIdx = 0; + for (const auto& bchSubset : qAsConst(newBchSubsets)) + { + // Ignore single point lines + if (bchSubset.idxs.size() < 2) { + qWarning() << "Not enough points to trace line for: " << bchSubset.name; + continue; + } + + // Update points + QXYSeries* oldSeries = (QXYSeries*)oldChartSeries[newSeriesIdx]; + oldSeries->clear(); + + double xFallback = 0.; + for (int idx : bchSubset.idxs) + { + QString xName = newBchResults.getParamName(mPlotParams.xType == PlotArgumentType, + idx, mPlotParams.xIdx); + double xVal = BenchResults::getParamValue(xName, custDataName, custDataAxis, xFallback); + + // Add point + oldSeries->append(xVal, getYPlotValue(newBchResults.benchmarks[idx], mPlotParams.yType) * mCurrentTimeFactor); + } + ++newSeriesIdx; + } + } + // Reset update if all benchmarks + else if (mAllIndexes) + { + saveConfig(); + setupChart(newBchResults, mBenchIdxs, mPlotParams, false); + setupOptions(false); + } + else + { + QMessageBox::critical(this, "Chart reload", errorMsg); + return; + } + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +void PlotterLineChart::onSnapshotClicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save snapshot"), "", tr("Images (*.png)")); + + if ( !fileName.isEmpty() ) + { + QPixmap pixmap = mChartView->grab(); + + bool ok = pixmap.save(fileName, "PNG"); + if (!ok) + QMessageBox::warning(this, "Chart snapshot", "Error saving snapshot file."); + } +} diff --git a/specifelse/benchtest/jomt/src/resource/jomt_icon.png b/specifelse/benchtest/jomt/src/resource/jomt_icon.png new file mode 100644 index 0000000..bfa8fd5 Binary files /dev/null and b/specifelse/benchtest/jomt/src/resource/jomt_icon.png differ diff --git a/specifelse/benchtest/jomt/src/resource/resource.qrc b/specifelse/benchtest/jomt/src/resource/resource.qrc new file mode 100644 index 0000000..3afb4a0 --- /dev/null +++ b/specifelse/benchtest/jomt/src/resource/resource.qrc @@ -0,0 +1,5 @@ + + + jomt_icon.png + + diff --git a/specifelse/benchtest/jomt/src/result_parser.cpp b/specifelse/benchtest/jomt/src/result_parser.cpp new file mode 100644 index 0000000..0f68c67 --- /dev/null +++ b/specifelse/benchtest/jomt/src/result_parser.cpp @@ -0,0 +1,669 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "result_parser.h" + +#include +#include +#include +#include + +#define PARSE_DEBUG false +#if PARSE_DEBUG + #include +#endif + + +// Find benchmark index by name +static int findExistingBenchmark(const BenchResults &bchResults, const QString &run_name) +{ + int idx = -1; + for (int i=0; idx<0 && i 0) + { + if (bchData.repetitions <= 0) + { + bool ok = false; + int repetitions = bchData.run_name.midRef(lastIdx + aggSuffix.size()).toInt(&ok); + if (ok) + bchData.repetitions = repetitions; + } + bchData.run_name.truncate(lastIdx); + bchData.name = bchData.run_name; + } +} + + +// Parse benchmark results from json file +BenchResults ResultParser::parseJsonFile(const QString &filename, QString& errorMsg) +{ + BenchResults bchResults; + + // Read file + QFile benchFile(filename); + if ( !benchFile.open(QIODevice::ReadOnly) ) { + errorMsg = "Couldn't open benchmark results file."; + return bchResults; + } + QByteArray benchData = benchFile.readAll(); + benchFile.close(); + + // Get Json main object + QJsonDocument benchDoc( QJsonDocument::fromJson(benchData) ); + if (!benchDoc.isObject()) { + errorMsg = "Not a json benchmark results file."; + return bchResults; + } + QJsonObject benchObj = benchDoc.object(); + if (benchObj.isEmpty()) { + errorMsg = "Empty json benchmark results file."; + return bchResults; + } + + + /* + * Context + */ + if (benchObj.contains("context") && benchObj["context"].isObject()) + { + QJsonObject ctxObj = benchObj["context"].toObject(); + + // Meta + if (ctxObj.contains("date") && ctxObj["date"].isString()) + { + bchResults.context.date = ctxObj["date"].toString(); + if (PARSE_DEBUG) qDebug() << "date: " << bchResults.context.date; + } + if (ctxObj.contains("host_name") && ctxObj["host_name"].isString()) + { + bchResults.context.host_name = ctxObj["host_name"].toString(); + if (PARSE_DEBUG) qDebug() << "host_name: " << bchResults.context.host_name; + } + if (ctxObj.contains("executable") && ctxObj["executable"].isString()) + { + bchResults.context.executable = ctxObj["executable"].toString(); + if (PARSE_DEBUG) qDebug() << "executable: " << bchResults.context.executable; + } + + // Build + if (ctxObj.contains("library_build_type") && ctxObj["library_build_type"].isString()) + { + bchResults.context.build_type = ctxObj["library_build_type"].toString(); + if (PARSE_DEBUG) qDebug() << "library_build_type: " << bchResults.context.build_type; + } + else if (ctxObj.contains("build_type") && ctxObj["build_type"].isString()) + { + bchResults.context.build_type = ctxObj["build_type"].toString(); + if (PARSE_DEBUG) qDebug() << "build_type: " << bchResults.context.build_type; + } + + // CPU + if (ctxObj.contains("num_cpus") && ctxObj["num_cpus"].isDouble()) + { + bchResults.context.num_cpus = ctxObj["num_cpus"].toInt(); + if (PARSE_DEBUG) qDebug() << "num_cpus: " << bchResults.context.num_cpus; + } + if (ctxObj.contains("mhz_per_cpu") && ctxObj["mhz_per_cpu"].isDouble()) + { + bchResults.context.mhz_per_cpu = ctxObj["mhz_per_cpu"].toInt(); + if (PARSE_DEBUG) qDebug() << "mhz_per_cpu: " << bchResults.context.mhz_per_cpu; + } + if (ctxObj.contains("cpu_scaling_enabled") && ctxObj["cpu_scaling_enabled"].isBool()) + { + bchResults.context.cpu_scaling_enabled = ctxObj["cpu_scaling_enabled"].toBool(); + if (PARSE_DEBUG) qDebug() << "cpu_scaling_enabled: " << bchResults.context.cpu_scaling_enabled; + } + + // Caches + if (ctxObj.contains("caches") && ctxObj["caches"].isArray()) + { + QJsonArray cchArray = ctxObj["caches"].toArray(); + bchResults.context.caches.reserve( cchArray.size() ); + + for (int cchIdx = 0; cchIdx < cchArray.size(); ++cchIdx) + { + // Cache + QJsonObject cchObj = cchArray[cchIdx].toObject(); + BenchCache bchCache; + if (PARSE_DEBUG) qDebug() << "Context cache"; + + // Meta + if (cchObj.contains("type") && cchObj["type"].isString()) + { + bchCache.type = cchObj["type"].toString(); + if (PARSE_DEBUG) qDebug() << "-> type:" << bchCache.type; + } + if (cchObj.contains("level") && cchObj["level"].isDouble()) + { + bchCache.level = cchObj["level"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> level:" << bchCache.level; + } + if (cchObj.contains("size") && cchObj["size"].isDouble()) + { + bchCache.size = static_cast( cchObj["size"].toDouble() ); + if (PARSE_DEBUG) qDebug() << "-> size:" << bchCache.size; + } + if (cchObj.contains("num_sharing") && cchObj["num_sharing"].isDouble()) + { + bchCache.num_sharing = cchObj["num_sharing"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> num_sharing:" << bchCache.num_sharing; + } + + // + // Push bench cache + bchResults.context.caches.append(bchCache); + + // New line between caches + if (PARSE_DEBUG) qDebug() << ""; + } + } + } + else + qCritical() << "Results parsing: missing field 'context'"; + + // New line between context and benchmarks + if (PARSE_DEBUG) qDebug() << ""; + + + /* + * Benchmarks + */ + if (benchObj.contains("benchmarks") && benchObj["benchmarks"].isArray()) + { + QJsonArray bchArray = benchObj["benchmarks"].toArray(); + bchResults.benchmarks.reserve( bchArray.size() ); + + for (int bchIdx = 0; bchIdx < bchArray.size(); ++bchIdx) + { + // Benchmark + QJsonObject bchObj = bchArray[bchIdx].toObject(); + BenchData bchData; + + // + // Name + if (bchObj.contains("name") && bchObj["name"].isString()) + { + bchData.name = bchObj["name"].toString(); + if (PARSE_DEBUG) qDebug() << "bench name:" << bchData.name; + } + else { + qCritical() << "Results parsing: missing benchmark field 'name'"; + continue; + } + // Run name + if (bchObj.contains("run_name") && bchObj["run_name"].isString()) + { + bchData.run_name = bchObj["run_name"].toString(); + if (PARSE_DEBUG) qDebug() << "-> run_name:" << bchData.run_name; + } + else { + bchData.run_name = bchData.name; + if (PARSE_DEBUG) qDebug() << "-> name as run_name:" << bchData.run_name; + } + cleanupName(bchData); + // Run type + if (bchObj.contains("run_type") && bchObj["run_type"].isString()) + { + bchData.run_type = bchObj["run_type"].toString(); + if (PARSE_DEBUG) qDebug() << "-> run_type:" << bchData.run_type; + } + else { + bchData.run_type = "iteration"; + if (PARSE_DEBUG) qDebug() << "-> default run_type:" << bchData.run_type; + } + + // + // Timing + if (bchObj.contains("iterations") && bchObj["iterations"].isDouble()) + { + bchData.iterations = bchObj["iterations"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> iterations:" << bchData.iterations; + } + else { + qCritical() << "Results parsing: missing benchmark field 'iterations'"; + continue; + } + + if (bchObj.contains("real_time") && bchObj["real_time"].isDouble()) + { + bchData.real_time.append( bchObj["real_time"].toDouble() ); + if (PARSE_DEBUG) qDebug() << "-> real_time:" << bchData.real_time.back(); + } + else { + qCritical() << "Results parsing: missing benchmark field 'real_time'"; + continue; + } + + if (bchObj.contains("cpu_time") && bchObj["cpu_time"].isDouble()) + { + bchData.cpu_time.append( bchObj["cpu_time"].toDouble() ); + if (PARSE_DEBUG) qDebug() << "-> cpu_time:" << bchData.cpu_time.back(); + } + else { + qCritical() << "Results parsing: missing benchmark field 'cpu_time'"; + continue; + } + + if (bchObj.contains("time_unit") && bchObj["time_unit"].isString()) + { + bchData.time_unit = bchObj["time_unit"].toString(); + if (PARSE_DEBUG) qDebug() << "-> time_unit:" << bchData.time_unit; + } + else { + bchData.time_unit = "ns"; + if (PARSE_DEBUG) qDebug() << "-> default time_unit:" << bchData.time_unit; + } + // Time normalization (us) + double timeFactor = 1.; + if (bchData.time_unit == "ns") + { + timeFactor = 0.001; + if (bchResults.meta.time_unit.isEmpty()) bchResults.meta.time_unit = "ns"; + else if (bchResults.meta.time_unit != "ns") bchResults.meta.time_unit = "us"; + } + else if (bchData.time_unit == "ms") + { + timeFactor = 1000.; + if (bchResults.meta.time_unit.isEmpty()) bchResults.meta.time_unit = "ms"; + else if (bchResults.meta.time_unit != "ms") bchResults.meta.time_unit = "us"; + + } + else { + bchResults.meta.time_unit = "us"; + } + bchData.real_time_us = bchData.real_time.back() * timeFactor; + bchData.cpu_time_us = bchData.cpu_time.back() * timeFactor; + + // + // Throughput + if (bchObj.contains("bytes_per_second") && bchObj["bytes_per_second"].isDouble()) + { + bchData.kbytes_sec.append(bchObj["bytes_per_second"].toDouble() * 0.001); + bchData.kbytes_sec_dflt = bchData.kbytes_sec.back(); + bchResults.meta.hasBytesSec = true; + if (PARSE_DEBUG) qDebug() << "-> kbytes_sec:" << bchData.kbytes_sec_dflt; + } + if (bchObj.contains("items_per_second") && bchObj["items_per_second"].isDouble()) + { + bchData.kitems_sec.append(bchObj["items_per_second"].toDouble() * 0.001); + bchData.kitems_sec_dflt = bchData.kitems_sec.back(); + bchResults.meta.hasItemsSec = true; + if (PARSE_DEBUG) qDebug() << "-> kitems_sec:" << bchData.kitems_sec_dflt; + } + + + /* + * Existing benchmark + */ + int idx = findExistingBenchmark(bchResults, bchData.run_name); + if (idx >= 0) + { + BenchData &exBchData = bchResults.benchmarks[idx]; + + /* + * Aggregate type + */ + if (bchData.run_type == "aggregate") + { + if (PARSE_DEBUG) qDebug() << "-> append aggregate:" << exBchData.name; + + // Name + QString aggregate_name; + if (bchObj.contains("aggregate_name") && bchObj["aggregate_name"].isString()) + { + aggregate_name = bchObj["aggregate_name"].toString(); + if (PARSE_DEBUG) qDebug() << "-> aggregate_name:" << aggregate_name; + } + else { + qCritical() << "Results parsing: missing benchmark field 'aggregate_name'"; + continue; + } + // Type + if (aggregate_name == "mean") { + exBchData.mean_cpu = bchData.cpu_time_us; + exBchData.mean_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + exBchData.mean_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + exBchData.mean_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "median") { + exBchData.median_cpu = bchData.cpu_time_us; + exBchData.median_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + exBchData.median_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + exBchData.median_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "stddev") { + exBchData.stddev_cpu = bchData.cpu_time_us; + exBchData.stddev_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + exBchData.stddev_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + exBchData.stddev_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "cv") { + exBchData.cv_cpu = bchData.cpu_time.back() * 100; // percent + exBchData.cv_real = bchData.real_time.back() * 100; + if ( !bchData.kbytes_sec.isEmpty() ) + exBchData.cv_kbytes = bchData.kbytes_sec_dflt * 100; + if ( !bchData.kitems_sec.isEmpty() ) + exBchData.cv_kitems = bchData.kitems_sec_dflt * 100; + bchResults.meta.hasCv = true; + } + else { + qCritical() << "Results parsing: unknown benchmark value for 'aggregate_name' ->" << aggregate_name; + continue; + } + + // New aggregate line + if (PARSE_DEBUG) qDebug() << "||"; + } + + /* + * Iteration type (from aggregate) + */ + else + { + if (PARSE_DEBUG) qDebug() << "-> append iteration:" << exBchData.name; + + // Append data + exBchData.cpu_time.append( bchData.cpu_time.back() ); + exBchData.cpu_time_us = std::min(exBchData.cpu_time_us, bchData.cpu_time_us); + + exBchData.real_time.append( bchData.real_time.back() ); + exBchData.real_time_us = std::min(exBchData.real_time_us, bchData.real_time_us); + + if ( !bchData.kbytes_sec.isEmpty() ) { + exBchData.kbytes_sec.append( bchData.kbytes_sec_dflt ); + exBchData.kbytes_sec_dflt = std::min(exBchData.kbytes_sec_dflt, bchData.kbytes_sec_dflt); + } + if ( !bchData.kitems_sec.isEmpty() ) { + exBchData.kitems_sec.append( bchData.kitems_sec_dflt ); + exBchData.kitems_sec_dflt = std::min(exBchData.kitems_sec_dflt, bchData.kitems_sec_dflt); + } + + // Min/Max + if (!exBchData.hasAggregate) //First -> init + { + exBchData.min_cpu = exBchData.cpu_time_us; + exBchData.max_cpu = std::max(exBchData.cpu_time_us, bchData.cpu_time_us); + + exBchData.min_real = exBchData.real_time_us; + exBchData.max_real = std::max(exBchData.real_time_us, bchData.real_time_us); + + if ( !bchData.kbytes_sec.isEmpty() ) { + exBchData.min_kbytes = exBchData.kbytes_sec_dflt; + exBchData.max_kbytes = std::max(exBchData.kbytes_sec_dflt, bchData.kbytes_sec_dflt); + } + if ( !bchData.kitems_sec.isEmpty() ) { + exBchData.min_kitems = exBchData.kitems_sec_dflt; + exBchData.max_kitems = std::max(exBchData.kitems_sec_dflt, bchData.kitems_sec_dflt); + } + } + else + { + if (exBchData.min_cpu > bchData.cpu_time_us) exBchData.min_cpu = bchData.cpu_time_us; + if (exBchData.max_cpu < bchData.cpu_time_us) exBchData.max_cpu = bchData.cpu_time_us; + + if (exBchData.min_real > bchData.real_time_us) exBchData.min_real = bchData.real_time_us; + if (exBchData.max_real < bchData.real_time_us) exBchData.max_real = bchData.real_time_us; + + if ( !bchData.kbytes_sec.isEmpty() ) { + if (exBchData.min_kbytes > bchData.kbytes_sec_dflt) exBchData.min_kbytes = bchData.kbytes_sec_dflt; + if (exBchData.max_kbytes < bchData.kbytes_sec_dflt) exBchData.max_kbytes = bchData.kbytes_sec_dflt; + } + if ( !bchData.kitems_sec.isEmpty() ) { + if (exBchData.min_kitems > bchData.kitems_sec_dflt) exBchData.min_kitems = bchData.kitems_sec_dflt; + if (exBchData.max_kitems < bchData.kitems_sec_dflt) exBchData.max_kitems = bchData.kitems_sec_dflt; + } + } + + // State + exBchData.hasAggregate = true; + bchResults.meta.hasAggregate = true; + bchResults.meta.onlyAggregate = false; + + // Debug + if (PARSE_DEBUG) { + qDebug() << "** exBchData.min_cpu:" << exBchData.min_cpu; + qDebug() << "** exBchData.max_cpu:" << exBchData.max_cpu; + qDebug() << "** exBchData.min_real:" << exBchData.min_real; + qDebug() << "** exBchData.max_real:" << exBchData.max_real; + if ( !exBchData.kbytes_sec.isEmpty() ) { + qDebug() << "** exBchData.min_kbytes:" << exBchData.min_kbytes; + qDebug() << "** exBchData.max_kbytes:" << exBchData.max_kbytes; + } + if ( !exBchData.kitems_sec.isEmpty() ) { + qDebug() << "** exBchData.min_kitems:" << exBchData.min_kitems; + qDebug() << "** exBchData.max_kitems:" << exBchData.max_kitems; + } + } + + // New append line + if (PARSE_DEBUG) qDebug() << "|"; + } + } + + /* + * New benchmark + */ + else + { + /* + * Aggregate-only type + */ + if (bchData.run_type == "aggregate") + { + if (PARSE_DEBUG) qDebug() << "-> new aggregate-only"; + + // Name + QString aggregate_name; + if (bchObj.contains("aggregate_name") && bchObj["aggregate_name"].isString()) + { + aggregate_name = bchObj["aggregate_name"].toString(); + if (PARSE_DEBUG) qDebug() << "-> aggregate_name:" << aggregate_name; + } + else { + qCritical() << "Results parsing: missing benchmark field 'aggregate_name'"; + continue; + } + // Type + if (aggregate_name == "mean") { + bchData.mean_cpu = bchData.cpu_time_us; + bchData.mean_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + bchData.mean_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + bchData.mean_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "median") { + bchData.median_cpu = bchData.cpu_time_us; + bchData.median_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + bchData.median_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + bchData.median_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "stddev") { + bchData.stddev_cpu = bchData.cpu_time_us; + bchData.stddev_real = bchData.real_time_us; + if ( !bchData.kbytes_sec.isEmpty() ) + bchData.stddev_kbytes = bchData.kbytes_sec_dflt; + if ( !bchData.kitems_sec.isEmpty() ) + bchData.stddev_kitems = bchData.kitems_sec_dflt; + } + else if (aggregate_name == "cv") { + bchData.cv_cpu = bchData.cpu_time.back() * 100; // percent + bchData.cv_real = bchData.real_time.back() * 100; + if ( !bchData.kbytes_sec.isEmpty() ) + bchData.cv_kbytes = bchData.kbytes_sec_dflt * 100; + if ( !bchData.kitems_sec.isEmpty() ) + bchData.cv_kitems = bchData.kitems_sec_dflt * 100; + bchResults.meta.hasCv = true; + } + else { + qCritical() << "Results parsing: unknown benchmark value for 'aggregate_name' ->" << aggregate_name; + continue; + } + + // Init + bchData.hasAggregate = true; + bchResults.meta.hasAggregate = true; + + bchData.cpu_time_us = -1; + bchData.real_time_us = -1; + bchData.min_cpu = bchData.max_cpu = -1; + bchData.min_real = bchData.max_real = -1; + } + + /* + * Add new benchmark + */ + // Arguments (extract from 'run_name') + bchData.arguments = bchData.run_name.split('/'); + QString bchName = bchData.arguments.front(); + bchData.arguments.pop_front(); + + // Debug: params + for (int prmIdx = 0; prmIdx < bchData.arguments.size(); ++prmIdx) + if (PARSE_DEBUG) qDebug() << "-> param[" << prmIdx << "]:" << bchData.arguments[prmIdx]; + + // Templates (extract from 'run_name' too) + int tpltIdx = bchName.indexOf("<"); + if (tpltIdx > 0) + { + int tpltLast = bchName.lastIndexOf(">"); + if (tpltLast != bchName.size()-1) { + qCritical() << "Bad benchmark template formatting:" << bchName; + continue; + } + QString tpltName = bchName.mid(tpltIdx+1, tpltLast-tpltIdx-1); + + // Split + int startIdx = 0; + int commaIdx = tpltName.indexOf(","); + while (commaIdx > 0) + { + QString leftString = tpltName.left(commaIdx); + int open = leftString.count('<'); + int close = leftString.count('>'); + + if (open <= close) + { + bchData.templates.append( tpltName.left(commaIdx).trimmed() ); + tpltName.remove(0, commaIdx+1); + startIdx = 0; + } + else { + startIdx = commaIdx+1; + } + commaIdx = tpltName.indexOf(",", startIdx); + } + // Last + bchData.templates.append( tpltName.trimmed() ); + + // For base name + bchName.truncate(tpltIdx); + } + // Debug: templates + for (int idx = 0; idx < bchData.templates.size(); ++idx) + if (PARSE_DEBUG) qDebug() << "-> template[" << idx << "]:" << bchData.templates[idx]; + + // Base name (i.e. name without templates/arguments) + bchData.base_name = bchName; + if (PARSE_DEBUG) qDebug() << "-> base_name:" << bchData.base_name; + + // JOMT + // Family / Container + if ( bchData.base_name.startsWith("JOMT_") ) + { + // Examples: "JOMT_Fill_vector/64" Vs "JOMT_Fill_deque/64" + bchData.base_name = bchData.base_name.remove(0,5); //remove prefix + int idx = bchData.base_name.indexOf('_'); + if (idx > 0) + { + bchData.family = bchData.base_name.left(idx); + bchData.container = bchData.base_name; + bchData.container = bchData.container.remove(0,idx+1); + } + } + // Classic (base name as family name) + else + bchData.family = bchData.base_name; + + if (PARSE_DEBUG) qDebug() << "-> family:" << bchData.family; + if (PARSE_DEBUG) qDebug() << "-> container:" << bchData.container; + + + // + // Meta + if (bchObj.contains("repetitions") && bchObj["repetitions"].isDouble()) + { + bchData.repetitions = bchObj["repetitions"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> repetitions:" << bchData.repetitions; + } + if (bchObj.contains("repetition_index") && bchObj["repetition_index"].isDouble()) + { + bchData.repetition_index = bchObj["repetition_index"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> repetition_index:" << bchData.repetition_index; + } + if (bchObj.contains("threads") && bchObj["threads"].isDouble()) + { + bchData.threads = bchObj["threads"].toInt(); + if (PARSE_DEBUG) qDebug() << "-> threads:" << bchData.threads; + } + + // + // Global Meta + if (bchData.arguments.size() > bchResults.meta.maxArguments) + bchResults.meta.maxArguments = bchData.arguments.size(); + if (bchData.templates.size() > bchResults.meta.maxTemplates) + bchResults.meta.maxTemplates = bchData.templates.size(); + bchResults.meta.onlyAggregate &= bchData.min_real < 0.; + + // + // Push new BenchData + bchResults.benchmarks.append(bchData); + + // New line between benchmarks + if (PARSE_DEBUG) qDebug() << ""; + } + } + } + else + qCritical() << "Results parsing: missing field 'benchmarks'"; + + // Debug + if (PARSE_DEBUG) { + qDebug() << "meta.maxArgs:" << bchResults.meta.maxArguments; + qDebug() << "meta.maxTemplates:" << bchResults.meta.maxTemplates; + qDebug() << "meta.hasAggregate:" << bchResults.meta.hasAggregate; + } + + + return bchResults; +} diff --git a/specifelse/benchtest/jomt/src/result_selector.cpp b/specifelse/benchtest/jomt/src/result_selector.cpp new file mode 100644 index 0000000..f6a2eff --- /dev/null +++ b/specifelse/benchtest/jomt/src/result_selector.cpp @@ -0,0 +1,1013 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "result_selector.h" +#include "ui_result_selector.h" + +#include "result_parser.h" +#include "plot_parameters.h" + +#include "plotter_linechart.h" +#include "plotter_barchart.h" +#include "plotter_boxchart.h" +#include "plotter_3dbars.h" +#include "plotter_3dsurface.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* config_file = "config_selector.json"; + + +ResultSelector::ResultSelector(QWidget *parent) + : QWidget(parent) + , ui(new Ui::ResultSelector) + , mWatcher(parent) +{ + ui->setupUi(this); + + this->setWindowTitle("JOMT"); + + ui->pushButtonAppend->setEnabled(false); + ui->pushButtonOverwrite->setEnabled(false); + ui->pushButtonReload->setEnabled(false); + ui->pushButtonSelectAll->setEnabled(false); + ui->pushButtonSelectNone->setEnabled(false); + ui->pushButtonPlot->setEnabled(false); + + connectUI(); + loadConfig(); +} + +ResultSelector::ResultSelector(const BenchResults &bchResults, const QString &fileName, QWidget *parent) + : QWidget(parent) + , ui(new Ui::ResultSelector) + , mBchResults(bchResults) + , mWatcher(parent) +{ + ui->setupUi(this); + + if ( !fileName.isEmpty() ) { + QFileInfo fileInfo(fileName); + this->setWindowTitle("JOMT - " + fileInfo.fileName()); + } + mOrigFilename = fileName; // For reload + updateResults(false); + + connectUI(); + loadConfig(); + + if (ui->checkBoxAutoReload->isChecked()) + onCheckAutoReload(Qt::Checked); +} + +ResultSelector::~ResultSelector() +{ + saveConfig(); + delete ui; +} + +// Private +void ResultSelector::connectUI() +{ + connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &ResultSelector::onItemChanged); + + connect(ui->comboBoxType, QOverload::of(&QComboBox::activated), this, &ResultSelector::onComboTypeChanged); + connect(ui->comboBoxX, QOverload::of(&QComboBox::activated), this, &ResultSelector::onComboXChanged); + connect(ui->comboBoxZ, QOverload::of(&QComboBox::activated), this, &ResultSelector::onComboZChanged); + + connect(&mWatcher, &QFileSystemWatcher::fileChanged, this, &ResultSelector::onAutoReload); + connect(ui->checkBoxAutoReload, &QCheckBox::stateChanged, this, &ResultSelector::onCheckAutoReload); + connect(ui->pushButtonReload, &QPushButton::clicked, this, &ResultSelector::onReloadClicked); + + connect(ui->pushButtonNew, &QPushButton::clicked, this, &ResultSelector::onNewClicked); + connect(ui->pushButtonAppend, &QPushButton::clicked, this, &ResultSelector::onAppendClicked); + connect(ui->pushButtonOverwrite, &QPushButton::clicked, this, &ResultSelector::onOverwriteClicked); + + connect(ui->pushButtonSelectAll, &QPushButton::clicked, this, &ResultSelector::onSelectAllClicked); + connect(ui->pushButtonSelectNone, &QPushButton::clicked, this, &ResultSelector::onSelectNoneClicked); + + connect(ui->pushButtonPlot, &QPushButton::clicked, this, &ResultSelector::onPlotClicked); +} + +void ResultSelector::loadConfig() +{ + // Load options from file + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::ReadOnly)) + { + QByteArray configData = configFile.readAll(); + configFile.close(); + QJsonDocument configDoc(QJsonDocument::fromJson(configData)); + QJsonObject json = configDoc.object(); + + // FileDialog + if (json.contains("workingDir") && json["workingDir"].isString()) + mWorkingDir = json["workingDir"].toString(); + // Actions + if (json.contains("autoReload") && json["autoReload"].isBool()) + ui->checkBoxAutoReload->setChecked( json["autoReload"].toBool() ); + } + else + { + if (configFile.exists()) + qWarning() << "Couldn't read: " << QString(config_folder) + config_file; + } + + // Default size + QSize size = this->size(); + QSize newSize = QGuiApplication::primaryScreen()->size(); + newSize *= 0.375f; + if (newSize.width() > size.width() && newSize.height() > size.height()) + this->resize(newSize.height() * 16.f/9.f, newSize.height()); +} + +void ResultSelector::saveConfig() +{ + // Save options to file + QFile configFile(QString(config_folder) + config_file); + if (configFile.open(QIODevice::WriteOnly)) + { + QJsonObject json; + + // FileDialog + json["workingDir"] = mWorkingDir; + // Actions + json["autoReload"] = ui->checkBoxAutoReload->isChecked(); + + configFile.write( QJsonDocument(json).toJson() ); + } + else + qWarning() << "Couldn't update: " << QString(config_folder) + config_file; +} + +void ResultSelector::updateComboBoxY() +{ + PlotChartType chartType = (PlotChartType)ui->comboBoxType->currentData().toInt(); + PlotValueType prevYType = (PlotValueType)-1; + if (ui->comboBoxY->count() > 0) + prevYType = (PlotValueType)ui->comboBoxY->currentData().toInt(); + + // Classic or Boxes + if (!mBchResults.meta.hasAggregate || chartType == ChartBoxType) + { + ui->comboBoxY->clear(); + + ui->comboBoxY->addItem("Real time", QVariant(RealTimeType)); + ui->comboBoxY->addItem("CPU time", QVariant(CpuTimeType)); + ui->comboBoxY->addItem("Iterations", QVariant(IterationsType)); + if (mBchResults.meta.hasBytesSec) + ui->comboBoxY->addItem("Bytes/s", QVariant(BytesType)); + if (mBchResults.meta.hasItemsSec) + ui->comboBoxY->addItem("Items/s", QVariant(ItemsType)); + } + // Aggregate + else + { + ui->comboBoxY->clear(); + + if (!mBchResults.meta.onlyAggregate) + ui->comboBoxY->addItem("Real min time", QVariant(RealTimeMinType)); + ui->comboBoxY->addItem("Real mean time", QVariant(RealTimeMeanType)); + ui->comboBoxY->addItem("Real median time", QVariant(RealTimeMedianType)); + ui->comboBoxY->addItem("Real stddev time", QVariant(RealTimeStddevType)); + if (mBchResults.meta.hasCv) + ui->comboBoxY->addItem("Real cv percent", QVariant(RealTimeCvType)); + + if (!mBchResults.meta.onlyAggregate) + ui->comboBoxY->addItem("CPU min time", QVariant(CpuTimeMinType)); + ui->comboBoxY->addItem("CPU mean time", QVariant(CpuTimeMeanType)); + ui->comboBoxY->addItem("CPU median time", QVariant(CpuTimeMedianType)); + ui->comboBoxY->addItem("CPU stddev time", QVariant(CpuTimeStddevType)); + if (mBchResults.meta.hasCv) + ui->comboBoxY->addItem("CPU cv percent", QVariant(CpuTimeCvType)); + + ui->comboBoxY->addItem("Iterations", QVariant(IterationsType)); + + if (mBchResults.meta.hasBytesSec) { + ui->comboBoxY->addItem("Bytes/s min", QVariant(BytesMinType)); + ui->comboBoxY->addItem("Bytes/s mean", QVariant(BytesMeanType)); + ui->comboBoxY->addItem("Bytes/s median", QVariant(BytesMedianType)); + ui->comboBoxY->addItem("Bytes/s stddev", QVariant(BytesStddevType)); + if (mBchResults.meta.hasCv) + ui->comboBoxY->addItem("Bytes/s cv", QVariant(BytesCvType)); + } + if (mBchResults.meta.hasItemsSec) { + ui->comboBoxY->addItem("Items/s min", QVariant(ItemsMinType)); + ui->comboBoxY->addItem("Items/s mean", QVariant(ItemsMeanType)); + ui->comboBoxY->addItem("Items/s median", QVariant(ItemsMedianType)); + ui->comboBoxY->addItem("Items/s stddev", QVariant(ItemsStddevType)); + if (mBchResults.meta.hasCv) + ui->comboBoxY->addItem("Items/s cv", QVariant(ItemsCvType)); + } + } + // Restore + int yIdx = ui->comboBoxY->findData(prevYType); + if (yIdx >= 0) + ui->comboBoxY->setCurrentIndex(yIdx); +} + +// Tree +class NumericTreeWidgetItem : public QTreeWidgetItem +{ +public: + NumericTreeWidgetItem(const QStringList &strings) + : QTreeWidgetItem(strings) {} +private: + bool operator<(const QTreeWidgetItem &other) const + { + QCollator collator; + collator.setNumericMode(true); + int column = treeWidget()->sortColumn(); + return collator.compare( text( column ), other.text( column ) ) < 0; + } +}; + +static QTreeWidgetItem* buildTreeItem(const BenchData &bchData, double timeFactor, bool onlyAggregate, QTreeWidgetItem *item = nullptr) +{ + QStringList labels = { + bchData.base_name, bchData.templates.join(", "), bchData.arguments.join("/"), + QString::number((!onlyAggregate ? bchData.real_time_us : bchData.mean_real) * timeFactor), + QString::number((!onlyAggregate ? bchData.cpu_time_us : bchData.mean_cpu) * timeFactor) + }; + if ( !bchData.kbytes_sec.isEmpty() ) + labels.append( QString::number(bchData.kbytes_sec_dflt) ); + if ( !bchData.kitems_sec.isEmpty() ) + labels.append( QString::number(bchData.kitems_sec_dflt) ); + + if (item == nullptr) { + item = new NumericTreeWidgetItem(labels); + } + else { + for (int iC=0; iCsetText(iC, labels[iC]); + } + + return item; +} + +static QSet getUnselectedBenchmarks(const QTreeWidget *tree, const BenchResults &bchResults) +{ + QSet resNames; + + for (int i=0; itopLevelItemCount(); ++i) + { + QTreeWidgetItem *topItem = tree->topLevelItem(i); + if (topItem->childCount() <= 0) + { + if (topItem->checkState(0) == Qt::Unchecked) { + int idx = topItem->data(0, Qt::UserRole).toInt(); + resNames.insert( bchResults.getBenchName(idx) ); + } + } + else + { + for (int j=0; jchildCount(); ++j) + { + QTreeWidgetItem *midItem = topItem->child(j); + if (midItem->childCount() <= 0) + { + if (midItem->checkState(0) == Qt::Unchecked) { + int idx = midItem->data(0, Qt::UserRole).toInt(); + resNames.insert( bchResults.getBenchName(idx) ); + } + } + else + { + for (int k=0; kchildCount(); ++k) + { + QTreeWidgetItem *lowItem = midItem->child(k); + if (lowItem->checkState(0) == Qt::Unchecked) { + int idx = lowItem->data(0, Qt::UserRole).toInt(); + resNames.insert( bchResults.getBenchName(idx) ); + } + } + } + } + } + } + + return resNames; +} + +void ResultSelector::updateResults(bool clear, const QSet unselected) +{ + // + // Tree widget +// QSet unselected; + if (clear) + { +// if (keepSelection) +// unselected = getUnselectedBenchmarks(ui->treeWidget, mBchResults); + ui->treeWidget->clear(); + } + else + ui->treeWidget->sortByColumn(-1, Qt::SortOrder::AscendingOrder); // init: unsorted + ui->treeWidget->setSortingEnabled(true); + + // Columns + int iCol = 5; + if (mBchResults.meta.hasBytesSec) ++iCol; + if (mBchResults.meta.hasItemsSec) ++iCol; + ui->treeWidget->setColumnCount(iCol); + + // Time unit + double timeFactor = 1.; + if ( mBchResults.meta.time_unit == "ns") timeFactor = 1000.; + else if (mBchResults.meta.time_unit == "ms") timeFactor = 0.001; + else mBchResults.meta.time_unit = "us"; + + // Populate tree + bool anySelected = false; + QList items; + + QVector bchFamilies = mBchResults.segmentFamilies(); + for (const auto &bchFamily : qAsConst(bchFamilies)) + { + bool oneTopSelected = false; + bool allTopSelected = true; + QTreeWidgetItem* topItem = new QTreeWidgetItem( QStringList(bchFamily.name) ); + + // JOMT: family + container + if ( !mBchResults.benchmarks[bchFamily.idxs[0]].container.isEmpty() ) + { + QVector bchContainers = mBchResults.segmentContainers(bchFamily.idxs); + for (const auto &bchContainer : qAsConst(bchContainers)) + { + bool oneMidSelected = false; + bool allMidSelected = true; + QTreeWidgetItem* midItem = new QTreeWidgetItem( QStringList(bchContainer.name) ); + + for (int idx : bchContainer.idxs) + { + QTreeWidgetItem *child = buildTreeItem(mBchResults.benchmarks[idx], timeFactor, mBchResults.meta.onlyAggregate); + bool selected = !unselected.contains( mBchResults.getBenchName(idx) ); + oneMidSelected |= selected; + allMidSelected &= selected; + child->setCheckState(0, selected ? Qt::Checked : Qt::Unchecked); + child->setData(0, Qt::UserRole, idx); + midItem->addChild(child); + } + oneTopSelected |= oneMidSelected; + allTopSelected &= allMidSelected; + midItem->setCheckState(0, oneMidSelected ? (allMidSelected ? Qt::Checked : Qt::PartiallyChecked) : Qt::Unchecked); + topItem->addChild(midItem); + } + } + // Classic + else + { + // Single + if (bchFamily.idxs.size() == 1) + { + int idx = bchFamily.idxs[0]; + buildTreeItem(mBchResults.benchmarks[idx], timeFactor, mBchResults.meta.onlyAggregate, topItem); + oneTopSelected = !unselected.contains( mBchResults.getBenchName(idx) ); + topItem->setData(0, Qt::UserRole, idx); + } + else // Family + { + for (int idx : bchFamily.idxs) + { + QTreeWidgetItem *child = buildTreeItem(mBchResults.benchmarks[idx], timeFactor, mBchResults.meta.onlyAggregate); + bool selected = !unselected.contains( mBchResults.getBenchName(idx) ); + oneTopSelected |= selected; + allTopSelected &= selected; + child->setCheckState(0, selected ? Qt::Checked : Qt::Unchecked); + child->setData(0, Qt::UserRole, idx); + topItem->addChild(child); + } + } + } + anySelected |= oneTopSelected; + topItem->setCheckState(0, oneTopSelected ? (allTopSelected ? Qt::Checked : Qt::PartiallyChecked) : Qt::Unchecked); + items.insert(0, topItem); + } + ui->treeWidget->insertTopLevelItems(0, items); + + ui->pushButtonPlot->setEnabled(anySelected); + + // Headers + QStringList labels = {"Benchmark", "Templates", "Arguments"}; + if (!mBchResults.meta.hasAggregate) { + labels << "Real time (" + mBchResults.meta.time_unit + ")" + << "CPU time (" + mBchResults.meta.time_unit + ")"; + if (mBchResults.meta.hasBytesSec) labels << "Bytes/s (k)"; + if (mBchResults.meta.hasItemsSec) labels << "Items/s (k)"; + } + else { + if (!mBchResults.meta.onlyAggregate) { + labels << "Real min time (" + mBchResults.meta.time_unit + ")" + << "CPU min time (" + mBchResults.meta.time_unit + ")"; + } + else { + labels << "Real mean time (" + mBchResults.meta.time_unit + ")" + << "CPU mean time (" + mBchResults.meta.time_unit + ")"; + } + if (mBchResults.meta.hasBytesSec) labels << "Bytes/s min (k)"; + if (mBchResults.meta.hasItemsSec) labels << "Items/s min (k)"; + } + + ui->treeWidget->setHeaderLabels(labels); + + ui->treeWidget->expandAll(); + for (int iC=0; iCtreeWidget->columnCount(); ++iC) + ui->treeWidget->resizeColumnToContents(iC); + + + // + // Chart options + PlotChartType prevChartType = (PlotChartType)-1; + PlotParamType prevXType = PlotEmptyType; + PlotValueType prevYType = (PlotValueType)-1; + PlotParamType prevZType = PlotEmptyType; + if (clear) + { + if (ui->comboBoxType->count() > 0) + { + prevChartType = (PlotChartType)ui->comboBoxType->currentData().toInt(); + if (ui->comboBoxX->count() > 0) + prevXType = (PlotParamType)ui->comboBoxX->currentData().toList()[0].toInt(); + if (ui->comboBoxY->count() > 0) + prevYType = (PlotValueType)ui->comboBoxY->currentData().toInt(); + if (ui->comboBoxZ->count() > 0) + prevZType = (PlotParamType)ui->comboBoxZ->currentData().toList()[0].toInt(); + } + ui->comboBoxType->clear(); + ui->comboBoxX->clear(); + ui->comboBoxY->clear(); + ui->comboBoxZ->clear(); + } + + // Type + if (mBchResults.meta.maxArguments > 0 || mBchResults.meta.maxTemplates > 0) { + ui->comboBoxType->addItem("Lines", ChartLineType); + ui->comboBoxType->addItem("Splines", ChartSplineType); + } + ui->comboBoxType->addItem("Bars", ChartBarType); + ui->comboBoxType->addItem("HBars", ChartHBarType); + if (mBchResults.meta.hasAggregate && !mBchResults.meta.onlyAggregate) + ui->comboBoxType->addItem("Boxes", ChartBoxType); + ui->comboBoxType->addItem("3D Bars", Chart3DBarsType); + if (mBchResults.meta.maxArguments > 0 || mBchResults.meta.maxTemplates > 0) + ui->comboBoxType->addItem("3D Surface", Chart3DSurfaceType); + + + // X-axis + for (int i=0; i qvList; + qvList.append(PlotArgumentType); qvList.append(i); + ui->comboBoxX->addItem("Argument " + QString::number(i+1), qvList); + } + for (int i=0; i qvList; + qvList.append(PlotTemplateType); qvList.append(i); + ui->comboBoxX->addItem("Template " + QString::number(i+1), qvList); + } + ui->comboBoxX->setEnabled(ui->comboBoxX->count() > 0); + + // Y-axis + updateComboBoxY(); + + // Z-axis + if ( !ui->comboBoxX->isEnabled() ) { + ui->comboBoxZ->setEnabled(false); + } + else + { + { + QList qvList; + qvList.append(PlotEmptyType); qvList.append(0); + ui->comboBoxZ->addItem("Auto", qvList); + } + + PlotChartType chartType = (PlotChartType)ui->comboBoxType->currentData().toInt(); + if (chartType == Chart3DBarsType || chartType == Chart3DSurfaceType) // Any 3D charts + ui->comboBoxZ->setEnabled(true); + else + ui->comboBoxZ->setEnabled(false); + + for (int i=0; i qvList; + qvList.append(PlotArgumentType); qvList.append(i); + ui->comboBoxZ->addItem("Argument " + QString::number(i+1), qvList); + } + for (int i=0; i qvList; + qvList.append(PlotTemplateType); qvList.append(i); + ui->comboBoxZ->addItem("Template " + QString::number(i+1), qvList); + } + } + + // Restore options (if possible) + if (prevChartType >= 0) + { + int chartIdx = ui->comboBoxType->findData(prevChartType); + if (chartIdx >= 0) { + ui->comboBoxType->setCurrentIndex(chartIdx); + updateComboBoxY(); + } + // X + if (prevXType >= 0) { + for (int i=0; icomboBoxX->count(); ++i) { + if ((PlotParamType)ui->comboBoxX->itemData(i).toList()[0].toInt() == prevXType) + ui->comboBoxX->setCurrentIndex(i); + } + } + // Y + int yIdx = ui->comboBoxY->findData(prevYType); + if (yIdx >= 0) + ui->comboBoxY->setCurrentIndex(yIdx); + // Z + if (prevZType >= 0) { + for (int i=0; icomboBoxZ->count(); ++i) { + if ((PlotParamType)ui->comboBoxZ->itemData(i).toList()[0].toInt() == prevZType) + ui->comboBoxZ->setCurrentIndex(i); + } + } + ui->comboBoxZ->setEnabled(ui->comboBoxX->isEnabled() + && (prevChartType == Chart3DBarsType || prevChartType == Chart3DSurfaceType)); // Any 3D charts + } + + // Reload + ui->checkBoxAutoReload->setEnabled(true); + ui->labelLastReload->setEnabled(true); + ui->pushButtonReload->setEnabled(true); + + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +// Slots +static void updateItemParentsState(QTreeWidgetItem *item) +{ + auto parent = item->parent(); + if (parent == nullptr) return; + + bool allChecked = true, allUnchecked = true; + for (int idx=0; (allChecked || allUnchecked) && idxchildCount(); ++idx) { + allChecked &= parent->child(idx)->checkState(0) == Qt::Checked; + allUnchecked &= parent->child(idx)->checkState(0) == Qt::Unchecked; + } + + if (allChecked) parent->setCheckState(0, Qt::Checked); + else if (allUnchecked) parent->setCheckState(0, Qt::Unchecked); + else parent->setCheckState(0, Qt::PartiallyChecked); +} + +static void updateItemChildrenState(QTreeWidgetItem *item) +{ + if (item->childCount() <= 0) return; + + if (item->checkState(0) == Qt::Checked) { + for (int idx=0; idxchildCount(); ++idx) { + item->child(idx)->setCheckState(0, Qt::Checked); + } + } + else if (item->checkState(0) == Qt::Unchecked) + { + for (int idx=0; idxchildCount(); ++idx) { + item->child(idx)->setCheckState(0, Qt::Unchecked); + } + } + // Nothing if 'PartiallyChecked' +} + +void ResultSelector::onItemChanged(QTreeWidgetItem *item, int /*column*/) +{ + if (item == nullptr) return; + + updateItemChildrenState(item); + updateItemParentsState(item); + + // Disable plot button if no items selected + bool allUnchecked = true; + for (int i=0; allUnchecked && itreeWidget->topLevelItemCount(); ++i) + allUnchecked &= ui->treeWidget->topLevelItem(i)->checkState(0) == Qt::Unchecked; + + ui->pushButtonPlot->setEnabled(!allUnchecked); +} + +static QVector getSelectedBenchmarks(const QTreeWidget *tree) +{ + QVector resIdxs; + + for (int i=0; itopLevelItemCount(); ++i) + { + QTreeWidgetItem *topItem = tree->topLevelItem(i); + if (topItem->childCount() <= 0) + { + if (topItem->checkState(0) == Qt::Checked) + resIdxs.append( topItem->data(0, Qt::UserRole).toInt() ); + } + else + { + for (int j=0; jchildCount(); ++j) + { + QTreeWidgetItem *midItem = topItem->child(j); + if (midItem->childCount() <= 0) + { + if (midItem->checkState(0) == Qt::Checked) + resIdxs.append( midItem->data(0, Qt::UserRole).toInt() ); + } + else + { + for (int k=0; kchildCount(); ++k) + { + QTreeWidgetItem *lowItem = midItem->child(k); + if (lowItem->checkState(0) == Qt::Checked) + resIdxs.append( lowItem->data(0, Qt::UserRole).toInt() ); + } + } + } + } + } + + return resIdxs; +} + + +void ResultSelector::onComboTypeChanged(int /*index*/) +{ + PlotChartType chartType = (PlotChartType)ui->comboBoxType->currentData().toInt(); + + if (chartType == Chart3DBarsType || chartType == Chart3DSurfaceType) // Any 3D charts + ui->comboBoxZ->setEnabled( ui->comboBoxX->isEnabled() ); + else + ui->comboBoxZ->setEnabled(false); + + if (mBchResults.meta.hasAggregate) + updateComboBoxY(); +} + +void ResultSelector::onComboXChanged(int /*index*/) +{ + PlotParamType xType = (PlotParamType)ui->comboBoxX->currentData().toList()[0].toInt(); + int xIdx = ui->comboBoxX->currentData().toList()[1].toInt(); + + PlotParamType zType = (PlotParamType)ui->comboBoxZ->currentData().toList()[0].toInt(); + int zIdx = ui->comboBoxZ->currentData().toList()[1].toInt(); + + // Change comboZ to avoid having same value + if (xType == zType && xIdx == zIdx) + { + if (ui->comboBoxZ->currentIndex() == 0) + ui->comboBoxZ->setCurrentIndex(1); + else + ui->comboBoxZ->setCurrentIndex(0); + } +} + +void ResultSelector::onComboZChanged(int /*index*/) +{ + PlotParamType xType = (PlotParamType)ui->comboBoxX->currentData().toList()[0].toInt(); + int xIdx = ui->comboBoxX->currentData().toList()[1].toInt(); + + PlotParamType zType = (PlotParamType)ui->comboBoxZ->currentData().toList()[0].toInt(); + int zIdx = ui->comboBoxZ->currentData().toList()[1].toInt(); + + // Change comboX to avoid having same value + if (zType == xType && zIdx == xIdx) + { + if (ui->comboBoxX->currentIndex() == 0) + ui->comboBoxX->setCurrentIndex(1); + else + ui->comboBoxX->setCurrentIndex(0); + } +} + +// Reload +void ResultSelector::onAutoReload(const QString &path) +{ + QFileInfo fi(path); + if (fi.exists() && fi.isReadable() && fi.size() > 0) + onReloadClicked(); + else + qWarning() << "Unable to auto-reload file: " << path; +} + +void ResultSelector::updateReloadWatchList() +{ + if (ui->checkBoxAutoReload->isChecked()) + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : qAsConst(mAddFilenames)) + mWatcher.addPath( addFilename.filename ); + } +} + +void ResultSelector::onCheckAutoReload(int state) +{ + if (state == Qt::Checked) + { + if (mWatcher.files().empty()) + { + mWatcher.addPath(mOrigFilename); + for (const auto& addFilename : qAsConst(mAddFilenames)) + mWatcher.addPath( addFilename.filename ); + } + } + else + { + if (!mWatcher.files().empty()) + mWatcher.removePaths( mWatcher.files() ); + } +} + +void ResultSelector::onReloadClicked() +{ + // Check original + if ( mOrigFilename.isEmpty() ) { + QMessageBox::warning(this, "Reload benchmark results", "No file to reload"); + return; + } + if ( !QFile::exists(mOrigFilename) ) { + QMessageBox::warning(this, "Reload benchmark results", + "File to reload does no exist:" + mOrigFilename); + return; + } + // Load original + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(mOrigFilename, errorMsg); + if (newResults.benchmarks.size() <= 0) { + QMessageBox::warning(this, "Reload benchmark results", + "Error parsing file: " + mOrigFilename + "\n" + errorMsg); + return; + } + + // Load additionnals + for (const auto &addFile : qAsConst(mAddFilenames)) + { + QString errorMsg; + BenchResults addResults = ResultParser::parseJsonFile(addFile.filename, errorMsg); + if (addResults.benchmarks.size() <= 0) { + QMessageBox::warning(this, "Reload benchmark results", + "Error parsing file: " + addFile.filename + "\n" + errorMsg); + return; + } + // Append / Overwrite + if (addFile.isAppend) + newResults.appendResults(addResults); + else + newResults.overwriteResults(addResults); + } + + // Replace & update + auto unselected = getUnselectedBenchmarks(ui->treeWidget, mBchResults); + mBchResults = newResults; + updateResults(true, unselected); + + // Update timestamp + QDateTime today = QDateTime::currentDateTime(); + QTime now = today.time(); + ui->labelLastReload->setText("(Last: " + now.toString() + ")"); +} + +// File +void ResultSelector::onNewClicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Open benchmark results"), mWorkingDir, tr("Benchmark results (*.json)")); + + if ( !fileName.isEmpty() && QFile::exists(fileName) ) + { + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(fileName, errorMsg); + if (newResults.benchmarks.size() <= 0) { + QMessageBox::warning(this, "Open benchmark results", + "Error parsing file: " + fileName + "\n" + errorMsg); + return; + } + // Replace & upate + mBchResults = newResults; + ui->treeWidget->sortByColumn(-1, Qt::SortOrder::AscendingOrder); // reset sorting + updateResults(true); + + // Update UI + ui->pushButtonAppend->setEnabled(true); + ui->pushButtonOverwrite->setEnabled(true); + ui->pushButtonReload->setEnabled(true); + ui->pushButtonSelectAll->setEnabled(true); + ui->pushButtonSelectNone->setEnabled(true); + ui->pushButtonPlot->setEnabled(true); + + // Save for reload + mOrigFilename = fileName; + mAddFilenames.clear(); + updateReloadWatchList(); + + // Window title + QFileInfo fileInfo(fileName); + this->setWindowTitle("JOMT - " + fileInfo.fileName()); + + mWorkingDir = fileInfo.absoluteDir().absolutePath(); + } +} + +void ResultSelector::onAppendClicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Append benchmark results"), mWorkingDir, tr("Benchmark results (*.json)")); + + if ( !fileName.isEmpty() && QFile::exists(fileName) ) + { + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(fileName, errorMsg); + if (newResults.benchmarks.size() <= 0) { + QMessageBox::warning(this, "Open benchmark results", + "Error parsing file: " + fileName + "\n" + errorMsg); + return; + } + // Append & upate + auto unselected = getUnselectedBenchmarks(ui->treeWidget, mBchResults); + mBchResults.appendResults(newResults); + updateResults(true, unselected); + + // Save for reload + mAddFilenames.append( {fileName, true} ); + updateReloadWatchList(); + + // Window title + if ( !this->windowTitle().endsWith(" + ...") ) + this->setWindowTitle( this->windowTitle() + " + ..." ); + + QFileInfo fileInfo(fileName); + mWorkingDir = fileInfo.absoluteDir().absolutePath(); + } +} + +void ResultSelector::onOverwriteClicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Overwrite benchmark results"), mWorkingDir, tr("Benchmark results (*.json)")); + + if ( !fileName.isEmpty() && QFile::exists(fileName) ) + { + QString errorMsg; + BenchResults newResults = ResultParser::parseJsonFile(fileName, errorMsg); + if (newResults.benchmarks.size() <= 0) { + QMessageBox::warning(this, "Open benchmark results", + "Error parsing file: " + fileName + "\n" + errorMsg); + return; + } + // Overwrite & upate + auto unselected = getUnselectedBenchmarks(ui->treeWidget, mBchResults); + mBchResults.overwriteResults(newResults); + updateResults(true, unselected); + + // Save for reload + mAddFilenames.append( {fileName, false} ); + updateReloadWatchList(); + + // Window title + if ( !this->windowTitle().endsWith(" + ...") ) + this->setWindowTitle( this->windowTitle() + " + ..." ); + + QFileInfo fileInfo(fileName); + mWorkingDir = fileInfo.absoluteDir().absolutePath(); + } +} + +// Selection +void ResultSelector::onSelectAllClicked() +{ + for (int i=0; itreeWidget->topLevelItemCount(); ++i) + { + QTreeWidgetItem *topItem = ui->treeWidget->topLevelItem(i); + topItem->setCheckState(0, Qt::Checked); + } +} + +void ResultSelector::onSelectNoneClicked() +{ + for (int i=0; itreeWidget->topLevelItemCount(); ++i) + { + QTreeWidgetItem *topItem = ui->treeWidget->topLevelItem(i); + topItem->setCheckState(0, Qt::Unchecked); + } +} + +// Plot +void ResultSelector::onPlotClicked() +{ + // Params + PlotParams plotParams; + + plotParams.type = (PlotChartType)ui->comboBoxType->currentData().toInt(); + + // Axes + if (ui->comboBoxX->currentIndex() >= 0) { + plotParams.xType = (PlotParamType)ui->comboBoxX->currentData().toList()[0].toInt(); + plotParams.xIdx = ui->comboBoxX->currentData().toList()[1].toInt(); + } + else { + plotParams.xType = PlotEmptyType; + plotParams.xIdx = -1; + } + + plotParams.yType = (PlotValueType)ui->comboBoxY->currentData().toInt(); + + if ( ui->comboBoxZ->isEnabled() && ui->comboBoxZ->currentIndex() >= 0) { + plotParams.zType = (PlotParamType)ui->comboBoxZ->currentData().toList()[0].toInt(); + plotParams.zIdx = ui->comboBoxZ->currentData().toList()[1].toInt(); + } + else { + plotParams.zType = PlotEmptyType; + plotParams.zIdx = -1; + } + + // Selected items + const auto &bchIdxs = getSelectedBenchmarks(ui->treeWidget); + + // + // Call plotter + bool is3D = false; + QWidget* widget = nullptr; + switch (plotParams.type) + { + case ChartLineType: + case ChartSplineType: + { + widget = new PlotterLineChart(mBchResults, bchIdxs, + plotParams, mOrigFilename, mAddFilenames); + break; + } + case ChartBarType: + case ChartHBarType: + { + widget = new PlotterBarChart(mBchResults, bchIdxs, + plotParams, mOrigFilename, mAddFilenames); + break; + } + case ChartBoxType: + { + widget = new PlotterBoxChart(mBchResults, bchIdxs, + plotParams, mOrigFilename, mAddFilenames); + break; + } + case Chart3DBarsType: + { + widget = new Plotter3DBars(mBchResults, bchIdxs, + plotParams, mOrigFilename, mAddFilenames); + is3D = true; + break; + } + case Chart3DSurfaceType: + { + widget = new Plotter3DSurface(mBchResults, bchIdxs, + plotParams, mOrigFilename, mAddFilenames); + is3D = true; + break; + } + } + + if (widget) + { + // Default size + QSize newSize = widget->size(); + QSize screenSize = QGuiApplication::primaryScreen()->size(); + float scale = screenSize.height() * 0.375f / newSize.height(); + float ratio = 16.f/9.f; + float h3DScale = 1.15f; + if (scale > 1.f) { + newSize *= scale; + ratio = 2.3f; + h3DScale = 1.5f; + } + newSize.setWidth(newSize.height() * ratio); + if (is3D) + newSize.setHeight(newSize.height() * h3DScale); + widget->resize(newSize.width(), newSize.height()); + + widget->show(); + } + else + qWarning() << "Unable to instantiate plot widget"; +} diff --git a/specifelse/benchtest/jomt/src/series_dialog.cpp b/specifelse/benchtest/jomt/src/series_dialog.cpp new file mode 100644 index 0000000..b5ab62f --- /dev/null +++ b/specifelse/benchtest/jomt/src/series_dialog.cpp @@ -0,0 +1,141 @@ +// Copyright 2019 Guillaume AUJAY. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "series_dialog.h" +#include "ui_series_dialog.h" + +#include +#include +#include +#include +#include +#include +#include + + +class FieldWidget : public QWidget +{ +public: + QLineEdit nameEdit; + QPushButton colorButton; + QColor colorValue; + + FieldWidget(const QString& name, const QColor& color, QWidget *parent) + : QWidget(parent) + , nameEdit(name, this) + , colorButton(this) + , colorValue(color) + { + // Name + nameEdit.setCursorPosition(0); + if (nameEdit.text().isEmpty()) + nameEdit.setEnabled(false); + + // Color + colorButton.setStyleSheet( "QPushButton { background-color: " + colorValue.name() + "; }" + + "QPushButton:hover:!pressed { border-style: inset; border-width: 3px; }" ); + colorButton.setMinimumHeight( std::max(20, nameEdit.height()) ); + colorButton.setMinimumWidth(colorButton.minimumHeight() * 1.5); + colorButton.setFixedSize(colorButton.minimumWidth(), colorButton.minimumHeight()); + colorButton.setToolTip("Change color"); + + // Connect + connect(&colorButton, &QPushButton::clicked, this, &FieldWidget::onColorClicked); + + // Layout + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(&nameEdit); + layout->addWidget(&colorButton); + layout->setContentsMargins(0,0,0,0); + setLayout(layout); + } + +public slots: + void onColorClicked() + { + QColor newColor = QColorDialog::getColor(colorValue, this, nameEdit.text()); + if (newColor.isValid() && newColor != colorValue) + { + colorValue = newColor; + colorButton.setStyleSheet("QPushButton { background-color: " + colorValue.name() + "; }"); + } + } +}; + + +SeriesDialog::SeriesDialog(const SeriesMapping &mapping, QWidget *parent) + : QDialog(parent) + , ui(new Ui::SeriesDialog) + , mMapping(mapping) +{ + ui->setupUi(this); + this->setWindowTitle( "Edit series" ); + + // Setup form + ui->formLayout->addRow("Original:", new QLabel("Modified:", this)); + + for (const auto& config : qAsConst(mMapping)) { + ui->formLayout->addRow(config.oldName.isEmpty() ? "" : config.oldName, + new FieldWidget(config.newName, config.newColor, this)); + } + + // Connect + connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, &SeriesDialog::onRestoreClicked); + + // Default size + QSize size = this->size(); + QSize newSize = QGuiApplication::primaryScreen()->size(); + newSize *= 0.25f; + if (newSize.width() > size.width()) + resize(newSize.width(), size.height()); +} + +SeriesDialog::~SeriesDialog() +{ + delete ui; +} + + +void SeriesDialog::accept() +{ + // Save edited + for (int idx = 0; idx < mMapping.size(); ++idx) + { + auto item = ui->formLayout->itemAt(idx + 1, QFormLayout::FieldRole); + auto fieldWidget = dynamic_cast(item->widget()); + + if (!fieldWidget->nameEdit.text().isEmpty()) { + mMapping[idx].newName = fieldWidget->nameEdit.text(); + } + mMapping[idx].newColor = fieldWidget->colorValue; + } + + QDialog::accept(); +} + +void SeriesDialog::onRestoreClicked() +{ + for (int idx = 0; idx < mMapping.size(); ++idx) + { + auto item = ui->formLayout->itemAt(idx + 1, QFormLayout::FieldRole); + auto fieldWidget = dynamic_cast(item->widget()); + + fieldWidget->nameEdit.setText( mMapping[idx].oldName ); + fieldWidget->nameEdit.setCursorPosition(0); + fieldWidget->colorValue = mMapping[idx].oldColor; + fieldWidget->colorButton.setStyleSheet( "QPushButton { background-color: " + fieldWidget->colorValue.name() + "; }" + + "QPushButton:hover:!pressed { border-style: inset; border-width: 3px; }" ); + } +} diff --git a/specifelse/benchtest/jomt/src/ui/mainwindow.ui b/specifelse/benchtest/jomt/src/ui/mainwindow.ui new file mode 100644 index 0000000..b232854 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/mainwindow.ui @@ -0,0 +1,22 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/plotter_3dbars.ui b/specifelse/benchtest/jomt/src/ui/plotter_3dbars.ui new file mode 100644 index 0000000..7514c21 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/plotter_3dbars.ui @@ -0,0 +1,579 @@ + + + Plotter3DBars + + + + 0 + 0 + 1080 + 655 + + + + 3D Bars + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 9 + + + + + + + + + + Theme + + + + + + + + + + + + Bars + + + + + + + + + + + + + Thick: + + + + + + + + 60 + 16777215 + + + + 0.010000000000000 + + + 10.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + + + Floor: + + + + + + + + 60 + 16777215 + + + + 999999.000000000000000 + + + 0.100000000000000 + + + + + + + + + + + + + Spacing: + + + + + + + 0.000000000000000 + + + 2.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + 0.000000000000000 + + + 2.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + + + Series: + + + + + + + Edit + + + + + + + + + + + Time unit: + + + + + + + + + + + + + + + Axes + + + + + + + + + + + true + + + Rotate + + + false + + + + + + + Title + + + true + + + + + + + + + + + false + + + Log + + + + + + + Log base: + + + + + + + false + + + 2 + + + 1000 + + + 10 + + + + + + + + + + + Title: + + + + + + + + + false + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + + + + false + + + 1 + + + 1 + + + + + + + false + + + 1 + + + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + + Range: + + + + + + + + + true + + + Format: + + + + + + + + + true + + + Ticks: + + + + + + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + + + + Auto-reload + + + true + + + + + + + (Last: ) + + + + + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Reload + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Snapshot + + + + + + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/plotter_3dsurface.ui b/specifelse/benchtest/jomt/src/ui/plotter_3dsurface.ui new file mode 100644 index 0000000..5565f44 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/plotter_3dsurface.ui @@ -0,0 +1,443 @@ + + + Plotter3DSurface + + + + 0 + 0 + 1080 + 607 + + + + 3D Surface + + + + + + + + + 16777215 + 16777215 + + + + + 9 + + + + + + + + + + Theme + + + + + + + + + + Surface + + + + + + + + Flip horizontal grid + + + + + + + + + + + + + + Series: + + + + + + + Edit + + + + + + + + + + + Time unit: + + + + + + + + + + + + + + + Axes + + + + + + + + + + + true + + + Rotate + + + false + + + + + + + Title + + + true + + + + + + + + + + + true + + + Log + + + + + + + Log base: + + + + + + + true + + + 2 + + + 1000 + + + 10 + + + + + + + + + + + Title: + + + + + + + + + true + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + + + + true + + + 1 + + + 1 + + + + + + + true + + + 1 + + + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 3 + + + 9999999999.000000000000000 + + + 1.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 3 + + + 9999999999.000000000000000 + + + 1.000000000000000 + + + + + + + + + Range: + + + + + + + + + true + + + Format: + + + + + + + + + true + + + Ticks: + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Auto-reload + + + true + + + + + + + (Last: ) + + + + + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Reload + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Snapshot + + + + + + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/plotter_barchart.ui b/specifelse/benchtest/jomt/src/ui/plotter_barchart.ui new file mode 100644 index 0000000..7427b83 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/plotter_barchart.ui @@ -0,0 +1,552 @@ + + + PlotterBarChart + + + + 0 + 0 + 1080 + 682 + + + + Form + + + + + + + + + 9 + + + + + + + + + + Theme + + + + + + + + + + + + Legend + + + + + + + + Visible + + + true + + + + + + + + + + + + + + Font size: + + + + + + + + + + + + + + Series: + + + + + + + Edit + + + + + + + + + + + Time unit: + + + + + + + + + + + + + + + Axes + + + + + + + + + + + Visible + + + true + + + + + + + Title + + + true + + + + + + + + + + + false + + + Log + + + + + + + Log base: + + + + + + + false + + + 2 + + + 1000 + + + 10 + + + + + + + + + + + 1 + + + 72 + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + 1 + + + 72 + + + + + + + + + false + + + 2 + + + + + + + false + + + + + + + + + Title: + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + + + + true + + + Format: + + + + + + + Bar value: + + + + + + + + + Label size: + + + + + + + true + + + Ticks: + + + + + + + Range: + + + + + + + Title size: + + + + + + + + + true + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Auto-reload + + + true + + + + + + + (Last: ) + + + + + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Reload + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Snapshot + + + + + + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/plotter_boxchart.ui b/specifelse/benchtest/jomt/src/ui/plotter_boxchart.ui new file mode 100644 index 0000000..0217b41 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/plotter_boxchart.ui @@ -0,0 +1,513 @@ + + + PlotterBoxChart + + + + 0 + 0 + 1080 + 682 + + + + Form + + + + + + + + + 9 + + + + + + + + + + Theme + + + + + + + + + + + + Legend + + + + + + + + Visible + + + true + + + + + + + + + + + + + + Font size: + + + + + + + + + + + + + + Series: + + + + + + + Edit + + + + + + + + + + + Time unit: + + + + + + + + + + + + + + + Axes + + + + + + + + + + + Visible + + + true + + + + + + + Title + + + true + + + + + + + + + + + false + + + Log + + + + + + + Log base: + + + + + + + false + + + 2 + + + 1000 + + + 10 + + + + + + + + + + + 1 + + + 72 + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + 1 + + + 72 + + + + + + + + + false + + + 2 + + + + + + + false + + + + + + + + + Title: + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + 6 + + + 9999999999.000000000000000 + + + 0.100000000000000 + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + + + + + + + + + true + + + Format: + + + + + + + + + Label size: + + + + + + + true + + + Ticks: + + + + + + + Range: + + + + + + + Title size: + + + + + + + + + false + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Auto-reload + + + true + + + + + + + (Last: ) + + + + + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Reload + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Snapshot + + + + + + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/plotter_linechart.ui b/specifelse/benchtest/jomt/src/ui/plotter_linechart.ui new file mode 100644 index 0000000..5ba7546 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/plotter_linechart.ui @@ -0,0 +1,455 @@ + + + PlotterLineChart + + + + 0 + 0 + 1080 + 680 + + + + LineChart + + + + + + + + + 9 + + + + + + + + + + Theme + + + + + + + + + + + + Legend + + + + + + + + Visible + + + true + + + + + + + + + + + + + + Font size: + + + + + + + 1 + + + 72 + + + + + + + + + + + Series: + + + + + + + Edit + + + + + + + + + + + Time unit: + + + + + + + + + + + + + + + Axes + + + + + + + + + + + Visible + + + true + + + + + + + Title + + + true + + + + + + + + + + + Log + + + + + + + Log base: + + + + + + + false + + + 2 + + + 1000 + + + 10 + + + + + + + + + + + Ticks: + + + + + + + + + 2 + + + + + + + + + + + + 1 + + + 72 + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + Title: + + + + + + + Title size: + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 58 + 16777215 + + + + 3 + + + 9999999999.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 58 + 16777215 + + + + 3 + + + 9999999999.000000000000000 + + + + + + + + + Format: + + + + + + + Range: + + + + + + + + 0 + 0 + + + + + 116 + 16777215 + + + + + + + + Label size: + + + + + + + 1 + + + 72 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Auto-reload + + + true + + + + + + + (Last: ) + + + + + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Reload + + + + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + Snapshot + + + + + + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/result_selector.ui b/specifelse/benchtest/jomt/src/ui/result_selector.ui new file mode 100644 index 0000000..6ad7e69 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/result_selector.ui @@ -0,0 +1,348 @@ + + + ResultSelector + + + + 0 + 0 + 940 + 512 + + + + + 9 + + + + Benchmark results + + + + + + + + + + + 1 + + + + + + + + + + + 9 + 50 + false + + + + false + + + Result files + + + false + + + false + + + + + + New... + + + + + + + Append... + + + + + + + Overwrite... + + + + + + + + + + + 9 + + + + Selection + + + false + + + + + + All + + + + + + + None + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 140 + 0 + + + + + 9 + 50 + false + + + + Chart + + + false + + + + + + + 9 + 50 + false + + + + Type + + + + + + + + 9 + 50 + false + + + + + + + + Qt::Horizontal + + + + + + + + 9 + 50 + false + + + + X + + + + + + + + 9 + 50 + false + + + + + + + + + 9 + 50 + false + + + + Y + + + + + + + + 9 + 50 + false + + + + + + + + + 9 + 50 + false + + + + Z + + + + + + + + 9 + 50 + false + + + + + + + + + + + State + + + + + + false + + + Auto-reload + + + true + + + + + + + false + + + (Last: ) + + + + + + + false + + + Reload + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 50 + + + + + 9 + 50 + false + + + + Plot + + + + + + + + + + + + diff --git a/specifelse/benchtest/jomt/src/ui/series_dialog.ui b/specifelse/benchtest/jomt/src/ui/series_dialog.ui new file mode 100644 index 0000000..4997af1 --- /dev/null +++ b/specifelse/benchtest/jomt/src/ui/series_dialog.ui @@ -0,0 +1,83 @@ + + + SeriesDialog + + + Qt::WindowModal + + + + 0 + 0 + 564 + 304 + + + + + 9 + + + + Dialog + + + true + + + + + + + + + + 9 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + buttonBox + accepted() + SeriesDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SeriesDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/specifelse/slides.pptx b/specifelse/slides.pptx index 8b4883b..23ad163 100644 Binary files a/specifelse/slides.pptx and b/specifelse/slides.pptx differ