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

Side by Side Diff: mojom/mojom_parser/utils/wellfounded_graphs.go

Issue 1908273003: Mojom frontend: An algorithm for detecting ill-founded nodes in a graph. (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Created 4 years, 8 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
« no previous file with comments | « mojom/mojom_parser/BUILD.gn ('k') | mojom/mojom_parser/utils/wellfounded_graphs_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package utils
6
7 import (
8 "bytes"
9 "fmt"
10 )
11
12 // This file contains an implementation of an algorithm to check the well-founde dness
13 // of two-sorted directed graphs. See the paper
14 // "Well-Founded Two-Sorted Directed Graphs" (https://goo.gl/ipxFKu) for backgro und
15 // and detailed definitions. Here we give only a high-level overview.
16 //
17 // A two-sorted graph is a directed graph that contains two sorts of nodes calle d circle nodes and
18 // square nodes. A node in a two-sorted graph is *well-founded* iff it satisfies the
19 // following recursive definition:
20 // (i) Leaf nodes are well-founded
21 // (ii) A circle node is well-founded iff all of its children are well-founded
22 // (iii) A non-leaf square node is well-founded iff at least one of its children is well-founded.
23 // (See the paper for a more logically correct definition.)
24 //
25 // See the comments on the main function |CheckWellFounded| below for a descript ion
26 // of the algorithm.
27
28 ///////////////////////////////////////////////////
29 // Node type
30 //
31 // A |Node| is a node in a directed graph.
32 ///////////////////////////////////////////////////
33
34 type Node interface {
35 // Returns the list of children of this node.
36 OutEdges() []Node
37
38 // Returns whether or not this node is a square node.
39 IsSquare() bool
40
41 // SetKnownWellFounded is invoked by the algorithm in order to set the
42 // fact that the algorithm has determined that this node is well-founded .
43 // An implementation of |Node| should cache this--even between different
44 // invocations of the algorithm on different starting nodes--and return
45 // |true| from the method |KnownWellFounded| just in case this method ha s
46 // ever been invoked. In this way any nodes verified to be well-founded during
47 // one run of the algorithm do not need to be checked during a later run of the
48 // algorithm on a different starting node.
49 SetKnownWellFounded()
50
51 // Returns whether or not the function |SetKnownWellFounded| has ever
52 // been invoked on this node, during the lifetime of a program using
53 // this library.
54 KnownWellFounded() bool
55
56 // Returns the name of this node, appropriate for debugging.
57 DebugName() string
58 }
59
60 ///////////////////////////////////////////////////
61 // CheckWellFounded function
62 //
63 // This is the main public function.
64 ///////////////////////////////////////////////////
65
66 // CheckWellFounded checks whether the sub-graph rooted at |root| contains any i ll-founded nodes.
67 // If any ill-founded nodes are detected then a non-nil |CycleDescription| is re turned containing
68 // a cycle of ill-founded nodes. If every node is well-founded then nil is retur ned.
69 //
70 // If the graph contains only circle nodes then well-foundedness is equivalent t o
71 // acyclicality and so the returned cycle is a proof that the nodes contained in it
72 // are ill-founded. But in general (if the graph contains square nodes) the retu rned
73 // |CycleDescription| is only meant to serve as an example of some of the
74 // ill-foundedness contained in the subgraph. The cycle is guaranteed to contain only
75 // ill-founded nodes and the cycle may be considered part of a proof that thes n odes
76 // are in fact ill-founded. But the cycle does not necessarily contain every
77 // ill-founded node and it does not
78 // necessarily constitute a complete proof of the ill-foundedness
79 // because a graph that contains square nodes is allowed to contain some cycles and still be
80 // well-founded. The intended application of the returned CycleDescription is to be used to
81 // describe to a user the location of the ill-foundedness in order to allow the user to modify
82 // the graph in order to make it well-founded.
83 //
84 // This function may be invoked multiple times on different starting nodes durin g the life
85 // of a program. If the function is invoked once on node |x| and no ill-foundedn ess is
86 // found and then the function is invoked later on node |y| and if node |z| is r eachable
87 // from both |x| and |y| then node |z| will be marked |KnownWellFounded| during the
88 // first run of the function and so the graph below |z| will not have to be insp ected
89 // during the second run of the function.
90 //
91 // Our algorithm proceeds in three phases:
92 // (1) Phase 1 consists of a depth-first traversal of the graph whose purpose is two-fold:
93 // (a) To prove directly that as many nodes as possible are well-founded and mark them
94 // so by invoking SetKnownWellFounded().
95 // (b) To prepare for phase 2 by constructing two sets of nodes called the | foundationSet| and
96 // the |pendingSet|
97 // See the comments at |checkWellFoundedPhase1| for more details.
98 //
99 // In many cases phase 1 will be able to prove that every node is well-founded a nd so the algorithm
100 // will terminate without entering phase 2. This is the case for example if the graph has no
101 // cycles at all.
102 //
103 // (2) The purpose of phase 2 is to propogate the |KnownWellFounded| property to the remaining well-founded
104 // nodes (the ones that could not be verified as well-founded during phase 1 .) Phase 2 proceeds in
105 // multiple rounds. During each round the |KnownWellFounded| property is pro pogated from the
106 // |foundationSet| to the |pendingSet|. See the comments at |checkWellFounde dPhase2| for more details.
107 //
108 // If there are no ill-founded nodes then the algorithm terminates after phase 2.
109 //
110 // (3) Phase 3 of the algorithm consists of building a |CycleDescription| in the case that there are ill-founded
111 // nodes. See the method |findKnownCycle|.
112
113 func CheckWellFounded(root Node) *CycleDescription {
114 return checkWellFounded(root, nil)
115 }
116
117 // checkWellFounded is a package-private version of CheckWellFounded intendend t o be invoked by tests.
118 // It offers a second parameter |debugDataRequest|. If this is non-nil then its fields will be filled
119 // in with debugging data about the output of phase 1.
120 func checkWellFounded(root Node, debugDataRequest *debugData) *CycleDescription {
121 if root == nil {
122 return nil
123 }
124 finder := makeCycleFinder()
125 finder.debugDataRequest = debugDataRequest
126 holder := finder.holderForNode(root)
127 return finder.checkWellFounded(holder)
128 }
129
130 ///////////////////////////////////////////////////
131 /// cycleFinder type
132 ///
133 /// A cycleFinder is an object that holds state during the execution of the algo rithm.
134 ///////////////////////////////////////////////////
135 type cycleFinder struct {
136
137 // Maps each node seen by the computation to its holder.
138 nodeToHolder map[Node]*nodeHolder
139
140 foundationSet, pendingSet NodeSet
141
142 visitationIndex int
143 currentPath nodeStack
144
145 debugDataRequest *debugData
146 }
147
148 type debugData struct {
149 initialPendingSet []Node
150 initialFoundationSet []Node
151 }
152
153 func makeCycleFinder() cycleFinder {
154 finder := cycleFinder{}
155 finder.nodeToHolder = make(map[Node]*nodeHolder)
156 finder.foundationSet = MakeNodeSet()
157 finder.pendingSet = MakeNodeSet()
158 finder.currentPath = nodeStack{}
159 return finder
160 }
161
162 // checkWellFounded contains the top-level implementation of the algorithm.
163 func (finder *cycleFinder) checkWellFounded(nodeHolder *nodeHolder) *CycleDescri ption {
164 if nodeHolder.node.KnownWellFounded() {
165 // This node is already known to be well-founded because of an e arlier
166 // execution of the algorithm.
167 return nil
168 }
169
170 // Perform phase 1.
171 finder.checkWellFoundedPhase1(nodeHolder)
172
173 // In tests we pass back some debugging information here.
174 if finder.debugDataRequest != nil {
175 finder.debugDataRequest.initialPendingSet = finder.pendingSet.To NodeSlice()
176 finder.debugDataRequest.initialFoundationSet = finder.foundation Set.ToNodeSlice()
177 }
178
179 // All nodes have been verified as well-founded.
180 if finder.pendingSet.Empty() {
181 return nil
182 }
183
184 // Perform phase 2.
185 finder.checkWellFoundedPhase2(nodeHolder)
186
187 // All nodes have been verified as well-founded.
188 if finder.pendingSet.Empty() {
189 return nil
190 }
191
192 // If we are here then there is at least one ill-founded node.
193
194 // In order to build a canonical cycle description, find the illfounded
195 // node with the least visitation order.
196 var minVisitationOrder int
197 var minIllfoundedNode Node = nil
198 for n, _ := range finder.pendingSet.elements {
199 if minIllfoundedNode == nil || n.visitationOrder < minVisitation Order {
200 minVisitationOrder = n.visitationOrder
201 minIllfoundedNode = n.node
202 }
203 }
204
205 // Starting from the ill-founded node with the least visitation order,
206 // build a canonical cycle.
207 freshCycleFinder := makeCycleFinder()
208 holder := freshCycleFinder.holderForNode(minIllfoundedNode)
209 return freshCycleFinder.findKnownCycle(holder)
210 }
211
212 // checkWellFoundedPhase1 is a recursive helper function that does a depth-first traversal
213 // of the graph rooted at |nodeHolder|. The goal of phase 1 is to mark as many
214 // of the nodes as possible as |KnownWellFounded| and to set up the |pendingSet| ,
215 // the |foundationSet|, and the |parentsToBeNotified| sets so that the remaining
216 // well-founded nodes will be marked as |KnownWellFounded| in phase 2.
217 //
218 // In more detail the following steps are performed for each node x:
219 // (a1) If it can be verified during the traversal that x is well-founded then
220 // x will be marked as |KnownWellFounded|. This occurs if x is a leaf, or x
221 // is a circle and each child node of x is known well-founded before being
222 // visited as a child of x, or x is a square and at-least one child node of x
223 // is known well-founded before being visited as a child of x.
224 // (a2) Otherwise if it cannot be determined during traveral that x is well-foun ded then
225 // (i) x is added to the |pendingSet|.
226 // (ii) x is added to the |parentsToBeNotified| set of all of its children.
227 // (b) In step (a1) if at the time x is found to be well-founded x already has
228 // some parent node x' in its |parentsToBeNotified| set (meaning that step a2 oc curred
229 // earlier for x' and so x' is in the |pendingSet|) then x is added to the |foun dationSet|.
230 // In phase 2, the fact that x is in the foundation set and x' is in the pending set will be
231 // used to propogate known-wellfoundedness to x'.
232 func (finder *cycleFinder) checkWellFoundedPhase1(nodeHolder *nodeHolder) {
233 if nodeHolder.node.KnownWellFounded() {
234 // This node is known to be well-founded before being visited.
235 // This occurs when the node was marked |KnownWellFounded| durin g a
236 // previous run of the algorithm. It follows that all nodes reac hable
237 // from this node have also been so marked. We therefore don't n eed
238 // to traverse the part of the graph below this node during this run
239 // of the algorithm and so we treat this node as a leaf node.
240 nodeHolder.state = vsVisitEnded
241 return
242 }
243
244 // Mark the visit as started.
245 nodeHolder.state = vsVisitStarted
246
247 // Next we examine each of the children and recurse into the unvisited o nes.
248 sawUnverifiedChild := false
249 for _, child := range nodeHolder.node.OutEdges() {
250 childHolder := finder.holderForNode(child)
251 if childHolder.state == vsUnvisited {
252 // Recursively visit this child.
253 finder.checkWellFoundedPhase1(childHolder)
254 }
255
256 // After having visited a child we use the results to update the status of this node.
257 // We could express the logic here more concisely, but the logi c is easier
258 // to understand if we treat circles and squares seperately,
259 if nodeHolder.node.IsSquare() {
260 if nodeHolder.node.KnownWellFounded() {
261 // This square node has already been marked |Kno wnWellFounded| becuase
262 // of an earlier iteration through this loop. Th ere is nothing else to do.
263 continue
264 }
265 if childHolder.node.KnownWellFounded() {
266 // We mark a square node as |KnownWellFounded| a s soon as we can so
267 // that if any of its descendants are also paren ts, the well-foundedness
268 // has a chance to propogate to the descendant i n a recursive call.
269 nodeHolder.node.SetKnownWellFounded()
270 } else {
271 // This square node is not yet known to be well- founded and the child node
272 // is not yet known to be well-founded. Set up a back link from the child.
273 childHolder.parentsToBeNotified.Add(nodeHolder)
274 sawUnverifiedChild = true
275 }
276 continue // Done handling the square case.
277 }
278
279 // Else the node is a circle. If the child is not yet known to b e well-founded
280 // set up a back link from the child to this node.
281 if !childHolder.node.KnownWellFounded() {
282 childHolder.parentsToBeNotified.Add(nodeHolder)
283 sawUnverifiedChild = true
284 }
285 }
286
287 // If a circle node has only well-founded children, or a square node has no children at all,
288 // then the node is well-founded.
289 if !sawUnverifiedChild && !nodeHolder.node.KnownWellFounded() {
290 nodeHolder.node.SetKnownWellFounded()
291 }
292
293 // Possibly add this node to the |foundationSet| or the |pendingSet|.
294 if nodeHolder.node.KnownWellFounded() {
295 if !nodeHolder.parentsToBeNotified.Empty() {
296 finder.foundationSet.Add(nodeHolder)
297 }
298 } else {
299 finder.pendingSet.Add(nodeHolder)
300 }
301
302 // Mark the visit as ended.
303 nodeHolder.state = vsVisitEnded
304 return
305 }
306
307 // checkWellFoundedPhase2 performs phase 2 of the algorithm. The goal is to
308 // propogate known well-foundedness along the back-links that were established
309 // during phase 1. We have two sets of nodes: the |foundationSet| and the
310 // |pendingSet|. The |pendingSet| consists of all nodes that are not currently
311 // known to be well-founded. If the |pendingSet| is not empty when this method
312 // returns, then the nodes in the |pendingSet| are ill-founded. The algorithm
313 // proceeds in rounds. The |foundationSet| consists of the current frontier of
314 // the propogation. That is, the |foundationSet| consists of the nodes discovere d
315 // to be well-founded in the previous round. (It starts with the nodes discovere d
316 // to be well-founded during phase 1, pruned to nodes that have parents that
317 // are in the |pendingSet|.) During each round we propogate known well-foundedne ss
318 // from the nodes in the |foundationSet| to their parents in the |pendingSet|
319 // that can now be verified as known well-foudned.
320 func (finder *cycleFinder) checkWellFoundedPhase2(nodeHolder *nodeHolder) {
321 for !finder.foundationSet.Empty() {
322 nextFoundationSet := MakeNodeSet()
azani 2016/04/22 18:37:14 Alternatively, you could add a Pop() method on Nod
rudominer 2016/04/23 01:22:30 Done.
323 for n, _ := range finder.foundationSet.elements {
324 for p, _ := range n.parentsToBeNotified.elements {
325 if finder.pendingSet.Contains(p) {
326 knownWellFounded := true
327 if !p.node.IsSquare() {
328 for _, child := range p.node.Out Edges() {
329 if child != p.node && !c hild.KnownWellFounded() {
330 knownWellFounded = false
331 break
332 }
333 }
334 }
335 if knownWellFounded {
336 p.node.SetKnownWellFounded()
337 nextFoundationSet.Add(p)
338 finder.pendingSet.Remove(p)
339 }
340 }
341 }
342 }
343 finder.foundationSet = nextFoundationSet
344 }
345 }
346
347 // findKnownCycle finds and returns a |CycleDescription| starting from a node th at is known
348 // to be ill-founded. This proceeds by following edges from an ill-founded node to
349 // an ill-founded child node until a cycle is formed. We return a *canonical* cy cle,
350 // meaning we start from the least possible node and follow edges to the least p ossible
azani 2016/04/22 18:37:14 "least possible node" is ambiguous. Also, I wonde
rudominer 2016/04/23 01:22:30 Done
351 // child node. This is done in order to make testing of the algorithm easier.
352 // We are not concerned with optimizing the performance of phase 3 because in th e intended application
353 // phase 3 can occur at most once in the lifetime of a program: Once an ill-foun ded node is detected the
354 // program exits with a cycle descritpion allowing the user to fix the ill-found edness.
azani 2016/04/22 18:37:14 s/descritpion/description
rudominer 2016/04/23 01:22:30 Done.
355 func (finder *cycleFinder) findKnownCycle(nodeHolder *nodeHolder) *CycleDescript ion {
azani 2016/04/22 18:37:14 Maybe rename findKnownIllFoundedCycle.
rudominer 2016/04/23 01:22:30 Done.
356 // Mark the current node as started
357 nodeHolder.state = vsVisitStarted
358 finder.currentPath.Push(nodeHolder)
359 for _, child := range nodeHolder.node.OutEdges() {
360 childHolder := finder.holderForNode(child)
361 if childHolder.state == vsVisitStarted {
362 // If the child has been started but not finished then w e have found a cycle
363 // from the child to the current node back to the child.
364 return newCycleDescription(finder.currentPath.elements, childHolder, nodeHolder)
365 } else if !childHolder.node.KnownWellFounded() {
366 return finder.findKnownCycle(childHolder)
367 }
368 }
369 panic("Should not get here.")
azani 2016/04/22 18:37:14 Maybe: "Could not find a known ill-founded cycle.
rudominer 2016/04/23 01:22:30 Done.
370 }
371
372 // Returns the nodeHolder for the given node.
373 func (finder *cycleFinder) holderForNode(node Node) *nodeHolder {
374 if holder, found := finder.nodeToHolder[node]; found {
375 return holder
376 }
377
378 // This is the first time we have seen this node. Assign it a new
379 // visitor order.
380 holder := newNodeHolder(node, finder.visitationIndex)
381 finder.visitationIndex++
382 finder.nodeToHolder[node] = holder
383 return holder
384 }
385
386 ////////////////////////////////////////////////////
387 // nodeHolder type
388 ////////////////////////////////////////////////////
389
390 type visitationState int
391
392 const (
393 vsUnvisited visitationState = iota
394 vsVisitStarted
395 vsVisitEnded
396 )
397
398 // A nodeHolder is an internal data structure used by the algorithm.
399 // It holds one node plus data about that node used by the algorithm.
400 type nodeHolder struct {
401 // The node
402 node Node
403
404 parentsToBeNotified NodeSet
405
406 visitationOrder int
407
408 state visitationState
409 }
410
411 func newNodeHolder(node Node, visitationOrder int) *nodeHolder {
412 nodeHolder := new(nodeHolder)
413 nodeHolder.node = node
414 nodeHolder.parentsToBeNotified = MakeNodeSet()
415 nodeHolder.state = vsUnvisited
416 nodeHolder.visitationOrder = visitationOrder
417 return nodeHolder
418 }
419
420 //////////////////////////////////////////////
421 /// nodeStack type
422 //////////////////////////////////////////////
423
424 // A nodeStack is a stack of *nodeHolders
425 type nodeStack struct {
426 elements []*nodeHolder
427 }
428
429 func (stack *nodeStack) Push(n *nodeHolder) {
430 stack.elements = append(stack.elements, n)
431 }
432
433 func (stack *nodeStack) Size() int {
434 return len(stack.elements)
435 }
436
437 func (stack *nodeStack) Pop() (n *nodeHolder) {
438 lastIndex := stack.Size() - 1
439 n = stack.elements[lastIndex]
440 stack.elements = stack.elements[:lastIndex]
441 return
442 }
443
444 func (stack *nodeStack) Peek() (n *nodeHolder) {
445 return stack.elements[stack.Size()-1]
446 }
447
448 func (stack *nodeStack) String() string {
449 var buffer bytes.Buffer
450 fmt.Fprintf(&buffer, "[")
451 first := true
452 for _, e := range stack.elements {
453 if !first {
454 fmt.Fprintf(&buffer, ", ")
455 }
456 fmt.Fprintf(&buffer, "%s", e.node.DebugName())
457 first = false
458 }
459 fmt.Fprintln(&buffer, "]")
460 return buffer.String()
461 }
462
463 ///////////////////////////////////////////////////
464 /// NodeSet type
465 ///////////////////////////////////////////////////
466
467 // A NodeSet is a set of nodeHolders.
468 type NodeSet struct {
469 elements map[*nodeHolder]bool
470 }
471
472 // MakeNodeSet makes a new empty NodeSet.
473 func MakeNodeSet() NodeSet {
474 nodeSet := NodeSet{}
475 nodeSet.elements = make(map[*nodeHolder]bool)
476 return nodeSet
477 }
478
479 // Add adds a Node to a NodeSet.
480 func (set *NodeSet) Add(node *nodeHolder) {
481 set.elements[node] = true
482 }
483
484 // AddAll adds all the nodes from |otherSet| to |set|.
485 func (set *NodeSet) AddAll(otherSet NodeSet) {
486 for e, _ := range otherSet.elements {
487 set.elements[e] = true
488 }
489 }
490
491 // Contains returns whether or not |node| is an element of |set|.
492 func (set *NodeSet) Contains(node *nodeHolder) bool {
493 _, ok := set.elements[node]
494 return ok
495 }
496
497 func (set *NodeSet) Remove(node *nodeHolder) {
498 delete(set.elements, node)
499 }
500
501 func (set *NodeSet) Empty() bool {
502 return len(set.elements) == 0
503 }
504
505 func (set *NodeSet) ToNodeSlice() []Node {
506 slice := make([]Node, 0, len(set.elements))
507 for n, _ := range set.elements {
508 slice = append(slice, n.node)
509 }
510 return slice
511 }
512
513 func (set *NodeSet) Size() int {
514 return len(set.elements)
515 }
516
517 // compareNodeSets is a package-private method used in our tests. It returns
518 // a non-nil error in case expected is not equal to actual.
519 func compareNodeSets(expected, actual *NodeSet) error {
520 for n, _ := range expected.elements {
521 if !actual.Contains(n) {
522 return fmt.Errorf("%s is in expected but not actual", n. node.DebugName())
523 }
524 }
525 for n, _ := range actual.elements {
526 if !expected.Contains(n) {
527 return fmt.Errorf("%s is in actual but not expected", n. node.DebugName())
528 }
529 }
530 return nil
531 }
532
533 // String returns a human readable string representation of |set|.
534 func (set *NodeSet) String() string {
535 var buffer bytes.Buffer
536 fmt.Fprintf(&buffer, "{")
537 first := true
538 for e, _ := range set.elements {
539 if !first {
540 fmt.Fprintf(&buffer, ", ")
541 }
542 fmt.Fprintf(&buffer, "%s", e.node.DebugName())
543 first = false
544 }
545 fmt.Fprintln(&buffer, "}")
546 return buffer.String()
547 }
548
549 ///////////////////////////////////////////////////
550 // CycleDescription type
551 //
552 // A |CycleDescription| describes a cycle in a directed graph.
553 ///////////////////////////////////////////////////
554
555 type CycleDescription struct {
556 first, last Node
557 path []Node
558 }
559
560 func (c *CycleDescription) String() string {
561 var buffer bytes.Buffer
562 fmt.Fprintf(&buffer, "first:%s", c.first.DebugName())
563 fmt.Fprintf(&buffer, ", last:%s", c.last.DebugName())
564 fmt.Fprintf(&buffer, ", path:{")
565 first := true
566 for _, n := range c.path {
567 if !first {
568 fmt.Fprintf(&buffer, ", ")
569 }
570 fmt.Fprintf(&buffer, "%s", n.DebugName())
571 first = false
572 }
573 fmt.Fprintln(&buffer, "}")
574 return buffer.String()
575 }
576
577 func newCycleDescription(path []*nodeHolder, first, last *nodeHolder) *CycleDesc ription {
578 description := CycleDescription{}
579 description.first = first.node
580 description.last = last.node
581 description.path = make([]Node, 0, len(path))
582 for _, n := range path {
583 if len(description.path) > 0 || n.node == first.node {
584 description.path = append(description.path, n.node)
585 }
586 }
587 if description.path[len(description.path)-1] != last.node {
588 panic(fmt.Sprintf("%s != %s", description.path[len(description.p ath)-1], last.node))
589 }
590 return &description
591 }
OLDNEW
« no previous file with comments | « mojom/mojom_parser/BUILD.gn ('k') | mojom/mojom_parser/utils/wellfounded_graphs_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698