├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.template.md ├── browserstack-config.json ├── examples └── client.html ├── package-lock.json ├── package.json ├── run_browserstack.sh └── src ├── ndt7-download-worker.js ├── ndt7-download-worker.min.js ├── ndt7-upload-worker.js ├── ndt7-upload-worker.min.js ├── ndt7.js ├── ndt7.min.js └── test ├── e2e ├── index.html ├── server.js └── test.js ├── index.html ├── test.js └── traces ├── save-download-trace.js └── test-download.jsonl /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | *.min.js 4 | src/test/e2e/*.js 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2020": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "google" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 11 13 | }, 14 | "rules": { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/test/e2e/*.min.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 17 5 | 6 | # Required by TestCafe. 7 | before_install: 8 | - stty cols 80 9 | 10 | addons: 11 | firefox: latest 12 | chrome: stable 13 | browserstack: 14 | username: 15 | secure: "gmPqQKABPgef6L+GxJHHW7wK5OC+DlB2GiMZfs7x4b09IumZxk3k889tkHQU6FrFHCPhaZdsL8y4G1Xl3LkW0anFqPTZ6B9dG/2O1dy+UjO/bl5Mc5Q3+FnMKJlAa0lSpGQzwcoimkhQDdaJjQvP5NwJgAZkMA48C6qayO69bY0zieDQ7b56Nicf+cYrIy6AsmEvCLOgogi56DbCkBB1GGr4XXp+gINlIEeavoZPDBwRkWCKnN2KiBS0Tz08YTC3dR8wju/YHFNa/L3rB/fGJ3vufUHsojVnH3clZDEPGrGqJFyxtnff8zcqslZK0Gck95wbzHSZqkqtczOlN7dZ4KUwHOKkFS29fqaWfNM6ltAWi4ZPnPrIROo+TmnbNzt4sD0uYpjowtZA7wvB6H9+xR9+Md3ZRiyQkybDSfsylgkN7RRyWICtgQatOoEfzodV5eqp6p/rjoKjC6Vp35T8gpbZeoJIsPL91ZuYApicVBIGASA+kuNrLBK4ECpviOsPEGwdL/Kcp2NaQQr8eoMZk6Yv1zieXU0LFcpEVu2ADMgQJAV/lvcPrBR932Oj+Yyt7k58K0g5qaxthvRnF07YqZVkPMMjGU8/hefAPxVmBdhvtgr/QppHUvZ9tLYp7wi31bLN2z7TmfX0ms6t8su1Vsu9EnR2guXqUdZfvDu769I=" 16 | access_key: 17 | secure: "e5B3s5IBg7O+J+N6yX2knoXyYPhQDDchxht0NHvqhKy+vj4Rqt+x20130VMdbIR/LZyMV7axTnOLn3GFL28g7n/Aw2+HGsbNSb46QSm+WvC/fjpXGcBTdDxcTUDVDjqDYrZRYAKAOl6Sr0ogYb8X9+dysfbolL+lYscm+QtxMLB5J+uuHB2LDJcinCToVYnyUrlQXnhQVqen7PRvfWAn4tfAUwabLb6+WmuP2Sc8Tlxi3hkvtwsahUXT9TFKEvq1FhLztf16Tl2gFG7NWMvRM401B5Hmc1YlrVR3175ZJ2DHVZXSyeZCw75V4pjrJwej5B6/4truQAlKniIQsani4VkJB/8WaqMqps20gzQieNzLL//yToe8j+6YBf3p4loVucQ1mEruJDlp+piA33yJ2IOQ/zDOrnN9/VglAvoLzjqk5Lg0jOGBOo/AY7f9rcMSjOKPXB7gVwQB3pdnvL2Hm3qRZg+r0DahfB/ExlWLH5odIxpiD0mHmkX7quFZJiF5Y0lzyiH9xHDhHgCnm9pg12/FcLn+H15wHeyyV/YC5LfD6p0N6nQapln3PMWjaOS7A6/efyZY8LGZAi7PVMIVuVuStSWXAjNjZDHAgsczEH6EV7Tj2FoGkqPkHd0mD5ipGx6uKe9/ZNffH7mC6hd+jkW31dIl3JQJ2Aw3DMIvQBA=" 18 | 19 | script: 20 | # Coverage runs the tests, so this implies `npm test` 21 | # TODO: upload coverage to coveralls.io 22 | - npm run coverage 23 | # Lint the code. Note that the linter version is saved in 24 | # package-lock.json, which means that eslint on the host and the server 25 | # should be exactly the same. 26 | - npm run lint 27 | # Verify that the things that are compiled and checked in have been 28 | # generated correctly from the most recent sources. 29 | - npm run document && git diff --exit-code README.md 30 | - npm run minify && git diff --exit-code src/ndt7.min.js 31 | # TODO Upload the code to npm after it is tagged. 32 | 33 | deploy: 34 | # Set up a deployment script to run browserstack tests when the deployment is 35 | # triggered via a cronjob. 36 | - provider: script 37 | skip_cleanup: true 38 | script: "npm run browserstack" 39 | on: 40 | repo: m-lab/ndt7-js 41 | all_branches: true 42 | condition: "$TRAVIS_EVENT_TYPE == cron" 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ndt7-js 2 | 3 | The official [NDT7](https://github.com/m-lab/ndt-server) Javascript client 4 | library. This code works in all modern browsers and is the source for the npm 5 | package [`@m-lab/ndt7`](https://www.npmjs.com/package/@m-lab/ndt7). 6 | 7 | Includes an example web client in the `examples/` directory. Pull requests 8 | gratefully accepted if you would like to write a more sophisticated web client 9 | that uses the returned measurements to debug network conditions. 10 | 11 | In case you need a standalone client binary that you can build and run on 12 | multiple operating systems and CPU architectures (including embedded devices) 13 | have a look at the 14 | [official Go client](https://github.com/m-lab/ndt7-client-go) instead. 15 | 16 | ## API Reference 17 | 18 | 19 | 20 | ## ndt7 : object 21 | **Kind**: global namespace 22 | 23 | * [ndt7](#ndt7) : object 24 | * [.discoverServerURLS](#ndt7.discoverServerURLS) 25 | * [.downloadTest](#ndt7.downloadTest) ⇒ number 26 | * [.uploadTest](#ndt7.uploadTest) ⇒ number 27 | * [.test](#ndt7.test) ⇒ number 28 | 29 | 30 | 31 | ### ndt7.discoverServerURLS 32 | discoverServerURLs contacts a web service (likely the Measurement Lab 33 | locate service, but not necessarily) and gets URLs with access tokens in 34 | them for the client. It can be short-circuted if config.server exists, 35 | which is useful for clients served from the webserver of an NDT server. 36 | 37 | **Kind**: static property of [ndt7](#ndt7) 38 | **Access**: public 39 | 40 | | Param | Type | Description | 41 | | --- | --- | --- | 42 | | config | Object | An associative array of configuration options. | 43 | | userCallbacks | Object | An associative array of user callbacks. It uses the callback functions `error`, `serverDiscovery`, and `serverChosen`. | 44 | 45 | 46 | 47 | ### ndt7.downloadTest ⇒ number 48 | downloadTest runs just the NDT7 download test. 49 | 50 | **Kind**: static property of [ndt7](#ndt7) 51 | **Returns**: number - Zero on success, and non-zero error code on failure. 52 | **Access**: public 53 | 54 | | Param | Type | Description | 55 | | --- | --- | --- | 56 | | config | Object | An associative array of configuration strings | 57 | | userCallbacks | Object | | 58 | | urlPromise | Object | A promise that will resolve to urls. | 59 | 60 | 61 | 62 | ### ndt7.uploadTest ⇒ number 63 | uploadTest runs just the NDT7 download test. 64 | 65 | **Kind**: static property of [ndt7](#ndt7) 66 | **Returns**: number - Zero on success, and non-zero error code on failure. 67 | **Access**: public 68 | 69 | | Param | Type | Description | 70 | | --- | --- | --- | 71 | | config | Object | An associative array of configuration strings | 72 | | userCallbacks | Object | | 73 | | urlPromise | Object | A promise that will resolve to urls. | 74 | 75 | 76 | 77 | ### ndt7.test ⇒ number 78 | test discovers a server to run against and then runs a download test 79 | followed by an upload test. 80 | 81 | **Kind**: static property of [ndt7](#ndt7) 82 | **Returns**: number - Zero on success, and non-zero error code on failure. 83 | **Access**: public 84 | 85 | | Param | Type | Description | 86 | | --- | --- | --- | 87 | | config | Object | An associative array of configuration strings | 88 | | userCallbacks | Object | | 89 | 90 | -------------------------------------------------------------------------------- /README.template.md: -------------------------------------------------------------------------------- 1 | # ndt7-js 2 | 3 | The official [NDT7](https://github.com/m-lab/ndt-server) Javascript client 4 | library. This code works in all modern browsers and is the source for the npm 5 | package [`@m-lab/ndt7`](https://www.npmjs.com/package/@m-lab/ndt7). 6 | 7 | Includes an example web client in the `examples/` directory. Pull requests 8 | gratefully accepted if you would like to write a more sophisticated web client 9 | that uses the returned measurements to debug network conditions. 10 | 11 | In case you need a standalone client binary that you can build and run on 12 | multiple operating systems and CPU architectures (including embedded devices) 13 | have a look at the 14 | [official Go client](https://github.com/m-lab/ndt7-client-go) instead. 15 | 16 | ## API Reference 17 | 18 | {{#namespace name="ndt7"}} 19 | {{>main}} 20 | {{/namespace}} 21 | -------------------------------------------------------------------------------- /browserstack-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserstack.networkProfile": "4g-lte-good" 3 | } 4 | -------------------------------------------------------------------------------- /examples/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@m-lab/ndt7", 3 | "version": "0.0.6", 4 | "description": "NDT7 client for measuring networks", 5 | "main": "src/ndt7.js", 6 | "scripts": { 7 | "test": "mocha src/test && cp src/*.min.js src/test/e2e/ && testcafe chrome:headless,firefox:headless src/test/e2e/test.js --app \"node src/test/e2e/server.js\"", 8 | "coverage": "nyc --reporter=lcovonly --file=coverage.cov --reporter=text npm run test", 9 | "lint": "eslint --ext .js src/", 10 | "document": "jsdoc2md --files=src/ndt7.js --template README.template.md > README.md", 11 | "minify": "minify --js < src/ndt7.js | sed -e 's/load-worker.js/load-worker.min.js/g' > src/ndt7.min.js && minify --js < src/ndt7-download-worker.js > src/ndt7-download-worker.min.js && minify --js < src/ndt7-upload-worker.js > src/ndt7-upload-worker.min.js", 12 | "browserstack": "cp src/*.min.js src/test/e2e/ && ./run_browserstack.sh" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "github.com/m-lab/ndt7-js" 17 | }, 18 | "keywords": [ 19 | "network", 20 | "measurement", 21 | "bbr", 22 | "ndt", 23 | "ndt7" 24 | ], 25 | "author": "Measurement Lab", 26 | "license": "Apache-2.0", 27 | "engines": { 28 | "node": ">=12" 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.2.0", 32 | "coveralls": "^3.1.0", 33 | "eslint": "^7.2.0", 34 | "eslint-config-google": "^0.14.0", 35 | "express": "^4.17.1", 36 | "jsdoc-to-markdown": "^6.0.1", 37 | "minify": "^6.0.1", 38 | "mocha": "^7.2.0", 39 | "nyc": "^15.1.0", 40 | "testcafe": "^1.18.4", 41 | "testcafe-browser-provider-browserstack": "^1.13.2" 42 | }, 43 | "files": [ 44 | "src/**/*" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /run_browserstack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script runs the e2e tests on browserstack. 3 | 4 | set -e # Exit immediately if a command exits with a non-zero status. 5 | set -u # Treat unset variables as an error. 6 | 7 | # Check that BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY are set. 8 | BROWSERSTACK_USERNAME=${BROWSERSTACK_USERNAME:?"Please set BROWSERSTACK_USERNAME"} 9 | BROWSERSTACK_ACCESS_KEY=${BROWSERSTACK_ACCESS_KEY:?"Please set BROWSERSTACK_ACCESS_KEY"} 10 | 11 | # Define an array of browsers to run. 12 | BROWSERS_WINDOWS=( 13 | "browserstack:chrome:Windows 11" 14 | "browserstack:edge:Windows 11" 15 | "browserstack:firefox:Windows 11" 16 | ) 17 | 18 | BROWSERS_MACOS_SAFARI=( 19 | # Note: Safari 15 support is broken on TestCafe. 20 | # https://github.com/DevExpress/testcafe/issues/6632 21 | # 22 | #"browserstack:safari@15.3:OS X Monterey" 23 | "browserstack:safari:OS X Big Sur" 24 | "browserstack:safari:OS X Catalina" 25 | "browserstack:safari:OS X Mojave" 26 | "browserstack:safari:OS X High Sierra" 27 | ) 28 | 29 | BROWSERS_MACOS_OTHERS=( 30 | "browserstack:chrome:OS X Monterey" 31 | "browserstack:edge:OS X Monterey" 32 | "browserstack:firefox:OS X Monterey" 33 | ) 34 | 35 | BROWSERS_IPHONE=( 36 | "browserstack:iPhone 13@15" 37 | "browserstack:iPhone 12@14" 38 | "browserstack:iPhone 11@13" 39 | "browserstack:iPhone 8@13" 40 | ) 41 | 42 | BROWSERS_ANDROID=( 43 | "browserstack:Google Nexus 6@6.0" 44 | "browserstack:Samsung Galaxy S8@7.0" 45 | "browserstack:Google Pixel@7.1" 46 | "browserstack:OnePlus 9@11.0" 47 | "browserstack:Xiaomi Redmi Note 8@9.0" 48 | ) 49 | 50 | function run_tests() { 51 | # Run TestCafe for each browser. Here we could define multiple environments 52 | # on a single TestCafe instance, but this does not work with Safari, for 53 | # reasons unclear. Thus, we run TestCafe for each browser in a separate 54 | # instance, save the pids of the instances, and wait for them to finish. 55 | browsers=("$@") 56 | pids="" 57 | for ((i = 0; i < ${#browsers[@]}; i++)); do 58 | echo "Running tests for ${browsers[$i]}..." 59 | export BROWSERSTACK_TEST_RUN_NAME="${browsers[$i]}" 60 | testcafe "${browsers[$i]}" src/test/e2e/test.js & 61 | pids+=" $!" 62 | done 63 | 64 | # Wait for the processes to finish. If any of them fails, exit with a 65 | # non-zero status. 66 | for pid in $pids; do 67 | if ! wait "$pid"; then 68 | echo "TestCafe tests failed for at least one browser." 69 | exit 1 70 | fi 71 | done 72 | } 73 | 74 | # If there isn't an identifier for BrowserStackLocal already, download the 75 | # binary and start it to create the tunnel. The BROWSERSTACK_LOCAL_IDENTIFIER 76 | # variable is set already when running on Travis. This allows running this 77 | # script locally. 78 | BROWSERSTACK_LOCAL_IDENTIFIER=${BROWSERSTACK_LOCAL_IDENTIFIER:-} 79 | BROWSERSTACK_LOCAL_PID="" 80 | if [ -z "$BROWSERSTACK_LOCAL_IDENTIFIER" ]; then 81 | echo "Starting BrowserStackLocal..." 82 | # Download the binary if needed. 83 | if [ ! -f "./BrowserStackLocal" ]; then 84 | wget https://www.browserstack.com/browserstack-local/BrowserStackLocal-linux-x64.zip 85 | unzip BrowserStackLocal-linux-x64.zip 86 | fi 87 | # --parallel-runs is set to the number of parallels supported by the 88 | # BrowserStack account. 89 | ./BrowserStackLocal --key $BROWSERSTACK_ACCESS_KEY \ 90 | --local-identifier testcafe-manual-tunnel --parallel-runs 5 & 91 | BROWSERSTACK_LOCAL_PID=$! 92 | 93 | # Give BrowserStackLocal some time to start. 94 | sleep 3 95 | 96 | export BROWSERSTACK_LOCAL_IDENTIFIER="testcafe-manual-tunnel" 97 | fi 98 | 99 | # Run the test server. 100 | node src/test/e2e/server.js & 101 | NODE_PID=$! 102 | 103 | # Some capabilities can only be configured via a separate JSON file. 104 | export BROWSERSTACK_CAPABILITIES_CONFIG_PATH="`pwd`/browserstack-config.json" 105 | # When this script is run by Travis, we just use Travis' build ID. 106 | # When it is run locally, we generate an ID with the current date/time. 107 | TRAVIS_BUILD_ID=${TRAVIS_BUILD_ID:-manual-$(date +%Y%m%d-%H%M%S)} 108 | export BROWSERSTACK_BUILD_ID="${TRAVIS_BUILD_ID}" 109 | # Use Automate instead of Javascript Testing. This allows to specify further 110 | # capabilities (such as network logs and connection throttling). 111 | export BROWSERSTACK_USE_AUTOMATE="1" 112 | # Save console and network logs. 113 | export BROWSERSTACK_CONSOLE="verbose" 114 | export BROWSERSTACK_NETWORK_LOGS="1" 115 | 116 | # Run each group of tests. 117 | run_tests "${BROWSERS_WINDOWS[@]}" 118 | run_tests "${BROWSERS_MACOS_SAFARI[@]}" 119 | run_tests "${BROWSERS_MACOS_OTHERS[@]}" 120 | run_tests "${BROWSERS_IPHONE[@]}" 121 | run_tests "${BROWSERS_ANDROID[@]}" 122 | 123 | # Terminate the test server. 124 | kill $NODE_PID 125 | 126 | # Terminate the browserstack tunnel if needed. 127 | if [ -z "$BROWSERSTACK_LOCAL_PID" ]; then 128 | kill $BROWSERSTACK_LOCAL_PID 129 | fi -------------------------------------------------------------------------------- /src/ndt7-download-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, node, worker */ 2 | 3 | // workerMain is the WebWorker function that runs the ndt7 download test. 4 | const workerMain = function(ev) { 5 | 'use strict'; 6 | const url = ev.data['///ndt/v7/download']; 7 | const sock = new WebSocket(url, 'net.measurementlab.ndt.v7'); 8 | let now; 9 | if (typeof performance !== 'undefined' && 10 | typeof performance.now === 'function') { 11 | now = () => performance.now(); 12 | } else { 13 | now = () => Date.now(); 14 | } 15 | downloadTest(sock, postMessage, now); 16 | }; 17 | 18 | /** 19 | * downloadTest is a function that runs an ndt7 download test using the 20 | * passed-in websocket instance and the passed-in callback function. The 21 | * socket and callback are passed in to enable testing and mocking. 22 | * 23 | * @param {WebSocket} sock - The WebSocket being read. 24 | * @param {function} postMessage - A function for messages to the main thread. 25 | * @param {function} now - A function returning a time in milliseconds. 26 | */ 27 | const downloadTest = function(sock, postMessage, now) { 28 | sock.onclose = function() { 29 | postMessage({ 30 | MsgType: 'complete', 31 | }); 32 | }; 33 | 34 | sock.onerror = function(ev) { 35 | postMessage({ 36 | MsgType: 'error', 37 | Error: ev.type, 38 | }); 39 | }; 40 | 41 | let start = now(); 42 | let previous = start; 43 | let total = 0; 44 | 45 | sock.onopen = function() { 46 | start = now(); 47 | previous = start; 48 | total = 0; 49 | postMessage({ 50 | MsgType: 'start', 51 | Data: { 52 | ClientStartTime: start, 53 | }, 54 | }); 55 | }; 56 | 57 | sock.onmessage = function(ev) { 58 | total += 59 | (typeof ev.data.size !== 'undefined') ? ev.data.size : ev.data.length; 60 | // Perform a client-side measurement 4 times per second. 61 | const t = now(); 62 | const every = 250; // ms 63 | if (t - previous > every) { 64 | postMessage({ 65 | MsgType: 'measurement', 66 | ClientData: { 67 | ElapsedTime: (t - start) / 1000, // seconds 68 | NumBytes: total, 69 | // MeanClientMbps is calculated via the logic: 70 | // (bytes) * (bits / byte) * (megabits / bit) = Megabits 71 | // (Megabits) * (1/milliseconds) * (milliseconds / second) = Mbps 72 | // Collect the conversion constants, we find it is 8*1000/1000000 73 | // When we simplify we get: 8*1000/1000000 = .008 74 | MeanClientMbps: (total / (t - start)) * 0.008, 75 | }, 76 | Source: 'client', 77 | }); 78 | previous = t; 79 | } 80 | 81 | // Pass along every server-side measurement. 82 | if (typeof ev.data === 'string') { 83 | postMessage({ 84 | MsgType: 'measurement', 85 | ServerMessage: ev.data, 86 | Source: 'server', 87 | }); 88 | } 89 | }; 90 | }; 91 | 92 | // Node and browsers get onmessage defined differently. 93 | if (typeof self !== 'undefined') { 94 | self.onmessage = workerMain; 95 | } else if (typeof this !== 'undefined') { 96 | this.onmessage = workerMain; 97 | } else if (typeof onmessage !== 'undefined') { 98 | onmessage = workerMain; 99 | } 100 | -------------------------------------------------------------------------------- /src/ndt7-download-worker.min.js: -------------------------------------------------------------------------------- 1 | const workerMain=function(e){"use strict";const n=e.data["///ndt/v7/download"],t=new WebSocket(n,"net.measurementlab.ndt.v7");let o;o="undefined"!=typeof performance&&"function"==typeof performance.now?()=>performance.now():()=>Date.now(),downloadTest(t,postMessage,o)},downloadTest=function(e,n,t){e.onclose=function(){n({MsgType:"complete"})},e.onerror=function(e){n({MsgType:"error",Error:e.type})};let o=t(),s=o,a=0;e.onopen=function(){o=t(),s=o,a=0,n({MsgType:"start",Data:{ClientStartTime:o}})},e.onmessage=function(e){a+=void 0!==e.data.size?e.data.size:e.data.length;const r=t();r-s>250&&(n({MsgType:"measurement",ClientData:{ElapsedTime:(r-o)/1e3,NumBytes:a,MeanClientMbps:a/(r-o)*.008},Source:"client"}),s=r),"string"==typeof e.data&&n({MsgType:"measurement",ServerMessage:e.data,Source:"server"})}};"undefined"!=typeof self?self.onmessage=workerMain:void 0!==this?this.onmessage=workerMain:"undefined"!=typeof onmessage&&(onmessage=workerMain); 2 | -------------------------------------------------------------------------------- /src/ndt7-upload-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6, browser, node, worker */ 2 | 3 | // WebWorker that runs the ndt7 upload test 4 | const workerMain = function(ev) { 5 | const url = ev.data['///ndt/v7/upload']; 6 | const sock = new WebSocket(url, 'net.measurementlab.ndt.v7'); 7 | let now; 8 | if (typeof performance !== 'undefined' && 9 | typeof performance.now === 'function') { 10 | now = () => performance.now(); 11 | } else { 12 | now = () => Date.now(); 13 | } 14 | uploadTest(sock, postMessage, now); 15 | }; 16 | 17 | const uploadTest = function(sock, postMessage, now) { 18 | let closed = false; 19 | sock.onclose = function() { 20 | if (!closed) { 21 | closed = true; 22 | postMessage({ 23 | MsgType: 'complete', 24 | }); 25 | } 26 | }; 27 | 28 | sock.onerror = function(ev) { 29 | postMessage({ 30 | MsgType: 'error', 31 | Error: ev.type, 32 | }); 33 | }; 34 | 35 | sock.onmessage = function(ev) { 36 | if (typeof ev.data !== 'undefined') { 37 | postMessage({ 38 | MsgType: 'measurement', 39 | Source: 'server', 40 | ServerMessage: ev.data, 41 | }); 42 | } 43 | }; 44 | 45 | /** 46 | * uploader is the main loop that uploads data in the web browser. It must 47 | * carefully balance a bunch of factors: 48 | * 1) message size determines measurement granularity on the client side, 49 | * 2) the JS event loop can only fire off so many times per second, and 50 | * 3) websocket buffer tracking seems inconsistent between browsers. 51 | * 52 | * Because of (1), we need to have small messages on slow connections, or 53 | * else this will not accurately measure slow connections. Because of (2), if 54 | * we use small messages on fast connections, then we will not fill the link. 55 | * Because of (3), we can't depend on the websocket buffer to "fill up" in a 56 | * reasonable amount of time. 57 | * 58 | * So on fast connections we need a big message size (one the message has 59 | * been handed off to the browser, it runs on the browser's fast compiled 60 | * internals) and on slow connections we need a small message. Because this 61 | * is used as a speed test, we don't know before the test which strategy we 62 | * will be using, because we don't know the speed before we test it. 63 | * Therefore, we use a strategy where we grow the message exponentially over 64 | * time. In an effort to be kind to the memory allocator, we always double 65 | * the message size instead of growing it by e.g. 1.3x. 66 | * 67 | * @param {*} data 68 | * @param {*} start 69 | * @param {*} end 70 | * @param {*} previous 71 | * @param {*} total 72 | */ 73 | function uploader(data, start, end, previous, total) { 74 | if (closed) { 75 | // socket.send() with too much buffering causes socket.close(). We only 76 | // observed this behaviour with pre-Chromium Edge. 77 | return; 78 | } 79 | const t = now(); 80 | if (t >= end) { 81 | sock.close(); 82 | // send one last measurement. 83 | postClientMeasurement(total, sock.bufferedAmount, start); 84 | return; 85 | } 86 | 87 | const maxMessageSize = 8388608; /* = (1<<23) = 8MB */ 88 | const clientMeasurementInterval = 250; // ms 89 | 90 | // Message size is doubled after the first 16 messages, and subsequently 91 | // every 8, up to maxMessageSize. 92 | const nextSizeIncrement = 93 | (data.length >= maxMessageSize) ? Infinity : 16 * data.length; 94 | if ((total - sock.bufferedAmount) >= nextSizeIncrement) { 95 | data = new Uint8Array(data.length * 2); 96 | } 97 | 98 | // We keep 7 messages in the send buffer, so there is always some more 99 | // data to send. The maximum buffer size is 8 * 8MB - 1 byte ~= 64M. 100 | const desiredBuffer = 7 * data.length; 101 | if (sock.bufferedAmount < desiredBuffer) { 102 | sock.send(data); 103 | total += data.length; 104 | } 105 | 106 | if (t >= previous + clientMeasurementInterval) { 107 | postClientMeasurement(total, sock.bufferedAmount, start); 108 | previous = t; 109 | } 110 | 111 | // Loop the uploader function in a way that respects the JS event handler. 112 | setTimeout(() => uploader(data, start, end, previous, total), 0); 113 | } 114 | 115 | /** Report measurement back to the main thread. 116 | * 117 | * @param {*} total 118 | * @param {*} bufferedAmount 119 | * @param {*} start 120 | */ 121 | function postClientMeasurement(total, bufferedAmount, start) { 122 | // bytes sent - bytes buffered = bytes actually sent 123 | const numBytes = total - bufferedAmount; 124 | // ms / 1000 = seconds 125 | const elapsedTime = (now() - start) / 1000; 126 | // bytes * bits/byte * megabits/bit * 1/seconds = Mbps 127 | const meanMbps = numBytes * 8 / 1000000 / elapsedTime; 128 | postMessage({ 129 | MsgType: 'measurement', 130 | ClientData: { 131 | ElapsedTime: elapsedTime, 132 | NumBytes: numBytes, 133 | MeanClientMbps: meanMbps, 134 | }, 135 | Source: 'client', 136 | Test: 'upload', 137 | }); 138 | } 139 | 140 | sock.onopen = function() { 141 | const initialMessageSize = 8192; /* (1<<13) = 8kBytes */ 142 | // TODO(bassosimone): fill this message - see above comment 143 | const data = new Uint8Array(initialMessageSize); 144 | const start = now(); // ms since epoch 145 | const duration = 10000; // ms 146 | const end = start + duration; // ms since epoch 147 | 148 | postMessage({ 149 | MsgType: 'start', 150 | Data: { 151 | StartTime: start / 1000, // seconds since epoch 152 | ExpectedEndTime: end / 1000, // seconds since epoch 153 | }, 154 | }); 155 | 156 | // Start the upload loop. 157 | uploader(data, start, end, start, 0); 158 | }; 159 | }; 160 | 161 | // Node and browsers get onmessage defined differently. 162 | if (typeof self !== 'undefined') { 163 | self.onmessage = workerMain; 164 | } else if (typeof this !== 'undefined') { 165 | this.onmessage = workerMain; 166 | } else if (typeof onmessage !== 'undefined') { 167 | onmessage = workerMain; 168 | } 169 | -------------------------------------------------------------------------------- /src/ndt7-upload-worker.min.js: -------------------------------------------------------------------------------- 1 | const workerMain=function(e){const n=e.data["///ndt/v7/upload"],t=new WebSocket(n,"net.measurementlab.ndt.v7");let o;o="undefined"!=typeof performance&&"function"==typeof performance.now?()=>performance.now():()=>Date.now(),uploadTest(t,postMessage,o)},uploadTest=function(e,n,t){let o=!1;function r(n,a,u,i,f){if(o)return;const c=t();if(c>=u)return e.close(),void s(f,e.bufferedAmount,a);const d=n.length>=8388608?1/0:16*n.length;f-e.bufferedAmount>=d&&(n=new Uint8Array(2*n.length));const m=7*n.length;e.bufferedAmount=i+250&&(s(f,e.bufferedAmount,a),i=c),setTimeout((()=>r(n,a,u,i,f)),0)}function s(e,o,r){const s=e-o,a=(t()-r)/1e3;n({MsgType:"measurement",ClientData:{ElapsedTime:a,NumBytes:s,MeanClientMbps:8*s/1e6/a},Source:"client",Test:"upload"})}e.onclose=function(){o||(o=!0,n({MsgType:"complete"}))},e.onerror=function(e){n({MsgType:"error",Error:e.type})},e.onmessage=function(e){void 0!==e.data&&n({MsgType:"measurement",Source:"server",ServerMessage:e.data})},e.onopen=function(){const e=new Uint8Array(8192),o=t(),s=o+1e4;n({MsgType:"start",Data:{StartTime:o/1e3,ExpectedEndTime:s/1e3}}),r(e,o,s,o,0)}};"undefined"!=typeof self?self.onmessage=workerMain:void 0!==this?this.onmessage=workerMain:"undefined"!=typeof onmessage&&(onmessage=workerMain); 2 | -------------------------------------------------------------------------------- /src/ndt7.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, node, worker */ 2 | 3 | // ndt7 contains the core ndt7 client functionality. Please, refer 4 | // to the ndt7 spec available at the following URL: 5 | // 6 | // https://github.com/m-lab/ndt-server/blob/master/spec/ndt7-protocol.md 7 | // 8 | // This implementation uses v0.9.0 of the spec. 9 | 10 | // Wrap everything in a closure to ensure that local definitions don't 11 | // permanently shadow global definitions. 12 | (function() { 13 | 'use strict'; 14 | 15 | /** 16 | * @name ndt7 17 | * @namespace ndt7 18 | */ 19 | const ndt7 = (function() { 20 | const staticMetadata = { 21 | 'client_library_name': 'ndt7-js', 22 | 'client_library_version': '0.0.6', 23 | }; 24 | // cb creates a default-empty callback function, allowing library users to 25 | // only need to specify callback functions for the events they care about. 26 | // 27 | // This function is not exported. 28 | const cb = function(name, callbacks, defaultFn) { 29 | if (typeof(callbacks) !== 'undefined' && name in callbacks) { 30 | return callbacks[name]; 31 | } else if (typeof defaultFn !== 'undefined') { 32 | return defaultFn; 33 | } else { 34 | // If no default function is provided, use the empty function. 35 | return function() {}; 36 | } 37 | }; 38 | 39 | // The default response to an error is to throw an exception. 40 | const defaultErrCallback = function(err) { 41 | throw new Error(err); 42 | }; 43 | 44 | /** 45 | * discoverServerURLs contacts a web service (likely the Measurement Lab 46 | * locate service, but not necessarily) and gets URLs with access tokens in 47 | * them for the client. It can be short-circuted if config.server exists, 48 | * which is useful for clients served from the webserver of an NDT server. 49 | * 50 | * @param {Object} config - An associative array of configuration options. 51 | * @param {Object} userCallbacks - An associative array of user callbacks. 52 | * 53 | * It uses the callback functions `error`, `serverDiscovery`, and 54 | * `serverChosen`. 55 | * 56 | * @name ndt7.discoverServerURLS 57 | * @public 58 | */ 59 | async function discoverServerURLs(config, userCallbacks) { 60 | config.metadata = Object.assign({}, config.metadata); 61 | config.metadata = Object.assign(config.metadata, staticMetadata); 62 | const callbacks = { 63 | error: cb('error', userCallbacks, defaultErrCallback), 64 | serverDiscovery: cb('serverDiscovery', userCallbacks), 65 | serverChosen: cb('serverChosen', userCallbacks), 66 | }; 67 | let protocol = 'wss'; 68 | if (config && ('protocol' in config)) { 69 | protocol = config.protocol; 70 | } 71 | 72 | const metadata = new URLSearchParams(config.metadata); 73 | // If a server was specified, use it. 74 | if (config && ('server' in config)) { 75 | // Add metadata as querystring parameters. 76 | const downloadURL = new URL(protocol + '://' + config.server + '/ndt/v7/download'); 77 | const uploadURL = new URL(protocol + '://' + config.server + '/ndt/v7/upload'); 78 | downloadURL.search = metadata; 79 | uploadURL.search = metadata; 80 | return { 81 | '///ndt/v7/download': downloadURL.toString(), 82 | '///ndt/v7/upload': uploadURL.toString(), 83 | }; 84 | } 85 | 86 | // If no server was specified then use a loadbalancer. If no loadbalancer 87 | // is specified, use the locate service from Measurement Lab. 88 | const lbURL = (config && ('loadbalancer' in config)) ? new URL(config.loadbalancer) : new URL('https://locate.measurementlab.net/v2/nearest/ndt/ndt7'); 89 | lbURL.search = metadata; 90 | callbacks.serverDiscovery({loadbalancer: lbURL}); 91 | const response = await fetch(lbURL).catch((err) => { 92 | throw new Error(err); 93 | }); 94 | const js = await response.json(); 95 | if (! ('results' in js) ) { 96 | callbacks.error(`Could not understand response from ${lbURL}: ${js}`); 97 | return {}; 98 | } 99 | 100 | // TODO: do not discard unused results. If the first server is unavailable 101 | // the client should quickly try the next server. 102 | // 103 | // Choose the first result sent by the load balancer. This ensures that 104 | // in cases where we have a single pod in a metro, that pod is used to 105 | // run the measurement. When there are multiple pods in the same metro, 106 | // they are randomized by the load balancer already. 107 | const choice = js.results[0]; 108 | callbacks.serverChosen(choice); 109 | 110 | return { 111 | '///ndt/v7/download': choice.urls[protocol + ':///ndt/v7/download'], 112 | '///ndt/v7/upload': choice.urls[protocol + ':///ndt/v7/upload'], 113 | }; 114 | } 115 | 116 | /* 117 | * runNDT7Worker is a helper function that runs a webworker. It uses the 118 | * callback functions `error`, `start`, `measurement`, and `complete`. It 119 | * returns a c-style return code. 0 is success, non-zero is some kind of 120 | * failure. 121 | * 122 | * @private 123 | */ 124 | const runNDT7Worker = async function( 125 | config, callbacks, urlPromise, filename, testType) { 126 | if (config.userAcceptedDataPolicy !== true && 127 | config.mlabDataPolicyInapplicable !== true) { 128 | callbacks.error('The M-Lab data policy is applicable and the user ' + 129 | 'has not explicitly accepted that data policy.'); 130 | return 1; 131 | } 132 | 133 | let clientMeasurement; 134 | let serverMeasurement; 135 | 136 | // This makes the worker. The worker won't actually start until it 137 | // receives a message. 138 | const worker = new Worker(filename); 139 | 140 | // When the workerPromise gets resolved it will terminate the worker. 141 | // Workers are resolved with c-style return codes. 0 for success, 142 | // non-zero for failure. 143 | const workerPromise = new Promise((resolve) => { 144 | worker.resolve = function(returnCode) { 145 | if (returnCode == 0) { 146 | callbacks.complete({ 147 | LastClientMeasurement: clientMeasurement, 148 | LastServerMeasurement: serverMeasurement, 149 | }); 150 | } 151 | worker.terminate(); 152 | resolve(returnCode); 153 | }; 154 | }); 155 | 156 | // If the worker takes 12 seconds, kill it and return an error code. 157 | // Most clients take longer than 10 seconds to complete the upload and 158 | // finish sending the buffer's content, sometimes hitting the socket's 159 | // timeout of 15 seconds. This makes sure uploads terminate on time and 160 | // get a chance to send one last measurement after 10s. 161 | const workerTimeout = setTimeout(() => worker.resolve(0), 12000); 162 | 163 | // This is how the worker communicates back to the main thread of 164 | // execution. The MsgTpe of `ev` determines which callback the message 165 | // gets forwarded to. 166 | worker.onmessage = function(ev) { 167 | if (!ev.data || !ev.data.MsgType || ev.data.MsgType === 'error') { 168 | clearTimeout(workerTimeout); 169 | worker.resolve(1); 170 | const msg = (!ev.data) ? `${testType} error` : ev.data.Error; 171 | callbacks.error(msg); 172 | } else if (ev.data.MsgType === 'start') { 173 | callbacks.start(ev.data.Data); 174 | } else if (ev.data.MsgType == 'measurement') { 175 | // For performance reasons, we parse the JSON outside of the thread 176 | // doing the downloading or uploading. 177 | if (ev.data.Source == 'server') { 178 | serverMeasurement = JSON.parse(ev.data.ServerMessage); 179 | callbacks.measurement({ 180 | Source: ev.data.Source, 181 | Data: serverMeasurement, 182 | }); 183 | } else { 184 | clientMeasurement = ev.data.ClientData; 185 | callbacks.measurement({ 186 | Source: ev.data.Source, 187 | Data: ev.data.ClientData, 188 | }); 189 | } 190 | } else if (ev.data.MsgType == 'complete') { 191 | clearTimeout(workerTimeout); 192 | worker.resolve(0); 193 | } 194 | }; 195 | 196 | // We can't start the worker until we know the right server, so we wait 197 | // here to find that out. 198 | const urls = await urlPromise.catch((err) => { 199 | // Clear timer, terminate the worker and rethrow the error. 200 | clearTimeout(workerTimeout); 201 | worker.resolve(2); 202 | throw err; 203 | }); 204 | 205 | // Start the worker. 206 | worker.postMessage(urls); 207 | 208 | // Await the resolution of the workerPromise. 209 | return await workerPromise; 210 | 211 | // Liveness guarantee - once the promise is resolved, .terminate() has 212 | // been called and the webworker will be terminated or in the process of 213 | // being terminated. 214 | }; 215 | 216 | /** 217 | * downloadTest runs just the NDT7 download test. 218 | * @param {Object} config - An associative array of configuration strings 219 | * @param {Object} userCallbacks 220 | * @param {Object} urlPromise - A promise that will resolve to urls. 221 | * 222 | * @return {number} Zero on success, and non-zero error code on failure. 223 | * 224 | * @name ndt7.downloadTest 225 | * @public 226 | */ 227 | async function downloadTest(config, userCallbacks, urlPromise) { 228 | const callbacks = { 229 | error: cb('error', userCallbacks, defaultErrCallback), 230 | start: cb('downloadStart', userCallbacks), 231 | measurement: cb('downloadMeasurement', userCallbacks), 232 | complete: cb('downloadComplete', userCallbacks), 233 | }; 234 | const workerfile = config.downloadworkerfile || 'ndt7-download-worker.js'; 235 | return await runNDT7Worker( 236 | config, callbacks, urlPromise, workerfile, 'download') 237 | .catch((err) => { 238 | callbacks.error(err); 239 | }); 240 | } 241 | 242 | /** 243 | * uploadTest runs just the NDT7 download test. 244 | * @param {Object} config - An associative array of configuration strings 245 | * @param {Object} userCallbacks 246 | * @param {Object} urlPromise - A promise that will resolve to urls. 247 | * 248 | * @return {number} Zero on success, and non-zero error code on failure. 249 | * 250 | * @name ndt7.uploadTest 251 | * @public 252 | */ 253 | async function uploadTest(config, userCallbacks, urlPromise) { 254 | const callbacks = { 255 | error: cb('error', userCallbacks, defaultErrCallback), 256 | start: cb('uploadStart', userCallbacks), 257 | measurement: cb('uploadMeasurement', userCallbacks), 258 | complete: cb('uploadComplete', userCallbacks), 259 | }; 260 | const workerfile = config.uploadworkerfile || 'ndt7-upload-worker.js'; 261 | const rv = await runNDT7Worker( 262 | config, callbacks, urlPromise, workerfile, 'upload') 263 | .catch((err) => { 264 | callbacks.error(err); 265 | }); 266 | return rv << 4; 267 | } 268 | 269 | /** 270 | * test discovers a server to run against and then runs a download test 271 | * followed by an upload test. 272 | * 273 | * @param {Object} config - An associative array of configuration strings 274 | * @param {Object} userCallbacks 275 | * 276 | * @return {number} Zero on success, and non-zero error code on failure. 277 | * 278 | * @name ndt7.test 279 | * @public 280 | */ 281 | async function test(config, userCallbacks) { 282 | // Starts the asynchronous process of server discovery, allowing other 283 | // stuff to proceed in the background. 284 | const urlPromise = discoverServerURLs(config, userCallbacks); 285 | const downloadSuccess = await downloadTest( 286 | config, userCallbacks, urlPromise); 287 | const uploadSuccess = await uploadTest( 288 | config, userCallbacks, urlPromise); 289 | return downloadSuccess + uploadSuccess; 290 | } 291 | 292 | return { 293 | discoverServerURLs: discoverServerURLs, 294 | downloadTest: downloadTest, 295 | uploadTest: uploadTest, 296 | test: test, 297 | }; 298 | })(); 299 | 300 | // Modules are used by `require`, if this file is included on a web page, then 301 | // module will be undefined and we use the window.ndt7 piece. 302 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 303 | module.exports = ndt7; 304 | } else { 305 | window.ndt7 = ndt7; 306 | } 307 | })(); 308 | -------------------------------------------------------------------------------- /src/ndt7.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const e=function(){const e={client_library_name:"ndt7-js",client_library_version:"0.0.6"},t=function(e,t,r){return void 0!==t&&e in t?t[e]:void 0!==r?r:function(){}},r=function(e){throw new Error(e)};async function a(a,o){a.metadata=Object.assign({},a.metadata),a.metadata=Object.assign(a.metadata,e);const n={error:t("error",o,r),serverDiscovery:t("serverDiscovery",o),serverChosen:t("serverChosen",o)};let s="wss";a&&"protocol"in a&&(s=a.protocol);const d=new URLSearchParams(a.metadata);if(a&&"server"in a){const e=new URL(s+"://"+a.server+"/ndt/v7/download"),t=new URL(s+"://"+a.server+"/ndt/v7/upload");return e.search=d,t.search=d,{"///ndt/v7/download":e.toString(),"///ndt/v7/upload":t.toString()}}const c=a&&"loadbalancer"in a?new URL(a.loadbalancer):new URL("https://locate.measurementlab.net/v2/nearest/ndt/ndt7");c.search=d,n.serverDiscovery({loadbalancer:c});const l=await fetch(c).catch((e=>{throw new Error(e)})),i=await l.json();if(!("results"in i))return n.error(`Could not understand response from ${c}: ${i}`),{};const u=i.results[0];return n.serverChosen(u),{"///ndt/v7/download":u.urls[s+":///ndt/v7/download"],"///ndt/v7/upload":u.urls[s+":///ndt/v7/upload"]}}const o=async function(e,t,r,a,o){if(!0!==e.userAcceptedDataPolicy&&!0!==e.mlabDataPolicyInapplicable)return t.error("The M-Lab data policy is applicable and the user has not explicitly accepted that data policy."),1;let n,s;const d=new Worker(a),c=new Promise((e=>{d.resolve=function(r){0==r&&t.complete({LastClientMeasurement:n,LastServerMeasurement:s}),d.terminate(),e(r)}})),l=setTimeout((()=>d.resolve(0)),12e3);d.onmessage=function(e){if(e.data&&e.data.MsgType&&"error"!==e.data.MsgType)"start"===e.data.MsgType?t.start(e.data.Data):"measurement"==e.data.MsgType?"server"==e.data.Source?(s=JSON.parse(e.data.ServerMessage),t.measurement({Source:e.data.Source,Data:s})):(n=e.data.ClientData,t.measurement({Source:e.data.Source,Data:e.data.ClientData})):"complete"==e.data.MsgType&&(clearTimeout(l),d.resolve(0));else{clearTimeout(l),d.resolve(1);const r=e.data?e.data.Error:`${o} error`;t.error(r)}};const i=await r.catch((e=>{throw clearTimeout(l),d.resolve(2),e}));return d.postMessage(i),await c};async function n(e,a,n){const s={error:t("error",a,r),start:t("downloadStart",a),measurement:t("downloadMeasurement",a),complete:t("downloadComplete",a)},d=e.downloadworkerfile||"ndt7-download-worker.min.js";return await o(e,s,n,d,"download").catch((e=>{s.error(e)}))}async function s(e,a,n){const s={error:t("error",a,r),start:t("uploadStart",a),measurement:t("uploadMeasurement",a),complete:t("uploadComplete",a)},d=e.uploadworkerfile||"ndt7-upload-worker.min.js";return await o(e,s,n,d,"upload").catch((e=>{s.error(e)}))<<4}return{discoverServerURLs:a,downloadTest:n,uploadTest:s,test:async function(e,t){const r=a(e,t);return await n(e,t,r)+await s(e,t,r)}}}();"undefined"!=typeof module&&void 0!==module.exports?module.exports=e:window.ndt7=e}(); 2 | -------------------------------------------------------------------------------- /src/test/e2e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ndt7-js test client 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/test/e2e/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | app.use(express.static(__dirname)); 5 | const server = app.listen(5000); 6 | -------------------------------------------------------------------------------- /src/test/e2e/test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | fixture `Test ndt7-js client` 4 | .page `http://localhost:5000`; 5 | 6 | const server = Selector('#server'); 7 | const downloadStatus = Selector('#downloadStatus'); 8 | const download = Selector('#download'); 9 | const uploadStatus = Selector('#uploadStatus'); 10 | const upload = Selector('#upload'); 11 | 12 | test('Basic functionality tests', async t => { 13 | // The assertion timeout is set to 10 seconds, so notEql() will wait until 14 | // the server div becomes non-empty. This should be plenty of time for 15 | // Locate to return a server, but there is no guarantee. If that does not 16 | // happen it indicates a problem with either the testing machine or the 17 | // Locate service. In both cases, the test should fail. 18 | await t.expect(server.innerText).notEql('', { timeout: 10000 }); 19 | 20 | // We're in the middle of the download test, so these divs should be 21 | // populated. A 10-second timeout is used here as well, to handle cases 22 | // where connecting to the server takes longer than usual. 23 | await t.expect(downloadStatus.innerText).eql('measuring', { timeout: 10000 }); 24 | await t.expect(download.innerText).notEql(''); 25 | 26 | // The download test takes 10 seconds, so we expect downloadStatus to 27 | // become "complete" within 15s at most. 28 | await t.expect(downloadStatus.innerText).eql('complete', { timeout: 15000 }); 29 | await t.expect(download.innerText).notEql(''); 30 | 31 | // The same 10-second timeout as above is used for the client to connect 32 | // for the upload test. 33 | await t.expect(uploadStatus.innerText).eql('measuring', { timeout: 10000 }); 34 | await t.expect(upload.innerText).notEql(''); 35 | 36 | // The upload test takes 10 seconds, so we expect uploadStatus to 37 | // become "complete" within 15s at most. 38 | await t.expect(uploadStatus.innerText).eql('complete', { timeout: 15000 }); 39 | await t.expect(upload.innerText).notEql(''); 40 | 41 | // The error div should be empty. 42 | await t.expect(Selector('#error').innerText).eql(''); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /src/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Tests 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | /* eslint-env es6, node, browser, mocha */ 3 | 4 | 'use strict'; 5 | 6 | const ndt7 = require('../ndt7'); 7 | const chai = require('chai'); 8 | 9 | /* 10 | function webSocketMocks() { 11 | const vars = { 12 | instance: undefined, 13 | out: [], 14 | realURL: undefined, 15 | realProtocol: undefined, 16 | }; 17 | vars.newWebSocket = function(url, protocol) { 18 | if (vars.instance !== undefined) { 19 | throw new Error('too many new WebSocket calls'); 20 | } 21 | vars.realURL = url; 22 | vars.realProtocol = protocol; 23 | this.onclose = undefined; 24 | this.onopen = undefined; 25 | this.onmessage = undefined; 26 | this.close = function() { 27 | this.onclose(); 28 | }; 29 | this.bufferedAmount = 0; 30 | this.send = function(msg) { 31 | vars.out.push(msg); 32 | }; 33 | vars.instance = this; 34 | }; 35 | vars.open = function() { 36 | vars.instance.onopen(); 37 | }; 38 | vars.send = function(msg) { 39 | vars.instance.onmessage(msg); 40 | }; 41 | return vars; 42 | } 43 | 44 | function blobMocks(array) { 45 | array.size = array.length; 46 | } 47 | 48 | function dateMocks() { 49 | const times = []; 50 | return { 51 | newDate: function() { 52 | this.getTime = function() { 53 | return times.pop(); 54 | }; 55 | }, 56 | pushTime: function(now) { 57 | times.push(now); 58 | }, 59 | size: function() { 60 | return times.length; 61 | }, 62 | }; 63 | } 64 | */ 65 | 66 | /** 67 | * postMessageMocks contains mocks for postMessage 68 | * @return {Object} An object with a postMessage function. 69 | */ 70 | /* 71 | function postMessageMocks() { 72 | const messages = []; 73 | return { 74 | postMessage: function(msg) { 75 | messages.push(msg); 76 | }, 77 | pop: function() { 78 | return messages.pop(); 79 | }, 80 | size: function() { 81 | return messages.length; 82 | }, 83 | }; 84 | } 85 | */ 86 | 87 | describe('ndt7.noop', function() { 88 | it('this is a placeholder for working unit tests', function(done) { 89 | chai.assert(typeof(ndt7) !== 'undefined'); 90 | chai.assert(true); 91 | done(); 92 | }); 93 | }); 94 | 95 | /* 96 | describe('ndt7.DownloadTest', function() { 97 | it('should work as intended', function(done) { 98 | // create the mocks 99 | const datemocks = dateMocks(); 100 | const postmessagemocks = postMessageMocks(); 101 | const wsmocks = webSocketMocks(); 102 | // start download 103 | ndt7.startDownload({ 104 | Blob: blobMocks, 105 | Date: datemocks.newDate, 106 | WebSocket: wsmocks.newWebSocket, 107 | baseURL: 'https://www.example.com/', 108 | postMessage: postmessagemocks.postMessage, 109 | }); 110 | // we open the connection 111 | datemocks.pushTime(10); 112 | wsmocks.open(); 113 | if (datemocks.size() !== 0) { 114 | throw new Error('the code did not get the test start time'); 115 | } 116 | chai.assert(wsmocks.realURL === 'wss://www.example.com/ndt/v7/download'); 117 | chai.assert(wsmocks.realProtocol === 'net.measurementlab.ndt.v7'); 118 | // we receive a binary messages 119 | datemocks.pushTime(100); 120 | wsmocks.send({ 121 | data: new BlobMocks(new Uint8Array(4096)), 122 | }); 123 | if (postmessagemocks.size() !== 0) { 124 | throw new Error('the code did fire postMessage too early'); 125 | } 126 | if (datemocks.size() !== 0) { 127 | throw new Error('the code did not get the current time'); 128 | } 129 | // we receive a server measurement 130 | datemocks.pushTime(150); 131 | wsmocks.send({ 132 | data: `{"TCPInfo": {"RTT": 100}}`, 133 | }); 134 | if (postmessagemocks.size() !== 1) { 135 | throw new Error('unexpected number of posted messages'); 136 | } 137 | chai.assert.equal(postmessagemocks.pop(), { 138 | Origin: 'server', 139 | TCPInfo: { 140 | RTT: 100, 141 | }, 142 | Test: 'download', 143 | }); 144 | if (datemocks.size() !== 0) { 145 | throw new Error('the code did not get the current time'); 146 | } 147 | // we receive more data after more than 250 ms from previous beginning 148 | datemocks.pushTime(350); 149 | wsmocks.send({ 150 | data: new BlobMocks(new Uint8Array(4096)), 151 | }); 152 | if (postmessagemocks.size() !== 1) { 153 | throw new Error('unexpected number of posted messages'); 154 | } 155 | chai.assert.equal(postmessagemocks.pop(), { 156 | AppInfo: { 157 | ElapsedTime: 340000, 158 | NumBytes: 8217, 159 | }, 160 | Origin: 'client', 161 | Test: 'download', 162 | }); 163 | if (datemocks.size() !== 0) { 164 | throw new Error('the code did not get the current time'); 165 | } 166 | done(); 167 | }); 168 | }); 169 | 170 | describe('ndt7.startUpload', function() { 171 | it('should work as intended', function(done) { 172 | // create the mocks 173 | const datemocks = dateMocks(); 174 | const wsmocks = webSocketMocks(); 175 | let lastNumBytes = 0; 176 | let lastElapsedTime = 0; 177 | // start download 178 | ndt7.startUpload({ 179 | Date: datemocks.newDate, 180 | WebSocket: wsmocks.newWebSocket, 181 | baseURL: 'https://www.example.com/', 182 | postMessage: function(ev) { 183 | if (ev === null) { 184 | if (wsmocks.out.length != 22) { 185 | throw new Error('unexpected number of queued messages'); 186 | } 187 | if (wsmocks.out[17].length != 16384) { 188 | throw new Error('messages were not scaled'); 189 | } 190 | done(); 191 | return; 192 | } 193 | if (ev.Origin !== 'client') { 194 | throw new Error('unexpected message Origin'); 195 | } 196 | if (ev.Test !== 'upload') { 197 | throw new Error('unexpected message Test'); 198 | } 199 | if (ev.AppInfo.NumBytes <= lastNumBytes) { 200 | throw new Error('NumBytes didn\'t increment'); 201 | } 202 | if (ev.AppInfo.ElapsedTime <= lastElapsedTime) { 203 | throw new Error('ElapsedTime didn\'t increment'); 204 | } 205 | lastElapsedTime = ev.AppInfo.ElapsedTime; 206 | lastNumBytes = ev.AppInfo.NumBytes; 207 | }, 208 | }); 209 | // we open the connection 210 | for (let t = 11000; t > 0; t -= 440) { // reverse order since we push 211 | datemocks.pushTime(t); 212 | } 213 | wsmocks.open(); 214 | chai.assert(wsmocks.realURL === 'wss://www.example.com/ndt/v7/upload'); 215 | chai.assert(wsmocks.realProtocol === 'net.measurementlab.ndt.v7'); 216 | }); 217 | }); 218 | 219 | function makeMocks(config) { 220 | const state = { 221 | workerMain: config.workerMain, 222 | workerTerminated: 0, 223 | }; 224 | state.Worker = function() { 225 | this.onerror = undefined; 226 | this.onmessage = undefined; 227 | this.terminate = function() { 228 | state.workerTerminated++; 229 | }; 230 | this.postMessage = function(ev) { 231 | state.workerMain(this, ev); 232 | }; 233 | }; 234 | return state; 235 | } 236 | 237 | describe('ndt7.start', function() { 238 | it('should throw if there is no config', function() { 239 | chai.assert.throws(function() { 240 | ndt7.start(); 241 | }, Error); 242 | }); 243 | 244 | it('should throw if there is no userAcceptDataPolicy field', function() { 245 | chai.assert.throws(function() { 246 | ndt7.start({}); 247 | }, Error); 248 | }); 249 | 250 | it('should throw if user did not accept data policy', function() { 251 | chai.assert.throws(function() { 252 | ndt7.start({userAcceptedDataPolicy: false}); 253 | }, Error, 'fatal: user must accept data policy first'); 254 | }); 255 | 256 | it('should work as intended without optional callbacks', function(done) { 257 | const expectedURL = 'https://www.example.com/'; 258 | ndt7.start({ 259 | doStartWorker: function(config, url, name, callback) { 260 | chai.assert(typeof config === 'object'); 261 | chai.assert(url === expectedURL); 262 | chai.assert(name === 'download' || name === 'upload'); 263 | callback(); 264 | }, 265 | locate: function(callback) { 266 | callback(expectedURL); 267 | }, 268 | oncomplete: done, 269 | onstarting: function() {}, 270 | userAcceptedDataPolicy: true, 271 | }); 272 | }); 273 | 274 | it('should work as intended with optional callbacks', function(done) { 275 | let calledOnStarting = false; 276 | let calledOnServerURL = false; 277 | const expectedURL = 'https://www.example.com/'; 278 | ndt7.start({ 279 | doStartWorker: function(config, url, name, callback) { 280 | chai.assert(typeof config === 'object'); 281 | chai.assert(url === expectedURL); 282 | chai.assert(name === 'download' || name === 'upload'); 283 | chai.assert(calledOnServerURL == true); 284 | chai.assert(calledOnStarting == true); 285 | callback(); 286 | }, 287 | locate: function(callback) { 288 | callback(expectedURL); 289 | }, 290 | oncomplete: done, 291 | onstarting: function() { 292 | calledOnStarting = true; 293 | }, 294 | onserverurl: function(url) { 295 | calledOnServerURL = true; 296 | chai.assert(url === expectedURL); 297 | }, 298 | userAcceptedDataPolicy: true, 299 | }); 300 | }); 301 | 302 | it('should behave correctly when the worker is killed', function(done) { 303 | const complete = []; 304 | const mocks = makeMocks({ 305 | workerMain: function() {}, 306 | }); 307 | ndt7.start({ 308 | Worker: mocks.Worker, 309 | killAfter: 7, 310 | locate: function(callback) { 311 | callback('https://www.example.com/'); 312 | }, 313 | oncomplete: function() { 314 | chai.assert(mocks.workerTerminated === 2); 315 | chai.assert(complete.length === 2); 316 | chai.assert(complete[0].Origin === 'client'); 317 | chai.assert(complete[0].Test === 'download'); 318 | chai.assert(complete[0].WorkerInfo.ElapsedTime > 0); 319 | chai.assert(complete[0].WorkerInfo.Error === 'Terminated with timeout'); 320 | chai.assert(complete[1].Origin === 'client'); 321 | chai.assert(complete[1].Test === 'upload'); 322 | chai.assert(complete[1].WorkerInfo.ElapsedTime > 0); 323 | chai.assert(complete[1].WorkerInfo.Error === 'Terminated with timeout'); 324 | done(); 325 | }, 326 | ontestcomplete: function(ev) { 327 | complete.push(ev); 328 | }, 329 | userAcceptedDataPolicy: true, 330 | }); 331 | }); 332 | 333 | it('should behave correctly when the worker throws', function(done) { 334 | const complete = []; 335 | const mocks = makeMocks({ 336 | workerMain: function(worker) { 337 | setTimeout(function() { 338 | worker.onerror({}); 339 | }, 10); 340 | }, 341 | }); 342 | ndt7.start({ 343 | Worker: mocks.Worker, 344 | locate: function(callback) { 345 | callback('https://www.example.com/'); 346 | }, 347 | oncomplete: function() { 348 | chai.assert(complete.length === 2); 349 | chai.assert(complete[0].Origin === 'client'); 350 | chai.assert(complete[0].Test === 'download'); 351 | chai.assert(complete[0].WorkerInfo.ElapsedTime > 0); 352 | chai.assert( 353 | complete[0].WorkerInfo.Error === 'Terminated with exception'); 354 | chai.assert(complete[1].Origin === 'client'); 355 | chai.assert(complete[1].Test === 'upload'); 356 | chai.assert(complete[1].WorkerInfo.ElapsedTime > 0); 357 | chai.assert( 358 | complete[1].WorkerInfo.Error === 'Terminated with exception'); 359 | done(); 360 | }, 361 | ontestcomplete: function(ev) { 362 | complete.push(ev); 363 | }, 364 | userAcceptedDataPolicy: true, 365 | }); 366 | }); 367 | 368 | it( 369 | 'should behave correctly when the worker works as intended', 370 | function(done) { 371 | const complete = []; 372 | const mocks = makeMocks({ 373 | workerMain: function(worker, ev) { 374 | chai.assert(ev.baseURL === 'https://www.example.com/'); 375 | setTimeout(function() { 376 | worker.onmessage({data: {antani: 1}}); 377 | worker.onmessage({data: null}); 378 | }, 7); 379 | }, 380 | }); 381 | const msgs = []; 382 | ndt7.start({ 383 | Worker: mocks.Worker, 384 | locate: function(callback) { 385 | callback('https://www.example.com/'); 386 | }, 387 | oncomplete: function() { 388 | chai.assert(msgs.length === 2); 389 | chai.assert(msgs[0].antani === 1); 390 | chai.assert(msgs[1].antani === 1); 391 | chai.assert(complete.length === 2); 392 | chai.assert(complete[0].Origin === 'client'); 393 | chai.assert(complete[0].Test === 'download'); 394 | chai.assert(complete[0].WorkerInfo.ElapsedTime > 0); 395 | chai.assert(complete[0].WorkerInfo.Error === null); 396 | chai.assert(complete[1].Origin === 'client'); 397 | chai.assert(complete[1].Test === 'upload'); 398 | chai.assert(complete[1].WorkerInfo.ElapsedTime > 0); 399 | chai.assert(complete[1].WorkerInfo.Error === null); 400 | done(); 401 | }, 402 | ontestcomplete: function(ev) { 403 | complete.push(ev); 404 | }, 405 | ontestmeasurement: function(ev) { 406 | msgs.push(ev); 407 | }, 408 | userAcceptedDataPolicy: true, 409 | }); 410 | }, 411 | ); 412 | }); 413 | */ 414 | -------------------------------------------------------------------------------- /src/test/traces/save-download-trace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A node.js utility for saving a download trace to a specified 3 | * .jsonl file for subsequent playback to aid in end-to-end testing of clients. 4 | */ 5 | const argv = require('minimist')( 6 | process.argv.slice(2), 7 | { 8 | 'default': { 9 | 'server': undefined, 10 | 'tracefile': undefined, 11 | 'verbose': false, 12 | }, 13 | 'string': ['server', 'tracefile'], 14 | 'boolean': ['verbose'], 15 | }); 16 | const fs = require('fs'); 17 | 18 | /** Defines a bunch of callbacks to write a websocket to a file. 19 | * @param {string} filename The name of the file to save the jsonl to 20 | * @param {bool} verbose Whether to print debug output 21 | * @return {object} an object containing callback functions 22 | */ 23 | function traceSaver(filename, verbose) { 24 | let f; 25 | const measurement = function(msg) { 26 | if (msg instanceof Buffer) { 27 | return; 28 | } 29 | if (verbose) { 30 | if (msg.length > 40) { 31 | console.log(msg.substring(0, 40) + '...'); 32 | } else { 33 | console.log(msg); 34 | } 35 | } 36 | f.write(msg); 37 | }; 38 | const start = function(msg) { 39 | f = fs.createWriteStream(filename); 40 | }; 41 | const complete = function(msg) { 42 | f.end(); 43 | f = undefined; 44 | }; 45 | 46 | return { 47 | start: start, 48 | measurement: measurement, 49 | complete: complete, 50 | }; 51 | } 52 | 53 | if (!argv.server || !argv.tracefile) { 54 | console.error('You must provide --server=... and --tracefile=... args.'); 55 | process.exit(1); 56 | } 57 | 58 | const callbacks = traceSaver(argv.tracefile, argv.verbose); 59 | const WebSocket = require('ws'); 60 | 61 | const ws = new WebSocket('ws://' + argv.server + '/ndt/v7/download', 'net.measurementlab.ndt.v7'); 62 | ws.on('open', callbacks.start); 63 | ws.on('message', callbacks.measurement); 64 | ws.on('close', callbacks.complete); 65 | // The download can only proceed once this main() is done, as JS is 66 | // single-threaded. 67 | -------------------------------------------------------------------------------- /src/test/traces/test-download.jsonl: -------------------------------------------------------------------------------- 1 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1269047089,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":98963},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":5,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":100,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":252,"RTTVar":13,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":1570445773,"MaxPacingRate":-1,"BytesAcked":32735652,"BytesReceived":284,"SegsOut":556,"SegsIn":230,"NotsentBytes":3536082,"MinRTT":5,"DataSegsIn":1,"DataSegsOut":555,"DeliveryRate":1114604255,"BusyTime":100000,"RWndLimited":68000,"SndBufLimited":0,"Delivered":551,"DeliveredCE":0,"BytesSent":33095835,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":1,"RcvOooPack":0,"SndWnd":338944,"ElapsedTime":98963}} 2 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1802283472,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":153724},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":4,"LastAckSent":0,"LastDataRecv":28,"LastAckRecv":4,"PMTU":65535,"RcvSsThresh":65483,"RTT":165,"RTTVar":34,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2230325797,"MaxPacingRate":-1,"BytesAcked":55327287,"BytesReceived":309,"SegsOut":897,"SegsIn":379,"NotsentBytes":3601565,"MinRTT":5,"DataSegsIn":2,"DataSegsOut":895,"DeliveryRate":1354820689,"BusyTime":156000,"RWndLimited":116000,"SndBufLimited":0,"Delivered":896,"DeliveredCE":0,"BytesSent":55360055,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":1,"RcvOooPack":0,"SndWnd":13568,"ElapsedTime":153724}} 3 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2046343750,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":246251},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":52,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":131,"RTTVar":18,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2532350390,"MaxPacingRate":-1,"BytesAcked":89247481,"BytesReceived":334,"SegsOut":1418,"SegsIn":624,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":3,"DataSegsOut":1415,"DeliveryRate":1623545454,"BusyTime":248000,"RWndLimited":196000,"SndBufLimited":0,"Delivered":1414,"DeliveredCE":0,"BytesSent":89411215,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":1,"RcvOooPack":0,"SndWnd":308096,"ElapsedTime":246251}} 4 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2389889912,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":365298},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":100,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":242,"RTTVar":209,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2957488766,"MaxPacingRate":-1,"BytesAcked":155254345,"BytesReceived":359,"SegsOut":2425,"SegsIn":1089,"NotsentBytes":3470599,"MinRTT":5,"DataSegsIn":4,"DataSegsOut":2421,"DeliveryRate":2182766666,"BusyTime":364000,"RWndLimited":288000,"SndBufLimited":0,"Delivered":2422,"DeliveredCE":0,"BytesSent":155287113,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":1,"RcvOooPack":0,"SndWnd":13568,"ElapsedTime":365298}} 5 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2164724498,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":485114},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":104,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":129,"RTTVar":16,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2678846567,"MaxPacingRate":-1,"BytesAcked":224666325,"BytesReceived":384,"SegsOut":3486,"SegsIn":1591,"NotsentBytes":3143184,"MinRTT":5,"DataSegsIn":5,"DataSegsOut":3481,"DeliveryRate":1853292452,"BusyTime":484000,"RWndLimited":388000,"SndBufLimited":0,"Delivered":3482,"DeliveredCE":0,"BytesSent":224699093,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":1,"RcvOooPack":0,"SndWnd":9088,"ElapsedTime":485114}} 6 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2598529639,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":567158},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":72,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":140,"RTTVar":39,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":3215680428,"MaxPacingRate":-1,"BytesAcked":276332412,"BytesReceived":409,"SegsOut":4275,"SegsIn":1949,"NotsentBytes":3339633,"MinRTT":5,"DataSegsIn":6,"DataSegsOut":4270,"DeliveryRate":2403045871,"BusyTime":568000,"RWndLimited":456000,"SndBufLimited":0,"Delivered":4271,"DeliveredCE":0,"BytesSent":276365180,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":48128,"ElapsedTime":567158}} 7 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2258031790,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":601828},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":24,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":130,"RTTVar":11,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2794314341,"MaxPacingRate":-1,"BytesAcked":303638823,"BytesReceived":434,"SegsOut":4692,"SegsIn":2130,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":7,"DataSegsOut":4687,"DeliveryRate":1571592000,"BusyTime":604000,"RWndLimited":480000,"SndBufLimited":0,"Delivered":4688,"DeliveredCE":0,"BytesSent":303671591,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":601828}} 8 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1999479336,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":648868},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":20,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":152,"RTTVar":81,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2474355679,"MaxPacingRate":-1,"BytesAcked":331141683,"BytesReceived":459,"SegsOut":5115,"SegsIn":2319,"NotsentBytes":3863497,"MinRTT":5,"DataSegsIn":8,"DataSegsOut":5110,"DeliveryRate":617764150,"BusyTime":648000,"RWndLimited":508000,"SndBufLimited":0,"Delivered":5108,"DeliveredCE":0,"BytesSent":331370900,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":338944,"ElapsedTime":648868}} 9 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2146981047,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":947199},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":4,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":280,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":173,"RTTVar":77,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2656889045,"MaxPacingRate":-1,"BytesAcked":545140127,"BytesReceived":484,"SegsOut":8384,"SegsIn":3847,"NotsentBytes":2553837,"MinRTT":5,"DataSegsIn":9,"DataSegsOut":8379,"DeliveryRate":1559119047,"BusyTime":948000,"RWndLimited":696000,"SndBufLimited":0,"Delivered":8376,"DeliveredCE":0,"BytesSent":545434827,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":338944,"ElapsedTime":947199}} 10 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1540776378,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":1083429},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":124,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":180,"RTTVar":13,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":1906710768,"MaxPacingRate":-1,"BytesAcked":638256953,"BytesReceived":509,"SegsOut":9802,"SegsIn":4521,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":10,"DataSegsOut":9797,"DeliveryRate":1447138121,"BusyTime":1084000,"RWndLimited":796000,"SndBufLimited":0,"Delivered":9798,"DeliveredCE":0,"BytesSent":638289721,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":45568,"ElapsedTime":1083429}} 11 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2359746341,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":1118991},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":16,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":123,"RTTVar":19,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2920186097,"MaxPacingRate":-1,"BytesAcked":658163785,"BytesReceived":534,"SegsOut":10106,"SegsIn":4664,"NotsentBytes":2946735,"MinRTT":5,"DataSegsIn":11,"DataSegsOut":10101,"DeliveryRate":1522860465,"BusyTime":1120000,"RWndLimited":824000,"SndBufLimited":0,"Delivered":10102,"DeliveredCE":0,"BytesSent":658196553,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":2,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":1118991}} 12 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2182764585,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":1575758},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":444,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":129,"RTTVar":18,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2701171173,"MaxPacingRate":-1,"BytesAcked":1004306923,"BytesReceived":559,"SegsOut":15392,"SegsIn":7124,"NotsentBytes":3208667,"MinRTT":5,"DataSegsIn":12,"DataSegsOut":15387,"DeliveryRate":1610237704,"BusyTime":1576000,"RWndLimited":1184000,"SndBufLimited":0,"Delivered":15388,"DeliveredCE":0,"BytesSent":1004339691,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":3,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":1575758}} 13 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2242567744,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":1623407},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":32,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":149,"RTTVar":69,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2775177583,"MaxPacingRate":-1,"BytesAcked":1036982940,"BytesReceived":584,"SegsOut":15894,"SegsIn":7359,"NotsentBytes":3704867,"MinRTT":5,"DataSegsIn":13,"DataSegsOut":15889,"DeliveryRate":818537500,"BusyTime":1624000,"RWndLimited":1228000,"SndBufLimited":0,"Delivered":15887,"DeliveredCE":0,"BytesSent":1037212157,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":3,"RcvOooPack":0,"SndWnd":308096,"ElapsedTime":1623407}} 14 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2238734909,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":1648628},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":12,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":273,"RTTVar":252,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2770434450,"MaxPacingRate":-1,"BytesAcked":1052174996,"BytesReceived":609,"SegsOut":16123,"SegsIn":7468,"NotsentBytes":3405116,"MinRTT":5,"DataSegsIn":14,"DataSegsOut":16118,"DeliveryRate":1964490000,"BusyTime":1648000,"RWndLimited":1252000,"SndBufLimited":0,"Delivered":16119,"DeliveredCE":0,"BytesSent":1052207764,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":3,"RcvOooPack":0,"SndWnd":9088,"ElapsedTime":1648628}} 15 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2705907574,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2100148},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":436,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":113,"RTTVar":12,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":3348560623,"MaxPacingRate":-1,"BytesAcked":1402378080,"BytesReceived":634,"SegsOut":21471,"SegsIn":9996,"NotsentBytes":3667048,"MinRTT":5,"DataSegsIn":15,"DataSegsOut":21466,"DeliveryRate":1984333333,"BusyTime":2100000,"RWndLimited":1540000,"SndBufLimited":0,"Delivered":21467,"DeliveredCE":0,"BytesSent":1402410848,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":5,"RcvOooPack":0,"SndWnd":47616,"ElapsedTime":2100148}} 16 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2227310562,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2201692},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":88,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":156,"RTTVar":52,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":1,"PacingRate":2756296820,"MaxPacingRate":-1,"BytesAcked":1464979828,"BytesReceived":659,"SegsOut":22429,"SegsIn":10445,"NotsentBytes":3601565,"MinRTT":5,"DataSegsIn":16,"DataSegsOut":22424,"DeliveryRate":2227312925,"BusyTime":2204000,"RWndLimited":1620000,"SndBufLimited":0,"Delivered":22423,"DeliveredCE":0,"BytesSent":1465143562,"BytesRetrans":32768,"DSackDups":1,"ReordSeen":5,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":2201692}} 17 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2578069636,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2330482},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":116,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":132,"RTTVar":24,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":3190361175,"MaxPacingRate":-1,"BytesAcked":1557048926,"BytesReceived":684,"SegsOut":23834,"SegsIn":11117,"NotsentBytes":2881252,"MinRTT":5,"DataSegsIn":17,"DataSegsOut":23829,"DeliveryRate":1559119047,"BusyTime":2332000,"RWndLimited":1720000,"SndBufLimited":0,"Delivered":23830,"DeliveredCE":0,"BytesSent":1557147177,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":5,"RcvOooPack":0,"SndWnd":9088,"ElapsedTime":2330482}} 18 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2447960099,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2498458},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":156,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":146,"RTTVar":49,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":3029350623,"MaxPacingRate":-1,"BytesAcked":1675376707,"BytesReceived":709,"SegsOut":25641,"SegsIn":11960,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":18,"DataSegsOut":25636,"DeliveryRate":1693525862,"BusyTime":2500000,"RWndLimited":1848000,"SndBufLimited":0,"Delivered":25637,"DeliveredCE":0,"BytesSent":1675474958,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":6,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":2498458}} 19 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2499348195,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2544422},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":36,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":123,"RTTVar":17,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":3092943391,"MaxPacingRate":-1,"BytesAcked":1711392357,"BytesReceived":734,"SegsOut":26192,"SegsIn":12224,"NotsentBytes":3339633,"MinRTT":5,"DataSegsIn":19,"DataSegsOut":26187,"DeliveryRate":1711973856,"BusyTime":2544000,"RWndLimited":1888000,"SndBufLimited":0,"Delivered":26187,"DeliveredCE":0,"BytesSent":1711556091,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":7,"RcvOooPack":0,"SndWnd":338944,"ElapsedTime":2544422}} 20 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2751386390,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2569569},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":4,"LastAckSent":0,"LastDataRecv":16,"LastAckRecv":4,"PMTU":65535,"RcvSsThresh":65483,"RTT":177,"RTTVar":155,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":3404840658,"MaxPacingRate":-1,"BytesAcked":1730382427,"BytesReceived":759,"SegsOut":26481,"SegsIn":12359,"NotsentBytes":3863497,"MinRTT":5,"DataSegsIn":20,"DataSegsOut":26476,"DeliveryRate":389779761,"BusyTime":2572000,"RWndLimited":1908000,"SndBufLimited":0,"Delivered":26477,"DeliveredCE":0,"BytesSent":1730480678,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":7,"RcvOooPack":0,"SndWnd":12928,"ElapsedTime":2569569}} 21 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2403043329,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2710302},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":128,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":119,"RTTVar":33,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":2973766119,"MaxPacingRate":-1,"BytesAcked":1824023117,"BytesReceived":784,"SegsOut":27913,"SegsIn":13035,"NotsentBytes":3732531,"MinRTT":5,"DataSegsIn":21,"DataSegsOut":27908,"DeliveryRate":2317982300,"BusyTime":2712000,"RWndLimited":2012000,"SndBufLimited":0,"Delivered":27907,"DeliveredCE":0,"BytesSent":1824252334,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":7,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":2710302}} 22 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2232372871,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2849301},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":120,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":153,"RTTVar":47,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":2,"PacingRate":2762561427,"MaxPacingRate":-1,"BytesAcked":1924212107,"BytesReceived":809,"SegsOut":29442,"SegsIn":13741,"NotsentBytes":3798014,"MinRTT":5,"DataSegsIn":22,"DataSegsOut":29437,"DeliveryRate":1415848648,"BusyTime":2848000,"RWndLimited":2104000,"SndBufLimited":0,"Delivered":29437,"DeliveredCE":0,"BytesSent":1924375841,"BytesRetrans":98251,"DSackDups":2,"ReordSeen":7,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":2849301}} 23 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2471054689,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":2970203},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":96,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":116,"RTTVar":25,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3057930177,"MaxPacingRate":-1,"BytesAcked":2007244551,"BytesReceived":834,"SegsOut":30711,"SegsIn":14320,"NotsentBytes":3405116,"MinRTT":5,"DataSegsIn":23,"DataSegsOut":30706,"DeliveryRate":2471056603,"BusyTime":2972000,"RWndLimited":2204000,"SndBufLimited":0,"Delivered":30707,"DeliveredCE":0,"BytesSent":2007473768,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":8,"RcvOooPack":0,"SndWnd":43648,"ElapsedTime":2970203}} 24 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1925968521,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3055426},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":72,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":143,"RTTVar":18,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2383386045,"MaxPacingRate":-1,"BytesAcked":2068340190,"BytesReceived":859,"SegsOut":31646,"SegsIn":14762,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":24,"DataSegsOut":31641,"DeliveryRate":1794054794,"BusyTime":3056000,"RWndLimited":2268000,"SndBufLimited":0,"Delivered":31640,"DeliveredCE":0,"BytesSent":2068700373,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":8,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":3055426}} 25 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2381197729,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3080602},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":48000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":8,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":116,"RTTVar":16,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2946732189,"MaxPacingRate":-1,"BytesAcked":2084841906,"BytesReceived":884,"SegsOut":31896,"SegsIn":14882,"NotsentBytes":2825646,"MinRTT":5,"DataSegsIn":25,"DataSegsOut":31891,"DeliveryRate":2089882978,"BusyTime":3080000,"RWndLimited":2288000,"SndBufLimited":0,"Delivered":31892,"DeliveredCE":0,"BytesSent":2085071123,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":8,"RcvOooPack":0,"SndWnd":9088,"ElapsedTime":3080602}} 26 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2359746341,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3105991},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":44000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":4,"LastAckSent":0,"LastDataRecv":16,"LastAckRecv":4,"PMTU":65535,"RcvSsThresh":65483,"RTT":134,"RTTVar":35,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2920186097,"MaxPacingRate":-1,"BytesAcked":2102653282,"BytesReceived":909,"SegsOut":32168,"SegsIn":15014,"NotsentBytes":3892656,"MinRTT":5,"DataSegsIn":26,"DataSegsOut":32163,"DeliveryRate":1818972222,"BusyTime":3108000,"RWndLimited":2296000,"SndBufLimited":0,"Delivered":32164,"DeliveredCE":0,"BytesSent":2102882499,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":8,"RcvOooPack":0,"SndWnd":13568,"ElapsedTime":3105991}} 27 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2471054689,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3332331},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":52000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":208,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":130,"RTTVar":26,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3057930177,"MaxPacingRate":-1,"BytesAcked":2277820307,"BytesReceived":934,"SegsOut":34844,"SegsIn":16269,"NotsentBytes":3079194,"MinRTT":5,"DataSegsIn":27,"DataSegsOut":34839,"DeliveryRate":897027397,"BusyTime":3332000,"RWndLimited":2464000,"SndBufLimited":0,"Delivered":34839,"DeliveredCE":0,"BytesSent":2278115007,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":8,"RcvOooPack":0,"SndWnd":77056,"ElapsedTime":3332331}} 28 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2518574821,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3813973},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":468,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":130,"RTTVar":33,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3116736341,"MaxPacingRate":-1,"BytesAcked":2638304222,"BytesReceived":959,"SegsOut":40350,"SegsIn":18818,"NotsentBytes":3754942,"MinRTT":5,"DataSegsIn":28,"DataSegsOut":40345,"DeliveryRate":782665338,"BusyTime":3816000,"RWndLimited":2812000,"SndBufLimited":0,"Delivered":40344,"DeliveredCE":0,"BytesSent":2638664405,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":12,"RcvOooPack":0,"SndWnd":302976,"ElapsedTime":3813973}} 29 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2798418636,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3839157},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":8,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":114,"RTTVar":13,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3463043062,"MaxPacingRate":-1,"BytesAcked":2654216591,"BytesReceived":984,"SegsOut":40594,"SegsIn":18936,"NotsentBytes":3181542,"MinRTT":5,"DataSegsIn":29,"DataSegsOut":40589,"DeliveryRate":2158780219,"BusyTime":3840000,"RWndLimited":2828000,"SndBufLimited":0,"Delivered":40587,"DeliveredCE":0,"BytesSent":2654642257,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":12,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":3839157}} 30 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2683726308,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3884009},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":32,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":112,"RTTVar":14,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3321111307,"MaxPacingRate":-1,"BytesAcked":2687089057,"BytesReceived":1009,"SegsOut":41096,"SegsIn":19166,"NotsentBytes":3017671,"MinRTT":5,"DataSegsIn":30,"DataSegsOut":41091,"DeliveryRate":2004581632,"BusyTime":3884000,"RWndLimited":2868000,"SndBufLimited":0,"Delivered":41089,"DeliveredCE":0,"BytesSent":2687514723,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":12,"RcvOooPack":0,"SndWnd":307456,"ElapsedTime":3884009}} 31 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2311164568,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":3920922},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":24,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":211,"RTTVar":129,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2860066153,"MaxPacingRate":-1,"BytesAcked":2714853849,"BytesReceived":1034,"SegsOut":41517,"SegsIn":19354,"NotsentBytes":4125429,"MinRTT":5,"DataSegsIn":31,"DataSegsOut":41512,"DeliveryRate":1243348101,"BusyTime":3920000,"RWndLimited":2896000,"SndBufLimited":0,"Delivered":41513,"DeliveredCE":0,"BytesSent":2715083066,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":12,"RcvOooPack":0,"SndWnd":45056,"ElapsedTime":3920922}} 32 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2258031790,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":4546004},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":612,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":133,"RTTVar":9,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2794314341,"MaxPacingRate":-1,"BytesAcked":3207155043,"BytesReceived":1059,"SegsOut":49035,"SegsIn":22887,"NotsentBytes":2930200,"MinRTT":5,"DataSegsIn":32,"DataSegsOut":49030,"DeliveryRate":1423543478,"BusyTime":4548000,"RWndLimited":3336000,"SndBufLimited":0,"Delivered":49031,"DeliveredCE":0,"BytesSent":3207384260,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":15,"RcvOooPack":0,"SndWnd":12288,"ElapsedTime":4546004}} 33 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2640441030,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":4657268},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":96,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":123,"RTTVar":36,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3267545774,"MaxPacingRate":-1,"BytesAcked":3269363893,"BytesReceived":1084,"SegsOut":49985,"SegsIn":23340,"NotsentBytes":3601565,"MinRTT":5,"DataSegsIn":33,"DataSegsOut":49980,"DeliveryRate":1984333333,"BusyTime":4656000,"RWndLimited":3400000,"SndBufLimited":0,"Delivered":49981,"DeliveredCE":0,"BytesSent":3269593110,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":15,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":4657268}} 34 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2494590327,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":4682537},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":16,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":102,"RTTVar":10,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3087055530,"MaxPacingRate":-1,"BytesAcked":3291235215,"BytesReceived":1109,"SegsOut":50319,"SegsIn":23501,"NotsentBytes":4125429,"MinRTT":5,"DataSegsIn":34,"DataSegsOut":50314,"DeliveryRate":2486696202,"BusyTime":4684000,"RWndLimited":3420000,"SndBufLimited":0,"Delivered":50315,"DeliveredCE":0,"BytesSent":3291464432,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":16,"RcvOooPack":0,"SndWnd":11008,"ElapsedTime":4682537}} 35 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2359746341,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":5307995},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":600,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":177,"RTTVar":122,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":2920186097,"MaxPacingRate":-1,"BytesAcked":3769392081,"BytesReceived":1134,"SegsOut":57621,"SegsIn":26911,"NotsentBytes":3802274,"MinRTT":5,"DataSegsIn":35,"DataSegsOut":57616,"DeliveryRate":1835971962,"BusyTime":5308000,"RWndLimited":3924000,"SndBufLimited":0,"Delivered":57617,"DeliveredCE":0,"BytesSent":3769621298,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":18,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":5307995}} 36 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2471054689,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":5575275},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":252,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":205,"RTTVar":116,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":4,"PacingRate":3057930177,"MaxPacingRate":-1,"BytesAcked":3966495911,"BytesReceived":1159,"SegsOut":60631,"SegsIn":28272,"NotsentBytes":3798014,"MinRTT":5,"DataSegsIn":36,"DataSegsOut":60626,"DeliveryRate":1385883597,"BusyTime":5576000,"RWndLimited":4116000,"SndBufLimited":0,"Delivered":60627,"DeliveredCE":0,"BytesSent":3966725128,"BytesRetrans":229217,"DSackDups":4,"ReordSeen":19,"RcvOooPack":0,"SndWnd":10368,"ElapsedTime":5575275}} 37 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2359746341,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":5705946},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":112,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":116,"RTTVar":11,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2920186097,"MaxPacingRate":-1,"BytesAcked":4062232057,"BytesReceived":1184,"SegsOut":62097,"SegsIn":28971,"NotsentBytes":2815769,"MinRTT":5,"DataSegsIn":37,"DataSegsOut":62092,"DeliveryRate":2014861538,"BusyTime":5708000,"RWndLimited":4224000,"SndBufLimited":0,"Delivered":62091,"DeliveredCE":0,"BytesSent":4062723206,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":210560,"ElapsedTime":5705946}} 38 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2518574821,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":5800566},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":84,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":111,"RTTVar":7,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3116736341,"MaxPacingRate":-1,"BytesAcked":4132167901,"BytesReceived":1209,"SegsOut":63164,"SegsIn":29463,"NotsentBytes":2684803,"MinRTT":5,"DataSegsIn":38,"DataSegsOut":63159,"DeliveryRate":1984333333,"BusyTime":5800000,"RWndLimited":4300000,"SndBufLimited":0,"Delivered":63159,"DeliveredCE":0,"BytesSent":4132593567,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":307456,"ElapsedTime":5800566}} 39 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":1984331440,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":5838220},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":20,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":142,"RTTVar":17,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2455610158,"MaxPacingRate":-1,"BytesAcked":4156527577,"BytesReceived":1234,"SegsOut":63538,"SegsIn":29639,"NotsentBytes":3667048,"MinRTT":5,"DataSegsIn":39,"DataSegsOut":63533,"DeliveryRate":1857673758,"BusyTime":5840000,"RWndLimited":4332000,"SndBufLimited":0,"Delivered":63531,"DeliveredCE":0,"BytesSent":4157084209,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":5838220}} 40 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2480414301,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":6447625},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":52000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":596,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":155,"RTTVar":82,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3069512697,"MaxPacingRate":-1,"BytesAcked":4646209451,"BytesReceived":1259,"SegsOut":71014,"SegsIn":33107,"NotsentBytes":3602888,"MinRTT":5,"DataSegsIn":40,"DataSegsOut":71009,"DeliveryRate":1769810810,"BusyTime":6448000,"RWndLimited":4784000,"SndBufLimited":0,"Delivered":71009,"DeliveredCE":0,"BytesSent":4646635117,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":74496,"ElapsedTime":6447625}} 41 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2338677456,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":6936029},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":468,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":130,"RTTVar":35,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2894113352,"MaxPacingRate":-1,"BytesAcked":5039500349,"BytesReceived":1284,"SegsOut":77022,"SegsIn":35915,"NotsentBytes":3928980,"MinRTT":5,"DataSegsIn":41,"DataSegsOut":77017,"DeliveryRate":727588888,"BusyTime":6936000,"RWndLimited":5120000,"SndBufLimited":0,"Delivered":77015,"DeliveredCE":0,"BytesSent":5040056981,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":274816,"ElapsedTime":6936029}} 42 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2403043329,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":7228533},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":276,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":113,"RTTVar":7,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2973766119,"MaxPacingRate":-1,"BytesAcked":5243872792,"BytesReceived":1309,"SegsOut":80142,"SegsIn":37344,"NotsentBytes":3732531,"MinRTT":5,"DataSegsIn":42,"DataSegsOut":80137,"DeliveryRate":2317982300,"BusyTime":7228000,"RWndLimited":5352000,"SndBufLimited":0,"Delivered":80136,"DeliveredCE":0,"BytesSent":5244363941,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":336384,"ElapsedTime":7228533}} 43 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2640441030,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":7774128},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":532,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":117,"RTTVar":12,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3267545774,"MaxPacingRate":-1,"BytesAcked":5656612141,"BytesReceived":1334,"SegsOut":86443,"SegsIn":40274,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":43,"DataSegsOut":86438,"DeliveryRate":2112354838,"BusyTime":7776000,"RWndLimited":5792000,"SndBufLimited":0,"Delivered":86439,"DeliveredCE":0,"BytesSent":5656972324,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":19,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":7774128}} 44 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2317979364,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8012749},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":224,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":168,"RTTVar":35,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2868499464,"MaxPacingRate":-1,"BytesAcked":5831320785,"BytesReceived":1359,"SegsOut":89114,"SegsIn":41527,"NotsentBytes":3208667,"MinRTT":5,"DataSegsIn":44,"DataSegsOut":89109,"DeliveryRate":1371371727,"BusyTime":8012000,"RWndLimited":5956000,"SndBufLimited":0,"Delivered":89107,"DeliveredCE":0,"BytesSent":5831877417,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":20,"RcvOooPack":0,"SndWnd":205440,"ElapsedTime":8012749}} 45 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2355499778,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8077656},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":2,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":40,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":138,"RTTVar":38,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2914930975,"MaxPacingRate":-1,"BytesAcked":5866354190,"BytesReceived":1384,"SegsOut":89648,"SegsIn":41798,"NotsentBytes":3012218,"MinRTT":5,"DataSegsIn":45,"DataSegsOut":89643,"DeliveryRate":1853292452,"BusyTime":8080000,"RWndLimited":6016000,"SndBufLimited":0,"Delivered":89642,"DeliveredCE":0,"BytesSent":5866845339,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":20,"RcvOooPack":0,"SndWnd":274176,"ElapsedTime":8077656}} 46 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2578069636,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8703691},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":612,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":111,"RTTVar":10,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3190361175,"MaxPacingRate":-1,"BytesAcked":6361864051,"BytesReceived":1409,"SegsOut":97213,"SegsIn":45321,"NotsentBytes":2703990,"MinRTT":5,"DataSegsIn":46,"DataSegsOut":97208,"DeliveryRate":1888932692,"BusyTime":8704000,"RWndLimited":6512000,"SndBufLimited":0,"Delivered":97209,"DeliveredCE":0,"BytesSent":6362224234,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":23,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":8703691}} 47 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2798418636,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8731817},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":3,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":16,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":148,"RTTVar":72,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3463043062,"MaxPacingRate":-1,"BytesAcked":6381901849,"BytesReceived":1434,"SegsOut":97522,"SegsIn":45463,"NotsentBytes":3667048,"MinRTT":5,"DataSegsIn":47,"DataSegsOut":97517,"DeliveryRate":774946745,"BusyTime":8732000,"RWndLimited":6532000,"SndBufLimited":0,"Delivered":97515,"DeliveredCE":0,"BytesSent":6382458481,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":23,"RcvOooPack":0,"SndWnd":208000,"ElapsedTime":8731817}} 48 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2317979364,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8899514},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":52000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":152,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":118,"RTTVar":16,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2868499464,"MaxPacingRate":-1,"BytesAcked":6515880067,"BytesReceived":1459,"SegsOut":99565,"SegsIn":46431,"NotsentBytes":3667048,"MinRTT":5,"DataSegsIn":48,"DataSegsOut":99560,"DeliveryRate":1769810810,"BusyTime":8900000,"RWndLimited":6672000,"SndBufLimited":0,"Delivered":99561,"DeliveredCE":0,"BytesSent":6516240250,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":24,"RcvOooPack":0,"SndWnd":42496,"ElapsedTime":8899514}} 49 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2751386390,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":8988563},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":52000,"SndMSS":65483,"RcvMSS":536,"Unacked":1,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":72,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":113,"RTTVar":11,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3404840658,"MaxPacingRate":-1,"BytesAcked":6580773720,"BytesReceived":1484,"SegsOut":100557,"SegsIn":46895,"NotsentBytes":2553837,"MinRTT":5,"DataSegsIn":49,"DataSegsOut":100552,"DeliveryRate":2751386554,"BusyTime":8988000,"RWndLimited":6728000,"SndBufLimited":0,"Delivered":100552,"DeliveredCE":0,"BytesSent":6581199386,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":24,"RcvOooPack":0,"SndWnd":208640,"ElapsedTime":8988563}} 50 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2258031790,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":9614232},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":52000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":612,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":121,"RTTVar":22,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":2794314341,"MaxPacingRate":-1,"BytesAcked":7079754180,"BytesReceived":1509,"SegsOut":108176,"SegsIn":50439,"NotsentBytes":3536082,"MinRTT":5,"DataSegsIn":50,"DataSegsOut":108171,"DeliveryRate":2046343750,"BusyTime":9616000,"RWndLimited":7188000,"SndBufLimited":0,"Delivered":108172,"DeliveredCE":0,"BytesSent":7080114363,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":24,"RcvOooPack":0,"SndWnd":11648,"ElapsedTime":9614232}} 51 | {"ConnectionInfo":{"Client":"127.0.0.1:34140","Server":"127.0.0.1:80","UUID":"pbootheeu.c.googlers.com_1610929701_unsafe_000000000007FF98"},"BBRInfo":{"BW":2447960099,"MinRTT":5,"PacingGain":320,"CwndGain":512,"ElapsedTime":10000085},"TCPInfo":{"State":1,"CAState":0,"Retransmits":0,"Probes":0,"Backoff":0,"Options":7,"WScale":119,"AppLimited":0,"RTO":204000,"ATO":40000,"SndMSS":65483,"RcvMSS":536,"Unacked":0,"Sacked":0,"Lost":0,"Retrans":0,"Fackets":0,"LastDataSent":0,"LastAckSent":0,"LastDataRecv":372,"LastAckRecv":0,"PMTU":65535,"RcvSsThresh":65483,"RTT":107,"RTTVar":9,"SndSsThresh":22,"SndCwnd":14,"AdvMSS":65483,"Reordering":4,"RcvRTT":0,"RcvSpace":65483,"TotalRetrans":6,"PacingRate":3029350623,"MaxPacingRate":-1,"BytesAcked":7384904960,"BytesReceived":1534,"SegsOut":112836,"SegsIn":52587,"NotsentBytes":2750286,"MinRTT":5,"DataSegsIn":51,"DataSegsOut":112831,"DeliveryRate":2067884210,"BusyTime":10000000,"RWndLimited":7492000,"SndBufLimited":0,"Delivered":112832,"DeliveredCE":0,"BytesSent":7385265143,"BytesRetrans":360183,"DSackDups":6,"ReordSeen":25,"RcvOooPack":0,"SndWnd":9088,"ElapsedTime":10000085}} 52 | --------------------------------------------------------------------------------