├── .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 |
43 | What's this? 44 | 45 | 46 | 47 |
48 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apenwarr/blip/d85ea18e0cf9f6bb1f092bc58d683a0ab2bcb087/qr.png --------------------------------------------------------------------------------