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

Side by Side Diff: sky/engine/core/rendering/compositing/RenderLayerCompositor.cpp

Issue 758843004: Delete most of rendering/compositing. (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 6 years 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
(Empty)
1 /*
2 * Copyright (C) 2009, 2010 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26 #include "sky/engine/config.h"
27
28 #include "sky/engine/core/rendering/compositing/RenderLayerCompositor.h"
29
30 #include "gen/sky/platform/RuntimeEnabledFeatures.h"
31 #include "sky/engine/core/animation/DocumentAnimations.h"
32 #include "sky/engine/core/frame/FrameView.h"
33 #include "sky/engine/core/frame/LocalFrame.h"
34 #include "sky/engine/core/frame/Settings.h"
35 #include "sky/engine/core/inspector/InspectorNodeIds.h"
36 #include "sky/engine/core/page/Chrome.h"
37 #include "sky/engine/core/page/ChromeClient.h"
38 #include "sky/engine/core/page/Page.h"
39 #include "sky/engine/core/rendering/RenderLayerStackingNode.h"
40 #include "sky/engine/core/rendering/RenderLayerStackingNodeIterator.h"
41 #include "sky/engine/core/rendering/RenderView.h"
42 #include "sky/engine/platform/ScriptForbiddenScope.h"
43 #include "sky/engine/platform/TraceEvent.h"
44 #include "sky/engine/platform/graphics/GraphicsLayer.h"
45 #include "sky/engine/public/platform/Platform.h"
46
47 namespace blink {
48
49 RenderLayerCompositor::RenderLayerCompositor(RenderView& renderView)
50 : m_renderView(renderView)
51 , m_compositingReasonFinder(renderView)
52 , m_pendingUpdateType(CompositingUpdateNone)
53 , m_compositing(false)
54 , m_rootShouldAlwaysCompositeDirty(true)
55 , m_needsUpdateFixedBackground(false)
56 , m_isTrackingPaintInvalidations(false)
57 , m_rootLayerAttachment(RootLayerUnattached)
58 {
59 updateAcceleratedCompositingSettings();
60 }
61
62 RenderLayerCompositor::~RenderLayerCompositor()
63 {
64 ASSERT(m_rootLayerAttachment == RootLayerUnattached);
65 }
66
67 bool RenderLayerCompositor::inCompositingMode() const
68 {
69 // FIXME: This should assert that lificycle is >= CompositingClean since
70 // the last step of updateIfNeeded can set this bit to false.
71 ASSERT(!m_rootShouldAlwaysCompositeDirty);
72 return m_compositing;
73 }
74
75 bool RenderLayerCompositor::staleInCompositingMode() const
76 {
77 return m_compositing;
78 }
79
80 void RenderLayerCompositor::setCompositingModeEnabled(bool enable)
81 {
82 if (enable == m_compositing)
83 return;
84
85 m_compositing = enable;
86
87 if (m_compositing)
88 ensureRootLayer();
89 else
90 destroyRootLayer();
91 }
92
93 void RenderLayerCompositor::enableCompositingModeIfNeeded()
94 {
95 if (!m_rootShouldAlwaysCompositeDirty)
96 return;
97
98 m_rootShouldAlwaysCompositeDirty = false;
99 if (m_compositing)
100 return;
101
102 if (rootShouldAlwaysComposite()) {
103 // FIXME: Is this needed? It was added in https://bugs.webkit.org/show_b ug.cgi?id=26651.
104 // No tests fail if it's deleted.
105 setNeedsCompositingUpdate(CompositingUpdateRebuildTree);
106 setCompositingModeEnabled(true);
107 }
108 }
109
110 bool RenderLayerCompositor::rootShouldAlwaysComposite() const
111 {
112 return false;
113 }
114
115 void RenderLayerCompositor::updateAcceleratedCompositingSettings()
116 {
117 m_compositingReasonFinder.updateTriggers();
118 m_rootShouldAlwaysCompositeDirty = true;
119 }
120
121 bool RenderLayerCompositor::hasAcceleratedCompositing() const
122 {
123 return false;
124 }
125
126 bool RenderLayerCompositor::layerSquashingEnabled() const
127 {
128 if (!RuntimeEnabledFeatures::layerSquashingEnabled())
129 return false;
130 return true;
131 }
132
133 bool RenderLayerCompositor::preferCompositingToLCDTextEnabled() const
134 {
135 return m_compositingReasonFinder.hasOverflowScrollTrigger();
136 }
137
138 void RenderLayerCompositor::updateIfNeededRecursive()
139 {
140 TRACE_EVENT0("blink", "RenderLayerCompositor::updateIfNeededRecursive");
141
142 ASSERT(!m_renderView.needsLayout());
143
144 ScriptForbiddenScope forbidScript;
145
146 // FIXME: enableCompositingModeIfNeeded can trigger a CompositingUpdateRebui ldTree,
147 // which asserts that it's not InCompositingUpdate.
148 enableCompositingModeIfNeeded();
149
150 lifecycle().advanceTo(DocumentLifecycle::InCompositingUpdate);
151 updateIfNeeded();
152 lifecycle().advanceTo(DocumentLifecycle::CompositingClean);
153
154 DocumentAnimations::startPendingAnimations(m_renderView.document());
155
156 #if ENABLE(ASSERT)
157 ASSERT(lifecycle().state() == DocumentLifecycle::CompositingClean);
158 assertNoUnresolvedDirtyBits();
159 #endif
160 }
161
162 void RenderLayerCompositor::setNeedsCompositingUpdate(CompositingUpdateType upda teType)
163 {
164 ASSERT(updateType != CompositingUpdateNone);
165 m_pendingUpdateType = std::max(m_pendingUpdateType, updateType);
166 page()->animator().scheduleVisualUpdate();
167 lifecycle().ensureStateAtMost(DocumentLifecycle::LayoutClean);
168 }
169
170 void RenderLayerCompositor::didLayout()
171 {
172 // FIXME: Technically we only need to do this when the FrameView's
173 // isScrollable method would return a different value.
174 m_rootShouldAlwaysCompositeDirty = true;
175 enableCompositingModeIfNeeded();
176
177 // FIXME: Rather than marking the entire RenderView as dirty, we should
178 // track which RenderLayers moved during layout and only dirty those
179 // specific RenderLayers.
180 rootRenderLayer()->setNeedsCompositingInputsUpdate();
181 }
182
183 #if ENABLE(ASSERT)
184
185 void RenderLayerCompositor::assertNoUnresolvedDirtyBits()
186 {
187 ASSERT(m_pendingUpdateType == CompositingUpdateNone);
188 ASSERT(!m_rootShouldAlwaysCompositeDirty);
189 }
190
191 #endif
192
193 void RenderLayerCompositor::updateWithoutAcceleratedCompositing(CompositingUpdat eType updateType)
194 {
195 }
196
197 void RenderLayerCompositor::updateIfNeeded()
198 {
199 m_pendingUpdateType = CompositingUpdateNone;
200 }
201
202 bool RenderLayerCompositor::allocateOrClearCompositedLayerMapping(RenderLayer*, const CompositingStateTransitionType)
203 {
204 return false;
205 }
206
207 void RenderLayerCompositor::paintInvalidationOnCompositingChange(RenderLayer* la yer)
208 {
209 // If the renderer is not attached yet, no need to issue paint invalidations .
210 if (layer->renderer() != &m_renderView && !layer->renderer()->parent())
211 return;
212
213 // For querying RenderLayer::compositingState()
214 // Eager invalidation here is correct, since we are invalidating with respec t to the previous frame's
215 // compositing state when changing the compositing backing of the layer.
216 DisableCompositingQueryAsserts disabler;
217
218 layer->paintInvalidator().paintInvalidationIncludingNonCompositingDescendant s();
219 }
220
221 void RenderLayerCompositor::frameViewDidChangeLocation(const IntPoint& contentsO ffset)
222 {
223 if (m_overflowControlsHostLayer)
224 m_overflowControlsHostLayer->setPosition(contentsOffset);
225 }
226
227 void RenderLayerCompositor::frameViewDidChangeSize()
228 {
229 if (m_containerLayer) {
230 FrameView* frameView = m_renderView.frameView();
231 m_containerLayer->setSize(frameView->unscaledVisibleContentSize());
232 }
233 }
234
235 void RenderLayerCompositor::rootFixedBackgroundsChanged()
236 {
237 if (!supportsFixedRootBackgroundCompositing())
238 return;
239
240 // To avoid having to make the fixed root background layer fixed positioned to
241 // stay put, we position it in the layer tree as follows:
242 //
243 // + Overflow controls host
244 // + LocalFrame clip
245 // + (Fixed root background) <-- Here.
246 // + LocalFrame scroll
247 // + Root content layer
248 // + Scrollbars
249 //
250 // That is, it needs to be the first child of the frame clip, the sibling of
251 // the frame scroll layer. The compositor does not own the background layer, it
252 // just positions it (like the foreground layer).
253 if (GraphicsLayer* backgroundLayer = fixedRootBackgroundLayer())
254 m_containerLayer->addChildBelow(backgroundLayer, m_scrollLayer.get());
255 }
256
257 String RenderLayerCompositor::layerTreeAsText(LayerTreeFlags flags)
258 {
259 ASSERT(lifecycle().state() >= DocumentLifecycle::PaintInvalidationClean);
260
261 if (!m_rootContentLayer)
262 return String();
263
264 // We skip dumping the scroll and clip layers to keep layerTreeAsText output
265 // similar between platforms (unless we explicitly request dumping from the
266 // root.
267 GraphicsLayer* rootLayer = m_rootContentLayer.get();
268 if (flags & LayerTreeIncludesRootLayer)
269 rootLayer = rootGraphicsLayer();
270
271 String layerTreeText = rootLayer->layerTreeAsText(flags);
272
273 // The true root layer is not included in the dump, so if we want to report
274 // its paint invalidation rects, they must be included here.
275 if (flags & LayerTreeIncludesPaintInvalidationRects)
276 return m_renderView.frameView()->trackedPaintInvalidationRectsAsText() + layerTreeText;
277
278 return layerTreeText;
279 }
280
281 static void fullyInvalidatePaintRecursive(RenderLayer* layer)
282 {
283 for (RenderLayer* child = layer->firstChild(); child; child = child->nextSib ling())
284 fullyInvalidatePaintRecursive(child);
285 }
286
287 void RenderLayerCompositor::fullyInvalidatePaint()
288 {
289 // We're walking all compositing layers and invalidating them, so there's
290 // no need to have up-to-date compositing state.
291 DisableCompositingQueryAsserts disabler;
292 fullyInvalidatePaintRecursive(rootRenderLayer());
293 }
294
295 RenderLayer* RenderLayerCompositor::rootRenderLayer() const
296 {
297 return m_renderView.layer();
298 }
299
300 GraphicsLayer* RenderLayerCompositor::rootGraphicsLayer() const
301 {
302 if (m_overflowControlsHostLayer)
303 return m_overflowControlsHostLayer.get();
304 return m_rootContentLayer.get();
305 }
306
307 GraphicsLayer* RenderLayerCompositor::scrollLayer() const
308 {
309 return m_scrollLayer.get();
310 }
311
312 GraphicsLayer* RenderLayerCompositor::containerLayer() const
313 {
314 return m_containerLayer.get();
315 }
316
317 GraphicsLayer* RenderLayerCompositor::ensureRootTransformLayer()
318 {
319 ASSERT(rootGraphicsLayer());
320
321 if (!m_rootTransformLayer.get()) {
322 m_rootTransformLayer = GraphicsLayer::create(graphicsLayerFactory(), thi s);
323 m_overflowControlsHostLayer->addChild(m_rootTransformLayer.get());
324 m_rootTransformLayer->addChild(m_containerLayer.get());
325 }
326
327 return m_rootTransformLayer.get();
328 }
329
330 void RenderLayerCompositor::setIsInWindow(bool isInWindow)
331 {
332 if (!staleInCompositingMode())
333 return;
334
335 if (isInWindow) {
336 if (m_rootLayerAttachment != RootLayerUnattached)
337 return;
338
339 RootLayerAttachment attachment = RootLayerAttachedViaChromeClient;
340 attachRootLayer(attachment);
341 } else {
342 if (m_rootLayerAttachment == RootLayerUnattached)
343 return;
344
345 detachRootLayer();
346 }
347 }
348
349 void RenderLayerCompositor::updateRootLayerPosition()
350 {
351 if (m_rootContentLayer) {
352 const IntRect& documentRect = m_renderView.documentRect();
353 m_rootContentLayer->setSize(documentRect.size());
354 m_rootContentLayer->setPosition(documentRect.location());
355 }
356 if (m_containerLayer) {
357 FrameView* frameView = m_renderView.frameView();
358 m_containerLayer->setSize(frameView->unscaledVisibleContentSize());
359 }
360 }
361
362 void RenderLayerCompositor::updatePotentialCompositingReasonsFromStyle(RenderLay er* layer)
363 {
364 layer->setPotentialCompositingReasonsFromStyle(m_compositingReasonFinder.pot entialCompositingReasonsFromStyle(layer->renderer()));
365 }
366
367 void RenderLayerCompositor::updateDirectCompositingReasons(RenderLayer* layer)
368 {
369 layer->setCompositingReasons(m_compositingReasonFinder.directReasons(layer), CompositingReasonComboAllDirectReasons);
370 }
371
372 void RenderLayerCompositor::setOverlayLayer(GraphicsLayer* layer)
373 {
374 ASSERT(rootGraphicsLayer());
375
376 if (layer->parent() != m_overflowControlsHostLayer.get())
377 m_overflowControlsHostLayer->addChild(layer);
378 }
379
380 bool RenderLayerCompositor::canBeComposited(const RenderLayer* layer) const
381 {
382 // FIXME: We disable accelerated compositing for elements in a RenderFlowThr ead as it doesn't work properly.
383 // See http://webkit.org/b/84900 to re-enable it.
384 return layer->isSelfPaintingLayer();
385 }
386
387 // Return true if the given layer is a stacking context and has compositing chil d
388 // layers that it needs to clip. In this case we insert a clipping GraphicsLayer
389 // into the hierarchy between this layer and its children in the z-order hierarc hy.
390 bool RenderLayerCompositor::clipsCompositingDescendants(const RenderLayer* layer ) const
391 {
392 return layer->hasCompositingDescendant() && layer->renderer()->hasClipOrOver flowClip();
393 }
394
395 // If an element has negative z-index children, those children render in front o f the
396 // layer background, so we need an extra 'contents' layer for the foreground of the layer
397 // object.
398 bool RenderLayerCompositor::needsContentsCompositingLayer(const RenderLayer* lay er) const
399 {
400 return layer->stackingNode()->hasNegativeZOrderList();
401 }
402
403 void RenderLayerCompositor::paintContents(const GraphicsLayer* graphicsLayer, Gr aphicsContext& context, GraphicsLayerPaintingPhase, const IntRect& clip)
404 {
405 // FIXME(sky): Remove.
406 }
407
408 bool RenderLayerCompositor::supportsFixedRootBackgroundCompositing() const
409 {
410 if (Settings* settings = m_renderView.document().settings())
411 return settings->preferCompositingToLCDTextEnabled();
412 return false;
413 }
414
415 bool RenderLayerCompositor::needsFixedRootBackgroundLayer(const RenderLayer* lay er) const
416 {
417 if (layer != m_renderView.layer())
418 return false;
419
420 return supportsFixedRootBackgroundCompositing() && m_renderView.rootBackgrou ndIsEntirelyFixed();
421 }
422
423 GraphicsLayer* RenderLayerCompositor::fixedRootBackgroundLayer() const
424 {
425 return 0;
426 }
427
428 static void resetTrackedPaintInvalidationRectsRecursive(GraphicsLayer* graphicsL ayer)
429 {
430 if (!graphicsLayer)
431 return;
432
433 graphicsLayer->resetTrackedPaintInvalidations();
434
435 for (size_t i = 0; i < graphicsLayer->children().size(); ++i)
436 resetTrackedPaintInvalidationRectsRecursive(graphicsLayer->children()[i] );
437
438 if (GraphicsLayer* maskLayer = graphicsLayer->maskLayer())
439 resetTrackedPaintInvalidationRectsRecursive(maskLayer);
440
441 if (GraphicsLayer* clippingMaskLayer = graphicsLayer->contentsClippingMaskLa yer())
442 resetTrackedPaintInvalidationRectsRecursive(clippingMaskLayer);
443 }
444
445 void RenderLayerCompositor::resetTrackedPaintInvalidationRects()
446 {
447 if (GraphicsLayer* rootLayer = rootGraphicsLayer())
448 resetTrackedPaintInvalidationRectsRecursive(rootLayer);
449 }
450
451 void RenderLayerCompositor::setTracksPaintInvalidations(bool tracksPaintInvalida tions)
452 {
453 ASSERT(lifecycle().state() == DocumentLifecycle::PaintInvalidationClean);
454 m_isTrackingPaintInvalidations = tracksPaintInvalidations;
455 }
456
457 bool RenderLayerCompositor::isTrackingPaintInvalidations() const
458 {
459 return m_isTrackingPaintInvalidations;
460 }
461
462 void RenderLayerCompositor::ensureRootLayer()
463 {
464 RootLayerAttachment expectedAttachment = RootLayerAttachedViaChromeClient;
465 if (expectedAttachment == m_rootLayerAttachment)
466 return;
467
468 if (!m_rootContentLayer) {
469 m_rootContentLayer = GraphicsLayer::create(graphicsLayerFactory(), this) ;
470 IntRect overflowRect = m_renderView.pixelSnappedLayoutOverflowRect();
471 m_rootContentLayer->setSize(FloatSize(overflowRect.maxX(), overflowRect. maxY()));
472 m_rootContentLayer->setPosition(FloatPoint());
473 m_rootContentLayer->setOwnerNodeId(InspectorNodeIds::idForNode(m_renderV iew.node()));
474
475 // Need to clip to prevent transformed content showing outside this fram e
476 m_rootContentLayer->setMasksToBounds(true);
477 }
478
479 if (!m_overflowControlsHostLayer) {
480 ASSERT(!m_scrollLayer);
481 ASSERT(!m_containerLayer);
482
483 // Create a layer to host the clipping layer and the overflow controls l ayers.
484 m_overflowControlsHostLayer = GraphicsLayer::create(graphicsLayerFactory (), this);
485
486 // Create a clipping layer if this is an iframe or settings require to c lip.
487 m_containerLayer = GraphicsLayer::create(graphicsLayerFactory(), this);
488 bool containerMasksToBounds = false;
489 if (Settings* settings = m_renderView.document().settings()) {
490 if (settings->mainFrameClipsContent())
491 containerMasksToBounds = true;
492 }
493 m_containerLayer->setMasksToBounds(containerMasksToBounds);
494
495 m_scrollLayer = GraphicsLayer::create(graphicsLayerFactory(), this);
496
497 // Hook them up
498 m_overflowControlsHostLayer->addChild(m_containerLayer.get());
499 m_containerLayer->addChild(m_scrollLayer.get());
500 m_scrollLayer->addChild(m_rootContentLayer.get());
501
502 frameViewDidChangeSize();
503 }
504
505 // Check to see if we have to change the attachment
506 if (m_rootLayerAttachment != RootLayerUnattached)
507 detachRootLayer();
508
509 attachRootLayer(expectedAttachment);
510 }
511
512 void RenderLayerCompositor::destroyRootLayer()
513 {
514 if (!m_rootContentLayer)
515 return;
516
517 detachRootLayer();
518
519 if (m_overflowControlsHostLayer) {
520 m_overflowControlsHostLayer = nullptr;
521 m_containerLayer = nullptr;
522 m_scrollLayer = nullptr;
523 }
524 ASSERT(!m_scrollLayer);
525 m_rootContentLayer = nullptr;
526 m_rootTransformLayer = nullptr;
527 }
528
529 void RenderLayerCompositor::attachRootLayer(RootLayerAttachment attachment)
530 {
531 if (!m_rootContentLayer)
532 return;
533
534 switch (attachment) {
535 case RootLayerUnattached:
536 ASSERT_NOT_REACHED();
537 break;
538 case RootLayerAttachedViaChromeClient: {
539 LocalFrame& frame = m_renderView.frameView()->frame();
540 Page* page = frame.page();
541 if (!page)
542 return;
543 page->chrome().client().attachRootGraphicsLayer(rootGraphicsLayer()) ;
544 break;
545 }
546 case RootLayerAttachedViaEnclosingFrame: {
547 ASSERT_NOT_REACHED();
548 }
549 }
550
551 m_rootLayerAttachment = attachment;
552 }
553
554 void RenderLayerCompositor::detachRootLayer()
555 {
556 if (!m_rootContentLayer || m_rootLayerAttachment == RootLayerUnattached)
557 return;
558
559 switch (m_rootLayerAttachment) {
560 case RootLayerAttachedViaEnclosingFrame: {
561 ASSERT_NOT_REACHED();
562 }
563 case RootLayerAttachedViaChromeClient: {
564 LocalFrame& frame = m_renderView.frameView()->frame();
565 Page* page = frame.page();
566 if (!page)
567 return;
568 page->chrome().client().attachRootGraphicsLayer(0);
569 }
570 break;
571 case RootLayerUnattached:
572 break;
573 }
574
575 m_rootLayerAttachment = RootLayerUnattached;
576 }
577
578 void RenderLayerCompositor::updateRootLayerAttachment()
579 {
580 ensureRootLayer();
581 }
582
583 GraphicsLayerFactory* RenderLayerCompositor::graphicsLayerFactory() const
584 {
585 if (Page* page = this->page())
586 return page->chrome().client().graphicsLayerFactory();
587 return 0;
588 }
589
590 Page* RenderLayerCompositor::page() const
591 {
592 return m_renderView.frameView()->frame().page();
593 }
594
595 DocumentLifecycle& RenderLayerCompositor::lifecycle() const
596 {
597 return m_renderView.document().lifecycle();
598 }
599
600 String RenderLayerCompositor::debugName(const GraphicsLayer* graphicsLayer)
601 {
602 String name;
603 if (graphicsLayer == m_rootContentLayer.get()) {
604 name = "Content Root Layer";
605 } else if (graphicsLayer == m_rootTransformLayer.get()) {
606 name = "Root Transform Layer";
607 } else if (graphicsLayer == m_overflowControlsHostLayer.get()) {
608 name = "Overflow Controls Host Layer";
609 } else if (graphicsLayer == m_containerLayer.get()) {
610 name = "LocalFrame Clipping Layer";
611 } else if (graphicsLayer == m_scrollLayer.get()) {
612 name = "LocalFrame Scrolling Layer";
613 } else {
614 ASSERT_NOT_REACHED();
615 }
616
617 return name;
618 }
619
620 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698