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

Unified Diff: chrome/browser/geolocation/network_location_provider.cc

Issue 552250: Port the gears geolocation network provider to Chromium... (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: '' Created 10 years, 11 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 side-by-side diff with in-line comments
Download patch
Index: chrome/browser/geolocation/network_location_provider.cc
===================================================================
--- chrome/browser/geolocation/network_location_provider.cc (revision 37842)
+++ chrome/browser/geolocation/network_location_provider.cc (working copy)
@@ -1,77 +1,56 @@
-// Copyright 2008, Google Inc.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice,
-// this list of conditions and the following disclaimer.
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-// this list of conditions and the following disclaimer in the documentation
-// and/or other materials provided with the distribution.
-// 3. Neither the name of Google Inc. nor the names of its contributors may be
-// used to endorse or promote products derived from this software without
-// specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
-// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
-// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
-// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
-// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
-// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
-// TODO(joth): port to chromium
-#if 0
+#include "chrome/browser/geolocation/network_location_provider.h"
-#include "gears/geolocation/network_location_provider.h"
+#include "base/time.h"
-#include "gears/base/common/base_class.h"
-#include "gears/base/common/stopwatch.h" // For GetCurrentTimeMillis
-#include "gears/geolocation/access_token_manager.h"
-#include "gears/geolocation/backoff_manager.h"
-
+namespace {
// The maximum period of time we'll wait for a complete set of device data
// before sending the request.
-static const int kDataCompleteWaitPeriod = 1000 * 2; // 2 seconds
+const int kDataCompleteWaitPeriod = 1000 * 2; // 2 seconds
// The maximum size of the cache of positions for previously requested device
// data.
-static const size_t kMaximumCacheSize = 10;
+const size_t kMaximumCacheSize = 10;
+// TODO(joth): Share, or remove usage by porting callers to Time et al.
+int64 GetCurrentTimeMillis() {
+ return static_cast<int64>(base::Time::Now().ToDoubleT() * 1000);
+}
+} // namespace
// The PositionCache handles caching and retrieving a position returned by a
// network location provider. It is not thread safe. It's methods are called on
// multiple threads by NetworkLocationProvider, but the timing is such that
// thread safety is not required.
-class PositionCache {
+class NetworkLocationProvider::PositionCache {
public:
// Caches the current position response for the current set of cell ID and
// WiFi data. Returns true on success, false otherwise.
- bool CachePosition(const RadioData &radio_data,
- const WifiData &wifi_data,
- const Position &position) {
+ bool CachePosition(const RadioData& radio_data,
+ const WifiData& wifi_data,
+ const Position& position) {
// Check that we can generate a valid key for the device data.
- std::string16 key;
+ string16 key;
if (!MakeKey(radio_data, wifi_data, &key)) {
return false;
}
// If the cache is full, remove the oldest entry.
if (cache_.size() == kMaximumCacheSize) {
- assert(cache_times_.size() == kMaximumCacheSize);
+ DCHECK(cache_times_.size() == kMaximumCacheSize);
CacheTimesMap::iterator oldest_entry = cache_times_.begin();
- assert(oldest_entry != cache_times_.end());
+ DCHECK(oldest_entry != cache_times_.end());
cache_.erase(oldest_entry->second);
cache_times_.erase(oldest_entry);
}
// Insert the position into the cache.
std::pair<CacheMap::iterator, bool> result =
cache_.insert(std::make_pair(key, position));
- assert(result.second);
+ DCHECK(result.second);
cache_times_[position.timestamp] = result.first;
- assert(cache_.size() == cache_times_.size());
+ DCHECK(cache_.size() == cache_times_.size());
return true;
}
@@ -79,7 +58,7 @@
// WiFi data. Returns the cached position if available, NULL otherwise.
const Position *FindPosition(const RadioData &radio_data,
const WifiData &wifi_data) {
- std::string16 key;
+ string16 key;
if (!MakeKey(radio_data, wifi_data, &key)) {
return NULL;
}
@@ -90,419 +69,225 @@
// Makes the key for the map of cached positions, using a set of
// device data. Returns true if a good key was generated, false otherwise.
static bool MakeKey(const RadioData& /*radio_data*/,
- const WifiData &wifi_data,
- std::string16 *key) {
+ const WifiData& wifi_data,
+ string16* key) {
// Currently we use only the WiFi data, and base the key only on the MAC
// addresses.
// TODO(steveblock): Make use of radio_data.
+ DCHECK(key);
key->clear();
+ key->reserve(wifi_data.access_point_data.size() * 19);
+ const string16 separator(ASCIIToUTF16("|"));
for (WifiData::AccessPointDataSet::const_iterator iter =
wifi_data.access_point_data.begin();
iter != wifi_data.access_point_data.begin();
iter++) {
- key->append(STRING16(L"|") + iter->mac_address + STRING16(L"|"));
+ *key += separator;
+ *key += iter->mac_address;
+ *key += separator;
}
// If the key is the empty string, return false, as we don't want to cache a
// position for such a set of device data.
return !key->empty();
}
-private:
+ private:
// The cache of positions. This is stored using two maps. One map is keyed on
// a string that represents a set of device data, the other is keyed on the
// timestamp of the position.
- typedef std::map<std::string16, Position> CacheMap;
+ typedef std::map<string16, Position> CacheMap;
CacheMap cache_;
typedef std::map<int64, CacheMap::iterator> CacheTimesMap;
CacheTimesMap cache_times_;
};
-
// NetworkLocationProvider factory function
-LocationProviderBase *NewNetworkLocationProvider(
- BrowsingContext *browsing_context,
- const std::string16 &url,
- const std::string16 &host_name,
- const std::string16 &language) {
- return new NetworkLocationProvider(browsing_context, url, host_name,
- language);
+LocationProviderBase* NewNetworkLocationProvider(
+ LocationProviderBase::AccessTokenStore* access_token_store,
+ URLRequestContextGetter* context,
+ const GURL& url,
+ const string16& host_name) {
+ return new NetworkLocationProvider(access_token_store, context,
+ url, host_name);
}
-
// NetworkLocationProvider
NetworkLocationProvider::NetworkLocationProvider(
- BrowsingContext *browsing_context,
- const std::string16 &url,
- const std::string16 &host_name,
- const std::string16 &language)
- : request_(NULL),
- url_(url),
- host_name_(host_name),
- request_address_(false),
- request_address_from_last_request_(false),
- address_language_(language),
- is_shutting_down_(false),
+ AccessTokenStore* access_token_store,
+ URLRequestContextGetter* url_context_getter,
+ const GURL& url,
+ const string16& host_name)
+ : access_token_store_(access_token_store),
+ radio_data_provider_(NULL),
+ wifi_data_provider_(NULL),
+ is_radio_data_complete_(false),
+ is_wifi_data_complete_(false),
+ device_data_updated_timestamp_(kint64min),
is_new_data_available_(false),
- is_new_listener_waiting_(false),
- browsing_context_(browsing_context) {
- // TODO(steveblock): Consider allowing multiple values for "address_language"
- // in the network protocol to allow better sharing of network location
- // providers.
-
- // Get the device data providers. The first call to Register will create the
- // provider and it will be deleted by ref counting.
- radio_data_provider_ = RadioDataProvider::Register(this);
- wifi_data_provider_ = WifiDataProvider::Register(this);
-
- AccessTokenManager::GetInstance()->Register(url_);
-
+ ALLOW_THIS_IN_INITIALIZER_LIST(delayed_start_task_(this)) {
// Create the position cache.
position_cache_.reset(new PositionCache());
- // Start the worker thread
- if (!Start()) {
- // This should never happen.
- LOG(("Could not start the NLR"));
- assert(false);
- }
+ request_.reset(new NetworkLocationRequest(url_context_getter, url,
+ host_name, this));
}
NetworkLocationProvider::~NetworkLocationProvider() {
- // Shut down the worker thread
- is_shutting_down_ = true;
- thread_notification_event_.Signal();
- Join();
-
- // Must keep the request around until our worker thread has stopped.
- if (request_) {
- request_->StopThreadAndDelete();
- request_ = NULL;
- }
-
- radio_data_provider_->Unregister(this);
- wifi_data_provider_->Unregister(this);
-
- AccessTokenManager::GetInstance()->Unregister();
+ if (radio_data_provider_)
+ radio_data_provider_->Unregister(this);
+ if (wifi_data_provider_)
+ wifi_data_provider_->Unregister(this);
}
-void NetworkLocationProvider::RegisterListener(
- LocationProviderBase::ListenerInterface *listener,
- bool request_address) {
- // Determine whether this listener requires an address when the last request
- // does not.
- bool new_listener_requires_address =
- !request_address_from_last_request_ && request_address;
-
- // Update whether or not we need to request an address.
- request_address_ |= request_address;
-
- // If we now need to request an address when we did not before, we don't add
- // the listener. This is because if a request is currently in progress, we
- // don't want the new listener to be called back with a position without an
- // address. We add the listener when we make the next request.
- if (new_listener_requires_address) {
- MutexLock lock(&new_listeners_requiring_address_mutex_);
- new_listeners_requiring_address_.insert(listener);
- } else {
- LocationProviderBase::RegisterListener(listener, request_address);
- }
-
- // Signal to the worker thread that there is a new listener.
- is_new_listener_waiting_ = true;
- thread_notification_event_.Signal();
-}
-
-void NetworkLocationProvider::UnregisterListener(
- LocationProviderBase::ListenerInterface *listener) {
- assert(listener);
-
- // First try removing the listener from the set of new listeners waiting for
- // an address. Otherwise, try the regular listeners.
- MutexLock new_listeners_lock(&new_listeners_requiring_address_mutex_);
- ListenerSet::iterator iter = new_listeners_requiring_address_.find(listener);
- if (iter != new_listeners_requiring_address_.end()) {
- new_listeners_requiring_address_.erase(iter);
- } else {
- LocationProviderBase::UnregisterListener(listener);
- }
-
- // Update whether or not we need to request an address.
- if (request_address_) {
- if (!new_listeners_requiring_address_.empty()) {
- return;
- }
- MutexLock listeners_lock(GetListenersMutex());
- ListenerMap *listeners = GetListeners();
- for (ListenerMap::const_iterator iter = listeners->begin();
- iter != listeners->end();
- iter++) {
- if (iter->second.first == true) {
- return;
- }
- }
- request_address_ = false;
- }
-}
-
// LocationProviderBase implementation
void NetworkLocationProvider::GetPosition(Position *position) {
- assert(position);
- MutexLock lock(&position_mutex_);
+ DCHECK(position);
+ AutoLock lock(position_mutex_);
*position = position_;
}
+void NetworkLocationProvider::UpdatePosition() {
+ if (is_new_data_available_) {
+ RequestPosition();
+ }
+}
+
// DeviceDataProviderInterface::ListenerInterface implementation.
void NetworkLocationProvider::DeviceDataUpdateAvailable(
- RadioDataProvider *provider) {
- MutexLock lock(&data_mutex_);
- assert(provider == radio_data_provider_);
- is_radio_data_complete_ = radio_data_provider_->GetData(&radio_data_);
-
- DeviceDataUpdateAvailableImpl();
+ RadioDataProvider* provider) {
+ {
+ AutoLock lock(data_mutex_);
+ DCHECK(provider == radio_data_provider_);
+ is_radio_data_complete_ = radio_data_provider_->GetData(&radio_data_);
+ }
+ OnDeviceDataUpdated();
}
void NetworkLocationProvider::DeviceDataUpdateAvailable(
- WifiDataProvider *provider) {
- assert(provider == wifi_data_provider_);
- MutexLock lock(&data_mutex_);
- is_wifi_data_complete_ = wifi_data_provider_->GetData(&wifi_data_);
-
- DeviceDataUpdateAvailableImpl();
+ WifiDataProvider* provider) {
+ {
+ AutoLock lock(data_mutex_);
+ DCHECK(provider == wifi_data_provider_);
+ is_wifi_data_complete_ = wifi_data_provider_->GetData(&wifi_data_);
+ }
+ OnDeviceDataUpdated();
}
// NetworkLocationRequest::ListenerInterface implementation.
void NetworkLocationProvider::LocationResponseAvailable(
- const Position &position,
+ const Position& position,
bool server_error,
- const std::string16 &access_token) {
+ const string16& access_token) {
+ CheckRunningInClientLoop();
// Record the position and update our cache.
- position_mutex_.Lock();
- position_ = position;
- if (position_.IsGoodFix()) {
- MutexLock lock(&data_mutex_);
- position_cache_->CachePosition(radio_data_, wifi_data_, position_);
+ {
+ AutoLock position_lock(position_mutex_);
+ position_ = position;
}
- position_mutex_.Unlock();
+ if (position.IsValidFix()) {
+ AutoLock lock(data_mutex_);
+ position_cache_->CachePosition(radio_data_, wifi_data_, position);
+ }
// Record access_token if it's set.
- if (!access_token.empty()) {
- AccessTokenManager::GetInstance()->SetToken(url_, access_token);
+ if (!access_token.empty() && access_token_ != access_token) {
+ access_token_ = access_token;
+ access_token_store_->SetAccessToken(request_->url(), access_token);
}
- // Get earliest time for next request.
- earliest_next_request_time_ = BackoffManager::ReportResponse(url_,
- server_error);
-
- // Signal to the worker thread that this request has completed.
- is_last_request_complete_ = true;
- thread_notification_event_.Signal();
-
+ // If new data arrived whilst request was pending reissue the request.
+ UpdatePosition();
// Let listeners know that we now have a position available.
UpdateListeners();
}
-// Thread implementation
-void NetworkLocationProvider::Run() {
- // Create the network request object. We must do this on the same thread from
- // which we'll call Start().
- request_ = NetworkLocationRequest::Create(browsing_context_, url_,
- host_name_, this);
- if (!request_) {
- LOG(("Failed to create NetworkLocationRequest object.\n"));
- assert(false);
- return;
+bool NetworkLocationProvider::StartProvider() {
+ CheckRunningInClientLoop();
+ DCHECK(radio_data_provider_ == NULL);
+ DCHECK(wifi_data_provider_ == NULL);
+ if (!request_->url().is_valid()) {
+ LOG(WARNING) << "StartProvider() : Failed, Bad URL: "
+ << request_->url().possibly_invalid_spec();
+ return false;
}
+ // Get the device data providers. The first call to Register will create the
+ // provider and it will be deleted by ref counting.
+ radio_data_provider_ = RadioDataProvider::Register(this);
+ wifi_data_provider_ = WifiDataProvider::Register(this);
+
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ delayed_start_task_.NewRunnableMethod(
+ &NetworkLocationProvider::RequestPosition),
+ kDataCompleteWaitPeriod);
+ {
// Get the device data.
- data_mutex_.Lock();
+ AutoLock lock(data_mutex_);
is_radio_data_complete_ = radio_data_provider_->GetData(&radio_data_);
is_wifi_data_complete_ = wifi_data_provider_->GetData(&wifi_data_);
- timestamp_ = GetCurrentTimeMillis();
-
- // For the first request, wait for a certain maximum time period to get as
- // much device data as possible.
- int64 start_time = timestamp_;
- while (true) {
- if (is_radio_data_complete_ && is_wifi_data_complete_) {
- data_mutex_.Unlock();
- break;
- }
- data_mutex_.Unlock();
-
- int64 elapsed_time = GetCurrentTimeMillis() - start_time;
- int timeout = kDataCompleteWaitPeriod - static_cast<int>(elapsed_time);
- if (timeout <= 0) {
- break;
- }
- if (!thread_notification_event_.WaitWithTimeout(timeout)) {
- // Quit waiting if we time out.
- break;
- }
- // Terminate the thread if requested.
- if (is_shutting_down_) {
- return;
- }
- // The event should be due to new device data or a new listener.
- assert(is_new_data_available_ || is_new_listener_waiting_);
- // If we have new data available, inform listeners of movement.
- if (is_new_data_available_) {
- InformListenersOfMovement();
- }
- // Lock the data mutex to test is_radio_data_complete_ and
- // is_wifi_data_complete_ on the next loop.
- data_mutex_.Lock();
}
-
- earliest_next_request_time_ = 0;
- MakeRequest();
-
- // Loop continually, making requests whenever new data becomes available,
- // subject to the minimum interval.
- //
- // This loop is structured such that we don't require mutex locks to
- // synchronise changes to is_new_data_available_ etc with signals on
- // thread_notification_event_. Note that if we get a signal before we wait,
- // the wait will proceed immediately, so we don't miss signals.
- int64 remaining_time = 1;
- while (!is_shutting_down_) {
- if (remaining_time > 0) {
- remaining_time = earliest_next_request_time_ - GetCurrentTimeMillis();
- }
-
- // If the minimum time period has not yet elapsed, set the timeout such
- // that the wait expires when the period has elapsed.
- if (remaining_time > 0) {
- thread_notification_event_.WaitWithTimeout(
- static_cast<int>(remaining_time));
- } else {
- thread_notification_event_.Wait();
- }
-
- // Update remaining time now we've woken up. Note that it can never
- // transition from <= 0 to > 0.
- if (remaining_time > 0) {
- remaining_time = earliest_next_request_time_ - GetCurrentTimeMillis();
- }
-
- bool make_request = false;
- if (is_new_listener_waiting_) {
- // A new listener has just registered with this provider. If new data is
- // available, force a new request now, unless a request is already in
- // progress. If not, update listeners with the last known position,
- // provided we have one.
- if (is_new_data_available_) {
- if (is_last_request_complete_) {
- make_request = true;
- }
- } else {
- // Before the first network request completes, position_ may not be
- // valid, so we do not update the listeners. They will be updated once
- // the network request completes.
- if (position_.IsInitialized()) {
- // Update listeners with the last known position.
- UpdateListeners();
- }
- }
- is_new_listener_waiting_ = false;
- }
-
- // If a new listener has now registered such that we now require an address,
- // we make a new request once the current request completes.
- new_listeners_requiring_address_mutex_.Lock();
- if (!new_listeners_requiring_address_.empty()) {
- if (is_last_request_complete_) {
- make_request = true;
- }
- }
- new_listeners_requiring_address_mutex_.Unlock();
-
- // If the thread is not shutting down, we have new data, the last request
- // has completed, and the minimum time has elapsed, make the next request.
- if (!is_shutting_down_ &&
- is_new_data_available_ &&
- is_last_request_complete_ &&
- remaining_time <= 0) {
- make_request = true;
- }
-
- // If we have new data available, inform listeners of movement.
- if (is_new_data_available_) {
- InformListenersOfMovement();
- }
-
- // TODO(steveblock): If the request does not complete within some maximum
- // time period, we should kill it and start a new request.
- if (make_request) {
- MakeRequest();
- remaining_time = 1;
- }
- }
+ if (is_radio_data_complete_ || is_wifi_data_complete_)
+ OnDeviceDataUpdated();
+ return true;
}
// Other methods
+void NetworkLocationProvider::RequestPosition() {
+ CheckRunningInClientLoop();
-bool NetworkLocationProvider::MakeRequest() {
- // If we have new listeners waiting for an address, request_address_
- // must be true.
- assert(new_listeners_requiring_address_.empty() || request_address_);
-
- // Move the new listeners waiting for an address to the list of listeners.
- MutexLock lock(&new_listeners_requiring_address_mutex_);
- for (ListenerSet::const_iterator iter =
- new_listeners_requiring_address_.begin();
- iter != new_listeners_requiring_address_.end();
- iter++) {
- LocationProviderBase::RegisterListener(*iter, true);
+ delayed_start_task_.RevokeAll();
+ const Position* cached_position;
+ {
+ AutoLock lock(data_mutex_);
+ cached_position = position_cache_->FindPosition(radio_data_, wifi_data_);
}
- new_listeners_requiring_address_.clear();
-
- request_address_from_last_request_ = request_address_;
-
- BackoffManager::ReportRequest(url_);
-
- std::string16 access_token;
- AccessTokenManager::GetInstance()->GetToken(url_, &access_token);
-
- // Reset flags
- is_new_data_available_ = false;
- is_new_listener_waiting_ = false;
-
- data_mutex_.Lock();
- const Position *cached_position =
- position_cache_->FindPosition(radio_data_, wifi_data_);
- data_mutex_.Unlock();
+ DCHECK_NE(device_data_updated_timestamp_, kint64min) <<
+ "Timestamp must be set before looking up position";
if (cached_position) {
- assert(cached_position->IsGoodFix());
+ DCHECK(cached_position->IsValidFix());
// Record the position and update its timestamp.
- position_mutex_.Lock();
- position_ = *cached_position;
- position_.timestamp = timestamp_;
- position_mutex_.Unlock();
-
+ {
+ AutoLock lock(position_mutex_);
+ position_ = *cached_position;
+ // The timestamp of a position fix is determined by the timestamp
+ // of the source data update. (The value of position_.timestamp from
+ // the cache could be from weeks ago!)
+ position_.timestamp = device_data_updated_timestamp_;
+ }
+ is_new_data_available_ = false;
// Let listeners know that we now have a position available.
UpdateListeners();
- return true;
+ return;
}
- assert(request_);
- is_last_request_complete_ = false;
- MutexLock data_lock(&data_mutex_);
- return request_->MakeRequest(access_token,
- radio_data_,
- wifi_data_,
- request_address_,
- address_language_,
- kBadLatLng, // We don't have a position to pass
- kBadLatLng, // to the server.
- timestamp_);
-}
+ DCHECK(request_ != NULL);
-void NetworkLocationProvider::DeviceDataUpdateAvailableImpl() {
- timestamp_ = GetCurrentTimeMillis();
+ // TODO(joth): Consider timing out any pending request.
+ if (request_->is_request_pending())
+ return;
- // Signal to the worker thread that new data is available.
- is_new_data_available_ = true;
- thread_notification_event_.Signal();
+ is_new_data_available_ = false;
+
+ if (access_token_.empty())
+ access_token_store_->GetAccessToken(request_->url(), &access_token_);
+
+ AutoLock data_lock(data_mutex_);
+ request_->MakeRequest(access_token_, radio_data_, wifi_data_,
+ device_data_updated_timestamp_);
}
-#endif // if 0
+void NetworkLocationProvider::OnDeviceDataUpdated() {
+ if (MessageLoop::current() != client_loop()) {
+ client_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &NetworkLocationProvider::OnDeviceDataUpdated));
+ return;
+ }
+ device_data_updated_timestamp_ = GetCurrentTimeMillis();
+
+ is_new_data_available_ = is_radio_data_complete_ || is_radio_data_complete_;
+ if (delayed_start_task_.empty() ||
+ (is_radio_data_complete_ && is_radio_data_complete_)) {
+ UpdatePosition();
+ }
+}
« no previous file with comments | « chrome/browser/geolocation/network_location_provider.h ('k') | chrome/browser/geolocation/network_location_request.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698