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

Side by Side Diff: net/nqe/network_quality_estimator.cc

Issue 2128793003: Factor out NetworkID and caching mechanism from n_q_e.{h,cc} (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressed asvitkine comments Created 4 years, 4 months 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
« no previous file with comments | « net/nqe/network_quality_estimator.h ('k') | net/nqe/network_quality_estimator_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 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/nqe/network_quality_estimator.h" 5 #include "net/nqe/network_quality_estimator.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <cmath> 8 #include <cmath>
9 #include <limits> 9 #include <limits>
10 #include <utility> 10 #include <utility>
(...skipping 289 matching lines...) Expand 10 before | Expand all | Expand 10 after
300 algorithm_name_to_enum_.find(GetEffectiveConnectionTypeAlgorithm( 300 algorithm_name_to_enum_.find(GetEffectiveConnectionTypeAlgorithm(
301 variation_params)) == algorithm_name_to_enum_.end() 301 variation_params)) == algorithm_name_to_enum_.end()
302 ? kDefaultEffectiveConnectionTypeAlgorithm 302 ? kDefaultEffectiveConnectionTypeAlgorithm
303 : algorithm_name_to_enum_ 303 : algorithm_name_to_enum_
304 .find(GetEffectiveConnectionTypeAlgorithm(variation_params)) 304 .find(GetEffectiveConnectionTypeAlgorithm(variation_params))
305 ->second), 305 ->second),
306 tick_clock_(new base::DefaultTickClock()), 306 tick_clock_(new base::DefaultTickClock()),
307 effective_connection_type_recomputation_interval_( 307 effective_connection_type_recomputation_interval_(
308 base::TimeDelta::FromSeconds(15)), 308 base::TimeDelta::FromSeconds(15)),
309 last_connection_change_(tick_clock_->NowTicks()), 309 last_connection_change_(tick_clock_->NowTicks()),
310 current_network_id_( 310 current_network_id_(nqe::internal::NetworkID(
311 NetworkID(NetworkChangeNotifier::ConnectionType::CONNECTION_UNKNOWN, 311 NetworkChangeNotifier::ConnectionType::CONNECTION_UNKNOWN,
312 std::string())), 312 std::string())),
313 downstream_throughput_kbps_observations_(weight_multiplier_per_second_), 313 downstream_throughput_kbps_observations_(weight_multiplier_per_second_),
314 rtt_observations_(weight_multiplier_per_second_), 314 rtt_observations_(weight_multiplier_per_second_),
315 effective_connection_type_at_last_main_frame_( 315 effective_connection_type_at_last_main_frame_(
316 EFFECTIVE_CONNECTION_TYPE_UNKNOWN), 316 EFFECTIVE_CONNECTION_TYPE_UNKNOWN),
317 external_estimate_provider_(std::move(external_estimates_provider)), 317 external_estimate_provider_(std::move(external_estimates_provider)),
318 effective_connection_type_(EFFECTIVE_CONNECTION_TYPE_UNKNOWN), 318 effective_connection_type_(EFFECTIVE_CONNECTION_TYPE_UNKNOWN),
319 min_signal_strength_since_connection_change_(INT32_MAX), 319 min_signal_strength_since_connection_change_(INT32_MAX),
320 max_signal_strength_since_connection_change_(INT32_MIN), 320 max_signal_strength_since_connection_change_(INT32_MIN),
321 correlation_uma_logging_probability_( 321 correlation_uma_logging_probability_(
322 GetDoubleValueForVariationParamWithDefaultValue( 322 GetDoubleValueForVariationParamWithDefaultValue(
323 variation_params, 323 variation_params,
324 "correlation_logging_probability", 324 "correlation_logging_probability",
325 0.0)), 325 0.0)),
326 weak_ptr_factory_(this) { 326 weak_ptr_factory_(this) {
327 static_assert(kDefaultHalfLifeSeconds > 0, 327 static_assert(kDefaultHalfLifeSeconds > 0,
328 "Default half life duration must be > 0"); 328 "Default half life duration must be > 0");
329 static_assert(kMaximumNetworkQualityCacheSize > 0,
330 "Size of the network quality cache must be > 0");
331 // This limit should not be increased unless the logic for removing the
332 // oldest cache entry is rewritten to use a doubly-linked-list LRU queue.
333 static_assert(kMaximumNetworkQualityCacheSize <= 10,
334 "Size of the network quality cache must <= 10");
335 // None of the algorithms can have an empty name. 329 // None of the algorithms can have an empty name.
336 DCHECK(algorithm_name_to_enum_.end() == 330 DCHECK(algorithm_name_to_enum_.end() ==
337 algorithm_name_to_enum_.find(std::string())); 331 algorithm_name_to_enum_.find(std::string()));
338 332
339 DCHECK_EQ(algorithm_name_to_enum_.size(), 333 DCHECK_EQ(algorithm_name_to_enum_.size(),
340 static_cast<size_t>(EffectiveConnectionTypeAlgorithm:: 334 static_cast<size_t>(EffectiveConnectionTypeAlgorithm::
341 EFFECTIVE_CONNECTION_TYPE_ALGORITHM_LAST)); 335 EFFECTIVE_CONNECTION_TYPE_ALGORITHM_LAST));
342 DCHECK_NE(EffectiveConnectionTypeAlgorithm:: 336 DCHECK_NE(EffectiveConnectionTypeAlgorithm::
343 EFFECTIVE_CONNECTION_TYPE_ALGORITHM_LAST, 337 EFFECTIVE_CONNECTION_TYPE_ALGORITHM_LAST,
344 effective_connection_type_algorithm_); 338 effective_connection_type_algorithm_);
(...skipping 571 matching lines...) Expand 10 before | Expand all | Expand 10 after
916 EXTERNAL_ESTIMATE_PROVIDER_STATUS_BOUNDARY); 910 EXTERNAL_ESTIMATE_PROVIDER_STATUS_BOUNDARY);
917 } 911 }
918 912
919 void NetworkQualityEstimator::OnConnectionTypeChanged( 913 void NetworkQualityEstimator::OnConnectionTypeChanged(
920 NetworkChangeNotifier::ConnectionType type) { 914 NetworkChangeNotifier::ConnectionType type) {
921 DCHECK(thread_checker_.CalledOnValidThread()); 915 DCHECK(thread_checker_.CalledOnValidThread());
922 916
923 RecordMetricsOnConnectionTypeChanged(); 917 RecordMetricsOnConnectionTypeChanged();
924 918
925 // Write the estimates of the previous network to the cache. 919 // Write the estimates of the previous network to the cache.
926 CacheNetworkQualityEstimate(); 920 network_quality_store_.Add(current_network_id_,
921 nqe::internal::CachedNetworkQuality(
922 last_effective_connection_type_computation_,
923 estimated_quality_at_last_main_frame_));
927 924
928 // Clear the local state. 925 // Clear the local state.
929 last_connection_change_ = tick_clock_->NowTicks(); 926 last_connection_change_ = tick_clock_->NowTicks();
930 peak_network_quality_ = nqe::internal::NetworkQuality(); 927 peak_network_quality_ = nqe::internal::NetworkQuality();
931 downstream_throughput_kbps_observations_.Clear(); 928 downstream_throughput_kbps_observations_.Clear();
932 rtt_observations_.Clear(); 929 rtt_observations_.Clear();
933 930
934 #if defined(OS_ANDROID) 931 #if defined(OS_ANDROID)
935 if (NetworkChangeNotifier::IsConnectionCellular(current_network_id_.type)) { 932 if (NetworkChangeNotifier::IsConnectionCellular(current_network_id_.type)) {
936 UMA_HISTOGRAM_BOOLEAN( 933 UMA_HISTOGRAM_BOOLEAN(
(...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after
1163 NetworkQualityEstimator::GetRecentEffectiveConnectionTypeUsingMetrics( 1160 NetworkQualityEstimator::GetRecentEffectiveConnectionTypeUsingMetrics(
1164 const base::TimeTicks& start_time, 1161 const base::TimeTicks& start_time,
1165 NetworkQualityEstimator::MetricUsage http_rtt_metric, 1162 NetworkQualityEstimator::MetricUsage http_rtt_metric,
1166 NetworkQualityEstimator::MetricUsage transport_rtt_metric, 1163 NetworkQualityEstimator::MetricUsage transport_rtt_metric,
1167 NetworkQualityEstimator::MetricUsage downstream_throughput_kbps_metric) 1164 NetworkQualityEstimator::MetricUsage downstream_throughput_kbps_metric)
1168 const { 1165 const {
1169 DCHECK(thread_checker_.CalledOnValidThread()); 1166 DCHECK(thread_checker_.CalledOnValidThread());
1170 1167
1171 // If the device is currently offline, then return 1168 // If the device is currently offline, then return
1172 // EFFECTIVE_CONNECTION_TYPE_OFFLINE. 1169 // EFFECTIVE_CONNECTION_TYPE_OFFLINE.
1173 if (GetCurrentNetworkID().type == NetworkChangeNotifier::CONNECTION_NONE) 1170 if (current_network_id_.type == NetworkChangeNotifier::CONNECTION_NONE)
1174 return EFFECTIVE_CONNECTION_TYPE_OFFLINE; 1171 return EFFECTIVE_CONNECTION_TYPE_OFFLINE;
1175 1172
1176 base::TimeDelta http_rtt = nqe::internal::InvalidRTT(); 1173 base::TimeDelta http_rtt = nqe::internal::InvalidRTT();
1177 if (http_rtt_metric != NetworkQualityEstimator::MetricUsage::DO_NOT_USE && 1174 if (http_rtt_metric != NetworkQualityEstimator::MetricUsage::DO_NOT_USE &&
1178 !GetRecentHttpRTTMedian(start_time, &http_rtt)) { 1175 !GetRecentHttpRTTMedian(start_time, &http_rtt)) {
1179 http_rtt = nqe::internal::InvalidRTT(); 1176 http_rtt = nqe::internal::InvalidRTT();
1180 } 1177 }
1181 1178
1182 base::TimeDelta transport_rtt = nqe::internal::InvalidRTT(); 1179 base::TimeDelta transport_rtt = nqe::internal::InvalidRTT();
1183 if (transport_rtt_metric != 1180 if (transport_rtt_metric !=
(...skipping 164 matching lines...) Expand 10 before | Expand all | Expand 10 after
1348 // thus a higher percentile throughput will be faster than a lower one. 1345 // thus a higher percentile throughput will be faster than a lower one.
1349 int32_t kbps = nqe::internal::kInvalidThroughput; 1346 int32_t kbps = nqe::internal::kInvalidThroughput;
1350 if (!downstream_throughput_kbps_observations_.GetPercentile( 1347 if (!downstream_throughput_kbps_observations_.GetPercentile(
1351 start_time, &kbps, 100 - percentile, 1348 start_time, &kbps, 100 - percentile,
1352 std::vector<NetworkQualityObservationSource>())) { 1349 std::vector<NetworkQualityObservationSource>())) {
1353 return nqe::internal::kInvalidThroughput; 1350 return nqe::internal::kInvalidThroughput;
1354 } 1351 }
1355 return kbps; 1352 return kbps;
1356 } 1353 }
1357 1354
1358 NetworkQualityEstimator::NetworkID 1355 nqe::internal::NetworkID NetworkQualityEstimator::GetCurrentNetworkID() const {
1359 NetworkQualityEstimator::GetCurrentNetworkID() const {
1360 DCHECK(thread_checker_.CalledOnValidThread()); 1356 DCHECK(thread_checker_.CalledOnValidThread());
1361 1357
1362 // TODO(tbansal): crbug.com/498068 Add NetworkQualityEstimatorAndroid class 1358 // TODO(tbansal): crbug.com/498068 Add NetworkQualityEstimatorAndroid class
1363 // that overrides this method on the Android platform. 1359 // that overrides this method on the Android platform.
1364 1360
1365 // It is possible that the connection type changed between when 1361 // It is possible that the connection type changed between when
1366 // GetConnectionType() was called and when the API to determine the 1362 // GetConnectionType() was called and when the API to determine the
1367 // network name was called. Check if that happened and retry until the 1363 // network name was called. Check if that happened and retry until the
1368 // connection type stabilizes. This is an imperfect solution but should 1364 // connection type stabilizes. This is an imperfect solution but should
1369 // capture majority of cases, and should not significantly affect estimates 1365 // capture majority of cases, and should not significantly affect estimates
1370 // (that are approximate to begin with). 1366 // (that are approximate to begin with).
1371 while (true) { 1367 while (true) {
1372 NetworkQualityEstimator::NetworkID network_id( 1368 nqe::internal::NetworkID network_id(
1373 NetworkChangeNotifier::GetConnectionType(), std::string()); 1369 NetworkChangeNotifier::GetConnectionType(), std::string());
1374 1370
1375 switch (network_id.type) { 1371 switch (network_id.type) {
1376 case NetworkChangeNotifier::ConnectionType::CONNECTION_UNKNOWN: 1372 case NetworkChangeNotifier::ConnectionType::CONNECTION_UNKNOWN:
1377 case NetworkChangeNotifier::ConnectionType::CONNECTION_NONE: 1373 case NetworkChangeNotifier::ConnectionType::CONNECTION_NONE:
1378 case NetworkChangeNotifier::ConnectionType::CONNECTION_BLUETOOTH: 1374 case NetworkChangeNotifier::ConnectionType::CONNECTION_BLUETOOTH:
1379 case NetworkChangeNotifier::ConnectionType::CONNECTION_ETHERNET: 1375 case NetworkChangeNotifier::ConnectionType::CONNECTION_ETHERNET:
1380 break; 1376 break;
1381 case NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI: 1377 case NetworkChangeNotifier::ConnectionType::CONNECTION_WIFI:
1382 #if defined(OS_ANDROID) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \ 1378 #if defined(OS_ANDROID) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \
(...skipping 15 matching lines...) Expand all
1398 1394
1399 if (network_id.type == NetworkChangeNotifier::GetConnectionType()) 1395 if (network_id.type == NetworkChangeNotifier::GetConnectionType())
1400 return network_id; 1396 return network_id;
1401 } 1397 }
1402 NOTREACHED(); 1398 NOTREACHED();
1403 } 1399 }
1404 1400
1405 bool NetworkQualityEstimator::ReadCachedNetworkQualityEstimate() { 1401 bool NetworkQualityEstimator::ReadCachedNetworkQualityEstimate() {
1406 DCHECK(thread_checker_.CalledOnValidThread()); 1402 DCHECK(thread_checker_.CalledOnValidThread());
1407 1403
1408 // If the network name is unavailable, caching should not be performed. 1404 nqe::internal::CachedNetworkQuality cached_network_quality;
1409 if (current_network_id_.id.empty()) 1405
1406 const bool cached_estimate_available = network_quality_store_.GetById(
1407 current_network_id_, &cached_network_quality);
1408 UMA_HISTOGRAM_BOOLEAN("NQE.CachedNetworkQualityAvailable",
1409 cached_estimate_available);
1410
1411 if (!cached_estimate_available)
1410 return false; 1412 return false;
1411 1413
1412 CachedNetworkQualities::const_iterator it = 1414 const base::TimeTicks now = tick_clock_->NowTicks();
1413 cached_network_qualities_.find(current_network_id_);
1414 1415
1415 if (it == cached_network_qualities_.end()) 1416 if (cached_network_quality.network_quality().downstream_throughput_kbps() !=
1416 return false;
1417
1418 nqe::internal::NetworkQuality network_quality(it->second.network_quality());
1419
1420 const base::TimeTicks now = tick_clock_->NowTicks();
1421 bool read_cached_estimate = false;
1422
1423 if (network_quality.downstream_throughput_kbps() !=
1424 nqe::internal::kInvalidThroughput) { 1417 nqe::internal::kInvalidThroughput) {
1425 read_cached_estimate = true;
1426 ThroughputObservation througphput_observation( 1418 ThroughputObservation througphput_observation(
1427 network_quality.downstream_throughput_kbps(), now, 1419 cached_network_quality.network_quality().downstream_throughput_kbps(),
1428 NETWORK_QUALITY_OBSERVATION_SOURCE_CACHED_ESTIMATE); 1420 now, NETWORK_QUALITY_OBSERVATION_SOURCE_CACHED_ESTIMATE);
1429 downstream_throughput_kbps_observations_.AddObservation( 1421 downstream_throughput_kbps_observations_.AddObservation(
1430 througphput_observation); 1422 througphput_observation);
1431 NotifyObserversOfThroughput(througphput_observation); 1423 NotifyObserversOfThroughput(througphput_observation);
1432 } 1424 }
1433 1425
1434 if (network_quality.http_rtt() != nqe::internal::InvalidRTT()) { 1426 if (cached_network_quality.network_quality().http_rtt() !=
1435 read_cached_estimate = true; 1427 nqe::internal::InvalidRTT()) {
1436 RttObservation rtt_observation( 1428 RttObservation rtt_observation(
1437 network_quality.http_rtt(), now, 1429 cached_network_quality.network_quality().http_rtt(), now,
1438 NETWORK_QUALITY_OBSERVATION_SOURCE_CACHED_ESTIMATE); 1430 NETWORK_QUALITY_OBSERVATION_SOURCE_CACHED_ESTIMATE);
1439 rtt_observations_.AddObservation(rtt_observation); 1431 rtt_observations_.AddObservation(rtt_observation);
1440 NotifyObserversOfRTT(rtt_observation); 1432 NotifyObserversOfRTT(rtt_observation);
1441 } 1433 }
1442 1434 return true;
1443 return read_cached_estimate;
1444 } 1435 }
1445 1436
1446 void NetworkQualityEstimator::OnUpdatedEstimateAvailable( 1437 void NetworkQualityEstimator::OnUpdatedEstimateAvailable(
1447 const base::TimeDelta& rtt, 1438 const base::TimeDelta& rtt,
1448 int32_t downstream_throughput_kbps, 1439 int32_t downstream_throughput_kbps,
1449 int32_t upstream_throughput_kbps) { 1440 int32_t upstream_throughput_kbps) {
1450 DCHECK(thread_checker_.CalledOnValidThread()); 1441 DCHECK(thread_checker_.CalledOnValidThread());
1451 DCHECK(external_estimate_provider_); 1442 DCHECK(external_estimate_provider_);
1452 1443
1453 RecordExternalEstimateProviderMetrics( 1444 RecordExternalEstimateProviderMetrics(
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after
1524 void NetworkQualityEstimator::SetTickClockForTesting( 1515 void NetworkQualityEstimator::SetTickClockForTesting(
1525 std::unique_ptr<base::TickClock> tick_clock) { 1516 std::unique_ptr<base::TickClock> tick_clock) {
1526 DCHECK(thread_checker_.CalledOnValidThread()); 1517 DCHECK(thread_checker_.CalledOnValidThread());
1527 tick_clock_ = std::move(tick_clock); 1518 tick_clock_ = std::move(tick_clock);
1528 } 1519 }
1529 1520
1530 double NetworkQualityEstimator::RandDouble() const { 1521 double NetworkQualityEstimator::RandDouble() const {
1531 return base::RandDouble(); 1522 return base::RandDouble();
1532 } 1523 }
1533 1524
1534 void NetworkQualityEstimator::CacheNetworkQualityEstimate() {
1535 DCHECK(thread_checker_.CalledOnValidThread());
1536 DCHECK_LE(cached_network_qualities_.size(),
1537 static_cast<size_t>(kMaximumNetworkQualityCacheSize));
1538
1539 // If the network name is unavailable, caching should not be performed.
1540 if (current_network_id_.id.empty())
1541 return;
1542
1543 base::TimeDelta http_rtt = nqe::internal::InvalidRTT();
1544 int32_t downlink_throughput_kbps = nqe::internal::kInvalidThroughput;
1545
1546 if (!GetHttpRTTEstimate(&http_rtt) ||
1547 !GetDownlinkThroughputKbpsEstimate(&downlink_throughput_kbps)) {
1548 return;
1549 }
1550
1551 // |transport_rtt| is currently not cached.
1552 nqe::internal::NetworkQuality network_quality = nqe::internal::NetworkQuality(
1553 http_rtt, nqe::internal::InvalidRTT() /* transport_rtt */,
1554 downlink_throughput_kbps);
1555
1556 if (cached_network_qualities_.size() == kMaximumNetworkQualityCacheSize) {
1557 // Remove the oldest entry.
1558 CachedNetworkQualities::iterator oldest_entry_iterator =
1559 cached_network_qualities_.begin();
1560
1561 for (CachedNetworkQualities::iterator it =
1562 cached_network_qualities_.begin();
1563 it != cached_network_qualities_.end(); ++it) {
1564 if ((it->second).OlderThan(oldest_entry_iterator->second))
1565 oldest_entry_iterator = it;
1566 }
1567 cached_network_qualities_.erase(oldest_entry_iterator);
1568 }
1569 DCHECK_LT(cached_network_qualities_.size(),
1570 static_cast<size_t>(kMaximumNetworkQualityCacheSize));
1571
1572 cached_network_qualities_.insert(
1573 std::make_pair(current_network_id_,
1574 nqe::internal::CachedNetworkQuality(network_quality)));
1575 DCHECK_LE(cached_network_qualities_.size(),
1576 static_cast<size_t>(kMaximumNetworkQualityCacheSize));
1577 }
1578
1579 void NetworkQualityEstimator::OnUpdatedRTTAvailable( 1525 void NetworkQualityEstimator::OnUpdatedRTTAvailable(
1580 SocketPerformanceWatcherFactory::Protocol protocol, 1526 SocketPerformanceWatcherFactory::Protocol protocol,
1581 const base::TimeDelta& rtt) { 1527 const base::TimeDelta& rtt) {
1582 DCHECK(thread_checker_.CalledOnValidThread()); 1528 DCHECK(thread_checker_.CalledOnValidThread());
1583 DCHECK_NE(nqe::internal::InvalidRTT(), rtt); 1529 DCHECK_NE(nqe::internal::InvalidRTT(), rtt);
1584 1530
1585 RttObservation observation(rtt, tick_clock_->NowTicks(), 1531 RttObservation observation(rtt, tick_clock_->NowTicks(),
1586 ProtocolSourceToObservationSource(protocol)); 1532 ProtocolSourceToObservationSource(protocol));
1587 NotifyObserversOfRTT(observation); 1533 NotifyObserversOfRTT(observation);
1588 rtt_observations_.AddObservation(observation); 1534 rtt_observations_.AddObservation(observation);
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
1666 NotifyObserversOfEffectiveConnectionTypeChanged() { 1612 NotifyObserversOfEffectiveConnectionTypeChanged() {
1667 DCHECK(thread_checker_.CalledOnValidThread()); 1613 DCHECK(thread_checker_.CalledOnValidThread());
1668 1614
1669 // TODO(tbansal): Add hysteresis in the notification. 1615 // TODO(tbansal): Add hysteresis in the notification.
1670 FOR_EACH_OBSERVER( 1616 FOR_EACH_OBSERVER(
1671 EffectiveConnectionTypeObserver, effective_connection_type_observer_list_, 1617 EffectiveConnectionTypeObserver, effective_connection_type_observer_list_,
1672 OnEffectiveConnectionTypeChanged(effective_connection_type_)); 1618 OnEffectiveConnectionTypeChanged(effective_connection_type_));
1673 } 1619 }
1674 1620
1675 } // namespace net 1621 } // namespace net
OLDNEW
« no previous file with comments | « net/nqe/network_quality_estimator.h ('k') | net/nqe/network_quality_estimator_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698