├── .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 |
--------------------------------------------------------------------------------