├── .gitattributes ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── release-drafter.yml ├── .gitignore ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── header-template.txt ├── jetty-load-generator-client ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── mortbay │ │ └── jetty │ │ └── load │ │ └── generator │ │ ├── HTTP1ClientTransportBuilder.java │ │ ├── HTTP2ClientTransportBuilder.java │ │ ├── HTTPClientTransportBuilder.java │ │ ├── LoadGenerator.java │ │ ├── Resource.java │ │ └── ServerInfo.java │ └── test │ ├── java │ └── org │ │ └── mortbay │ │ └── jetty │ │ └── load │ │ └── generator │ │ ├── FailFastTest.java │ │ ├── HTTP1WebsiteLoadGeneratorTest.java │ │ ├── HTTP2LoadGeneratorTest.java │ │ ├── HTTP2WebsiteLoadGeneratorTest.java │ │ ├── LoadGeneratorTest.java │ │ ├── ResourceBuildTest.java │ │ ├── TestHandler.java │ │ └── WebsiteLoadGeneratorTest.java │ └── resources │ ├── jetty-logging.properties │ ├── keystore.p12 │ ├── website_profile.groovy │ └── website_profile.xml ├── jetty-load-generator-listeners ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── mortbay │ └── jetty │ └── load │ └── generator │ └── listeners │ └── ReportListener.java ├── jetty-load-generator-starter ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── mortbay │ │ └── jetty │ │ └── load │ │ └── generator │ │ └── starter │ │ ├── LoadGeneratorStarter.java │ │ └── LoadGeneratorStarterArgs.java │ └── test │ ├── java │ └── org │ │ └── mortbay │ │ └── jetty │ │ └── load │ │ └── generator │ │ └── starter │ │ └── LoadGeneratorStarterTest.java │ └── resources │ ├── single_fail_resource.groovy │ └── tree_resources.groovy └── pom.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | *.bat eol=crlf 3 | *.txt eol=lf 4 | *.properties eol=lf 5 | *.java eol=lf 6 | *.mod eol=lf 7 | *.adoc eol=lf 8 | *.xml eol=lf 9 | Jenkinsfile eol=lf 10 | *.js eol=lf 11 | *.raw binary 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | open-pull-requests-limit: 50 6 | target-branch: "1.1.x" 7 | schedule: 8 | interval: "daily" 9 | ignore: 10 | - dependency-name: "org.eclipse.jetty:*" 11 | versions: [ ">=10.0.0" ] 12 | 13 | - package-ecosystem: "maven" 14 | directory: "/" 15 | open-pull-requests-limit: 50 16 | target-branch: "2.1.x" 17 | schedule: 18 | interval: "daily" 19 | ignore: 20 | - dependency-name: "org.eclipse.jetty:*" 21 | versions: [ ">=11.0.0" ] 22 | 23 | - package-ecosystem: "maven" 24 | directory: "/" 25 | open-pull-requests-limit: 50 26 | target-branch: "3.1.x" 27 | schedule: 28 | interval: "daily" 29 | ignore: 30 | - dependency-name: "org.eclipse.jetty:*" 31 | versions: [ ">=12.0.0" ] 32 | 33 | 34 | - package-ecosystem: "maven" 35 | directory: "/" 36 | open-pull-requests-limit: 50 37 | target-branch: "4.0.x" 38 | schedule: 39 | interval: "daily" 40 | ignore: 41 | - dependency-name: "org.eclipse.jetty:*" 42 | versions: [ ">=12.1.0" ] 43 | 44 | - package-ecosystem: "github-actions" 45 | directory: "/" 46 | schedule: 47 | interval: "daily" 48 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## Changes 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | java: [17,21] 12 | fail-fast: false 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Setup Java 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' 23 | java-version: ${{ matrix.java }} 24 | check-latest: true 25 | cache: 'maven' 26 | - name: Build with Maven 27 | run: mvn clean install -B -V -e 28 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - 1.0.x 7 | - 2.0.x 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6.1.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | .classpath 3 | .project 4 | .settings 5 | 6 | # maven 7 | target/ 8 | *.versionsBackup 9 | *.releaseBackup 10 | bin/ 11 | 12 | # common junk 13 | *.log 14 | *.diff 15 | *.patch 16 | *.sw[a-p] 17 | *.bak 18 | *.backup 19 | *.debug 20 | *.dump 21 | .attach_pid* 22 | 23 | # vim 24 | .*.sw[a-p] 25 | *~ 26 | ~* 27 | 28 | # intellij / android studio 29 | *.iml 30 | *.ipr 31 | *.iws 32 | .idea/ 33 | 34 | # Mac filesystem dust 35 | .DS_Store 36 | 37 | # pmd 38 | .pmdruleset 39 | .pmd 40 | 41 | # netbeans 42 | /nbproject 43 | 44 | # merge tooling 45 | *.orig 46 | 47 | # test generated content 48 | .flattened-pom.xml 49 | 50 | # Jenkins dust 51 | .work/ -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | pipeline { 4 | agent none 5 | // save some io during the build 6 | options { 7 | skipDefaultCheckout() 8 | durabilityHint('PERFORMANCE_OPTIMIZED') 9 | buildDiscarder logRotator( numToKeepStr: '60' ) 10 | disableRestartFromStage() 11 | } 12 | stages { 13 | stage("Parallel Stage") { 14 | parallel { 15 | stage("Build / Test - JDK21") { 16 | agent { node { label 'linux-light' } } 17 | steps { 18 | timeout( time: 180, unit: 'MINUTES' ) { 19 | checkout scm 20 | mavenBuild( "jdk21", "clean install", "maven3", true) 21 | recordCoverage id: "coverage-jdk21", name: "Coverage jdk21", tools: [[parser: 'JACOCO']] 22 | mavenBuild( "jdk21", "clean javadoc:javadoc -Djacoco.skip=true", "maven3", false) 23 | } 24 | } 25 | } 26 | 27 | stage("Build / Test - JDK17") { 28 | agent { node { label 'linux-light' } } 29 | steps { 30 | timeout( time: 180, unit: 'MINUTES' ) { 31 | checkout scm 32 | mavenBuild( "jdk17", "clean install ", "maven3", true) // javadoc:javadoc 33 | recordCoverage id: "coverage-jdk17", name: "Coverage jdk17", tools: [[parser: 'JACOCO']] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | 43 | /** 44 | * To other developers, if you are using this method above, please use the following syntax. 45 | * 46 | * mavenBuild("", " " 47 | * 48 | * @param jdk the jdk tool name (in jenkins) to use for this build 49 | * @param cmdline the command line in " "`format. 50 | * @return the Jenkinsfile step representing a maven build 51 | */ 52 | def mavenBuild(jdk, cmdline, mvnName, junitReport) { 53 | script { 54 | try { 55 | withEnv(["JAVA_HOME=${ tool "$jdk" }", 56 | "PATH+MAVEN=${ tool "$jdk" }/bin:${tool "$mvnName"}/bin", 57 | "MAVEN_OPTS=-Xms2g -Xmx4g -Djava.awt.headless=true -client -XX:+UnlockDiagnosticVMOptions -XX:GCLockerRetryAllocationCount=100"]) { 58 | configFileProvider( 59 | [configFile(fileId: 'oss-settings.xml', variable: 'GLOBAL_MVN_SETTINGS')]) { 60 | sh "mvn -DsettingsPath=$GLOBAL_MVN_SETTINGS -ntp -s $GLOBAL_MVN_SETTINGS -Dmaven.repo.local=.repository -V -B -e -U $cmdline" 61 | } 62 | } 63 | } 64 | finally 65 | { 66 | if(junitReport) { 67 | junit testResults: '**/target/surefire-reports/**/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true 68 | } 69 | } 70 | } 71 | } 72 | 73 | // vim: et:ts=2:sw=2:ft=groovy 74 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | 279 | 280 | Apache License 281 | Version 2.0, January 2004 282 | http://www.apache.org/licenses/ 283 | 284 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 285 | 286 | 1. Definitions. 287 | 288 | "License" shall mean the terms and conditions for use, reproduction, 289 | and distribution as defined by Sections 1 through 9 of this document. 290 | 291 | "Licensor" shall mean the copyright owner or entity authorized by 292 | the copyright owner that is granting the License. 293 | 294 | "Legal Entity" shall mean the union of the acting entity and all 295 | other entities that control, are controlled by, or are under common 296 | control with that entity. For the purposes of this definition, 297 | "control" means (i) the power, direct or indirect, to cause the 298 | direction or management of such entity, whether by contract or 299 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 300 | outstanding shares, or (iii) beneficial ownership of such entity. 301 | 302 | "You" (or "Your") shall mean an individual or Legal Entity 303 | exercising permissions granted by this License. 304 | 305 | "Source" form shall mean the preferred form for making modifications, 306 | including but not limited to software source code, documentation 307 | source, and configuration files. 308 | 309 | "Object" form shall mean any form resulting from mechanical 310 | transformation or translation of a Source form, including but 311 | not limited to compiled object code, generated documentation, 312 | and conversions to other media types. 313 | 314 | "Work" shall mean the work of authorship, whether in Source or 315 | Object form, made available under the License, as indicated by a 316 | copyright notice that is included in or attached to the work 317 | (an example is provided in the Appendix below). 318 | 319 | "Derivative Works" shall mean any work, whether in Source or Object 320 | form, that is based on (or derived from) the Work and for which the 321 | editorial revisions, annotations, elaborations, or other modifications 322 | represent, as a whole, an original work of authorship. For the purposes 323 | of this License, Derivative Works shall not include works that remain 324 | separable from, or merely link (or bind by name) to the interfaces of, 325 | the Work and Derivative Works thereof. 326 | 327 | "Contribution" shall mean any work of authorship, including 328 | the original version of the Work and any modifications or additions 329 | to that Work or Derivative Works thereof, that is intentionally 330 | submitted to Licensor for inclusion in the Work by the copyright owner 331 | or by an individual or Legal Entity authorized to submit on behalf of 332 | the copyright owner. For the purposes of this definition, "submitted" 333 | means any form of electronic, verbal, or written communication sent 334 | to the Licensor or its representatives, including but not limited to 335 | communication on electronic mailing lists, source code control systems, 336 | and issue tracking systems that are managed by, or on behalf of, the 337 | Licensor for the purpose of discussing and improving the Work, but 338 | excluding communication that is conspicuously marked or otherwise 339 | designated in writing by the copyright owner as "Not a Contribution." 340 | 341 | "Contributor" shall mean Licensor and any individual or Legal Entity 342 | on behalf of whom a Contribution has been received by Licensor and 343 | subsequently incorporated within the Work. 344 | 345 | 2. Grant of Copyright License. Subject to the terms and conditions of 346 | this License, each Contributor hereby grants to You a perpetual, 347 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 348 | copyright license to reproduce, prepare Derivative Works of, 349 | publicly display, publicly perform, sublicense, and distribute the 350 | Work and such Derivative Works in Source or Object form. 351 | 352 | 3. Grant of Patent License. Subject to the terms and conditions of 353 | this License, each Contributor hereby grants to You a perpetual, 354 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 355 | (except as stated in this section) patent license to make, have made, 356 | use, offer to sell, sell, import, and otherwise transfer the Work, 357 | where such license applies only to those patent claims licensable 358 | by such Contributor that are necessarily infringed by their 359 | Contribution(s) alone or by combination of their Contribution(s) 360 | with the Work to which such Contribution(s) was submitted. If You 361 | institute patent litigation against any entity (including a 362 | cross-claim or counterclaim in a lawsuit) alleging that the Work 363 | or a Contribution incorporated within the Work constitutes direct 364 | or contributory patent infringement, then any patent licenses 365 | granted to You under this License for that Work shall terminate 366 | as of the date such litigation is filed. 367 | 368 | 4. Redistribution. You may reproduce and distribute copies of the 369 | Work or Derivative Works thereof in any medium, with or without 370 | modifications, and in Source or Object form, provided that You 371 | meet the following conditions: 372 | 373 | (a) You must give any other recipients of the Work or 374 | Derivative Works a copy of this License; and 375 | 376 | (b) You must cause any modified files to carry prominent notices 377 | stating that You changed the files; and 378 | 379 | (c) You must retain, in the Source form of any Derivative Works 380 | that You distribute, all copyright, patent, trademark, and 381 | attribution notices from the Source form of the Work, 382 | excluding those notices that do not pertain to any part of 383 | the Derivative Works; and 384 | 385 | (d) If the Work includes a "NOTICE" text file as part of its 386 | distribution, then any Derivative Works that You distribute must 387 | include a readable copy of the attribution notices contained 388 | within such NOTICE file, excluding those notices that do not 389 | pertain to any part of the Derivative Works, in at least one 390 | of the following places: within a NOTICE text file distributed 391 | as part of the Derivative Works; within the Source form or 392 | documentation, if provided along with the Derivative Works; or, 393 | within a display generated by the Derivative Works, if and 394 | wherever such third-party notices normally appear. The contents 395 | of the NOTICE file are for informational purposes only and 396 | do not modify the License. You may add Your own attribution 397 | notices within Derivative Works that You distribute, alongside 398 | or as an addendum to the NOTICE text from the Work, provided 399 | that such additional attribution notices cannot be construed 400 | as modifying the License. 401 | 402 | You may add Your own copyright statement to Your modifications and 403 | may provide additional or different license terms and conditions 404 | for use, reproduction, or distribution of Your modifications, or 405 | for any such Derivative Works as a whole, provided Your use, 406 | reproduction, and distribution of the Work otherwise complies with 407 | the conditions stated in this License. 408 | 409 | 5. Submission of Contributions. Unless You explicitly state otherwise, 410 | any Contribution intentionally submitted for inclusion in the Work 411 | by You to the Licensor shall be under the terms and conditions of 412 | this License, without any additional terms or conditions. 413 | Notwithstanding the above, nothing herein shall supersede or modify 414 | the terms of any separate license agreement you may have executed 415 | with Licensor regarding such Contributions. 416 | 417 | 6. Trademarks. This License does not grant permission to use the trade 418 | names, trademarks, service marks, or product names of the Licensor, 419 | except as required for reasonable and customary use in describing the 420 | origin of the Work and reproducing the content of the NOTICE file. 421 | 422 | 7. Disclaimer of Warranty. Unless required by applicable law or 423 | agreed to in writing, Licensor provides the Work (and each 424 | Contributor provides its Contributions) on an "AS IS" BASIS, 425 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 426 | implied, including, without limitation, any warranties or conditions 427 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 428 | PARTICULAR PURPOSE. You are solely responsible for determining the 429 | appropriateness of using or redistributing the Work and assume any 430 | risks associated with Your exercise of permissions under this License. 431 | 432 | 8. Limitation of Liability. In no event and under no legal theory, 433 | whether in tort (including negligence), contract, or otherwise, 434 | unless required by applicable law (such as deliberate and grossly 435 | negligent acts) or agreed to in writing, shall any Contributor be 436 | liable to You for damages, including any direct, indirect, special, 437 | incidental, or consequential damages of any character arising as a 438 | result of this License or out of the use or inability to use the 439 | Work (including but not limited to damages for loss of goodwill, 440 | work stoppage, computer failure or malfunction, or any and all 441 | other commercial damages or losses), even if such Contributor 442 | has been advised of the possibility of such damages. 443 | 444 | 9. Accepting Warranty or Additional Liability. While redistributing 445 | the Work or Derivative Works thereof, You may choose to offer, 446 | and charge a fee for, acceptance of support, warranty, indemnity, 447 | or other liability obligations and/or rights consistent with this 448 | License. However, in accepting such obligations, You may act only 449 | on Your own behalf and on Your sole responsibility, not on behalf 450 | of any other Contributor, and only if You agree to indemnify, 451 | defend, and hold each Contributor harmless for any liability 452 | incurred by, or claims asserted against, such Contributor by reason 453 | of your accepting any such warranty or additional liability. 454 | 455 | END OF TERMS AND CONDITIONS 456 | 457 | APPENDIX: How to apply the Apache License to your work. 458 | 459 | To apply the Apache License to your work, attach the following 460 | boilerplate notice, with the fields enclosed by brackets "[]" 461 | replaced with your own identifying information. (Don't include 462 | the brackets!) The text should be enclosed in the appropriate 463 | comment syntax for the file format. We also recommend that a 464 | file or class name and description of purpose be included on the 465 | same "printed page" as the copyright notice for easier 466 | identification within third-party archives. 467 | 468 | Copyright [yyyy] [name of copyright owner] 469 | 470 | Licensed under the Apache License, Version 2.0 (the "License"); 471 | you may not use this file except in compliance with the License. 472 | You may obtain a copy of the License at 473 | 474 | http://www.apache.org/licenses/LICENSE-2.0 475 | 476 | Unless required by applicable law or agreed to in writing, software 477 | distributed under the License is distributed on an "AS IS" BASIS, 478 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 479 | See the License for the specific language governing permissions and 480 | limitations under the License. 481 | 482 | 483 | SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub CI](https://github.com/jetty-project/jetty-load-generator/workflows/GitHub%20CI/badge.svg) 2 | 3 | # Jetty Load Generator Project 4 | 5 | Jetty's `LoadGenerator` is an API to load-test HTTP servers, based on Jetty's `HttpClient`. 6 | 7 | | Jetty Load Generator Version | Jetty Version | Java Version | Status | 8 | |:----------------------------:|:-------------:|:------------:|:------------:| 9 | | 4.0.x | 12.0.x | Java 17+ | Stable | 10 | | 3.1.x | 11.0.x | Java 11+ | Stable | 11 | | 2.1.x | 10.0.x | Java 11+ | Stable | 12 | | 1.1.x | 9.4.x | Java 8+ | End-Of-Life | 13 | | 2.0.x | 11.0.x | Java 11+ | End-Of-Life | 14 | | 1.0.7 | 9.4.x | Java 8+ | End-Of-Life | 15 | | 1.0.0-1.0.6 | 9.4.x | Java 11+ | End-Of-Life | 16 | 17 | The design of the `LoadGenerator` is based around these concepts: 18 | 19 | * Generate requests asynchronously at a constant rate, independently of responses. 20 | * Model [requests as web pages](#resource-apis), simulating what browsers do to download a web page. 21 | * Support both HTTP/1.1 and HTTP/2 (and future versions of the HTTP protocol). 22 | * Emit response events asynchronously, so they can be recorded, for example, in a response time histogram. 23 | 24 | You can embed Jetty's `LoadGenerator` in Java applications -- this will give you full flexibility, or you can use it as a command-line tool -- and therefore use it in scripts. 25 | 26 | The project artifacts are: 27 | 28 | * `jetty-load-generator-client` -- Java APIs, see [this section](#load-generator-apis) 29 | * `jetty-load-generator-listeners` -- useful listeners for events emitted during load-test 30 | * `jetty-load-generator-starter` -- command-line load test uber-jar, see [this section](#command-line-load-generation) 31 | 32 | ## Recommended Load Generation Setup 33 | 34 | 1. Assume that the load generator is the bottleneck. You may need several load generators on different machines to make the server even break a sweat. 35 | 1. Establish a baseline to verify that all the parties involved in the load runs behave correctly, including network, load generators, server(s), load balancer(s), etc. 36 | 1. Use one or more _loaders_ to generate load on the server. A loader is a load generator that imposes a load on the server, but does not record response times. 37 | 1. Use a _probe_ to record response times. A probe is a load generator that imposes a light load on the server and records response times. 38 | 1. Use Brendan Gregg's [USE method](http://www.brendangregg.com/usemethod.html) to analyze the results. 39 | 40 | For example, let's say you want to plot how the server responds to increasing load. 41 | You setup, say, 4 _loaders_ and one _probe_. 42 | Configure each loader with, say, `threads=2`, `usersPerThread=50` and `requestRate=20`. 43 | Configure the probe with, say, `threads=1`, `usersPerThread=1` and `requestRate=1`. 44 | The total load on the server is therefore 81 requests/s from 401 users, from all the loaders and the probe. 45 | Perform a run with this configuration and record the results from the probe. 46 | Then change the configuration of the loaders to increase the load (but don't change the probe configuration), say to `usersPerThread=75` and `requestRate=30`, so now the total load on the server is 121 requests/s from 601 users. 47 | Perform another runs and record the results from the probe. 48 | Increment again to `usersPerThread=100` and `requestRate=40`, that is 161 requests/s from 801 users. 49 | 50 | Loaders should not affect each other, so ideally each loader should be on a separate machine with a separate network link to the server. 51 | For non-critical loads, loaders may share the same machine/link, but they will obviously steal CPU and bandwidth from each other. 52 | Loaders should not affect the probe, so ideally the probe should run on a separate machine with a separate network link to the server, to avoid that loaders steal CPUs and bandwidth from the probe that will therefore record bogus results. 53 | 54 | Monitor continuously each loader request rate and compare it with its response rate. 55 | The effective request rate should be close to the nominal request rate you want to impose. 56 | The response rate should be as close as possible to the request rate. 57 | If these conditions are not met, it means that the loader is over capacity, and you must reduce the load and possibly spawn a new loader. 58 | 59 | ## Load Generator APIs 60 | 61 | ### `Resource` APIs 62 | 63 | You can use the `Resource` APIs to define resources that `LoadGenerator` requests to the server. 64 | 65 | A simple resource: 66 | 67 | ```java 68 | Resource resource = new Resource("/index.html"); 69 | ``` 70 | 71 | A web-page like `Resource` tree: 72 | 73 | ```java 74 | Resource resource = new Resource("/index.html", 75 | new Resource("/styles.css"), 76 | new Resource("/application.js") 77 | ); 78 | ``` 79 | 80 | `Resource` trees are requested to the server similarly to how a browser would do. 81 | In the example above, `/index.html` will be requested and awaited; when its response arrives, `LoadGenerator` will send its children (in parallel if possible): `/styles.css` and `/application.js`. 82 | 83 | Resources can be defined in Java, Groovy files, Jetty XML files, or JSON files. 84 | 85 | ### `LoadGenerator` APIs 86 | 87 | `LoadGenerator` offers a builder-style API: 88 | 89 | ```java 90 | LoadGenerator generator = LoadGenerator.builder() 91 | .scheme(scheme) 92 | .host(serverHost) 93 | .port(serverPort) 94 | .resource(resource) 95 | .httpClientTransportBuilder(transportBuilder) 96 | .threads(1) 97 | .usersPerThread(10) 98 | .channelsPerUser(6) 99 | .warmupIterationsPerThread(10) 100 | .iterationsPerThread(100) 101 | .runFor(2, TimeUnit.MINUTES) // Overrides iterationsPerThread() 102 | .resourceListener(resourceListener) 103 | .build(); 104 | 105 | // Start the load generation. 106 | CompletableFuture complete = generator.begin(); 107 | 108 | // Now the load generator is running. 109 | 110 | // You can wait for the CompletableFuture to complete. 111 | // Or you can interrupt the load generation: 112 | generator.interrupt(); 113 | ``` 114 | 115 | `LoadGenerator` uses _sender_ threads to request resources to the server. 116 | 117 | Each sender thread can be configured with a number of _users_; each user is a separate `HttpClient` instance that simulates a browser, and has its own connection pool. 118 | Each user opens at least one TCP connection to the server -- the exact number of connections opened depends on the protocol used (HTTP/1.1 vs HTTP/2), the user channels (see below), and the resource rate. 119 | 120 | Each user may send requests in parallel through _channels_. 121 | A channel is either a new connection in HTTP/1.1, or a new HTTP/2 stream. 122 | 123 | Each sender thread runs an optional number of _warmup_ iterations, that are not recorded -- no events will be emitted for these warmup requests. 124 | 125 | After the warmup iterations, each sender thread runs the configured number of _iterations_ or, alternatively, runs for the configured time. 126 | These requests will emit events that may be recorded by listeners, see below. 127 | 128 | 129 | ### Listener APIs 130 | 131 | `LoadGenerator` emits a variety of events that you can listen to. 132 | 133 | `LoadGenerator` emits events at: 134 | * load generation begin, emitted when the load generation begins, to `LoadGenerator.BeginListener` 135 | * load generation ready, emitted when the load generation has finished the warmup, to `LoadGenerator.ReadyListener` 136 | * load generation end, emitted when the load generation ends (that is, the last request has been sent), to `LoadGenerator.EndListener` 137 | * load generation complete, emitted when the load generation completes (that is, the last response has been received), to `LoadGenerator.CompleteListener` 138 | 139 | Most interesting are events related to resources. 140 | 141 | `Resource.NodeListener` is notified every time a resource is received by `LoadGenerator`. 142 | `Resource.TreeListener` is notified every time a whole resource tree is received by `LoadGenerator` -- this is useful to gather "page load" times. 143 | 144 | For both resource listeners, the information is carried by `Resource.Info`, that provides the timestamps (in nanoseconds) for resource send, resource received, resource content bytes, HTTP status, etc. 145 | 146 | You can use histograms to record the response times: 147 | 148 | ```java 149 | class ResponseTimeListener implements Resource.NodeListener, LoadGenerator.CompleteListener { 150 | private final org.HdrHistogram.Recorder recorder; 151 | private org.HdrHistogram.Histogram histogram; 152 | 153 | // Invoked every time a resource is received. 154 | @Override 155 | public void onResourceNode(Resource.Info info) { 156 | long responseTime = info.getResponseTime() - info.getRequestTime(); 157 | recorder.recordValue(responseTime); 158 | } 159 | 160 | // Invoked at the end of the load generation. 161 | @Override 162 | public void onEnd(LoadGenerator generator) { 163 | // Retrieve the histogram, resetting the recorder. 164 | this.histogram = recorder.getIntervalHistogram(); 165 | } 166 | } 167 | ``` 168 | 169 | The `Histogram` APIs provides count, percentiles, average, minimum and maximum values. 170 | 171 | ## Command-Line Load Generation 172 | 173 | Artifact `jetty-load-generator-starter--uber.jar` allows you to generate load using the command-line. 174 | The uber-jar already contains all the required dependencies. 175 | 176 | To display usage: 177 | 178 | ``` 179 | java -jar jetty-load-generator-starter--uber.jar --help 180 | ``` 181 | 182 | Example: 183 | 184 | ```shell 185 | java -jar jetty-load-generator-starter--uber.jar 186 | --scheme https 187 | --host serverHost 188 | --port serverPort 189 | --resource-json-path /tmp/resource.json 190 | --transport h2 # secure HTTP/2 191 | --threads 1 192 | --users-per-thread 10 193 | --channels-per-user 6 194 | --warmup-iterations 10 195 | --iterations 100 196 | --display-stats 197 | ``` 198 | 199 | The `/tmp/resource.json` can be as simple as: 200 | 201 | ```json 202 | { 203 | "path": "/index.html" 204 | } 205 | ``` 206 | -------------------------------------------------------------------------------- /header-template.txt: -------------------------------------------------------------------------------- 1 | ======================================================================== 2 | Copyright (c) ${copyright-range} Mort Bay Consulting Pty Ltd and others. 3 | 4 | This program and the accompanying materials are made available under the 5 | terms of the Eclipse Public License v. 2.0 which is available at 6 | https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | 9 | SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | ======================================================================== 11 | -------------------------------------------------------------------------------- /jetty-load-generator-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.mortbay.jetty.loadgenerator 5 | jetty-load-generator 6 | 4.0.23-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | jetty-load-generator-client 11 | jar 12 | Jetty :: Load Generator :: Client 13 | 14 | 15 | ${project.groupId}.load.generator.client 16 | INFO 17 | INFO 18 | 19 | 20 | 21 | 22 | org.eclipse.jetty 23 | jetty-client 24 | 25 | 26 | org.eclipse.jetty.http2 27 | jetty-http2-client-transport 28 | 29 | 30 | org.eclipse.jetty 31 | jetty-alpn-java-client 32 | 33 | 34 | org.eclipse.jetty 35 | jetty-util-ajax 36 | 37 | 38 | org.slf4j 39 | slf4j-api 40 | 41 | 42 | 43 | junit 44 | junit 45 | test 46 | 47 | 48 | org.eclipse.jetty 49 | jetty-server 50 | test 51 | 52 | 53 | org.eclipse.jetty 54 | jetty-alpn-java-server 55 | test 56 | 57 | 58 | org.eclipse.jetty 59 | jetty-jmx 60 | test 61 | 62 | 63 | org.eclipse.jetty 64 | jetty-xml 65 | test 66 | 67 | 68 | org.eclipse.jetty 69 | jetty-slf4j-impl 70 | test 71 | 72 | 73 | org.eclipse.jetty.toolchain 74 | jetty-perf-helper 75 | test 76 | 77 | 78 | org.eclipse.jetty.http2 79 | jetty-http2-server 80 | test 81 | 82 | 83 | org.apache.groovy 84 | groovy 85 | test 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/main/java/org/mortbay/jetty/load/generator/HTTP1ClientTransportBuilder.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import org.eclipse.jetty.client.HttpClientTransport; 17 | import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; 18 | import org.eclipse.jetty.io.ClientConnector; 19 | 20 | /** 21 | *

Helper builder to provide an http(s) {@link HttpClientTransport}.

22 | */ 23 | public class HTTP1ClientTransportBuilder extends HTTPClientTransportBuilder { 24 | public static final String TYPE = "http/1.1"; 25 | 26 | @Override 27 | public HTTP1ClientTransportBuilder selectors(int selectors) { 28 | super.selectors(selectors); 29 | return this; 30 | } 31 | 32 | @Override 33 | public HTTP1ClientTransportBuilder connector(ClientConnector connector) { 34 | super.connector(connector); 35 | return this; 36 | } 37 | 38 | @Override 39 | public String getType() { 40 | return TYPE; 41 | } 42 | 43 | @Override 44 | protected HttpClientTransport newHttpClientTransport(ClientConnector connector) { 45 | return new HttpClientTransportOverHTTP(connector); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/main/java/org/mortbay/jetty/load/generator/HTTP2ClientTransportBuilder.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.Map; 17 | 18 | import org.eclipse.jetty.client.HttpClientTransport; 19 | import org.eclipse.jetty.http2.client.HTTP2Client; 20 | import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; 21 | import org.eclipse.jetty.io.ClientConnector; 22 | import org.eclipse.jetty.util.ajax.JSON; 23 | 24 | /** 25 | *

Helper builder to provide an HTTP/2 {@link HttpClientTransport}.

26 | */ 27 | public class HTTP2ClientTransportBuilder extends HTTPClientTransportBuilder { 28 | public static final String TYPE = "http/2"; 29 | 30 | private int sessionRecvWindow = 16 * 1024 * 1024; 31 | private int streamRecvWindow = 16 * 1024 * 1024; 32 | 33 | @Override 34 | public HTTP2ClientTransportBuilder selectors(int selectors) { 35 | super.selectors(selectors); 36 | return this; 37 | } 38 | 39 | @Override 40 | public HTTP2ClientTransportBuilder connector(ClientConnector connector) { 41 | super.connector(connector); 42 | return this; 43 | } 44 | 45 | /** 46 | * @param sessionRecvWindow the HTTP/2 session flow control receive window 47 | * @return this builder instance 48 | */ 49 | public HTTP2ClientTransportBuilder sessionRecvWindow(int sessionRecvWindow) { 50 | this.sessionRecvWindow = sessionRecvWindow; 51 | return this; 52 | } 53 | 54 | public int getSessionRecvWindow() { 55 | return sessionRecvWindow; 56 | } 57 | 58 | /** 59 | * @param streamRecvWindow the HTTP/2 stream flow control receive window 60 | * @return this builder instance 61 | */ 62 | public HTTP2ClientTransportBuilder streamRecvWindow(int streamRecvWindow) { 63 | this.streamRecvWindow = streamRecvWindow; 64 | return this; 65 | } 66 | 67 | public int getStreamRecvWindow() { 68 | return streamRecvWindow; 69 | } 70 | 71 | @Override 72 | public String getType() { 73 | return TYPE; 74 | } 75 | 76 | @Override 77 | protected HttpClientTransport newHttpClientTransport(ClientConnector connector) { 78 | HTTP2Client http2Client = new HTTP2Client(connector); 79 | http2Client.setInitialSessionRecvWindow(getSessionRecvWindow()); 80 | http2Client.setInitialStreamRecvWindow(getStreamRecvWindow()); 81 | return new HttpClientTransportOverHTTP2(http2Client); 82 | } 83 | 84 | @Override 85 | public void toJSON(JSON.Output out) { 86 | super.toJSON(out); 87 | out.add("sessionRecvWindow", getSessionRecvWindow()); 88 | out.add("streamRecvWindow", getStreamRecvWindow()); 89 | } 90 | 91 | @Override 92 | public void fromJSON(Map map) { 93 | super.fromJSON(map); 94 | sessionRecvWindow = LoadGenerator.Config.asInt(map, "sessionRecvWindow"); 95 | streamRecvWindow = LoadGenerator.Config.asInt(map, "streamRecvWindow"); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/main/java/org/mortbay/jetty/load/generator/HTTPClientTransportBuilder.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.Map; 17 | import org.eclipse.jetty.client.HttpClientTransport; 18 | import org.eclipse.jetty.io.ClientConnector; 19 | import org.eclipse.jetty.util.ajax.JSON; 20 | 21 | /** 22 | *

A builder for {@link HttpClientTransport}.

23 | */ 24 | public abstract class HTTPClientTransportBuilder implements JSON.Convertible { 25 | protected int selectors; 26 | protected ClientConnector connector; 27 | 28 | /** 29 | * @param selectors the number of NIO selectors 30 | * @return this builder instance 31 | */ 32 | public HTTPClientTransportBuilder selectors(int selectors) { 33 | this.selectors = selectors; 34 | return this; 35 | } 36 | 37 | public int getSelectors() { 38 | return selectors; 39 | } 40 | 41 | public HTTPClientTransportBuilder connector(ClientConnector connector) { 42 | this.connector = connector; 43 | return this; 44 | } 45 | 46 | public ClientConnector getConnector() { 47 | return connector; 48 | } 49 | 50 | /** 51 | * @return the transport type, such as "http/1.1" or "http/2" 52 | */ 53 | public abstract String getType(); 54 | 55 | /** 56 | * @return a new HttpClientTransport instance 57 | */ 58 | public HttpClientTransport build() { 59 | ClientConnector connector = getConnector(); 60 | if (connector == null) { 61 | connector = new ClientConnector(); 62 | } 63 | int selectors = getSelectors(); 64 | if (selectors > 0) { 65 | connector.setSelectors(selectors); 66 | } 67 | return newHttpClientTransport(connector); 68 | } 69 | 70 | protected abstract HttpClientTransport newHttpClientTransport(ClientConnector connector); 71 | 72 | @Override 73 | public void toJSON(JSON.Output out) { 74 | out.add("type", getType()); 75 | out.add("selectors", getSelectors()); 76 | } 77 | 78 | @Override 79 | public void fromJSON(Map map) { 80 | selectors = LoadGenerator.Config.asInt(map, "selectors"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/main/java/org/mortbay/jetty/load/generator/Resource.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.net.URI; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.Collection; 20 | import java.util.Collections; 21 | import java.util.EventListener; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.stream.Collectors; 25 | 26 | import org.eclipse.jetty.http.HttpField; 27 | import org.eclipse.jetty.http.HttpFields; 28 | import org.eclipse.jetty.http.HttpMethod; 29 | import org.eclipse.jetty.util.ajax.JSON; 30 | 31 | /** 32 | *

A resource node to be fetched by the load generator.

33 | *

Resources are organized in a tree, and the load generator 34 | * fetches parent resources before children resources, while sibling 35 | * resources are fetched in parallel.

36 | *

A Resource without a path is a group resource, 37 | * only meant to group resources together (for example to fetch all 38 | * JavaScript resources as a group before fetching the image resources).

39 | */ 40 | public class Resource implements JSON.Convertible { 41 | public static final String RESPONSE_LENGTH = "JLG-Response-Length"; 42 | 43 | private final List resources = new ArrayList<>(); 44 | private final HttpFields.Mutable requestHeaders = HttpFields.build(); 45 | private String method = HttpMethod.GET.asString(); 46 | private String path; 47 | private long requestLength; 48 | private long responseLength; 49 | 50 | public Resource() { 51 | this((String)null); 52 | } 53 | 54 | public Resource(String path) { 55 | this(path, new Resource[0]); 56 | } 57 | 58 | public Resource(Resource... resources) { 59 | this(null, resources); 60 | } 61 | 62 | public Resource(String path, Resource... resources) { 63 | this.path = path; 64 | if (resources != null) { 65 | Collections.addAll(this.resources, resources); 66 | } 67 | } 68 | 69 | /** 70 | * @param method the HTTP method to use to fetch the resource 71 | * @return this Resource 72 | */ 73 | public Resource method(String method) { 74 | this.method = method; 75 | return this; 76 | } 77 | 78 | public String getMethod() { 79 | return method; 80 | } 81 | 82 | /** 83 | * @param path the resource path 84 | * @return this Resource 85 | */ 86 | public Resource path(String path) { 87 | this.path = path.startsWith("/") ? path : "/" + path; 88 | return this; 89 | } 90 | 91 | public String getPath() { 92 | return path; 93 | } 94 | 95 | /** 96 | * @param requestLength the request content length 97 | * @return this Resource 98 | */ 99 | public Resource requestLength(long requestLength) { 100 | this.requestLength = requestLength; 101 | return this; 102 | } 103 | 104 | public long getRequestLength() { 105 | return requestLength; 106 | } 107 | 108 | /** 109 | *

Adds a request header.

110 | * 111 | * @param name the header name 112 | * @param value the header value 113 | * @return this Resource 114 | */ 115 | public Resource requestHeader(String name, String value) { 116 | this.requestHeaders.add(name, value); 117 | return this; 118 | } 119 | 120 | /** 121 | *

Adds request headers.

122 | * 123 | * @param headers the request headers 124 | * @return this Resource 125 | */ 126 | public Resource requestHeaders(HttpFields headers) { 127 | this.requestHeaders.add(headers); 128 | return this; 129 | } 130 | 131 | public HttpFields getRequestHeaders() { 132 | return requestHeaders; 133 | } 134 | 135 | /** 136 | *

Sets the response content length.

137 | *

The response content length is conveyed as the request header 138 | * specified by {@link #RESPONSE_LENGTH}. Servers may ignore it 139 | * or honor it, responding with the desired response content length.

140 | * 141 | * @param responseLength the response content length 142 | * @return this Resource 143 | */ 144 | public Resource responseLength(long responseLength) { 145 | this.responseLength = responseLength; 146 | return this; 147 | } 148 | 149 | public long getResponseLength() { 150 | return responseLength; 151 | } 152 | 153 | /** 154 | *

Adds children resources.

155 | * 156 | * @param resources the children resources to add 157 | * @return this Resource 158 | */ 159 | public Resource resources(Resource... resources) { 160 | this.resources.addAll(List.of(resources)); 161 | return this; 162 | } 163 | 164 | /** 165 | * @return the children resources 166 | */ 167 | public List getResources() { 168 | return resources; 169 | } 170 | 171 | /** 172 | * Finds a descendant resource by path and query with the given URI. 173 | * 174 | * @param uri the URI with the path and query to find 175 | * @return a matching descendant resource, or null if there is no match 176 | */ 177 | public Resource findDescendant(URI uri) { 178 | String pathQuery = uri.getRawPath(); 179 | String query = uri.getRawQuery(); 180 | if (query != null) { 181 | pathQuery += "?" + query; 182 | } 183 | for (Resource child : getResources()) { 184 | if (pathQuery.equals(child.getPath())) { 185 | return child; 186 | } else { 187 | Resource result = child.findDescendant(uri); 188 | if (result != null) { 189 | return result; 190 | } 191 | } 192 | } 193 | return null; 194 | } 195 | 196 | /** 197 | * @return the number of descendant resource nodes, including this node 198 | */ 199 | public int descendantCount() { 200 | return descendantCount(this); 201 | } 202 | 203 | private int descendantCount(Resource resource) { 204 | int result = 1; 205 | for (Resource child : resource.getResources()) { 206 | result += descendantCount(child); 207 | } 208 | return result; 209 | } 210 | 211 | Info newInfo(LoadGenerator generator) { 212 | return new Info(generator, this); 213 | } 214 | 215 | @Override 216 | public void toJSON(JSON.Output out) { 217 | String method = getMethod(); 218 | if (method != null) { 219 | out.add("method", method); 220 | } 221 | String path = getPath(); 222 | if (path == null) { 223 | path = "/"; 224 | } 225 | out.add("path", path); 226 | out.add("requestLength", getRequestLength()); 227 | out.add("responseLength", getResponseLength()); 228 | HttpFields requestHeaders = getRequestHeaders(); 229 | if (requestHeaders != null) { 230 | out.add("requestHeaders", toMap(requestHeaders)); 231 | } 232 | List resources = getResources(); 233 | if (resources != null) { 234 | out.add("resources", resources); 235 | } 236 | } 237 | 238 | @Override 239 | public void fromJSON(Map map) { 240 | String method = (String)map.get("method"); 241 | if (method != null) { 242 | method(method); 243 | } 244 | String path = (String)map.get("path"); 245 | if (path == null) { 246 | path = "/"; 247 | } 248 | path(path); 249 | Number requestLength = (Number)map.get("requestLength"); 250 | if (requestLength != null) { 251 | requestLength(requestLength.longValue()); 252 | } 253 | Number responseLength = (Number)map.get("responseLength"); 254 | if (responseLength != null) { 255 | responseLength(responseLength.longValue()); 256 | } 257 | @SuppressWarnings("unchecked") 258 | Map requestHeaders = (Map)map.get("requestHeaders"); 259 | requestHeaders(toHttpFields(requestHeaders)); 260 | resources(toResources(map.get("resources"))); 261 | } 262 | 263 | private static Map toMap(HttpFields fields) { 264 | return fields.stream() 265 | .collect(Collectors.toMap(HttpField::getName, HttpField::getValues)); 266 | } 267 | 268 | private static HttpFields toHttpFields(Map map) { 269 | HttpFields.Mutable fields = HttpFields.build(); 270 | if (map != null) { 271 | map.entrySet().stream() 272 | .map(entry -> new HttpField(entry.getKey(), Arrays.stream((Object[])entry.getValue()).map(String::valueOf).collect(Collectors.joining(",")))) 273 | .forEach(fields::put); 274 | } 275 | return fields.asImmutable(); 276 | } 277 | 278 | private static Resource[] toResources(Object objects) { 279 | if (objects != null) { 280 | Object[] array = null; 281 | if (objects.getClass().isArray()) { 282 | array = (Object[])objects; 283 | } else if (objects instanceof Collection) { 284 | array = ((Collection)objects).toArray(); 285 | } 286 | if (array != null) { 287 | return Arrays.stream(array) 288 | .map(element -> { 289 | Resource child = new Resource(); 290 | @SuppressWarnings("unchecked") 291 | Map map = (Map)element; 292 | child.fromJSON(map); 293 | return child; 294 | }) 295 | .toArray(Resource[]::new); 296 | } 297 | } 298 | return new Resource[0]; 299 | } 300 | 301 | @Override 302 | public String toString() { 303 | return String.format("%s@%h{%s %s,reqLen=%d,respLen=%d,children=%d}", 304 | getClass().getSimpleName(), 305 | hashCode(), 306 | getMethod(), 307 | getPath(), 308 | getRequestLength(), 309 | getResponseLength(), 310 | getResources().size()); 311 | } 312 | 313 | /** 314 | *

Value class containing information per-resource and per-request.

315 | */ 316 | public static class Info { 317 | private final LoadGenerator generator; 318 | private final Resource resource; 319 | private long requestTime; 320 | private long latencyTime; 321 | private long responseTime; 322 | private long treeTime; 323 | private long contentLength; 324 | private boolean pushed; 325 | private int status; 326 | private Throwable failure; 327 | 328 | private Info(LoadGenerator generator, Resource resource) { 329 | this.generator = generator; 330 | this.resource = resource; 331 | } 332 | 333 | public LoadGenerator getLoadGenerator() { 334 | return generator; 335 | } 336 | 337 | /** 338 | * @return the corresponding Resource 339 | */ 340 | public Resource getResource() { 341 | return resource; 342 | } 343 | 344 | /** 345 | * @return the time, in ns, the request is being sent 346 | */ 347 | public long getRequestTime() { 348 | return requestTime; 349 | } 350 | 351 | void setRequestTime(long requestTime) { 352 | this.requestTime = requestTime; 353 | } 354 | 355 | /** 356 | * @return the time, in ns, the response first byte arrived 357 | */ 358 | public long getLatencyTime() { 359 | return latencyTime; 360 | } 361 | 362 | void setLatencyTime(long latencyTime) { 363 | this.latencyTime = latencyTime; 364 | } 365 | 366 | /** 367 | * @return the time, in ns, the response last byte arrived 368 | */ 369 | public long getResponseTime() { 370 | return responseTime; 371 | } 372 | 373 | void setResponseTime(long responseTime) { 374 | this.responseTime = responseTime; 375 | } 376 | 377 | /** 378 | * @return the time, in ns, the last byte of the whole resource tree arrived 379 | */ 380 | public long getTreeTime() { 381 | return treeTime; 382 | } 383 | 384 | void setTreeTime(long treeTime) { 385 | this.treeTime = treeTime; 386 | } 387 | 388 | /** 389 | * @param bytes the number of bytes to add to the response content length 390 | */ 391 | void addContent(int bytes) { 392 | contentLength += bytes; 393 | } 394 | 395 | /** 396 | * @return the response content length in bytes 397 | */ 398 | public long getContentLength() { 399 | return contentLength; 400 | } 401 | 402 | /** 403 | * @return whether the resource has been pushed by the server 404 | */ 405 | public boolean isPushed() { 406 | return pushed; 407 | } 408 | 409 | void setPushed(boolean pushed) { 410 | this.pushed = pushed; 411 | } 412 | 413 | /** 414 | * @return the response HTTP status code 415 | */ 416 | public int getStatus() { 417 | return status; 418 | } 419 | 420 | void setStatus(int status) { 421 | this.status = status; 422 | } 423 | 424 | /** 425 | * @return the request/response failure, if any 426 | */ 427 | public Throwable getFailure() { 428 | return failure; 429 | } 430 | 431 | void setFailure(Throwable failure) { 432 | this.failure = failure; 433 | } 434 | 435 | @Override 436 | public String toString() { 437 | return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), getResource()); 438 | } 439 | } 440 | 441 | /** 442 | *

Generic listener for resource events.

443 | * 444 | * @see NodeListener 445 | * @see TreeListener 446 | */ 447 | public interface Listener extends EventListener { 448 | } 449 | 450 | /** 451 | *

Listener for resource node events.

452 | *

Resource node events are emitted for non-warmup resource requests that completed successfully.

453 | */ 454 | public interface NodeListener extends Listener { 455 | public void onResourceNode(Info info); 456 | } 457 | 458 | /** 459 | *

Listener for resource tree events.

460 | *

Resource tree events are emitted for the non-warmup root resource.

461 | */ 462 | public interface TreeListener extends Listener { 463 | public void onResourceTree(Info info); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/main/java/org/mortbay/jetty/load/generator/ServerInfo.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.io.IOException; 17 | import java.net.URI; 18 | import java.util.Map; 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | import org.eclipse.jetty.client.BufferingResponseListener; 22 | import org.eclipse.jetty.client.HttpClient; 23 | import org.eclipse.jetty.client.Result; 24 | import org.eclipse.jetty.http.HttpStatus; 25 | import org.eclipse.jetty.util.ajax.JSON; 26 | 27 | /** 28 | *

A value class representing server-side information.

29 | *

A server would expose a well known path such as {@code /.well-known/serverInfo} 30 | * that can be invoked via:

31 | *
 32 |  * ServerInfo serverInfo = ServerInfo.retrieveServerInfo(httpClient, URI.create("http://localhost:8080/.well-known/serverInfo"));
 33 |  * 
34 | *

The server should respond with JSON content with the following format:

35 | *
 36 |  * {
 37 |  *     "serverVersion": "jetty-9.4.x",
 38 |  *     "processorCount": 12,
 39 |  *     "totalMemory": 34359738368,
 40 |  *     "gitHash": "0123456789abcdef",
 41 |  *     "javaVersion": "11.0.10+9"
 42 |  * }
 43 |  * 
44 | */ 45 | public class ServerInfo implements JSON.Convertible { 46 | private String serverVersion; 47 | private int processorCount; 48 | private long totalMemory; 49 | private String gitHash; 50 | private String javaVersion; 51 | 52 | public String getServerVersion() { 53 | return serverVersion; 54 | } 55 | 56 | public void setServerVersion(String serverVersion) { 57 | this.serverVersion = serverVersion; 58 | } 59 | 60 | public int getProcessorCount() { 61 | return processorCount; 62 | } 63 | 64 | public void setProcessorCount(int processorCount) { 65 | this.processorCount = processorCount; 66 | } 67 | 68 | public long getTotalMemory() { 69 | return totalMemory; 70 | } 71 | 72 | public void setTotalMemory(long totalMemory) { 73 | this.totalMemory = totalMemory; 74 | } 75 | 76 | public String getGitHash() { 77 | return gitHash; 78 | } 79 | 80 | public void setGitHash(String gitHash) { 81 | this.gitHash = gitHash; 82 | } 83 | 84 | public String getJavaVersion() { 85 | return javaVersion; 86 | } 87 | 88 | public void setJavaVersion(String javaVersion) { 89 | this.javaVersion = javaVersion; 90 | } 91 | 92 | @Override 93 | public void toJSON(JSON.Output out) { 94 | out.add("serverVersion", getServerVersion()); 95 | out.add("processorCount", getProcessorCount()); 96 | out.add("totalMemory", getTotalMemory()); 97 | out.add("gitHash", getGitHash()); 98 | out.add("javaVersion", getJavaVersion()); 99 | } 100 | 101 | @Override 102 | public void fromJSON(Map map) { 103 | setServerVersion((String)map.get("serverVersion")); 104 | Number processorCount = (Number)map.get("processorCount"); 105 | if (processorCount != null) { 106 | setProcessorCount(processorCount.intValue()); 107 | } 108 | Number totalMemory = (Number)map.get("totalMemory"); 109 | if (totalMemory != null) { 110 | setTotalMemory(totalMemory.longValue()); 111 | } 112 | setGitHash((String)map.get("gitHash")); 113 | setJavaVersion((String)map.get("javaVersion")); 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return String.format("%s{jettyVersion=%s, availableProcessors=%d, totalMemory=%d, gitHash=%s, javaVersion=%s}", 119 | getClass().getSimpleName(), getServerVersion(), getProcessorCount(), getTotalMemory(), getGitHash(), getJavaVersion()); 120 | } 121 | 122 | public static CompletableFuture retrieveServerInfo(HttpClient httpClient, URI uri) { 123 | CompletableFuture complete = new CompletableFuture<>(); 124 | httpClient.newRequest(uri).send(new BufferingResponseListener() { 125 | @Override 126 | public void onComplete(Result result) { 127 | if (result.isSucceeded()) { 128 | int status = result.getResponse().getStatus(); 129 | if (status == HttpStatus.OK_200) { 130 | ServerInfo serverInfo = new ServerInfo(); 131 | @SuppressWarnings("unchecked") 132 | Map map = (Map)new JSON().parse(new JSON.StringSource(getContentAsString())); 133 | serverInfo.fromJSON(map); 134 | complete.complete(serverInfo); 135 | } else { 136 | complete.completeExceptionally(new IOException("could not retrieve server info: HTTP " + status)); 137 | } 138 | } else { 139 | complete.completeExceptionally(result.getFailure()); 140 | } 141 | } 142 | }); 143 | return complete; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/FailFastTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | 19 | import org.eclipse.jetty.client.Request; 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.server.Handler; 22 | import org.eclipse.jetty.server.HttpConfiguration; 23 | import org.eclipse.jetty.server.HttpConnectionFactory; 24 | import org.eclipse.jetty.server.Response; 25 | import org.eclipse.jetty.server.Server; 26 | import org.eclipse.jetty.server.ServerConnector; 27 | import org.eclipse.jetty.server.handler.StatisticsHandler; 28 | import org.eclipse.jetty.util.Callback; 29 | import org.eclipse.jetty.util.Fields; 30 | import org.eclipse.jetty.util.component.LifeCycle; 31 | import org.eclipse.jetty.util.thread.ExecutorThreadPool; 32 | import org.junit.After; 33 | import org.junit.Assert; 34 | import org.junit.Before; 35 | import org.junit.Ignore; 36 | import org.junit.Test; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | public class FailFastTest { 41 | private static final Logger LOGGER = LoggerFactory.getLogger(FailFastTest.class); 42 | 43 | private Server server; 44 | private ServerConnector connector; 45 | 46 | @Before 47 | public void startJetty() throws Exception { 48 | server = new Server(new ExecutorThreadPool(5120)); 49 | connector = new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration())); 50 | server.addConnector(connector); 51 | 52 | ServerStopHandler serverStopHandler = new ServerStopHandler(server); 53 | StatisticsHandler statisticsHandler = new StatisticsHandler(); 54 | statisticsHandler.setHandler(serverStopHandler); 55 | server.setHandler(statisticsHandler); 56 | server.start(); 57 | } 58 | 59 | @After 60 | public void stopJetty() throws Exception { 61 | if (server != null) { 62 | server.stop(); 63 | } 64 | } 65 | 66 | @Test 67 | // Other load test tools continue to send load even if the server is down. 68 | @Ignore 69 | public void testFailFastOnServerStop() { 70 | AtomicInteger onFailure = new AtomicInteger(0); 71 | AtomicInteger onCommit = new AtomicInteger(0); 72 | LoadGenerator.Builder builder = LoadGenerator.builder() 73 | .host("localhost") 74 | .port(connector.getLocalPort()) 75 | .resource(new Resource("/index.html?fail=5")) 76 | .warmupIterationsPerThread(1) 77 | .usersPerThread(1) 78 | .threads(1) 79 | .resourceRate(5) 80 | .iterationsPerThread(25) 81 | .requestListener(new Request.Listener() { 82 | @Override 83 | public void onFailure(Request request, Throwable failure) { 84 | LOGGER.info("fail: {}", onFailure.incrementAndGet()); 85 | } 86 | 87 | @Override 88 | public void onCommit(Request request) { 89 | LOGGER.info("onCommit: {}", onCommit.incrementAndGet()); 90 | } 91 | }); 92 | 93 | try { 94 | builder.build().begin().get(15, TimeUnit.SECONDS); 95 | Assert.fail(); 96 | } catch (Exception ignored) { 97 | } 98 | 99 | LOGGER.info("onFailure: {}, onCommit: {}", onFailure, onCommit); 100 | int onFailureCall = onFailure.get(); 101 | // The value is really dependant on machine... 102 | Assert.assertTrue("onFailureCall is " + onFailureCall, onFailureCall < 10); 103 | } 104 | 105 | private static class ServerStopHandler extends Handler.Abstract { 106 | private final AtomicInteger requests = new AtomicInteger(); 107 | private final Server server; 108 | 109 | private ServerStopHandler(Server server) { 110 | this.server = server; 111 | } 112 | 113 | @Override 114 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 115 | Fields parameters = org.eclipse.jetty.server.Request.extractQueryParameters(request); 116 | if (requests.incrementAndGet() > Integer.parseInt(parameters.getValue("fail"))) { 117 | new Thread(() -> LifeCycle.stop(server)).start(); 118 | } 119 | Content.Sink.write(response, true, "Jetty rocks!!", callback); 120 | return true; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/HTTP1WebsiteLoadGeneratorTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | import org.HdrHistogram.AtomicHistogram; 19 | import org.HdrHistogram.Histogram; 20 | import org.eclipse.jetty.client.Request; 21 | import org.eclipse.jetty.server.HttpConnectionFactory; 22 | import org.eclipse.jetty.toolchain.perf.HistogramSnapshot; 23 | import org.eclipse.jetty.util.thread.MonitoredQueuedThreadPool; 24 | import org.junit.Assert; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | 28 | public class HTTP1WebsiteLoadGeneratorTest extends WebsiteLoadGeneratorTest { 29 | @Before 30 | public void prepare() throws Exception { 31 | prepareServer(new HttpConnectionFactory(), new TestHandler()); 32 | } 33 | 34 | @Test 35 | public void testHTTP1() throws Exception { 36 | MonitoredQueuedThreadPool executor = new MonitoredQueuedThreadPool(1024); 37 | executor.start(); 38 | 39 | AtomicLong requests = new AtomicLong(); 40 | Histogram treeHistogram = new AtomicHistogram(TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10), 3); 41 | Histogram rootHistogram = new AtomicHistogram(TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10), 3); 42 | LoadGenerator loadGenerator = prepareLoadGenerator(new HTTP1ClientTransportBuilder()) 43 | .warmupIterationsPerThread(10) 44 | .iterationsPerThread(100) 45 | // .warmupIterationsPerThread(1000) 46 | // .runFor(2, TimeUnit.MINUTES) 47 | .usersPerThread(100) 48 | .channelsPerUser(6) 49 | .resourceRate(20) 50 | .executor(executor) 51 | .resourceListener((Resource.TreeListener)info -> { 52 | rootHistogram.recordValue(info.getResponseTime() - info.getRequestTime()); 53 | treeHistogram.recordValue(info.getTreeTime() - info.getRequestTime()); 54 | }) 55 | .requestListener(new Request.Listener() { 56 | @Override 57 | public void onQueued(Request request) { 58 | requests.incrementAndGet(); 59 | } 60 | }) 61 | .requestListener(new Request.Listener() { 62 | @Override 63 | public void onBegin(Request request) { 64 | requests.decrementAndGet(); 65 | } 66 | }) 67 | .build(); 68 | 69 | long begin = System.nanoTime(); 70 | loadGenerator.begin().join(); 71 | long elapsed = System.nanoTime() - begin; 72 | 73 | Assert.assertEquals(0, requests.get()); 74 | 75 | int serverRequests = serverStats.getRequests(); 76 | System.err.printf("%nserver - requests: %d, rate: %.3f, max_request_time: %d%n%n", 77 | serverRequests, 78 | elapsed > 0 ? serverRequests * 1_000_000_000F / elapsed : 0F, 79 | TimeUnit.NANOSECONDS.toMillis(serverStats.getRequestTimeMax())); 80 | 81 | HistogramSnapshot treeSnapshot = new HistogramSnapshot(treeHistogram, 20, "tree response time", "us", TimeUnit.NANOSECONDS::toMicros); 82 | System.err.println(treeSnapshot); 83 | HistogramSnapshot rootSnapshot = new HistogramSnapshot(rootHistogram, 20, "root response time", "us", TimeUnit.NANOSECONDS::toMicros); 84 | System.err.println(rootSnapshot); 85 | 86 | System.err.printf("client thread pool - max_threads: %d, max_queue_size: %d, max_queue_latency: %dms%n%n", 87 | executor.getMaxBusyThreads(), 88 | executor.getMaxQueueSize(), 89 | TimeUnit.NANOSECONDS.toMillis(executor.getMaxQueueLatency()) 90 | ); 91 | 92 | executor.stop(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/HTTP2LoadGeneratorTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; 19 | import org.eclipse.jetty.client.Request; 20 | import org.eclipse.jetty.http.HttpFields; 21 | import org.eclipse.jetty.http.HttpMethod; 22 | import org.eclipse.jetty.http.HttpStatus; 23 | import org.eclipse.jetty.http.HttpURI; 24 | import org.eclipse.jetty.http.HttpVersion; 25 | import org.eclipse.jetty.http.MetaData; 26 | import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; 27 | import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; 28 | import org.eclipse.jetty.server.Handler; 29 | import org.eclipse.jetty.server.HttpConfiguration; 30 | import org.eclipse.jetty.server.Response; 31 | import org.eclipse.jetty.server.SecureRequestCustomizer; 32 | import org.eclipse.jetty.server.Server; 33 | import org.eclipse.jetty.server.ServerConnector; 34 | import org.eclipse.jetty.server.SslConnectionFactory; 35 | import org.eclipse.jetty.util.Callback; 36 | import org.eclipse.jetty.util.ssl.SslContextFactory; 37 | import org.junit.After; 38 | import org.junit.Assert; 39 | import org.junit.Test; 40 | 41 | public class HTTP2LoadGeneratorTest { 42 | private Server server; 43 | private ServerConnector connector; 44 | private ServerConnector tlsConnector; 45 | 46 | private void startServer(Handler handler) throws Exception { 47 | server = new Server(); 48 | 49 | HttpConfiguration httpConfig = new HttpConfiguration(); 50 | connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig)); 51 | server.addConnector(connector); 52 | 53 | HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); 54 | httpsConfig.addCustomizer(new SecureRequestCustomizer()); 55 | HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); 56 | ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); 57 | alpn.setDefaultProtocol(h2.getProtocol()); 58 | SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); 59 | sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); 60 | sslContextFactory.setKeyStoreType("pkcs12"); 61 | sslContextFactory.setKeyStorePassword("storepwd"); 62 | SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); 63 | tlsConnector = new ServerConnector(server, 1, 1, tls, alpn, h2); 64 | server.addConnector(tlsConnector); 65 | 66 | server.setHandler(handler); 67 | server.start(); 68 | } 69 | 70 | @After 71 | public void dispose() throws Exception { 72 | if (server != null) { 73 | server.stop(); 74 | } 75 | } 76 | 77 | @Test 78 | public void testHTTP2() throws Exception { 79 | startServer(new TestHandler()); 80 | 81 | AtomicLong responses = new AtomicLong(); 82 | LoadGenerator loadGenerator = LoadGenerator.builder() 83 | .scheme("https") 84 | .port(tlsConnector.getLocalPort()) 85 | .sslContextFactory(new SslContextFactory.Client(true)) 86 | .httpClientTransportBuilder(new HTTP2ClientTransportBuilder()) 87 | .resource(new Resource("/", new Resource("/1"), new Resource("/2")).responseLength(128 * 1024)) 88 | .resourceListener((Resource.NodeListener)info -> { 89 | if (info.getStatus() == HttpStatus.OK_200) { 90 | responses.incrementAndGet(); 91 | } 92 | }) 93 | .build(); 94 | loadGenerator.begin().get(5, TimeUnit.SECONDS); 95 | 96 | Assert.assertEquals(3, responses.get()); 97 | } 98 | 99 | @Test 100 | public void testPush() throws Exception { 101 | startServer(new TestHandler() { 102 | @Override 103 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 104 | if ("/".equals(org.eclipse.jetty.server.Request.getPathInContext(request))) { 105 | MetaData.Request push1 = new MetaData.Request( 106 | HttpMethod.GET.asString(), 107 | HttpURI.build(request.getHttpURI()).path("/1"), 108 | HttpVersion.HTTP_2, 109 | HttpFields.build().put(Resource.RESPONSE_LENGTH, String.valueOf(10 * 1024)) 110 | ); 111 | request.push(push1); 112 | 113 | MetaData.Request push2 = new MetaData.Request( 114 | HttpMethod.GET.asString(), 115 | HttpURI.build(request.getHttpURI()).path("/2"), 116 | HttpVersion.HTTP_2, 117 | HttpFields.build().put(Resource.RESPONSE_LENGTH, String.valueOf(32 * 1024)) 118 | ); 119 | request.push(push2); 120 | } 121 | return super.handle(request, response, callback); 122 | } 123 | }); 124 | 125 | AtomicLong requests = new AtomicLong(); 126 | AtomicLong sent = new AtomicLong(); 127 | AtomicLong pushed = new AtomicLong(); 128 | LoadGenerator loadGenerator = new LoadGenerator.Builder() 129 | .httpClientTransportBuilder(new HTTP2ClientTransportBuilder()) 130 | .port(connector.getLocalPort()) 131 | .resource(new Resource("/", new Resource("/1"), new Resource("/2")).responseLength(128 * 1024)) 132 | .requestListener(new Request.Listener() { 133 | @Override 134 | public void onBegin(Request request) { 135 | requests.incrementAndGet(); 136 | } 137 | }) 138 | .resourceListener((Resource.NodeListener)info -> { 139 | if (info.isPushed()) { 140 | pushed.incrementAndGet(); 141 | } else { 142 | sent.incrementAndGet(); 143 | } 144 | }) 145 | .build(); 146 | loadGenerator.begin().get(5, TimeUnit.SECONDS); 147 | 148 | Assert.assertEquals(1, requests.get()); 149 | Assert.assertEquals(1, sent.get()); 150 | Assert.assertEquals(2, pushed.get()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/HTTP2WebsiteLoadGeneratorTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | import org.HdrHistogram.AtomicHistogram; 19 | import org.HdrHistogram.Histogram; 20 | import org.eclipse.jetty.client.Request; 21 | import org.eclipse.jetty.http.HttpFields; 22 | import org.eclipse.jetty.http.HttpMethod; 23 | import org.eclipse.jetty.http.HttpURI; 24 | import org.eclipse.jetty.http.HttpVersion; 25 | import org.eclipse.jetty.http.MetaData; 26 | import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; 27 | import org.eclipse.jetty.server.HttpConfiguration; 28 | import org.eclipse.jetty.server.Response; 29 | import org.eclipse.jetty.toolchain.perf.HistogramSnapshot; 30 | import org.eclipse.jetty.util.Callback; 31 | import org.eclipse.jetty.util.thread.MonitoredQueuedThreadPool; 32 | import org.junit.Assert; 33 | import org.junit.Test; 34 | 35 | public class HTTP2WebsiteLoadGeneratorTest extends WebsiteLoadGeneratorTest { 36 | @Test 37 | public void testHTTP2WithoutPush() throws Exception { 38 | prepareServer(new HTTP2ServerConnectionFactory(new HttpConfiguration()), new TestHandler()); 39 | testHTTP2(); 40 | } 41 | 42 | @Test 43 | public void testHTTP2WithPush() throws Exception { 44 | prepareServer(new HTTP2ServerConnectionFactory(new HttpConfiguration()), new PushingHandler()); 45 | testHTTP2(); 46 | } 47 | 48 | private void testHTTP2() throws Exception { 49 | MonitoredQueuedThreadPool executor = new MonitoredQueuedThreadPool(1024); 50 | executor.start(); 51 | 52 | AtomicLong requests = new AtomicLong(); 53 | Histogram treeHistogram = new AtomicHistogram(TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10), 3); 54 | Histogram rootHistogram = new AtomicHistogram(TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10), 3); 55 | LoadGenerator loadGenerator = prepareLoadGenerator(new HTTP2ClientTransportBuilder()) 56 | .warmupIterationsPerThread(10) 57 | .iterationsPerThread(100) 58 | // .warmupIterationsPerThread(1000) 59 | // .runFor(2, TimeUnit.MINUTES) 60 | .usersPerThread(100) 61 | .channelsPerUser(1000) 62 | .resourceRate(20) 63 | .executor(executor) 64 | .resourceListener((Resource.TreeListener)info -> { 65 | rootHistogram.recordValue(info.getResponseTime() - info.getRequestTime()); 66 | treeHistogram.recordValue(info.getTreeTime() - info.getRequestTime()); 67 | }) 68 | .requestListener(new Request.Listener() { 69 | @Override 70 | public void onQueued(Request request) { 71 | requests.incrementAndGet(); 72 | } 73 | }) 74 | .requestListener(new Request.Listener() { 75 | @Override 76 | public void onBegin(Request request) { 77 | requests.decrementAndGet(); 78 | } 79 | }) 80 | .build(); 81 | 82 | long begin = System.nanoTime(); 83 | loadGenerator.begin().join(); 84 | long elapsed = System.nanoTime() - begin; 85 | 86 | Assert.assertEquals(0, requests.get()); 87 | 88 | int serverRequests = serverStats.getRequests(); 89 | System.err.printf("%nserver - requests: %d, rate: %.3f, max_request_time: %d%n%n", 90 | serverRequests, 91 | elapsed > 0 ? serverRequests * 1_000_000_000F / elapsed : 0F, 92 | TimeUnit.NANOSECONDS.toMillis(serverStats.getRequestTimeMax())); 93 | 94 | HistogramSnapshot treeSnapshot = new HistogramSnapshot(treeHistogram, 20, "tree response time", "us", TimeUnit.NANOSECONDS::toMicros); 95 | System.err.println(treeSnapshot); 96 | HistogramSnapshot rootSnapshot = new HistogramSnapshot(rootHistogram, 20, "root response time", "us", TimeUnit.NANOSECONDS::toMicros); 97 | System.err.println(rootSnapshot); 98 | 99 | System.err.printf("client thread pool - max_threads: %d, max_queue_size: %d, max_queue_latency: %dms%n%n", 100 | executor.getMaxBusyThreads(), 101 | executor.getMaxQueueSize(), 102 | TimeUnit.NANOSECONDS.toMillis(executor.getMaxQueueLatency()) 103 | ); 104 | 105 | executor.stop(); 106 | } 107 | 108 | private class PushingHandler extends TestHandler { 109 | @Override 110 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 111 | if ("/".equals(org.eclipse.jetty.server.Request.getPathInContext(request))) { 112 | for (Resource resource : resource.getResources()) { 113 | MetaData.Request push = new MetaData.Request( 114 | HttpMethod.GET.asString(), 115 | HttpURI.build(request.getHttpURI()).path(resource.getPath()), 116 | HttpVersion.HTTP_2, 117 | HttpFields.build().put(Resource.RESPONSE_LENGTH, Long.toString(resource.getResponseLength())) 118 | ); 119 | request.push(push); 120 | } 121 | } 122 | return super.handle(request, response, callback); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/ResourceBuildTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.InputStreamReader; 20 | import java.net.URL; 21 | import java.util.Objects; 22 | import java.util.stream.Collectors; 23 | 24 | import groovy.lang.GroovyShell; 25 | import org.codehaus.groovy.control.CompilerConfiguration; 26 | import org.eclipse.jetty.http.HttpMethod; 27 | import org.eclipse.jetty.xml.XmlConfiguration; 28 | import org.junit.Assert; 29 | import org.junit.Test; 30 | 31 | public class ResourceBuildTest { 32 | @Test 33 | public void testSimpleBuild() { 34 | Resource resourceProfile = new Resource(new Resource("/index.html").requestLength(1024)); 35 | 36 | Assert.assertEquals(1, resourceProfile.getResources().size()); 37 | Assert.assertEquals("/index.html", resourceProfile.getResources().get(0).getPath()); 38 | Assert.assertEquals(1024, resourceProfile.getResources().get(0).getRequestLength()); 39 | Assert.assertEquals("GET", resourceProfile.getResources().get(0).getMethod()); 40 | } 41 | 42 | @Test 43 | public void testSimpleTwoResources() { 44 | Resource resourceProfile = new Resource( 45 | new Resource("/index.html").requestLength(1024), 46 | new Resource("/beer.html").requestLength(2048).method(HttpMethod.POST.asString()) 47 | ); 48 | 49 | Assert.assertEquals(2, resourceProfile.getResources().size()); 50 | Assert.assertEquals("/index.html", resourceProfile.getResources().get(0).getPath()); 51 | Assert.assertEquals(1024, resourceProfile.getResources().get(0).getRequestLength()); 52 | Assert.assertEquals("GET", resourceProfile.getResources().get(0).getMethod()); 53 | Assert.assertEquals("/beer.html", resourceProfile.getResources().get(1).getPath()); 54 | Assert.assertEquals(2048, resourceProfile.getResources().get(1).getRequestLength()); 55 | Assert.assertEquals("POST", resourceProfile.getResources().get(1).getMethod()); 56 | } 57 | 58 | @Test 59 | public void testWebsiteTree() { 60 | Resource sample = new Resource( 61 | new Resource("index.html", 62 | new Resource("/style.css", 63 | new Resource("/logo.gif"), 64 | new Resource("/spacer.png") 65 | ), 66 | new Resource("/fancy.css"), 67 | new Resource("/script.js", 68 | new Resource("/library.js"), 69 | new Resource("/morestuff.js") 70 | ), 71 | new Resource("/anotherScript.js"), 72 | new Resource("/iframeContents.html"), 73 | new Resource("/moreIframeContents.html"), 74 | new Resource("/favicon.ico") 75 | )); 76 | 77 | assertWebsiteTree(sample); 78 | } 79 | 80 | @Test 81 | public void testWebsiteTreeWithXML() throws Exception { 82 | URL xml = Thread.currentThread().getContextClassLoader().getResource("website_profile.xml"); 83 | Resource sample = (Resource)new XmlConfiguration(Objects.requireNonNull(org.eclipse.jetty.util.resource.ResourceFactory.root().newResource(xml))).configure(); 84 | assertWebsiteTree(sample); 85 | } 86 | 87 | @Test 88 | public void testWebsiteTreeWithGroovy() throws Exception { 89 | try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("website_profile.groovy")) { 90 | Resource sample = (Resource)evaluateScript(read(inputStream)); 91 | assertWebsiteTree(sample); 92 | } 93 | } 94 | 95 | private static String read(InputStream input) throws IOException { 96 | try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { 97 | return buffer.lines().collect(Collectors.joining(System.lineSeparator())); 98 | } 99 | } 100 | 101 | public Object evaluateScript(String script) { 102 | CompilerConfiguration config = new CompilerConfiguration(CompilerConfiguration.DEFAULT); 103 | config.setDebug(true); 104 | config.setVerbose(true); 105 | GroovyShell interpreter = new GroovyShell(config); 106 | return interpreter.evaluate(script); 107 | } 108 | 109 | protected void assertWebsiteTree(Resource sample) { 110 | /* 111 | GET index.html 112 | style.css 113 | logo.gif 114 | spacer.png 115 | fancy.css 116 | script.js 117 | library.js 118 | morestuff.js 119 | anotherScript.js 120 | iframeContents.html 121 | moreIframeContents.html 122 | favicon.ico 123 | */ 124 | 125 | Assert.assertEquals(1, sample.getResources().size()); 126 | Assert.assertEquals(7, sample.getResources().get(0).getResources().size()); 127 | Assert.assertEquals("/style.css", sample.getResources().get(0).getResources().get(0).getPath()); 128 | Assert.assertEquals("/logo.gif", sample.getResources().get(0) 129 | .getResources().get(0).getResources().get(0).getPath()); 130 | Assert.assertEquals("/spacer.png", sample.getResources().get(0) 131 | .getResources().get(0).getResources().get(1).getPath()); 132 | Assert.assertEquals(2, sample.getResources().get(0) 133 | .getResources().get(0).getResources().size()); 134 | Assert.assertEquals(2, sample.getResources().get(0) 135 | .getResources().get(2).getResources().size()); 136 | Assert.assertEquals("/library.js", sample.getResources().get(0) 137 | .getResources().get(2).getResources().get(0).getPath()); 138 | Assert.assertEquals("/morestuff.js", sample.getResources().get(0) 139 | .getResources().get(2).getResources().get(1).getPath()); 140 | Assert.assertEquals("/anotherScript.js", sample.getResources().get(0) 141 | .getResources().get(3).getPath()); 142 | Assert.assertEquals("/moreIframeContents.html", sample.getResources().get(0) 143 | .getResources().get(5).getPath()); 144 | Assert.assertEquals("/favicon.ico", sample.getResources().get(0) 145 | .getResources().get(6).getPath()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/TestHandler.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import java.nio.ByteBuffer; 17 | 18 | import org.eclipse.jetty.server.Handler; 19 | import org.eclipse.jetty.server.Request; 20 | import org.eclipse.jetty.server.Response; 21 | import org.eclipse.jetty.util.Callback; 22 | import org.eclipse.jetty.util.IteratingNestedCallback; 23 | 24 | public class TestHandler extends Handler.Abstract { 25 | @Override 26 | public boolean handle(Request request, Response response, Callback callback) { 27 | long length = request.getHeaders().getLongField(Resource.RESPONSE_LENGTH); 28 | if (length > 0) { 29 | sendResponseContent(response, length, callback); 30 | } else { 31 | callback.succeeded(); 32 | } 33 | return true; 34 | } 35 | 36 | private void sendResponseContent(Response response, long contentLength, Callback callback) { 37 | new IteratingNestedCallback(callback) { 38 | private long length = contentLength; 39 | 40 | @Override 41 | protected Action process() { 42 | if (length == 0) { 43 | return Action.SUCCEEDED; 44 | } 45 | int l = (int)Math.min(length, 2048); 46 | length -= l; 47 | response.write(length == 0, ByteBuffer.allocate(l), this); 48 | return Action.SCHEDULED; 49 | } 50 | }.iterate(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/java/org/mortbay/jetty/load/generator/WebsiteLoadGeneratorTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator; 15 | 16 | import org.eclipse.jetty.http.HttpFields; 17 | import org.eclipse.jetty.server.ConnectionFactory; 18 | import org.eclipse.jetty.server.CustomRequestLog; 19 | import org.eclipse.jetty.server.Handler; 20 | import org.eclipse.jetty.server.RequestLogWriter; 21 | import org.eclipse.jetty.server.Server; 22 | import org.eclipse.jetty.server.ServerConnector; 23 | import org.eclipse.jetty.server.handler.StatisticsHandler; 24 | import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; 25 | import org.eclipse.jetty.util.thread.Scheduler; 26 | import org.junit.After; 27 | 28 | public abstract class WebsiteLoadGeneratorTest { 29 | protected Resource resource; 30 | protected Server server; 31 | protected ServerConnector connector; 32 | protected Scheduler scheduler; 33 | protected StatisticsHandler serverStats; 34 | 35 | public WebsiteLoadGeneratorTest() { 36 | // A dump of the resources needed by the webtide.com website. 37 | HttpFields.Mutable headers = HttpFields.build(); 38 | headers.put("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0"); 39 | headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); 40 | headers.put("Accept-Language", "en-US,en;q=0.5"); 41 | headers.put("Cookie", "__utma=124097164.2025215041.1465995519.1483973120.1485461487.58; __utmz=124097164.1480932641.29.9.utmcsr=localhost:8080|utmccn=(referral)|utmcmd=referral|utmcct=/; wp-settings-3=editor%3Dhtml%26wplink%3D1%26post_dfw%3Doff%26posts_list_mode%3Dlist; wp-settings-time-3=1483536385; wp-settings-time-4=1485794804; wp-settings-4=editor%3Dhtml; _ga=GA1.2.2025215041.1465995519; wordpress_google_apps_login=30a7b62f9ae5db1653367cafa3accacd; PHPSESSID=r8rr7hnl7kttpq40q7bkbcn5c2; ckon1703=sject1703_bfc34a0618c85; JCS_INENREF=; JCS_INENTIM=1489507850637; _gat=1"); 42 | resource = new Resource("/", 43 | new Resource("/styles.css").requestHeaders(headers).responseLength(1600), 44 | new Resource("/pagenavi-css.css").requestHeaders(headers).responseLength(426), 45 | new Resource("/style.css").requestHeaders(headers).responseLength(74900), 46 | new Resource("/genericicons.css").requestHeaders(headers).responseLength(27700), 47 | new Resource("/font-awesome.min.css").requestHeaders(headers).responseLength(28400), 48 | new Resource("/jquery.js").requestHeaders(headers).responseLength(95000), 49 | new Resource("/jquery-migrate.min.js").requestHeaders(headers).responseLength(9900), 50 | new Resource("/picturefill.min.js").requestHeaders(headers).responseLength(11600), 51 | new Resource("/jscripts.php").requestHeaders(headers).responseLength(842), 52 | new Resource("/cropped-WTLogo-2.png").requestHeaders(headers).responseLength(3900), 53 | new Resource("/pexels-photo-40120-1.jpeg").requestHeaders(headers).responseLength(143000), 54 | new Resource("/Keyboard.jpg").requestHeaders(headers).responseLength(90000), 55 | new Resource("/Jetty-Code-2x.jpg").requestHeaders(headers).responseLength(697000), 56 | new Resource("/rocket.png").requestHeaders(headers).responseLength(3700), 57 | new Resource("/aperture2.png").requestHeaders(headers).responseLength(2900), 58 | new Resource("/dev.png").requestHeaders(headers).responseLength(3500), 59 | new Resource("/jetty-avatar.png").requestHeaders(headers).responseLength(10000), 60 | new Resource("/megaphone.png").requestHeaders(headers).responseLength(2400), 61 | new Resource("/jquery.form.min.js").requestHeaders(headers).responseLength(14900), 62 | new Resource("/scripts.js").requestHeaders(headers).responseLength(11900), 63 | new Resource("/jquery.circle2.min.js").requestHeaders(headers).responseLength(22500), 64 | new Resource("/jquery.circle2.swipe.min.js").requestHeaders(headers).responseLength(9900), 65 | new Resource("/waypoints.min.js").requestHeaders(headers).responseLength(7400), 66 | new Resource("/jquery.counterup.min.js").requestHeaders(headers).responseLength(1000), 67 | new Resource("/navigation.min.js").requestHeaders(headers).responseLength(582), 68 | new Resource("/spacious-custom.min.js").requestHeaders(headers).responseLength(1300), 69 | new Resource("/jscripts-ftr-min.js").requestHeaders(headers).responseLength(998), 70 | new Resource("/wp-embed.min.js").requestHeaders(headers).responseLength(1400), 71 | new Resource("/wp-emoji-release.min.js").requestHeaders(headers).responseLength(11200), 72 | new Resource("/fontawesome-webfont.woff2").requestHeaders(headers).responseLength(70300) 73 | ).requestHeaders(headers).responseLength(30700); 74 | } 75 | 76 | protected void prepareServer(ConnectionFactory connectionFactory, Handler handler) throws Exception { 77 | server = new Server(); 78 | connector = new ServerConnector(server, connectionFactory); 79 | server.addConnector(connector); 80 | 81 | // The request log ensures that the request 82 | // is inspected how an application would do. 83 | CustomRequestLog requestLog = new CustomRequestLog(new RequestLogWriter() { 84 | @Override 85 | public void write(String log) { 86 | // Do not write the log. 87 | } 88 | }, CustomRequestLog.NCSA_FORMAT); 89 | server.setRequestLog(requestLog); 90 | serverStats = new StatisticsHandler(); 91 | 92 | server.setHandler(serverStats); 93 | serverStats.setHandler(handler); 94 | 95 | scheduler = new ScheduledExecutorScheduler(); 96 | server.addBean(scheduler, true); 97 | 98 | server.start(); 99 | } 100 | 101 | protected LoadGenerator.Builder prepareLoadGenerator(HTTPClientTransportBuilder clientTransportBuilder) { 102 | return LoadGenerator.builder() 103 | .threads(1) 104 | .port(connector.getLocalPort()) 105 | .httpClientTransportBuilder(clientTransportBuilder) 106 | .resource(resource) 107 | .scheduler(scheduler); 108 | } 109 | 110 | @After 111 | public void dispose() throws Exception { 112 | if (server != null) { 113 | server.stop(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | #org.eclipse.jetty.LEVEL=DEBUG 2 | #org.eclipse.jetty.client.LEVEL=DEBUG 3 | #org.eclipse.jetty.http2.LEVEL=DEBUG 4 | #org.eclipse.jetty.http2.client.LEVEL=DEBUG 5 | #org.mortbay.jetty.load.generator.LEVEL=DEBUG 6 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/resources/keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetty-project/jetty-load-generator/c419f8f12b869192f3fc35d65974cf8127595adc/jetty-load-generator-client/src/test/resources/keystore.p12 -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/resources/website_profile.groovy: -------------------------------------------------------------------------------- 1 | import org.mortbay.jetty.load.generator.Resource 2 | 3 | return new Resource( 4 | new Resource("index.html", 5 | new Resource("/style.css", 6 | new Resource("/logo.gif"), 7 | new Resource("/spacer.png") 8 | ), 9 | new Resource("/fancy.css"), 10 | new Resource("/script.js", 11 | new Resource("/library.js"), 12 | new Resource("/morestuff.js") 13 | ), 14 | new Resource("/anotherScript.js"), 15 | new Resource("/iframeContents.html"), 16 | new Resource("/moreIframeContents.html"), 17 | new Resource("/favicon.ico")) 18 | ) 19 | -------------------------------------------------------------------------------- /jetty-load-generator-client/src/test/resources/website_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | index.html 9 | 10 | 11 | 12 | 13 | /style.css 14 | 15 | 16 | 17 | 18 | /logo.gif 19 | 20 | 21 | 22 | 23 | /spacer.png 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | fancy.css 33 | 34 | 35 | 36 | 37 | script.js 38 | 39 | 40 | 41 | 42 | /library.js 43 | 44 | 45 | 46 | 47 | /morestuff.js 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | /anotherScript.js 57 | 58 | 59 | 60 | 61 | iframeContents.html 62 | 63 | 64 | 65 | 66 | /moreIframeContents.html 67 | 68 | 69 | 70 | 71 | /favicon.ico 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /jetty-load-generator-listeners/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.mortbay.jetty.loadgenerator 5 | jetty-load-generator 6 | 4.0.23-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | jetty-load-generator-listeners 11 | jar 12 | Jetty :: Load Generator :: Listeners 13 | 14 | 15 | ${project.groupId}.listeners 16 | 17 | 18 | 19 | 20 | org.mortbay.jetty.loadgenerator 21 | jetty-load-generator-client 22 | ${project.version} 23 | 24 | 25 | org.hdrhistogram 26 | HdrHistogram 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /jetty-load-generator-listeners/src/main/java/org/mortbay/jetty/load/generator/listeners/ReportListener.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator.listeners; 15 | 16 | import java.io.ByteArrayOutputStream; 17 | import java.lang.management.ManagementFactory; 18 | import java.nio.charset.StandardCharsets; 19 | import java.time.Duration; 20 | import java.time.Instant; 21 | import java.time.ZoneOffset; 22 | import java.util.Map; 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.atomic.LongAdder; 26 | import javax.management.MBeanServer; 27 | import javax.management.ObjectName; 28 | 29 | import org.HdrHistogram.Histogram; 30 | import org.HdrHistogram.HistogramLogWriter; 31 | import org.HdrHistogram.Recorder; 32 | import org.eclipse.jetty.io.Connection; 33 | import org.eclipse.jetty.io.ConnectionStatistics; 34 | import org.eclipse.jetty.util.ajax.JSON; 35 | import org.eclipse.jetty.util.component.ContainerLifeCycle; 36 | import org.mortbay.jetty.load.generator.LoadGenerator; 37 | import org.mortbay.jetty.load.generator.Resource; 38 | 39 | /** 40 | *

A load generator listener that reports information about a load run.

41 | *

Usage:

42 | *
 43 |  * // Create the report listener.
 44 |  * ReportListener listener = new ReportListener();
 45 |  *
 46 |  * // Create the LoadGenerator, passing the listener to relevant builder methods.
 47 |  * LoadGenerator generator = LoadGenerator.builder()
 48 |  *     ...
 49 |  *     .listener(listener)
 50 |  *     .resourceListener(listener)
 51 |  *     .build();
 52 |  *
 53 |  * // Add the listener as a bean of the generator.
 54 |  * generator.addBean(listener);
 55 |  *
 56 |  * // Start the load generation.
 57 |  * generator.begin();
 58 |  *
 59 |  * // Wait for the load generation to complete to get the report.
 60 |  * ReportListener.Report report = listener.whenComplete().join();
 61 |  *
 62 |  * System.err.printf("max response time: %d", report.getResponseTimeHistogram().getMaxValue());
 63 |  * 
64 | */ 65 | public class ReportListener extends ContainerLifeCycle implements LoadGenerator.BeginListener, LoadGenerator.ReadyListener, LoadGenerator.EndListener, LoadGenerator.CompleteListener, Resource.NodeListener, Connection.Listener { 66 | private final Report report = new Report(); 67 | private final CompletableFuture reportPromise = new CompletableFuture<>(); 68 | private final ConnectionStatistics connectionStats = new ConnectionStatistics(); 69 | private final Recorder recorder; 70 | 71 | /** 72 | *

Creates a report listener that records values between 1 microsecond and 1 minute with 3 digit precision.

73 | */ 74 | public ReportListener() { 75 | this(TimeUnit.MICROSECONDS.toNanos(1), TimeUnit.MINUTES.toNanos(1), 3); 76 | } 77 | 78 | /** 79 | *

Creates a report listener that record values between the {@code lowestDiscernibleValue} 80 | * and {@code highestTrackableValue} with {@code numberOfSignificantValueDigits} precision. 81 | * 82 | * @param lowestDiscernibleValue the minimum value recorded 83 | * @param highestTrackableValue the maximum value recorded 84 | * @param numberOfSignificantValueDigits the number of significant digits. 85 | * @see Histogram 86 | */ 87 | public ReportListener(long lowestDiscernibleValue, long highestTrackableValue, int numberOfSignificantValueDigits) { 88 | recorder = new Recorder(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits); 89 | addBean(connectionStats); 90 | } 91 | 92 | /** 93 | * @return a CompletableFuture that is completed when the load generation is complete 94 | */ 95 | public CompletableFuture whenComplete() { 96 | return reportPromise; 97 | } 98 | 99 | /** 100 | * @return the Instant of the load generation {@link LoadGenerator.BeginListener begin event} 101 | * @deprecated use {@link Report#getBeginInstant()} instead 102 | */ 103 | @Deprecated 104 | public Instant getBeginInstant() { 105 | return report.getBeginInstant(); 106 | } 107 | 108 | /** 109 | * @return the Instant of the load generation {@link LoadGenerator.CompleteListener complete event} 110 | * @deprecated use {@link Report#getCompleteInstant()} instead 111 | */ 112 | @Deprecated 113 | public Instant getCompleteInstant() { 114 | return report.getCompleteInstant(); 115 | } 116 | 117 | /** 118 | *

Returns the duration of the load generation recording.

119 | *

The recording starts at the {@link LoadGenerator.ReadyListener ready event} 120 | * and therefore excludes warmup.

121 | * 122 | * @return the Duration of the load generation recording 123 | * @deprecated use {@link Report#getRecordingDuration()} instead 124 | */ 125 | @Deprecated 126 | public Duration getRecordingDuration() { 127 | return report.getRecordingDuration(); 128 | } 129 | 130 | /** 131 | *

Returns the response time histogram.

132 | *

The response time is the time between a request is queued to be sent, 133 | * to the time the response is fully received, in nanoseconds.

134 | *

Warmup requests are not recorded.

135 | * 136 | * @return the response time histogram 137 | * @deprecated use {@link Report#getResponseTimeHistogram()} instead 138 | */ 139 | @Deprecated 140 | public Histogram getResponseTimeHistogram() { 141 | return report.getResponseTimeHistogram(); 142 | } 143 | 144 | /** 145 | * @return the request rate, in requests/s 146 | * @deprecated use {@link Report#getRequestRate()} instead 147 | */ 148 | @Deprecated 149 | public double getRequestRate() { 150 | return report.getRequestRate(); 151 | } 152 | 153 | /** 154 | * @return the response rate in responses/s 155 | * @deprecated use {@link Report#getResponseRate()} instead 156 | */ 157 | @Deprecated 158 | public double getResponseRate() { 159 | return report.getResponseRate(); 160 | } 161 | 162 | /** 163 | * @return the rate of bytes sent, in bytes/s 164 | * @deprecated use {@link Report#getSentBytesRate()} instead 165 | */ 166 | @Deprecated 167 | public double getSentBytesRate() { 168 | return report.getSentBytesRate(); 169 | } 170 | 171 | /** 172 | * @return the rate of bytes received, in bytes/s 173 | * @deprecated use {@link Report#getReceivedBytesRate()} instead 174 | */ 175 | @Deprecated 176 | public double getReceivedBytesRate() { 177 | return report.getReceivedBytesRate(); 178 | } 179 | 180 | /** 181 | * @return the number of HTTP 1xx responses 182 | * @deprecated use {@link Report#getResponses1xx()} instead 183 | */ 184 | @Deprecated 185 | public long getResponses1xx() { 186 | return report.getResponses1xx(); 187 | } 188 | 189 | /** 190 | * @return the number of HTTP 2xx responses 191 | * @deprecated use {@link Report#getResponses2xx()} instead 192 | */ 193 | @Deprecated 194 | public long getResponses2xx() { 195 | return report.getResponses2xx(); 196 | } 197 | 198 | /** 199 | * @return the number of HTTP 3xx responses 200 | * @deprecated use {@link Report#getResponses3xx()} instead 201 | */ 202 | @Deprecated 203 | public long getResponses3xx() { 204 | return report.getResponses3xx(); 205 | } 206 | 207 | /** 208 | * @return the number of HTTP 4xx responses 209 | * @deprecated use {@link Report#getResponses4xx()} instead 210 | */ 211 | @Deprecated 212 | public long getResponses4xx() { 213 | return report.getResponses4xx(); 214 | } 215 | 216 | /** 217 | * @return the number of HTTP 5xx responses 218 | * @deprecated use {@link Report#getResponses5xx()} instead 219 | */ 220 | @Deprecated 221 | public long getResponses5xx() { 222 | return report.getResponses5xx(); 223 | } 224 | 225 | /** 226 | * @return the number of failures 227 | * @deprecated use {@link Report#getFailures()} instead 228 | */ 229 | @Deprecated 230 | public long getFailures() { 231 | return report.getFailures(); 232 | } 233 | 234 | /** 235 | *

Returns the average CPU load during recording.

236 | *

This is the CPU time for the load generator JVM, across all cores, divided by the recording duration.

237 | *

This number is typically greater than 100 because it takes into account all cores.

238 | *

For example, a value of {@code 456.789} on a 12 core machine means that during the recording 239 | * about 4.5 cores out of 12 were at 100% utilization. 240 | * Equivalently, it means that each core was at {@code 456.789/12}, about 38%, utilization.

241 | * 242 | * @return the average CPU load during recording 243 | * @deprecated use {@link Report#getAverageCPUPercent()} instead 244 | */ 245 | @Deprecated 246 | public double getAverageCPUPercent() { 247 | return report.getAverageCPUPercent(); 248 | } 249 | 250 | @Override 251 | public void onBegin(LoadGenerator generator) { 252 | report.beginInstant = Instant.now(); 253 | report.beginTime = System.nanoTime(); 254 | } 255 | 256 | @Override 257 | public void onReady(LoadGenerator generator) { 258 | report.readyTime = System.nanoTime(); 259 | report.readyCPUTime = getProcessCPUTime(); 260 | } 261 | 262 | @Override 263 | public void onEnd(LoadGenerator generator) { 264 | report.endTime = System.nanoTime(); 265 | } 266 | 267 | @Override 268 | public void onComplete(LoadGenerator generator) { 269 | report.completeTime = System.nanoTime(); 270 | report.completeCPUTime = getProcessCPUTime(); 271 | // The histogram is reset every time getIntervalHistogram() is called. 272 | report.histogram = recorder.getIntervalHistogram(); 273 | report.sentBytes = connectionStats.getSentBytes(); 274 | report.recvBytes = connectionStats.getReceivedBytes(); 275 | reportPromise.complete(report); 276 | } 277 | 278 | @Override 279 | public void onResourceNode(Resource.Info info) { 280 | if (info.getFailure() == null) { 281 | recordResponseGroup(info); 282 | long responseTime = info.getResponseTime() - info.getRequestTime(); 283 | recorder.recordValue(responseTime); 284 | report.responseContent.add(info.getContentLength()); 285 | } else { 286 | report.failures.increment(); 287 | } 288 | } 289 | 290 | @Override 291 | public void onOpened(Connection connection) { 292 | connectionStats.onOpened(connection); 293 | } 294 | 295 | @Override 296 | public void onClosed(Connection connection) { 297 | connectionStats.onClosed(connection); 298 | } 299 | 300 | private void recordResponseGroup(Resource.Info info) { 301 | switch (info.getStatus() / 100) { 302 | case 1: 303 | report.responses1xx.increment(); 304 | break; 305 | case 2: 306 | report.responses2xx.increment(); 307 | break; 308 | case 3: 309 | report.responses3xx.increment(); 310 | break; 311 | case 4: 312 | report.responses4xx.increment(); 313 | break; 314 | case 5: 315 | report.responses5xx.increment(); 316 | break; 317 | default: 318 | break; 319 | } 320 | } 321 | 322 | private static long getProcessCPUTime() { 323 | try { 324 | MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); 325 | ObjectName osObjectName = new ObjectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME); 326 | return (Long)mbeanServer.getAttribute(osObjectName, "ProcessCpuTime"); 327 | } catch (Throwable x) { 328 | return 0; 329 | } 330 | } 331 | 332 | public static class Report implements JSON.Convertible { 333 | private final LongAdder responses1xx = new LongAdder(); 334 | private final LongAdder responses2xx = new LongAdder(); 335 | private final LongAdder responses3xx = new LongAdder(); 336 | private final LongAdder responses4xx = new LongAdder(); 337 | private final LongAdder responses5xx = new LongAdder(); 338 | private final LongAdder responseContent = new LongAdder(); 339 | private final LongAdder failures = new LongAdder(); 340 | private volatile Histogram histogram; 341 | private volatile Instant beginInstant; 342 | private volatile long beginTime; 343 | private volatile long readyTime; 344 | private volatile long endTime; 345 | private volatile long completeTime; 346 | private volatile long readyCPUTime; 347 | private volatile long completeCPUTime; 348 | private volatile long sentBytes; 349 | private volatile long recvBytes; 350 | 351 | /** 352 | * @return the Instant of the load generation {@link LoadGenerator.BeginListener begin event} 353 | */ 354 | public Instant getBeginInstant() { 355 | return beginInstant; 356 | } 357 | 358 | /** 359 | * @return the Instant of the load generation {@link LoadGenerator.CompleteListener complete event} 360 | */ 361 | public Instant getCompleteInstant() { 362 | return beginInstant.plusNanos(completeTime - beginTime); 363 | } 364 | 365 | /** 366 | *

Returns the duration of the load generation recording.

367 | *

The recording starts at the {@link LoadGenerator.ReadyListener ready event} 368 | * and therefore excludes warmup.

369 | * 370 | * @return the Duration of the load generation recording 371 | */ 372 | public Duration getRecordingDuration() { 373 | return Duration.ofNanos(getRecordingNanos()); 374 | } 375 | 376 | /** 377 | *

Returns the response time histogram.

378 | *

The response time is the time between a request is queued to be sent, 379 | * to the time the response is fully received, in nanoseconds.

380 | *

Warmup requests are not recorded.

381 | * 382 | * @return the response time histogram 383 | */ 384 | public Histogram getResponseTimeHistogram() { 385 | return histogram; 386 | } 387 | 388 | /** 389 | * @return the request rate, in requests/s 390 | */ 391 | public double getRequestRate() { 392 | return nanoRate(getResponseTimeHistogram().getTotalCount(), endTime - readyTime); 393 | } 394 | 395 | /** 396 | * @return the response rate in responses/s 397 | */ 398 | public double getResponseRate() { 399 | return nanoRate(getResponseTimeHistogram().getTotalCount(), getRecordingNanos()); 400 | } 401 | 402 | /** 403 | * @return the rate of bytes sent, in bytes/s 404 | */ 405 | public double getSentBytesRate() { 406 | return nanoRate(sentBytes, getRecordingNanos()); 407 | } 408 | 409 | /** 410 | * @return the rate of bytes received, in bytes/s 411 | */ 412 | public double getReceivedBytesRate() { 413 | return nanoRate(recvBytes, getRecordingNanos()); 414 | } 415 | 416 | /** 417 | * @return the number of HTTP 1xx responses 418 | */ 419 | public long getResponses1xx() { 420 | return responses1xx.longValue(); 421 | } 422 | 423 | /** 424 | * @return the number of HTTP 2xx responses 425 | */ 426 | public long getResponses2xx() { 427 | return responses2xx.longValue(); 428 | } 429 | 430 | /** 431 | * @return the number of HTTP 3xx responses 432 | */ 433 | public long getResponses3xx() { 434 | return responses3xx.longValue(); 435 | } 436 | 437 | /** 438 | * @return the number of HTTP 4xx responses 439 | */ 440 | public long getResponses4xx() { 441 | return responses4xx.longValue(); 442 | } 443 | 444 | /** 445 | * @return the number of HTTP 5xx responses 446 | */ 447 | public long getResponses5xx() { 448 | return responses5xx.longValue(); 449 | } 450 | 451 | /** 452 | * @return the number of failures 453 | */ 454 | public long getFailures() { 455 | return failures.longValue(); 456 | } 457 | 458 | /** 459 | *

Returns the average CPU load during recording.

460 | *

This is the CPU time for the load generator JVM, across all cores, divided by the recording duration.

461 | *

This number is typically greater than 100 because it takes into account all cores.

462 | *

For example, a value of {@code 456.789} on a 12 core machine means that during the recording 463 | * about 4.5 cores out of 12 were at 100% utilization. 464 | * Equivalently, it means that each core was at {@code 456.789/12}, about 38%, utilization.

465 | * 466 | * @return the average CPU load during recording 467 | */ 468 | public double getAverageCPUPercent() { 469 | long elapsedTime = getRecordingNanos(); 470 | return elapsedTime == 0 ? 0 : 100D * (completeCPUTime - readyCPUTime) / elapsedTime; 471 | } 472 | 473 | private long getRecordingNanos() { 474 | return completeTime - readyTime; 475 | } 476 | 477 | private static double nanoRate(double dividend, long divisor) { 478 | return divisor == 0 ? 0 : (dividend * TimeUnit.SECONDS.toNanos(1)) / divisor; 479 | } 480 | 481 | @Override 482 | public void toJSON(JSON.Output out) { 483 | out.add("beginInstant", getBeginInstant().atZone(ZoneOffset.UTC).toString()); 484 | out.add("completeInstant", getCompleteInstant().atZone(ZoneOffset.UTC).toString()); 485 | out.add("recordingDuration", getRecordingDuration().toMillis()); 486 | out.add("availableProcessors", Runtime.getRuntime().availableProcessors()); 487 | out.add("averageCPUPercent", getAverageCPUPercent()); 488 | out.add("requestRate", getRequestRate()); 489 | out.add("responseRate", getResponseRate()); 490 | out.add("sentBytesRate", getSentBytesRate()); 491 | out.add("receivedBytesRate", getReceivedBytesRate()); 492 | out.add("failures", getFailures()); 493 | out.add("1xx", getResponses1xx()); 494 | out.add("2xx", getResponses2xx()); 495 | out.add("3xx", getResponses3xx()); 496 | out.add("4xx", getResponses4xx()); 497 | out.add("5xx", getResponses5xx()); 498 | ByteArrayOutputStream histogramOutput = new ByteArrayOutputStream(); 499 | HistogramLogWriter hw = new HistogramLogWriter(histogramOutput); 500 | hw.outputIntervalHistogram(getResponseTimeHistogram()); 501 | hw.close(); 502 | out.add("histogram", histogramOutput.toString(StandardCharsets.UTF_8)); 503 | } 504 | 505 | @Override 506 | public void fromJSON(Map map) { 507 | throw new UnsupportedOperationException(); 508 | } 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.mortbay.jetty.loadgenerator 5 | jetty-load-generator 6 | 4.0.23-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | jetty-load-generator-starter 11 | jar 12 | Jetty :: Load Generator :: Starter 13 | 14 | 15 | ${project.groupId}.load.generator.starter 16 | 17 | 18 | 19 | 20 | org.mortbay.jetty.loadgenerator 21 | jetty-load-generator-client 22 | ${project.version} 23 | 24 | 25 | org.mortbay.jetty.loadgenerator 26 | jetty-load-generator-listeners 27 | ${project.version} 28 | 29 | 30 | org.apache.groovy 31 | groovy 32 | 33 | 34 | com.beust 35 | jcommander 36 | 37 | 38 | org.eclipse.jetty 39 | jetty-client 40 | 41 | 42 | org.eclipse.jetty 43 | jetty-jmx 44 | 45 | 46 | org.eclipse.jetty 47 | jetty-xml 48 | 49 | 50 | org.eclipse.jetty.toolchain 51 | jetty-perf-helper 52 | 53 | 54 | org.slf4j 55 | slf4j-api 56 | 57 | 58 | 59 | junit 60 | junit 61 | test 62 | 63 | 64 | org.eclipse.jetty 65 | jetty-server 66 | test 67 | 68 | 69 | org.eclipse.jetty 70 | jetty-slf4j-impl 71 | test 72 | 73 | 74 | 75 | 76 | 77 | 78 | maven-shade-plugin 79 | 80 | 81 | package 82 | 83 | shade 84 | 85 | 86 | false 87 | true 88 | uber 89 | 90 | 91 | org.mortbay.jetty.load.generator.starter.LoadGeneratorStarter 92 | 93 | 94 | 95 | 96 | 97 | *:* 98 | 99 | about.html 100 | META-INF/INDEX.LIST 101 | META-INF/MANIFEST.MF 102 | META-INF/NOTICE.txt 103 | META-INF/*.SF 104 | META-INF/*.DSA 105 | META-INF/*.RSA 106 | **/module-info.class 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/src/main/java/org/mortbay/jetty/load/generator/starter/LoadGeneratorStarter.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator.starter; 15 | 16 | import java.io.OutputStream; 17 | import java.lang.management.ManagementFactory; 18 | import java.nio.charset.StandardCharsets; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.time.Instant; 22 | import java.time.ZoneId; 23 | import java.time.format.DateTimeFormatter; 24 | import java.util.Arrays; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.TimeUnit; 29 | import java.util.function.Supplier; 30 | 31 | import com.beust.jcommander.JCommander; 32 | import org.HdrHistogram.Histogram; 33 | import org.eclipse.jetty.jmx.MBeanContainer; 34 | import org.eclipse.jetty.toolchain.perf.HistogramSnapshot; 35 | import org.eclipse.jetty.util.ajax.JSON; 36 | import org.mortbay.jetty.load.generator.LoadGenerator; 37 | import org.mortbay.jetty.load.generator.listeners.ReportListener; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | 41 | /** 42 | *

A convenience class to run the load generator from the command line.

43 | *
 44 |  * java -jar jetty-load-generator-starter.jar --help
 45 |  * 
46 | */ 47 | public class LoadGeneratorStarter { 48 | private static final Logger LOGGER = LoggerFactory.getLogger(LoadGeneratorStarter.class); 49 | 50 | public static void main(String[] args) throws Exception { 51 | LoadGeneratorStarterArgs starterArgs = parse(args); 52 | if (starterArgs == null) { 53 | return; 54 | } 55 | LoadGenerator.Builder builder = configure(starterArgs); 56 | ReportListener listener = new ReportListener(); 57 | LoadGenerator generator = builder 58 | .listener(listener) 59 | .resourceListener(listener) 60 | .build(); 61 | generator.addBean(listener); 62 | if (starterArgs.isJMX()) { 63 | MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); 64 | generator.addBean(mbeanContainer); 65 | } 66 | run(generator); 67 | ReportListener.Report report = listener.whenComplete().join(); 68 | if (starterArgs.isDisplayStats()) { 69 | displayReport(generator.getConfig(), report); 70 | } 71 | String statsFile = starterArgs.getStatsFile(); 72 | if (statsFile != null) { 73 | try (OutputStream output = Files.newOutputStream(Path.of(statsFile))) { 74 | Map map = new HashMap<>(); 75 | map.put("config", generator.getConfig()); 76 | map.put("report", report); 77 | JSON json = new JSON(); 78 | output.write(json.toJSON(map).getBytes(StandardCharsets.UTF_8)); 79 | LOGGER.info("load generator report saved to: {}", statsFile); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | *

Parses the program arguments, returning the default arguments holder.

86 | * 87 | * @param args the program arguments to parse 88 | * @return the default arguments holder 89 | * @see #parse(String[], Supplier) 90 | */ 91 | public static LoadGeneratorStarterArgs parse(String[] args) { 92 | return parse(args, LoadGeneratorStarterArgs::new); 93 | } 94 | 95 | /** 96 | *

Parses the program arguments, returning a custom arguments holder.

97 | * 98 | * @param args the program arguments to parse 99 | * @param argsSupplier the supplier for the custom arguments holder 100 | * @param the custom argument holder type 101 | * @return the custom arguments holder 102 | */ 103 | public static T parse(String[] args, Supplier argsSupplier) { 104 | T starterArgs = argsSupplier.get(); 105 | JCommander jCommander = new JCommander(starterArgs); 106 | jCommander.setAcceptUnknownOptions(true); 107 | jCommander.parse(args); 108 | if (starterArgs.isHelp()) { 109 | jCommander.usage(); 110 | return null; 111 | } 112 | return starterArgs; 113 | } 114 | 115 | /** 116 | *

Creates a new LoadGenerator.Builder, configuring it from the given arguments holder.

117 | * 118 | * @param starterArgs the arguments holder 119 | * @return a new LoadGenerator.Builder 120 | */ 121 | public static LoadGenerator.Builder configure(LoadGeneratorStarterArgs starterArgs) { 122 | try { 123 | LoadGenerator.Builder builder = LoadGenerator.builder(); 124 | return builder 125 | .threads(starterArgs.getThreads()) 126 | .warmupIterationsPerThread(starterArgs.getWarmupIterations()) 127 | .iterationsPerThread(starterArgs.getIterations()) 128 | .runFor(starterArgs.getRunningTime(), starterArgs.getRunningTimeUnit()) 129 | .usersPerThread(starterArgs.getUsersPerThread()) 130 | .channelsPerUser(starterArgs.getChannelsPerUser()) 131 | .resource(starterArgs.getResource(builder)) 132 | .resourceRate(starterArgs.getResourceRate()) 133 | .rateRampUpPeriod(starterArgs.getRateRampUpPeriod()) 134 | .scheme(starterArgs.getScheme()) 135 | .host(starterArgs.getHost()) 136 | .port(starterArgs.getPort()) 137 | .httpClientTransportBuilder(starterArgs.getHttpClientTransportBuilder()) 138 | .sslContextFactory(starterArgs.getSslContextFactory()) 139 | .maxRequestsQueued(starterArgs.getMaxRequestsQueued()) 140 | .connectBlocking(starterArgs.isConnectBlocking()) 141 | .connectTimeout(starterArgs.getConnectTimeout()) 142 | .idleTimeout(starterArgs.getIdleTimeout()) 143 | .executor(starterArgs.getExecutor()) 144 | .scheduler(starterArgs.getScheduler()); 145 | } catch (Exception x) { 146 | throw new RuntimeException(x); 147 | } 148 | } 149 | 150 | /** 151 | *

Runs a load generation, waiting indefinitely for completion.

152 | * 153 | * @param loadGenerator the load generator to run 154 | */ 155 | public static void run(LoadGenerator loadGenerator) { 156 | LOGGER.info("load generator config: {}", loadGenerator.getConfig()); 157 | LOGGER.info("load generation begin"); 158 | CompletableFuture cf = loadGenerator.begin(); 159 | cf.whenComplete((x, f) -> { 160 | if (f == null) { 161 | LOGGER.info("load generation complete"); 162 | } else { 163 | LOGGER.info("load generation failure", f); 164 | } 165 | }).join(); 166 | } 167 | 168 | private static void displayReport(LoadGenerator.Config config, ReportListener.Report report) { 169 | Histogram responseTimes = report.getResponseTimeHistogram(); 170 | HistogramSnapshot snapshot = new HistogramSnapshot(responseTimes, 20, "response times", "ms", TimeUnit.NANOSECONDS::toMillis); 171 | DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault()); 172 | LOGGER.info(""); 173 | LOGGER.info("----------------------------------------------------"); 174 | LOGGER.info("------------- Load Generator Report --------------"); 175 | LOGGER.info("----------------------------------------------------"); 176 | LOGGER.info("{}://{}:{} over {}", config.getScheme(), config.getHost(), config.getPort(), config.getHttpClientTransportBuilder().getType()); 177 | int resourceCount = config.getResource().descendantCount(); 178 | LOGGER.info("resource tree : {} resource(s)", resourceCount); 179 | Instant beginInstant = report.getBeginInstant(); 180 | LOGGER.info("begin date time : {}", dateTimeFormatter.format(beginInstant)); 181 | Instant completeInstant = report.getCompleteInstant(); 182 | LOGGER.info("complete date time: {}", dateTimeFormatter.format(completeInstant)); 183 | LOGGER.info("recording time : {} s", String.format("%.3f", (double)report.getRecordingDuration().toMillis() / 1000)); 184 | LOGGER.info("average cpu load : {}/{}", String.format("%.3f", report.getAverageCPUPercent()), Runtime.getRuntime().availableProcessors() * 100); 185 | LOGGER.info(""); 186 | if (responseTimes.getTotalCount() > 0) { 187 | LOGGER.info("histogram:"); 188 | Arrays.stream(snapshot.toString().split(System.lineSeparator())).forEach(line -> LOGGER.info("{}", line)); 189 | LOGGER.info(""); 190 | } 191 | double resourceRate = config.getResourceRate(); 192 | LOGGER.info("nominal resource rate (resources/s): {}", String.format("%.3f", resourceRate)); 193 | LOGGER.info("nominal request rate (requests/s) : {}", String.format("%.3f", resourceRate * resourceCount)); 194 | LOGGER.info("request rate (requests/s) : {}", String.format("%.3f", report.getRequestRate())); 195 | LOGGER.info("response rate (responses/s) : {}", String.format("%.3f", report.getResponseRate())); 196 | LOGGER.info("send rate (bytes/s) : {}", String.format("%.3f", report.getSentBytesRate())); 197 | LOGGER.info("receive rate (bytes/s) : {}", String.format("%.3f", report.getReceivedBytesRate())); 198 | LOGGER.info("failures : {}", report.getFailures()); 199 | LOGGER.info("response 1xx group: {}", report.getResponses1xx()); 200 | LOGGER.info("response 2xx group: {}", report.getResponses2xx()); 201 | LOGGER.info("response 3xx group: {}", report.getResponses3xx()); 202 | LOGGER.info("response 4xx group: {}", report.getResponses4xx()); 203 | LOGGER.info("response 5xx group: {}", report.getResponses5xx()); 204 | LOGGER.info("----------------------------------------------------"); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/src/main/java/org/mortbay/jetty/load/generator/starter/LoadGeneratorStarterArgs.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator.starter; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.IOException; 18 | import java.io.Reader; 19 | import java.nio.charset.StandardCharsets; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | import java.util.HashMap; 24 | import java.util.Locale; 25 | import java.util.Map; 26 | import java.util.concurrent.Executor; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import com.beust.jcommander.Parameter; 30 | import groovy.lang.Binding; 31 | import groovy.lang.GroovyShell; 32 | import org.codehaus.groovy.control.CompilerConfiguration; 33 | import org.eclipse.jetty.util.ajax.JSON; 34 | import org.eclipse.jetty.util.ssl.SslContextFactory; 35 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 36 | import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; 37 | import org.eclipse.jetty.util.thread.Scheduler; 38 | import org.eclipse.jetty.xml.XmlConfiguration; 39 | import org.mortbay.jetty.load.generator.HTTP1ClientTransportBuilder; 40 | import org.mortbay.jetty.load.generator.HTTP2ClientTransportBuilder; 41 | import org.mortbay.jetty.load.generator.HTTPClientTransportBuilder; 42 | import org.mortbay.jetty.load.generator.LoadGenerator; 43 | import org.mortbay.jetty.load.generator.Resource; 44 | 45 | public class LoadGeneratorStarterArgs { 46 | @Parameter(names = {"--threads", "-t"}, description = "Number of sender threads") 47 | private int threads = 1; 48 | 49 | @Parameter(names = {"--warmup-iterations", "-wi"}, description = "Number of warmup iterations per sender thread") 50 | private int warmupIterations; 51 | 52 | @Parameter(names = {"--iterations", "-i"}, description = "Number of iterations per sender thread") 53 | private int iterations = 1; 54 | 55 | @Parameter(names = {"--running-time", "-rt"}, description = "Load generation running time") 56 | private long runningTime; 57 | 58 | @Parameter(names = {"--running-time-unit", "-rtu"}, description = "Load generation running time unit (h/m/s/ms)") 59 | private String runningTimeUnit = "s"; 60 | 61 | @Parameter(names = {"--users-per-thread", "-upt"}, description = "Number of users/connections per sender thread") 62 | private int usersPerThread = 1; 63 | 64 | @Parameter(names = {"--channels-per-user", "-cpu"}, description = "Number of concurrent connections/streams per user") 65 | private int channelsPerUser = 128; 66 | 67 | @Parameter(names = {"--resource-xml-path", "-rxp"}, description = "Path to resource XML file") 68 | private String resourceXMLPath; 69 | 70 | @Parameter(names = {"--resource-json-path", "-rjp"}, description = "Path to resource JSON file") 71 | private String resourceJSONPath; 72 | 73 | @Parameter(names = {"--resource-groovy-path", "-rgp"}, description = "Path to resource Groovy file") 74 | private String resourceGroovyPath; 75 | 76 | @Parameter(names = {"--resource-rate", "-rr"}, description = "Total resource tree rate, per second; use 0 for max request rate") 77 | private int resourceRate = 1; 78 | 79 | @Parameter(names = {"--rate-ramp-up", "-rru"}, description = "Rate ramp-up period, in seconds") 80 | private long rateRampUpPeriod = 0; 81 | 82 | @Parameter(names = {"--scheme", "-s"}, description = "Target scheme (http/https)") 83 | private String scheme = "http"; 84 | 85 | @Parameter(names = {"--host", "-h"}, description = "Target host") 86 | private String host = "localhost"; 87 | 88 | @Parameter(names = {"--port", "-p"}, description = "Target port") 89 | private int port = 8080; 90 | 91 | @Parameter(names = {"--transport", "-tr"}, description = "Transport (http, https, h2, h2c)") 92 | private String transport = "http"; 93 | 94 | @Parameter(names = {"--selectors"}, description = "Number of NIO selectors") 95 | private int selectors = 1; 96 | 97 | @Parameter(names = {"--max-requests-queued", "-mrq"}, description = "Maximum number of queued requests") 98 | private int maxRequestsQueued = 1024; 99 | 100 | @Parameter(names = {"--connect-blocking", "-cb"}, description = "Whether TCP connect is blocking") 101 | private boolean connectBlocking = true; 102 | 103 | @Parameter(names = {"--connect-timeout", "-ct"}, description = "TCP connect timeout, in milliseconds") 104 | private long connectTimeout = 5000; 105 | 106 | @Parameter(names = {"--idle-timeout", "-it"}, description = "TCP connection idle timeout, in milliseconds") 107 | private long idleTimeout = 15000; 108 | 109 | @Parameter(names = {"--stats-file", "-sf"}, description = "Statistics output file path in JSON format") 110 | private String statsFile; 111 | 112 | @Parameter(names = {"--display-stats", "-ds"}, description = "Whether to display statistics in the terminal") 113 | private boolean displayStats; 114 | 115 | @Parameter(names = {"--jmx"}, description = "Exports load generator components to the JVM platform MBeanServer as MBeans") 116 | private boolean jmx; 117 | 118 | @Parameter(names = {"--executor-max-threads"}, description = "Max number of executor threads") 119 | private int executorMaxThreads = 256; 120 | 121 | @Parameter(names = {"--scheduler-max-threads"}, description = "Max number of scheduler threads") 122 | private int schedulerMaxThreads = 1; 123 | 124 | @Parameter(names = {"--help"}, description = "Displays usage") 125 | private boolean help; 126 | 127 | // Getters and setters are needed by JCommander. 128 | 129 | public int getThreads() { 130 | return threads; 131 | } 132 | 133 | public void setThreads(int threads) { 134 | this.threads = threads; 135 | } 136 | 137 | public int getWarmupIterations() { 138 | return warmupIterations; 139 | } 140 | 141 | public void setWarmupIterations(int warmupIterations) { 142 | this.warmupIterations = warmupIterations; 143 | } 144 | 145 | public int getIterations() { 146 | return iterations; 147 | } 148 | 149 | public void setIterations(int iterations) { 150 | this.iterations = iterations; 151 | } 152 | 153 | public long getRunningTime() { 154 | return runningTime; 155 | } 156 | 157 | public void setRunningTime(long runningTime) { 158 | this.runningTime = runningTime; 159 | } 160 | 161 | public TimeUnit getRunningTimeUnit() { 162 | switch (this.runningTimeUnit) { 163 | case "m": 164 | case "minutes": 165 | case "MINUTES": 166 | return TimeUnit.MINUTES; 167 | case "h": 168 | case "hours": 169 | case "HOURS": 170 | return TimeUnit.HOURS; 171 | case "s": 172 | case "seconds": 173 | case "SECONDS": 174 | return TimeUnit.SECONDS; 175 | case "ms": 176 | case "milliseconds": 177 | case "MILLISECONDS": 178 | return TimeUnit.MILLISECONDS; 179 | default: 180 | throw new IllegalArgumentException(runningTimeUnit + " is not recognized"); 181 | } 182 | } 183 | 184 | public void setRunningTimeUnit(String runningTimeUnit) { 185 | this.runningTimeUnit = runningTimeUnit; 186 | } 187 | 188 | public int getUsersPerThread() { 189 | return usersPerThread; 190 | } 191 | 192 | public void setUsersPerThread(int usersPerThread) { 193 | this.usersPerThread = usersPerThread; 194 | } 195 | 196 | public int getChannelsPerUser() { 197 | return channelsPerUser; 198 | } 199 | 200 | public void setChannelsPerUser(int channelsPerUser) { 201 | this.channelsPerUser = channelsPerUser; 202 | } 203 | 204 | public String getResourceXMLPath() { 205 | return resourceXMLPath; 206 | } 207 | 208 | public void setResourceXMLPath(String resourceXMLPath) { 209 | this.resourceXMLPath = resourceXMLPath; 210 | } 211 | 212 | public String getResourceJSONPath() { 213 | return resourceJSONPath; 214 | } 215 | 216 | public void setResourceJSONPath(String resourceJSONPath) { 217 | this.resourceJSONPath = resourceJSONPath; 218 | } 219 | 220 | public String getResourceGroovyPath() { 221 | return resourceGroovyPath; 222 | } 223 | 224 | public void setResourceGroovyPath(String resourceGroovyPath) { 225 | this.resourceGroovyPath = resourceGroovyPath; 226 | } 227 | 228 | public int getResourceRate() { 229 | return resourceRate; 230 | } 231 | 232 | public void setResourceRate(int resourceRate) { 233 | this.resourceRate = resourceRate; 234 | } 235 | 236 | public long getRateRampUpPeriod() { 237 | return rateRampUpPeriod; 238 | } 239 | 240 | public void setRateRampUpPeriod(long rateRampUpPeriod) { 241 | this.rateRampUpPeriod = rateRampUpPeriod; 242 | } 243 | 244 | public String getScheme() { 245 | return scheme; 246 | } 247 | 248 | public void setScheme(String scheme) { 249 | this.scheme = scheme; 250 | } 251 | 252 | public String getHost() { 253 | return host; 254 | } 255 | 256 | public void setHost(String host) { 257 | this.host = host; 258 | } 259 | 260 | public int getPort() { 261 | return port; 262 | } 263 | 264 | public void setPort(int port) { 265 | this.port = port; 266 | } 267 | 268 | public String getTransport() { 269 | return transport; 270 | } 271 | 272 | public void setTransport(String transport) { 273 | transport = transport.toLowerCase(Locale.ENGLISH); 274 | switch (transport) { 275 | case "http": 276 | case "https": 277 | case "h2c": 278 | case "h2": 279 | this.transport = transport; 280 | break; 281 | default: 282 | throw new IllegalArgumentException("unsupported transport " + transport); 283 | } 284 | } 285 | 286 | public int getSelectors() { 287 | return selectors; 288 | } 289 | 290 | public void setSelectors(int selectors) { 291 | this.selectors = selectors; 292 | } 293 | 294 | public int getMaxRequestsQueued() { 295 | return maxRequestsQueued; 296 | } 297 | 298 | public void setMaxRequestsQueued(int maxRequestsQueued) { 299 | this.maxRequestsQueued = maxRequestsQueued; 300 | } 301 | 302 | public boolean isConnectBlocking() { 303 | return connectBlocking; 304 | } 305 | 306 | public void setConnectBlocking(boolean connectBlocking) { 307 | this.connectBlocking = connectBlocking; 308 | } 309 | 310 | public long getConnectTimeout() { 311 | return connectTimeout; 312 | } 313 | 314 | public void setConnectTimeout(long connectTimeout) { 315 | this.connectTimeout = connectTimeout; 316 | } 317 | 318 | public long getIdleTimeout() { 319 | return idleTimeout; 320 | } 321 | 322 | public void setIdleTimeout(long idleTimeout) { 323 | this.idleTimeout = idleTimeout; 324 | } 325 | 326 | public String getStatsFile() { 327 | return statsFile; 328 | } 329 | 330 | public void setStatsFile(String statsFile) { 331 | this.statsFile = statsFile; 332 | } 333 | 334 | public boolean isDisplayStats() { 335 | return displayStats; 336 | } 337 | 338 | public void setDisplayStats(boolean displayStats) { 339 | this.displayStats = displayStats; 340 | } 341 | 342 | public boolean isJMX() { 343 | return jmx; 344 | } 345 | 346 | public void setJMX(boolean jmx) { 347 | this.jmx = jmx; 348 | } 349 | 350 | public int getExecutorMaxThreads() { 351 | return executorMaxThreads; 352 | } 353 | 354 | public void setExecutorMaxThreads(int executorMaxThreads) { 355 | this.executorMaxThreads = executorMaxThreads; 356 | } 357 | 358 | public int getSchedulerMaxThreads() { 359 | return schedulerMaxThreads; 360 | } 361 | 362 | public void setSchedulerMaxThreads(int schedulerMaxThreads) { 363 | this.schedulerMaxThreads = schedulerMaxThreads; 364 | } 365 | 366 | public boolean isHelp() { 367 | return help; 368 | } 369 | 370 | public void setHelp(boolean help) { 371 | this.help = help; 372 | } 373 | 374 | // APIs used by LoadGeneratorStarter. 375 | 376 | public SslContextFactory.Client getSslContextFactory() { 377 | return new SslContextFactory.Client(true); 378 | } 379 | 380 | public HTTPClientTransportBuilder getHttpClientTransportBuilder() { 381 | String transport = getTransport(); 382 | switch (transport) { 383 | case "http": 384 | case "https": { 385 | return new HTTP1ClientTransportBuilder().selectors(getSelectors()); 386 | } 387 | case "h2c": 388 | case "h2": { 389 | return new HTTP2ClientTransportBuilder().selectors(getSelectors()); 390 | } 391 | default: { 392 | throw new IllegalArgumentException("unsupported transport " + transport); 393 | } 394 | } 395 | } 396 | 397 | Resource getResource(LoadGenerator.Builder builder) throws Exception { 398 | String jsonPath = getResourceJSONPath(); 399 | if (jsonPath != null) { 400 | Path path = Paths.get(jsonPath); 401 | return evaluateJSON(path); 402 | } 403 | String xmlPath = getResourceXMLPath(); 404 | if (xmlPath != null) { 405 | Path path = Paths.get(xmlPath); 406 | return (Resource)new XmlConfiguration(org.eclipse.jetty.util.resource.ResourceFactory.root().newResource(path)).configure(); 407 | } 408 | String groovyPath = getResourceGroovyPath(); 409 | if (groovyPath != null) { 410 | Path path = Paths.get(groovyPath); 411 | try (Reader reader = Files.newBufferedReader(path)) { 412 | Map context = new HashMap<>(); 413 | context.put("loadGeneratorBuilder", builder); 414 | return evaluateGroovy(reader, context); 415 | } 416 | } 417 | return new Resource("/"); 418 | } 419 | 420 | static Resource evaluateJSON(Path profilePath) throws IOException { 421 | try (BufferedReader reader = Files.newBufferedReader(profilePath, StandardCharsets.UTF_8)) { 422 | return evaluateJSON(reader); 423 | } 424 | } 425 | 426 | static Resource evaluateJSON(Reader reader) { 427 | JSON json = new JSON(); 428 | Resource resource = new Resource(); 429 | @SuppressWarnings("unchecked") 430 | Map map = (Map)json.parse(new JSON.ReaderSource(reader)); 431 | resource.fromJSON(map); 432 | return resource; 433 | } 434 | 435 | static Resource evaluateGroovy(Reader script, Map context) { 436 | CompilerConfiguration config = new CompilerConfiguration(CompilerConfiguration.DEFAULT); 437 | config.setDebug(true); 438 | config.setVerbose(true); 439 | Binding binding = new Binding(context); 440 | GroovyShell interpreter = new GroovyShell(binding, config); 441 | return (Resource)interpreter.evaluate(script); 442 | } 443 | 444 | Executor getExecutor() { 445 | QueuedThreadPool executor = new QueuedThreadPool(getExecutorMaxThreads()); 446 | executor.setName("load-generator-executor"); 447 | return executor; 448 | } 449 | 450 | Scheduler getScheduler() { 451 | return new ScheduledExecutorScheduler("load-generator-scheduler", false, getSchedulerMaxThreads()); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/src/test/java/org/mortbay/jetty/load/generator/starter/LoadGeneratorStarterTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // ======================================================================== 3 | // Copyright (c) 2016-2022 Mort Bay Consulting Pty Ltd and others. 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License v. 2.0 which is available at 7 | // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // ======================================================================== 12 | // 13 | 14 | package org.mortbay.jetty.load.generator.starter; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.BufferedWriter; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.InputStream; 20 | import java.io.Reader; 21 | import java.io.StringReader; 22 | import java.nio.charset.StandardCharsets; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.nio.file.Paths; 26 | import java.util.List; 27 | import java.util.Locale; 28 | import java.util.Map; 29 | import java.util.concurrent.atomic.AtomicInteger; 30 | 31 | import org.HdrHistogram.EncodableHistogram; 32 | import org.HdrHistogram.HistogramLogReader; 33 | import org.eclipse.jetty.client.Request; 34 | import org.eclipse.jetty.io.Content; 35 | import org.eclipse.jetty.server.Handler; 36 | import org.eclipse.jetty.server.HttpConfiguration; 37 | import org.eclipse.jetty.server.HttpConnectionFactory; 38 | import org.eclipse.jetty.server.Response; 39 | import org.eclipse.jetty.server.Server; 40 | import org.eclipse.jetty.server.ServerConnector; 41 | import org.eclipse.jetty.server.handler.StatisticsHandler; 42 | import org.eclipse.jetty.util.Callback; 43 | import org.eclipse.jetty.util.Fields; 44 | import org.eclipse.jetty.util.ajax.JSON; 45 | import org.eclipse.jetty.util.component.LifeCycle; 46 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 47 | import org.junit.After; 48 | import org.junit.Assert; 49 | import org.junit.Before; 50 | import org.junit.Ignore; 51 | import org.junit.Test; 52 | import org.mortbay.jetty.load.generator.LoadGenerator; 53 | import org.mortbay.jetty.load.generator.Resource; 54 | import org.slf4j.Logger; 55 | import org.slf4j.LoggerFactory; 56 | 57 | public class LoadGeneratorStarterTest { 58 | private static final Logger LOGGER = LoggerFactory.getLogger(LoadGeneratorStarterTest.class); 59 | 60 | private Server server; 61 | private ServerConnector connector; 62 | private TestHandler testHandler; 63 | 64 | @Before 65 | public void startJetty() throws Exception { 66 | QueuedThreadPool serverThreads = new QueuedThreadPool(); 67 | serverThreads.setName("server"); 68 | server = new Server(serverThreads); 69 | connector = new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration())); 70 | server.addConnector(connector); 71 | testHandler = new TestHandler(connector); 72 | StatisticsHandler statisticsHandler = new StatisticsHandler(); 73 | statisticsHandler.setHandler(testHandler); 74 | server.setHandler(statisticsHandler); 75 | server.start(); 76 | } 77 | 78 | @After 79 | public void stopJetty() { 80 | LifeCycle.stop(server); 81 | } 82 | 83 | @Test 84 | public void testSimple() throws Exception { 85 | String[] args = new String[]{ 86 | "--warmup-iterations", 87 | "10", 88 | "-h", 89 | "localhost", 90 | "--port", 91 | Integer.toString(connector.getLocalPort()), 92 | "--running-time", 93 | "10", 94 | "--running-time-unit", 95 | "s", 96 | "--resource-rate", 97 | "3", 98 | "--transport", 99 | "http", 100 | "--users", 101 | "3", 102 | "--resource-groovy-path", 103 | "src/test/resources/tree_resources.groovy" 104 | }; 105 | LoadGeneratorStarter.main(args); 106 | int getNumber = testHandler.getNumber.get(); 107 | LOGGER.debug("received get: {}", getNumber); 108 | Assert.assertTrue("getNumber return: " + getNumber, getNumber > 10); 109 | } 110 | 111 | @Test 112 | @Ignore("see FailFastTest") 113 | public void testFailFast() { 114 | String[] args = new String[]{ 115 | "--warmup-iterations", 116 | "10", 117 | "-h", 118 | "localhost", 119 | "--port", 120 | Integer.toString(connector.getLocalPort()), 121 | "--running-time", 122 | "10", 123 | "--running-time-unit", 124 | "s", 125 | "--resource-rate", 126 | "3", 127 | "--transport", 128 | "http", 129 | "--users", 130 | "1", 131 | "--resource-groovy-path", 132 | "src/test/resources/single_fail_resource.groovy" 133 | }; 134 | LoadGeneratorStarterArgs starterArgs = LoadGeneratorStarter.parse(args); 135 | LoadGenerator.Builder builder = LoadGeneratorStarter.configure(starterArgs); 136 | 137 | AtomicInteger onFailure = new AtomicInteger(0); 138 | AtomicInteger onCommit = new AtomicInteger(0); 139 | Request.Listener requestListener = new Request.Listener() { 140 | @Override 141 | public void onFailure(Request request, Throwable failure) { 142 | LOGGER.info("fail: {}", onFailure.incrementAndGet()); 143 | } 144 | 145 | @Override 146 | public void onCommit(Request request) { 147 | LOGGER.info("onCommit: {}", onCommit.incrementAndGet()); 148 | } 149 | }; 150 | builder.requestListener(requestListener); 151 | 152 | try { 153 | LoadGeneratorStarter.run(builder.build()); 154 | Assert.fail(); 155 | } catch (Exception x) { 156 | // Expected. 157 | } 158 | 159 | int getNumber = testHandler.getNumber.get(); 160 | Assert.assertEquals(5, getNumber); 161 | Assert.assertTrue(onFailure.get() < 10); 162 | } 163 | 164 | @Test 165 | public void testFromGroovyToJSON() throws Exception { 166 | try (Reader reader = Files.newBufferedReader(Paths.get("src/test/resources/tree_resources.groovy"))) { 167 | Resource resource = LoadGeneratorStarterArgs.evaluateGroovy(reader, Map.of()); 168 | Path tmpPath = Files.createTempFile("resources_", ".tmp"); 169 | tmpPath.toFile().deleteOnExit(); 170 | try (BufferedWriter writer = Files.newBufferedWriter(tmpPath, StandardCharsets.UTF_8)) { 171 | JSON json = new JSON(); 172 | writer.write(json.toJSON(resource)); 173 | } 174 | Resource fromJson = LoadGeneratorStarterArgs.evaluateJSON(tmpPath); 175 | Assert.assertEquals(resource.descendantCount(), fromJson.descendantCount()); 176 | } 177 | } 178 | 179 | @Test 180 | public void testCalculateDescendantCount() throws Exception { 181 | try (Reader reader = Files.newBufferedReader(Paths.get("src/test/resources/tree_resources.groovy"))) { 182 | Resource resource = LoadGeneratorStarterArgs.evaluateGroovy(reader, Map.of()); 183 | Assert.assertEquals(17, resource.descendantCount()); 184 | } 185 | } 186 | 187 | @Test 188 | public void testSimplestJSON() { 189 | String path = "/index.html"; 190 | try (StringReader reader = new StringReader("{\"path\":\"" + path + "\"}")) { 191 | Resource resource = LoadGeneratorStarterArgs.evaluateJSON(reader); 192 | Assert.assertEquals(path, resource.getPath()); 193 | } 194 | } 195 | 196 | @Test 197 | public void testFullJSON() { 198 | try (StringReader reader = new StringReader("" + 199 | "{" + 200 | "\"method\":\"POST\"," + 201 | "\"path\":\"/index.html\"," + 202 | "\"requestLength\":1," + 203 | "\"responseLength\":2," + 204 | "\"requestHeaders\":{\"Foo\":[\"Bar\"]}," + 205 | "\"resources\":[{\"path\":\"/styles.css\"}]" + 206 | "}")) { 207 | Resource resource = LoadGeneratorStarterArgs.evaluateJSON(reader); 208 | Assert.assertEquals("POST", resource.getMethod()); 209 | Assert.assertEquals("/index.html", resource.getPath()); 210 | Assert.assertEquals(1, resource.getRequestLength()); 211 | Assert.assertEquals(2, resource.getResponseLength()); 212 | Assert.assertEquals("Bar", resource.getRequestHeaders().get("Foo")); 213 | List children = resource.getResources(); 214 | Assert.assertEquals(1, children.size()); 215 | Assert.assertEquals("/styles.css", children.get(0).getPath()); 216 | } 217 | } 218 | 219 | @Test 220 | public void testStatsFile() throws Exception { 221 | Path statsPath = Files.createTempFile(Path.of("target"), "jlg-stats-", ".json"); 222 | statsPath.toFile().deleteOnExit(); 223 | String[] args = new String[]{ 224 | "--port", 225 | Integer.toString(connector.getLocalPort()), 226 | "--iterations", 227 | "10", 228 | "--resource-rate", 229 | "10", 230 | "--stats-file", 231 | statsPath.toString() 232 | }; 233 | LoadGeneratorStarter.main(args); 234 | 235 | try (BufferedReader reader = Files.newBufferedReader(statsPath, StandardCharsets.UTF_8)) { 236 | JSON json = new JSON(); 237 | @SuppressWarnings("unchecked") 238 | Map map = (Map)json.parse(new JSON.ReaderSource(reader)); 239 | 240 | @SuppressWarnings("unchecked") 241 | Map configMap = (Map)map.get("config"); 242 | LoadGenerator.Config config = new LoadGenerator.Config(); 243 | config.fromJSON(configMap); 244 | Assert.assertEquals(connector.getLocalPort(), config.getPort()); 245 | 246 | @SuppressWarnings("unchecked") 247 | Map reportMap = (Map)map.get("report"); 248 | try (InputStream inputStream = new ByteArrayInputStream(((String)reportMap.get("histogram")).getBytes(StandardCharsets.UTF_8))) { 249 | HistogramLogReader histogramReader = new HistogramLogReader(inputStream); 250 | EncodableHistogram histogram = histogramReader.nextIntervalHistogram(); 251 | Assert.assertNotNull(histogram); 252 | } 253 | } 254 | } 255 | 256 | private static class TestHandler extends Handler.Abstract { 257 | private final AtomicInteger getNumber = new AtomicInteger(0); 258 | private final AtomicInteger postNumber = new AtomicInteger(0); 259 | private final ServerConnector connector; 260 | 261 | private TestHandler(ServerConnector connector) { 262 | this.connector = connector; 263 | } 264 | 265 | @Override 266 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 267 | Fields parameters = org.eclipse.jetty.server.Request.extractQueryParameters(request); 268 | String method = request.getMethod().toUpperCase(Locale.ENGLISH); 269 | switch (method) { 270 | case "GET": { 271 | String fail = parameters.getValue("fail"); 272 | if (fail != null) { 273 | if (getNumber.get() >= Integer.parseInt(fail)) { 274 | try { 275 | connector.stop(); 276 | } catch (Exception e) { 277 | throw new RuntimeException(e.getMessage(), e); 278 | } 279 | } 280 | } 281 | Content.Sink.write(response, true, "Jetty rocks!!", callback); 282 | getNumber.addAndGet(1); 283 | break; 284 | } 285 | case "POST": { 286 | Content.copy(request, response, callback); 287 | postNumber.addAndGet(1); 288 | break; 289 | } 290 | } 291 | return true; 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/src/test/resources/single_fail_resource.groovy: -------------------------------------------------------------------------------- 1 | import org.mortbay.jetty.load.generator.Resource 2 | 3 | return new Resource("/index.html?fail=5") 4 | -------------------------------------------------------------------------------- /jetty-load-generator-starter/src/test/resources/tree_resources.groovy: -------------------------------------------------------------------------------- 1 | import org.mortbay.jetty.load.generator.Resource 2 | 3 | return new Resource("/index.html", 4 | new Resource("/css/bootstrap.css", 5 | new Resource("/css/bootstrap-theme.css").requestHeader("X-Header", "value"), 6 | new Resource("/js/jquery-3.1.1.min.js"), 7 | new Resource("/js/jquery-3.1.1.min.js"), 8 | new Resource("/js/jquery-3.1.1.min.js"), 9 | new Resource("/js/jquery-3.1.1.min.js") 10 | ), 11 | new Resource("/js/bootstrap.js", 12 | new Resource("/js/bootstrap.js"), 13 | new Resource("/js/bootstrap.js"), 14 | new Resource("/js/bootstrap.js") 15 | ), 16 | new Resource("/hello").method("POST").requestLength(42).responseLength(4242), 17 | new Resource("/dump.jsp?wine=foo&foo=bar"), 18 | new Resource("/not_here.html"), 19 | new Resource("/hello?name=foo"), 20 | new Resource("/hello?name=foo"), 21 | new Resource("/upload").method("PUT").requestLength(8192) 22 | ) 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | org.mortbay.jetty.loadgenerator 6 | jetty-load-generator 7 | 4.0.23-SNAPSHOT 8 | pom 9 | Jetty :: Load Generator 10 | https://github.com/jetty-project/jetty-load-generator 11 | 12 | The Jetty HTTP and HTTP/2 Load Generator 13 | 14 | 15 | 2016 16 | 17 | 18 | UTF-8 19 | 12.0.22 20 | true 21 | 0.7.1 22 | 23 | 24 | 25 | 26 | Apache Software License - Version 2.0 27 | https://www.apache.org/licenses/LICENSE-2.0 28 | 29 | 30 | Eclipse Public License - Version 2.0 31 | https://www.eclipse.org/legal/epl-2.0 32 | 33 | 34 | 35 | 36 | scm:git:git://github.com/jetty-project/jetty-load-generator.git 37 | scm:git:ssh://git@github.com/jetty-project/jetty-load-generator.git 38 | https://github.com/jetty-project/jetty-load-generator 39 | jetty-load-generator-4.0.15 40 | 41 | 42 | 43 | 44 | sonatype-cp 45 | Central Portal 46 | https://repo.maven.apache.org/maven2 47 | 48 | 49 | sonatype-cp 50 | Central Portal 51 | https://central.sonatype.com/repository/maven-snapshots 52 | 53 | 54 | 55 | 56 | jetty-load-generator-client 57 | jetty-load-generator-listeners 58 | jetty-load-generator-starter 59 | 60 | 61 | 62 | 63 | 64 | com.beust 65 | jcommander 66 | 1.82 67 | 68 | 69 | org.hdrhistogram 70 | HdrHistogram 71 | 2.2.2 72 | 73 | 74 | org.eclipse.jetty 75 | jetty-client 76 | ${jetty.version} 77 | 78 | 79 | org.eclipse.jetty 80 | jetty-alpn-java-client 81 | ${jetty.version} 82 | 83 | 84 | org.eclipse.jetty 85 | jetty-alpn-java-server 86 | ${jetty.version} 87 | 88 | 89 | org.eclipse.jetty 90 | jetty-jmx 91 | ${jetty.version} 92 | 93 | 94 | org.eclipse.jetty.http2 95 | jetty-http2-client-transport 96 | ${jetty.version} 97 | 98 | 99 | org.eclipse.jetty.toolchain 100 | jetty-perf-helper 101 | 1.0.7 102 | 103 | 104 | org.eclipse.jetty 105 | jetty-server 106 | ${jetty.version} 107 | 108 | 109 | org.eclipse.jetty 110 | jetty-util-ajax 111 | ${jetty.version} 112 | 113 | 114 | org.eclipse.jetty 115 | jetty-xml 116 | ${jetty.version} 117 | 118 | 119 | org.apache.groovy 120 | groovy 121 | 4.0.27 122 | 123 | 124 | org.slf4j 125 | slf4j-api 126 | 2.0.17 127 | 128 | 129 | 130 | org.eclipse.jetty 131 | jetty-slf4j-impl 132 | ${jetty.version} 133 | test 134 | 135 | 136 | org.eclipse.jetty.http2 137 | jetty-http2-server 138 | ${jetty.version} 139 | test 140 | 141 | 142 | junit 143 | junit 144 | 4.13.2 145 | test 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | maven-enforcer-plugin 154 | false 155 | 156 | 157 | enforce-versions 158 | 159 | enforce 160 | 161 | 162 | 163 | 164 | [11,) 165 | 166 | 167 | [3.9,) 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | maven-compiler-plugin 176 | 177 | 17 178 | 17 179 | 17 180 | true 181 | true 182 | -Xlint:all,-serial,-unchecked 183 | 184 | 185 | 186 | com.mycila 187 | license-maven-plugin 188 | false 189 | 190 | 191 | 192 |
header-template.txt
193 | 194 | **/*.java 195 | 196 |
197 |
198 | true 199 | true 200 | true 201 | 202 | ${project.inceptionYear}-2022 203 | 204 | 205 | DOUBLESLASH_STYLE 206 | 207 |
208 | 209 | 210 | check-copyright-header 211 | verify 212 | 213 | check 214 | 215 | 216 | 217 |
218 | 219 | org.jacoco 220 | jacoco-maven-plugin 221 | 222 | 223 | jacoco-initialize 224 | initialize 225 | 226 | prepare-agent 227 | 228 | 229 | 230 | jacoco-report 231 | package 232 | 233 | report 234 | 235 | 236 | 237 | **/org/mortbay/jetty/load/generator/** 238 | 239 | 240 | 241 | 242 | 243 | 244 | maven-release-plugin 245 | 246 | true 247 | deploy 248 | clean install 249 | forked-path 250 | true 251 | release 252 | 253 | 254 |
255 | 256 | 257 | 258 | 259 | eu.maveniverse.maven.plugins 260 | njord 261 | ${njord.version} 262 | 263 | 264 | org.apache.maven.plugins 265 | maven-clean-plugin 266 | 3.5.0 267 | 268 | 269 | org.apache.maven.plugins 270 | maven-compiler-plugin 271 | 3.14.0 272 | 273 | 274 | org.apache.maven.plugins 275 | maven-deploy-plugin 276 | 3.1.4 277 | 278 | 279 | org.apache.maven.plugins 280 | maven-enforcer-plugin 281 | 3.5.0 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-gpg-plugin 286 | 3.2.7 287 | 288 | 289 | org.apache.maven.plugins 290 | maven-install-plugin 291 | 3.1.4 292 | 293 | 294 | org.apache.maven.plugins 295 | maven-jar-plugin 296 | 3.4.2 297 | 298 | 299 | org.apache.maven.plugins 300 | maven-javadoc-plugin 301 | 3.11.2 302 | 303 | 8 304 | 305 | -html5 306 | 307 | 308 | 309 | 310 | org.apache.maven.plugins 311 | maven-release-plugin 312 | 3.1.1 313 | 314 | 315 | org.apache.maven.plugins 316 | maven-resources-plugin 317 | 3.3.1 318 | 319 | 320 | org.apache.maven.plugins 321 | maven-scm-plugin 322 | 2.1.0 323 | 324 | 325 | org.apache.maven.plugins 326 | maven-shade-plugin 327 | 3.6.0 328 | 329 | 330 | org.apache.maven.plugins 331 | maven-site-plugin 332 | 4.0.0-M16 333 | 334 | 335 | org.apache.maven.plugins 336 | maven-source-plugin 337 | 3.3.1 338 | 339 | 340 | org.apache.maven.plugins 341 | maven-surefire-plugin 342 | 3.5.3 343 | 344 | 345 | org.jacoco 346 | jacoco-maven-plugin 347 | 0.8.13 348 | 349 | 350 | com.mycila 351 | license-maven-plugin 352 | 5.0.0 353 | 354 | 355 | 356 | 357 | 358 | 359 | eu.maveniverse.maven.njord 360 | extension 361 | ${njord.version} 362 | 363 | 364 |
365 | 366 | 367 | 368 | sbordet 369 | Simone Bordet 370 | sbordet@webtide.com 371 | Webtide 372 | https://webtide.com 373 | 374 | 375 | olamy 376 | olamy@webtide.com 377 | Olivier Lamy 378 | Webtide 379 | https://webtide.com 380 | 381 | 382 | 383 | 384 | 385 | release 386 | 387 | 388 | 389 | maven-gpg-plugin 390 | 391 | 392 | sign-artifacts 393 | verify 394 | 395 | sign 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | jetty.snapshot 408 | https://oss.sonatype.org/content/repositories/jetty-snapshots/ 409 | 410 | false 411 | 412 | 413 | true 414 | 415 | 416 | 417 | jetty.staging 418 | https://oss.sonatype.org/content/groups/jetty-with-staging 419 | 420 | true 421 | 422 | 423 | false 424 | 425 | 426 | 427 | 428 |
429 | --------------------------------------------------------------------------------