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

Side by Side Diff: components/ntp_snippets/remote/scheduling_remote_suggestions_provider.cc

Issue 2699613002: [remote suggestions] Add separte fetch interval for NTP open trigger (Closed)
Patch Set: remove extra test - will have a separate CL Created 3 years, 10 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
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 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 "components/ntp_snippets/remote/scheduling_remote_suggestions_provider. h" 5 #include "components/ntp_snippets/remote/scheduling_remote_suggestions_provider. h"
6 6
7 #include <random> 7 #include <random>
8 #include <string> 8 #include <string>
9 #include <utility> 9 #include <utility>
10 10
(...skipping 19 matching lines...) Expand all
30 // for each interval. Initially all the timers are started at the same time. 30 // for each interval. Initially all the timers are started at the same time.
31 // Fetches are 31 // Fetches are
32 // only performed when certain conditions associated with the intervals are 32 // only performed when certain conditions associated with the intervals are
33 // met. If a fetch failed, then only the corresponding timer is reset. The 33 // met. If a fetch failed, then only the corresponding timer is reset. The
34 // other timers are not touched. 34 // other timers are not touched.
35 // TODO(markusheintz): Describe the individual intervals. 35 // TODO(markusheintz): Describe the individual intervals.
36 enum class FetchingInterval { 36 enum class FetchingInterval {
37 PERSISTENT_FALLBACK, 37 PERSISTENT_FALLBACK,
38 PERSISTENT_WIFI, 38 PERSISTENT_WIFI,
39 SOFT_ON_USAGE_EVENT, 39 SOFT_ON_USAGE_EVENT,
40 SOFT_ON_NTP_OPENED,
40 COUNT 41 COUNT
41 }; 42 };
42 43
43 // The following arrays specify default values for remote suggestions fetch 44 // The following arrays specify default values for remote suggestions fetch
44 // intervals corresponding to individual user classes. The user classes are 45 // intervals corresponding to individual user classes. The user classes are
45 // defined by the user classifier. There must be an array for each user class. 46 // defined by the user classifier. There must be an array for each user class.
46 // The values of each array specify a default time interval for the intervals 47 // The values of each array specify a default time interval for the intervals
47 // defined by the enum FetchingInterval. The default time intervals defined in 48 // defined by the enum FetchingInterval. The default time intervals defined in
48 // the arrays can be overridden using different variation parameters. 49 // the arrays can be overridden using different variation parameters.
49 const double kDefaultFetchingIntervalHoursRareNtpUser[] = {48.0, 24.0, 12.0}; 50 const double kDefaultFetchingIntervalHoursRareNtpUser[] = {48.0, 24.0, 12.0,
50 const double kDefaultFetchingIntervalHoursActiveNtpUser[] = {24.0, 6.0, 2.0}; 51 6.0};
52 const double kDefaultFetchingIntervalHoursActiveNtpUser[] = {24.0, 6.0, 2.0,
53 2.0};
51 const double kDefaultFetchingIntervalHoursActiveSuggestionsConsumer[] = { 54 const double kDefaultFetchingIntervalHoursActiveSuggestionsConsumer[] = {
52 24.0, 6.0, 2.0}; 55 24.0, 6.0, 2.0, 1.0};
53 56
54 // Variation parameters than can be used to override the default fetching 57 // Variation parameters than can be used to override the default fetching
55 // intervals. 58 // intervals.
56 const char* kFetchingIntervalParamNameRareNtpUser[] = { 59 const char* kFetchingIntervalParamNameRareNtpUser[] = {
57 "fetching_interval_hours-fallback-rare_ntp_user", 60 "fetching_interval_hours-fallback-rare_ntp_user",
58 "fetching_interval_hours-wifi-rare_ntp_user", 61 "fetching_interval_hours-wifi-rare_ntp_user",
59 "soft_fetching_interval_hours-active-rare_ntp_user"}; 62 "soft_fetching_interval_hours-active-rare_ntp_user",
63 "soft_on_ntp_opened_interval_hours-rare_ntp_user"};
60 const char* kFetchingIntervalParamNameActiveNtpUser[] = { 64 const char* kFetchingIntervalParamNameActiveNtpUser[] = {
61 "fetching_interval_hours-fallback-active_ntp_user", 65 "fetching_interval_hours-fallback-active_ntp_user",
62 "fetching_interval_hours-wifi-active_ntp_user", 66 "fetching_interval_hours-wifi-active_ntp_user",
63 "soft_fetching_interval_hours-active-active_ntp_user"}; 67 "soft_fetching_interval_hours-active-active_ntp_user",
68 "soft_on_ntp_opened_interval_hours-active_ntp_user"};
64 const char* kFetchingIntervalParamNameActiveSuggestionsConsumer[] = { 69 const char* kFetchingIntervalParamNameActiveSuggestionsConsumer[] = {
65 "fetching_interval_hours-fallback-active_suggestions_consumer", 70 "fetching_interval_hours-fallback-active_suggestions_consumer",
66 "fetching_interval_hours-wifi-active_suggestions_consumer", 71 "fetching_interval_hours-wifi-active_suggestions_consumer",
67 "soft_fetching_interval_hours-active-active_suggestions_consumer"}; 72 "soft_fetching_interval_hours-active-active_suggestions_consumer",
73 "soft_on_ntp_opened_interval_hours-active_suggestions_consumer"};
68 74
69 static_assert( 75 static_assert(
70 static_cast<unsigned int>(FetchingInterval::COUNT) == 76 static_cast<unsigned int>(FetchingInterval::COUNT) ==
71 arraysize(kDefaultFetchingIntervalHoursRareNtpUser) && 77 arraysize(kDefaultFetchingIntervalHoursRareNtpUser) &&
72 static_cast<unsigned int>(FetchingInterval::COUNT) == 78 static_cast<unsigned int>(FetchingInterval::COUNT) ==
73 arraysize(kDefaultFetchingIntervalHoursActiveNtpUser) && 79 arraysize(kDefaultFetchingIntervalHoursActiveNtpUser) &&
74 static_cast<unsigned int>(FetchingInterval::COUNT) == 80 static_cast<unsigned int>(FetchingInterval::COUNT) ==
75 arraysize(kDefaultFetchingIntervalHoursActiveSuggestionsConsumer) && 81 arraysize(kDefaultFetchingIntervalHoursActiveSuggestionsConsumer) &&
76 static_cast<unsigned int>(FetchingInterval::COUNT) == 82 static_cast<unsigned int>(FetchingInterval::COUNT) ==
77 arraysize(kFetchingIntervalParamNameRareNtpUser) && 83 arraysize(kFetchingIntervalParamNameRareNtpUser) &&
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
124 130
125 return base::TimeDelta::FromSecondsD(value_hours * 3600.0); 131 return base::TimeDelta::FromSecondsD(value_hours * 3600.0);
126 } 132 }
127 133
128 } // namespace 134 } // namespace
129 135
130 // static 136 // static
131 SchedulingRemoteSuggestionsProvider::FetchingSchedule 137 SchedulingRemoteSuggestionsProvider::FetchingSchedule
132 SchedulingRemoteSuggestionsProvider::FetchingSchedule::Empty() { 138 SchedulingRemoteSuggestionsProvider::FetchingSchedule::Empty() {
133 return FetchingSchedule{base::TimeDelta(), base::TimeDelta(), 139 return FetchingSchedule{base::TimeDelta(), base::TimeDelta(),
134 base::TimeDelta()}; 140 base::TimeDelta(), base::TimeDelta()};
135 } 141 }
136 142
137 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator==( 143 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator==(
138 const FetchingSchedule& other) const { 144 const FetchingSchedule& other) const {
139 return interval_persistent_wifi == other.interval_persistent_wifi && 145 return interval_persistent_wifi == other.interval_persistent_wifi &&
140 interval_persistent_fallback == other.interval_persistent_fallback && 146 interval_persistent_fallback == other.interval_persistent_fallback &&
141 interval_soft_on_usage_event == other.interval_soft_on_usage_event; 147 interval_soft_on_usage_event == other.interval_soft_on_usage_event &&
148 interval_soft_on_ntp_opened == other.interval_soft_on_ntp_opened;
142 } 149 }
143 150
144 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator!=( 151 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator!=(
145 const FetchingSchedule& other) const { 152 const FetchingSchedule& other) const {
146 return !operator==(other); 153 return !operator==(other);
147 } 154 }
148 155
149 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::is_empty() const { 156 bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::is_empty() const {
150 return interval_persistent_wifi.is_zero() && 157 return interval_persistent_wifi.is_zero() &&
151 interval_persistent_fallback.is_zero() && 158 interval_persistent_fallback.is_zero() &&
152 interval_soft_on_usage_event.is_zero(); 159 interval_soft_on_usage_event.is_zero() &&
160 interval_soft_on_ntp_opened.is_zero();
153 } 161 }
154 162
155 // The TriggerType enum specifies values for the events that can trigger 163 // The TriggerType enum specifies values for the events that can trigger
156 // fetching remote suggestions. These values are written to logs. New enum 164 // fetching remote suggestions. These values are written to logs. New enum
157 // values can be added, but existing enums must never be renumbered or deleted 165 // values can be added, but existing enums must never be renumbered or deleted
158 // and reused. When adding new entries, also update the array 166 // and reused. When adding new entries, also update the array
159 // |kTriggerTypeNames| above. 167 // |kTriggerTypeNames| above.
160 enum class SchedulingRemoteSuggestionsProvider::TriggerType { 168 enum class SchedulingRemoteSuggestionsProvider::TriggerType {
161 PERSISTENT_SCHEDULER_WAKE_UP = 0, 169 PERSISTENT_SCHEDULER_WAKE_UP = 0,
162 NTP_OPENED = 1, 170 NTP_OPENED = 1,
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
198 206
199 // static 207 // static
200 void SchedulingRemoteSuggestionsProvider::RegisterProfilePrefs( 208 void SchedulingRemoteSuggestionsProvider::RegisterProfilePrefs(
201 PrefRegistrySimple* registry) { 209 PrefRegistrySimple* registry) {
202 registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalWifi, 0); 210 registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalWifi, 0);
203 registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalFallback, 211 registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalFallback,
204 0); 212 0);
205 registry->RegisterInt64Pref(prefs::kSnippetSoftFetchingIntervalOnUsageEvent, 213 registry->RegisterInt64Pref(prefs::kSnippetSoftFetchingIntervalOnUsageEvent,
206 0); 214 0);
207 registry->RegisterInt64Pref(prefs::kSnippetLastFetchAttempt, 0); 215 registry->RegisterInt64Pref(prefs::kSnippetLastFetchAttempt, 0);
216 registry->RegisterInt64Pref(prefs::kSnippetSoftFetchingIntervalOnNtpOpened,
217 0);
208 } 218 }
209 219
210 void SchedulingRemoteSuggestionsProvider::RescheduleFetching() { 220 void SchedulingRemoteSuggestionsProvider::RescheduleFetching() {
211 // Force the reschedule by stopping and starting it again. 221 // Force the reschedule by stopping and starting it again.
212 StopScheduling(); 222 StopScheduling();
213 StartScheduling(); 223 StartScheduling();
214 } 224 }
215 225
216 void SchedulingRemoteSuggestionsProvider::OnPersistentSchedulerWakeUp() { 226 void SchedulingRemoteSuggestionsProvider::OnPersistentSchedulerWakeUp() {
217 RefetchInTheBackgroundIfEnabled(TriggerType::PERSISTENT_SCHEDULER_WAKE_UP); 227 RefetchInTheBackgroundIfEnabled(TriggerType::PERSISTENT_SCHEDULER_WAKE_UP);
218 } 228 }
219 229
220 void SchedulingRemoteSuggestionsProvider::OnBrowserForegrounded() { 230 void SchedulingRemoteSuggestionsProvider::OnBrowserForegrounded() {
221 // TODO(jkrcal): Consider that this is called whenever we open or return to an 231 // TODO(jkrcal): Consider that this is called whenever we open or return to an
222 // Activity. Therefore, keep work light for fast start up calls. 232 // Activity. Therefore, keep work light for fast start up calls.
223 if (!ShouldRefetchInTheBackgroundNow()) { 233 if (!ShouldRefetchInTheBackgroundNow(TriggerType::BROWSER_FOREGROUNDED)) {
224 return; 234 return;
225 } 235 }
226 236
227 RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_FOREGROUNDED); 237 RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_FOREGROUNDED);
228 } 238 }
229 239
230 void SchedulingRemoteSuggestionsProvider::OnBrowserColdStart() { 240 void SchedulingRemoteSuggestionsProvider::OnBrowserColdStart() {
231 // TODO(fhorschig|jkrcal): Consider that work here must be kept light for fast 241 // TODO(fhorschig|jkrcal): Consider that work here must be kept light for fast
232 // cold start ups. 242 // cold start ups.
233 if (!ShouldRefetchInTheBackgroundNow()) { 243 if (!ShouldRefetchInTheBackgroundNow(TriggerType::BROWSER_COLD_START)) {
234 return; 244 return;
235 } 245 }
236 246
237 RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_COLD_START); 247 RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_COLD_START);
238 } 248 }
239 249
240 void SchedulingRemoteSuggestionsProvider::OnNTPOpened() { 250 void SchedulingRemoteSuggestionsProvider::OnNTPOpened() {
241 if (!ShouldRefetchInTheBackgroundNow()) { 251 if (!ShouldRefetchInTheBackgroundNow(TriggerType::NTP_OPENED)) {
242 return; 252 return;
243 } 253 }
244 254
245 RefetchInTheBackgroundIfEnabled(TriggerType::NTP_OPENED); 255 RefetchInTheBackgroundIfEnabled(TriggerType::NTP_OPENED);
246 } 256 }
247 257
248 void SchedulingRemoteSuggestionsProvider::SetProviderStatusCallback( 258 void SchedulingRemoteSuggestionsProvider::SetProviderStatusCallback(
249 std::unique_ptr<ProviderStatusCallback> callback) { 259 std::unique_ptr<ProviderStatusCallback> callback) {
250 provider_->SetProviderStatusCallback(std::move(callback)); 260 provider_->SetProviderStatusCallback(std::move(callback));
251 } 261 }
(...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after
389 SchedulingRemoteSuggestionsProvider::GetDesiredFetchingSchedule() const { 399 SchedulingRemoteSuggestionsProvider::GetDesiredFetchingSchedule() const {
390 UserClassifier::UserClass user_class = user_classifier_->GetUserClass(); 400 UserClassifier::UserClass user_class = user_classifier_->GetUserClass();
391 401
392 FetchingSchedule schedule; 402 FetchingSchedule schedule;
393 schedule.interval_persistent_wifi = 403 schedule.interval_persistent_wifi =
394 GetDesiredFetchingInterval(FetchingInterval::PERSISTENT_WIFI, user_class); 404 GetDesiredFetchingInterval(FetchingInterval::PERSISTENT_WIFI, user_class);
395 schedule.interval_persistent_fallback = GetDesiredFetchingInterval( 405 schedule.interval_persistent_fallback = GetDesiredFetchingInterval(
396 FetchingInterval::PERSISTENT_FALLBACK, user_class); 406 FetchingInterval::PERSISTENT_FALLBACK, user_class);
397 schedule.interval_soft_on_usage_event = GetDesiredFetchingInterval( 407 schedule.interval_soft_on_usage_event = GetDesiredFetchingInterval(
398 FetchingInterval::SOFT_ON_USAGE_EVENT, user_class); 408 FetchingInterval::SOFT_ON_USAGE_EVENT, user_class);
409 schedule.interval_soft_on_ntp_opened = GetDesiredFetchingInterval(
410 FetchingInterval::SOFT_ON_NTP_OPENED, user_class);
399 411
400 return schedule; 412 return schedule;
401 } 413 }
402 414
403 void SchedulingRemoteSuggestionsProvider::LoadLastFetchingSchedule() { 415 void SchedulingRemoteSuggestionsProvider::LoadLastFetchingSchedule() {
404 schedule_.interval_persistent_wifi = base::TimeDelta::FromInternalValue( 416 schedule_.interval_persistent_wifi = base::TimeDelta::FromInternalValue(
405 pref_service_->GetInt64(prefs::kSnippetPersistentFetchingIntervalWifi)); 417 pref_service_->GetInt64(prefs::kSnippetPersistentFetchingIntervalWifi));
406 schedule_.interval_persistent_fallback = 418 schedule_.interval_persistent_fallback =
407 base::TimeDelta::FromInternalValue(pref_service_->GetInt64( 419 base::TimeDelta::FromInternalValue(pref_service_->GetInt64(
408 prefs::kSnippetPersistentFetchingIntervalFallback)); 420 prefs::kSnippetPersistentFetchingIntervalFallback));
409 schedule_.interval_soft_on_usage_event = base::TimeDelta::FromInternalValue( 421 schedule_.interval_soft_on_usage_event = base::TimeDelta::FromInternalValue(
410 pref_service_->GetInt64(prefs::kSnippetSoftFetchingIntervalOnUsageEvent)); 422 pref_service_->GetInt64(prefs::kSnippetSoftFetchingIntervalOnUsageEvent));
423 schedule_.interval_soft_on_ntp_opened = base::TimeDelta::FromInternalValue(
424 pref_service_->GetInt64(prefs::kSnippetSoftFetchingIntervalOnNtpOpened));
411 } 425 }
412 426
413 void SchedulingRemoteSuggestionsProvider::StoreFetchingSchedule() { 427 void SchedulingRemoteSuggestionsProvider::StoreFetchingSchedule() {
414 pref_service_->SetInt64(prefs::kSnippetPersistentFetchingIntervalWifi, 428 pref_service_->SetInt64(prefs::kSnippetPersistentFetchingIntervalWifi,
415 schedule_.interval_persistent_wifi.ToInternalValue()); 429 schedule_.interval_persistent_wifi.ToInternalValue());
416 pref_service_->SetInt64( 430 pref_service_->SetInt64(
417 prefs::kSnippetPersistentFetchingIntervalFallback, 431 prefs::kSnippetPersistentFetchingIntervalFallback,
418 schedule_.interval_persistent_fallback.ToInternalValue()); 432 schedule_.interval_persistent_fallback.ToInternalValue());
419 pref_service_->SetInt64( 433 pref_service_->SetInt64(
420 prefs::kSnippetSoftFetchingIntervalOnUsageEvent, 434 prefs::kSnippetSoftFetchingIntervalOnUsageEvent,
421 schedule_.interval_soft_on_usage_event.ToInternalValue()); 435 schedule_.interval_soft_on_usage_event.ToInternalValue());
436 pref_service_->SetInt64(
437 prefs::kSnippetSoftFetchingIntervalOnNtpOpened,
438 schedule_.interval_soft_on_ntp_opened.ToInternalValue());
422 } 439 }
423 440
424 void SchedulingRemoteSuggestionsProvider::RefetchInTheBackgroundIfEnabled( 441 void SchedulingRemoteSuggestionsProvider::RefetchInTheBackgroundIfEnabled(
425 SchedulingRemoteSuggestionsProvider::TriggerType trigger) { 442 SchedulingRemoteSuggestionsProvider::TriggerType trigger) {
426 if (BackgroundFetchesDisabled(trigger)) { 443 if (BackgroundFetchesDisabled(trigger)) {
427 return; 444 return;
428 } 445 }
429 446
430 UMA_HISTOGRAM_ENUMERATION( 447 UMA_HISTOGRAM_ENUMERATION(
431 "NewTabPage.ContentSuggestions.BackgroundFetchTrigger", 448 "NewTabPage.ContentSuggestions.BackgroundFetchTrigger",
432 static_cast<int>(trigger), static_cast<int>(TriggerType::COUNT)); 449 static_cast<int>(trigger), static_cast<int>(TriggerType::COUNT));
433 450
434 RefetchInTheBackground(/*callback=*/nullptr); 451 RefetchInTheBackground(/*callback=*/nullptr);
435 } 452 }
436 453
437 bool SchedulingRemoteSuggestionsProvider::ShouldRefetchInTheBackgroundNow() { 454 bool SchedulingRemoteSuggestionsProvider::ShouldRefetchInTheBackgroundNow(
438 base::Time first_allowed_fetch_time = 455 SchedulingRemoteSuggestionsProvider::TriggerType trigger) {
439 base::Time::FromInternalValue( 456 const base::Time last_fetch_attempt_time = base::Time::FromInternalValue(
440 pref_service_->GetInt64(prefs::kSnippetLastFetchAttempt)) + 457 pref_service_->GetInt64(prefs::kSnippetLastFetchAttempt));
441 schedule_.interval_soft_on_usage_event; 458 base::Time first_allowed_fetch_time;
459 switch (trigger) {
460 case TriggerType::NTP_OPENED:
461 first_allowed_fetch_time =
462 last_fetch_attempt_time + schedule_.interval_soft_on_ntp_opened;
463 break;
464 case TriggerType::BROWSER_FOREGROUNDED:
465 case TriggerType::BROWSER_COLD_START:
466 first_allowed_fetch_time =
467 last_fetch_attempt_time + schedule_.interval_soft_on_usage_event;
468 break;
469 case TriggerType::PERSISTENT_SCHEDULER_WAKE_UP:
470 case TriggerType::COUNT:
471 NOTREACHED();
472 break;
473 }
442 return first_allowed_fetch_time <= clock_->Now(); 474 return first_allowed_fetch_time <= clock_->Now();
443 } 475 }
444 476
445 bool SchedulingRemoteSuggestionsProvider::BackgroundFetchesDisabled( 477 bool SchedulingRemoteSuggestionsProvider::BackgroundFetchesDisabled(
446 SchedulingRemoteSuggestionsProvider::TriggerType trigger) const { 478 SchedulingRemoteSuggestionsProvider::TriggerType trigger) const {
447 if (schedule_.is_empty()) { 479 if (schedule_.is_empty()) {
448 return true; // Background fetches are disabled in general. 480 return true; // Background fetches are disabled in general.
449 } 481 }
450 482
451 if (enabled_triggers_.count(trigger) == 0) { 483 if (enabled_triggers_.count(trigger) == 0) {
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
528 return enabled_types; 560 return enabled_types;
529 } 561 }
530 562
531 std::set<SchedulingRemoteSuggestionsProvider::TriggerType> 563 std::set<SchedulingRemoteSuggestionsProvider::TriggerType>
532 SchedulingRemoteSuggestionsProvider::GetDefaultEnabledTriggerTypes() { 564 SchedulingRemoteSuggestionsProvider::GetDefaultEnabledTriggerTypes() {
533 return {TriggerType::PERSISTENT_SCHEDULER_WAKE_UP, TriggerType::NTP_OPENED, 565 return {TriggerType::PERSISTENT_SCHEDULER_WAKE_UP, TriggerType::NTP_OPENED,
534 TriggerType::BROWSER_FOREGROUNDED}; 566 TriggerType::BROWSER_FOREGROUNDED};
535 } 567 }
536 568
537 } // namespace ntp_snippets 569 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698