Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(223)

Side by Side Diff: net/base/network_change_notifier.cc

Issue 11360108: Start calculating new combined NetworkChangeNotifier signal (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address szym's first round of comments Created 8 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "net/base/network_change_notifier.h" 5 #include "net/base/network_change_notifier.h"
6 6
7 #include "base/metrics/histogram.h" 7 #include "base/metrics/histogram.h"
8 #include "base/synchronization/lock.h" 8 #include "base/synchronization/lock.h"
9 #include "build/build_config.h" 9 #include "build/build_config.h"
10 #include "googleurl/src/gurl.h" 10 #include "googleurl/src/gurl.h"
(...skipping 17 matching lines...) Expand all
28 // in ways that would require us to place locks around access to this object. 28 // in ways that would require us to place locks around access to this object.
29 // (The prohibition on global non-POD objects makes it tricky to do such a thing 29 // (The prohibition on global non-POD objects makes it tricky to do such a thing
30 // anyway.) 30 // anyway.)
31 NetworkChangeNotifier* g_network_change_notifier = NULL; 31 NetworkChangeNotifier* g_network_change_notifier = NULL;
32 32
33 // Class factory singleton. 33 // Class factory singleton.
34 NetworkChangeNotifierFactory* g_network_change_notifier_factory = NULL; 34 NetworkChangeNotifierFactory* g_network_change_notifier_factory = NULL;
35 35
36 class MockNetworkChangeNotifier : public NetworkChangeNotifier { 36 class MockNetworkChangeNotifier : public NetworkChangeNotifier {
37 public: 37 public:
38 MockNetworkChangeNotifier()
39 : NetworkChangeNotifier(NetworkChangeCalculatorParams()) {}
38 virtual ConnectionType GetCurrentConnectionType() const OVERRIDE { 40 virtual ConnectionType GetCurrentConnectionType() const OVERRIDE {
39 return CONNECTION_UNKNOWN; 41 return CONNECTION_UNKNOWN;
40 } 42 }
41 }; 43 };
42 44
43 } // namespace 45 } // namespace
44 46
45 // The main observer class that records UMAs for network events. 47 // The main observer class that records UMAs for network events.
46 class HistogramWatcher 48 class HistogramWatcher
47 : public NetworkChangeNotifier::ConnectionTypeObserver, 49 : public NetworkChangeNotifier::ConnectionTypeObserver,
48 public NetworkChangeNotifier::IPAddressObserver, 50 public NetworkChangeNotifier::IPAddressObserver,
49 public NetworkChangeNotifier::DNSObserver { 51 public NetworkChangeNotifier::DNSObserver,
52 public NetworkChangeNotifier::NetworkChangeObserver {
50 public: 53 public:
51 HistogramWatcher() 54 HistogramWatcher()
52 : last_ip_address_change_(base::TimeTicks::Now()), 55 : last_ip_address_change_(base::TimeTicks::Now()),
53 last_connection_change_(base::TimeTicks::Now()), 56 last_connection_change_(base::TimeTicks::Now()),
54 last_dns_change_(base::TimeTicks::Now()), 57 last_dns_change_(base::TimeTicks::Now()),
58 last_network_change_(base::TimeTicks::Now()),
55 last_connection_type_(NetworkChangeNotifier::CONNECTION_UNKNOWN), 59 last_connection_type_(NetworkChangeNotifier::CONNECTION_UNKNOWN),
56 offline_packets_received_(0) {} 60 offline_packets_received_(0) {}
57 61
58 // Registers our three Observer implementations. This is called from the 62 // Registers our three Observer implementations. This is called from the
59 // network thread so that our Observer implementations are also called 63 // network thread so that our Observer implementations are also called
60 // from the network thread. This avoids multi-threaded race conditions 64 // from the network thread. This avoids multi-threaded race conditions
61 // because the only other interface, |NotifyDataReceived| is also 65 // because the only other interface, |NotifyDataReceived| is also
62 // only called from the network thread. 66 // only called from the network thread.
63 void Init() { 67 void Init() {
64 NetworkChangeNotifier::AddConnectionTypeObserver(this); 68 NetworkChangeNotifier::AddConnectionTypeObserver(this);
65 NetworkChangeNotifier::AddIPAddressObserver(this); 69 NetworkChangeNotifier::AddIPAddressObserver(this);
66 NetworkChangeNotifier::AddDNSObserver(this); 70 NetworkChangeNotifier::AddDNSObserver(this);
71 NetworkChangeNotifier::AddNetworkChangeObserver(this);
67 } 72 }
68 73
69 virtual ~HistogramWatcher() {} 74 virtual ~HistogramWatcher() {}
70 75
71 // NetworkChangeNotifier::IPAddressObserver implementation. 76 // NetworkChangeNotifier::IPAddressObserver implementation.
72 virtual void OnIPAddressChanged() OVERRIDE { 77 virtual void OnIPAddressChanged() OVERRIDE {
73 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.IPAddressChange", 78 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.IPAddressChange",
74 SinceLast(&last_ip_address_change_)); 79 SinceLast(&last_ip_address_change_));
80 UMA_HISTOGRAM_MEDIUM_TIMES(
81 "NCN.ConnectionTypeChangeToIPAddressChange",
82 last_ip_address_change_ - last_connection_change_);
75 } 83 }
76 84
77 // NetworkChangeNotifier::ConnectionTypeObserver implementation. 85 // NetworkChangeNotifier::ConnectionTypeObserver implementation.
78 virtual void OnConnectionTypeChanged( 86 virtual void OnConnectionTypeChanged(
79 NetworkChangeNotifier::ConnectionType type) OVERRIDE { 87 NetworkChangeNotifier::ConnectionType type) OVERRIDE {
80 if (type != NetworkChangeNotifier::CONNECTION_NONE) { 88 if (type != NetworkChangeNotifier::CONNECTION_NONE) {
81 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OnlineChange", 89 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OnlineChange",
82 SinceLast(&last_connection_change_)); 90 SinceLast(&last_connection_change_));
83 91
84 if (offline_packets_received_) { 92 if (offline_packets_received_) {
85 if ((last_connection_change_ - last_offline_packet_received_) < 93 if ((last_connection_change_ - last_offline_packet_received_) <
86 base::TimeDelta::FromSeconds(5)) { 94 base::TimeDelta::FromSeconds(5)) {
87 // We can compare this sum with the sum of NCN.OfflineDataRecv. 95 // We can compare this sum with the sum of NCN.OfflineDataRecv.
88 UMA_HISTOGRAM_COUNTS_10000( 96 UMA_HISTOGRAM_COUNTS_10000(
89 "NCN.OfflineDataRecvAny5sBeforeOnline", 97 "NCN.OfflineDataRecvAny5sBeforeOnline",
90 offline_packets_received_); 98 offline_packets_received_);
91 } 99 }
92 100
93 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OfflineDataRecvUntilOnline", 101 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OfflineDataRecvUntilOnline",
94 last_connection_change_ - 102 last_connection_change_ -
95 last_offline_packet_received_); 103 last_offline_packet_received_);
96 } 104 }
97 } else { 105 } else {
98 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OfflineChange", 106 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.OfflineChange",
99 SinceLast(&last_connection_change_)); 107 SinceLast(&last_connection_change_));
100 } 108 }
109 UMA_HISTOGRAM_MEDIUM_TIMES(
110 "NCN.IPAddressChangeToConnectionTypeChange",
111 last_connection_change_ - last_ip_address_change_);
101 112
102 offline_packets_received_ = 0; 113 offline_packets_received_ = 0;
103 last_connection_type_ = type; 114 last_connection_type_ = type;
104 polling_interval_ = base::TimeDelta::FromSeconds(1); 115 polling_interval_ = base::TimeDelta::FromSeconds(1);
105 } 116 }
106 117
107 // NetworkChangeNotifier::DNSObserver implementation. 118 // NetworkChangeNotifier::DNSObserver implementation.
108 virtual void OnDNSChanged() OVERRIDE { 119 virtual void OnDNSChanged() OVERRIDE {
109 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.DNSConfigChange", 120 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.DNSConfigChange",
110 SinceLast(&last_dns_change_)); 121 SinceLast(&last_dns_change_));
111 } 122 }
112 123
124 // NetworkChangeNotifier::NetworkChangeObserver implementation.
125 virtual void OnNetworkChanged(
126 NetworkChangeNotifier::ConnectionType type) OVERRIDE {
127 if (type != NetworkChangeNotifier::CONNECTION_NONE) {
128 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.NetworkOnlineChange",
129 SinceLast(&last_network_change_));
130 } else {
131 UMA_HISTOGRAM_MEDIUM_TIMES("NCN.NetworkOfflineChange",
132 SinceLast(&last_network_change_));
133 }
134 }
135
113 // Record histogram data whenever we receive a packet but think we're 136 // Record histogram data whenever we receive a packet but think we're
114 // offline. Should only be called from the network thread. 137 // offline. Should only be called from the network thread.
115 void NotifyDataReceived(const GURL& source) { 138 void NotifyDataReceived(const GURL& source) {
116 if (last_connection_type_ != NetworkChangeNotifier::CONNECTION_NONE || 139 if (last_connection_type_ != NetworkChangeNotifier::CONNECTION_NONE ||
117 IsLocalhost(source.host()) || 140 IsLocalhost(source.host()) ||
118 !(source.SchemeIs("http") || source.SchemeIs("https"))) { 141 !(source.SchemeIs("http") || source.SchemeIs("https"))) {
119 return; 142 return;
120 } 143 }
121 144
122 base::TimeTicks current_time = base::TimeTicks::Now(); 145 base::TimeTicks current_time = base::TimeTicks::Now();
(...skipping 23 matching lines...) Expand all
146 static base::TimeDelta SinceLast(base::TimeTicks *last_time) { 169 static base::TimeDelta SinceLast(base::TimeTicks *last_time) {
147 base::TimeTicks current_time = base::TimeTicks::Now(); 170 base::TimeTicks current_time = base::TimeTicks::Now();
148 base::TimeDelta delta = current_time - *last_time; 171 base::TimeDelta delta = current_time - *last_time;
149 *last_time = current_time; 172 *last_time = current_time;
150 return delta; 173 return delta;
151 } 174 }
152 175
153 base::TimeTicks last_ip_address_change_; 176 base::TimeTicks last_ip_address_change_;
154 base::TimeTicks last_connection_change_; 177 base::TimeTicks last_connection_change_;
155 base::TimeTicks last_dns_change_; 178 base::TimeTicks last_dns_change_;
179 base::TimeTicks last_network_change_;
156 base::TimeTicks last_offline_packet_received_; 180 base::TimeTicks last_offline_packet_received_;
157 base::TimeTicks last_polled_connection_; 181 base::TimeTicks last_polled_connection_;
158 // |polling_interval_| is initialized by |OnConnectionTypeChanged| on our 182 // |polling_interval_| is initialized by |OnConnectionTypeChanged| on our
159 // first transition to offline and on subsequent transitions. Once offline, 183 // first transition to offline and on subsequent transitions. Once offline,
160 // |polling_interval_| doubles as offline data is received and we poll 184 // |polling_interval_| doubles as offline data is received and we poll
161 // with |NetworkChangeNotifier::GetConnectionType| to verify the connection 185 // with |NetworkChangeNotifier::GetConnectionType| to verify the connection
162 // state. 186 // state.
163 base::TimeDelta polling_interval_; 187 base::TimeDelta polling_interval_;
164 // |last_connection_type_| is the last value passed to 188 // |last_connection_type_| is the last value passed to
165 // |OnConnectionTypeChanged|. 189 // |OnConnectionTypeChanged|.
(...skipping 20 matching lines...) Expand all
186 void SetDnsConfig(const DnsConfig& dns_config) { 210 void SetDnsConfig(const DnsConfig& dns_config) {
187 base::AutoLock lock(lock_); 211 base::AutoLock lock(lock_);
188 dns_config_ = dns_config; 212 dns_config_ = dns_config;
189 } 213 }
190 214
191 private: 215 private:
192 mutable base::Lock lock_; 216 mutable base::Lock lock_;
193 DnsConfig dns_config_; 217 DnsConfig dns_config_;
194 }; 218 };
195 219
220 NetworkChangeNotifier::NetworkChangeCalculatorParams::
221 NetworkChangeCalculatorParams() {
222 }
223
224 // Calculates NetworkChange signal from IPAddress and ConnectionType signals.
225 class NetworkChangeNotifier::NetworkChangeCalculator
226 : public ConnectionTypeObserver,
227 public IPAddressObserver {
228 public:
229 NetworkChangeCalculator(const NetworkChangeCalculatorParams& params)
230 : params_(params),
231 have_announced_(false),
232 last_announced_connection_type_(CONNECTION_NONE),
233 pending_connection_type_(CONNECTION_NONE) {}
234 void Init() {
szym 2012/11/27 22:22:05 nit: add blank line
235 AddConnectionTypeObserver(this);
236 AddIPAddressObserver(this);
szym 2012/11/27 22:22:05 This makes it so that NetworkChangeCalculator will
237 }
szym 2012/11/27 22:22:05 nit: add blank line
238 virtual ~NetworkChangeCalculator() {
239 RemoveConnectionTypeObserver(this);
240 RemoveIPAddressObserver(this);
241 }
242
243 // NetworkChangeNotifier::IPAddressObserver implementation.
244 virtual void OnIPAddressChanged() OVERRIDE {
245 base::TimeDelta delay = last_announced_connection_type_ == CONNECTION_NONE
246 ? params_.ip_address_offline_delay_ : params_.ip_address_online_delay_;
247 // Cancels any previous timer.
248 timer_.Start(FROM_HERE, delay, this, &NetworkChangeCalculator::Notify);
249 }
250
251 // NetworkChangeNotifier::ConnectionTypeObserver implementation.
252 virtual void OnConnectionTypeChanged(ConnectionType type) OVERRIDE {
253 pending_connection_type_ = type;
254 base::TimeDelta delay = last_announced_connection_type_ == CONNECTION_NONE
255 ? params_.connection_type_offline_delay_
256 : params_.connection_type_online_delay_;
257 // Cancels any previous timer.
258 timer_.Start(FROM_HERE, delay, this, &NetworkChangeCalculator::Notify);
259 }
260
261 private:
262 void Notify() {
263 // Don't bother signaling about dead connections.
264 if (have_announced_ &&
265 (last_announced_connection_type_ == CONNECTION_NONE) &&
266 (pending_connection_type_ == CONNECTION_NONE)) {
267 return;
268 }
269 have_announced_ = true;
270 last_announced_connection_type_ = pending_connection_type_;
271 // Immediately before sending out an online signal, send out an offline
272 // signal to perform any destructive actions before constructive actions.
273 if (pending_connection_type_ != CONNECTION_NONE)
274 NetworkChangeNotifier::NotifyObserversOfNetworkChange(CONNECTION_NONE);
275 NetworkChangeNotifier::NotifyObserversOfNetworkChange(
276 pending_connection_type_);
277 }
278
279 const NetworkChangeCalculatorParams params_;
280
281 // Indicates if NotifyObserversOfNetworkChange has been called yet.
282 bool have_announced_;
283 // Last value passed to NotifyObserversOfNetworkChange.
284 ConnectionType last_announced_connection_type_;
285 // Value to pass to NotifyObserversOfNetworkChange when Notify is called.
286 ConnectionType pending_connection_type_;
287 // Used to delay notifications so duplicates can be combined.
288 base::OneShotTimer<NetworkChangeCalculator> timer_;
289 };
290
196 NetworkChangeNotifier::~NetworkChangeNotifier() { 291 NetworkChangeNotifier::~NetworkChangeNotifier() {
197 DCHECK_EQ(this, g_network_change_notifier); 292 DCHECK_EQ(this, g_network_change_notifier);
198 g_network_change_notifier = NULL; 293 g_network_change_notifier = NULL;
199 } 294 }
200 295
201 // static 296 // static
202 void NetworkChangeNotifier::SetFactory( 297 void NetworkChangeNotifier::SetFactory(
203 NetworkChangeNotifierFactory* factory) { 298 NetworkChangeNotifierFactory* factory) {
204 CHECK(!g_network_change_notifier_factory); 299 CHECK(!g_network_change_notifier_factory);
205 g_network_change_notifier_factory = factory; 300 g_network_change_notifier_factory = factory;
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after
339 } 434 }
340 } 435 }
341 436
342 void NetworkChangeNotifier::AddDNSObserver(DNSObserver* observer) { 437 void NetworkChangeNotifier::AddDNSObserver(DNSObserver* observer) {
343 if (g_network_change_notifier) { 438 if (g_network_change_notifier) {
344 g_network_change_notifier->resolver_state_observer_list_->AddObserver( 439 g_network_change_notifier->resolver_state_observer_list_->AddObserver(
345 observer); 440 observer);
346 } 441 }
347 } 442 }
348 443
444 void NetworkChangeNotifier::AddNetworkChangeObserver(
445 NetworkChangeObserver* observer) {
446 if (g_network_change_notifier) {
447 g_network_change_notifier->network_change_observer_list_->AddObserver(
448 observer);
449 }
450 }
451
349 void NetworkChangeNotifier::RemoveIPAddressObserver( 452 void NetworkChangeNotifier::RemoveIPAddressObserver(
350 IPAddressObserver* observer) { 453 IPAddressObserver* observer) {
351 if (g_network_change_notifier) { 454 if (g_network_change_notifier) {
352 g_network_change_notifier->ip_address_observer_list_->RemoveObserver( 455 g_network_change_notifier->ip_address_observer_list_->RemoveObserver(
353 observer); 456 observer);
354 } 457 }
355 } 458 }
356 459
357 void NetworkChangeNotifier::RemoveConnectionTypeObserver( 460 void NetworkChangeNotifier::RemoveConnectionTypeObserver(
358 ConnectionTypeObserver* observer) { 461 ConnectionTypeObserver* observer) {
359 if (g_network_change_notifier) { 462 if (g_network_change_notifier) {
360 g_network_change_notifier->connection_type_observer_list_->RemoveObserver( 463 g_network_change_notifier->connection_type_observer_list_->RemoveObserver(
361 observer); 464 observer);
362 } 465 }
363 } 466 }
364 467
365 void NetworkChangeNotifier::RemoveDNSObserver(DNSObserver* observer) { 468 void NetworkChangeNotifier::RemoveDNSObserver(DNSObserver* observer) {
366 if (g_network_change_notifier) { 469 if (g_network_change_notifier) {
367 g_network_change_notifier->resolver_state_observer_list_->RemoveObserver( 470 g_network_change_notifier->resolver_state_observer_list_->RemoveObserver(
368 observer); 471 observer);
369 } 472 }
370 } 473 }
371 474
372 NetworkChangeNotifier::NetworkChangeNotifier() 475 void NetworkChangeNotifier::RemoveNetworkChangeObserver(
476 NetworkChangeObserver* observer) {
477 if (g_network_change_notifier) {
478 g_network_change_notifier->network_change_observer_list_->RemoveObserver(
479 observer);
480 }
481 }
482
483 NetworkChangeNotifier::NetworkChangeNotifier(
484 const NetworkChangeCalculatorParams& params)
373 : ip_address_observer_list_( 485 : ip_address_observer_list_(
374 new ObserverListThreadSafe<IPAddressObserver>( 486 new ObserverListThreadSafe<IPAddressObserver>(
375 ObserverListBase<IPAddressObserver>::NOTIFY_EXISTING_ONLY)), 487 ObserverListBase<IPAddressObserver>::NOTIFY_EXISTING_ONLY)),
376 connection_type_observer_list_( 488 connection_type_observer_list_(
377 new ObserverListThreadSafe<ConnectionTypeObserver>( 489 new ObserverListThreadSafe<ConnectionTypeObserver>(
378 ObserverListBase<ConnectionTypeObserver>::NOTIFY_EXISTING_ONLY)), 490 ObserverListBase<ConnectionTypeObserver>::NOTIFY_EXISTING_ONLY)),
379 resolver_state_observer_list_( 491 resolver_state_observer_list_(
380 new ObserverListThreadSafe<DNSObserver>( 492 new ObserverListThreadSafe<DNSObserver>(
381 ObserverListBase<DNSObserver>::NOTIFY_EXISTING_ONLY)), 493 ObserverListBase<DNSObserver>::NOTIFY_EXISTING_ONLY)),
494 network_change_observer_list_(
495 new ObserverListThreadSafe<NetworkChangeObserver>(
496 ObserverListBase<NetworkChangeObserver>::NOTIFY_EXISTING_ONLY)),
382 network_state_(new NetworkState()), 497 network_state_(new NetworkState()),
383 histogram_watcher_(new HistogramWatcher()) { 498 histogram_watcher_(new HistogramWatcher()),
499 network_change_calculator_(new NetworkChangeCalculator(params)) {
384 DCHECK(!g_network_change_notifier); 500 DCHECK(!g_network_change_notifier);
385 g_network_change_notifier = this; 501 g_network_change_notifier = this;
502 network_change_calculator_->Init();
386 } 503 }
387 504
388 #if defined(OS_LINUX) 505 #if defined(OS_LINUX)
389 const internal::AddressTrackerLinux* 506 const internal::AddressTrackerLinux*
390 NetworkChangeNotifier::GetAddressTrackerInternal() const { 507 NetworkChangeNotifier::GetAddressTrackerInternal() const {
391 return NULL; 508 return NULL;
392 } 509 }
393 #endif 510 #endif
394 511
395 // static 512 // static
(...skipping 21 matching lines...) Expand all
417 } 534 }
418 535
419 void NetworkChangeNotifier::NotifyObserversOfConnectionTypeChange() { 536 void NetworkChangeNotifier::NotifyObserversOfConnectionTypeChange() {
420 if (g_network_change_notifier) { 537 if (g_network_change_notifier) {
421 g_network_change_notifier->connection_type_observer_list_->Notify( 538 g_network_change_notifier->connection_type_observer_list_->Notify(
422 &ConnectionTypeObserver::OnConnectionTypeChanged, 539 &ConnectionTypeObserver::OnConnectionTypeChanged,
423 GetConnectionType()); 540 GetConnectionType());
424 } 541 }
425 } 542 }
426 543
544 void NetworkChangeNotifier::NotifyObserversOfNetworkChange(
545 ConnectionType type) {
546 if (g_network_change_notifier) {
547 g_network_change_notifier->network_change_observer_list_->Notify(
548 &NetworkChangeObserver::OnNetworkChanged,
549 type);
550 }
551 }
552
427 NetworkChangeNotifier::DisableForTest::DisableForTest() 553 NetworkChangeNotifier::DisableForTest::DisableForTest()
428 : network_change_notifier_(g_network_change_notifier) { 554 : network_change_notifier_(g_network_change_notifier) {
429 DCHECK(g_network_change_notifier); 555 DCHECK(g_network_change_notifier);
430 g_network_change_notifier = NULL; 556 g_network_change_notifier = NULL;
431 } 557 }
432 558
433 NetworkChangeNotifier::DisableForTest::~DisableForTest() { 559 NetworkChangeNotifier::DisableForTest::~DisableForTest() {
434 DCHECK(!g_network_change_notifier); 560 DCHECK(!g_network_change_notifier);
435 g_network_change_notifier = network_change_notifier_; 561 g_network_change_notifier = network_change_notifier_;
436 } 562 }
437 563
438 } // namespace net 564 } // namespace net
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698