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

Side by Side Diff: sync/engine/sync_scheduler_impl.cc

Issue 488843002: [Sync] Add support for server controlled nudge delays (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Self review Created 6 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 | 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 "sync/engine/sync_scheduler_impl.h" 5 #include "sync/engine/sync_scheduler_impl.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <cstring> 8 #include <cstring>
9 9
10 #include "base/auto_reset.h" 10 #include "base/auto_reset.h"
(...skipping 14 matching lines...) Expand all
25 using base::TimeTicks; 25 using base::TimeTicks;
26 26
27 namespace syncer { 27 namespace syncer {
28 28
29 using sessions::SyncSession; 29 using sessions::SyncSession;
30 using sessions::SyncSessionSnapshot; 30 using sessions::SyncSessionSnapshot;
31 using sync_pb::GetUpdatesCallerInfo; 31 using sync_pb::GetUpdatesCallerInfo;
32 32
33 namespace { 33 namespace {
34 34
35 // Delays for syncer nudges.
36 const int kDefaultNudgeDelayMilliseconds = 200;
37 const int kSlowNudgeDelayMilliseconds = 2000;
38 const int kDefaultSessionsCommitDelaySeconds = 10;
39 const int kSyncRefreshDelayMsec = 500;
40 const int kSyncSchedulerDelayMsec = 250;
41
42 bool IsConfigRelatedUpdateSourceValue(
43 GetUpdatesCallerInfo::GetUpdatesSource source) {
44 switch (source) {
45 case GetUpdatesCallerInfo::RECONFIGURATION:
46 case GetUpdatesCallerInfo::MIGRATION:
47 case GetUpdatesCallerInfo::NEW_CLIENT:
48 case GetUpdatesCallerInfo::NEWLY_SUPPORTED_DATATYPE:
49 return true;
50 default:
51 return false;
52 }
53 }
54
35 bool ShouldRequestEarlyExit(const SyncProtocolError& error) { 55 bool ShouldRequestEarlyExit(const SyncProtocolError& error) {
36 switch (error.error_type) { 56 switch (error.error_type) {
37 case SYNC_SUCCESS: 57 case SYNC_SUCCESS:
38 case MIGRATION_DONE: 58 case MIGRATION_DONE:
39 case THROTTLED: 59 case THROTTLED:
40 case TRANSIENT_ERROR: 60 case TRANSIENT_ERROR:
41 return false; 61 return false;
42 case NOT_MY_BIRTHDAY: 62 case NOT_MY_BIRTHDAY:
43 case CLEAR_PENDING: 63 case CLEAR_PENDING:
44 case DISABLED_BY_ADMIN: 64 case DISABLED_BY_ADMIN:
(...skipping 13 matching lines...) Expand all
58 default: 78 default:
59 NOTREACHED(); 79 NOTREACHED();
60 return false; 80 return false;
61 } 81 }
62 } 82 }
63 83
64 bool IsActionableError( 84 bool IsActionableError(
65 const SyncProtocolError& error) { 85 const SyncProtocolError& error) {
66 return (error.action != UNKNOWN_ACTION); 86 return (error.action != UNKNOWN_ACTION);
67 } 87 }
88
89 TimeDelta GetDefaultDelayForType(ModelType model_type,
90 TimeDelta minimum_delay) {
91 switch (model_type) {
92 case AUTOFILL:
93 // Accompany types rely on nudges from other types, and hence have long
94 // nudge delays.
95 return TimeDelta::FromSeconds(kDefaultShortPollIntervalSeconds);
96 case BOOKMARKS:
97 case PREFERENCES:
98 // Types with sometimes automatic changes get longer delays to allow more
99 // coalescing.
100 return TimeDelta::FromMilliseconds(kSlowNudgeDelayMilliseconds);
101 case SESSIONS:
102 case FAVICON_IMAGES:
103 case FAVICON_TRACKING:
104 // Types with navigation triggered changes get longer delays to allow more
105 // coalescing.
106 return TimeDelta::FromSeconds(kDefaultSessionsCommitDelaySeconds);
107 default:
108 return minimum_delay;
109 }
110 }
111
68 } // namespace 112 } // namespace
69 113
70 ConfigurationParams::ConfigurationParams() 114 ConfigurationParams::ConfigurationParams()
71 : source(GetUpdatesCallerInfo::UNKNOWN) {} 115 : source(GetUpdatesCallerInfo::UNKNOWN) {}
72 ConfigurationParams::ConfigurationParams( 116 ConfigurationParams::ConfigurationParams(
73 const sync_pb::GetUpdatesCallerInfo::GetUpdatesSource& source, 117 const sync_pb::GetUpdatesCallerInfo::GetUpdatesSource& source,
74 ModelTypeSet types_to_download, 118 ModelTypeSet types_to_download,
75 const ModelSafeRoutingInfo& routing_info, 119 const ModelSafeRoutingInfo& routing_info,
76 const base::Closure& ready_task, 120 const base::Closure& ready_task,
77 const base::Closure& retry_task) 121 const base::Closure& retry_task)
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
125 // Helper macros to log with the syncer thread name; useful when there 169 // Helper macros to log with the syncer thread name; useful when there
126 // are multiple syncer threads involved. 170 // are multiple syncer threads involved.
127 171
128 #define SLOG(severity) LOG(severity) << name_ << ": " 172 #define SLOG(severity) LOG(severity) << name_ << ": "
129 173
130 #define SDVLOG(verbose_level) DVLOG(verbose_level) << name_ << ": " 174 #define SDVLOG(verbose_level) DVLOG(verbose_level) << name_ << ": "
131 175
132 #define SDVLOG_LOC(from_here, verbose_level) \ 176 #define SDVLOG_LOC(from_here, verbose_level) \
133 DVLOG_LOC(from_here, verbose_level) << name_ << ": " 177 DVLOG_LOC(from_here, verbose_level) << name_ << ": "
134 178
135 namespace {
136
137 const int kDefaultSessionsCommitDelaySeconds = 10;
138
139 bool IsConfigRelatedUpdateSourceValue(
140 GetUpdatesCallerInfo::GetUpdatesSource source) {
141 switch (source) {
142 case GetUpdatesCallerInfo::RECONFIGURATION:
143 case GetUpdatesCallerInfo::MIGRATION:
144 case GetUpdatesCallerInfo::NEW_CLIENT:
145 case GetUpdatesCallerInfo::NEWLY_SUPPORTED_DATATYPE:
146 return true;
147 default:
148 return false;
149 }
150 }
151
152 } // namespace
153
154 SyncSchedulerImpl::SyncSchedulerImpl(const std::string& name, 179 SyncSchedulerImpl::SyncSchedulerImpl(const std::string& name,
155 BackoffDelayProvider* delay_provider, 180 BackoffDelayProvider* delay_provider,
156 sessions::SyncSessionContext* context, 181 sessions::SyncSessionContext* context,
157 Syncer* syncer) 182 Syncer* syncer)
158 : name_(name), 183 : name_(name),
159 started_(false), 184 started_(false),
160 syncer_short_poll_interval_seconds_( 185 syncer_short_poll_interval_seconds_(
161 TimeDelta::FromSeconds(kDefaultShortPollIntervalSeconds)), 186 TimeDelta::FromSeconds(kDefaultShortPollIntervalSeconds)),
162 syncer_long_poll_interval_seconds_( 187 syncer_long_poll_interval_seconds_(
163 TimeDelta::FromSeconds(kDefaultLongPollIntervalSeconds)), 188 TimeDelta::FromSeconds(kDefaultLongPollIntervalSeconds)),
164 sessions_commit_delay_( 189 minimum_nudge_delay_ms_(kDefaultNudgeDelayMilliseconds),
165 TimeDelta::FromSeconds(kDefaultSessionsCommitDelaySeconds)),
166 mode_(NORMAL_MODE), 190 mode_(NORMAL_MODE),
167 delay_provider_(delay_provider), 191 delay_provider_(delay_provider),
168 syncer_(syncer), 192 syncer_(syncer),
169 session_context_(context), 193 session_context_(context),
170 no_scheduling_allowed_(false), 194 no_scheduling_allowed_(false),
171 do_poll_after_credentials_updated_(false), 195 do_poll_after_credentials_updated_(false),
172 next_sync_session_job_priority_(NORMAL_PRIORITY), 196 next_sync_session_job_priority_(NORMAL_PRIORITY),
173 weak_ptr_factory_(this), 197 weak_ptr_factory_(this),
174 weak_ptr_factory_for_weak_handle_(this) { 198 weak_ptr_factory_for_weak_handle_(this) {
175 weak_handle_this_ = MakeWeakHandle( 199 weak_handle_this_ = MakeWeakHandle(
176 weak_ptr_factory_for_weak_handle_.GetWeakPtr()); 200 weak_ptr_factory_for_weak_handle_.GetWeakPtr());
201 ModelTypeSet protocol_types = ProtocolTypes();
202 for (ModelTypeSet::Iterator iter = protocol_types.First();
203 iter.Good(); iter.Inc()) {
204 nudge_delays_[iter.Get()] = GetDefaultDelayForType(
205 iter.Get(), TimeDelta::FromMilliseconds(minimum_nudge_delay_ms_));
206 }
177 } 207 }
178 208
179 SyncSchedulerImpl::~SyncSchedulerImpl() { 209 SyncSchedulerImpl::~SyncSchedulerImpl() {
180 DCHECK(CalledOnValidThread()); 210 DCHECK(CalledOnValidThread());
181 Stop(); 211 Stop();
182 } 212 }
183 213
184 void SyncSchedulerImpl::OnCredentialsUpdated() { 214 void SyncSchedulerImpl::OnCredentialsUpdated() {
185 DCHECK(CalledOnValidThread()); 215 DCHECK(CalledOnValidThread());
186 216
(...skipping 162 matching lines...) Expand 10 before | Expand all | Expand 10 after
349 379
350 if (mode_ == CONFIGURATION_MODE) { 380 if (mode_ == CONFIGURATION_MODE) {
351 SDVLOG(1) << "Not running nudge because we're in configuration mode."; 381 SDVLOG(1) << "Not running nudge because we're in configuration mode.";
352 return false; 382 return false;
353 } 383 }
354 384
355 return true; 385 return true;
356 } 386 }
357 387
358 void SyncSchedulerImpl::ScheduleLocalNudge( 388 void SyncSchedulerImpl::ScheduleLocalNudge(
359 const TimeDelta& desired_delay,
360 ModelTypeSet types, 389 ModelTypeSet types,
361 const tracked_objects::Location& nudge_location) { 390 const tracked_objects::Location& nudge_location) {
362 DCHECK(CalledOnValidThread()); 391 DCHECK(CalledOnValidThread());
363 DCHECK(!types.Empty()); 392 DCHECK(!types.Empty());
364 393
365 SDVLOG_LOC(nudge_location, 2) 394 SDVLOG_LOC(nudge_location, 2)
366 << "Scheduling sync because of local change to " 395 << "Scheduling sync because of local change to "
367 << ModelTypeSetToString(types); 396 << ModelTypeSetToString(types);
368 UpdateNudgeTimeRecords(types); 397 UpdateNudgeTimeRecords(types);
369 nudge_tracker_.RecordLocalChange(types); 398 nudge_tracker_.RecordLocalChange(types);
370 ScheduleNudgeImpl(desired_delay, nudge_location); 399 ScheduleNudgeImpl(GetNudgeDelayForTypes(types), nudge_location);
371 } 400 }
372 401
373 void SyncSchedulerImpl::ScheduleLocalRefreshRequest( 402 void SyncSchedulerImpl::ScheduleLocalRefreshRequest(
374 const TimeDelta& desired_delay,
375 ModelTypeSet types, 403 ModelTypeSet types,
376 const tracked_objects::Location& nudge_location) { 404 const tracked_objects::Location& nudge_location) {
377 DCHECK(CalledOnValidThread()); 405 DCHECK(CalledOnValidThread());
378 DCHECK(!types.Empty()); 406 DCHECK(!types.Empty());
379 407
380 SDVLOG_LOC(nudge_location, 2) 408 SDVLOG_LOC(nudge_location, 2)
381 << "Scheduling sync because of local refresh request for " 409 << "Scheduling sync because of local refresh request for "
382 << ModelTypeSetToString(types); 410 << ModelTypeSetToString(types);
383 nudge_tracker_.RecordLocalRefreshRequest(types); 411 nudge_tracker_.RecordLocalRefreshRequest(types);
384 ScheduleNudgeImpl(desired_delay, nudge_location); 412 ScheduleNudgeImpl(base::TimeDelta::FromMilliseconds(kSyncRefreshDelayMsec),
413 nudge_location);
385 } 414 }
386 415
387 void SyncSchedulerImpl::ScheduleInvalidationNudge( 416 void SyncSchedulerImpl::ScheduleInvalidationNudge(
388 const TimeDelta& desired_delay,
389 syncer::ModelType model_type, 417 syncer::ModelType model_type,
390 scoped_ptr<InvalidationInterface> invalidation, 418 scoped_ptr<InvalidationInterface> invalidation,
391 const tracked_objects::Location& nudge_location) { 419 const tracked_objects::Location& nudge_location) {
392 DCHECK(CalledOnValidThread()); 420 DCHECK(CalledOnValidThread());
393 421
394 SDVLOG_LOC(nudge_location, 2) 422 SDVLOG_LOC(nudge_location, 2)
395 << "Scheduling sync because we received invalidation for " 423 << "Scheduling sync because we received invalidation for "
396 << ModelTypeToString(model_type); 424 << ModelTypeToString(model_type);
397 nudge_tracker_.RecordRemoteInvalidation(model_type, invalidation.Pass()); 425 nudge_tracker_.RecordRemoteInvalidation(model_type, invalidation.Pass());
398 ScheduleNudgeImpl(desired_delay, nudge_location); 426 ScheduleNudgeImpl(base::TimeDelta::FromMilliseconds(kSyncSchedulerDelayMsec),
427 nudge_location);
399 } 428 }
400 429
401 void SyncSchedulerImpl::ScheduleInitialSyncNudge(syncer::ModelType model_type) { 430 void SyncSchedulerImpl::ScheduleInitialSyncNudge(syncer::ModelType model_type) {
402 DCHECK(CalledOnValidThread()); 431 DCHECK(CalledOnValidThread());
403 432
404 SDVLOG(2) << "Scheduling non-blocking initial sync for " 433 SDVLOG(2) << "Scheduling non-blocking initial sync for "
405 << ModelTypeToString(model_type); 434 << ModelTypeToString(model_type);
406 nudge_tracker_.RecordInitialSyncRequired(model_type); 435 nudge_tracker_.RecordInitialSyncRequired(model_type);
407 ScheduleNudgeImpl(TimeDelta::FromSeconds(0), FROM_HERE); 436 ScheduleNudgeImpl(TimeDelta::FromSeconds(0), FROM_HERE);
408 } 437 }
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
454 } 483 }
455 484
456 const char* SyncSchedulerImpl::GetModeString(SyncScheduler::Mode mode) { 485 const char* SyncSchedulerImpl::GetModeString(SyncScheduler::Mode mode) {
457 switch (mode) { 486 switch (mode) {
458 ENUM_CASE(CONFIGURATION_MODE); 487 ENUM_CASE(CONFIGURATION_MODE);
459 ENUM_CASE(NORMAL_MODE); 488 ENUM_CASE(NORMAL_MODE);
460 } 489 }
461 return ""; 490 return "";
462 } 491 }
463 492
493 void SyncSchedulerImpl::SetDefaultNudgeDelay(int delay_ms) {
494 minimum_nudge_delay_ms_ = delay_ms;
495 ModelTypeSet protocol_types = syncer::ProtocolTypes();
496 for (ModelTypeSet::Iterator iter = protocol_types.First(); iter.Good();
497 iter.Inc()) {
498 nudge_delays_[iter.Get()] = GetDefaultDelayForType(
499 iter.Get(), TimeDelta::FromMilliseconds(minimum_nudge_delay_ms_));
500 }
501 }
502
464 void SyncSchedulerImpl::DoNudgeSyncSessionJob(JobPriority priority) { 503 void SyncSchedulerImpl::DoNudgeSyncSessionJob(JobPriority priority) {
465 DCHECK(CalledOnValidThread()); 504 DCHECK(CalledOnValidThread());
466 DCHECK(CanRunNudgeJobNow(priority)); 505 DCHECK(CanRunNudgeJobNow(priority));
467 506
468 DVLOG(2) << "Will run normal mode sync cycle with types " 507 DVLOG(2) << "Will run normal mode sync cycle with types "
469 << ModelTypeSetToString(session_context_->GetEnabledTypes()); 508 << ModelTypeSetToString(session_context_->GetEnabledTypes());
470 scoped_ptr<SyncSession> session(SyncSession::Build(session_context_, this)); 509 scoped_ptr<SyncSession> session(SyncSession::Build(session_context_, this));
471 bool premature_exit = !syncer_->NormalSyncShare( 510 bool premature_exit = !syncer_->NormalSyncShare(
472 GetEnabledAndUnthrottledTypes(), 511 GetEnabledAndUnthrottledTypes(),
473 nudge_tracker_, 512 nudge_tracker_,
(...skipping 338 matching lines...) Expand 10 before | Expand all | Expand 10 after
812 *session_context_->listeners(), 851 *session_context_->listeners(),
813 OnThrottledTypesChanged(types)); 852 OnThrottledTypesChanged(types));
814 } 853 }
815 854
816 bool SyncSchedulerImpl::IsBackingOff() const { 855 bool SyncSchedulerImpl::IsBackingOff() const {
817 DCHECK(CalledOnValidThread()); 856 DCHECK(CalledOnValidThread());
818 return wait_interval_.get() && wait_interval_->mode == 857 return wait_interval_.get() && wait_interval_->mode ==
819 WaitInterval::EXPONENTIAL_BACKOFF; 858 WaitInterval::EXPONENTIAL_BACKOFF;
820 } 859 }
821 860
861 TimeDelta SyncSchedulerImpl::GetNudgeDelayForTypes(ModelTypeSet types) const {
862 // Start with the longest delay.
863 TimeDelta delay =
864 TimeDelta::FromMilliseconds(kDefaultShortPollIntervalSeconds);
865 // Take the shorted delay from all requested types.
rlarocque 2014/08/20 00:13:31 shortest?
Nicolas Zea 2014/08/20 22:49:43 Done.
866 for (ModelTypeSet::Iterator iter = types.First(); iter.Good(); iter.Inc()) {
867 std::map<ModelType, TimeDelta>::const_iterator delay_iter =
868 nudge_delays_.find(iter.Get());
869 if (delay_iter != nudge_delays_.end() &&
870 delay_iter->second < delay) {
871 delay = delay_iter->second;
872 }
873 }
874 return delay;
875 }
876
822 void SyncSchedulerImpl::OnThrottled(const base::TimeDelta& throttle_duration) { 877 void SyncSchedulerImpl::OnThrottled(const base::TimeDelta& throttle_duration) {
823 DCHECK(CalledOnValidThread()); 878 DCHECK(CalledOnValidThread());
824 wait_interval_.reset(new WaitInterval(WaitInterval::THROTTLED, 879 wait_interval_.reset(new WaitInterval(WaitInterval::THROTTLED,
825 throttle_duration)); 880 throttle_duration));
826 NotifyRetryTime(base::Time::Now() + wait_interval_->length); 881 NotifyRetryTime(base::Time::Now() + wait_interval_->length);
827 NotifyThrottledTypesChanged(ModelTypeSet::All()); 882 NotifyThrottledTypesChanged(ModelTypeSet::All());
828 } 883 }
829 884
830 void SyncSchedulerImpl::OnTypesThrottled( 885 void SyncSchedulerImpl::OnTypesThrottled(
831 ModelTypeSet types, 886 ModelTypeSet types,
(...skipping 23 matching lines...) Expand all
855 DCHECK(CalledOnValidThread()); 910 DCHECK(CalledOnValidThread());
856 syncer_short_poll_interval_seconds_ = new_interval; 911 syncer_short_poll_interval_seconds_ = new_interval;
857 } 912 }
858 913
859 void SyncSchedulerImpl::OnReceivedLongPollIntervalUpdate( 914 void SyncSchedulerImpl::OnReceivedLongPollIntervalUpdate(
860 const base::TimeDelta& new_interval) { 915 const base::TimeDelta& new_interval) {
861 DCHECK(CalledOnValidThread()); 916 DCHECK(CalledOnValidThread());
862 syncer_long_poll_interval_seconds_ = new_interval; 917 syncer_long_poll_interval_seconds_ = new_interval;
863 } 918 }
864 919
865 void SyncSchedulerImpl::OnReceivedSessionsCommitDelay( 920 void SyncSchedulerImpl::OnReceivedCustomNudgeDelays(
866 const base::TimeDelta& new_delay) { 921 const std::map<ModelType, int>& nudge_delays) {
rlarocque 2014/08/20 00:13:31 I'd prefer it if this parameter were a map<ModelTy
Nicolas Zea 2014/08/20 22:49:43 Done.
867 DCHECK(CalledOnValidThread()); 922 DCHECK(CalledOnValidThread());
868 sessions_commit_delay_ = new_delay; 923 for (std::map<ModelType, int>::const_iterator iter = nudge_delays.begin();
924 iter != nudge_delays.end(); ++iter) {
925 // Only accept delays that are longer or equal to the minimum delay.
926 // Otherwise reset to default for that type. This provides a way for the
927 // server to "unset" a custom nudge delay by assigning a 0 value to it.
928 if (iter->second >= minimum_nudge_delay_ms_) {
929 nudge_delays_[iter->first] = TimeDelta::FromMilliseconds(iter->second);
930 } else {
931 nudge_delays_[iter->first] = GetDefaultDelayForType(
932 iter->first,
933 base::TimeDelta::FromMilliseconds(minimum_nudge_delay_ms_));
934 }
935 }
869 } 936 }
870 937
871 void SyncSchedulerImpl::OnReceivedClientInvalidationHintBufferSize(int size) { 938 void SyncSchedulerImpl::OnReceivedClientInvalidationHintBufferSize(int size) {
872 if (size > 0) 939 if (size > 0)
873 nudge_tracker_.SetHintBufferSize(size); 940 nudge_tracker_.SetHintBufferSize(size);
874 else 941 else
875 NOTREACHED() << "Hint buffer size should be > 0."; 942 NOTREACHED() << "Hint buffer size should be > 0.";
876 } 943 }
877 944
878 void SyncSchedulerImpl::OnSyncProtocolError( 945 void SyncSchedulerImpl::OnSyncProtocolError(
(...skipping 25 matching lines...) Expand all
904 971
905 void SyncSchedulerImpl::SetNotificationsEnabled(bool notifications_enabled) { 972 void SyncSchedulerImpl::SetNotificationsEnabled(bool notifications_enabled) {
906 DCHECK(CalledOnValidThread()); 973 DCHECK(CalledOnValidThread());
907 session_context_->set_notifications_enabled(notifications_enabled); 974 session_context_->set_notifications_enabled(notifications_enabled);
908 if (notifications_enabled) 975 if (notifications_enabled)
909 nudge_tracker_.OnInvalidationsEnabled(); 976 nudge_tracker_.OnInvalidationsEnabled();
910 else 977 else
911 nudge_tracker_.OnInvalidationsDisabled(); 978 nudge_tracker_.OnInvalidationsDisabled();
912 } 979 }
913 980
914 base::TimeDelta SyncSchedulerImpl::GetSessionsCommitDelay() const {
915 DCHECK(CalledOnValidThread());
916 return sessions_commit_delay_;
917 }
918
919 #undef SDVLOG_LOC 981 #undef SDVLOG_LOC
920 982
921 #undef SDVLOG 983 #undef SDVLOG
922 984
923 #undef SLOG 985 #undef SLOG
924 986
925 #undef ENUM_CASE 987 #undef ENUM_CASE
926 988
927 } // namespace syncer 989 } // namespace syncer
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698