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