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

Side by Side Diff: cc/trees/property_tree.cc

Issue 2448403002: cc: Clean up transform tree (Closed)
Patch Set: PAC Created 4 years, 1 month 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
« no previous file with comments | « cc/trees/property_tree.h ('k') | cc/trees/property_tree_builder.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 <stddef.h> 5 #include <stddef.h>
6 6
7 #include <set> 7 #include <set>
8 #include <vector> 8 #include <vector>
9 9
10 #include "base/logging.h" 10 #include "base/logging.h"
11 #include "base/memory/ptr_util.h" 11 #include "base/memory/ptr_util.h"
12 #include "base/trace_event/trace_event_argument.h" 12 #include "base/trace_event/trace_event_argument.h"
13 #include "cc/animation/animation_host.h" 13 #include "cc/animation/animation_host.h"
14 #include "cc/layers/layer_impl.h" 14 #include "cc/layers/layer_impl.h"
15 #include "cc/output/copy_output_request.h" 15 #include "cc/output/copy_output_request.h"
16 #include "cc/proto/gfx_conversions.h" 16 #include "cc/proto/gfx_conversions.h"
17 #include "cc/proto/property_tree.pb.h" 17 #include "cc/proto/property_tree.pb.h"
18 #include "cc/proto/synced_property_conversions.h" 18 #include "cc/proto/synced_property_conversions.h"
19 #include "cc/trees/clip_node.h" 19 #include "cc/trees/clip_node.h"
20 #include "cc/trees/effect_node.h" 20 #include "cc/trees/effect_node.h"
21 #include "cc/trees/layer_tree_host_common.h" 21 #include "cc/trees/layer_tree_host_common.h"
22 #include "cc/trees/layer_tree_impl.h" 22 #include "cc/trees/layer_tree_impl.h"
23 #include "cc/trees/property_tree.h" 23 #include "cc/trees/property_tree.h"
24 #include "cc/trees/scroll_node.h" 24 #include "cc/trees/scroll_node.h"
25 #include "cc/trees/transform_node.h" 25 #include "cc/trees/transform_node.h"
26 #include "ui/gfx/geometry/vector2d_conversions.h" 26 #include "ui/gfx/geometry/vector2d_conversions.h"
27
27 namespace cc { 28 namespace cc {
28 29
29 template <typename T> 30 template <typename T>
30 PropertyTree<T>::PropertyTree() 31 PropertyTree<T>::PropertyTree()
31 : needs_update_(false) { 32 : needs_update_(false) {
32 nodes_.push_back(T()); 33 nodes_.push_back(T());
33 back()->id = kRootNodeId; 34 back()->id = kRootNodeId;
34 back()->parent_id = kInvalidNodeId; 35 back()->parent_id = kInvalidNodeId;
35 } 36 }
36 37
(...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after
173 DCHECK(tree == *this); 174 DCHECK(tree == *this);
174 #endif 175 #endif
175 } 176 }
176 177
177 void TransformTree::set_needs_update(bool needs_update) { 178 void TransformTree::set_needs_update(bool needs_update) {
178 if (needs_update && !needs_update_) 179 if (needs_update && !needs_update_)
179 property_trees()->UpdateCachedNumber(); 180 property_trees()->UpdateCachedNumber();
180 needs_update_ = needs_update; 181 needs_update_ = needs_update;
181 } 182 }
182 183
183 bool TransformTree::ComputeTransform(int source_id, 184 bool TransformTree::ComputeTransformForTesting(
184 int dest_id, 185 int source_id,
185 gfx::Transform* transform) const { 186 int dest_id,
187 gfx::Transform* transform) const {
186 transform->MakeIdentity(); 188 transform->MakeIdentity();
187 189
188 if (source_id == dest_id) 190 if (source_id == dest_id)
189 return true; 191 return true;
190 192
191 if (source_id > dest_id) { 193 if (source_id > dest_id) {
192 CombineTransformsBetween(source_id, dest_id, transform); 194 CombineTransformsBetween(source_id, dest_id, transform);
193 return true; 195 return true;
194 } 196 }
195 197
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
271 TransformNode* target_node = Node(TargetId(id)); 273 TransformNode* target_node = Node(TargetId(id));
272 TransformNode* source_node = Node(node->source_node_id); 274 TransformNode* source_node = Node(node->source_node_id);
273 // TODO(flackr): Only dirty when scroll offset changes. 275 // TODO(flackr): Only dirty when scroll offset changes.
274 if (node->sticky_position_constraint_id >= 0 || 276 if (node->sticky_position_constraint_id >= 0 ||
275 node->needs_local_transform_update || NeedsSourceToParentUpdate(node)) { 277 node->needs_local_transform_update || NeedsSourceToParentUpdate(node)) {
276 UpdateLocalTransform(node); 278 UpdateLocalTransform(node);
277 } else { 279 } else {
278 UndoSnapping(node); 280 UndoSnapping(node);
279 } 281 }
280 UpdateScreenSpaceTransform(node, parent_node, target_node); 282 UpdateScreenSpaceTransform(node, parent_node, target_node);
281 UpdateSurfaceContentsScale(node);
282 UpdateAnimationProperties(node, parent_node); 283 UpdateAnimationProperties(node, parent_node);
283 UpdateSnapping(node); 284 UpdateSnapping(node);
284 UpdateNodeAndAncestorsHaveIntegerTranslations(node, parent_node); 285 UpdateNodeAndAncestorsHaveIntegerTranslations(node, parent_node);
285 UpdateTransformChanged(node, parent_node, source_node); 286 UpdateTransformChanged(node, parent_node, source_node);
286 UpdateNodeAndAncestorsAreAnimatedOrInvertible(node, parent_node); 287 UpdateNodeAndAncestorsAreAnimatedOrInvertible(node, parent_node);
287 } 288 }
288 289
289 bool TransformTree::IsDescendant(int desc_id, int source_id) const { 290 bool TransformTree::IsDescendant(int desc_id, int source_id) const {
290 while (desc_id != source_id) { 291 while (desc_id != source_id) {
291 if (desc_id == kInvalidNodeId) 292 if (desc_id == kInvalidNodeId)
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
325 // then we visit these nodes in reverse order, flattening as needed. We 326 // then we visit these nodes in reverse order, flattening as needed. We
326 // early-out if we get to a node whose target node is the destination, since 327 // early-out if we get to a node whose target node is the destination, since
327 // we can then re-use the target space transform stored at that node. However, 328 // we can then re-use the target space transform stored at that node. However,
328 // we cannot re-use a stored target space transform if the destination has a 329 // we cannot re-use a stored target space transform if the destination has a
329 // zero surface contents scale, since stored target space transforms have 330 // zero surface contents scale, since stored target space transforms have
330 // surface contents scale baked in, but we need to compute an unscaled 331 // surface contents scale baked in, but we need to compute an unscaled
331 // transform. 332 // transform.
332 std::vector<int> source_to_destination; 333 std::vector<int> source_to_destination;
333 source_to_destination.push_back(current->id); 334 source_to_destination.push_back(current->id);
334 current = parent(current); 335 current = parent(current);
335 bool destination_has_non_zero_surface_contents_scale =
336 dest->surface_contents_scale.x() != 0.f &&
337 dest->surface_contents_scale.y() != 0.f;
338 DCHECK(destination_has_non_zero_surface_contents_scale ||
339 !dest->ancestors_are_invertible);
340 for (; current && current->id > dest_id; current = parent(current)) 336 for (; current && current->id > dest_id; current = parent(current))
341 source_to_destination.push_back(current->id); 337 source_to_destination.push_back(current->id);
342 338
343 gfx::Transform combined_transform; 339 gfx::Transform combined_transform;
344 if (current->id > dest_id) { 340 if (current->id < dest_id) {
345 // The stored target space transform has surface contents scale baked in,
346 // but we need the unscaled transform.
347 combined_transform.matrix().postScale(
348 1.0f / dest->surface_contents_scale.x(),
349 1.0f / dest->surface_contents_scale.y(), 1.0f);
350 } else if (current->id < dest_id) {
351 // We have reached the lowest common ancestor of the source and destination 341 // We have reached the lowest common ancestor of the source and destination
352 // nodes. This case can occur when we are transforming between a node 342 // nodes. This case can occur when we are transforming between a node
353 // corresponding to a fixed-position layer (or its descendant) and the node 343 // corresponding to a fixed-position layer (or its descendant) and the node
354 // corresponding to the layer's render target. For example, consider the 344 // corresponding to the layer's render target. For example, consider the
355 // layer tree R->T->S->F where F is fixed-position, S owns a render surface, 345 // layer tree R->T->S->F where F is fixed-position, S owns a render surface,
356 // and T has a significant transform. This will yield the following 346 // and T has a significant transform. This will yield the following
357 // transform tree: 347 // transform tree:
358 // R 348 // R
359 // | 349 // |
360 // T 350 // T
(...skipping 215 matching lines...) Expand 10 before | Expand all | Expand 10 after
576 parent_node->node_and_ancestors_are_flat && node->to_parent.IsFlat(); 566 parent_node->node_and_ancestors_are_flat && node->to_parent.IsFlat();
577 SetToScreen(node->id, to_screen_space_transform); 567 SetToScreen(node->id, to_screen_space_transform);
578 } 568 }
579 569
580 gfx::Transform from_screen; 570 gfx::Transform from_screen;
581 if (!ToScreen(node->id).GetInverse(&from_screen)) 571 if (!ToScreen(node->id).GetInverse(&from_screen))
582 node->ancestors_are_invertible = false; 572 node->ancestors_are_invertible = false;
583 SetFromScreen(node->id, from_screen); 573 SetFromScreen(node->id, from_screen);
584 } 574 }
585 575
586 void TransformTree::UpdateSurfaceContentsScale(TransformNode* node) {
587 // The surface contents scale depends on the screen space transform, so update
588 // it too.
589 if (!node->needs_surface_contents_scale) {
590 node->surface_contents_scale = gfx::Vector2dF(1.0f, 1.0f);
591 return;
592 }
593
594 float layer_scale_factor =
595 device_scale_factor_ * device_transform_scale_factor_;
596 if (node->in_subtree_of_page_scale_layer)
597 layer_scale_factor *= page_scale_factor_;
598 node->surface_contents_scale = MathUtil::ComputeTransform2dScaleComponents(
599 ToScreen(node->id), layer_scale_factor);
600 }
601
602 void TransformTree::UpdateTargetSpaceTransform(TransformNode* node,
603 TransformNode* target_node) {
604 gfx::Transform target_space_transform;
605 if (node->needs_surface_contents_scale) {
606 target_space_transform.MakeIdentity();
607 target_space_transform.Scale(node->surface_contents_scale.x(),
608 node->surface_contents_scale.y());
609 } else {
610 // In order to include the root transform for the root surface, we walk up
611 // to the root of the transform tree in ComputeTransform.
612 int target_id = target_node->id;
613 ComputeTransform(node->id, target_id, &target_space_transform);
614 target_space_transform.matrix().postScale(
615 target_node->surface_contents_scale.x(),
616 target_node->surface_contents_scale.y(), 1.f);
617 }
618
619 gfx::Transform from_target;
620 if (!target_space_transform.GetInverse(&from_target))
621 node->ancestors_are_invertible = false;
622 SetToTarget(node->id, target_space_transform);
623 SetFromTarget(node->id, from_target);
624 }
625
626 void TransformTree::UpdateAnimationProperties(TransformNode* node, 576 void TransformTree::UpdateAnimationProperties(TransformNode* node,
627 TransformNode* parent_node) { 577 TransformNode* parent_node) {
628 bool ancestor_is_animating = false; 578 bool ancestor_is_animating = false;
629 if (parent_node) 579 if (parent_node)
630 ancestor_is_animating = parent_node->to_screen_is_potentially_animated; 580 ancestor_is_animating = parent_node->to_screen_is_potentially_animated;
631 node->to_screen_is_potentially_animated = 581 node->to_screen_is_potentially_animated =
632 node->has_potential_animation || ancestor_is_animating; 582 node->has_potential_animation || ancestor_is_animating;
633 } 583 }
634 584
635 void TransformTree::UndoSnapping(TransformNode* node) { 585 void TransformTree::UndoSnapping(TransformNode* node) {
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
721 if (node->post_local == post_local) 671 if (node->post_local == post_local)
722 return; 672 return;
723 node->post_local = post_local; 673 node->post_local = post_local;
724 node->needs_local_transform_update = true; 674 node->needs_local_transform_update = true;
725 set_needs_update(true); 675 set_needs_update(true);
726 } 676 }
727 677
728 void TransformTree::SetScreenSpaceScaleOnRootNode( 678 void TransformTree::SetScreenSpaceScaleOnRootNode(
729 gfx::Vector2dF screen_space_scale_components) { 679 gfx::Vector2dF screen_space_scale_components) {
730 TransformNode* node = Node(kRootNodeId); 680 TransformNode* node = Node(kRootNodeId);
731 if (node->surface_contents_scale == screen_space_scale_components) 681 gfx::Transform to_screen;
682 to_screen.Scale(screen_space_scale_components.x(),
683 screen_space_scale_components.y());
684 if (ToScreen(node->id) == to_screen)
732 return; 685 return;
733 node->needs_surface_contents_scale = true;
734 node->surface_contents_scale = screen_space_scale_components;
735 gfx::Transform to_screen;
736 to_screen.Scale(node->surface_contents_scale.x(),
737 node->surface_contents_scale.y());
738 SetToScreen(node->id, to_screen); 686 SetToScreen(node->id, to_screen);
739 gfx::Transform from_screen; 687 gfx::Transform from_screen;
740 if (!ToScreen(node->id).GetInverse(&from_screen)) 688 if (!ToScreen(node->id).GetInverse(&from_screen))
741 node->ancestors_are_invertible = false; 689 node->ancestors_are_invertible = false;
742 SetFromScreen(node->id, from_screen); 690 SetFromScreen(node->id, from_screen);
743 set_needs_update(true); 691 set_needs_update(true);
744 } 692 }
745 693
746 void TransformTree::SetRootTransformsAndScales( 694 void TransformTree::SetRootTransformsAndScales(
747 float device_scale_factor, 695 float device_scale_factor,
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
797 } 745 }
798 746
799 bool TransformTree::HasNodesAffectedByInnerViewportBoundsDelta() const { 747 bool TransformTree::HasNodesAffectedByInnerViewportBoundsDelta() const {
800 return !nodes_affected_by_inner_viewport_bounds_delta_.empty(); 748 return !nodes_affected_by_inner_viewport_bounds_delta_.empty();
801 } 749 }
802 750
803 bool TransformTree::HasNodesAffectedByOuterViewportBoundsDelta() const { 751 bool TransformTree::HasNodesAffectedByOuterViewportBoundsDelta() const {
804 return !nodes_affected_by_outer_viewport_bounds_delta_.empty(); 752 return !nodes_affected_by_outer_viewport_bounds_delta_.empty();
805 } 753 }
806 754
807 gfx::Transform TransformTree::FromTarget(int node_id, int effect_id) const {
808 gfx::Transform from_target;
809 property_trees()->GetFromTarget(node_id, effect_id, &from_target);
810 return from_target;
811 }
812
813 void TransformTree::SetFromTarget(int node_id,
814 const gfx::Transform& transform) {
815 DCHECK(static_cast<int>(cached_data_.size()) > node_id);
816 cached_data_[node_id].from_target = transform;
817 }
818
819 gfx::Transform TransformTree::ToTarget(int node_id, int effect_id) const {
820 gfx::Transform to_target;
821 property_trees()->GetToTarget(node_id, effect_id, &to_target);
822 return to_target;
823 }
824
825 void TransformTree::SetToTarget(int node_id, const gfx::Transform& transform) {
826 DCHECK(static_cast<int>(cached_data_.size()) > node_id);
827 cached_data_[node_id].to_target = transform;
828 }
829
830 const gfx::Transform& TransformTree::FromScreen(int node_id) const { 755 const gfx::Transform& TransformTree::FromScreen(int node_id) const {
831 DCHECK(static_cast<int>(cached_data_.size()) > node_id); 756 DCHECK(static_cast<int>(cached_data_.size()) > node_id);
832 return cached_data_[node_id].from_screen; 757 return cached_data_[node_id].from_screen;
833 } 758 }
834 759
835 void TransformTree::SetFromScreen(int node_id, 760 void TransformTree::SetFromScreen(int node_id,
836 const gfx::Transform& transform) { 761 const gfx::Transform& transform) {
837 DCHECK(static_cast<int>(cached_data_.size()) > node_id); 762 DCHECK(static_cast<int>(cached_data_.size()) > node_id);
838 cached_data_[node_id].from_screen = transform; 763 cached_data_[node_id].from_screen = transform;
839 } 764 }
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after
1028 TransformNode* transform_node = transform_tree.Node(node->transform_id); 953 TransformNode* transform_node = transform_tree.Node(node->transform_id);
1029 if (transform_node->is_invertible && 954 if (transform_node->is_invertible &&
1030 transform_node->ancestors_are_invertible) { 955 transform_node->ancestors_are_invertible) {
1031 if (transform_node->sorting_context_id) { 956 if (transform_node->sorting_context_id) {
1032 const TransformNode* parent_transform_node = 957 const TransformNode* parent_transform_node =
1033 transform_tree.parent(transform_node); 958 transform_tree.parent(transform_node);
1034 if (parent_transform_node && 959 if (parent_transform_node &&
1035 parent_transform_node->sorting_context_id == 960 parent_transform_node->sorting_context_id ==
1036 transform_node->sorting_context_id) { 961 transform_node->sorting_context_id) {
1037 gfx::Transform surface_draw_transform; 962 gfx::Transform surface_draw_transform;
1038 property_trees()->ComputeTransformToTarget( 963 property_trees()->GetToTarget(transform_node->id, node->target_id,
1039 transform_node->id, node->target_id, &surface_draw_transform); 964 &surface_draw_transform);
1040 node->hidden_by_backface_visibility = 965 node->hidden_by_backface_visibility =
1041 surface_draw_transform.IsBackFaceVisible(); 966 surface_draw_transform.IsBackFaceVisible();
1042 } else { 967 } else {
1043 node->hidden_by_backface_visibility = 968 node->hidden_by_backface_visibility =
1044 transform_node->local.IsBackFaceVisible(); 969 transform_node->local.IsBackFaceVisible();
1045 } 970 }
1046 return; 971 return;
1047 } 972 }
1048 } 973 }
1049 } 974 }
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after
1162 // For non-root surfaces, transform only by sub-layer scale. 1087 // For non-root surfaces, transform only by sub-layer scale.
1163 source_id = destination_id; 1088 source_id = destination_id;
1164 } else { 1089 } else {
1165 // The root surface doesn't have the notion of sub-layer scale, but 1090 // The root surface doesn't have the notion of sub-layer scale, but
1166 // instead has a similar notion of transforming from the space of the root 1091 // instead has a similar notion of transforming from the space of the root
1167 // layer to the space of the screen. 1092 // layer to the space of the screen.
1168 DCHECK_EQ(kRootNodeId, destination_id); 1093 DCHECK_EQ(kRootNodeId, destination_id);
1169 source_id = TransformTree::kContentsRootNodeId; 1094 source_id = TransformTree::kContentsRootNodeId;
1170 } 1095 }
1171 gfx::Transform transform; 1096 gfx::Transform transform;
1172 property_trees()->transform_tree.ComputeTransform(source_id, destination_id, 1097 property_trees()->GetToTarget(source_id, node_id, &transform);
1173 &transform);
1174 transform.matrix().postScale(effect_node->surface_contents_scale.x(),
1175 effect_node->surface_contents_scale.y(), 1.f);
1176 it->set_area(MathUtil::MapEnclosingClippedRect(transform, it->area())); 1098 it->set_area(MathUtil::MapEnclosingClippedRect(transform, it->area()));
1177 } 1099 }
1178 } 1100 }
1179 1101
1180 bool EffectTree::HasCopyRequests() const { 1102 bool EffectTree::HasCopyRequests() const {
1181 return !copy_requests_.empty(); 1103 return !copy_requests_.empty();
1182 } 1104 }
1183 1105
1184 void EffectTree::ClearCopyRequests() { 1106 void EffectTree::ClearCopyRequests() {
1185 for (auto& node : nodes()) { 1107 for (auto& node : nodes()) {
(...skipping 1146 matching lines...) Expand 10 before | Expand all | Expand 10 after
2332 gfx::Transform screen_space_transform = transform_tree.ToScreen(transform_id); 2254 gfx::Transform screen_space_transform = transform_tree.ToScreen(transform_id);
2333 const EffectNode* effect_node = effect_tree.Node(effect_id); 2255 const EffectNode* effect_node = effect_tree.Node(effect_id);
2334 2256
2335 if (effect_node->surface_contents_scale.x() != 0.0 && 2257 if (effect_node->surface_contents_scale.x() != 0.0 &&
2336 effect_node->surface_contents_scale.y() != 0.0) 2258 effect_node->surface_contents_scale.y() != 0.0)
2337 screen_space_transform.Scale(1.0 / effect_node->surface_contents_scale.x(), 2259 screen_space_transform.Scale(1.0 / effect_node->surface_contents_scale.x(),
2338 1.0 / effect_node->surface_contents_scale.y()); 2260 1.0 / effect_node->surface_contents_scale.y());
2339 return screen_space_transform; 2261 return screen_space_transform;
2340 } 2262 }
2341 2263
2342 bool PropertyTrees::ComputeTransformToTarget(int transform_id,
2343 int effect_id,
2344 gfx::Transform* transform) const {
2345 transform->MakeIdentity();
2346 if (transform_id == TransformTree::kInvalidNodeId)
2347 return true;
2348
2349 const EffectNode* effect_node = effect_tree.Node(effect_id);
2350
2351 bool success = true;
2352 success = GetToTarget(transform_id, effect_id, transform);
2353 if (effect_node->surface_contents_scale.x() != 0.f &&
2354 effect_node->surface_contents_scale.y() != 0.f)
2355 transform->matrix().postScale(
2356 1.0f / effect_node->surface_contents_scale.x(),
2357 1.0f / effect_node->surface_contents_scale.y(), 1.0f);
2358 return success;
2359 }
2360
2361 bool PropertyTrees::ComputeTransformFromTarget( 2264 bool PropertyTrees::ComputeTransformFromTarget(
2362 int transform_id, 2265 int transform_id,
2363 int effect_id, 2266 int effect_id,
2364 gfx::Transform* transform) const { 2267 gfx::Transform* transform) const {
2365 transform->MakeIdentity(); 2268 transform->MakeIdentity();
2366 if (transform_id == TransformTree::kInvalidNodeId) 2269 if (transform_id == TransformTree::kInvalidNodeId)
2367 return true; 2270 return true;
2368 2271
2369 const EffectNode* effect_node = effect_tree.Node(effect_id); 2272 const EffectNode* effect_node = effect_tree.Node(effect_id);
2370 2273
2371 bool success = GetFromTarget(transform_id, effect_id, transform); 2274 bool success = GetFromTarget(transform_id, effect_id, transform);
2372 transform->Scale(effect_node->surface_contents_scale.x(), 2275 transform->Scale(effect_node->surface_contents_scale.x(),
2373 effect_node->surface_contents_scale.y()); 2276 effect_node->surface_contents_scale.y());
2374 return success; 2277 return success;
2375 } 2278 }
2376 2279
2377 } // namespace cc 2280 } // namespace cc
OLDNEW
« no previous file with comments | « cc/trees/property_tree.h ('k') | cc/trees/property_tree_builder.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698