OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 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/scheduler/renderer/renderer_scheduler_impl.h" | 5 #include "components/scheduler/renderer/renderer_scheduler_impl.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/debug/stack_trace.h" | 8 #include "base/debug/stack_trace.h" |
9 #include "base/logging.h" | 9 #include "base/logging.h" |
10 #include "base/trace_event/trace_event.h" | 10 #include "base/trace_event/trace_event.h" |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
42 base::TimeDelta()), | 42 base::TimeDelta()), |
43 render_widget_scheduler_signals_(this), | 43 render_widget_scheduler_signals_(this), |
44 control_task_runner_(helper_.ControlTaskRunner()), | 44 control_task_runner_(helper_.ControlTaskRunner()), |
45 compositor_task_runner_( | 45 compositor_task_runner_( |
46 helper_.NewTaskQueue(TaskQueue::Spec("compositor_tq") | 46 helper_.NewTaskQueue(TaskQueue::Spec("compositor_tq") |
47 .SetShouldMonitorQuiescence(true))), | 47 .SetShouldMonitorQuiescence(true))), |
48 delayed_update_policy_runner_( | 48 delayed_update_policy_runner_( |
49 base::Bind(&RendererSchedulerImpl::UpdatePolicy, | 49 base::Bind(&RendererSchedulerImpl::UpdatePolicy, |
50 base::Unretained(this)), | 50 base::Unretained(this)), |
51 helper_.ControlTaskRunner()), | 51 helper_.ControlTaskRunner()), |
52 main_thread_only_(compositor_task_runner_, helper_.tick_clock()), | 52 main_thread_only_(compositor_task_runner_, |
| 53 helper_.scheduler_tqm_delegate().get()), |
53 policy_may_need_update_(&any_thread_lock_), | 54 policy_may_need_update_(&any_thread_lock_), |
54 weak_factory_(this) { | 55 weak_factory_(this) { |
55 update_policy_closure_ = base::Bind(&RendererSchedulerImpl::UpdatePolicy, | 56 update_policy_closure_ = base::Bind(&RendererSchedulerImpl::UpdatePolicy, |
56 weak_factory_.GetWeakPtr()); | 57 weak_factory_.GetWeakPtr()); |
57 end_renderer_hidden_idle_period_closure_.Reset(base::Bind( | 58 end_renderer_hidden_idle_period_closure_.Reset(base::Bind( |
58 &RendererSchedulerImpl::EndIdlePeriod, weak_factory_.GetWeakPtr())); | 59 &RendererSchedulerImpl::EndIdlePeriod, weak_factory_.GetWeakPtr())); |
59 | 60 |
60 suspend_timers_when_backgrounded_closure_.Reset( | 61 suspend_timers_when_backgrounded_closure_.Reset( |
61 base::Bind(&RendererSchedulerImpl::SuspendTimerQueueWhenBackgrounded, | 62 base::Bind(&RendererSchedulerImpl::SuspendTimerQueueWhenBackgrounded, |
62 weak_factory_.GetWeakPtr())); | 63 weak_factory_.GetWeakPtr())); |
(...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
245 } | 246 } |
246 } | 247 } |
247 | 248 |
248 void RendererSchedulerImpl::DidCommitFrameToCompositor() { | 249 void RendererSchedulerImpl::DidCommitFrameToCompositor() { |
249 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), | 250 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), |
250 "RendererSchedulerImpl::DidCommitFrameToCompositor"); | 251 "RendererSchedulerImpl::DidCommitFrameToCompositor"); |
251 helper_.CheckOnValidThread(); | 252 helper_.CheckOnValidThread(); |
252 if (helper_.IsShutdown()) | 253 if (helper_.IsShutdown()) |
253 return; | 254 return; |
254 | 255 |
255 base::TimeTicks now(helper_.tick_clock()->NowTicks()); | 256 base::TimeTicks now(helper_.scheduler_tqm_delegate()->NowTicks()); |
256 if (now < MainThreadOnly().estimated_next_frame_begin) { | 257 if (now < MainThreadOnly().estimated_next_frame_begin) { |
257 // TODO(rmcilroy): Consider reducing the idle period based on the runtime of | 258 // TODO(rmcilroy): Consider reducing the idle period based on the runtime of |
258 // the next pending delayed tasks (as currently done in for long idle times) | 259 // the next pending delayed tasks (as currently done in for long idle times) |
259 idle_helper_.StartIdlePeriod( | 260 idle_helper_.StartIdlePeriod( |
260 IdleHelper::IdlePeriodState::IN_SHORT_IDLE_PERIOD, now, | 261 IdleHelper::IdlePeriodState::IN_SHORT_IDLE_PERIOD, now, |
261 MainThreadOnly().estimated_next_frame_begin); | 262 MainThreadOnly().estimated_next_frame_begin); |
262 } | 263 } |
263 | 264 |
264 MainThreadOnly().idle_time_estimator.DidCommitFrameToCompositor(); | 265 MainThreadOnly().idle_time_estimator.DidCommitFrameToCompositor(); |
265 } | 266 } |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
302 end_idle_when_hidden_delay); | 303 end_idle_when_hidden_delay); |
303 MainThreadOnly().renderer_hidden = true; | 304 MainThreadOnly().renderer_hidden = true; |
304 } else { | 305 } else { |
305 MainThreadOnly().renderer_hidden = false; | 306 MainThreadOnly().renderer_hidden = false; |
306 EndIdlePeriod(); | 307 EndIdlePeriod(); |
307 } | 308 } |
308 | 309 |
309 // TODO(alexclarke): Should we update policy here? | 310 // TODO(alexclarke): Should we update policy here? |
310 TRACE_EVENT_OBJECT_SNAPSHOT_WITH_ID( | 311 TRACE_EVENT_OBJECT_SNAPSHOT_WITH_ID( |
311 TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), "RendererScheduler", | 312 TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), "RendererScheduler", |
312 this, AsValue(helper_.tick_clock()->NowTicks())); | 313 this, AsValue(helper_.scheduler_tqm_delegate()->NowTicks())); |
313 } | 314 } |
314 | 315 |
315 void RendererSchedulerImpl::SetHasVisibleRenderWidgetWithTouchHandler( | 316 void RendererSchedulerImpl::SetHasVisibleRenderWidgetWithTouchHandler( |
316 bool has_visible_render_widget_with_touch_handler) { | 317 bool has_visible_render_widget_with_touch_handler) { |
317 helper_.CheckOnValidThread(); | 318 helper_.CheckOnValidThread(); |
318 if (has_visible_render_widget_with_touch_handler == | 319 if (has_visible_render_widget_with_touch_handler == |
319 MainThreadOnly().has_visible_render_widget_with_touch_handler) | 320 MainThreadOnly().has_visible_render_widget_with_touch_handler) |
320 return; | 321 return; |
321 | 322 |
322 MainThreadOnly().has_visible_render_widget_with_touch_handler = | 323 MainThreadOnly().has_visible_render_widget_with_touch_handler = |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
395 return; | 396 return; |
396 | 397 |
397 UpdateForInputEventOnCompositorThread(web_input_event.type, event_state); | 398 UpdateForInputEventOnCompositorThread(web_input_event.type, event_state); |
398 } | 399 } |
399 | 400 |
400 void RendererSchedulerImpl::DidAnimateForInputOnCompositorThread() { | 401 void RendererSchedulerImpl::DidAnimateForInputOnCompositorThread() { |
401 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), | 402 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), |
402 "RendererSchedulerImpl::DidAnimateForInputOnCompositorThread"); | 403 "RendererSchedulerImpl::DidAnimateForInputOnCompositorThread"); |
403 base::AutoLock lock(any_thread_lock_); | 404 base::AutoLock lock(any_thread_lock_); |
404 AnyThread().fling_compositor_escalation_deadline = | 405 AnyThread().fling_compositor_escalation_deadline = |
405 helper_.tick_clock()->NowTicks() + | 406 helper_.scheduler_tqm_delegate()->NowTicks() + |
406 base::TimeDelta::FromMilliseconds(kFlingEscalationLimitMillis); | 407 base::TimeDelta::FromMilliseconds(kFlingEscalationLimitMillis); |
407 } | 408 } |
408 | 409 |
409 void RendererSchedulerImpl::UpdateForInputEventOnCompositorThread( | 410 void RendererSchedulerImpl::UpdateForInputEventOnCompositorThread( |
410 blink::WebInputEvent::Type type, | 411 blink::WebInputEvent::Type type, |
411 InputEventState input_event_state) { | 412 InputEventState input_event_state) { |
412 base::AutoLock lock(any_thread_lock_); | 413 base::AutoLock lock(any_thread_lock_); |
413 base::TimeTicks now = helper_.tick_clock()->NowTicks(); | 414 base::TimeTicks now = helper_.scheduler_tqm_delegate()->NowTicks(); |
414 | 415 |
415 // TODO(alexclarke): Move WebInputEventTraits where we can access it from here | 416 // TODO(alexclarke): Move WebInputEventTraits where we can access it from here |
416 // and record the name rather than the integer representation. | 417 // and record the name rather than the integer representation. |
417 TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), | 418 TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), |
418 "RendererSchedulerImpl::UpdateForInputEventOnCompositorThread", | 419 "RendererSchedulerImpl::UpdateForInputEventOnCompositorThread", |
419 "type", static_cast<int>(type), "input_event_state", | 420 "type", static_cast<int>(type), "input_event_state", |
420 InputEventStateToString(input_event_state)); | 421 InputEventStateToString(input_event_state)); |
421 | 422 |
422 bool gesture_already_in_progress = InputSignalsSuggestGestureInProgress(now); | 423 bool gesture_already_in_progress = InputSignalsSuggestGestureInProgress(now); |
423 bool was_awaiting_touch_start_response = | 424 bool was_awaiting_touch_start_response = |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
485 } | 486 } |
486 | 487 |
487 void RendererSchedulerImpl::DidHandleInputEventOnMainThread( | 488 void RendererSchedulerImpl::DidHandleInputEventOnMainThread( |
488 const blink::WebInputEvent& web_input_event) { | 489 const blink::WebInputEvent& web_input_event) { |
489 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), | 490 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), |
490 "RendererSchedulerImpl::DidHandleInputEventOnMainThread"); | 491 "RendererSchedulerImpl::DidHandleInputEventOnMainThread"); |
491 helper_.CheckOnValidThread(); | 492 helper_.CheckOnValidThread(); |
492 if (ShouldPrioritizeInputEvent(web_input_event)) { | 493 if (ShouldPrioritizeInputEvent(web_input_event)) { |
493 base::AutoLock lock(any_thread_lock_); | 494 base::AutoLock lock(any_thread_lock_); |
494 AnyThread().user_model.DidFinishProcessingInputEvent( | 495 AnyThread().user_model.DidFinishProcessingInputEvent( |
495 helper_.tick_clock()->NowTicks()); | 496 helper_.scheduler_tqm_delegate()->NowTicks()); |
496 } | 497 } |
497 } | 498 } |
498 | 499 |
499 bool RendererSchedulerImpl::IsHighPriorityWorkAnticipated() { | 500 bool RendererSchedulerImpl::IsHighPriorityWorkAnticipated() { |
500 helper_.CheckOnValidThread(); | 501 helper_.CheckOnValidThread(); |
501 if (helper_.IsShutdown()) | 502 if (helper_.IsShutdown()) |
502 return false; | 503 return false; |
503 | 504 |
504 MaybeUpdatePolicy(); | 505 MaybeUpdatePolicy(); |
505 // The touchstart, synchronized gesture and main-thread gesture use cases | 506 // The touchstart, synchronized gesture and main-thread gesture use cases |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
576 base::AutoLock lock(any_thread_lock_); | 577 base::AutoLock lock(any_thread_lock_); |
577 UpdatePolicyLocked(UpdateType::FORCE_UPDATE); | 578 UpdatePolicyLocked(UpdateType::FORCE_UPDATE); |
578 } | 579 } |
579 | 580 |
580 void RendererSchedulerImpl::UpdatePolicyLocked(UpdateType update_type) { | 581 void RendererSchedulerImpl::UpdatePolicyLocked(UpdateType update_type) { |
581 helper_.CheckOnValidThread(); | 582 helper_.CheckOnValidThread(); |
582 any_thread_lock_.AssertAcquired(); | 583 any_thread_lock_.AssertAcquired(); |
583 if (helper_.IsShutdown()) | 584 if (helper_.IsShutdown()) |
584 return; | 585 return; |
585 | 586 |
586 base::TimeTicks now = helper_.tick_clock()->NowTicks(); | 587 base::TimeTicks now = helper_.scheduler_tqm_delegate()->NowTicks(); |
587 policy_may_need_update_.SetWhileLocked(false); | 588 policy_may_need_update_.SetWhileLocked(false); |
588 | 589 |
589 base::TimeDelta expected_use_case_duration; | 590 base::TimeDelta expected_use_case_duration; |
590 UseCase use_case = ComputeCurrentUseCase(now, &expected_use_case_duration); | 591 UseCase use_case = ComputeCurrentUseCase(now, &expected_use_case_duration); |
591 MainThreadOnly().current_use_case = use_case; | 592 MainThreadOnly().current_use_case = use_case; |
592 | 593 |
593 base::TimeDelta touchstart_expected_flag_valid_for_duration; | 594 base::TimeDelta touchstart_expected_flag_valid_for_duration; |
594 bool touchstart_expected_soon = false; | 595 bool touchstart_expected_soon = false; |
595 if (MainThreadOnly().has_visible_render_widget_with_touch_handler) { | 596 if (MainThreadOnly().has_visible_render_widget_with_touch_handler) { |
596 touchstart_expected_soon = AnyThread().user_model.IsGestureExpectedSoon( | 597 touchstart_expected_soon = AnyThread().user_model.IsGestureExpectedSoon( |
(...skipping 281 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
878 base::AutoLock lock(any_thread_lock_); | 879 base::AutoLock lock(any_thread_lock_); |
879 return AsValueLocked(optional_now); | 880 return AsValueLocked(optional_now); |
880 } | 881 } |
881 | 882 |
882 scoped_refptr<base::trace_event::ConvertableToTraceFormat> | 883 scoped_refptr<base::trace_event::ConvertableToTraceFormat> |
883 RendererSchedulerImpl::AsValueLocked(base::TimeTicks optional_now) const { | 884 RendererSchedulerImpl::AsValueLocked(base::TimeTicks optional_now) const { |
884 helper_.CheckOnValidThread(); | 885 helper_.CheckOnValidThread(); |
885 any_thread_lock_.AssertAcquired(); | 886 any_thread_lock_.AssertAcquired(); |
886 | 887 |
887 if (optional_now.is_null()) | 888 if (optional_now.is_null()) |
888 optional_now = helper_.tick_clock()->NowTicks(); | 889 optional_now = helper_.scheduler_tqm_delegate()->NowTicks(); |
889 scoped_refptr<base::trace_event::TracedValue> state = | 890 scoped_refptr<base::trace_event::TracedValue> state = |
890 new base::trace_event::TracedValue(); | 891 new base::trace_event::TracedValue(); |
891 | 892 |
892 state->SetBoolean( | 893 state->SetBoolean( |
893 "has_visible_render_widget_with_touch_handler", | 894 "has_visible_render_widget_with_touch_handler", |
894 MainThreadOnly().has_visible_render_widget_with_touch_handler); | 895 MainThreadOnly().has_visible_render_widget_with_touch_handler); |
895 state->SetString("current_use_case", | 896 state->SetString("current_use_case", |
896 UseCaseToString(MainThreadOnly().current_use_case)); | 897 UseCaseToString(MainThreadOnly().current_use_case)); |
897 state->SetBoolean("loading_tasks_seem_expensive", | 898 state->SetBoolean("loading_tasks_seem_expensive", |
898 MainThreadOnly().loading_tasks_seem_expensive); | 899 MainThreadOnly().loading_tasks_seem_expensive); |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
956 } | 957 } |
957 | 958 |
958 void RendererSchedulerImpl::OnIdlePeriodStarted() { | 959 void RendererSchedulerImpl::OnIdlePeriodStarted() { |
959 base::AutoLock lock(any_thread_lock_); | 960 base::AutoLock lock(any_thread_lock_); |
960 AnyThread().in_idle_period = true; | 961 AnyThread().in_idle_period = true; |
961 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); | 962 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); |
962 } | 963 } |
963 | 964 |
964 void RendererSchedulerImpl::OnIdlePeriodEnded() { | 965 void RendererSchedulerImpl::OnIdlePeriodEnded() { |
965 base::AutoLock lock(any_thread_lock_); | 966 base::AutoLock lock(any_thread_lock_); |
966 AnyThread().last_idle_period_end_time = helper_.tick_clock()->NowTicks(); | 967 AnyThread().last_idle_period_end_time = |
| 968 helper_.scheduler_tqm_delegate()->NowTicks(); |
967 AnyThread().in_idle_period = false; | 969 AnyThread().in_idle_period = false; |
968 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); | 970 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); |
969 } | 971 } |
970 | 972 |
971 void RendererSchedulerImpl::AddPendingNavigation() { | 973 void RendererSchedulerImpl::AddPendingNavigation() { |
972 helper_.CheckOnValidThread(); | 974 helper_.CheckOnValidThread(); |
973 MainThreadOnly().navigation_task_expected_count++; | 975 MainThreadOnly().navigation_task_expected_count++; |
974 UpdatePolicy(); | 976 UpdatePolicy(); |
975 } | 977 } |
976 | 978 |
977 void RendererSchedulerImpl::RemovePendingNavigation() { | 979 void RendererSchedulerImpl::RemovePendingNavigation() { |
978 helper_.CheckOnValidThread(); | 980 helper_.CheckOnValidThread(); |
979 DCHECK_GT(MainThreadOnly().navigation_task_expected_count, 0); | 981 DCHECK_GT(MainThreadOnly().navigation_task_expected_count, 0); |
980 if (MainThreadOnly().navigation_task_expected_count > 0) | 982 if (MainThreadOnly().navigation_task_expected_count > 0) |
981 MainThreadOnly().navigation_task_expected_count--; | 983 MainThreadOnly().navigation_task_expected_count--; |
982 UpdatePolicy(); | 984 UpdatePolicy(); |
983 } | 985 } |
984 | 986 |
985 void RendererSchedulerImpl::OnNavigationStarted() { | 987 void RendererSchedulerImpl::OnNavigationStarted() { |
986 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), | 988 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("renderer.scheduler"), |
987 "RendererSchedulerImpl::OnNavigationStarted"); | 989 "RendererSchedulerImpl::OnNavigationStarted"); |
988 base::AutoLock lock(any_thread_lock_); | 990 base::AutoLock lock(any_thread_lock_); |
989 AnyThread().rails_loading_priority_deadline = | 991 AnyThread().rails_loading_priority_deadline = |
990 helper_.tick_clock()->NowTicks() + | 992 helper_.scheduler_tqm_delegate()->NowTicks() + |
991 base::TimeDelta::FromMilliseconds( | 993 base::TimeDelta::FromMilliseconds( |
992 kRailsInitialLoadingPrioritizationMillis); | 994 kRailsInitialLoadingPrioritizationMillis); |
993 ResetForNavigationLocked(); | 995 ResetForNavigationLocked(); |
994 } | 996 } |
995 | 997 |
996 bool RendererSchedulerImpl::HadAnIdlePeriodRecently(base::TimeTicks now) const { | 998 bool RendererSchedulerImpl::HadAnIdlePeriodRecently(base::TimeTicks now) const { |
997 return (now - AnyThread().last_idle_period_end_time) <= | 999 return (now - AnyThread().last_idle_period_end_time) <= |
998 base::TimeDelta::FromMilliseconds( | 1000 base::TimeDelta::FromMilliseconds( |
999 kIdlePeriodStarvationThresholdMillis); | 1001 kIdlePeriodStarvationThresholdMillis); |
1000 } | 1002 } |
(...skipping 15 matching lines...) Expand all Loading... |
1016 MainThreadOnly().timer_queue_suspended_when_backgrounded = false; | 1018 MainThreadOnly().timer_queue_suspended_when_backgrounded = false; |
1017 ForceUpdatePolicy(); | 1019 ForceUpdatePolicy(); |
1018 } | 1020 } |
1019 | 1021 |
1020 void RendererSchedulerImpl::ResetForNavigationLocked() { | 1022 void RendererSchedulerImpl::ResetForNavigationLocked() { |
1021 helper_.CheckOnValidThread(); | 1023 helper_.CheckOnValidThread(); |
1022 any_thread_lock_.AssertAcquired(); | 1024 any_thread_lock_.AssertAcquired(); |
1023 MainThreadOnly().loading_task_cost_estimator.Clear(); | 1025 MainThreadOnly().loading_task_cost_estimator.Clear(); |
1024 MainThreadOnly().timer_task_cost_estimator.Clear(); | 1026 MainThreadOnly().timer_task_cost_estimator.Clear(); |
1025 MainThreadOnly().idle_time_estimator.Clear(); | 1027 MainThreadOnly().idle_time_estimator.Clear(); |
1026 AnyThread().user_model.Reset(helper_.tick_clock()->NowTicks()); | 1028 AnyThread().user_model.Reset(helper_.scheduler_tqm_delegate()->NowTicks()); |
1027 MainThreadOnly().have_seen_a_begin_main_frame = false; | 1029 MainThreadOnly().have_seen_a_begin_main_frame = false; |
1028 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); | 1030 UpdatePolicyLocked(UpdateType::MAY_EARLY_OUT_IF_POLICY_UNCHANGED); |
1029 } | 1031 } |
1030 | 1032 |
| 1033 double RendererSchedulerImpl::CurrentTimeSeconds() const { |
| 1034 return helper_.scheduler_tqm_delegate()->CurrentTimeSeconds(); |
| 1035 } |
| 1036 |
| 1037 double RendererSchedulerImpl::MonotonicallyIncreasingTimeSeconds() const { |
| 1038 return helper_.scheduler_tqm_delegate()->NowTicks().ToInternalValue() / |
| 1039 static_cast<double>(base::Time::kMicrosecondsPerSecond); |
| 1040 } |
| 1041 |
1031 } // namespace scheduler | 1042 } // namespace scheduler |
OLD | NEW |