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

Side by Side Diff: src/heap.cc

Issue 9320: Semi-weekly merge from bleeding_edge to the toiger branch. (Closed) Base URL: http://v8.googlecode.com/svn/branches/experimental/toiger/
Patch Set: Created 12 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 | Annotate | Revision Log
« no previous file with comments | « src/heap.h ('k') | src/jsregexp.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 2006-2008 the V8 project authors. All rights reserved. 1 // Copyright 2006-2008 the V8 project authors. All rights reserved.
2 // Redistribution and use in source and binary forms, with or without 2 // Redistribution and use in source and binary forms, with or without
3 // modification, are permitted provided that the following conditions are 3 // modification, are permitted provided that the following conditions are
4 // met: 4 // met:
5 // 5 //
6 // * Redistributions of source code must retain the above copyright 6 // * Redistributions of source code must retain the above copyright
7 // notice, this list of conditions and the following disclaimer. 7 // notice, this list of conditions and the following disclaimer.
8 // * Redistributions in binary form must reproduce the above 8 // * Redistributions in binary form must reproduce the above
9 // copyright notice, this list of conditions and the following 9 // copyright notice, this list of conditions and the following
10 // disclaimer in the documentation and/or other materials provided 10 // disclaimer in the documentation and/or other materials provided
(...skipping 759 matching lines...) Expand 10 before | Expand all | Expand 10 after
770 // copied. 770 // copied.
771 if (first_word.IsForwardingAddress()) { 771 if (first_word.IsForwardingAddress()) {
772 *p = first_word.ToForwardingAddress(); 772 *p = first_word.ToForwardingAddress();
773 return; 773 return;
774 } 774 }
775 775
776 // Call the slow part of scavenge object. 776 // Call the slow part of scavenge object.
777 return ScavengeObjectSlow(p, object); 777 return ScavengeObjectSlow(p, object);
778 } 778 }
779 779
780
780 static inline bool IsShortcutCandidate(HeapObject* object, Map* map) { 781 static inline bool IsShortcutCandidate(HeapObject* object, Map* map) {
781 // A ConString object with Heap::empty_string() as the right side 782 // A ConsString object with Heap::empty_string() as the right side
782 // is a candidate for being shortcut by the scavenger. 783 // is a candidate for being shortcut by the scavenger.
783 ASSERT(object->map() == map); 784 ASSERT(object->map() == map);
784 return (map->instance_type() < FIRST_NONSTRING_TYPE) && 785 if (map->instance_type() >= FIRST_NONSTRING_TYPE) return false;
785 (String::cast(object)->map_representation_tag(map) == kConsStringTag) && 786 StringShape shape(map);
786 (ConsString::cast(object)->second() == Heap::empty_string()); 787 return (shape.representation_tag() == kConsStringTag) &&
788 (ConsString::cast(object)->unchecked_second() == Heap::empty_string());
787 } 789 }
788 790
789 791
790 void Heap::ScavengeObjectSlow(HeapObject** p, HeapObject* object) { 792 void Heap::ScavengeObjectSlow(HeapObject** p, HeapObject* object) {
791 ASSERT(InFromSpace(object)); 793 ASSERT(InFromSpace(object));
792 MapWord first_word = object->map_word(); 794 MapWord first_word = object->map_word();
793 ASSERT(!first_word.IsForwardingAddress()); 795 ASSERT(!first_word.IsForwardingAddress());
794 796
795 // Optimization: Bypass flattened ConsString objects. 797 // Optimization: Bypass flattened ConsString objects.
796 if (IsShortcutCandidate(object, first_word.ToMap())) { 798 if (IsShortcutCandidate(object, first_word.ToMap())) {
797 object = HeapObject::cast(ConsString::cast(object)->first()); 799 object = HeapObject::cast(ConsString::cast(object)->unchecked_first());
798 *p = object; 800 *p = object;
799 // After patching *p we have to repeat the checks that object is in the 801 // After patching *p we have to repeat the checks that object is in the
800 // active semispace of the young generation and not already copied. 802 // active semispace of the young generation and not already copied.
801 if (!InNewSpace(object)) return; 803 if (!InNewSpace(object)) return;
802 first_word = object->map_word(); 804 first_word = object->map_word();
803 if (first_word.IsForwardingAddress()) { 805 if (first_word.IsForwardingAddress()) {
804 *p = first_word.ToForwardingAddress(); 806 *p = first_word.ToForwardingAddress();
805 return; 807 return;
806 } 808 }
807 } 809 }
(...skipping 529 matching lines...) Expand 10 before | Expand all | Expand 10 after
1337 share->set_instance_class_name(Object_symbol()); 1339 share->set_instance_class_name(Object_symbol());
1338 share->set_function_data(undefined_value()); 1340 share->set_function_data(undefined_value());
1339 share->set_lazy_load_data(undefined_value()); 1341 share->set_lazy_load_data(undefined_value());
1340 share->set_script(undefined_value()); 1342 share->set_script(undefined_value());
1341 share->set_start_position_and_type(0); 1343 share->set_start_position_and_type(0);
1342 share->set_debug_info(undefined_value()); 1344 share->set_debug_info(undefined_value());
1343 return result; 1345 return result;
1344 } 1346 }
1345 1347
1346 1348
1347 Object* Heap::AllocateConsString(String* first, String* second) { 1349 Object* Heap::AllocateConsString(String* first,
1348 int first_length = first->length(); 1350 StringShape first_shape,
1349 int second_length = second->length(); 1351 String* second,
1352 StringShape second_shape) {
1353 int first_length = first->length(first_shape);
1354 int second_length = second->length(second_shape);
1350 int length = first_length + second_length; 1355 int length = first_length + second_length;
1351 bool is_ascii = first->is_ascii_representation() 1356 bool is_ascii = first_shape.IsAsciiRepresentation()
1352 && second->is_ascii_representation(); 1357 && second_shape.IsAsciiRepresentation();
1353 1358
1354 // If the resulting string is small make a flat string. 1359 // If the resulting string is small make a flat string.
1355 if (length < String::kMinNonFlatLength) { 1360 if (length < String::kMinNonFlatLength) {
1356 ASSERT(first->IsFlat()); 1361 ASSERT(first->IsFlat(first_shape));
1357 ASSERT(second->IsFlat()); 1362 ASSERT(second->IsFlat(second_shape));
1358 if (is_ascii) { 1363 if (is_ascii) {
1359 Object* result = AllocateRawAsciiString(length); 1364 Object* result = AllocateRawAsciiString(length);
1360 if (result->IsFailure()) return result; 1365 if (result->IsFailure()) return result;
1361 // Copy the characters into the new object. 1366 // Copy the characters into the new object.
1362 char* dest = SeqAsciiString::cast(result)->GetChars(); 1367 char* dest = SeqAsciiString::cast(result)->GetChars();
1363 String::WriteToFlat(first, dest, 0, first_length); 1368 String::WriteToFlat(first, first_shape, dest, 0, first_length);
1364 String::WriteToFlat(second, dest + first_length, 0, second_length); 1369 String::WriteToFlat(second,
1370 second_shape,
1371 dest + first_length,
1372 0,
1373 second_length);
1365 return result; 1374 return result;
1366 } else { 1375 } else {
1367 Object* result = AllocateRawTwoByteString(length); 1376 Object* result = AllocateRawTwoByteString(length);
1368 if (result->IsFailure()) return result; 1377 if (result->IsFailure()) return result;
1369 // Copy the characters into the new object. 1378 // Copy the characters into the new object.
1370 uc16* dest = SeqTwoByteString::cast(result)->GetChars(); 1379 uc16* dest = SeqTwoByteString::cast(result)->GetChars();
1371 String::WriteToFlat(first, dest, 0, first_length); 1380 String::WriteToFlat(first, first_shape, dest, 0, first_length);
1372 String::WriteToFlat(second, dest + first_length, 0, second_length); 1381 String::WriteToFlat(second,
1382 second_shape,
1383 dest + first_length,
1384 0,
1385 second_length);
1373 return result; 1386 return result;
1374 } 1387 }
1375 } 1388 }
1376 1389
1377 Map* map; 1390 Map* map;
1378 if (length <= String::kMaxShortStringSize) { 1391 if (length <= String::kMaxShortStringSize) {
1379 map = is_ascii ? short_cons_ascii_string_map() 1392 map = is_ascii ? short_cons_ascii_string_map()
1380 : short_cons_string_map(); 1393 : short_cons_string_map();
1381 } else if (length <= String::kMaxMediumStringSize) { 1394 } else if (length <= String::kMaxMediumStringSize) {
1382 map = is_ascii ? medium_cons_ascii_string_map() 1395 map = is_ascii ? medium_cons_ascii_string_map()
1383 : medium_cons_string_map(); 1396 : medium_cons_string_map();
1384 } else { 1397 } else {
1385 map = is_ascii ? long_cons_ascii_string_map() 1398 map = is_ascii ? long_cons_ascii_string_map()
1386 : long_cons_string_map(); 1399 : long_cons_string_map();
1387 } 1400 }
1388 1401
1389 Object* result = Allocate(map, NEW_SPACE); 1402 Object* result = Allocate(map, NEW_SPACE);
1390 if (result->IsFailure()) return result; 1403 if (result->IsFailure()) return result;
1391 ASSERT(InNewSpace(result)); 1404 ASSERT(InNewSpace(result));
1392 ConsString* cons_string = ConsString::cast(result); 1405 ConsString* cons_string = ConsString::cast(result);
1393 cons_string->set_first(first, SKIP_WRITE_BARRIER); 1406 cons_string->set_first(first, SKIP_WRITE_BARRIER);
1394 cons_string->set_second(second, SKIP_WRITE_BARRIER); 1407 cons_string->set_second(second, SKIP_WRITE_BARRIER);
1395 cons_string->set_length(length); 1408 cons_string->set_length(length);
1396 return result; 1409 return result;
1397 } 1410 }
1398 1411
1399 1412
1400 Object* Heap::AllocateSlicedString(String* buffer, int start, int end) { 1413 Object* Heap::AllocateSlicedString(String* buffer,
1414 StringShape buffer_shape,
1415 int start,
1416 int end) {
1401 int length = end - start; 1417 int length = end - start;
1402 1418
1403 // If the resulting string is small make a sub string. 1419 // If the resulting string is small make a sub string.
1404 if (end - start <= String::kMinNonFlatLength) { 1420 if (end - start <= String::kMinNonFlatLength) {
1405 return Heap::AllocateSubString(buffer, start, end); 1421 return Heap::AllocateSubString(buffer, buffer_shape, start, end);
1406 } 1422 }
1407 1423
1408 Map* map; 1424 Map* map;
1409 if (length <= String::kMaxShortStringSize) { 1425 if (length <= String::kMaxShortStringSize) {
1410 map = buffer->is_ascii_representation() ? short_sliced_ascii_string_map() 1426 map = buffer_shape.IsAsciiRepresentation() ?
1411 : short_sliced_string_map(); 1427 short_sliced_ascii_string_map() :
1428 short_sliced_string_map();
1412 } else if (length <= String::kMaxMediumStringSize) { 1429 } else if (length <= String::kMaxMediumStringSize) {
1413 map = buffer->is_ascii_representation() ? medium_sliced_ascii_string_map() 1430 map = buffer_shape.IsAsciiRepresentation() ?
1414 : medium_sliced_string_map(); 1431 medium_sliced_ascii_string_map() :
1432 medium_sliced_string_map();
1415 } else { 1433 } else {
1416 map = buffer->is_ascii_representation() ? long_sliced_ascii_string_map() 1434 map = buffer_shape.IsAsciiRepresentation() ?
1417 : long_sliced_string_map(); 1435 long_sliced_ascii_string_map() :
1436 long_sliced_string_map();
1418 } 1437 }
1419 1438
1420 Object* result = Allocate(map, NEW_SPACE); 1439 Object* result = Allocate(map, NEW_SPACE);
1421 if (result->IsFailure()) return result; 1440 if (result->IsFailure()) return result;
1422 1441
1423 SlicedString* sliced_string = SlicedString::cast(result); 1442 SlicedString* sliced_string = SlicedString::cast(result);
1424 sliced_string->set_buffer(buffer); 1443 sliced_string->set_buffer(buffer);
1425 sliced_string->set_start(start); 1444 sliced_string->set_start(start);
1426 sliced_string->set_length(length); 1445 sliced_string->set_length(length);
1427 1446
1428 return result; 1447 return result;
1429 } 1448 }
1430 1449
1431 1450
1432 Object* Heap::AllocateSubString(String* buffer, int start, int end) { 1451 Object* Heap::AllocateSubString(String* buffer,
1452 StringShape buffer_shape,
1453 int start,
1454 int end) {
1433 int length = end - start; 1455 int length = end - start;
1434 1456
1435 if (length == 1) { 1457 if (length == 1) {
1436 return Heap::LookupSingleCharacterStringFromCode(buffer->Get(start)); 1458 return Heap::LookupSingleCharacterStringFromCode(
1459 buffer->Get(buffer_shape, start));
1437 } 1460 }
1438 1461
1439 // Make an attempt to flatten the buffer to reduce access time. 1462 // Make an attempt to flatten the buffer to reduce access time.
1440 buffer->TryFlatten(); 1463 if (!buffer->IsFlat(buffer_shape)) {
1464 buffer->TryFlatten(buffer_shape);
1465 buffer_shape = StringShape(buffer);
1466 }
1441 1467
1442 Object* result = buffer->is_ascii_representation() 1468 Object* result = buffer_shape.IsAsciiRepresentation()
1443 ? AllocateRawAsciiString(length) 1469 ? AllocateRawAsciiString(length)
1444 : AllocateRawTwoByteString(length); 1470 : AllocateRawTwoByteString(length);
1445 if (result->IsFailure()) return result; 1471 if (result->IsFailure()) return result;
1446 1472
1447 // Copy the characters into the new object. 1473 // Copy the characters into the new object.
1448 String* string_result = String::cast(result); 1474 String* string_result = String::cast(result);
1475 StringShape result_shape(string_result);
1449 StringHasher hasher(length); 1476 StringHasher hasher(length);
1450 int i = 0; 1477 int i = 0;
1451 for (; i < length && hasher.is_array_index(); i++) { 1478 for (; i < length && hasher.is_array_index(); i++) {
1452 uc32 c = buffer->Get(start + i); 1479 uc32 c = buffer->Get(buffer_shape, start + i);
1453 hasher.AddCharacter(c); 1480 hasher.AddCharacter(c);
1454 string_result->Set(i, c); 1481 string_result->Set(result_shape, i, c);
1455 } 1482 }
1456 for (; i < length; i++) { 1483 for (; i < length; i++) {
1457 uc32 c = buffer->Get(start + i); 1484 uc32 c = buffer->Get(buffer_shape, start + i);
1458 hasher.AddCharacterNoIndex(c); 1485 hasher.AddCharacterNoIndex(c);
1459 string_result->Set(i, c); 1486 string_result->Set(result_shape, i, c);
1460 } 1487 }
1461 string_result->set_length_field(hasher.GetHashField()); 1488 string_result->set_length_field(hasher.GetHashField());
1462 return result; 1489 return result;
1463 } 1490 }
1464 1491
1465 1492
1466 Object* Heap::AllocateExternalStringFromAscii( 1493 Object* Heap::AllocateExternalStringFromAscii(
1467 ExternalAsciiString::Resource* resource) { 1494 ExternalAsciiString::Resource* resource) {
1468 Map* map; 1495 Map* map;
1469 int length = resource->length(); 1496 int length = resource->length();
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
1518 buffer[0] = static_cast<char>(code); 1545 buffer[0] = static_cast<char>(code);
1519 Object* result = LookupSymbol(Vector<const char>(buffer, 1)); 1546 Object* result = LookupSymbol(Vector<const char>(buffer, 1));
1520 1547
1521 if (result->IsFailure()) return result; 1548 if (result->IsFailure()) return result;
1522 Heap::single_character_string_cache()->set(code, result); 1549 Heap::single_character_string_cache()->set(code, result);
1523 return result; 1550 return result;
1524 } 1551 }
1525 1552
1526 Object* result = Heap::AllocateRawTwoByteString(1); 1553 Object* result = Heap::AllocateRawTwoByteString(1);
1527 if (result->IsFailure()) return result; 1554 if (result->IsFailure()) return result;
1528 String::cast(result)->Set(0, code); 1555 String* answer = String::cast(result);
1529 return result; 1556 answer->Set(StringShape(answer), 0, code);
1557 return answer;
1530 } 1558 }
1531 1559
1532 1560
1533 Object* Heap::AllocateByteArray(int length) { 1561 Object* Heap::AllocateByteArray(int length) {
1534 int size = ByteArray::SizeFor(length); 1562 int size = ByteArray::SizeFor(length);
1535 AllocationSpace space = 1563 AllocationSpace space =
1536 size > MaxHeapObjectSize() ? LO_SPACE : NEW_SPACE; 1564 size > MaxHeapObjectSize() ? LO_SPACE : NEW_SPACE;
1537 1565
1538 Object* result = AllocateRaw(size, space, OLD_DATA_SPACE); 1566 Object* result = AllocateRaw(size, space, OLD_DATA_SPACE);
1539 1567
(...skipping 358 matching lines...) Expand 10 before | Expand all | Expand 10 after
1898 // If the string is ascii, we do not need to convert the characters 1926 // If the string is ascii, we do not need to convert the characters
1899 // since UTF8 is backwards compatible with ascii. 1927 // since UTF8 is backwards compatible with ascii.
1900 if (is_ascii) return AllocateStringFromAscii(string, pretenure); 1928 if (is_ascii) return AllocateStringFromAscii(string, pretenure);
1901 1929
1902 Object* result = AllocateRawTwoByteString(chars, pretenure); 1930 Object* result = AllocateRawTwoByteString(chars, pretenure);
1903 if (result->IsFailure()) return result; 1931 if (result->IsFailure()) return result;
1904 1932
1905 // Convert and copy the characters into the new object. 1933 // Convert and copy the characters into the new object.
1906 String* string_result = String::cast(result); 1934 String* string_result = String::cast(result);
1907 decoder->Reset(string.start(), string.length()); 1935 decoder->Reset(string.start(), string.length());
1936 StringShape result_shape(string_result);
1908 for (int i = 0; i < chars; i++) { 1937 for (int i = 0; i < chars; i++) {
1909 uc32 r = decoder->GetNext(); 1938 uc32 r = decoder->GetNext();
1910 string_result->Set(i, r); 1939 string_result->Set(result_shape, i, r);
1911 } 1940 }
1912 return result; 1941 return result;
1913 } 1942 }
1914 1943
1915 1944
1916 Object* Heap::AllocateStringFromTwoByte(Vector<const uc16> string, 1945 Object* Heap::AllocateStringFromTwoByte(Vector<const uc16> string,
1917 PretenureFlag pretenure) { 1946 PretenureFlag pretenure) {
1918 // Check if the string is an ASCII string. 1947 // Check if the string is an ASCII string.
1919 int i = 0; 1948 int i = 0;
1920 while (i < string.length() && string[i] <= String::kMaxAsciiCharCode) i++; 1949 while (i < string.length() && string[i] <= String::kMaxAsciiCharCode) i++;
1921 1950
1922 Object* result; 1951 Object* result;
1923 if (i == string.length()) { // It's an ASCII string. 1952 if (i == string.length()) { // It's an ASCII string.
1924 result = AllocateRawAsciiString(string.length(), pretenure); 1953 result = AllocateRawAsciiString(string.length(), pretenure);
1925 } else { // It's not an ASCII string. 1954 } else { // It's not an ASCII string.
1926 result = AllocateRawTwoByteString(string.length(), pretenure); 1955 result = AllocateRawTwoByteString(string.length(), pretenure);
1927 } 1956 }
1928 if (result->IsFailure()) return result; 1957 if (result->IsFailure()) return result;
1929 1958
1930 // Copy the characters into the new object, which may be either ASCII or 1959 // Copy the characters into the new object, which may be either ASCII or
1931 // UTF-16. 1960 // UTF-16.
1932 String* string_result = String::cast(result); 1961 String* string_result = String::cast(result);
1962 StringShape result_shape(string_result);
1933 for (int i = 0; i < string.length(); i++) { 1963 for (int i = 0; i < string.length(); i++) {
1934 string_result->Set(i, string[i]); 1964 string_result->Set(result_shape, i, string[i]);
1935 } 1965 }
1936 return result; 1966 return result;
1937 } 1967 }
1938 1968
1939 1969
1940 Map* Heap::SymbolMapForString(String* string) { 1970 Map* Heap::SymbolMapForString(String* string) {
1941 // If the string is in new space it cannot be used as a symbol. 1971 // If the string is in new space it cannot be used as a symbol.
1942 if (InNewSpace(string)) return NULL; 1972 if (InNewSpace(string)) return NULL;
1943 1973
1944 // Find the corresponding symbol map for strings. 1974 // Find the corresponding symbol map for strings.
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after
2036 } 2066 }
2037 2067
2038 // Allocate string. 2068 // Allocate string.
2039 AllocationSpace space = 2069 AllocationSpace space =
2040 (size > MaxHeapObjectSize()) ? LO_SPACE : OLD_DATA_SPACE; 2070 (size > MaxHeapObjectSize()) ? LO_SPACE : OLD_DATA_SPACE;
2041 Object* result = AllocateRaw(size, space, OLD_DATA_SPACE); 2071 Object* result = AllocateRaw(size, space, OLD_DATA_SPACE);
2042 if (result->IsFailure()) return result; 2072 if (result->IsFailure()) return result;
2043 2073
2044 reinterpret_cast<HeapObject*>(result)->set_map(map); 2074 reinterpret_cast<HeapObject*>(result)->set_map(map);
2045 // The hash value contains the length of the string. 2075 // The hash value contains the length of the string.
2046 String::cast(result)->set_length_field(length_field); 2076 String* answer = String::cast(result);
2077 StringShape answer_shape(answer);
2078 answer->set_length_field(length_field);
2047 2079
2048 ASSERT_EQ(size, String::cast(result)->Size()); 2080 ASSERT_EQ(size, answer->Size());
2049 2081
2050 // Fill in the characters. 2082 // Fill in the characters.
2051 for (int i = 0; i < chars; i++) { 2083 for (int i = 0; i < chars; i++) {
2052 String::cast(result)->Set(i, buffer->GetNext()); 2084 answer->Set(answer_shape, i, buffer->GetNext());
2053 } 2085 }
2054 return result; 2086 return answer;
2055 } 2087 }
2056 2088
2057 2089
2058 Object* Heap::AllocateRawAsciiString(int length, PretenureFlag pretenure) { 2090 Object* Heap::AllocateRawAsciiString(int length, PretenureFlag pretenure) {
2059 AllocationSpace space = (pretenure == TENURED) ? OLD_DATA_SPACE : NEW_SPACE; 2091 AllocationSpace space = (pretenure == TENURED) ? OLD_DATA_SPACE : NEW_SPACE;
2060 int size = SeqAsciiString::SizeFor(length); 2092 int size = SeqAsciiString::SizeFor(length);
2061 if (size > MaxHeapObjectSize()) { 2093 if (size > MaxHeapObjectSize()) {
2062 space = LO_SPACE; 2094 space = LO_SPACE;
2063 } 2095 }
2064 2096
(...skipping 1155 matching lines...) Expand 10 before | Expand all | Expand 10 after
3220 #ifdef DEBUG 3252 #ifdef DEBUG
3221 bool Heap::GarbageCollectionGreedyCheck() { 3253 bool Heap::GarbageCollectionGreedyCheck() {
3222 ASSERT(FLAG_gc_greedy); 3254 ASSERT(FLAG_gc_greedy);
3223 if (Bootstrapper::IsActive()) return true; 3255 if (Bootstrapper::IsActive()) return true;
3224 if (disallow_allocation_failure()) return true; 3256 if (disallow_allocation_failure()) return true;
3225 return CollectGarbage(0, NEW_SPACE); 3257 return CollectGarbage(0, NEW_SPACE);
3226 } 3258 }
3227 #endif 3259 #endif
3228 3260
3229 } } // namespace v8::internal 3261 } } // namespace v8::internal
OLDNEW
« no previous file with comments | « src/heap.h ('k') | src/jsregexp.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698