├── .gitignore
├── COPYING
├── README.md
├── app.yaml
├── blip.js
├── index.html
└── qr.png
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .*~
3 | *.pyc
4 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | blip: a tool for seeing your Internet latency
2 | =============================================
3 | Tip:
4 | ----
5 |
6 | On your PC, laptop, tablet, phone, or iPod, try adding a
7 | bookmark to http://gfblip.appspot.com/ to your home screen
8 | for easy access.
9 |
10 |
11 |
12 |
13 | Too long, don't read:
14 | ---------------------
15 |
16 | - Go to http://gfblip.appspot.com/
17 |
18 | - It should work on any PC, laptop, tablet, phone, or iPod
19 | with javascript and HTML canvas support (which means
20 | almost everything nowadays).
21 |
22 | - X axis is time. Y axis is milliseconds of latency.
23 |
24 | - Green blips are your ping time to gstatic.com (a very
25 | fast site that should be close to you wherever you are).
26 |
27 | - Blue blips are your ping time to apenwarr.ca ("a site on
28 | the Internet"). It should be slower than gstatic.com.
29 | How much slower depends on how lucky you are.
30 |
31 | - Red blips mean something sucks.
32 |
33 | - A good Internet+Wifi connection should have no red
34 | blips. And lower latency is better than higher latency.
35 |
36 | - We send blips out as fast as they come back, up to 100
37 | per second, so you can notice very small variations.
38 |
39 | - If you watch the blip output while you do different
40 | things (switch wifi networks, start Youtube videos
41 | playing, walk around), you can immediately see what
42 | impact that change has on the quality of your Internet
43 | connection.
44 |
45 |
46 | Even longer, don't read:
47 | ------------------------
48 |
49 | People think more bandwidth will make your Internet
50 | connection seem faster, but that isn't even close to the
51 | whole story. There are three interrelated things you need
52 | to care about:
53 |
54 | - bandwidth
55 | - latency
56 | - packet loss.
57 |
58 | Bandwidth means, once things get going, how fast you can
59 | download. But "once things get going" can take a really
60 | long time. In fact, it can take longer than the whole
61 | download! This is especially true for simple web pages, or
62 | web pages made up of a bunch of tiny pieces, which is very
63 | common on today's web.
64 |
65 | That's where latency comes in. Latency is the time it
66 | takes to make a round trip to the server. Really good web
67 | designers know how to minimize the number of round trips,
68 | or at least do more round trips at the same time - which
69 | makes their pages load faster on everyone's connection.
70 | But every web page, whether optimized or not, automatically
71 | benefits pretty much proportionally to your network
72 | latency. Cut latency in half, and most pages will load
73 | about twice as fast.
74 |
75 | Packet loss is the third component, and it's often
76 | forgotten. If you run the 'ping' program, which most
77 | people don't do and which is hard or impossible to do from
78 | many modern Internet devices (phones, tablets, etc), it
79 | will show you how many packets are dropped, and how many
80 | got through. Unfortunately, most people don't run ping
81 | more than once per second, which gives a pretty low
82 | resolution; if you have a really brief outage, you might
83 | not even see it with ping. Plus, on the modern Internet,
84 | packet loss is hard to measure - you can't do it with a web
85 | browser. And it's not that useful anyway, since real web
86 | pages don't see "packet loss." On the web (and any
87 | TCP-based protocol), packet loss translates into packet
88 | retransmissions, which means latency in some cases is 2, 3,
89 | or more times higher than usual. If you have significant
90 | packet loss (say, 1% or more), your web performance will
91 | totally suck eggs even if your bandwidth and latency are
92 | both fantastic.
93 |
94 | Blip is an end-to-end testing tool designed to let you
95 | measure the latter two elements: latency and packet loss.
96 | These are the real indicators of your web browsing
97 | performance. It doesn't attempt to measure bandwidth; for
98 | that there's always good old http://speedtest.net/. (By
99 | the way, next time you're visiting speedtest.net, watch how
100 | the "download speedometer" dial starts off low and increases
101 | over time. That's what I mean when I say you might be done
102 | downloading by the time "things get going.")
103 |
104 | How is blip an end-to-end tool? Simple. It's written in
105 | pure javascript, so it runs purely in your browser, without
106 | needing a server-side component. It makes real requests to
107 | real http servers, rather than using synthetic "ping"
108 | packets. Then it measures the turnaround time on those
109 | requests and plots them on a graph. And it does this up to
110 | 100 times per second, so you can see your network quality
111 | in high resolution. It's the next best thing to actually
112 | browsing the web, except you get a pretty graph instead of
113 | "hmm, that page loaded kinda slowly today."
114 |
115 | Blip doesn't attempt to interpret the results for you; it
116 | just makes the plot in real time. If you try experimenting
117 | with it in a few different conditions, you can get an
118 | intuitive feel for what those conditions mean to the graph -
119 | and the graph can give you an intuitive feel for how
120 | sucky your web browsing performance will be under those
121 | conditions.
122 |
123 | Here are some observations I've made using blip:
124 |
125 | - If there's a red blip more than once every minute or so,
126 | your web browsing will be noticeably more annoying than
127 | if there isn't. Yes, real wifi connections exist, even
128 | in crowded buildings, with *zero* red blips. That
129 | should be your goal.
130 |
131 | - One of my tablet devices produces red blips even when
132 | another device, sitting right next to it, on the same
133 | access point, does not.
134 |
135 | - Some wifi routers give decent speedtest.net results most
136 | of the time, and terrible speedtest.net results other
137 | times. This is usually because they get angry and start
138 | losing packets at random times, which is easy to see with blip.
139 |
140 | - You can walk around your apartment or house and find out
141 | exactly where the wifi "dead zones" are, within seconds.
142 | It's kind of like a geiger counter; wave it around and
143 | see what happens to the clicks.
144 |
145 | - "Wifi signal strength" meters are all a bunch of evil
146 | liars. Don't trust them. Wondering why your Internet
147 | is slow even with 5 bars? Don't believe the hype. Blip
148 | will show you the truth.
149 |
150 | - For me, on a wired ethernet network I can get about 15ms
151 | (or less from some locations) green blips. On wifi it's more
152 | like 30-50ms. On 3G cellular, with a really good signal
153 | (eg. outdoors with no obstructions) the best I get is
154 | about 100ms. With obstructions, it's normally more like
155 | 200ms. And yes, the ratios between these numbers really
156 | do seem like the performance difference I see when web
157 | browsing.
158 |
159 | - 3G cellular networks, surprisingly, seem to have far
160 | fewer red blips than typical "public" wifi signals (eg.
161 | ones in malls, parks, etc), even in moderately crowded
162 | area. So even though the latency ("ping time") might
163 | look better on public wifi, the packet loss (shown as a
164 | nonzero number of red blips) means web browsing will be
165 | cruddy. That matches my experience, but now I can
166 | measure it for real.
167 |
168 |
169 | The Stupid Part
170 | ---------------
171 |
172 | So you might be wondering, hey, how did you make a
173 | javascript applet ping these arbitrary servers? What about
174 | cross-domain request protection?
175 |
176 | Answer: I did it by just making the queries anyway, and
177 | seeing how long it takes to get the error message back that
178 | my request was refused because of cross-domain request
179 | protection. Yes, this results in an infinite number of
180 | error messages to your javascript console. Don't look at your
181 | javascript console and you'll be fine. Trust me on this.
182 |
183 |
184 | The Fiddly Bits
185 | ---------------
186 |
187 | blip is open source software released under the Apache
188 | license. See the file COPYING and comments inside the code
189 | for more details.
190 |
191 | You can get the source code at: http://github.com/apenwarr/blip
192 |
193 | If you want to discuss this tool, you can join the
194 | blip-users@googlegroups.com mailing list. You don't need a
195 | Google Account to subscribe! Just send an email to
196 | blip-users+subscribe@googlegroups.com and you can join.
197 |
198 | blip probably needs lots of fancy new features. It's the
199 | first program I've ever written using HTML Canvas for
200 | display (which is really fun), so I probably did some dumb
201 | things. Javascript is also not my first choice of
202 | programming language (yet?) so I probably did some even dumber
203 | things. Send pull requests. That is all.
204 |
205 | -- apenwarr
206 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: python312
2 |
3 | handlers:
4 | - url: /blip.js
5 | static_files: blip.js
6 | upload: blip.js
7 |
8 | - url: /
9 | static_files: index.html
10 | upload: index.html
11 |
--------------------------------------------------------------------------------
/blip.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Google Inc. All Rights Reserved.
3 | * Copyright 2013 Avery Pennarun. All Rights Reserved.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | 'use strict';
18 |
19 | const localColor = 'rgba(0,128,0,0.8)';
20 | const internetColor = 'rgba(0,0,255,0.8)';
21 | const dnsColor = 'rgba(0,0,0,1.0)';
22 |
23 | const vbarColor = 'rgba(128,128,128,1.0)';
24 | const vbarEraseColor = 'rgba(255,255,255,1.0)'
25 |
26 | let running = true;
27 | let wantDns = false;
28 | let dnsName;
29 |
30 | let now;
31 | if (window.performance && window.performance.now) {
32 | now = function() { return window.performance.now(); };
33 | } else {
34 | now = function() { return new Date().getTime(); };
35 | }
36 |
37 | // Sigh, not all browsers have Object.values yet. Use the shim
38 | // unconditionally to avoid any obscure incompatibilities.
39 | function getValues(obj) {
40 | let out = [];
41 | for (let i in obj) {
42 | out.push(obj[i]);
43 | }
44 | return out;
45 | };
46 |
47 | let bestNextFrame =
48 | window.requestAnimationFrame ||
49 | window.webkitRequestAnimationFrame ||
50 | window.mozRequestAnimationFrame ||
51 | function(callback) {
52 | setTimeout(callback, 1000 / 60);
53 | };
54 | const brokenRTT = 1e6; // RTT that means we didn't get valid answer
55 | const minReasonableRTT = 2; // browser might reject invalid URLs in < this time
56 | const msecMax = 2200; // max timeout that fits on the chart
57 | const log_msecMax = Math.log(msecMax * 1.5);
58 | const absolute_mindelay = 10;
59 | let mindelay = absolute_mindelay;
60 | let lastBotch = 0;
61 |
62 | // constructor
63 | function BlipCanvas(canvas, width) {
64 | this.canvas = canvas;
65 | this.canvas.width = 1000;
66 | this.canvas.height = 1000;
67 | this.ctx = this.canvas.getContext('2d');
68 | this.xofs = 100;
69 | this.current_x = 0;
70 | this.xdiv = width / 1000;
71 |
72 | this.drawYAxis = function() {
73 | let labels = [2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000];
74 | this.ctx.fillStyle = 'black';
75 | this.ctx.textBaseline = 'middle';
76 | this.ctx.textAlign = 'right';
77 | this.ctx.font = '32px Arial';
78 | for (let i = 0; i < labels.length; i++) {
79 | let msecs = labels[i];
80 | this.ctx.fillText(msecs, (this.xofs - 10), this.msecToY(msecs));
81 | }
82 | this.ctx.scale(1, 2);
83 | this.ctx.textAlign = 'center';
84 | this.ctx.rotate(-Math.PI / 2);
85 | this.ctx.font = '24px Arial';
86 | this.ctx.fillText('milliseconds', -this.canvas.height / 2 / 2, 16);
87 | this.ctx.rotate(Math.PI / 2);
88 | this.ctx.scale(1, 1 / 2);
89 | };
90 |
91 | this.nextX = function(msecs) {
92 | let steps = msecs / absolute_mindelay;
93 | if (steps > 100) {
94 | steps = 100;
95 | }
96 | let x_inc = steps / this.xdiv;
97 | let new_x = (this.current_x + x_inc) % (this.canvas.width - this.xofs);
98 |
99 | // draw the new bar
100 | this.ctx.fillStyle = vbarColor;
101 | this.ctx.fillRect(new_x + this.xofs + 1, 0,
102 | 4, this.canvas.height);
103 |
104 | // wipe out the old bar
105 | this.ctx.fillStyle = vbarEraseColor;
106 | this.ctx.fillRect(this.current_x + this.xofs, 0,
107 | x_inc + 1, this.canvas.height);
108 | if (new_x < this.current_x) {
109 | this.ctx.fillRect(this.xofs, 0,
110 | new_x - 1, this.canvas.height);
111 | }
112 | this.current_x = new_x;
113 | };
114 |
115 | this.msecToY = function(msecs) {
116 | return this.canvas.height -
117 | (Math.log(msecs) * this.canvas.height / log_msecMax);
118 | };
119 |
120 | this.drawBlip = function(color, startTime, endTime, minlatency, width) {
121 | let msecs = endTime - startTime;
122 | if (msecs < minlatency) {
123 | // impossibly short; that implies we're not actually reaching the
124 | // remote end, probably because we're entirely offline
125 | lastBotch = endTime;
126 | }
127 | if (endTime > 2100 && endTime - lastBotch < 2100) {
128 | // if there were any "offline" problems recently, there might be
129 | // a bit of jitter where some of the requests are a bit slower than
130 | // the impossible timeout, but that doesn't mean it's working yet.
131 | // So stop reporting for a minimum amount of time. During that time,
132 | // we just want to show an error.
133 | msecs = msecMax;
134 | }
135 | let y = this.msecToY(msecs);
136 | let x = this.current_x + this.xofs;
137 | this.ctx.fillStyle = color;
138 | this.ctx.fillRect(x - width, y - 15, 1 + width, 31);
139 | if (msecs >= msecMax) {
140 | this.ctx.fillStyle = '#f00';
141 | this.ctx.fillRect(x - 1 - width, y - 5, 2 + width, 20);
142 | }
143 | };
144 | };
145 |
146 | const c1 = new BlipCanvas($('#hires')[0], 1000);
147 | const c2 = new BlipCanvas($('#lores')[0], 10000);
148 | const c3 = new BlipCanvas($('#vlores')[0], 100000);
149 |
150 | const blips = [];
151 |
152 | function addBlip(color, url, minlatency) {
153 | blips.push({color: color, url: url, minlatency: minlatency});
154 | };
155 |
156 | function gotBlip(color, url, minlatency, startTime) {
157 | const endTime = now();
158 | const blipWidth = url ? 1 : 3;
159 | c1.drawBlip(color, startTime, endTime, minlatency, blipWidth);
160 | c2.drawBlip(color, startTime, endTime, minlatency, blipWidth);
161 | c3.drawBlip(color, startTime, endTime, minlatency, blipWidth);
162 | addBlip(color, url, minlatency);
163 | };
164 |
165 | if (AbortSignal.timeout === undefined) {
166 | const controller = new AbortController();
167 | const signal = controller.signal;
168 | AbortSignal.timeout = function(msec) {
169 | setTimeout(() => controller.abort(), msec);
170 | return signal;
171 | }
172 | }
173 |
174 | function startFetch(url, msecTimeout) {
175 | return fetch(url, {
176 | method: 'HEAD',
177 | mode: 'no-cors',
178 | cache: 'no-cache',
179 | priority: 'high',
180 | signal: AbortSignal.timeout(msecTimeout),
181 | });
182 | }
183 |
184 | function startBlips() {
185 | while (blips.length) {
186 | let blip = blips.shift();
187 | if (!blip.url && !wantDns) {
188 | let createResult = function(blip) {
189 | return function() {
190 | addBlip(blip.color, blip.url, blip.minlatency);
191 | }
192 | };
193 | let result = createResult(blip);
194 | setTimeout(result, 1000);
195 | } else {
196 | let createResult = function(blip) {
197 | let startTime = now();
198 | let result = function() {
199 | gotBlip(blip.color, blip.url, blip.minlatency, startTime);
200 | };
201 | return result;
202 | };
203 | let result = createResult(blip);
204 | let url = blip.url;
205 | if (!blip.url) {
206 | // Desired URL format:
207 | // https://x..blipdns.apenwarr.ca:
208 | url = 'https://' +
209 | 'x' + Math.floor(Math.random()*1e9) +
210 | '.' + dnsName +
211 | '.blipdns.apenwarr.ca:8999';
212 | }
213 |
214 | startFetch(url, msecMax).then(result, result);
215 | }
216 | }
217 | };
218 |
219 | let lastTick = now(), lastStart = lastTick;
220 | function gotTick() {
221 | let t = now();
222 | let tdiff = t - lastTick;
223 | if (running) {
224 | if (tdiff >= absolute_mindelay) {
225 | if (t - lastStart > mindelay) {
226 | lastStart = t;
227 | startBlips();
228 | }
229 | c1.nextX(tdiff);
230 | c2.nextX(tdiff);
231 | c3.nextX(tdiff);
232 | lastTick = t;
233 | }
234 | bestNextFrame(gotTick);
235 | }
236 | };
237 |
238 | function toggleBlip() {
239 | if (running) {
240 | running = 0;
241 | } else {
242 | running = 1;
243 | lastTick = now();
244 | bestNextFrame(gotTick);
245 | }
246 | };
247 |
248 | function toggleDns() {
249 | wantDns = dnsName && !wantDns;
250 | };
251 |
252 | async function pickBestSite(hosts, minNeeded, maxParallel) {
253 | let hostsFinished = 0;
254 | let outstanding = 0;
255 | let promiseDone;
256 | let promise = new Promise((resolve, reject) => { promiseDone = resolve; });
257 | let queue = [];
258 |
259 | for (let h of hosts) {
260 | h.minRTT = brokenRTT;
261 | h.nProbes = 0;
262 | }
263 |
264 | let runTest = async function(h) {
265 | const startTime = now();
266 | outstanding++;
267 | try {
268 | await startFetch(h.url, msecMax * 1.5);
269 | }
270 | catch (e) {
271 | const rtt = now() - startTime;
272 | if (!(e instanceof TypeError)) {
273 | // timed out, or some non-network error
274 | console.log('Server check failed:', h, e);
275 | if (queue.length > 0) {
276 | return runTest(queue.shift());
277 | }
278 | // (don't increment hostsFinished here, since although this host is
279 | // "finished" it didn't give valid results.)
280 | outstanding--;
281 | if (outstanding==0) {
282 | promiseDone();
283 | }
284 | return;
285 | }
286 | // otherwise fall through and consider the probe to have passed
287 | }
288 | outstanding--;
289 |
290 | const rtt = now() - startTime;
291 | if (rtt < h.minRTT && rtt >= minReasonableRTT) {
292 | h.minRTT = rtt;
293 | }
294 | h.nProbes++;
295 |
296 | // try each host 3 times
297 | if (h.nProbes < 3) {
298 | queue.push(h);
299 | } else {
300 | if (h.minRTT < brokenRTT) {
301 | hostsFinished++;
302 | if (hostsFinished <= minNeeded) {
303 | console.log('Tested #' + hostsFinished +
304 | ' (target ' + minNeeded + '):',
305 | Math.round(h.minRTT) + 'ms',
306 | h.label, 'qlen', queue.length, 'outstanding', outstanding);
307 | }
308 | } else {
309 | console.log('Tested #(' + hostsFinished +
310 | ') (target ' + minNeeded + '):',
311 | '(too fast)' + 'ms',
312 | h.label, 'qlen', queue.length, 'outstanding', outstanding);
313 | }
314 | if (hostsFinished == minNeeded || outstanding == 0) {
315 | promiseDone();
316 | }
317 | }
318 |
319 | if (queue.length > 0 && hostsFinished < minNeeded) {
320 | runTest(queue.shift());
321 | }
322 | }
323 |
324 | for (let hi in hosts) {
325 | queue.push(hosts[hi]);
326 | }
327 |
328 | // start the first few tests in parallel
329 | for (let i = 0; i < maxParallel && queue.length > 0; i++) {
330 | runTest(queue.shift());
331 | }
332 |
333 | await promise;
334 |
335 | // when done enough of them...
336 | hosts.sort((a, b) => (a.minRTT - b.minRTT));
337 | console.log('pickBest', hosts);
338 |
339 | return hosts;
340 | }
341 |
342 | // Pick an Internet site with reasonably high latency (at least, higher than
343 | // the usually-very-low-latency gstatic.com) to add contrast to the graph.
344 | //
345 | // Nobody really cares about apenwarr.ca, which is just hosted on a cheap
346 | // VPS somewhere. If you overload it, I guess I'll be sort of impressed
347 | // that you like my program. So, you know, whatever.
348 | // -- apenwarr, 2013/04/26
349 | async function pickMlabSite() {
350 | let response = await fetch('https://mlab-ns.appspot.com/ndt_ssl?policy=all');
351 |
352 | // We want the selected hostname to be reasonably stable across page reloads
353 | // from a single location, even on separate devices. To help with this,
354 | // choose only from the first server in the first city in each country.
355 | // The latency between countries is hopefully different enough that there
356 | // should be little jitter.
357 | if (!response.ok) {
358 | console.error('m-lab response error:', response);
359 | return;
360 | }
361 |
362 | let ndt = await response.json();
363 | console.log('m-lab index:', ndt);
364 |
365 | let allHosts = [];
366 | for (let n of ndt) {
367 | // put the sort keys in the desired order
368 | allHosts.push([n.country, n.city, n.site, n.fqdn, n]);
369 | }
370 | allHosts.sort();
371 |
372 | let hosts = {};
373 | for (let i in allHosts) {
374 | let h = allHosts[i][4];
375 | let k = h.country + ' ' + h.city;
376 | if (!hosts[k]) {
377 | hosts[k] = {
378 | label: h.city + ', ' + h.country,
379 | url: 'https://' + h.ip[0],
380 | site: h.site,
381 | };
382 | }
383 | }
384 |
385 | // convert dict back into a list
386 | hosts = getValues(hosts);
387 |
388 | const need1 = Math.floor(Object.keys(hosts).length / 4), need2 = 10;
389 | const minNeeded = need1 < need2 ? need2 : need1;
390 | const maxParallel = 20;
391 |
392 | let results = await pickBestSite(hosts, minNeeded, maxParallel);
393 |
394 | // Pick a desirable entry.
395 | // The "fastest" server seems like an obvious choice, but it might not
396 | // test the "real" long-distance Internet link quality, so we opt for
397 | // something a little further away.
398 | let best = results[1];
399 | dnsName = best.site;
400 | $('#internetlegend').html('❚ ' + best.label);
401 | addBlip(internetColor, best.url, 5);
402 | }
403 |
404 | async function pickLocalSite() {
405 | let tryFastSites = [
406 | '192.168.0.1',
407 | '192.168.1.1',
408 | '192.168.2.1',
409 | '192.168.3.1',
410 | '192.168.4.1',
411 | '10.0.0.1',
412 | '10.1.1.1',
413 | 'gstatic.com',
414 | ];
415 |
416 | // Add the internet-facing client IP address (ie. after NATting).
417 | // This is not quite as good as the local router IP, but seems to
418 | // generally work pretty well as long as your ISP gives Connection Refused
419 | // instead of blackholing TCP connection requests.
420 | let cipr = await (await fetch('https://apenwarr.ca/blip/clientip')).json();
421 | let cip = cipr['client_ip'];
422 | console.log('clientip', cip);
423 | tryFastSites.unshift(cip);
424 |
425 | let hosts = [];
426 | for (let s of tryFastSites) {
427 | hosts.push({
428 | label: s,
429 | url: (s=='gstatic.com'
430 | ? 'https://' + s + '/generate_204'
431 | : 'https://' + s + ':8999/generate_204'),
432 | });
433 | }
434 |
435 | const minNeeded = 1, maxParallel = 20;
436 |
437 | let results = await pickBestSite(hosts, minNeeded, maxParallel);
438 |
439 | let fastest = results[0];
440 | $('#locallegend').html('❚ ' + fastest.label);
441 | addBlip(localColor, fastest.url, 0);
442 | }
443 |
444 | async function start() {
445 | c1.drawYAxis();
446 |
447 | // This one uses apenwarr.ca by default. Please be polite if modifying
448 | // blip to send a lot more traffic than usual.
449 | addBlip(dnsColor, null, 5);
450 |
451 | // this starts the polling and animation
452 | bestNextFrame(gotTick);
453 |
454 | // this will async add the "local-ish" blip
455 | await pickLocalSite();
456 |
457 | // this will async add the "Internet" blip
458 | await pickMlabSite();
459 | }
460 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Blip
5 |
40 |
41 |
42 |
55 |
56 |
57 |
58 |
59 |
60 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apenwarr/blip/d85ea18e0cf9f6bb1f092bc58d683a0ab2bcb087/qr.png
--------------------------------------------------------------------------------