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

Side by Side Diff: Source/core/html/parser/HTMLConstructionSite.cpp

Issue 26129005: Rewrite Text node attaching to not be N^2 (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Seems to actually work Created 7 years, 2 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 /* 1 /*
2 * Copyright (C) 2010 Google, Inc. All Rights Reserved. 2 * Copyright (C) 2010 Google, Inc. All Rights Reserved.
3 * Copyright (C) 2011 Apple Inc. All rights reserved. 3 * Copyright (C) 2011 Apple Inc. All rights reserved.
4 * 4 *
5 * Redistribution and use in source and binary forms, with or without 5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions 6 * modification, are permitted provided that the following conditions
7 * are met: 7 * are met:
8 * 1. Redistributions of source code must retain the above copyright 8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright 10 * 2. Redistributions in binary form must reproduce the above copyright
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after
112 ASSERT(task.operation == HTMLConstructionSiteTask::Insert); 112 ASSERT(task.operation == HTMLConstructionSiteTask::Insert);
113 113
114 insert(task); 114 insert(task);
115 115
116 task.child->beginParsingChildren(); 116 task.child->beginParsingChildren();
117 117
118 if (task.selfClosing) 118 if (task.selfClosing)
119 task.child->finishParsingChildren(); 119 task.child->finishParsingChildren();
120 } 120 }
121 121
122 static inline void executeInsertTextTask(HTMLConstructionSiteTask& task)
123 {
124 ASSERT(task.operation == HTMLConstructionSiteTask::InsertText);
125 ASSERT(task.child->isTextNode());
126
127 // Merge text nodes into previous ones if possible:
128 // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construc tion.html#insert-a-character
129 Text* newText = toText(task.child.get());
130 Node* previousChild = task.nextChild ? task.nextChild->previousSibling() : t ask.parent->lastChild();
131 if (previousChild && previousChild->isTextNode()) {
132 Text* previousText = toText(previousChild);
133 unsigned lengthLimit = textLengthLimitForContainer(task.parent.get());
134 if (previousText->length() + newText->length() < lengthLimit) {
135 previousText->parserAppendData(newText->data());
136 return;
137 }
138 }
139
140 insert(task);
141 }
142
122 static inline void executeReparentTask(HTMLConstructionSiteTask& task) 143 static inline void executeReparentTask(HTMLConstructionSiteTask& task)
123 { 144 {
124 ASSERT(task.operation == HTMLConstructionSiteTask::Reparent); 145 ASSERT(task.operation == HTMLConstructionSiteTask::Reparent);
125 146
126 if (ContainerNode* parent = task.child->parentNode()) 147 if (ContainerNode* parent = task.child->parentNode())
127 parent->parserRemoveChild(task.child.get()); 148 parent->parserRemoveChild(task.child.get());
128 149
129 task.parent->parserAppendChild(task.child); 150 task.parent->parserAppendChild(task.child);
130 } 151 }
131 152
(...skipping 10 matching lines...) Expand all
142 163
143 task.parent->parserTakeAllChildrenFrom(task.oldParent()); 164 task.parent->parserTakeAllChildrenFrom(task.oldParent());
144 } 165 }
145 166
146 void HTMLConstructionSite::executeTask(HTMLConstructionSiteTask& task) 167 void HTMLConstructionSite::executeTask(HTMLConstructionSiteTask& task)
147 { 168 {
148 ASSERT(m_taskQueue.isEmpty()); 169 ASSERT(m_taskQueue.isEmpty());
149 if (task.operation == HTMLConstructionSiteTask::Insert) 170 if (task.operation == HTMLConstructionSiteTask::Insert)
150 return executeInsertTask(task); 171 return executeInsertTask(task);
151 172
173 if (task.operation == HTMLConstructionSiteTask::InsertText)
174 return executeInsertTextTask(task);
175
152 // All the cases below this point are only used by the adoption agency. 176 // All the cases below this point are only used by the adoption agency.
153 177
154 if (task.operation == HTMLConstructionSiteTask::InsertAlreadyParsedChild) 178 if (task.operation == HTMLConstructionSiteTask::InsertAlreadyParsedChild)
155 return executeInsertAlreadyParsedChildTask(task); 179 return executeInsertAlreadyParsedChildTask(task);
156 180
157 if (task.operation == HTMLConstructionSiteTask::Reparent) 181 if (task.operation == HTMLConstructionSiteTask::Reparent)
158 return executeReparentTask(task); 182 return executeReparentTask(task);
159 183
160 if (task.operation == HTMLConstructionSiteTask::TakeAllChildren) 184 if (task.operation == HTMLConstructionSiteTask::TakeAllChildren)
161 return executeTakeAllChildrenTask(task); 185 return executeTakeAllChildrenTask(task);
162 186
163 ASSERT_NOT_REACHED(); 187 ASSERT_NOT_REACHED();
164 } 188 }
165 189
166 // This is only needed for TextDocuments where we might have text nodes 190 // This is only needed for TextDocuments where we might have text nodes
167 // approaching the default length limit (~64k) and we don't want to 191 // approaching the default length limit (~64k) and we don't want to
168 // break a text node in the middle of a combining character. 192 // break a text node in the middle of a combining character.
169 static unsigned findBreakIndexBetween(const String& string, unsigned currentPosi tion, unsigned proposedBreakIndex) 193 static unsigned findBreakIndexBetween(const StringBuilder& string, unsigned curr entPosition, unsigned proposedBreakIndex)
170 { 194 {
171 ASSERT(currentPosition < proposedBreakIndex); 195 ASSERT(currentPosition < proposedBreakIndex);
172 ASSERT(proposedBreakIndex <= string.length()); 196 ASSERT(proposedBreakIndex <= string.length());
173 // The end of the string is always a valid break. 197 // The end of the string is always a valid break.
174 if (proposedBreakIndex == string.length()) 198 if (proposedBreakIndex == string.length())
175 return proposedBreakIndex; 199 return proposedBreakIndex;
176 200
177 // Latin-1 does not have breakable boundaries. If we ever moved to a differn et 8-bit encoding this could be wrong. 201 // Latin-1 does not have breakable boundaries. If we ever moved to a differn et 8-bit encoding this could be wrong.
178 if (string.is8Bit()) 202 if (string.is8Bit())
179 return proposedBreakIndex; 203 return proposedBreakIndex;
(...skipping 15 matching lines...) Expand all
195 219
196 static String atomizeIfAllWhitespace(const String& string, WhitespaceMode whites paceMode) 220 static String atomizeIfAllWhitespace(const String& string, WhitespaceMode whites paceMode)
197 { 221 {
198 // Strings composed entirely of whitespace are likely to be repeated. 222 // Strings composed entirely of whitespace are likely to be repeated.
199 // Turn them into AtomicString so we share a single string for each. 223 // Turn them into AtomicString so we share a single string for each.
200 if (whitespaceMode == AllWhitespace || (whitespaceMode == WhitespaceUnknown && isAllWhitespace(string))) 224 if (whitespaceMode == AllWhitespace || (whitespaceMode == WhitespaceUnknown && isAllWhitespace(string)))
201 return AtomicString(string).string(); 225 return AtomicString(string).string();
202 return string; 226 return string;
203 } 227 }
204 228
229 void HTMLConstructionSite::flushPendingText()
230 {
231 if (m_pendingText.isEmpty())
232 return;
233
234 PendingText pendingText;
235 // Hold onto the current pending text on the stack so that queueTask doesn't recurse infinitely.
236 m_pendingText.swap(pendingText);
237 ASSERT(m_pendingText.isEmpty());
238
239 // Splitting text nodes into smaller chunks contradicts HTML5 spec, but is n ecessary
240 // for performance, see: https://bugs.webkit.org/show_bug.cgi?id=55898
241 unsigned lengthLimit = textLengthLimitForContainer(pendingText.parent.get()) ;
242
243 unsigned currentPosition = 0;
244 const StringBuilder& string = pendingText.stringBuilder;
245 while (currentPosition < string.length()) {
246 unsigned proposedBreakIndex = std::min(currentPosition + lengthLimit, st ring.length());
247 unsigned breakIndex = findBreakIndexBetween(string, currentPosition, pro posedBreakIndex);
248 ASSERT(breakIndex <= string.length());
249 String substring = string.substring(currentPosition, breakIndex - curren tPosition);
250 substring = atomizeIfAllWhitespace(substring, pendingText.whitespaceMode );
251
252 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::InsertText);
253 task.parent = pendingText.parent;
254 task.nextChild = pendingText.nextChild;
255 task.child = Text::create(task.parent->document(), substring);
256 queueTask(task);
257
258 ASSERT(breakIndex > currentPosition);
259 ASSERT(breakIndex - currentPosition == substring.length());
260 ASSERT(toText(task.child.get())->length() == substring.length());
261 currentPosition = breakIndex;
262 }
263 }
264
205 void HTMLConstructionSite::queueTask(const HTMLConstructionSiteTask& task) 265 void HTMLConstructionSite::queueTask(const HTMLConstructionSiteTask& task)
206 { 266 {
267 flushPendingText();
268 ASSERT(m_pendingText.isEmpty());
207 m_taskQueue.append(task); 269 m_taskQueue.append(task);
208 } 270 }
209 271
210 void HTMLConstructionSite::attachLater(ContainerNode* parent, PassRefPtr<Node> p rpChild, bool selfClosing) 272 void HTMLConstructionSite::attachLater(ContainerNode* parent, PassRefPtr<Node> p rpChild, bool selfClosing)
211 { 273 {
212 ASSERT(scriptingContentIsAllowed(m_parserContentPolicy) || !prpChild.get()-> isElementNode() || !toScriptLoaderIfPossible(toElement(prpChild.get()))); 274 ASSERT(scriptingContentIsAllowed(m_parserContentPolicy) || !prpChild.get()-> isElementNode() || !toScriptLoaderIfPossible(toElement(prpChild.get())));
213 ASSERT(pluginContentIsAllowed(m_parserContentPolicy) || !prpChild->isPluginE lement()); 275 ASSERT(pluginContentIsAllowed(m_parserContentPolicy) || !prpChild->isPluginE lement());
214 276
215 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); 277 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert);
216 task.parent = parent; 278 task.parent = parent;
217 task.child = prpChild; 279 task.child = prpChild;
218 task.selfClosing = selfClosing; 280 task.selfClosing = selfClosing;
219 281
220 if (shouldFosterParent()) { 282 if (shouldFosterParent()) {
221 fosterParent(task.child); 283 fosterParent(task.child);
222 return; 284 return;
223 } 285 }
224 286
225 // Add as a sibling of the parent if we have reached the maximum depth allow ed. 287 // Add as a sibling of the parent if we have reached the maximum depth allow ed.
226 if (m_openElements.stackDepth() > maximumHTMLParserDOMTreeDepth && task.pare nt->parentNode()) 288 if (m_openElements.stackDepth() > maximumHTMLParserDOMTreeDepth && task.pare nt->parentNode())
227 task.parent = task.parent->parentNode(); 289 task.parent = task.parent->parentNode();
228 290
229 ASSERT(task.parent); 291 ASSERT(task.parent);
230 queueTask(task); 292 queueTask(task);
231 } 293 }
232 294
233 void HTMLConstructionSite::executeQueuedTasks() 295 void HTMLConstructionSite::executeQueuedTasks()
234 { 296 {
297 // This has no affect on pendingText, and we may have pendingText
298 // remaining after executing all other queued tasks.
235 const size_t size = m_taskQueue.size(); 299 const size_t size = m_taskQueue.size();
236 if (!size) 300 if (!size)
237 return; 301 return;
238 302
239 // Copy the task queue into a local variable in case executeTask 303 // Copy the task queue into a local variable in case executeTask
240 // re-enters the parser. 304 // re-enters the parser.
241 TaskQueue queue; 305 TaskQueue queue;
242 queue.swap(m_taskQueue); 306 queue.swap(m_taskQueue);
243 307
244 for (size_t i = 0; i < size; ++i) 308 for (size_t i = 0; i < size; ++i)
(...skipping 22 matching lines...) Expand all
267 , m_inQuirksMode(fragment->document().inQuirksMode()) 331 , m_inQuirksMode(fragment->document().inQuirksMode())
268 { 332 {
269 ASSERT(m_document->isHTMLDocument() || m_document->isXHTMLDocument()); 333 ASSERT(m_document->isHTMLDocument() || m_document->isXHTMLDocument());
270 } 334 }
271 335
272 HTMLConstructionSite::~HTMLConstructionSite() 336 HTMLConstructionSite::~HTMLConstructionSite()
273 { 337 {
274 // Depending on why we're being destroyed it might be OK 338 // Depending on why we're being destroyed it might be OK
275 // to forget queued tasks, but currently we don't expect to. 339 // to forget queued tasks, but currently we don't expect to.
276 ASSERT(m_taskQueue.isEmpty()); 340 ASSERT(m_taskQueue.isEmpty());
341 // Currently we assume that text will never be the last token in the
342 // document and that we'll always queue some additional task text to cause i t to flush.
esprehn 2013/10/11 00:02:56 text task? "task text" is weird.
abarth-chromium 2013/10/11 18:54:04 "task text" -> task
343 ASSERT(m_pendingText.isEmpty());
277 } 344 }
278 345
279 void HTMLConstructionSite::detach() 346 void HTMLConstructionSite::detach()
280 { 347 {
348 // FIXME: We'd like to ASSERT here that we're canceling and not just discard ing
349 // text that really should have made it into the DOM earlier, but there
350 // doesn't seem to be a nice way to do that.
351 PendingText discardedText;
352 m_pendingText.swap(discardedText);
esprehn 2013/10/11 00:02:56 Adding a discard() method would be prettier.
abarth-chromium 2013/10/11 18:54:04 Agreed. It can use swap internally if that's help
353
281 m_document = 0; 354 m_document = 0;
282 m_attachmentRoot = 0; 355 m_attachmentRoot = 0;
283 } 356 }
284 357
285 void HTMLConstructionSite::setForm(HTMLFormElement* form) 358 void HTMLConstructionSite::setForm(HTMLFormElement* form)
286 { 359 {
287 // This method should only be needed for HTMLTreeBuilder in the fragment cas e. 360 // This method should only be needed for HTMLTreeBuilder in the fragment cas e.
288 ASSERT(!m_form); 361 ASSERT(!m_form);
289 m_form = form; 362 m_form = form;
290 } 363 }
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after
437 || (!systemId.isEmpty() && publicId.startsWith("-//W3C//DTD HTML 4.01 Fr ameset//", false)) 510 || (!systemId.isEmpty() && publicId.startsWith("-//W3C//DTD HTML 4.01 Fr ameset//", false))
438 || (!systemId.isEmpty() && publicId.startsWith("-//W3C//DTD HTML 4.01 Tr ansitional//", false))) { 511 || (!systemId.isEmpty() && publicId.startsWith("-//W3C//DTD HTML 4.01 Tr ansitional//", false))) {
439 setCompatibilityMode(Document::LimitedQuirksMode); 512 setCompatibilityMode(Document::LimitedQuirksMode);
440 return; 513 return;
441 } 514 }
442 515
443 // Otherwise we are No Quirks Mode. 516 // Otherwise we are No Quirks Mode.
444 setCompatibilityMode(Document::NoQuirksMode); 517 setCompatibilityMode(Document::NoQuirksMode);
445 } 518 }
446 519
520 void HTMLConstructionSite::flush()
521 {
522 flushPendingText();
523 executeQueuedTasks();
524 }
525
447 void HTMLConstructionSite::processEndOfFile() 526 void HTMLConstructionSite::processEndOfFile()
448 { 527 {
449 ASSERT(currentNode()); 528 ASSERT(currentNode());
529 flush();
450 openElements()->popAll(); 530 openElements()->popAll();
451 } 531 }
452 532
453 void HTMLConstructionSite::finishedParsing() 533 void HTMLConstructionSite::finishedParsing()
454 { 534 {
535 // We shouldn't have any queued tasks but we might have pending text which w e need to promote to tasks and execute.
455 ASSERT(m_taskQueue.isEmpty()); 536 ASSERT(m_taskQueue.isEmpty());
537 flush();
456 m_document->finishedParsing(); 538 m_document->finishedParsing();
457 } 539 }
458 540
459 void HTMLConstructionSite::insertDoctype(AtomicHTMLToken* token) 541 void HTMLConstructionSite::insertDoctype(AtomicHTMLToken* token)
460 { 542 {
461 ASSERT(token->type() == HTMLToken::DOCTYPE); 543 ASSERT(token->type() == HTMLToken::DOCTYPE);
462 544
463 const String& publicId = StringImpl::create8BitIfPossible(token->publicIdent ifier()); 545 const String& publicId = StringImpl::create8BitIfPossible(token->publicIdent ifier());
464 const String& systemId = StringImpl::create8BitIfPossible(token->systemIdent ifier()); 546 const String& systemId = StringImpl::create8BitIfPossible(token->systemIdent ifier());
465 RefPtr<DocumentType> doctype = DocumentType::create(m_document, token->name( ), publicId, systemId); 547 RefPtr<DocumentType> doctype = DocumentType::create(m_document, token->name( ), publicId, systemId);
(...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after
579 661
580 RefPtr<Element> element = createElement(token, namespaceURI); 662 RefPtr<Element> element = createElement(token, namespaceURI);
581 if (scriptingContentIsAllowed(m_parserContentPolicy) || !toScriptLoaderIfPos sible(element.get())) 663 if (scriptingContentIsAllowed(m_parserContentPolicy) || !toScriptLoaderIfPos sible(element.get()))
582 attachLater(currentNode(), element, token->selfClosing()); 664 attachLater(currentNode(), element, token->selfClosing());
583 if (!token->selfClosing()) 665 if (!token->selfClosing())
584 m_openElements.push(HTMLStackItem::create(element.release(), token, name spaceURI)); 666 m_openElements.push(HTMLStackItem::create(element.release(), token, name spaceURI));
585 } 667 }
586 668
587 void HTMLConstructionSite::insertTextNode(const String& string, WhitespaceMode w hitespaceMode) 669 void HTMLConstructionSite::insertTextNode(const String& string, WhitespaceMode w hitespaceMode)
588 { 670 {
589 HTMLConstructionSiteTask protoTask(HTMLConstructionSiteTask::Insert); 671 HTMLConstructionSiteTask dummyTask(HTMLConstructionSiteTask::Insert);
590 protoTask.parent = currentNode(); 672 dummyTask.parent = currentNode();
591 673
592 if (shouldFosterParent()) 674 if (shouldFosterParent())
593 findFosterSite(protoTask); 675 findFosterSite(dummyTask);
594 676
595 // FIXME: This probably doesn't need to be done both here and in insert(Task ). 677 // FIXME: This probably doesn't need to be done both here and in insert(Task ).
596 if (protoTask.parent->hasTagName(templateTag)) 678 if (dummyTask.parent->hasTagName(templateTag))
597 protoTask.parent = toHTMLTemplateElement(protoTask.parent.get())->conten t(); 679 dummyTask.parent = toHTMLTemplateElement(dummyTask.parent.get())->conten t();
598 680
599 // Splitting text nodes into smaller chunks contradicts HTML5 spec, but is n ecessary 681 // Unclear when parent != case occurs. Somehow we insert text into two separ ate nodes while processing the same Token.
600 // for performance, see: https://bugs.webkit.org/show_bug.cgi?id=55898 682 // The nextChild != dummy.nextChild case occurs whenever foster parenting ha ppened and we hit a new text node "<table>a</table>b"
601 unsigned lengthLimit = textLengthLimitForContainer(protoTask.parent.get()); 683 // In either case we have to flush the pending text into the task queue befo re making more.
602 unsigned currentPosition = 0; 684 if (!m_pendingText.isEmpty() && (m_pendingText.parent != dummyTask.parent || m_pendingText.nextChild != dummyTask.nextChild))
603 685 flushPendingText();
604 // Merge text nodes into previous ones if possible: 686 m_pendingText.append(dummyTask.parent, dummyTask.nextChild, string, whitespa ceMode);
605 // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construc tion.html#insert-a-character
606 Node* previousChild = protoTask.nextChild ? protoTask.nextChild->previousSib ling() : protoTask.parent->lastChild();
607 if (previousChild && previousChild->isTextNode()) {
608 Text* previousText = toText(previousChild);
609 unsigned appendLengthLimit = lengthLimit - previousText->length();
610
611 unsigned proposedBreakIndex = std::min(currentPosition + appendLengthLim it, string.length());
612 unsigned breakIndex = findBreakIndexBetween(string, currentPosition, pro posedBreakIndex);
613 ASSERT(breakIndex <= string.length());
614 // If we didn't find a breable piece to append, forget it.
615 if (breakIndex) {
616 String substring = string.substring(currentPosition, breakIndex - cu rrentPosition);
617 substring = atomizeIfAllWhitespace(substring, whitespaceMode);
618 previousText->parserAppendData(substring);
619 currentPosition += substring.length();
620 }
621 }
622
623 while (currentPosition < string.length()) {
624 unsigned proposedBreakIndex = std::min(currentPosition + lengthLimit, st ring.length());
625 unsigned breakIndex = findBreakIndexBetween(string, currentPosition, pro posedBreakIndex);
626 // We failed to find a breakable boudary between the minimum and the pro posed, just give up and break at the proposed index.
627 // We could go searching after the proposed index, but current callers a re attempting to break after 65k chars!
628 // 65k of unbreakable characters isn't worth trying to handle "correctly ".
629 if (!breakIndex)
630 breakIndex = proposedBreakIndex;
631 ASSERT(breakIndex <= string.length());
632 String substring = string.substring(currentPosition, breakIndex - curren tPosition);
633 substring = atomizeIfAllWhitespace(substring, whitespaceMode);
634
635 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert);
636 task.parent = protoTask.parent;
637 task.nextChild = protoTask.nextChild;
638 task.child = Text::create(task.parent->document(), substring);
639 queueTask(task);
640
641 ASSERT(breakIndex > currentPosition);
642 ASSERT(breakIndex - currentPosition == substring.length());
643 ASSERT(toText(task.child.get())->length() == substring.length());
644 currentPosition = breakIndex;
645 }
646 } 687 }
647 688
648 void HTMLConstructionSite::reparent(HTMLElementStack::ElementRecord* newParent, HTMLElementStack::ElementRecord* child) 689 void HTMLConstructionSite::reparent(HTMLElementStack::ElementRecord* newParent, HTMLElementStack::ElementRecord* child)
649 { 690 {
650 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Reparent); 691 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Reparent);
651 task.parent = newParent->node(); 692 task.parent = newParent->node();
652 task.child = child->node(); 693 task.child = child->node();
653 queueTask(task); 694 queueTask(task);
654 } 695 }
655 696
(...skipping 162 matching lines...) Expand 10 before | Expand all | Expand 10 after
818 void HTMLConstructionSite::fosterParent(PassRefPtr<Node> node) 859 void HTMLConstructionSite::fosterParent(PassRefPtr<Node> node)
819 { 860 {
820 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); 861 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert);
821 findFosterSite(task); 862 findFosterSite(task);
822 task.child = node; 863 task.child = node;
823 ASSERT(task.parent); 864 ASSERT(task.parent);
824 queueTask(task); 865 queueTask(task);
825 } 866 }
826 867
827 } 868 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698