├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs ├── org.eclipse.jdt.core.prefs └── org.eclipse.m2e.core.prefs ├── EclipseCodeStyle.xml ├── LICENSE ├── NOTICE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── net │ │ └── tuis │ │ └── ubench │ │ ├── InlineFormatter.java │ │ ├── InterleavedExecutionModel.java │ │ ├── NonClosingSystemOut.java │ │ ├── ParallelExecutionModel.java │ │ ├── ScaleControl.java │ │ ├── ScaleDetect.java │ │ ├── SequentialExecutionModel.java │ │ ├── StdoutHandler.java │ │ ├── Task.java │ │ ├── TaskExecutionModel.java │ │ ├── TaskRunner.java │ │ ├── UBench.java │ │ ├── UBenchRuntimeException.java │ │ ├── UMode.java │ │ ├── UReport.java │ │ ├── UScale.java │ │ ├── UStats.java │ │ ├── UUtils.java │ │ └── scale │ │ ├── MathEquation.java │ │ ├── MathModel.java │ │ └── Models.java └── resources │ └── net │ └── tuis │ └── ubench │ └── scale │ └── UScale.html └── test └── java └── net └── tuis └── ubench ├── AnalyzeTest.java ├── DataRandomizer.java ├── ExampleCode.java ├── ExampleScales.java ├── PrimeSieve.java ├── RandomSorter.java ├── ScaleTest.java ├── TestTaskStats.java └── TestUBench.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | /ebuild/ 15 | /target/ 16 | 17 | 18 | 19 | ### Intellij ### 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 21 | 22 | *.iml 23 | 24 | ## Directory-based project format: 25 | .idea/ 26 | # if you remove the above rule, at least ignore the following: 27 | 28 | # User-specific stuff: 29 | # .idea/workspace.xml 30 | # .idea/tasks.xml 31 | # .idea/dictionaries 32 | 33 | # Sensitive or high-churn files: 34 | # .idea/dataSources.ids 35 | # .idea/dataSources.xml 36 | # .idea/sqlDataSources.xml 37 | # .idea/dynamic.xml 38 | # .idea/uiDesigner.xml 39 | 40 | # Gradle: 41 | # .idea/gradle.xml 42 | # .idea/libraries 43 | 44 | # Mongo Explorer plugin: 45 | # .idea/mongoSettings.xml 46 | 47 | ## File-based project format: 48 | *.ipr 49 | *.iws 50 | 51 | ## Plugin-specific files: 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | /output/ 67 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | MicroBench 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/main/java=UTF-8 3 | encoding//src/test/java=UTF-8 4 | encoding/=UTF-8 5 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 4 | org.eclipse.jdt.core.compiler.compliance=1.8 5 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 6 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 7 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 8 | org.eclipse.jdt.core.compiler.source=1.8 9 | -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ================================================================= 2 | == NOTICE file for use with the Apache License, Version 2.0, == 3 | ================================================================= 4 | 5 | MicroBench (UBench) Java 8 tool 6 | Copyright 2015 Rolf Lear 7 | 8 | The original software is available from https://github.com/rolfl/MicroBench 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroBench 2 | Enabling simpler microbenchmarks of Java8 and more traditional code. 3 | 4 | Often, when developing performance sensitive code, it is convenient to benchmark that code in order to evaluate and improve its performance. UBench is a tool that can help. 5 | 6 | ##Maven 7 | 8 | Maven dependency is available on Maven Central: 9 | 10 | 11 | net.tuis.ubench 12 | ubench 13 | 0.2.0 14 | 15 | 16 | ##Example Use Case - Largest Prime Calculator 17 | 18 | See the running code in [the test section - net.tuis.ubench.PrimeSieve](https://github.com/rolfl/MicroBench/blob/master/src/test/java/net/tuis/ubench/PrimeSieve.java) 19 | 20 | Consider a function which returns the largest prime number less than a given input value. This is based off a [simple "Sieve of Eratosthenes"](http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes): 21 | 22 | /** 23 | * Basic "Sieve of Eratosthenes" implementation, no optimizations 24 | * @param limit only primes less than this limit will be calculated. 25 | * @return the largest prime less than the supplied limit. 26 | */ 27 | public static final int getMaxPrimeBefore(int limit) { 28 | boolean[] sieve = new boolean[limit]; 29 | Arrays.fill(sieve, true); 30 | sieve[0] = false; 31 | sieve[1] = false; 32 | int largest = 0; 33 | for (int p = 2; p < limit; p++) { 34 | if (sieve[p]) { 35 | largest = p; 36 | for (int np = p * 2; np < limit; np += p) { 37 | sieve[np] = false; 38 | } 39 | } 40 | } 41 | return largest; 42 | } 43 | 44 | What can UBench do for us that is useful? 45 | 46 | ##Simple Performance 47 | 48 | First up, how fast is it? What happens to the performance as the Java JIT compiler optimizes the code. What can we expect when the system is 'cold'? 49 | 50 | Simply run the code in a UBench task: 51 | 52 | new UBench("Simple Performance") 53 | .addIntTask("Prime less than 4000", () -> getMaxPrimeBefore(4000), p -> p == 3989) 54 | .press(10000) 55 | .report(); 56 | 57 | This will produce (on my machine) the results: 58 | 59 | Task Simple Performance -> Prime less than 4000: (Unit: MICROSECONDS) 60 | Count : 10000 Average : 17.4710 61 | Fastest : 13.0260 Slowest : 1107.2170 62 | 95Pctile : 23.6840 99Pctile : 42.6310 63 | TimeBlock : 25.102 16.466 16.442 16.549 16.401 18.256 15.208 17.560 16.564 16.166 64 | Histogram : 9717 186 61 26 8 1 1 65 | 66 | The above results can be interpreted in multiple ways. The TimeBlock and Histogram values are the ones which will be least obvious, though. 67 | 68 | ###TimeBlock 69 | 70 | This is a break down of the run time in to 10 zones. In the above example, with 10,000 runs, the average of the first 1000 runs is 25.102 microseconds. The next 1000 runs averages at 16.466 microseconds, and so on. You can see that the performance is about constant after the first 1000 runs. 71 | 72 | ###Histogram 73 | 74 | This value set counts the number of runs that fall within specific time buckets. Each bucket is twice as slow as the previous bucket. The first bucket is the reference, and is based on the fastest run in the results. In the exampe above, 9717 runs were between 1X and 2X as slow as the fastest run (13.0260 microseconds). 186 runs were between 2X and 4X as slow as the fastest run. 61 runs were between 4X and 8X slower than the fastest, and so on. 75 | 76 | The number of buckets is directly related to the number of times slower the slowest run is compared to the fastest run. In the example above, the slowest run is 85 times slower than the fastest run, so it is in bucket 64-to-128 times slower. 77 | 78 | ##Comparative Algorithms 79 | 80 | How about comparing a revised implementation? One which does not do the ```Arrays.fill(...)``` operation, but instead negates all values? The Arrays.fill is an operation that can be avoided if the logic using the boolean values in the sieve is negated. Consider code like: 81 | 82 | public static final int getMaxPrimeBeforeNeg(int limit) { 83 | boolean[] sieve = new boolean[limit]; 84 | // Arrays.fill(sieve, true); 85 | sieve[0] = true; 86 | sieve[1] = true; 87 | int largest = 0; 88 | for (int p = 2; p < limit; p++) { 89 | if (!sieve[p]) { 90 | largest = p; 91 | for (int np = p * 2; np < limit; np += p) { 92 | sieve[np] = true; 93 | } 94 | } 95 | } 96 | return largest; 97 | } 98 | 99 | Note that the code is the same as the initial example, but the method name is different, it does not do the Arrays.fill, and it has a negated check of the boolean value, and it sets values to true, instead of setting them to false. How much will the removal of the fill improve the performance? 100 | 101 | new UBench("Comparative Performance") 102 | .addIntTask("Primes Filled", () -> getMaxPrimeBefore(4000), p -> p == 3989) 103 | .addIntTask("Primes Negated", () -> getMaxPrimeBeforeNeg(4000), p -> p == 3989) 104 | .press(10000) 105 | .report("Effects of Arrays.fill()"); 106 | 107 | This produces the output: 108 | 109 | Effects of Arrays.fill() 110 | ======================== 111 | 112 | Task Comparative Performance -> Primes Filled: (Unit: MICROSECONDS) 113 | Count : 10000 Average : 15.9820 114 | Fastest : 14.2100 Slowest : 1458.1320 115 | 95Pctile : 18.5530 99Pctile : 25.2630 116 | TimeBlock : 15.529 17.707 15.695 15.539 15.944 17.046 15.855 15.546 15.447 15.518 117 | Histogram : 9931 53 9 4 1 1 1 118 | 119 | Task Comparative Performance -> Primes Negated: (Unit: MICROSECONDS) 120 | Count : 10000 Average : 15.0480 121 | Fastest : 13.4200 Slowest : 1403.2650 122 | 95Pctile : 16.9740 99Pctile : 24.4740 123 | TimeBlock : 14.417 15.140 14.637 14.483 14.823 15.335 16.488 14.954 15.041 15.168 124 | Histogram : 9938 45 11 4 1 0 1 125 | 126 | As you can see, the negated process is consistently about 1 microsecond faster, or, 8% in this case. 127 | 128 | ##Scalability testing 129 | 130 | How does the performance change with different values for the limit? Let's check it doubling input values. As we double the input, what happens to the performance? 131 | 132 | UBench bench = new UBench("Sieve Scalability"); 133 | 134 | int[] limits = {250, 500, 1000, 2000, 4000, 8000}; 135 | int[] primes = {241, 499, 997, 1999, 3989, 7993}; 136 | 137 | for (int i = 0; i < limits.length; i++) { 138 | final int limit = limits[i]; 139 | final int check = primes[i]; 140 | bench.addIntTask("Primes " + limit, () -> getMaxPrimeBefore(limit), p -> p == check); 141 | } 142 | 143 | bench.press(UMode.SEQUENTIAL, 10000).report("Prime Scalability"); 144 | 145 | We run the code and get the results: 146 | 147 | ``` 148 | Prime Scalability 149 | ================= 150 | 151 | Task Sieve Scalability -> Primes 250: (Unit: MICROSECONDS) 152 | Count : 10000 Average : 0.8840 153 | Fastest : 0.3940 Slowest : 1605.3660 154 | 95Pctile : 0.7900 99Pctile : 0.7900 155 | TimeBlock : 2.409 0.753 0.699 0.701 0.703 0.844 0.702 0.699 0.690 0.639 156 | Histogram : 2261 7712 14 5 3 2 1 1 0 0 0 1 157 | 158 | Task Sieve Scalability -> Primes 500: (Unit: MICROSECONDS) 159 | Count : 10000 Average : 1.3500 160 | Fastest : 1.1840 Slowest : 70.6570 161 | 95Pctile : 1.5790 99Pctile : 1.5790 162 | TimeBlock : 1.341 1.373 1.416 1.335 1.335 1.332 1.334 1.332 1.371 1.333 163 | Histogram : 9979 18 0 0 2 1 164 | 165 | Task Sieve Scalability -> Primes 1000: (Unit: MICROSECONDS) 166 | Count : 10000 Average : 3.1920 167 | Fastest : 2.3680 Slowest : 61.5780 168 | 95Pctile : 5.1310 99Pctile : 5.1320 169 | TimeBlock : 2.888 2.834 2.871 3.207 3.396 2.969 2.857 2.875 3.085 4.946 170 | Histogram : 8691 1302 3 3 1 171 | 172 | Task Sieve Scalability -> Primes 2000: (Unit: MICROSECONDS) 173 | Count : 10000 Average : 6.2780 174 | Fastest : 5.5260 Slowest : 1601.4190 175 | 95Pctile : 6.7100 99Pctile : 8.6840 176 | TimeBlock : 6.067 6.086 6.372 6.035 6.445 6.038 6.041 7.616 6.038 6.048 177 | Histogram : 9984 14 1 0 0 0 0 0 1 178 | 179 | Task Sieve Scalability -> Primes 4000: (Unit: MICROSECONDS) 180 | Count : 10000 Average : 13.3520 181 | Fastest : 11.8410 Slowest : 1562.7350 182 | 95Pctile : 14.6050 99Pctile : 18.5520 183 | TimeBlock : 13.395 13.266 13.143 13.347 13.207 13.130 14.799 13.314 13.124 12.798 184 | Histogram : 9979 17 3 0 0 0 0 1 185 | 186 | Task Sieve Scalability -> Primes 8000: (Unit: MICROSECONDS) 187 | Count : 10000 Average : 33.0100 188 | Fastest : 26.0520 Slowest : 35412.0070 189 | 95Pctile : 41.0520 99Pctile : 42.6300 190 | TimeBlock : 28.020 28.138 28.887 27.991 28.095 29.253 71.221 30.652 28.668 29.184 191 | Histogram : 9951 35 10 0 2 1 0 0 0 0 1 192 | 193 | ``` 194 | 195 | The results show a slightly more than linear progression of the various times, which is what would be expected for an algorithm that's reported to be ``O( n log( log( n ) ) )`` 196 | 197 | ##Profiling 198 | 199 | Perhaps you just want to profile the code, and get deeper insights. You can just set large values of iterations for the tests, or simply run the code for a given length of time: 200 | 201 | bench.press("For Profiling", 1, TimeUnit.DAYS); 202 | 203 | then kill the job when your profiling completes. Your profiles will show some overhead from the UBench itself, but the small footprint in terms of code and stack, makes UBench a convenient platform to use. 204 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | net.tuis.ubench 4 | ubench 5 | 0.2.1-SNAPSHOT 6 | MicroBench 7 | Tool for allowing microbenchmarking of code using Java 8 mnemonics 8 | 9 | src/main/java 10 | src/test/java 11 | 12 | 13 | maven-compiler-plugin 14 | 3.1 15 | 16 | 1.8 17 | 1.8 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-javadoc-plugin 23 | 24 | 25 | attach-javadocs 26 | 27 | jar 28 | 29 | 30 | 31 | 32 | 33 | maven-source-plugin 34 | 35 | 36 | attach-sources 37 | 38 | jar 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-gpg-plugin 46 | 47 | 48 | sign-artifacts 49 | verify 50 | 51 | sign 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-enforcer-plugin 59 | 1.4 60 | 61 | 62 | enforce-maven 63 | validate 64 | 65 | enforce 66 | 67 | 68 | 69 | 70 | Maven 2.6 required to manage Java8 Javadoc links correctly - points to Java6 api docs otherwise 71 | 2.6 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | junit 83 | junit 84 | 4.12 85 | test 86 | 87 | 88 | org.apache.commons 89 | commons-math3 90 | 3.4.1 91 | 92 | 93 | 94 | https://github.com/rolfl/MicroBench 95 | 96 | Rolf Lear 97 | https://github.com/rolfl 98 | 99 | 100 | 101 | 102 | rolfl 103 | Rolf Lear 104 | jdom@tuis.net 105 | 106 | 107 | 108 | 109 | 110 | 111 | https://github.com/rolfl/MicroBench 112 | scm:git:git@github.com:rolfl/MicroBench.git 113 | scm:git:git@github.com:rolfl/MicroBench.git 114 | 115 | 116 | 117 | https://github.com/rolfl/MicroBench/issues 118 | 119 | 120 | 1.8 121 | UTF-8 122 | 123 | 124 | 125 | 126 | sonatypeoss 127 | Maven Central 128 | https://oss.sonatype.org/service/local/staging/deploy/maven2 129 | 130 | 131 | sonatypeoss 132 | Maven Central 133 | https://oss.sonatype.org/content/repositories/snapshots/ 134 | 135 | 136 | 137 | 138 | 139 | Apache 2.0 140 | https://raw.github.com/rolfl/microbench/master/LICENSE 141 | repo 142 | 349 | 350 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/InlineFormatter.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.io.CharArrayWriter; 4 | import java.io.PrintWriter; 5 | import java.util.logging.Formatter; 6 | import java.util.logging.LogRecord; 7 | 8 | class InlineFormatter extends Formatter { 9 | 10 | private static final ThreadLocal chars = ThreadLocal.withInitial(CharArrayWriter::new); 11 | 12 | private static final class TraceWriter extends PrintWriter { 13 | public TraceWriter() { 14 | super(chars.get()); 15 | } 16 | } 17 | 18 | private static final ThreadLocal writers = ThreadLocal.withInitial(TraceWriter::new); 19 | 20 | @Override 21 | public String format(LogRecord log) { 22 | String msg = formatMessage(log); 23 | return String.format("%-6s %tF % { 27 | private final int index; 28 | private final TaskRunner runner; 29 | private final AtomicBoolean terminated; 30 | 31 | public Combiner(int index, TaskRunner runner, AtomicBoolean terminated) { 32 | super(); 33 | this.index = index; 34 | this.runner = runner; 35 | this.terminated = terminated; 36 | } 37 | 38 | @Override 39 | public Combiner call() throws Exception { 40 | boolean done = false; 41 | int loops = 0; 42 | do { 43 | loops++; 44 | if (loops % 1000 == 0) { 45 | // check each 1000 loops. 46 | if (terminated.get()) { 47 | return this; 48 | } 49 | } 50 | done = runner.invoke(); 51 | } while (!done); 52 | return this; 53 | } 54 | 55 | } 56 | 57 | @Override 58 | public final Thread newThread(Runnable r) { 59 | Thread t = new Thread(r, "Parallel Model " + threadId.incrementAndGet()); 60 | t.setDaemon(true); 61 | return t; 62 | } 63 | 64 | @Override 65 | public UStats[] executeTasks(String suite, TaskRunner...tasks) { 66 | 67 | UStats[] results = new UStats[tasks.length]; 68 | ExecutorService service = Executors.newFixedThreadPool(tasks.length, this); 69 | CompletionService completion = new ExecutorCompletionService<>(service); 70 | 71 | final AtomicBoolean terminator = new AtomicBoolean(false); 72 | try { 73 | for (int i = 0; i < tasks.length; i++) { 74 | completion.submit(new Combiner(i, tasks[i], terminator)); 75 | } 76 | for (int i = 0; i < tasks.length; i++) { 77 | Future fc = completion.take(); 78 | Combiner c = fc.get(); 79 | results[c.index] = c.runner.collect(suite); 80 | } 81 | return results; 82 | } catch (InterruptedException e) { 83 | e.printStackTrace(); 84 | Thread.currentThread().interrupt(); 85 | throw new UBenchRuntimeException("Parallel Execution interrupted. See cause.", e); 86 | } catch (ExecutionException e) { 87 | Throwable cause = e.getCause(); 88 | if (cause instanceof UBenchRuntimeException) { 89 | throw (UBenchRuntimeException) cause; 90 | } 91 | throw new UBenchRuntimeException("Parallel Execution failed. See cause.", cause); 92 | } finally { 93 | terminator.set(true); 94 | service.shutdown(); 95 | if (!service.isTerminated()) { 96 | try { 97 | service.awaitTermination(1, TimeUnit.SECONDS); 98 | if (!service.isTerminated()) { 99 | throw new UBenchRuntimeException( 100 | "Unable to cleanly shut down the Parallel execution in 1 second"); 101 | } 102 | } catch (InterruptedException ie) { 103 | throw new UBenchRuntimeException("Parallel Execution interrupted. See cause.", ie); 104 | } 105 | } 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/ScaleControl.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.function.Consumer; 7 | import java.util.function.IntFunction; 8 | import java.util.function.Supplier; 9 | 10 | class ScaleControl { 11 | 12 | private final Map> sources = new HashMap<>(32); 13 | private final boolean reusedata; 14 | private final IntFunction scaler; 15 | private final Consumer function; 16 | 17 | public ScaleControl(Consumer function, IntFunction scaler, boolean reusedata) { 18 | this.function = function; 19 | this.reusedata = reusedata; 20 | this.scaler = scaler; 21 | } 22 | 23 | 24 | private Supplier getStaticData(final T data) { 25 | return () -> data; 26 | } 27 | 28 | 29 | private Supplier dataSupply(final int scale) { 30 | return sources.computeIfAbsent(scale, key -> reusedata ? getStaticData(scaler.apply(scale)) : () -> scaler.apply(scale)); 31 | } 32 | 33 | public TaskRunner buildTask(final String name, final int scale) { 34 | Task task = () -> { 35 | 36 | T data = dataSupply(scale).get(); 37 | long start = System.nanoTime(); 38 | function.accept(data); 39 | long time = System.nanoTime() - start; 40 | return time; 41 | 42 | }; 43 | return new TaskRunner(name, task, scale, 1000000, 0, 0, TimeUnit.SECONDS.toNanos(1)); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/ScaleDetect.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import net.tuis.ubench.scale.MathEquation; 4 | import net.tuis.ubench.scale.MathModel; 5 | import net.tuis.ubench.scale.Models; 6 | import org.apache.commons.math3.linear.*; 7 | 8 | import java.util.Arrays; 9 | import java.util.Comparator; 10 | import java.util.function.DoubleUnaryOperator; 11 | import java.util.function.Function; 12 | 13 | /** 14 | * @author Simon Forsberg 15 | */ 16 | public class ScaleDetect { 17 | 18 | private static final double TOLERANCE = 1e-4; 19 | private static final double H = 1e-5; 20 | 21 | private static final Comparator ranking = Comparator.comparingInt((MathEquation eq) -> eq.isValid() ? 0 : 1) 22 | .thenComparingDouble(eqb -> - eqb.getRSquared()); 23 | 24 | /** 25 | * Finding the best fit using least-squares method for an equation system 26 | * 27 | * @param function Equation system to find fit for. Input: Parameters, Output: Residuals. 28 | * @param initial Initial 'guess' for function parameters 29 | * @param tolerance How much the function parameters may change before a solution is accepted 30 | * @return The parameters to the function that causes the least residuals 31 | */ 32 | private static double[] newtonSolve(Function function, double[] initial, double tolerance) { 33 | RealVector dx = new ArrayRealVector(initial.length); 34 | dx.set(tolerance + 1); 35 | int iterations = 0; 36 | int d = initial.length; 37 | double[] values = Arrays.copyOf(initial, initial.length); 38 | 39 | while (dx.getNorm() > tolerance) { 40 | double[] fx = function.apply(values); 41 | Array2DRowRealMatrix df = new Array2DRowRealMatrix(fx.length, d); 42 | ArrayRealVector fxVector = new ArrayRealVector(fx); 43 | for (int i = 0; i < d; i++) { 44 | double originalValue = values[i]; 45 | values[i] += H; 46 | double[] fxi = function.apply(values); 47 | values[i] = originalValue; 48 | ArrayRealVector fxiVector = new ArrayRealVector(fxi); 49 | RealVector result = fxiVector.subtract(fxVector); 50 | result = result.mapDivide(H); 51 | df.setColumn(i, result.toArray()); 52 | } 53 | dx = new RRQRDecomposition(df).getSolver().solve(fxVector.mapMultiply(-1)); 54 | // df has size = initial, and fx has size equal to whatever that function produces. 55 | for (int i = 0; i < values.length; i++) { 56 | values[i] += dx.getEntry(i); 57 | } 58 | iterations++; 59 | if (iterations % 100 == 0) { 60 | tolerance *= 10; 61 | } 62 | } 63 | return values; 64 | } 65 | 66 | /** 67 | * From the results of a UScale run, calculate the best-fit scaling equation 68 | * @param scale the results to analyze 69 | * @return the best-fit scaling equation. 70 | */ 71 | public static MathEquation detect(UScale scale) { 72 | return Arrays.stream(rank(scale)) 73 | .filter(eq -> eq.isValid()) 74 | .findFirst() 75 | .orElse(fit(scale, Models.CONSTANT)); // if no valid is found, it is because of constant data 76 | } 77 | 78 | private static MathEquation fit(UScale scale, MathModel model) { 79 | return detect(extractX(scale), extractY(scale), model); 80 | } 81 | 82 | private static double[] extractX(UScale scale) { 83 | return scale.getStats().stream().mapToDouble(st -> st.getIndex()).toArray(); 84 | } 85 | 86 | private static double[] extractY(UScale scale) { 87 | return scale.getStats().stream().mapToDouble(st -> st.getFastestNanos()).toArray(); 88 | } 89 | 90 | /** 91 | * From the scaling results calculate a number of scaling equations, and return their equations in descending order of relevance 92 | * @param scale the Scale results to rank 93 | * @return the possible equations in best-fit-first order. 94 | */ 95 | public static MathEquation[] rank(UScale scale) { 96 | double[] x = extractX(scale); 97 | double[] y = extractY(scale); 98 | return rank(x, y); 99 | } 100 | 101 | private static MathEquation[] rank(double[] x, double[] y) { 102 | MathModel[] models = new MathModel[]{ Models.CONSTANT, Models.LINEAR, 103 | Models.N_SQUARED, Models.createPolynom(3), Models.createPolynom(4), 104 | Models.LOG_N, Models.N_LOG_N, Models.EXPONENTIAL }; 105 | // sort by reverse rsquared, or negative r-squared... note the `-` in `eq -> - eq.getRSquared()` 106 | return Arrays.stream(models).map(m -> detect(x, y, m)).sorted(ranking).toArray(size -> new MathEquation[size]); 107 | } 108 | 109 | static MathEquation detect(double[] x, double[] y, MathModel model) { 110 | if (x.length != y.length) { 111 | throw new IllegalArgumentException("x and y size must match"); 112 | } 113 | 114 | Function function = new Function() { 115 | @Override 116 | public double[] apply(double[] doubles) { 117 | double[] result = new double[x.length]; 118 | DoubleUnaryOperator func = model.createFunction(doubles); 119 | for (int i = 0; i < x.length; i++) { 120 | result[i] = y[i] - func.applyAsDouble(x[i]); 121 | } 122 | return result; 123 | } 124 | }; 125 | double[] results = newtonSolve(function, model.getInitialValues(), TOLERANCE); 126 | DoubleUnaryOperator finalFunction = model.createFunction(results); 127 | 128 | double rSquared = calculateRSquared(finalFunction, x, y); 129 | MathEquation eq = new MathEquation(model, finalFunction, results, model.getFormat(), rSquared); 130 | return eq; 131 | } 132 | 133 | private static double calculateRSquared(DoubleUnaryOperator finalFunction, double[] x, double[] y) { 134 | double yAverage = Arrays.stream(y).average().getAsDouble(); 135 | double variance = 0; 136 | double residualSumOfSquares = 0; 137 | // double explainedSumOfSquares = 0; 138 | 139 | for (int i = 0; i < y.length; i++) { 140 | double yi = y[i]; 141 | double fi = finalFunction.applyAsDouble(x[i]); 142 | variance += (yi - yAverage) * (yi - yAverage); 143 | residualSumOfSquares += (yi - fi) * (yi - fi); 144 | // explainedSumOfSquares += (fi - yAverage) * (fi - yAverage); 145 | } 146 | 147 | double rSquared = 1 - residualSumOfSquares / variance; 148 | return rSquared; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/SequentialExecutionModel.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | /** 4 | * ExecutionModel that runs all of the first task, then all of the next, and so 5 | * on. 6 | * 7 | * @author rolf 8 | * 9 | */ 10 | class SequentialExecutionModel implements TaskExecutionModel { 11 | 12 | @Override 13 | public UStats[] executeTasks(String suite, TaskRunner...tasks) { 14 | UStats[] results = new UStats[tasks.length]; 15 | for (int i = 0; i < tasks.length; i++) { 16 | boolean complete = false; 17 | TaskRunner task = tasks[i]; 18 | do { 19 | complete = task.invoke(); 20 | } while (!complete); 21 | results[i] = task.collect(suite); 22 | } 23 | return results; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/StdoutHandler.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.logging.ConsoleHandler; 4 | 5 | /** 6 | * Direct logs to STDOut and not STDErr. 7 | * 8 | * @author rolf 9 | * 10 | */ 11 | final class StdoutHandler extends ConsoleHandler { 12 | 13 | public StdoutHandler() { 14 | super(); 15 | setOutputStream(System.out); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/Task.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | /** 4 | * This simple interface allows a task to be managed as an instance in the 5 | * UBench suite, and with a single time() entry point 6 | * 7 | * @author rolf 8 | * 9 | */ 10 | @FunctionalInterface 11 | interface Task { 12 | long time(); 13 | } -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/TaskExecutionModel.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | /** 4 | * Simple interface for potential execution models (parallel, sequential, etc.). 5 | * 6 | * @author rolf 7 | * 8 | */ 9 | interface TaskExecutionModel { 10 | 11 | UStats[] executeTasks(String suite, TaskRunner...tasks); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/TaskRunner.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * Class containing the in-flight details of the execution of a single task. 7 | * This class allows the class to be run in spurts, and to collect the results 8 | * in a single place 9 | * 10 | * @author rolf 11 | * 12 | */ 13 | final class TaskRunner { 14 | 15 | /** 16 | * Compute the length of the results array with some space for growth 17 | * 18 | * @param length 19 | * the current length 20 | * @return the desired length 21 | */ 22 | private static int expandTo(int length) { 23 | // add 25% + 100 - limit to Integer.Max 24 | int toAdd = 100 + (length >> 2); 25 | toAdd = Math.min(UBench.MAX_RESULTS - length, toAdd); 26 | return toAdd == 0 ? -1 : toAdd + length; 27 | } 28 | 29 | /** 30 | * Compute whether any of the values in times exceed the given bound, 31 | * relative to the minimum value in times. 32 | * 33 | * @param times 34 | * the times to compute the bounds on 35 | * @param bound 36 | * the bound is represented as a value like 1.10 for 10% greater 37 | * than the minimum 38 | * @return true if all values are in bounds. 39 | */ 40 | private static final boolean inBounds(final long[] times, final double bound) { 41 | long min = times[0]; 42 | long max = times[0]; 43 | long limit = (long) (min * bound); 44 | for (int i = 1; i < times.length; i++) { 45 | if (times[i] < min) { 46 | min = times[i]; 47 | limit = (long) (min * bound); 48 | if (max > limit) { 49 | return false; 50 | } 51 | } 52 | if (times[i] > max) { 53 | max = times[i]; 54 | // new max, is it slower than the worst allowed? 55 | if (max > limit) { 56 | return false; 57 | } 58 | } 59 | } 60 | return true; 61 | } 62 | 63 | private final Task task; 64 | private final String name; 65 | private final int index; 66 | private final long[] stable; 67 | private final int limit; 68 | private final double stableLimit; 69 | 70 | private long[] results; 71 | private boolean complete = false; 72 | private long remainingTime = 0L; 73 | private int iterations = 0; 74 | 75 | TaskRunner(String name, Task task, int index, final int iterations, final int stableSpan, 76 | final double stableLimit, final long timeLimit) { 77 | this.name = name; 78 | this.task = task; 79 | this.index = index; 80 | this.stableLimit = stableLimit; 81 | limit = Math.min(UBench.MAX_RESULTS, iterations > 0 ? iterations : UBench.MAX_RESULTS); 82 | stable = new long[Math.min(stableSpan, limit)]; 83 | results = new long[Math.min(limit, 10000)]; 84 | remainingTime = timeLimit > 0 ? timeLimit : Long.MAX_VALUE; 85 | } 86 | 87 | /** 88 | * Perform a single additional iteration of the task. 89 | * 90 | * @return true if this task is now complete. 91 | */ 92 | boolean invoke() { 93 | if (complete) { 94 | return complete; 95 | } 96 | 97 | if (iterations >= results.length) { 98 | int newlen = expandTo(results.length); 99 | if (newlen < 0) { 100 | complete = true; 101 | return complete; 102 | } 103 | results = Arrays.copyOf(results, newlen); 104 | } 105 | 106 | long res = Math.max(task.time(), 1); 107 | results[iterations] = res; 108 | if (stable.length > 0) { 109 | stable[iterations % stable.length] = res; 110 | if (iterations > stable.length && inBounds(stable, stableLimit)) { 111 | complete = true; 112 | } 113 | } 114 | 115 | remainingTime -= res; 116 | iterations++; 117 | 118 | if (iterations >= limit || remainingTime < 0) { 119 | complete = true; 120 | } 121 | return complete; 122 | } 123 | 124 | /** 125 | * Collect all statistics in to a single public UStats instance. 126 | * 127 | * @param suite 128 | * the name of the test suite this task is part of. 129 | * @return the UStats instance containing the data for statistical analysis 130 | */ 131 | UStats collect(String suite) { 132 | return new UStats(suite, name, index, Arrays.copyOf(results, iterations)); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UBench.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.function.DoublePredicate; 8 | import java.util.function.DoubleSupplier; 9 | import java.util.function.IntPredicate; 10 | import java.util.function.IntSupplier; 11 | import java.util.function.LongPredicate; 12 | import java.util.function.LongSupplier; 13 | import java.util.function.Predicate; 14 | import java.util.function.Supplier; 15 | import java.util.logging.Logger; 16 | 17 | /** 18 | * The UBench class encompasses a suite of tasks that are to be compared... 19 | * possibly relative to each other. 20 | *

21 | * Each task can be added to the suite. Once you have the tasks you need, then 22 | * all tasks can be benchmarked according to limits given in the run. 23 | * 24 | *


25 | * Example usages - which is faster, Arrays.sort(...), or IntStream.sorted()?: 26 | *
 27 |         Random random = new Random();
 28 | 
 29 |         // create an array of 10,000 random integer values.
 30 |         final int[] data = IntStream.generate(random::nextInt).limit(10000).toArray();
 31 |         // create a sorted version, trust the algorithm for the moment.
 32 |         final int[] sorted = Arrays.stream(data).sorted().toArray();
 33 |         // a way to ensure the value is in fact sorted.
 34 |         Predicate<int[]> validate = v -> Arrays.equals(v, sorted);
 35 |         
 36 |         // A stream-based way to sort an array of integers.
 37 |         Supplier<int[]> stream = () -> Arrays.stream(data).sorted().toArray();
 38 |         
 39 |         // The traditional way to sort an array of integers.
 40 |         Supplier<int[]> trad = () -> {
 41 |             int[] copy = Arrays.copyOf(data, data.length);
 42 |             Arrays.sort(copy);
 43 |             return copy;
 44 |         };
 45 |         
 46 |         UBench bench = new UBench("Sort Algorithms")
 47 |               .addTask("Functional", stream, validate);
 48 |               .addTask("Traditional", trad, validate);
 49 |               .press(10000)
 50 |               .report("With Warmup");
 51 |  * 
52 | * 53 | * You can expect results similar to: 54 | * 55 | *
 56 | 
 57 |         With Warmup
 58 |         ===========
 59 |         
 60 |         Task Sort Algorithms -> Functional: (Unit: MILLISECONDS)
 61 |           Count    :    10000      Average  :   0.4576
 62 |           Fastest  :   0.4194      Slowest  :   4.1327
 63 |           95Pctile :   0.5030      99Pctile :   0.6028
 64 |           TimeBlock : 0.493 0.436 0.443 0.459 0.458 0.454 0.457 0.458 0.463 0.456
 65 |           Histogram :  9959    19    21     1
 66 |         
 67 |         Task Sort Algorithms -> Traditional: (Unit: MILLISECONDS)
 68 |           Count    :    10000      Average  :   0.4219
 69 |           Fastest  :   0.4045      Slowest  :   3.6714
 70 |           95Pctile :   0.4656      99Pctile :   0.5420
 71 |           TimeBlock : 0.459 0.417 0.418 0.417 0.416 0.416 0.416 0.419 0.423 0.417
 72 |           Histogram :  9971    18    10     1
 73 | 
 74 | 
 75 |  * 
76 | * 77 | * See {@link UStats} for more details on what the statistics mean. 78 | * 79 | * @author rolf 80 | * 81 | */ 82 | public final class UBench { 83 | 84 | private static final Logger LOGGER = UUtils.getLogger(UBench.class); 85 | 86 | /** 87 | * At most a billion iterations of any task will be attempted. 88 | */ 89 | public static final int MAX_RESULTS = 1_000_000_000; 90 | 91 | private final Map tasks = new LinkedHashMap<>(); 92 | private final String suiteName; 93 | 94 | /** 95 | * Create a new UBench suite with the supplied name. 96 | * 97 | * @param suiteName 98 | * to be used in some automated reports. 99 | */ 100 | public UBench(String suiteName) { 101 | this.suiteName = suiteName; 102 | LOGGER.info(() -> String.format("Creating UBench for suite %s", suiteName)); 103 | } 104 | 105 | private UBench putTask(String name, Task t) { 106 | synchronized (tasks) { 107 | tasks.put(name, t); 108 | } 109 | LOGGER.fine(() -> String.format("UBench suite %s: adding task %s", suiteName, name)); 110 | return this; 111 | } 112 | 113 | /** 114 | * Include a named task (and validator) in to the benchmark. 115 | * 116 | * @param 117 | * The type of the task return value (which is the input to be 118 | * tested in the validator) 119 | * @param name 120 | * The name of the task. Only one task with any one name is 121 | * allowed. 122 | * @param task 123 | * The task to perform 124 | * @param check 125 | * The check of the results from the task. 126 | * @return The same object, for chaining calls. 127 | */ 128 | public UBench addTask(String name, Supplier task, Predicate check) { 129 | return putTask(name, () -> { 130 | long start = System.nanoTime(); 131 | T result = task.get(); 132 | long time = System.nanoTime() - start; 133 | if (check != null && !check.test(result)) { 134 | throw new UBenchRuntimeException(String.format("Task %s failed Result: %s", name, result)); 135 | } 136 | return time; 137 | }); 138 | } 139 | 140 | /** 141 | * Include a named task in to the benchmark. 142 | * 143 | * @param 144 | * The type of the return value from the task. It is ignored. 145 | * @param name 146 | * The name of the task. Only one task with any one name is 147 | * allowed. 148 | * @param task 149 | * The task to perform 150 | * 151 | * @return The same object, for chaining calls. 152 | */ 153 | public UBench addTask(String name, Supplier task) { 154 | return addTask(name, task, null); 155 | } 156 | 157 | /** 158 | * Include an int-specialized named task (and validator) in to the 159 | * benchmark. 160 | * 161 | * @param name 162 | * The name of the task. Only one task with any one name is 163 | * allowed. 164 | * @param task 165 | * The task to perform 166 | * @param check 167 | * The check of the results from the task. 168 | * @return The same object, for chaining calls. 169 | */ 170 | public UBench addIntTask(String name, IntSupplier task, IntPredicate check) { 171 | return putTask(name, () -> { 172 | long start = System.nanoTime(); 173 | int result = task.getAsInt(); 174 | long time = System.nanoTime() - start; 175 | if (check != null && !check.test(result)) { 176 | throw new UBenchRuntimeException(String.format("Task %s failed Result: %d", name, result)); 177 | } 178 | return time; 179 | }); 180 | } 181 | 182 | /** 183 | * Include an int-specialized named task in to the benchmark. 184 | * 185 | * @param name 186 | * The name of the task. Only one task with any one name is 187 | * allowed. 188 | * @param task 189 | * The task to perform 190 | * @return The same object, for chaining calls. 191 | */ 192 | public UBench addIntTask(String name, IntSupplier task) { 193 | return addIntTask(name, task, null); 194 | } 195 | 196 | /** 197 | * Include a long-specialized named task (and validator) in to the 198 | * benchmark. 199 | * 200 | * @param name 201 | * The name of the task. Only one task with any one name is 202 | * allowed. 203 | * @param task 204 | * The task to perform 205 | * @param check 206 | * The check of the results from the task. 207 | * @return The same object, for chaining calls. 208 | */ 209 | public UBench addLongTask(String name, LongSupplier task, LongPredicate check) { 210 | return putTask(name, () -> { 211 | long start = System.nanoTime(); 212 | long result = task.getAsLong(); 213 | long time = System.nanoTime() - start; 214 | if (check != null && !check.test(result)) { 215 | throw new UBenchRuntimeException(String.format("Task %s failed Result: %d", name, result)); 216 | } 217 | return time; 218 | }); 219 | } 220 | 221 | /** 222 | * Include a long-specialized named task in to the benchmark. 223 | * 224 | * @param name 225 | * The name of the task. Only one task with any one name is 226 | * allowed. 227 | * @param task 228 | * The task to perform 229 | * @return The same object, for chaining calls. 230 | */ 231 | public UBench addLongTask(String name, LongSupplier task) { 232 | return addLongTask(name, task, null); 233 | } 234 | 235 | /** 236 | * Include a double-specialized named task (and validator) in to the 237 | * benchmark. 238 | * 239 | * @param name 240 | * The name of the task. Only one task with any one name is 241 | * allowed. 242 | * @param task 243 | * The task to perform 244 | * @param check 245 | * The check of the results from the task. 246 | * @return The same object, for chaining calls. 247 | */ 248 | public UBench addDoubleTask(String name, DoubleSupplier task, DoublePredicate check) { 249 | return putTask(name, () -> { 250 | long start = System.nanoTime(); 251 | double result = task.getAsDouble(); 252 | long time = System.nanoTime() - start; 253 | if (check != null && !check.test(result)) { 254 | throw new UBenchRuntimeException(String.format("Task %s failed Result: %f", name, result)); 255 | } 256 | return time; 257 | }); 258 | } 259 | 260 | /** 261 | * Include a double-specialized named task in to the benchmark. 262 | * 263 | * @param name 264 | * The name of the task. Only one task with any one name is 265 | * allowed. 266 | * @param task 267 | * The task to perform 268 | * @return The same object, for chaining calls. 269 | */ 270 | public UBench addDoubleTask(String name, DoubleSupplier task) { 271 | return addDoubleTask(name, task, null); 272 | } 273 | 274 | 275 | /** 276 | * Include a named task that has no output value in to the benchmark. 277 | * 278 | * @param name 279 | * The name of the task. Only one task with any one name is 280 | * allowed. 281 | * @param task 282 | * The task to perform 283 | * @return The same object, for chaining calls. 284 | */ 285 | public UBench addTask(String name, Runnable task) { 286 | return putTask(name, () -> { 287 | long start = System.nanoTime(); 288 | task.run(); 289 | return System.nanoTime() - start; 290 | }); 291 | } 292 | 293 | /** 294 | * Benchmark all added tasks until they complete the desired iteration 295 | * count, reaches stability, or exceed the given time limit, whichever comes 296 | * first. 297 | * 298 | * @param mode 299 | * The UMode execution model to use for task execution 300 | * @param iterations 301 | * maximum number of iterations to run. 302 | * @param stableSpan 303 | * If this many iterations in a row are all within the 304 | * maxVariance, then the benchmark ends. A value less than, or 305 | * equal to 0 turns off this check 306 | * @param stableBound 307 | * Expressed as a percent from 0.0 to 100.0, and so on 308 | * @param timeLimit 309 | * combined with the timeUnit, indicates how long to run tests 310 | * for. A value less than or equal to 0 turns off this check. 311 | * @param timeUnit 312 | * combined with the timeLimit, indicates how long to run tests 313 | * for. 314 | * @return the results of all completed tasks. 315 | */ 316 | public UReport press(final UMode mode, final int iterations, final int stableSpan, final double stableBound, 317 | final long timeLimit, final TimeUnit timeUnit) { 318 | 319 | // make sense of any out-of-bounds input parameters. 320 | UMode vmode = mode == null ? UMode.INTERLEAVED : mode; 321 | int vit = iterations <= 0 ? 0 : Math.min(MAX_RESULTS, iterations); 322 | int vmin = (stableSpan <= 0 || stableBound <= 0) ? 0 : Math.min(stableSpan, vit); 323 | long vtime = timeLimit <= 0 ? 0 : (timeUnit == null ? 0 : timeUnit.toNanos(timeLimit)); 324 | 325 | final long start = System.nanoTime(); 326 | LOGGER.fine(() -> String.format("UBench suite %s: running all tasks in mode %s", suiteName, vmode.name())); 327 | 328 | TaskRunner[] mytasks = getTasks(vit, vmin, stableBound, vtime); 329 | UStats[] ret = vmode.getModel().executeTasks(suiteName, mytasks); 330 | 331 | final long time = System.nanoTime() - start; 332 | LOGGER.info(() -> String.format("UBench suite %s: completed benchmarking all tasks using mode %s in %.3fms", 333 | suiteName, vmode.name(), time / 1000000.0)); 334 | 335 | return new UReport(Arrays.asList(ret)); 336 | } 337 | 338 | /** 339 | * Benchmark all added tasks until they exceed the set time limit 340 | * 341 | * @param mode 342 | * The UMode execution model to use for task execution 343 | * @param timeLimit 344 | * combined with the timeUnit, indicates how long to run tests 345 | * for. A value less than or equal to 0 turns off this check. 346 | * @param timeUnit 347 | * combined with the timeLimit, indicates how long to run tests 348 | * for. 349 | * @return the results of all completed tasks. 350 | */ 351 | public UReport press(UMode mode, final long timeLimit, final TimeUnit timeUnit) { 352 | return press(mode, 0, 0, 0.0, timeLimit, timeUnit); 353 | } 354 | 355 | /** 356 | * Benchmark all added tasks until they complete the desired iteration 357 | * count. 358 | * 359 | * @param mode 360 | * The UMode execution model to use for task execution 361 | * @param iterations 362 | * maximum number of iterations to run. 363 | * @return the results of all completed tasks. 364 | */ 365 | public UReport press(UMode mode, final int iterations) { 366 | return press(mode, iterations, 0, 0.0, 0, null); 367 | } 368 | 369 | /** 370 | * Benchmark all added tasks until they complete the desired iteration 371 | * count, reaches stability, or exceed the given time limit, whichever comes 372 | * first. 373 | * 374 | * @param iterations 375 | * maximum number of iterations to run. 376 | * @param stableSpan 377 | * If this many iterations in a row are all within the 378 | * maxVariance, then the benchmark ends. A value less than, or 379 | * equal to 0 turns off this check 380 | * @param stableBound 381 | * Expressed as a percent from 0.0 to 100.0, and so on 382 | * @param timeLimit 383 | * combined with the timeUnit, indicates how long to run tests 384 | * for. A value less than or equal to 0 turns off this check. 385 | * @param timeUnit 386 | * combined with the timeLimit, indicates how long to run tests 387 | * for. 388 | * @return the results of all completed tasks. 389 | */ 390 | public UReport press(final int iterations, final int stableSpan, final double stableBound, 391 | final long timeLimit, final TimeUnit timeUnit) { 392 | return press(null, iterations, stableSpan, stableBound, timeLimit, timeUnit); 393 | } 394 | 395 | /** 396 | * Benchmark all added tasks until they exceed the set time limit 397 | * 398 | * @param timeLimit 399 | * combined with the timeUnit, indicates how long to run tests 400 | * for. A value less than or equal to 0 turns off this check. 401 | * @param timeUnit 402 | * combined with the timeLimit, indicates how long to run tests 403 | * for. 404 | * @return the results of all completed tasks. 405 | */ 406 | public UReport press(final long timeLimit, final TimeUnit timeUnit) { 407 | return press(null, timeLimit, timeUnit); 408 | } 409 | 410 | /** 411 | * Benchmark all added tasks until they complete the desired iteration 412 | * count. 413 | * 414 | * @param iterations 415 | * maximum number of iterations to run. 416 | * @return the results of all completed tasks. 417 | */ 418 | public UReport press(final int iterations) { 419 | return press(null, iterations); 420 | } 421 | 422 | private TaskRunner[] getTasks(final int count, final int stabLength, final double stabVariance, final long timeLimit) { 423 | synchronized (tasks) { 424 | TaskRunner[] tr = new TaskRunner[tasks.size()]; 425 | int pos = 0; 426 | for (Map.Entry me : tasks.entrySet()) { 427 | tr[pos++] = new TaskRunner(me.getKey(), me.getValue(), pos, count, stabLength, stabVariance, timeLimit); 428 | } 429 | return tr; 430 | } 431 | } 432 | 433 | @Override 434 | public String toString() { 435 | return String.format("%s with tasks: %s", suiteName, tasks.toString()); 436 | } 437 | 438 | /** 439 | * Return the name this UBench suite was created with. 440 | * @return the name of this suite. 441 | */ 442 | public String getSuiteName() { 443 | return suiteName; 444 | } 445 | 446 | } 447 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UBenchRuntimeException.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | /** 4 | * Simple Wrapper for IllegalStateException (RuntimeException) which allows for a more granular form of error handling. 5 | * 6 | * @author rolf 7 | * 8 | */ 9 | public class UBenchRuntimeException extends IllegalStateException { 10 | 11 | /** 12 | * 13 | */ 14 | private static final long serialVersionUID = -7469405348075121722L; 15 | 16 | /** 17 | * Initialize a new custom UBenchRuntimeException, which is just a specialized IllegalStateException. 18 | * 19 | * @param message The message to pass through to the IllegalStateException. 20 | * @param cause The cause to pass through to the IllegalStateException. 21 | */ 22 | public UBenchRuntimeException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | 26 | /** 27 | * Initialize a new custom UBenchRuntimeException, which is just a specialized IllegalStateException. 28 | * 29 | * @param message The message to pass through to the IllegalStateException. 30 | */ 31 | public UBenchRuntimeException(String message) { 32 | super(message); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UMode.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | /** 4 | * Indicate how to execute the UBench tasks, when run. 5 | * 6 | * @author rolf 7 | * 8 | */ 9 | public enum UMode { 10 | 11 | /** 12 | * Run all iterations of the tasks one task after the next. 13 | *

14 | * With 3 tasks, A, B, C: 15 | * 16 | *

17 |      * A1 A2 .. An
18 |      *               B1 B2 .. Bn
19 |      *                              C1 C2 .. Cn
20 |      * 
21 | * 22 | */ 23 | SEQUENTIAL(new SequentialExecutionModel()), 24 | 25 | /** 26 | * Allocate a separate thread to each task, and execute them all at once. 27 | *

28 | * With 3 tasks, A, B, C: 29 | *

30 |      * A1   A2 ..   An
31 |      * B1 B2  ..  Bn
32 |      * C1  C2   .. Cn
33 |      * 
34 | */ 35 | PARALLEL(new ParallelExecutionModel()), 36 | 37 | /** 38 | * Run one iteration from each task, then go back to the first task, repeat for all iterations. 39 | *

40 | * With 3 tasks, A, B, C: 41 | *

42 |      * A1       A2       ..       An
43 |      *    B1       B2       ..       Bn
44 |      *       C1       C2       ..       Cn
45 |      * 
46 | */ 47 | INTERLEAVED(new InterleavedExecutionModel()); 48 | 49 | private final TaskExecutionModel model; 50 | 51 | private UMode(TaskExecutionModel model) { 52 | this.model = model; 53 | } 54 | 55 | /** 56 | * Package Private: return the model implementation 57 | * @return the actual implementation 58 | */ 59 | TaskExecutionModel getModel() { 60 | return model; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UReport.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | import java.util.ArrayList; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * The UReport class encapsulates the results that are produced by 14 | * {@link UBench#press(UMode, int, int, double, long, TimeUnit)} and exposes 15 | * some convenient reporting methods. 16 | */ 17 | public class UReport { 18 | 19 | /** 20 | * A Comparator which sorts collections of UStats by the 95th 21 | * percentile time (ascending - fastest first) 22 | */ 23 | public static final Comparator BY_95PCTILE = Comparator.comparingLong(UStats::get95thPercentileNanos); 24 | /** 25 | * A Comparator which sorts collections of UStats by the 99th 26 | * percentile time (ascending - fastest first) 27 | */ 28 | public static final Comparator BY_99PCTILE = Comparator.comparingLong(UStats::get99thPercentileNanos); 29 | /** 30 | * A Comparator which sorts collections of UStats by the fastest time 31 | * (ascending - fastest first) 32 | */ 33 | public static final Comparator BY_FASTEST = Comparator.comparingLong(UStats::getFastestNanos); 34 | /** 35 | * A Comparator which sorts collections of UStats by the slowest time 36 | * (ascending - quickest of the slowest first) 37 | */ 38 | public static final Comparator BY_SLOWEST = Comparator.comparingLong(UStats::getFastestNanos); 39 | /** 40 | * A Comparator which sorts collections of UStats by the time consistency - 41 | * calculated as the slowest/fastest ratio (ascending - most consistent 42 | * first) 43 | */ 44 | public static final Comparator BY_CONSISTENCY = Comparator.comparingDouble(s -> s.getSlowestNanos() 45 | / (s.getFastestNanos() * 1.0)); 46 | /** 47 | * A Comparator which sorts collections of UStats by the average time 48 | * (ascending - fastest first) 49 | */ 50 | public static final Comparator BY_AVERAGE = Comparator.comparingDouble(UStats::getAverageRawNanos); 51 | /** 52 | * A Comparator which sorts collections of UStats by the order in which they 53 | * were added to the UBench suite 54 | */ 55 | public static final Comparator BY_ADDED = Comparator.comparingDouble(UStats::getIndex); 56 | 57 | private final List stats; 58 | 59 | /** 60 | * Construct a report container that includes all the specified statistics. 61 | * 62 | * @param stats 63 | * the statistics to report on. 64 | */ 65 | public UReport(List stats) { 66 | super(); 67 | this.stats = stats; 68 | } 69 | 70 | /** 71 | * Retrieve the raw statistics this Report would deliver, sorted in the 72 | * order they were added to the benchmark. 73 | * 74 | * @return the raw statistics. 75 | */ 76 | public List getStats() { 77 | return getStats(null); 78 | } 79 | 80 | /** 81 | * Retrieve the raw statistics this Report would deliver, sorted in the 82 | * order specified (or the order they were added to the benchmark, if null). 83 | * 84 | * @param comparator 85 | * The comparator to sort the results by. 86 | * @return the statistics in the specified order. 87 | */ 88 | public List getStats(final Comparator comparator) { 89 | List result = new ArrayList<>(stats); 90 | if (comparator != null) { 91 | result.sort(comparator); 92 | } 93 | return result; 94 | } 95 | 96 | /** 97 | * Simple helper method that prints the specified title, underlined with '=' 98 | * characters. 99 | * 100 | * @param writer 101 | * the writer to write the title to. 102 | * @param title 103 | * the title to print (null or empty titles will be ignored). 104 | * @throws IOException 105 | * if the writer fails. 106 | */ 107 | public static void title(Writer writer, String title) throws IOException { 108 | if (title == null || title.isEmpty()) { 109 | return; 110 | } 111 | String out = String.format("%s\n%s\n\n", title, 112 | Stream.generate(() -> "=").limit(title.length()).collect(Collectors.joining())); 113 | writer.write(out); 114 | } 115 | 116 | /** 117 | * Generate and print (System.out) the statistics report using the default ( 118 | * {@link #BY_ADDED}) sort order. 119 | * 120 | */ 121 | public void report() { 122 | reportSO(null, null); 123 | } 124 | 125 | /** 126 | * Generate and print (System.out) the statistics report using the specified 127 | * sort order. 128 | * 129 | * @param comparator 130 | * the Comparator to sort the UStats by (see class constants for 131 | * some useful suggestions) 132 | */ 133 | public void report(final Comparator comparator) { 134 | reportSO(null, comparator); 135 | } 136 | 137 | /** 138 | * Generate and print (System.out) the statistics report with the supplied 139 | * title, and using the default ({@link #BY_ADDED}) sort order. 140 | * 141 | * @param title 142 | * the title to use (e.g. "Warmup", "Cached Files", etc.) 143 | */ 144 | public void report(final String title) { 145 | reportSO(title, null); 146 | } 147 | 148 | /** 149 | * Generate and print (System.out) the statistics report with the supplied 150 | * title, and using the specified sort order. 151 | * 152 | * @param title 153 | * the title to use (e.g. "Warmup", "Cached Files", etc.) 154 | * @param comparator 155 | * the Comparator to sort the UStats by (see class constants for 156 | * some useful suggestions) 157 | */ 158 | public void report(final String title, final Comparator comparator) { 159 | reportSO(title, comparator); 160 | } 161 | 162 | /** 163 | * Generate and print (System.out) the statistics report using the default ( 164 | * {@link #BY_ADDED}) sort order. 165 | * 166 | * @param writer 167 | * the destination to report to 168 | * @throws IOException 169 | * if the writer destination fails 170 | */ 171 | public void report(final Writer writer) throws IOException { 172 | report(writer, null, null); 173 | } 174 | 175 | /** 176 | * Generate and print (System.out) the statistics report using the specified 177 | * sort order. 178 | * 179 | * @param writer 180 | * the destination to report to 181 | * @param comparator 182 | * the Comparator to sort the UStats by (see class constants for 183 | * some useful suggestions) 184 | * @throws IOException 185 | * if the writer destination fails 186 | */ 187 | public void report(final Writer writer, final Comparator comparator) throws IOException { 188 | report(writer, null, comparator); 189 | } 190 | 191 | /** 192 | * Generate and print (System.out) the statistics report with the supplied 193 | * title, and using the default ({@link #BY_ADDED}) sort order. 194 | * 195 | * @param writer 196 | * the destination to report to 197 | * @param title 198 | * the title to use (e.g. "Warmup", "Cached Files", etc.) 199 | * @throws IOException 200 | * if the writer destination fails 201 | */ 202 | public void report(final Writer writer, final String title) throws IOException { 203 | report(writer, title, null); 204 | } 205 | 206 | /** 207 | * Generate and print (System.out) the statistics report with the supplied 208 | * title, and using the specified sort order. 209 | * 210 | * @param writer 211 | * the destination to report to 212 | * @param title 213 | * the title to use (e.g. "Warmup", "Cached Files", etc.) 214 | * @param comparator 215 | * the Comparator to sort the UStats by (see class constants for 216 | * some useful suggestions) 217 | * @throws IOException 218 | * if the writer destination fails 219 | */ 220 | public void report(final Writer writer, final String title, final Comparator comparator) throws IOException { 221 | 222 | title(writer, title); 223 | 224 | long mintime = stats.stream().mapToLong(s -> s.getFastestNanos()).min().getAsLong(); 225 | TimeUnit tUnit = UStats.findBestUnit(mintime); 226 | for (UStats s : getStats(comparator)) { 227 | writer.write(s.formatResults(tUnit)); 228 | writer.write("\n"); 229 | } 230 | } 231 | 232 | private void reportSO(final String title, final Comparator comparator) { 233 | try (Writer w = new NonClosingSystemOut()) { 234 | report(w, title, comparator); 235 | } catch (IOException e) { 236 | throw new IllegalStateException("Should never be an exception writing to System.out", e); 237 | } 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UScale.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | import java.nio.charset.StandardCharsets; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Comparator; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.function.Consumer; 15 | import java.util.function.Function; 16 | import java.util.function.IntFunction; 17 | import java.util.logging.Level; 18 | import java.util.logging.Logger; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.Stream; 23 | 24 | import net.tuis.ubench.scale.MathEquation; 25 | 26 | /** 27 | * Factory class and reporting instances that allow functions to be tested for 28 | * scalability. 29 | * 30 | * @author rolf 31 | * @author Simon Forsberg 32 | * 33 | */ 34 | public class UScale { 35 | 36 | private static final Logger LOGGER = UUtils.getLogger(UScale.class); 37 | 38 | private static final int SCALE_LIMIT = 12_000_000; 39 | 40 | @FunctionalInterface 41 | private interface TaskRunnerBuilder { 42 | TaskRunner build(String name, int scale); 43 | } 44 | 45 | private final List stats; 46 | private final String title; 47 | 48 | private UScale(String title, List stats) { 49 | this.title = title; 50 | this.stats = stats; 51 | } 52 | 53 | /** 54 | * Get the name this UScale report was titled with. 55 | * @return the title used when created 56 | */ 57 | public String getTitle() { 58 | return title; 59 | } 60 | 61 | /** 62 | * Generate and print (System.out) the scalability report. 63 | */ 64 | public void report() { 65 | try (Writer w = new NonClosingSystemOut()) { 66 | report(w); 67 | } catch (IOException e) { 68 | throw new IllegalStateException("Should never be an exception writing to System.out", e); 69 | } 70 | } 71 | 72 | /** 73 | * Generate and print (System.out) the scalability report. 74 | * @param writer The writer to write the report to 75 | * @throws IOException in the event that the writer throws one. 76 | */ 77 | public void report(Writer writer) throws IOException { 78 | String report = stats.stream() 79 | .sorted(Comparator.comparingInt(UStats::getIndex)) 80 | .map(sr -> String.format( 81 | "Scale %4d -> %8d (count %d, threshold %d)", 82 | sr.getIndex(), sr.getAverageRawNanos(), sr.getCount(), UUtils.getNanoTick())) 83 | .collect(Collectors.joining("\n")); 84 | MathEquation bestFit = determineBestFit(); 85 | writer.write(title); 86 | char[] uls = new char[title.length()]; 87 | Arrays.fill(uls, '='); 88 | writer.write(uls); 89 | writer.write(report); 90 | writer.write("Best fit is: " + bestFit + "\n"); 91 | writer.flush(); 92 | } 93 | 94 | /** 95 | * Retrieve the best-fitting possible standard scaling equation that describes these scaling results. 96 | * @return the best fit equation 97 | */ 98 | public MathEquation determineBestFit() { 99 | return ScaleDetect.detect(this); 100 | } 101 | 102 | /** 103 | * Retrieve the possible standard scaling equations that describes these scaling results in best-fit first order. 104 | * @return the standard scaling equations in best-fit-first order. 105 | */ 106 | public MathEquation[] fitEquations() { 107 | return ScaleDetect.rank(this); 108 | } 109 | 110 | /** 111 | * Get the data as JSON data in an array format ([ [scale,nanos], ...] 112 | * @return a JSON formatted string containing the raw data. 113 | */ 114 | public String toJSONString() { 115 | 116 | String rawdata = stats.stream().sorted(Comparator.comparingInt(UStats::getIndex)) 117 | .map(sr -> sr.toJSONString()) 118 | .collect(Collectors.joining(",\n ", "[\n ", "\n ]")); 119 | 120 | String fields = Stream.of(UStats.getJSONFields()).collect(Collectors.joining("\", \"", "[\"", "\"]")); 121 | 122 | String models = Stream.of(ScaleDetect.rank(this)).map(me -> me.toJSONString()).collect(Collectors.joining(",\n ", "[\n ", "\n ]")); 123 | 124 | return String.format("{ title: \"%s\",\n nano_tick: %d,\n models: %s,\n fields: %s,\n data: %s\n}", 125 | title, UUtils.getNanoTick(), models, fields, rawdata); 126 | } 127 | 128 | /** 129 | * Create an HTML document (with data and chart) plotting the performance. 130 | * @param target the destination to store the HTML document at. 131 | * @throws IOException if there is a problem writing to the target path 132 | */ 133 | public void reportHTML(final Path target) throws IOException { 134 | 135 | Files.createDirectories(target.toAbsolutePath().getParent()); 136 | 137 | LOGGER.info(() -> "Preparing HTML Report '" + title + "' to path: " + target); 138 | 139 | String html = UUtils.readResource("net/tuis/ubench/scale/UScale.html"); 140 | Map subs = new HashMap<>(); 141 | subs.put("DATA", toJSONString()); 142 | 143 | subs.put("TITLE", title); 144 | for (Map.Entry me : subs.entrySet()) { 145 | html = html.replaceAll(Pattern.quote("${" + me.getKey() + "}"), Matcher.quoteReplacement(me.getValue())); 146 | } 147 | Files.write(target, html.getBytes(StandardCharsets.UTF_8)); 148 | LOGGER.info(() -> "Completed HTML Report '" + title + "' to path: " + target); 149 | } 150 | 151 | /** 152 | * Test the scalability of a consumer that requires T input data. 153 | *

154 | * This method calls scale(Consumer, IntFunction, boolean) with 155 | * the reusedata parameter set to true: 156 | * 157 | *

158 |      * return scale(function, scaler, true);
159 |      * 
160 | * 161 | * This means that the data will be generated once for each scale factor, 162 | * and reused. 163 | * 164 | * @param 165 | * the type of the input data needed by the Function 166 | * @param title 167 | * the title to apply to all reports and results 168 | * @param function 169 | * the Function that computes the T data 170 | * @param scaler 171 | * a supplier that can supply T data of different sizes 172 | * @return A UScale instance containing the results of the testing 173 | */ 174 | public static UScale function(String title, Function function, IntFunction scaler) { 175 | return function(title, function, scaler, true); 176 | } 177 | 178 | /** 179 | * Test the scalability of a consumer that requires T input data. 180 | * 181 | * @param 182 | * the type of the input data needed by the Function 183 | * @param title 184 | * the title to apply to all reports and results 185 | * @param function 186 | * the computer that processes the T data 187 | * @param scaler 188 | * a supplier that can supply T data of different sizes 189 | * @param reusedata 190 | * if true, data of each size will be created just once, and 191 | * reused often. 192 | * @return A UScale instance containing the results of the testing 193 | */ 194 | public static UScale function(String title, Function function, IntFunction scaler, final boolean reusedata) { 195 | return consumer(title, function::apply, scaler, reusedata); 196 | } 197 | 198 | /** 199 | * Test the scalability of a consumer that requires T input data. 200 | *

201 | * This method calls scale(Consumer, IntFunction, boolean) with 202 | * the reusedata parameter set to true: 203 | * 204 | *

205 |      * return scale(function, scaler, true);
206 |      * 
207 | * 208 | * This means that the data will be generated once for each scale factor, 209 | * and reused. 210 | * 211 | * @param 212 | * the type of the input data needed by the Consumer 213 | * @param title 214 | * the title to apply to all reports and results 215 | * @param consumer 216 | * the Consumer that processes the T data 217 | * @param scaler 218 | * a supplier that can supply T data of different sizes 219 | * @return A UScale instance containing the results of the testing 220 | */ 221 | public static UScale consumer(String title, Consumer consumer, IntFunction scaler) { 222 | return consumer(title, consumer, scaler, true); 223 | } 224 | 225 | /** 226 | * Test the scalability of a consumer that requires T input data. 227 | * 228 | * @param 229 | * the type of the input data needed by the Consumer 230 | * @param title 231 | * the title to apply to all reports and results 232 | * @param consumer 233 | * the Consumer that processes the T data 234 | * @param scaler 235 | * a supplier that can supply T data of different sizes 236 | * @param reusedata 237 | * if true, data of each size will be created just once, and 238 | * reused often. 239 | * @return A UScale instance containing the results of the testing 240 | */ 241 | public static UScale consumer(String title, Consumer consumer, IntFunction scaler, final boolean reusedata) { 242 | 243 | final ScaleControl scontrol = new ScaleControl<>(consumer, scaler, reusedata); 244 | 245 | final TaskRunnerBuilder builder = (name, scale) -> scontrol.buildTask(name, scale); 246 | 247 | return scaleMapper(title, builder); 248 | } 249 | 250 | private static final UScale scaleMapper(final String title, final TaskRunnerBuilder scaleBuilder) { 251 | LOGGER.info(title + ": Starting Scalability testing"); 252 | final long start = System.nanoTime(); 253 | LOGGER.finer("warming up task"); 254 | UStats[] rep = UMode.PARALLEL.getModel().executeTasks("Warmup", scaleBuilder.build("warmup", 2)); 255 | LOGGER.fine(() -> "Warmed up results:\n" + rep[0].toString()); 256 | 257 | final List results = new ArrayList<>(20); 258 | 259 | for (int i = 1; i <= SCALE_LIMIT; i *= 2) { 260 | 261 | results.add(runStats(title, i, scaleBuilder)); 262 | 263 | if (results.get(results.size() -1).getCount() <= 3) { 264 | break; 265 | } 266 | } 267 | if (results.size() > 4) { 268 | final int last = results.get(results.size() - 1).getIndex(); 269 | int step = last >> 3; 270 | for (int j = last - step; j > step; j -= step) { 271 | if (j == last >> 1 || j == last >> 2) { 272 | continue; 273 | } 274 | results.add(runStats(title, j, scaleBuilder)); 275 | } 276 | } 277 | 278 | LOGGER.info(String.format("%s: Completed tests in %.3fms", title, (System.nanoTime() - start) / 1000000.0)); 279 | 280 | return new UScale(title, results); 281 | } 282 | 283 | private static UStats runStats(String title, int i, TaskRunnerBuilder scaleBuilder) { 284 | final String runName = title + ": Scale " + i; 285 | 286 | final long beg = System.nanoTime(); 287 | 288 | final TaskRunner runner = scaleBuilder.build(runName, i); 289 | 290 | LOGGER.finer(() -> String.format("Built data for %s in %.3fms", runName, (System.nanoTime() - beg) / 1000000.0)); 291 | 292 | UStats stats = UMode.SEQUENTIAL.getModel().executeTasks(runName, runner)[0]; 293 | 294 | if (LOGGER.isLoggable(Level.INFO)) { 295 | final long time = System.nanoTime() - beg; 296 | LOGGER.fine(() -> String.format("Completed scale test %s in %.3fms", runName, time / 1000000.0)); 297 | } 298 | return stats; 299 | } 300 | 301 | /** 302 | * Obtain a copy of the statistics produced for this UScale instance. 303 | * @return a copy of the underlying statistics. 304 | */ 305 | public List getStats() { 306 | return new ArrayList<>(stats); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UStats.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | import java.util.Comparator; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.function.ToLongFunction; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.DoubleStream; 11 | import java.util.stream.IntStream; 12 | import java.util.stream.LongStream; 13 | 14 | /** 15 | * Statistics representing the individual iterations for a given task. 16 | *

17 | * Presents various statistics related to the run times that are useful for 18 | * interpreting the run performance. 19 | */ 20 | public final class UStats { 21 | 22 | /* 23 | * unit(Bounds|Factor|Order|Name) static members are a way of identifying 24 | * which time unit is most useful for displaying a time. 25 | * 26 | * A useful unit is one which presents the time as something between 0.1 and 27 | * 99.999. e.g. it is better to have 2.35668 milliseconds than 2356.82334 28 | * microseconds, or 0.00235 seconds. 29 | */ 30 | private static final long[] unitBounds = buildUnitBounds(); 31 | private static final double[] unitFactor = buildUnitFactors(); 32 | private static final TimeUnit[] unitOrder = buildUnitOrders(); 33 | private static final String[] unitName = buildUnitNames(); 34 | 35 | private static final Map> TIME_PULLER = new LinkedHashMap<>(); 36 | 37 | static { 38 | TIME_PULLER.put("index", s -> s.getIndex()); 39 | TIME_PULLER.put("fastest", s -> s.getFastestNanos()); 40 | TIME_PULLER.put("slowest", s -> s.getSlowestNanos()); 41 | TIME_PULLER.put("average", s -> s.getAverageRawNanos()); 42 | TIME_PULLER.put("pct95", s -> s.get95thPercentileNanos()); 43 | TIME_PULLER.put("pct99", s -> s.get99thPercentileNanos()); 44 | TIME_PULLER.put("count", s -> s.getCount()); 45 | } 46 | 47 | private static final double[] buildUnitFactors() { 48 | TimeUnit[] tus = TimeUnit.values(); 49 | double[] ret = new double[tus.length]; 50 | for (TimeUnit tu : tus) { 51 | ret[tu.ordinal()] = tu.toNanos(1); 52 | } 53 | return ret; 54 | } 55 | 56 | private static final TimeUnit[] buildUnitOrders() { 57 | TimeUnit[] tus = TimeUnit.values(); 58 | Arrays.sort(tus, Comparator.comparingLong(u -> u.toNanos(1))); 59 | return tus; 60 | } 61 | 62 | private static final long[] buildUnitBounds() { 63 | TimeUnit[] tus = TimeUnit.values(); 64 | long[] ret = new long[tus.length]; 65 | for (TimeUnit tu : tus) { 66 | ret[tu.ordinal()] = tu.toNanos(1) / 10L; 67 | } 68 | return ret; 69 | } 70 | 71 | private static final String[] buildUnitNames() { 72 | TimeUnit[] tus = TimeUnit.values(); 73 | String[] ret = new String[tus.length]; 74 | for (TimeUnit tu : tus) { 75 | ret[tu.ordinal()] = tu.toString(); 76 | } 77 | return ret; 78 | } 79 | 80 | /** 81 | * When outputting JSON data, the following fields will be populated 82 | * @return an array containing the populated fields. 83 | */ 84 | public static String[] getJSONFields() { 85 | return TIME_PULLER.keySet().stream().toArray(size -> new String[size]); 86 | } 87 | 88 | /** 89 | * Identify a TimeUnit that is convenient for the display of the supplied 90 | * nanosecond value. 91 | *

92 | * The best unit is the one which has no zeros after the decimal, and at 93 | * most two digits before. 94 | *

95 | * The following are examples of "best" displays: 96 | *

    97 | *
  • 1.2345 seconds 98 | *
  • 0.7247 microseconds 99 | *
  • 82.443 milliseconds 100 | *
101 | * 102 | * in contrast, the following would not be suggested as "best" units: 103 | * 104 | *
    105 | *
  • 623.2345 milliseconds 106 | *
  • 0.0000007247 seconds 107 | *
  • 825543.000 nanoseconds 108 | *
109 | * 110 | * It is suggested that you should find the best unit for the shortest time 111 | * value you will have in your data, and then use that same unit to display 112 | * all times. 113 | *

114 | * For example, if you have the following nanosecond times 115 | * [5432, 8954228, 665390, 492009] you should find the unit for 116 | * the shortest (5432) which will be TimeUnit.MICROSECONDS, and end up with 117 | * the display of: 118 | * 119 | *

120 |      *     5.432
121 |      *  8954.228
122 |      *   665.390
123 |      *   492.009
124 |      * 
125 | * 126 | * @param time 127 | * the time to display (in nanoseconds) 128 | * @return A Time Unit that will display the nanosecond time well. 129 | */ 130 | public static TimeUnit findBestUnit(long time) { 131 | for (int i = 1; i < unitOrder.length; i++) { 132 | if (unitBounds[unitOrder[i].ordinal()] > time) { 133 | return unitOrder[i - 1]; 134 | } 135 | } 136 | return unitOrder[unitOrder.length - 1]; 137 | } 138 | 139 | private static final String formatHisto(int[] histogramByXFactor) { 140 | return IntStream.of(histogramByXFactor).mapToObj(i -> String.format("%5d", i)).collect(Collectors.joining(" ")); 141 | } 142 | 143 | private static final String formatZoneTime(double[] zoneTimes) { 144 | return DoubleStream.of(zoneTimes).mapToObj(d -> String.format("%.3f", d)).collect(Collectors.joining(" ")); 145 | } 146 | 147 | private static final int logTwo(long numerator, long denominator) { 148 | long dividend = numerator / denominator; 149 | long tip = Long.highestOneBit(dividend); 150 | return Long.numberOfTrailingZeros(tip); 151 | } 152 | 153 | private final long[] results; 154 | private final long fastest; 155 | private final long slowest; 156 | private final long average; 157 | private final String suite; 158 | private final String name; 159 | private final int[] histogram; 160 | private final long p95ile; 161 | private final long p99ile; 162 | private final TimeUnit unit; 163 | private final int index; 164 | 165 | /** 166 | * Package Private: Construct statistics based on the nanosecond times of 167 | * multiple runs. 168 | *

169 | * Compute all derived statistics on the assumption that toString will be 170 | * called, and one comprehensive scan will have less effect than multiple 171 | * partial results. 172 | * 173 | * @param name 174 | * The name of the task that has been benchmarked 175 | * @param results 176 | * The nano-second run times of each successful run. 177 | */ 178 | UStats(String suit, String name, int index, long[] results) { 179 | this.suite = suit; 180 | this.name = name; 181 | this.index = index; 182 | 183 | if (results == null || results.length == 0) { 184 | this.results = new long[0]; 185 | fastest = 0; 186 | slowest = 0; 187 | p95ile = 0; 188 | p99ile = 0; 189 | 190 | average = 0; 191 | histogram = new int[0]; 192 | unit = findBestUnit(fastest); 193 | } else { 194 | this.results = results; 195 | // tmp is only used to compute percentile results. 196 | long[] tmp = Arrays.copyOf(results, results.length); 197 | Arrays.sort(tmp); 198 | 199 | fastest = tmp[0]; 200 | slowest = tmp[tmp.length - 1]; 201 | 202 | p95ile = tmp[(int) (tmp.length * 0.95)]; 203 | p99ile = tmp[(int) (tmp.length * 0.99)]; 204 | 205 | long sum = LongStream.of(results).sum(); 206 | average = sum / tmp.length; 207 | histogram = new int[logTwo(slowest, fastest) + 1]; 208 | for (long t : tmp) { 209 | histogram[logTwo(t, fastest)]++; 210 | } 211 | 212 | unit = findBestUnit(fastest); 213 | } 214 | 215 | } 216 | 217 | /** 218 | * The nanosecond time of the 95th percentile run. 219 | * 220 | * @return the nanosecond time of the 95th percentile run. 221 | */ 222 | public long get95thPercentileNanos() { 223 | return p95ile; 224 | } 225 | 226 | /** 227 | * The nanosecond time of the 99th percentile run. 228 | * 229 | * @return the nanosecond time of the 99th percentile run. 230 | */ 231 | public long get99thPercentileNanos() { 232 | return p99ile; 233 | } 234 | 235 | /** 236 | * The nanosecond time of the fastest run. 237 | * 238 | * @return the nanosecond time of the fastest run. 239 | */ 240 | public long getFastestNanos() { 241 | return fastest; 242 | } 243 | 244 | /** 245 | * The nanosecond time of the slowest run. 246 | * 247 | * @return the nanosecond time of the slowest run. 248 | */ 249 | public long getSlowestNanos() { 250 | return slowest; 251 | } 252 | 253 | /** 254 | * The nanosecond time of the average run. 255 | *

256 | * Note, this is in nanoseconds (using integer division of the total time / 257 | * count). Any sub-nano-second error is considered irrelevant 258 | * 259 | * @return the nanosecond time of the average run. 260 | */ 261 | public long getAverageRawNanos() { 262 | return average; 263 | } 264 | 265 | /** 266 | * Package Private: Used to identify the order in which the task was added to the UBench instance. 267 | * @return the index in UBench. 268 | */ 269 | int getIndex() { 270 | return index; 271 | } 272 | 273 | /** 274 | * Identify what a good time Unit would be to present the results in these 275 | * statistics. 276 | *

277 | * Calculated as the equivalent of 278 | * findBestUnit(getFastestRawNanos()) 279 | * 280 | * @return A time unit useful for scaling these statistical results. 281 | * @see UStats#findBestUnit(long) 282 | */ 283 | public TimeUnit getGoodUnit() { 284 | return unit; 285 | } 286 | 287 | /** 288 | * Get the raw data the statistics are based off. 289 | * 290 | * @return (a copy of) the individual test run times (in nanoseconds, and in order of 291 | * execution). 292 | */ 293 | public long[] getData() { 294 | return Arrays.copyOf(results, results.length); 295 | } 296 | 297 | /** 298 | * Summarize the time-progression of the run time for each iteration, in 299 | * order of execution (in milliseconds). 300 | *

301 | * An example helps. If there are 200 results, and a request for 10 zones, 302 | * then return 10 double values representing the average time of the first 303 | * 20 runs, then the next 20, and so on, until the 10th zone contains the 304 | * average time of the last 20 runs. 305 | *

306 | * This is a good way to see the effects of warm-up times and different 307 | * compile levels 308 | * 309 | * @param zoneCount 310 | * the number of zones to compute 311 | * @param timeUnit 312 | * the unit in which to report the times 313 | * @return an array of times (in the given unit) representing the average 314 | * time for all runs in the respective zone. 315 | */ 316 | public final double[] getZoneTimes(int zoneCount, TimeUnit timeUnit) { 317 | if (results.length == 0) { 318 | return new double[0]; 319 | } 320 | double[] ret = new double[Math.min(zoneCount, results.length)]; 321 | int perblock = results.length / ret.length; 322 | int overflow = results.length % ret.length; 323 | int pos = 0; 324 | double repFactor = unitFactor[timeUnit.ordinal()]; 325 | for (int block = 0; block < ret.length; block++) { 326 | int count = perblock + (block < overflow ? 1 : 0); 327 | int limit = pos + count; 328 | long nanos = 0; 329 | while (pos < limit) { 330 | nanos += results[pos]; 331 | pos++; 332 | } 333 | ret[block] = (nanos / repFactor) / count; 334 | } 335 | return ret; 336 | } 337 | 338 | /** 339 | * Compute a log-2-based histogram relative to the fastest run in the data 340 | * set. 341 | *

342 | * This gives a sense of what the general shape of the runs are in terms of 343 | * distribution of run times. The histogram is based on the fastest run. 344 | *

345 | * By way of an example, the output: 100, 50, 10, 1, 0, 1 would 346 | * suggest that: 347 | *

    348 | *
  • 100 runs were between 1 times and 2 times as slow as the fastest. 349 | *
  • 50 runs were between 2 and 4 times slower than the fastest. 350 | *
  • 10 runs were between 4 and 8 times slower 351 | *
  • 1 run was between 8 and 16 times slower 352 | *
  • 1 run was between 32 and 64 times slower 353 | *
354 | * 355 | * @return an int array containing the time distribution frequencies. 356 | */ 357 | public final int[] getDoublingHistogram() { 358 | return Arrays.copyOf(histogram, histogram.length); 359 | } 360 | 361 | /** 362 | * The 99th percentile of runtimes. 363 | *

364 | * 99% of all runs completed in this time, or faster. 365 | * 366 | * @param timeUnit 367 | * the unit in which to report the times 368 | * @return the time of the 99th percentile in the given time unit. 369 | */ 370 | public final double get99thPercentile(TimeUnit timeUnit) { 371 | return p99ile / unitFactor[timeUnit.ordinal()]; 372 | } 373 | 374 | /** 375 | * The 95th percentile of runtimes. 376 | *

377 | * 95% of all runs completed in this time, or faster. 378 | * 379 | * @param timeUnit 380 | * the unit in which to report the times 381 | * @return the time of the 95th percentile in the given time unit. 382 | */ 383 | public final double get95thPercentile(TimeUnit timeUnit) { 384 | return p95ile / unitFactor[timeUnit.ordinal()]; 385 | } 386 | 387 | /** 388 | * Compute the average time of all runs (in milliseconds). 389 | * 390 | * @param timeUnit 391 | * the unit in which to report the times 392 | * @return the average time (in milliseconds) 393 | */ 394 | public final double getAverage(TimeUnit timeUnit) { 395 | return average / unitFactor[timeUnit.ordinal()]; 396 | } 397 | 398 | /** 399 | * Compute the slowest run (in milliseconds). 400 | * 401 | * @param timeUnit 402 | * the unit in which to report the times 403 | * @return The slowest run time (in milliseconds). 404 | */ 405 | public final double getSlowest(TimeUnit timeUnit) { 406 | return slowest / unitFactor[timeUnit.ordinal()]; 407 | } 408 | 409 | /** 410 | * Compute the fastest run (in milliseconds). 411 | * 412 | * @param timeUnit 413 | * the unit in which to report the times 414 | * @return The fastest run time (in milliseconds). 415 | */ 416 | public final double getFastest(TimeUnit timeUnit) { 417 | return fastest / unitFactor[timeUnit.ordinal()]; 418 | } 419 | 420 | @Override 421 | public String toString() { 422 | return formatResults(unit); 423 | } 424 | 425 | /** 426 | * Represent this UStats as a name/value JSON structure. 427 | * 428 | * @return this data as a JSON-formatted string. 429 | */ 430 | public String toJSONString() { 431 | return TIME_PULLER.entrySet().stream() 432 | .map(me -> String.format("%s: %d", me.getKey(), me.getValue().applyAsLong(this))) 433 | .collect(Collectors.joining(", ", "{", "}")); 434 | } 435 | 436 | /** 437 | * Present the results from this task in a formatted string output. 438 | * @param tUnit the units in which to display the times (see {@link UStats#getGoodUnit() } for a suggestion). 439 | * @return A string representing the statistics. 440 | * @see UStats#getGoodUnit() 441 | */ 442 | public String formatResults(TimeUnit tUnit) { 443 | double avg = getAverage(tUnit); 444 | double fast = getFastest(tUnit); 445 | double slow = getSlowest(tUnit); 446 | double t95p = get95thPercentile(tUnit); 447 | double t99p = get99thPercentile(tUnit); 448 | int width = Math.max(8, DoubleStream.of(avg, fast, slow, t95p, t99p).mapToObj(d -> String.format("%.4f", d)) 449 | .mapToInt(String::length).max().getAsInt()); 450 | 451 | return String.format("Task %s -> %s: (Unit: %s)\n" 452 | + " Count : %" + width + "d Average : %" + width + ".4f\n" 453 | + " Fastest : %" + width + ".4f Slowest : %" + width + ".4f\n" 454 | + " 95Pctile : %" + width + ".4f 99Pctile : %" + width + ".4f\n" 455 | + " TimeBlock : %s\n" 456 | + " Histogram : %s\n", 457 | suite, name, unitName[tUnit.ordinal()], 458 | results.length, avg, 459 | fast, slow, 460 | t95p, t99p, 461 | formatZoneTime(getZoneTimes(10, tUnit)), 462 | formatHisto(getDoublingHistogram())); 463 | } 464 | 465 | /** 466 | * Retrieve the number of iterations that were executed. 467 | * @return the number of runs. 468 | */ 469 | public int getCount() { 470 | return results.length; 471 | } 472 | 473 | /** 474 | * The name of the UBench Suite this task was run in. 475 | * @return the suite name. 476 | */ 477 | public String getSuiteName() { 478 | return suite; 479 | } 480 | 481 | /** 482 | * The name of the UBench task these statistics are from. 483 | * @return the task name. 484 | */ 485 | public String getName() { 486 | return name; 487 | } 488 | 489 | } -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/UUtils.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.lang.Thread.UncaughtExceptionHandler; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | import java.util.logging.Handler; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.stream.LongStream; 13 | import java.util.stream.Stream; 14 | 15 | /** 16 | * Collection of common static utility methods used in other areas of the 17 | * package. 18 | * 19 | * @author rolf 20 | * 21 | */ 22 | public final class UUtils { 23 | 24 | private UUtils() { 25 | // private constructor, no instances possible. 26 | } 27 | 28 | // Create a LOGGER instance for the **PACKAGE**. 29 | // This will be the owner of the ubench namespace. 30 | private static final Logger LOGGER = Logger.getLogger(UScale.class.getPackage().getName()); 31 | 32 | /** 33 | * Simple wrapper that forces initialization of the package-level LOGGER 34 | * instance. 35 | * 36 | * @param clazz 37 | * The class to be logged. 38 | * @return A Logger using the name of the class as its hierarchy. 39 | */ 40 | public static Logger getLogger(final Class clazz) { 41 | if (!clazz.getPackage().getName().startsWith(LOGGER.getName())) { 42 | throw new IllegalArgumentException(String.format("Class %s is not a child of the package %s", 43 | clazz.getName(), LOGGER.getName())); 44 | } 45 | LOGGER.fine(() -> String.format("Locating logger for class %s", clazz)); 46 | return Logger.getLogger(clazz.getName()); 47 | } 48 | 49 | /** 50 | * Enable regular logging for all UBench code at the specified level. 51 | * 52 | * @param level 53 | * the level to log for. 54 | */ 55 | public static void setStandaloneLogging(Level level) { 56 | LOGGER.setUseParentHandlers(false); 57 | for (Handler h : LOGGER.getHandlers()) { 58 | LOGGER.removeHandler(h); 59 | } 60 | StdoutHandler handler = new StdoutHandler(); 61 | handler.setFormatter(new InlineFormatter()); 62 | LOGGER.addHandler(handler); 63 | 64 | final UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); 65 | Thread.setDefaultUncaughtExceptionHandler((t, e) -> { 66 | LOGGER.log(Level.SEVERE, "Uncaught Exception in thread " + t.getName(), e); 67 | if (ueh != null) { 68 | ueh.uncaughtException(t, e); 69 | } 70 | }); 71 | 72 | setLogLevel(level); 73 | } 74 | 75 | /** 76 | * Enable the specified debug level messages to be output. Note that both 77 | * this Logger and whatever Handler you use, have to be set to enable the 78 | * required log level for the handler to output the messages. If this UBench 79 | * code is logging 'stand alone' then this method will also change the 80 | * output level of the log handlers. 81 | * 82 | * @param level The log level to activate for future log levels. 83 | */ 84 | public static void setLogLevel(Level level) { 85 | // all other ubench loggers inherit from here. 86 | LOGGER.finer("Changing logging from " + LOGGER.getLevel()); 87 | LOGGER.setLevel(level); 88 | if (!LOGGER.getUseParentHandlers()) { 89 | LOGGER.setLevel(level); 90 | Stream.of(LOGGER.getHandlers()).forEach(h -> h.setLevel(level)); 91 | } 92 | LOGGER.finer("Changed logging to " + LOGGER.getLevel()); 93 | } 94 | 95 | private static final AtomicLong NANO_TICK = new AtomicLong(-1); 96 | /** 97 | * The minimum increment that the System.nanotime() can do. The nanotime 98 | * value returned by the system does not necessarily increment by 1, this 99 | * value indicates the smallest observed increment of the timer. 100 | * @return The number of nanoseconds in the smallest recorded clock tick. 101 | */ 102 | public static long getNanoTick() { 103 | synchronized(NANO_TICK) { 104 | long tick = NANO_TICK.get(); 105 | if (tick > 0) { 106 | return tick; 107 | } 108 | tick = computeTick(); 109 | NANO_TICK.set(tick); 110 | return tick; 111 | } 112 | } 113 | 114 | private static final long singleTick() { 115 | 116 | final long start = System.nanoTime(); 117 | long end = start; 118 | while (end == start) { 119 | end = System.nanoTime(); 120 | } 121 | return end - start; 122 | 123 | } 124 | 125 | private static long computeTick() { 126 | final long ticklen = LongStream.range(0, 1000).map(i -> singleTick()).min().getAsLong(); 127 | LOGGER.fine(() -> String.format("Incremental System.nanotime() tick is %d", ticklen)); 128 | return ticklen; 129 | } 130 | 131 | /** 132 | * Load a resource stored in the classpath, as a String. 133 | * 134 | * @param path 135 | * the system resource to read 136 | * @return the resource as a String. 137 | */ 138 | public static String readResource(String path) { 139 | final long start = System.nanoTime(); 140 | try (InputStream is = UScale.class.getClassLoader().getResourceAsStream(path);) { 141 | int len = 0; 142 | byte[] buffer = new byte[2048]; 143 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 144 | while ((len = is.read(buffer)) >= 0) { 145 | baos.write(buffer, 0, len); 146 | } 147 | return new String(baos.toByteArray(), StandardCharsets.UTF_8); 148 | } catch (IOException e) { 149 | LOGGER.log(Level.WARNING, e, () -> "IOException loading resource " + path); 150 | throw new IllegalStateException("Unable to read class loaded stream " + path, e); 151 | } catch (RuntimeException re) { 152 | LOGGER.log(Level.WARNING, re, () -> "Unexpected exception loading resource " + path); 153 | throw re; 154 | } finally { 155 | LOGGER.fine(() -> String.format("Loaded resource %s in %.3fms", path, 156 | (System.nanoTime() - start) / 1000000.0)); 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/scale/MathEquation.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench.scale; 2 | 3 | import java.util.Arrays; 4 | import java.util.function.DoubleUnaryOperator; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.DoubleStream; 7 | 8 | /** 9 | * A function that describes one of the standard scaling equations 10 | * 11 | * @author Simon Forsberg 12 | */ 13 | public class MathEquation { 14 | 15 | private final DoubleUnaryOperator equation; 16 | private final double[] parameters; 17 | private final String format; 18 | private final double rSquared; 19 | private final MathModel model; 20 | 21 | /** 22 | * A function that describes one of the standard scaling equations 23 | * 24 | * @param model 25 | * the model this function is based on 26 | * @param equation 27 | * the x-to-y equation for this instance 28 | * @param parameters 29 | * the parameters describing the required coefficients in the 30 | * equation 31 | * @param format 32 | * the string format for the equation 33 | * @param rSquared 34 | * the measure of the accuracy of this equation against the 35 | * actual results. 36 | */ 37 | public MathEquation(MathModel model, DoubleUnaryOperator equation, double[] parameters, String format, 38 | double rSquared) { 39 | this.model = model; 40 | this.equation = equation; 41 | this.parameters = parameters; 42 | this.format = format; 43 | this.rSquared = rSquared; 44 | } 45 | 46 | /** 47 | * Get a text-based description of this equation 48 | * 49 | * @return the string version of this equation 50 | */ 51 | public String getDescription() { 52 | Object[] params = new Object[parameters.length]; 53 | for (int i = 0; i < parameters.length; i++) { 54 | params[i] = parameters[i]; 55 | } 56 | return String.format(format, params); 57 | } 58 | 59 | /** 60 | * Get the parameters representing the various coefficients in this equation 61 | * 62 | * @return a copy of the equation coefficients 63 | */ 64 | public double[] getParameters() { 65 | return Arrays.copyOf(parameters, parameters.length); 66 | } 67 | 68 | /** 69 | * Get a function representing the x-to-y transform for this eqation 70 | * 71 | * @return an equation transforming an x position to a y offset. 72 | */ 73 | public DoubleUnaryOperator getEquation() { 74 | return equation; 75 | } 76 | 77 | /** 78 | * A String.format template for presenting the equation with its parameters 79 | * 80 | * @return A String format specification suitable for the parameters. 81 | */ 82 | public String getFormat() { 83 | return format; 84 | } 85 | 86 | /** 87 | * Get the accuracy measure for this equation against the actual results. A 88 | * value of 1.0 is a compelte match against the actual results, a value 89 | * close to zero is a fail-to-match 90 | * 91 | * @return the r-squared value representing this equation's accuracy 92 | */ 93 | public double getRSquared() { 94 | return rSquared; 95 | } 96 | 97 | /** 98 | * Get the mathematical model this equation is based on. 99 | * 100 | * @return the underlying model. 101 | */ 102 | public MathModel getModel() { 103 | return model; 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return getDescription() + " with precision " + rSquared; 109 | } 110 | 111 | /** 112 | * Convert this equation in to a JSON string representing the vital 113 | * statistics of the equation. 114 | * 115 | * @return a JSON interpretation of this equation. 116 | */ 117 | public String toJSONString() { 118 | String parms = DoubleStream.of(parameters) 119 | .mapToObj(d -> String.format("%f", d)) 120 | .collect(Collectors.joining(", ", "[", "]")); 121 | 122 | String desc = String.format(format, DoubleStream.of(parameters).mapToObj(Double::valueOf).toArray()); 123 | return String.format( 124 | "{name: \"%s\", valid: %s, format: \"%s\", description: \"%s\", parameters: %s, rsquare: %f}", 125 | model.getName(), isValid() ? "true" : "false", format, desc, parms, rSquared); 126 | } 127 | 128 | /** 129 | * Indicate whether this equation is a suitable match against the actual 130 | * data. 131 | * 132 | * @return true if this equation is useful when representing the actual 133 | * data's scalability 134 | */ 135 | public boolean isValid() { 136 | return Math.abs(parameters[0]) >= 0.001 && rSquared != Double.NEGATIVE_INFINITY && !Double.isNaN(rSquared); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/scale/MathModel.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench.scale; 2 | 3 | import java.util.Arrays; 4 | import java.util.function.DoubleUnaryOperator; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * A MathModel describes an abstract scaling function that could massaged to fit 9 | * the actual scaling of empirical data. 10 | * 11 | * @author Simon Forsberg 12 | */ 13 | public class MathModel { 14 | 15 | private final Function function; 16 | private final double[] initialValues; 17 | private final String format; 18 | private final String name; 19 | 20 | /** 21 | * Create a MathModel to describe a scaling function. 22 | * 23 | * @param name 24 | * the name of the model 25 | * @param format 26 | * the format for displaying the coefficients and variables in 27 | * the model 28 | * @param math 29 | * the function that converts the supplied array of coefficients 30 | * in to a concrete instance of this model represented as a 31 | * function too. 32 | * @param initialValues 33 | * sane initial values for estimating and fitting the function 34 | * against actual data. 35 | */ 36 | public MathModel(String name, String format, Function math, double[] initialValues) { 37 | this.name = name; 38 | this.function = math; 39 | this.initialValues = initialValues; 40 | this.format = format; 41 | } 42 | 43 | /** 44 | * Get the initial values useful as a starting point for applying this 45 | * function to actual results. 46 | * 47 | * @return the coefficients to start analysis with. 48 | */ 49 | public double[] getInitialValues() { 50 | return Arrays.copyOf(initialValues, initialValues.length); 51 | } 52 | 53 | /** 54 | * Converts an array of coefficients in to a concrete function 55 | * 56 | * @param params 57 | * The coefficents to use 58 | * @return a function that applies this model using the supplied 59 | * coefficients. 60 | */ 61 | public DoubleUnaryOperator createFunction(double[] params) { 62 | return function.apply(params); 63 | } 64 | 65 | /** 66 | * Get a format string that displays the coefficients in a way that 67 | * represents this model 68 | * 69 | * @return a String.format template representing this model. 70 | */ 71 | public String getFormat() { 72 | return format; 73 | } 74 | 75 | /** 76 | * Get the name this model was created with. 77 | * 78 | * @return the Model name. 79 | */ 80 | public String getName() { 81 | return name; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return format; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/net/tuis/ubench/scale/Models.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench.scale; 2 | 3 | import java.util.function.DoubleUnaryOperator; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Container of standard Scaling models. 8 | * 9 | * @author Simon Forsberg 10 | */ 11 | public class Models { 12 | 13 | /** 14 | * Represents O(an) 15 | */ 16 | public static final MathModel EXPONENTIAL = new MathModel("O(a^n)", "%f ^ n", 17 | params -> x -> Math.pow(params[0], x), new double[] { 2 }); 18 | 19 | /** 20 | * Represents O(n log n) 21 | */ 22 | public static final MathModel N_LOG_N = new MathModel("O(n log n)", "%f * n log n", 23 | params -> x -> params[0] * x * Math.log10(x), new double[] { 1 }); 24 | 25 | /** 26 | * Represents O(log n) 27 | */ 28 | public static final MathModel LOG_N = new MathModel("O(log n)", "%f * log n", 29 | params -> x -> params[0] * Math.log10(x), new double[] { 1 }); 30 | 31 | /** 32 | * Represents O(1) 33 | */ 34 | public static final MathModel CONSTANT = createPolynom(0); 35 | 36 | /** 37 | * Represents O(n) 38 | */ 39 | public static final MathModel LINEAR = createPolynom(1); 40 | 41 | /** 42 | * Represents O(n2) 43 | */ 44 | public static final MathModel N_SQUARED = createPolynom(2); 45 | 46 | /** 47 | * Create an nth degree polynomial model. 48 | * 49 | * e.g. createPolynom(4) would create an O(n4) 50 | * model. 51 | * 52 | * @param degree 53 | * the polynomial degree 54 | * @return a MathModel representing the specified degree. 55 | */ 56 | public static MathModel createPolynom(int degree) { 57 | if (degree < 0) { 58 | throw new IllegalArgumentException("Degree must be positive"); 59 | } 60 | double[] params = new double[degree + 1]; 61 | params[0] = 1; 62 | StringBuilder format = new StringBuilder(); 63 | for (int i = degree; i >= 0; i--) { 64 | if (i > 0) { 65 | format.append("%f*n"); 66 | if (i > 1) { 67 | format.append('^'); 68 | format.append(i); 69 | } 70 | format.append(" + "); 71 | } 72 | } 73 | format.append("%f"); 74 | Function equation = new Function() { 75 | @Override 76 | public DoubleUnaryOperator apply(double[] doubles) { 77 | return x -> { 78 | double sum = 0; 79 | for (int i = degree; i >= 0; i--) { 80 | sum += doubles[degree - i] * Math.pow(x, i); 81 | } 82 | return sum; 83 | }; 84 | } 85 | }; 86 | String name; 87 | switch (degree) { 88 | case 0: 89 | name = "O(1)"; 90 | break; 91 | case 1: 92 | name = "O(n)"; 93 | break; 94 | default: 95 | name = "O(n^" + degree + ")"; 96 | break; 97 | } 98 | return new MathModel(name, format.toString(), equation, params); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/resources/net/tuis/ubench/scale/UScale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ${TITLE} 5 | 6 | 7 | 8 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |

UScale Report - ${TITLE}

134 | 135 |
136 | 137 |
138 |
139 | 140 | 141 | 142 | 146 | 150 | 154 | 155 |
  143 | 144 | 145 | 147 | 148 | 149 | 151 | 152 | 153 |
156 |
157 | 158 |
159 |
160 |
Data
161 |
162 | 163 | 164 | 231 | 232 | 734 | 735 | 736 | 737 | 738 | 739 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/AnalyzeTest.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import net.tuis.ubench.scale.MathEquation; 4 | import net.tuis.ubench.scale.Models; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | /** 11 | * @author Simon Forsberg 12 | */ 13 | @SuppressWarnings("javadoc") 14 | public class AnalyzeTest { 15 | 16 | @Test 17 | public void nSquared() { 18 | MathEquation eq = ScaleDetect.detect(new double[]{42, 107, 73, 120}, new double[]{511, 312, 400, 242}, Models.N_SQUARED); 19 | assertArrayEquals(new double[]{ -0.0021060, -2.947499, 635.60559 }, eq.getParameters(), 0.001); 20 | } 21 | 22 | @Test 23 | public void linear() { 24 | MathEquation eq = ScaleDetect.detect(new double[]{1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0, 8192.0, 16384.0, 32768.0, 65536.0, 131072.0, 262144.0, 524288.0}, 25 | new double[]{905.0, 901.0, 939.0, 927.0, 920.0, 898.0, 884.0, 861.0, 852.0, 864.0, 869.0, 867.0, 866.0, 867.0, 857.0, 857.0, 854.0, 855.0, 872.0, 865.0}, 26 | Models.LINEAR); 27 | assertArrayEquals(new double[]{ -0.000048924907, 881.5650 }, eq.getParameters(), 0.001); 28 | } 29 | 30 | @Test 31 | public void nLogN() { 32 | MathEquation eq = ScaleDetect.detect(new double[]{1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0, 8192.0, 16384.0, 32768.0, 65536.0, 131072.0, 262144.0, 524288.0}, 33 | new double[]{857.0, 860.0, 898.0, 975.0, 993.0, 1601.0, 1530.0, 2947.0, 6106.0, 16111.0, 35937.0, 80497.0, 184819.0, 390424.0, 847658.0, 1820366.0, 4095873.0, 8463674.0, 17483933, 39126742}, 34 | Models.N_LOG_N); 35 | assertArrayEquals(new double[]{ 12.9000747 }, eq.getParameters(), 0.001); 36 | } 37 | 38 | @Test 39 | public void exponentialNaN() { 40 | double[] expX = { 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0, 8192.0, 16384.0, 14336.0, 12288.0, 10240.0, 6144.0 }; 41 | double[] expY = { 488.0, 488.0, 488.0, 488.0, 977.0, 3422.0, 11733.0, 44000.0, 170134.0, 672711.0, 2614089.0, 1.0346356E7, 4.1465605E7, 1.68587977E8, 6.8453302E8, 5.13354909E8, 3.99514184E8, 2.639047E8, 9.4308146E7 }; 42 | MathEquation eq = ScaleDetect.detect(expX, expY, Models.EXPONENTIAL); 43 | assertArrayEquals(new double[]{ Double.NaN }, eq.getParameters(), 0.001); 44 | } 45 | 46 | @Test 47 | public void exponential() { 48 | double[] expX = new double[]{1.0, 2.0, 4.0, 8.0, 16.0}; 49 | double[] expY = new double[]{2.3, 5.6, 32, 955, 913480}; 50 | MathEquation eq = ScaleDetect.detect(expX, expY, Models.EXPONENTIAL); 51 | assertArrayEquals(new double[]{ 2.3579999 }, eq.getParameters(), 0.001); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/DataRandomizer.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.concurrent.ConcurrentMap; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | import java.util.stream.IntStream; 8 | 9 | /** 10 | * @author rolfl 11 | * @author Simon Forsberg 12 | */ 13 | @SuppressWarnings("javadoc") 14 | public class DataRandomizer { 15 | 16 | private static final ConcurrentMap arrayCounts = new ConcurrentHashMap<>(); 17 | 18 | public static final int[] randomData(int size) { 19 | arrayCounts.computeIfAbsent(size, key -> new AtomicInteger(0)).incrementAndGet(); 20 | // System.out.println("Randomizing " + size); 21 | Random rand = new Random(size); 22 | return IntStream.generate(rand::nextInt).limit(size).toArray(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/ExampleCode.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | import java.util.concurrent.TimeUnit; 5 | import java.util.function.ToIntFunction; 6 | import java.util.logging.Level; 7 | import java.util.stream.IntStream; 8 | 9 | /** 10 | * Simple example test of UBench 11 | * 12 | * @author rolf 13 | * 14 | */ 15 | public class ExampleCode { 16 | 17 | private static final ToIntFunction charcount = (line) -> (int) IntStream.range(0, line.length()) 18 | .map(i -> line.charAt(i)).distinct().count(); 19 | 20 | private static final int countDistinctChars(String line) { 21 | if (line.length() <= 1) { 22 | return line.length(); 23 | } 24 | char[] chars = line.toCharArray(); 25 | Arrays.sort(chars); 26 | int count = 1; 27 | for (int i = 1; i < chars.length; i++) { 28 | if (chars[i] != chars[i - 1]) { 29 | count++; 30 | } 31 | } 32 | return count; 33 | 34 | } 35 | 36 | /** 37 | * Test entry point. 38 | * 39 | * @param args 40 | * ignored. 41 | */ 42 | public static void main(String[] args) { 43 | UUtils.setStandaloneLogging(Level.INFO); 44 | final String testdata = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"; 45 | final String hello = "Hello World!"; 46 | 47 | UBench bench = new UBench("distinct chars") 48 | .addIntTask("Functional alphas", () -> charcount.applyAsInt(testdata), g -> g == 63) 49 | .addIntTask("Functional hello", () -> charcount.applyAsInt(hello), g -> g == 9) 50 | 51 | .addIntTask("Traditional alphas", () -> countDistinctChars(testdata), g -> g == 63) 52 | .addIntTask("Traditional hello", () -> countDistinctChars(hello), g -> g == 9); 53 | 54 | bench.press(100000, 1000, 10.0, 500, TimeUnit.MILLISECONDS).report("Warmup"); 55 | bench.press(UMode.SEQUENTIAL, 100000, 1000, 10.0, 500, TimeUnit.MILLISECONDS).report("Sequential"); 56 | bench.press(UMode.PARALLEL, 100000, 1000, 10.0, 500, TimeUnit.MILLISECONDS).report("Parallel"); 57 | bench.press(UMode.INTERLEAVED, 100000, 1000, 10.0, 500, TimeUnit.MILLISECONDS).report("Interleaved"); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/ExampleScales.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Paths; 5 | import java.util.Arrays; 6 | import java.util.Random; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import java.util.logging.Level; 11 | import java.util.stream.IntStream; 12 | 13 | @SuppressWarnings("javadoc") 14 | public class ExampleScales { 15 | 16 | private static final ConcurrentMap arrayCounts = new ConcurrentHashMap<>(); 17 | 18 | private static final int[] randomData(int size) { 19 | arrayCounts.computeIfAbsent(size, key -> new AtomicInteger(0)).incrementAndGet(); 20 | // System.out.println("Randomizing " + size); 21 | Random rand = new Random(size); 22 | return IntStream.generate(rand::nextInt).limit(size).toArray(); 23 | } 24 | 25 | private static final long linear(long input) { 26 | long count = 0; 27 | while (input > 10) { 28 | input -= 10; 29 | count++; 30 | } 31 | return count; 32 | } 33 | 34 | public static void main(String[] args) throws IOException { 35 | UUtils.setStandaloneLogging(Level.INFO); 36 | //UScale.function(div -> div / 3, scale -> scale).report(); 37 | 38 | UScale.function( 39 | "Linear", 40 | data -> linear(data), 41 | scale -> scale, true) 42 | .reportHTML(Paths.get("output/Linear.html")); 43 | 44 | if (!Boolean.getBoolean("DOALL")) { 45 | return; 46 | } 47 | 48 | UScale scales = UScale.consumer("Arrays::Sort", Arrays::sort, scale -> randomData(scale), false); 49 | 50 | scales.report(); 51 | scales.reportHTML(Paths.get("output/ArraysSort.html")); 52 | System.out.println(scales.toJSONString()); 53 | 54 | 55 | arrayCounts.keySet().stream().sorted() 56 | .map(scale -> String.format("Scale %d -> created %d", scale, arrayCounts.get(scale).get())) 57 | .forEach(System.out::println); 58 | 59 | UScale.consumer("Arrays::Sort (presorted)", Arrays::sort, scale -> randomData(scale), true).report(); 60 | 61 | arrayCounts.keySet().stream().sorted() 62 | .map(scale -> String.format("Scale %d -> created %d", scale, arrayCounts.get(scale).get())) 63 | .forEach(System.out::println); 64 | 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/PrimeSieve.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | 5 | @SuppressWarnings("javadoc") 6 | public class PrimeSieve { 7 | 8 | /** 9 | * Basic "Sieve of Eratosthenes" implementation, no optimizations 10 | * @param limit only primes less than this limit will be calculated. 11 | * @return the largest prime less than the supplied limit. 12 | */ 13 | public static final int getMaxPrimeBefore(int limit) { 14 | boolean[] sieve = new boolean[limit]; 15 | Arrays.fill(sieve, true); 16 | sieve[0] = false; 17 | sieve[1] = false; 18 | int largest = 0; 19 | for (int p = 2; p < limit; p++) { 20 | if (sieve[p]) { 21 | largest = p; 22 | for (int np = p * 2; np < limit; np += p) { 23 | sieve[np] = false; 24 | } 25 | } 26 | } 27 | return largest; 28 | } 29 | 30 | public static final int getMaxPrimeBeforeNeg(int limit) { 31 | boolean[] sieve = new boolean[limit]; 32 | // Arrays.fill(sieve, true); 33 | sieve[0] = true; 34 | sieve[1] = true; 35 | int largest = 0; 36 | for (int p = 2; p < limit; p++) { 37 | if (!sieve[p]) { 38 | largest = p; 39 | for (int np = p * 2; np < limit; np += p) { 40 | sieve[np] = true; 41 | } 42 | } 43 | } 44 | return largest; 45 | } 46 | 47 | private static void benchSimple() { 48 | UBench bench = new UBench("Simple Performance"); 49 | bench.addIntTask("Prime less than 4000", () -> getMaxPrimeBefore(4000), p -> p == 3989); 50 | bench.press(10000).report("Simple Performance"); 51 | } 52 | 53 | private static void benchSimpleNeg() { 54 | UBench bench = new UBench("Negate Performance"); 55 | bench.addIntTask("Prime less than 4000", () -> getMaxPrimeBeforeNeg(4000), p -> p == 3989); 56 | bench.press(10000).report("Negated Performance"); 57 | } 58 | 59 | private static void benchCompare() { 60 | UBench bench = new UBench("Comparative Performance"); 61 | 62 | bench.addIntTask("Primes Filled", () -> getMaxPrimeBefore(4000), p -> p == 3989); 63 | bench.addIntTask("Primes Negated", () -> getMaxPrimeBeforeNeg(4000), p -> p == 3989); 64 | 65 | bench.press(10000).report("Effects of Arrays.fill()"); 66 | } 67 | 68 | private static void benchScale() { 69 | UBench bench = new UBench("Sieve Scalability"); 70 | 71 | int[] limits = {250, 500, 1000, 2000, 4000, 8000}; 72 | int[] primes = {241, 499, 997, 1999, 3989, 7993}; 73 | 74 | for (int i = 0; i < limits.length; i++) { 75 | final int limit = limits[i]; 76 | final int check = primes[i]; 77 | bench.addIntTask("Primes " + limit, () -> getMaxPrimeBefore(limit), p -> p == check); 78 | } 79 | 80 | bench.press(UMode.SEQUENTIAL, 10000).report("Prime Scalability"); 81 | } 82 | 83 | public static void main(String[] args) { 84 | 85 | benchSimple(); 86 | benchSimpleNeg(); // to warm up equally. 87 | benchCompare(); 88 | benchScale(); 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/RandomSorter.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import java.util.Arrays; 4 | import java.util.Random; 5 | import java.util.function.Predicate; 6 | import java.util.function.Supplier; 7 | import java.util.stream.IntStream; 8 | 9 | 10 | @SuppressWarnings("javadoc") 11 | public class RandomSorter { 12 | 13 | public static void main(String[] args) { 14 | Random random = new Random(); 15 | 16 | final int[] data = IntStream.generate(random::nextInt).limit(10000).toArray(); 17 | final int[] sorted = Arrays.stream(data).sorted().toArray(); 18 | 19 | Predicate validate = v -> Arrays.equals(v, sorted); 20 | 21 | Supplier stream = () -> Arrays.stream(data).sorted().toArray(); 22 | 23 | Supplier trad = () -> { 24 | int[] copy = Arrays.copyOf(data, data.length); 25 | Arrays.sort(copy); 26 | return copy; 27 | }; 28 | 29 | new UBench("Sort Algorithms") 30 | .addTask("Functional", stream, validate) 31 | .addTask("Traditional", trad, validate) 32 | .press(10000).report("With Warmup"); 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/ScaleTest.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.util.Arrays; 7 | import java.util.logging.Level; 8 | 9 | import net.tuis.ubench.scale.MathEquation; 10 | import net.tuis.ubench.scale.Models; 11 | 12 | import org.junit.BeforeClass; 13 | import org.junit.Test; 14 | 15 | /** 16 | * @author Simon Forsberg 17 | */ 18 | @SuppressWarnings("javadoc") 19 | public class ScaleTest { 20 | 21 | @BeforeClass 22 | public static void setup() { 23 | UUtils.setStandaloneLogging(Level.FINE); 24 | } 25 | 26 | @Test 27 | public void bubbleSort() { 28 | MathEquation eq = UScale.consumer("BubbleSort", arr -> bubbleSort(arr), i -> DataRandomizer.randomData(i), false).determineBestFit(); 29 | assertTrue(eq.isValid()); 30 | assertEquals(Models.N_SQUARED, eq.getModel()); 31 | } 32 | 33 | @Test 34 | public void integerDivide() { 35 | UScale scale = UScale.function("Int Divide", i -> i / 3, i -> i, false); 36 | MathEquation[] eqs = scale.fitEquations(); 37 | MathEquation eq = scale.determineBestFit(); 38 | System.out.println(Arrays.toString(eqs)); 39 | System.out.println(eq); 40 | double[] fastest = scale.getStats().stream().mapToDouble(s -> s.getFastestNanos()).toArray(); 41 | double[] avg = scale.getStats().stream().mapToDouble(s -> s.getAverageRawNanos()).toArray(); 42 | int[] scales = scale.getStats().stream().mapToInt(s -> s.getIndex()).toArray(); 43 | System.out.println(Arrays.toString(fastest)); 44 | System.out.println(Arrays.toString(avg)); 45 | System.out.println(Arrays.toString(scales)); 46 | scale.report(); 47 | assertEquals(Models.CONSTANT, eq.getModel()); 48 | } 49 | 50 | @Test 51 | public void linear() { 52 | MathEquation eq = UScale.function("Linear", data -> linear(data), scale -> scale, true) 53 | .determineBestFit(); 54 | assertEquals(Models.LINEAR, eq.getModel()); 55 | } 56 | 57 | @Test 58 | public void arraySort() { 59 | UScale scales = UScale.consumer("Array Sort", Arrays::sort, scale -> DataRandomizer.randomData(scale), false); 60 | MathEquation eq = scales.determineBestFit(); 61 | assertEquals(Models.N_LOG_N, eq.getModel()); 62 | } 63 | 64 | private static void bubbleSort(int[] data) { 65 | for (int i = 0; i < data.length; i++) { 66 | for (int j = 0; j < data.length - 1; j++) { 67 | if (i != j) { 68 | int a = data[j]; 69 | int b = data[j + 1]; 70 | if (a > b) { 71 | int temp = data[j]; 72 | data[j] = data[j + 1]; 73 | data[j + 1] = temp; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | private static final long linear(long input) { 81 | long count = 0; 82 | while (input > 10) { 83 | input -= 10; 84 | count++; 85 | } 86 | return count; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/TestTaskStats.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.stream.LongStream; 7 | 8 | import org.junit.Test; 9 | 10 | @SuppressWarnings("javadoc") 11 | public class TestTaskStats { 12 | 13 | @Test 14 | public void testEmptyResults() { 15 | UStats stats = new UStats("test", "test", 1, new long[0]); 16 | assertEquals(0, stats.getFastestNanos()); 17 | assertEquals(0, stats.getSlowestNanos()); 18 | assertArrayEquals(new int[0], stats.getDoublingHistogram()); 19 | assertEquals(0, stats.get95thPercentileNanos()); 20 | assertEquals(0, stats.get99thPercentileNanos()); 21 | assertEquals(0, stats.getAverageRawNanos()); 22 | assertArrayEquals(new double[0], stats.getZoneTimes(10, TimeUnit.MICROSECONDS), 0.0); 23 | } 24 | 25 | @Test 26 | public void testSingleResults() { 27 | UStats stats = new UStats("test", "test", 1, new long[]{100}); 28 | assertEquals(100, stats.getFastestNanos()); 29 | assertEquals(100, stats.getSlowestNanos()); 30 | assertArrayEquals(new int[]{1}, stats.getDoublingHistogram()); 31 | assertEquals(100, stats.get95thPercentileNanos()); 32 | assertEquals(100, stats.get99thPercentileNanos()); 33 | assertEquals(100, stats.getAverageRawNanos()); 34 | assertArrayEquals(new double[]{100}, stats.getZoneTimes(10, TimeUnit.NANOSECONDS), 0.0); 35 | } 36 | 37 | @Test 38 | public void testGetZoneTimesMilliSimple() { 39 | long[] times = { 1, 2, 3, 4, 5 }; 40 | UStats stats = new UStats("test", "test", 1, times); 41 | double[] zones = LongStream.of(times).mapToDouble(t -> t / 1000000.0).toArray(); 42 | assertArrayEquals(zones, stats.getZoneTimes(times.length, TimeUnit.MILLISECONDS), 0.0); 43 | } 44 | 45 | @Test 46 | public void testGetZoneTimesMilliUnEven() { 47 | long[] times = { 100, 100, 200, 200, 300, 400, 500 }; 48 | long[] expect = { 100, 200, 300, 400, 500 }; 49 | UStats stats = new UStats("test", "test", 1, times); 50 | double[] zones = LongStream.of(expect).mapToDouble(t -> t / 1000000.0).toArray(); 51 | assertArrayEquals(zones, stats.getZoneTimes(5, TimeUnit.MILLISECONDS), 0.0); 52 | } 53 | 54 | @Test 55 | public void testGetHistogramByDoublingFactor() { 56 | long[] times = new long[1000]; 57 | for (int i = 0; i < times.length; i++) { 58 | times[i] = (i + 1) * 100; 59 | } 60 | int[] expect = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 489 }; 61 | UStats stats = new UStats("test", "test", 1, times); 62 | assertArrayEquals(expect, stats.getDoublingHistogram()); 63 | } 64 | 65 | @Test 66 | public void testGet95thPercentileSmall() { 67 | long[] times = { 100, 100, 200, 200, 300, 400, 500 }; 68 | UStats stats = new UStats("test", "test", 1, times); 69 | assertEquals(0.0005, stats.get95thPercentile(TimeUnit.MILLISECONDS), 0.0); 70 | } 71 | 72 | @Test 73 | public void testGet95thPercentileLarge() { 74 | long[] times = new long[1000]; 75 | for (int i = 0; i < times.length; i++) { 76 | times[i] = (i + 1) * 100; 77 | } 78 | double expect = times[950] / 1000000.0; 79 | UStats stats = new UStats("test", "test", 1, times); 80 | assertEquals(expect, stats.get95thPercentile(TimeUnit.MILLISECONDS), 0.0); 81 | } 82 | 83 | @Test 84 | public void testGet99thPercentileSmall() { 85 | long[] times = { 100, 100, 200, 200, 300, 400, 500 }; 86 | UStats stats = new UStats("test", "test", 1, times); 87 | assertEquals(0.0005, stats.get99thPercentile(TimeUnit.MILLISECONDS), 0.0); 88 | } 89 | 90 | @Test 91 | public void testGet99thPercentileLarge() { 92 | long[] times = new long[1000]; 93 | for (int i = 0; i < times.length; i++) { 94 | times[i] = (i + 1) * 100; 95 | } 96 | double expect = times[990] / 1000000.0; 97 | UStats stats = new UStats("test", "test", 1, times); 98 | assertEquals(expect, stats.get99thPercentile(TimeUnit.MILLISECONDS), 0.0); 99 | } 100 | 101 | @Test 102 | public void testGetAverage() { 103 | long[] times = new long[1000]; 104 | long sum = 0; 105 | for (int i = 0; i < times.length; i++) { 106 | times[i] = (i + 1) * 100; 107 | sum += times[i]; 108 | } 109 | double expect = (sum / 1000000.0) / times.length; 110 | UStats stats = new UStats("test", "test", 1, times); 111 | assertEquals(expect, stats.getAverage(TimeUnit.MILLISECONDS), 0.0); 112 | } 113 | 114 | @Test 115 | public void testGetSlowest() { 116 | long[] times = { 100, 100, 200, 200, 300, 400, 500 }; 117 | UStats stats = new UStats("test", "test", 1, times); 118 | assertEquals(0.0005, stats.getSlowest(TimeUnit.MILLISECONDS), 0.0); 119 | } 120 | 121 | @Test 122 | public void testGetFastest() { 123 | long[] times = { 100, 100, 200, 200, 300, 400, 500 }; 124 | UStats stats = new UStats("test", "test", 1, times); 125 | assertEquals(0.0001, stats.getFastest(TimeUnit.MILLISECONDS), 0.0); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/net/tuis/ubench/TestUBench.java: -------------------------------------------------------------------------------- 1 | package net.tuis.ubench; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | @SuppressWarnings("javadoc") 8 | public class TestUBench { 9 | 10 | @Test 11 | public void testBenchName() { 12 | UBench bench = new UBench("foo"); 13 | assertEquals("foo", bench.getSuiteName()); 14 | } 15 | 16 | @Test(expected=UBenchRuntimeException.class) 17 | public void testIntTaskException() { 18 | UBench bench = new UBench("test"); 19 | bench.addIntTask("test", () -> 1, i -> i == 2); 20 | bench.press(1); 21 | } 22 | 23 | @Test(expected=UBenchRuntimeException.class) 24 | public void testDoubleTaskException() { 25 | UBench bench = new UBench("test"); 26 | bench.addDoubleTask("test", () -> 1.0, i -> i == 2.0); 27 | bench.press(1); 28 | } 29 | 30 | @Test(expected=UBenchRuntimeException.class) 31 | public void testLongTaskException() { 32 | UBench bench = new UBench("test"); 33 | bench.addLongTask("test", () -> 1L, i -> i == 2L); 34 | bench.press(1); 35 | } 36 | 37 | private static void CanFail(boolean fail) { 38 | if (fail) { 39 | throw new RuntimeException("Forced Fail"); 40 | } 41 | } 42 | 43 | @Test 44 | public void testRunnableTask() { 45 | UBench bench = new UBench("test"); 46 | bench.addTask("test", () -> CanFail(false)); 47 | bench.press(1); 48 | } 49 | 50 | @Test(expected=RuntimeException.class) 51 | public void testRunnableTaskException() { 52 | UBench bench = new UBench("test"); 53 | bench.addTask("test", () -> CanFail(true)); 54 | bench.press(1); 55 | } 56 | 57 | } 58 | --------------------------------------------------------------------------------