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

Side by Side Diff: content/renderer/pepper/pepper_plugin_instance_impl.cc

Issue 849723002: Plugin Power Saver: Make PepperPluginInstanceThrottler interface public. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 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 unified diff | Download patch
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 "content/renderer/pepper/pepper_plugin_instance_impl.h" 5 #include "content/renderer/pepper/pepper_plugin_instance_impl.h"
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/callback_helpers.h" 8 #include "base/callback_helpers.h"
9 #include "base/debug/trace_event.h" 9 #include "base/debug/trace_event.h"
10 #include "base/logging.h" 10 #include "base/logging.h"
(...skipping 20 matching lines...) Expand all
31 #include "content/renderer/pepper/fullscreen_container.h" 31 #include "content/renderer/pepper/fullscreen_container.h"
32 #include "content/renderer/pepper/gfx_conversion.h" 32 #include "content/renderer/pepper/gfx_conversion.h"
33 #include "content/renderer/pepper/host_dispatcher_wrapper.h" 33 #include "content/renderer/pepper/host_dispatcher_wrapper.h"
34 #include "content/renderer/pepper/host_globals.h" 34 #include "content/renderer/pepper/host_globals.h"
35 #include "content/renderer/pepper/message_channel.h" 35 #include "content/renderer/pepper/message_channel.h"
36 #include "content/renderer/pepper/pepper_browser_connection.h" 36 #include "content/renderer/pepper/pepper_browser_connection.h"
37 #include "content/renderer/pepper/pepper_compositor_host.h" 37 #include "content/renderer/pepper/pepper_compositor_host.h"
38 #include "content/renderer/pepper/pepper_file_ref_renderer_host.h" 38 #include "content/renderer/pepper/pepper_file_ref_renderer_host.h"
39 #include "content/renderer/pepper/pepper_graphics_2d_host.h" 39 #include "content/renderer/pepper/pepper_graphics_2d_host.h"
40 #include "content/renderer/pepper/pepper_in_process_router.h" 40 #include "content/renderer/pepper/pepper_in_process_router.h"
41 #include "content/renderer/pepper/pepper_plugin_instance_impl.h" 41 #include "content/renderer/pepper/pepper_plugin_instance_metrics.h"
42 #include "content/renderer/pepper/pepper_plugin_instance_throttler.h"
43 #include "content/renderer/pepper/pepper_try_catch.h" 42 #include "content/renderer/pepper/pepper_try_catch.h"
44 #include "content/renderer/pepper/pepper_url_loader_host.h" 43 #include "content/renderer/pepper/pepper_url_loader_host.h"
44 #include "content/renderer/pepper/plugin_instance_throttler_impl.h"
45 #include "content/renderer/pepper/plugin_module.h" 45 #include "content/renderer/pepper/plugin_module.h"
46 #include "content/renderer/pepper/plugin_object.h" 46 #include "content/renderer/pepper/plugin_object.h"
47 #include "content/renderer/pepper/ppapi_preferences_builder.h" 47 #include "content/renderer/pepper/ppapi_preferences_builder.h"
48 #include "content/renderer/pepper/ppb_buffer_impl.h" 48 #include "content/renderer/pepper/ppb_buffer_impl.h"
49 #include "content/renderer/pepper/ppb_graphics_3d_impl.h" 49 #include "content/renderer/pepper/ppb_graphics_3d_impl.h"
50 #include "content/renderer/pepper/ppb_image_data_impl.h" 50 #include "content/renderer/pepper/ppb_image_data_impl.h"
51 #include "content/renderer/pepper/renderer_ppapi_host_impl.h" 51 #include "content/renderer/pepper/renderer_ppapi_host_impl.h"
52 #include "content/renderer/pepper/url_request_info_util.h" 52 #include "content/renderer/pepper/url_request_info_util.h"
53 #include "content/renderer/pepper/url_response_info_util.h" 53 #include "content/renderer/pepper/url_response_info_util.h"
54 #include "content/renderer/render_frame_impl.h" 54 #include "content/renderer/render_frame_impl.h"
(...skipping 18 matching lines...) Expand all
73 #include "ppapi/c/private/ppb_find_private.h" 73 #include "ppapi/c/private/ppb_find_private.h"
74 #include "ppapi/c/private/ppp_find_private.h" 74 #include "ppapi/c/private/ppp_find_private.h"
75 #include "ppapi/c/private/ppp_instance_private.h" 75 #include "ppapi/c/private/ppp_instance_private.h"
76 #include "ppapi/c/private/ppp_pdf.h" 76 #include "ppapi/c/private/ppp_pdf.h"
77 #include "ppapi/host/ppapi_host.h" 77 #include "ppapi/host/ppapi_host.h"
78 #include "ppapi/proxy/ppapi_messages.h" 78 #include "ppapi/proxy/ppapi_messages.h"
79 #include "ppapi/proxy/serialized_var.h" 79 #include "ppapi/proxy/serialized_var.h"
80 #include "ppapi/proxy/uma_private_resource.h" 80 #include "ppapi/proxy/uma_private_resource.h"
81 #include "ppapi/proxy/url_loader_resource.h" 81 #include "ppapi/proxy/url_loader_resource.h"
82 #include "ppapi/shared_impl/ppapi_permissions.h" 82 #include "ppapi/shared_impl/ppapi_permissions.h"
83 #include "ppapi/shared_impl/ppapi_preferences.h"
84 #include "ppapi/shared_impl/ppb_gamepad_shared.h" 83 #include "ppapi/shared_impl/ppb_gamepad_shared.h"
85 #include "ppapi/shared_impl/ppb_input_event_shared.h" 84 #include "ppapi/shared_impl/ppb_input_event_shared.h"
86 #include "ppapi/shared_impl/ppb_url_util_shared.h" 85 #include "ppapi/shared_impl/ppb_url_util_shared.h"
87 #include "ppapi/shared_impl/ppb_view_shared.h" 86 #include "ppapi/shared_impl/ppb_view_shared.h"
88 #include "ppapi/shared_impl/ppp_instance_combined.h" 87 #include "ppapi/shared_impl/ppp_instance_combined.h"
89 #include "ppapi/shared_impl/resource.h" 88 #include "ppapi/shared_impl/resource.h"
90 #include "ppapi/shared_impl/scoped_pp_resource.h" 89 #include "ppapi/shared_impl/scoped_pp_resource.h"
91 #include "ppapi/shared_impl/scoped_pp_var.h" 90 #include "ppapi/shared_impl/scoped_pp_var.h"
92 #include "ppapi/shared_impl/time_conversion.h" 91 #include "ppapi/shared_impl/time_conversion.h"
93 #include "ppapi/shared_impl/url_request_info_data.h" 92 #include "ppapi/shared_impl/url_request_info_data.h"
(...skipping 28 matching lines...) Expand all
122 #include "third_party/khronos/GLES2/gl2.h" 121 #include "third_party/khronos/GLES2/gl2.h"
123 #include "ui/gfx/image/image_skia.h" 122 #include "ui/gfx/image/image_skia.h"
124 #include "ui/gfx/image/image_skia_rep.h" 123 #include "ui/gfx/image/image_skia_rep.h"
125 #include "ui/gfx/range/range.h" 124 #include "ui/gfx/range/range.h"
126 #include "v8/include/v8.h" 125 #include "v8/include/v8.h"
127 126
128 #if defined(OS_CHROMEOS) 127 #if defined(OS_CHROMEOS)
129 #include "ui/events/keycodes/keyboard_codes_posix.h" 128 #include "ui/events/keycodes/keyboard_codes_posix.h"
130 #endif 129 #endif
131 130
132 #if defined(OS_WIN)
133 #include "base/metrics/histogram.h"
134 #include "base/win/windows_version.h"
135 #include "skia/ext/platform_canvas.h"
136 #endif
137
138 using base::StringPrintf; 131 using base::StringPrintf;
139 using ppapi::InputEventData; 132 using ppapi::InputEventData;
140 using ppapi::PpapiGlobals; 133 using ppapi::PpapiGlobals;
141 using ppapi::PPB_InputEvent_Shared; 134 using ppapi::PPB_InputEvent_Shared;
142 using ppapi::PPB_View_Shared; 135 using ppapi::PPB_View_Shared;
143 using ppapi::PPP_Instance_Combined; 136 using ppapi::PPP_Instance_Combined;
144 using ppapi::Resource; 137 using ppapi::Resource;
145 using ppapi::ScopedPPResource; 138 using ppapi::ScopedPPResource;
146 using ppapi::ScopedPPVar; 139 using ppapi::ScopedPPVar;
147 using ppapi::StringVar; 140 using ppapi::StringVar;
(...skipping 331 matching lines...) Expand 10 before | Expand all | Expand 10 after
479 : RenderFrameObserver(render_frame), 472 : RenderFrameObserver(render_frame),
480 render_frame_(render_frame), 473 render_frame_(render_frame),
481 module_(module), 474 module_(module),
482 instance_interface_(instance_interface), 475 instance_interface_(instance_interface),
483 pp_instance_(0), 476 pp_instance_(0),
484 container_(container), 477 container_(container),
485 layer_bound_to_fullscreen_(false), 478 layer_bound_to_fullscreen_(false),
486 layer_is_hardware_(false), 479 layer_is_hardware_(false),
487 plugin_url_(plugin_url), 480 plugin_url_(plugin_url),
488 is_flash_plugin_(module->name() == kFlashPluginName), 481 is_flash_plugin_(module->name() == kFlashPluginName),
482 has_been_clicked_(false),
489 javascript_used_(false), 483 javascript_used_(false),
490 full_frame_(false), 484 full_frame_(false),
491 sent_initial_did_change_view_(false), 485 sent_initial_did_change_view_(false),
492 bound_graphics_2d_platform_(NULL), 486 bound_graphics_2d_platform_(NULL),
493 bound_compositor_(NULL), 487 bound_compositor_(NULL),
494 has_webkit_focus_(false), 488 has_webkit_focus_(false),
495 has_content_area_focus_(false), 489 has_content_area_focus_(false),
496 find_identifier_(-1), 490 find_identifier_(-1),
497 plugin_find_interface_(NULL), 491 plugin_find_interface_(NULL),
498 plugin_input_event_interface_(NULL), 492 plugin_input_event_interface_(NULL),
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after
615 609
616 UnSetAndDeleteLockTargetAdapter(); 610 UnSetAndDeleteLockTargetAdapter();
617 module_->InstanceDeleted(this); 611 module_->InstanceDeleted(this);
618 // If we switched from the NaCl plugin module, notify it too. 612 // If we switched from the NaCl plugin module, notify it too.
619 if (original_module_.get()) 613 if (original_module_.get())
620 original_module_->InstanceDeleted(this); 614 original_module_->InstanceDeleted(this);
621 615
622 // This should be last since some of the above "instance deleted" calls will 616 // This should be last since some of the above "instance deleted" calls will
623 // want to look up in the global map to get info off of our object. 617 // want to look up in the global map to get info off of our object.
624 HostGlobals::Get()->InstanceDeleted(pp_instance_); 618 HostGlobals::Get()->InstanceDeleted(pp_instance_);
619
620 if (throttler_)
621 throttler_->RemoveObserver(this);
625 } 622 }
626 623
627 // NOTE: Any of these methods that calls into the plugin needs to take into 624 // NOTE: Any of these methods that calls into the plugin needs to take into
628 // account that the plugin may use Var to remove the <embed> from the DOM, which 625 // account that the plugin may use Var to remove the <embed> from the DOM, which
629 // will make the PepperWebPluginImpl drop its reference, usually the last one. 626 // will make the PepperWebPluginImpl drop its reference, usually the last one.
630 // If a method needs to access a member of the instance after the call has 627 // If a method needs to access a member of the instance after the call has
631 // returned, then it needs to keep its own reference on the stack. 628 // returned, then it needs to keep its own reference on the stack.
632 629
633 v8::Local<v8::Object> PepperPluginInstanceImpl::GetMessageChannelObject() { 630 v8::Local<v8::Object> PepperPluginInstanceImpl::GetMessageChannelObject() {
634 return v8::Local<v8::Object>::New(isolate_, message_channel_object_); 631 return v8::Local<v8::Object>::New(isolate_, message_channel_object_);
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after
791 if (content_decryptor_delegate_) { 788 if (content_decryptor_delegate_) {
792 content_decryptor_delegate_->InstanceCrashed(); 789 content_decryptor_delegate_->InstanceCrashed();
793 content_decryptor_delegate_.reset(); 790 content_decryptor_delegate_.reset();
794 } 791 }
795 792
796 if (render_frame_) 793 if (render_frame_)
797 render_frame_->PluginCrashed(module_->path(), module_->GetPeerProcessId()); 794 render_frame_->PluginCrashed(module_->path(), module_->GetPeerProcessId());
798 UnSetAndDeleteLockTargetAdapter(); 795 UnSetAndDeleteLockTargetAdapter();
799 } 796 }
800 797
801 static void SetGPUHistogram(const ppapi::Preferences& prefs,
802 const std::vector<std::string>& arg_names,
803 const std::vector<std::string>& arg_values) {
804 // Calculate a histogram to let us determine how likely people are to try to
805 // run Stage3D content on machines that have it blacklisted.
806 #if defined(OS_WIN)
807 bool needs_gpu = false;
808 bool is_xp = base::win::GetVersion() <= base::win::VERSION_XP;
809
810 for (size_t i = 0; i < arg_names.size(); i++) {
811 if (arg_names[i] == "wmode") {
812 // In theory content other than Flash could have a "wmode" argument,
813 // but that's pretty unlikely.
814 if (arg_values[i] == "direct" || arg_values[i] == "gpu")
815 needs_gpu = true;
816 break;
817 }
818 }
819 // 0 : No 3D content and GPU is blacklisted
820 // 1 : No 3D content and GPU is not blacklisted
821 // 2 : 3D content but GPU is blacklisted
822 // 3 : 3D content and GPU is not blacklisted
823 // 4 : No 3D content and GPU is blacklisted on XP
824 // 5 : No 3D content and GPU is not blacklisted on XP
825 // 6 : 3D content but GPU is blacklisted on XP
826 // 7 : 3D content and GPU is not blacklisted on XP
827 UMA_HISTOGRAM_ENUMERATION(
828 "Flash.UsesGPU", is_xp * 4 + needs_gpu * 2 + prefs.is_webgl_supported, 8);
829 #endif
830 }
831
832 bool PepperPluginInstanceImpl::Initialize( 798 bool PepperPluginInstanceImpl::Initialize(
833 const std::vector<std::string>& arg_names, 799 const std::vector<std::string>& arg_names,
834 const std::vector<std::string>& arg_values, 800 const std::vector<std::string>& arg_values,
835 bool full_frame, 801 bool full_frame,
836 RenderFrame::PluginPowerSaverMode power_saver_mode) { 802 scoped_ptr<PluginInstanceThrottlerImpl> throttler) {
803 DCHECK(!throttler_);
804
837 if (!render_frame_) 805 if (!render_frame_)
838 return false; 806 return false;
839 807
840 blink::WebRect bounds = container()->element().boundsInViewportSpace(); 808 if (is_flash_plugin_ && RenderThread::Get()) {
809 RenderThread::Get()->RecordAction(
810 base::UserMetricsAction("Flash.PluginInstanceCreated"));
811 blink::WebRect bounds = container()->element().boundsInViewportSpace();
812 RecordFlashSizeMetric(bounds.width, bounds.height);
813 }
841 814
842 throttler_.reset(new PepperPluginInstanceThrottler( 815 if (throttler) {
843 render_frame(), bounds, is_flash_plugin_, plugin_url_, power_saver_mode, 816 throttler_.reset(throttler.release());
piman 2015/01/14 19:15:43 nit: or throttler_ = throttler.Pass()
tommycli 2015/01/14 19:22:05 Done.
844 base::Bind(&PepperPluginInstanceImpl::SendDidChangeView, 817 throttler_->AddObserver(this);
845 weak_factory_.GetWeakPtr()))); 818 }
846 819
847 message_channel_ = MessageChannel::Create(this, &message_channel_object_); 820 message_channel_ = MessageChannel::Create(this, &message_channel_object_);
848 821
849 full_frame_ = full_frame; 822 full_frame_ = full_frame;
850 823
851 UpdateTouchEventRequest(); 824 UpdateTouchEventRequest();
852 container_->setWantsWheelEvents(IsAcceptingWheelEvents()); 825 container_->setWantsWheelEvents(IsAcceptingWheelEvents());
853 826
854 SetGPUHistogram(ppapi::Preferences(PpapiPreferencesBuilder::Build( 827 SetGPUHistogram(ppapi::Preferences(PpapiPreferencesBuilder::Build(
855 render_frame_->render_view()->webkit_preferences())), 828 render_frame_->render_view()->webkit_preferences())),
(...skipping 236 matching lines...) Expand 10 before | Expand all | Expand 10 after
1092 gfx::Rect caret(text_input_caret_); 1065 gfx::Rect caret(text_input_caret_);
1093 caret.Offset(view_data_.rect.point.x, view_data_.rect.point.y); 1066 caret.Offset(view_data_.rect.point.x, view_data_.rect.point.y);
1094 return caret; 1067 return caret;
1095 } 1068 }
1096 1069
1097 bool PepperPluginInstanceImpl::HandleInputEvent( 1070 bool PepperPluginInstanceImpl::HandleInputEvent(
1098 const blink::WebInputEvent& event, 1071 const blink::WebInputEvent& event,
1099 WebCursorInfo* cursor_info) { 1072 WebCursorInfo* cursor_info) {
1100 TRACE_EVENT0("ppapi", "PepperPluginInstanceImpl::HandleInputEvent"); 1073 TRACE_EVENT0("ppapi", "PepperPluginInstanceImpl::HandleInputEvent");
1101 1074
1102 if (throttler_->ConsumeInputEvent(event)) 1075 if (!has_been_clicked_ && is_flash_plugin_ &&
1076 event.type == blink::WebInputEvent::MouseDown &&
1077 (event.modifiers & blink::WebInputEvent::LeftButtonDown)) {
1078 has_been_clicked_ = true;
1079 blink::WebRect bounds = container()->element().boundsInViewportSpace();
1080 RecordFlashClickSizeMetric(bounds.width, bounds.height);
1081 }
1082
1083 if (throttler_ && throttler_->ConsumeInputEvent(event))
1103 return true; 1084 return true;
1104 1085
1105 if (!render_frame_) 1086 if (!render_frame_)
1106 return false; 1087 return false;
1107 if (WebInputEvent::isMouseEventType(event.type)) { 1088 if (WebInputEvent::isMouseEventType(event.type)) {
1108 render_frame_->PepperDidReceiveMouseEvent(this); 1089 render_frame_->PepperDidReceiveMouseEvent(this);
1109 } 1090 }
1110 1091
1111 // Don't dispatch input events to crashed plugins. 1092 // Don't dispatch input events to crashed plugins.
1112 if (module()->is_crashed()) 1093 if (module()->is_crashed())
(...skipping 526 matching lines...) Expand 10 before | Expand all | Expand 10 after
1639 1620
1640 void PepperPluginInstanceImpl::SendDidChangeView() { 1621 void PepperPluginInstanceImpl::SendDidChangeView() {
1641 // Don't send DidChangeView to crashed plugins. 1622 // Don't send DidChangeView to crashed plugins.
1642 if (module()->is_crashed()) 1623 if (module()->is_crashed())
1643 return; 1624 return;
1644 1625
1645 ppapi::ViewData view_data = view_data_; 1626 ppapi::ViewData view_data = view_data_;
1646 1627
1647 // When plugin content is throttled, fake the page being offscreen. We cannot 1628 // When plugin content is throttled, fake the page being offscreen. We cannot
1648 // send empty view data here, as some plugins rely on accurate view data. 1629 // send empty view data here, as some plugins rely on accurate view data.
1649 if (throttler_ && throttler_->is_throttled()) { 1630 if (throttler_ && throttler_->IsThrottled()) {
1650 view_data.is_page_visible = false; 1631 view_data.is_page_visible = false;
1651 view_data.clip_rect.point.x = 0; 1632 view_data.clip_rect.point.x = 0;
1652 view_data.clip_rect.point.y = 0; 1633 view_data.clip_rect.point.y = 0;
1653 view_data.clip_rect.size.width = 0; 1634 view_data.clip_rect.size.width = 0;
1654 view_data.clip_rect.size.height = 0; 1635 view_data.clip_rect.size.height = 0;
1655 } 1636 }
1656 1637
1657 if (view_change_weak_ptr_factory_.HasWeakPtrs() || 1638 if (view_change_weak_ptr_factory_.HasWeakPtrs() ||
1658 (sent_initial_did_change_view_ && 1639 (sent_initial_did_change_view_ &&
1659 last_sent_view_data_.Equals(view_data))) 1640 last_sent_view_data_.Equals(view_data)))
(...skipping 391 matching lines...) Expand 10 before | Expand all | Expand 10 after
2051 scoped_ptr<cc::SingleReleaseCallback>* release_callback, 2032 scoped_ptr<cc::SingleReleaseCallback>* release_callback,
2052 bool use_shared_memory) { 2033 bool use_shared_memory) {
2053 if (!bound_graphics_2d_platform_) 2034 if (!bound_graphics_2d_platform_)
2054 return false; 2035 return false;
2055 return bound_graphics_2d_platform_->PrepareTextureMailbox(mailbox, 2036 return bound_graphics_2d_platform_->PrepareTextureMailbox(mailbox,
2056 release_callback); 2037 release_callback);
2057 } 2038 }
2058 2039
2059 void PepperPluginInstanceImpl::OnDestruct() { render_frame_ = NULL; } 2040 void PepperPluginInstanceImpl::OnDestruct() { render_frame_ = NULL; }
2060 2041
2042 void PepperPluginInstanceImpl::OnThrottleStateChange() {
2043 SendDidChangeView();
2044 }
2045
2061 void PepperPluginInstanceImpl::AddLatencyInfo( 2046 void PepperPluginInstanceImpl::AddLatencyInfo(
2062 const std::vector<ui::LatencyInfo>& latency_info) { 2047 const std::vector<ui::LatencyInfo>& latency_info) {
2063 if (render_frame_ && render_frame_->GetRenderWidget()) { 2048 if (render_frame_ && render_frame_->GetRenderWidget()) {
2064 RenderWidgetCompositor* compositor = 2049 RenderWidgetCompositor* compositor =
2065 render_frame_->GetRenderWidget()->compositor(); 2050 render_frame_->GetRenderWidget()->compositor();
2066 if (compositor) { 2051 if (compositor) {
2067 for (size_t i = 0; i < latency_info.size(); i++) { 2052 for (size_t i = 0; i < latency_info.size(); i++) {
2068 scoped_ptr<cc::SwapPromise> swap_promise( 2053 scoped_ptr<cc::SwapPromise> swap_promise(
2069 new cc::LatencyInfoSwapPromise(latency_info[i])); 2054 new cc::LatencyInfoSwapPromise(latency_info[i]));
2070 compositor->QueueSwapPromise(swap_promise.Pass()); 2055 compositor->QueueSwapPromise(swap_promise.Pass());
(...skipping 1221 matching lines...) Expand 10 before | Expand all | Expand 10 after
3292 3277
3293 void PepperPluginInstanceImpl::RecordFlashJavaScriptUse() { 3278 void PepperPluginInstanceImpl::RecordFlashJavaScriptUse() {
3294 if (!javascript_used_ && is_flash_plugin_) { 3279 if (!javascript_used_ && is_flash_plugin_) {
3295 javascript_used_ = true; 3280 javascript_used_ = true;
3296 RenderThread::Get()->RecordAction( 3281 RenderThread::Get()->RecordAction(
3297 base::UserMetricsAction("Flash.JavaScriptUsed")); 3282 base::UserMetricsAction("Flash.JavaScriptUsed"));
3298 } 3283 }
3299 } 3284 }
3300 3285
3301 } // namespace content 3286 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698