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

Side by Side Diff: src/deoptimizer.cc

Issue 6677164: Always iterate outgoing arguments as a part of caller frame. (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Created 9 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 | Annotate | Revision Log
« no previous file with comments | « src/deoptimizer.h ('k') | src/frames.h » ('j') | src/frames.h » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2010 the V8 project authors. All rights reserved. 1 // Copyright 2010 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 200 matching lines...) Expand 10 before | Expand all | Expand 10 after
211 Address from, 211 Address from,
212 int fp_to_sp_delta) 212 int fp_to_sp_delta)
213 : isolate_(isolate), 213 : isolate_(isolate),
214 function_(function), 214 function_(function),
215 bailout_id_(bailout_id), 215 bailout_id_(bailout_id),
216 bailout_type_(type), 216 bailout_type_(type),
217 from_(from), 217 from_(from),
218 fp_to_sp_delta_(fp_to_sp_delta), 218 fp_to_sp_delta_(fp_to_sp_delta),
219 output_count_(0), 219 output_count_(0),
220 output_(NULL), 220 output_(NULL),
221 integer32_values_(NULL), 221 deferred_heap_numbers_(0) {
222 double_values_(NULL) {
223 if (FLAG_trace_deopt && type != OSR) { 222 if (FLAG_trace_deopt && type != OSR) {
224 PrintF("**** DEOPT: "); 223 PrintF("**** DEOPT: ");
225 function->PrintName(); 224 function->PrintName();
226 PrintF(" at bailout #%u, address 0x%" V8PRIxPTR ", frame size %d\n", 225 PrintF(" at bailout #%u, address 0x%" V8PRIxPTR ", frame size %d\n",
227 bailout_id, 226 bailout_id,
228 reinterpret_cast<intptr_t>(from), 227 reinterpret_cast<intptr_t>(from),
229 fp_to_sp_delta - (2 * kPointerSize)); 228 fp_to_sp_delta - (2 * kPointerSize));
230 } else if (FLAG_trace_osr && type == OSR) { 229 } else if (FLAG_trace_osr && type == OSR) {
231 PrintF("**** OSR: "); 230 PrintF("**** OSR: ");
232 function->PrintName(); 231 function->PrintName();
(...skipping 18 matching lines...) Expand all
251 ASSERT(!optimized_code_->contains(from)); 250 ASSERT(!optimized_code_->contains(from));
252 } 251 }
253 ASSERT(HEAP->allow_allocation(false)); 252 ASSERT(HEAP->allow_allocation(false));
254 unsigned size = ComputeInputFrameSize(); 253 unsigned size = ComputeInputFrameSize();
255 input_ = new(size) FrameDescription(size, function); 254 input_ = new(size) FrameDescription(size, function);
256 } 255 }
257 256
258 257
259 Deoptimizer::~Deoptimizer() { 258 Deoptimizer::~Deoptimizer() {
260 ASSERT(input_ == NULL && output_ == NULL); 259 ASSERT(input_ == NULL && output_ == NULL);
261 delete[] integer32_values_;
262 delete[] double_values_;
263 } 260 }
264 261
265 262
266 void Deoptimizer::DeleteFrameDescriptions() { 263 void Deoptimizer::DeleteFrameDescriptions() {
267 delete input_; 264 delete input_;
268 for (int i = 0; i < output_count_; ++i) { 265 for (int i = 0; i < output_count_; ++i) {
269 if (output_[i] != input_) delete output_[i]; 266 if (output_[i] != input_) delete output_[i];
270 } 267 }
271 delete[] output_; 268 delete[] output_;
272 input_ = NULL; 269 input_ = NULL;
(...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after
383 TranslationIterator iterator(translations, translation_index); 380 TranslationIterator iterator(translations, translation_index);
384 Translation::Opcode opcode = 381 Translation::Opcode opcode =
385 static_cast<Translation::Opcode>(iterator.Next()); 382 static_cast<Translation::Opcode>(iterator.Next());
386 ASSERT(Translation::BEGIN == opcode); 383 ASSERT(Translation::BEGIN == opcode);
387 USE(opcode); 384 USE(opcode);
388 // Read the number of output frames and allocate an array for their 385 // Read the number of output frames and allocate an array for their
389 // descriptions. 386 // descriptions.
390 int count = iterator.Next(); 387 int count = iterator.Next();
391 ASSERT(output_ == NULL); 388 ASSERT(output_ == NULL);
392 output_ = new FrameDescription*[count]; 389 output_ = new FrameDescription*[count];
393 // Per-frame lists of untagged and unboxed int32 and double values.
394 integer32_values_ = new List<ValueDescriptionInteger32>[count];
395 double_values_ = new List<ValueDescriptionDouble>[count];
396 for (int i = 0; i < count; ++i) { 390 for (int i = 0; i < count; ++i) {
397 output_[i] = NULL; 391 output_[i] = NULL;
398 integer32_values_[i].Initialize(0);
399 double_values_[i].Initialize(0);
400 } 392 }
401 output_count_ = count; 393 output_count_ = count;
402 394
403 // Translate each output frame. 395 // Translate each output frame.
404 for (int i = 0; i < count; ++i) { 396 for (int i = 0; i < count; ++i) {
405 DoComputeFrame(&iterator, i); 397 DoComputeFrame(&iterator, i);
406 } 398 }
407 399
408 // Print some helpful diagnostic information. 400 // Print some helpful diagnostic information.
409 if (FLAG_trace_deopt) { 401 if (FLAG_trace_deopt) {
410 double ms = static_cast<double>(OS::Ticks() - start) / 1000; 402 double ms = static_cast<double>(OS::Ticks() - start) / 1000;
411 int index = output_count_ - 1; // Index of the topmost frame. 403 int index = output_count_ - 1; // Index of the topmost frame.
412 JSFunction* function = output_[index]->GetFunction(); 404 JSFunction* function = output_[index]->GetFunction();
413 PrintF("[deoptimizing: end 0x%08" V8PRIxPTR " ", 405 PrintF("[deoptimizing: end 0x%08" V8PRIxPTR " ",
414 reinterpret_cast<intptr_t>(function)); 406 reinterpret_cast<intptr_t>(function));
415 function->PrintName(); 407 function->PrintName();
416 PrintF(" => node=%u, pc=0x%08" V8PRIxPTR ", state=%s, took %0.3f ms]\n", 408 PrintF(" => node=%u, pc=0x%08" V8PRIxPTR ", state=%s, took %0.3f ms]\n",
417 node_id, 409 node_id,
418 output_[index]->GetPc(), 410 output_[index]->GetPc(),
419 FullCodeGenerator::State2String( 411 FullCodeGenerator::State2String(
420 static_cast<FullCodeGenerator::State>( 412 static_cast<FullCodeGenerator::State>(
421 output_[index]->GetState()->value())), 413 output_[index]->GetState()->value())),
422 ms); 414 ms);
423 } 415 }
424 } 416 }
425 417
426 418
427 void Deoptimizer::InsertHeapNumberValues(int index, JavaScriptFrame* frame) { 419 void Deoptimizer::MaterializeHeapNumbers() {
428 // We need to adjust the stack index by one for the top-most frame. 420 for (int i = 0; i < deferred_heap_numbers_.length(); i++) {
429 int extra_slot_count = (index == output_count() - 1) ? 1 : 0; 421 HeapNumberMaterializationDescriptor d = deferred_heap_numbers_[i];
430 List<ValueDescriptionInteger32>* ints = &integer32_values_[index]; 422 Handle<Object> num = isolate_->factory()->NewNumber(d.value());
431 for (int i = 0; i < ints->length(); i++) { 423 if (FLAG_trace_deopt) {
432 ValueDescriptionInteger32 value = ints->at(i); 424 PrintF("Materializing a new heap number %p [%e] in slot %p\n",
433 double val = static_cast<double>(value.int32_value()); 425 reinterpret_cast<void*>(*num),
434 InsertHeapNumberValue(frame, value.stack_index(), val, extra_slot_count); 426 d.value(),
427 d.slot_address());
428 }
429
430 Memory::Object_at(d.slot_address()) = *num;
435 } 431 }
436
437 // Iterate over double values and convert them to a heap number.
438 List<ValueDescriptionDouble>* doubles = &double_values_[index];
439 for (int i = 0; i < doubles->length(); ++i) {
440 ValueDescriptionDouble value = doubles->at(i);
441 InsertHeapNumberValue(frame, value.stack_index(), value.double_value(),
442 extra_slot_count);
443 }
444 }
445
446
447 void Deoptimizer::InsertHeapNumberValue(JavaScriptFrame* frame,
448 int stack_index,
449 double val,
450 int extra_slot_count) {
451 // Add one to the TOS index to take the 'state' pushed before jumping
452 // to the stub that calls Runtime::NotifyDeoptimized into account.
453 int tos_index = stack_index + extra_slot_count;
454 int index = (frame->ComputeExpressionsCount() - 1) - tos_index;
455 if (FLAG_trace_deopt) PrintF("Allocating a new heap number: %e\n", val);
456 Handle<Object> num = isolate_->factory()->NewNumber(val);
457 frame->SetExpression(index, *num);
458 } 432 }
459 433
460 434
461 void Deoptimizer::DoTranslateCommand(TranslationIterator* iterator, 435 void Deoptimizer::DoTranslateCommand(TranslationIterator* iterator,
462 int frame_index, 436 int frame_index,
463 unsigned output_offset) { 437 unsigned output_offset) {
464 disasm::NameConverter converter; 438 disasm::NameConverter converter;
465 // A GC-safe temporary placeholder that we can put in the output frame. 439 // A GC-safe temporary placeholder that we can put in the output frame.
466 const intptr_t kPlaceholder = reinterpret_cast<intptr_t>(Smi::FromInt(0)); 440 const intptr_t kPlaceholder = reinterpret_cast<intptr_t>(Smi::FromInt(0));
467 441
(...skipping 25 matching lines...) Expand all
493 converter.NameOfCPURegister(input_reg)); 467 converter.NameOfCPURegister(input_reg));
494 } 468 }
495 output_[frame_index]->SetFrameSlot(output_offset, input_value); 469 output_[frame_index]->SetFrameSlot(output_offset, input_value);
496 return; 470 return;
497 } 471 }
498 472
499 case Translation::INT32_REGISTER: { 473 case Translation::INT32_REGISTER: {
500 int input_reg = iterator->Next(); 474 int input_reg = iterator->Next();
501 intptr_t value = input_->GetRegister(input_reg); 475 intptr_t value = input_->GetRegister(input_reg);
502 bool is_smi = Smi::IsValid(value); 476 bool is_smi = Smi::IsValid(value);
503 unsigned output_index = output_offset / kPointerSize;
504 if (FLAG_trace_deopt) { 477 if (FLAG_trace_deopt) {
505 PrintF( 478 PrintF(
506 " 0x%08" V8PRIxPTR ": [top + %d] <- %" V8PRIdPTR " ; %s (%s)\n", 479 " 0x%08" V8PRIxPTR ": [top + %d] <- %" V8PRIdPTR " ; %s (%s)\n",
507 output_[frame_index]->GetTop() + output_offset, 480 output_[frame_index]->GetTop() + output_offset,
508 output_offset, 481 output_offset,
509 value, 482 value,
510 converter.NameOfCPURegister(input_reg), 483 converter.NameOfCPURegister(input_reg),
511 is_smi ? "smi" : "heap number"); 484 is_smi ? "smi" : "heap number");
512 } 485 }
513 if (is_smi) { 486 if (is_smi) {
514 intptr_t tagged_value = 487 intptr_t tagged_value =
515 reinterpret_cast<intptr_t>(Smi::FromInt(static_cast<int>(value))); 488 reinterpret_cast<intptr_t>(Smi::FromInt(static_cast<int>(value)));
516 output_[frame_index]->SetFrameSlot(output_offset, tagged_value); 489 output_[frame_index]->SetFrameSlot(output_offset, tagged_value);
517 } else { 490 } else {
518 // We save the untagged value on the side and store a GC-safe 491 // We save the untagged value on the side and store a GC-safe
519 // temporary placeholder in the frame. 492 // temporary placeholder in the frame.
520 AddInteger32Value(frame_index, 493 AddDoubleValue(output_[frame_index]->GetTop() + output_offset,
521 output_index, 494 static_cast<double>(static_cast<int32_t>(value)));
522 static_cast<int32_t>(value));
523 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder); 495 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder);
524 } 496 }
525 return; 497 return;
526 } 498 }
527 499
528 case Translation::DOUBLE_REGISTER: { 500 case Translation::DOUBLE_REGISTER: {
529 int input_reg = iterator->Next(); 501 int input_reg = iterator->Next();
530 double value = input_->GetDoubleRegister(input_reg); 502 double value = input_->GetDoubleRegister(input_reg);
531 unsigned output_index = output_offset / kPointerSize;
532 if (FLAG_trace_deopt) { 503 if (FLAG_trace_deopt) {
533 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- %e ; %s\n", 504 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- %e ; %s\n",
534 output_[frame_index]->GetTop() + output_offset, 505 output_[frame_index]->GetTop() + output_offset,
535 output_offset, 506 output_offset,
536 value, 507 value,
537 DoubleRegister::AllocationIndexToString(input_reg)); 508 DoubleRegister::AllocationIndexToString(input_reg));
538 } 509 }
539 // We save the untagged value on the side and store a GC-safe 510 // We save the untagged value on the side and store a GC-safe
540 // temporary placeholder in the frame. 511 // temporary placeholder in the frame.
541 AddDoubleValue(frame_index, output_index, value); 512 AddDoubleValue(output_[frame_index]->GetTop() + output_offset, value);
542 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder); 513 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder);
543 return; 514 return;
544 } 515 }
545 516
546 case Translation::STACK_SLOT: { 517 case Translation::STACK_SLOT: {
547 int input_slot_index = iterator->Next(); 518 int input_slot_index = iterator->Next();
548 unsigned input_offset = 519 unsigned input_offset =
549 input_->GetOffsetFromSlotIndex(this, input_slot_index); 520 input_->GetOffsetFromSlotIndex(this, input_slot_index);
550 intptr_t input_value = input_->GetFrameSlot(input_offset); 521 intptr_t input_value = input_->GetFrameSlot(input_offset);
551 if (FLAG_trace_deopt) { 522 if (FLAG_trace_deopt) {
552 PrintF(" 0x%08" V8PRIxPTR ": ", 523 PrintF(" 0x%08" V8PRIxPTR ": ",
553 output_[frame_index]->GetTop() + output_offset); 524 output_[frame_index]->GetTop() + output_offset);
554 PrintF("[top + %d] <- 0x%08" V8PRIxPTR " ; [esp + %d]\n", 525 PrintF("[top + %d] <- 0x%08" V8PRIxPTR " ; [esp + %d]\n",
555 output_offset, 526 output_offset,
556 input_value, 527 input_value,
557 input_offset); 528 input_offset);
558 } 529 }
559 output_[frame_index]->SetFrameSlot(output_offset, input_value); 530 output_[frame_index]->SetFrameSlot(output_offset, input_value);
560 return; 531 return;
561 } 532 }
562 533
563 case Translation::INT32_STACK_SLOT: { 534 case Translation::INT32_STACK_SLOT: {
564 int input_slot_index = iterator->Next(); 535 int input_slot_index = iterator->Next();
565 unsigned input_offset = 536 unsigned input_offset =
566 input_->GetOffsetFromSlotIndex(this, input_slot_index); 537 input_->GetOffsetFromSlotIndex(this, input_slot_index);
567 intptr_t value = input_->GetFrameSlot(input_offset); 538 intptr_t value = input_->GetFrameSlot(input_offset);
568 bool is_smi = Smi::IsValid(value); 539 bool is_smi = Smi::IsValid(value);
569 unsigned output_index = output_offset / kPointerSize;
570 if (FLAG_trace_deopt) { 540 if (FLAG_trace_deopt) {
571 PrintF(" 0x%08" V8PRIxPTR ": ", 541 PrintF(" 0x%08" V8PRIxPTR ": ",
572 output_[frame_index]->GetTop() + output_offset); 542 output_[frame_index]->GetTop() + output_offset);
573 PrintF("[top + %d] <- %" V8PRIdPTR " ; [esp + %d] (%s)\n", 543 PrintF("[top + %d] <- %" V8PRIdPTR " ; [esp + %d] (%s)\n",
574 output_offset, 544 output_offset,
575 value, 545 value,
576 input_offset, 546 input_offset,
577 is_smi ? "smi" : "heap number"); 547 is_smi ? "smi" : "heap number");
578 } 548 }
579 if (is_smi) { 549 if (is_smi) {
580 intptr_t tagged_value = 550 intptr_t tagged_value =
581 reinterpret_cast<intptr_t>(Smi::FromInt(static_cast<int>(value))); 551 reinterpret_cast<intptr_t>(Smi::FromInt(static_cast<int>(value)));
582 output_[frame_index]->SetFrameSlot(output_offset, tagged_value); 552 output_[frame_index]->SetFrameSlot(output_offset, tagged_value);
583 } else { 553 } else {
584 // We save the untagged value on the side and store a GC-safe 554 // We save the untagged value on the side and store a GC-safe
585 // temporary placeholder in the frame. 555 // temporary placeholder in the frame.
586 AddInteger32Value(frame_index, 556 AddDoubleValue(output_[frame_index]->GetTop() + output_offset,
587 output_index, 557 static_cast<double>(static_cast<int32_t>(value)));
588 static_cast<int32_t>(value));
589 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder); 558 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder);
590 } 559 }
591 return; 560 return;
592 } 561 }
593 562
594 case Translation::DOUBLE_STACK_SLOT: { 563 case Translation::DOUBLE_STACK_SLOT: {
595 int input_slot_index = iterator->Next(); 564 int input_slot_index = iterator->Next();
596 unsigned input_offset = 565 unsigned input_offset =
597 input_->GetOffsetFromSlotIndex(this, input_slot_index); 566 input_->GetOffsetFromSlotIndex(this, input_slot_index);
598 double value = input_->GetDoubleFrameSlot(input_offset); 567 double value = input_->GetDoubleFrameSlot(input_offset);
599 unsigned output_index = output_offset / kPointerSize;
600 if (FLAG_trace_deopt) { 568 if (FLAG_trace_deopt) {
601 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- %e ; [esp + %d]\n", 569 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- %e ; [esp + %d]\n",
602 output_[frame_index]->GetTop() + output_offset, 570 output_[frame_index]->GetTop() + output_offset,
603 output_offset, 571 output_offset,
604 value, 572 value,
605 input_offset); 573 input_offset);
606 } 574 }
607 // We save the untagged value on the side and store a GC-safe 575 // We save the untagged value on the side and store a GC-safe
608 // temporary placeholder in the frame. 576 // temporary placeholder in the frame.
609 AddDoubleValue(frame_index, output_index, value); 577 AddDoubleValue(output_[frame_index]->GetTop() + output_offset, value);
610 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder); 578 output_[frame_index]->SetFrameSlot(output_offset, kPlaceholder);
611 return; 579 return;
612 } 580 }
613 581
614 case Translation::LITERAL: { 582 case Translation::LITERAL: {
615 Object* literal = ComputeLiteral(iterator->Next()); 583 Object* literal = ComputeLiteral(iterator->Next());
616 if (FLAG_trace_deopt) { 584 if (FLAG_trace_deopt) {
617 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- ", 585 PrintF(" 0x%08" V8PRIxPTR ": [top + %d] <- ",
618 output_[frame_index]->GetTop() + output_offset, 586 output_[frame_index]->GetTop() + output_offset,
619 output_offset); 587 output_offset);
(...skipping 283 matching lines...) Expand 10 before | Expand all | Expand 10 after
903 871
904 872
905 Object* Deoptimizer::ComputeLiteral(int index) const { 873 Object* Deoptimizer::ComputeLiteral(int index) const {
906 DeoptimizationInputData* data = DeoptimizationInputData::cast( 874 DeoptimizationInputData* data = DeoptimizationInputData::cast(
907 optimized_code_->deoptimization_data()); 875 optimized_code_->deoptimization_data());
908 FixedArray* literals = data->LiteralArray(); 876 FixedArray* literals = data->LiteralArray();
909 return literals->get(index); 877 return literals->get(index);
910 } 878 }
911 879
912 880
913 void Deoptimizer::AddInteger32Value(int frame_index, 881 void Deoptimizer::AddDoubleValue(intptr_t slot_address,
914 int slot_index, 882 double value) {
915 int32_t value) { 883 HeapNumberMaterializationDescriptor value_desc(
916 ValueDescriptionInteger32 value_desc(slot_index, value); 884 reinterpret_cast<Address>(slot_address), value);
917 integer32_values_[frame_index].Add(value_desc); 885 deferred_heap_numbers_.Add(value_desc);
918 } 886 }
919 887
920 888
921 void Deoptimizer::AddDoubleValue(int frame_index,
922 int slot_index,
923 double value) {
924 ValueDescriptionDouble value_desc(slot_index, value);
925 double_values_[frame_index].Add(value_desc);
926 }
927
928
929 LargeObjectChunk* Deoptimizer::CreateCode(BailoutType type) { 889 LargeObjectChunk* Deoptimizer::CreateCode(BailoutType type) {
930 // We cannot run this if the serializer is enabled because this will 890 // We cannot run this if the serializer is enabled because this will
931 // cause us to emit relocation information for the external 891 // cause us to emit relocation information for the external
932 // references. This is fine because the deoptimizer's code section 892 // references. This is fine because the deoptimizer's code section
933 // isn't meant to be serialized at all. 893 // isn't meant to be serialized at all.
934 ASSERT(!Serializer::enabled()); 894 ASSERT(!Serializer::enabled());
935 895
936 MacroAssembler masm(Isolate::Current(), NULL, 16 * KB); 896 MacroAssembler masm(Isolate::Current(), NULL, 16 * KB);
937 masm.set_emit_debug_code(false); 897 masm.set_emit_debug_code(false);
938 GenerateDeoptimizationEntries(&masm, kNumberOfEntries, type); 898 GenerateDeoptimizationEntries(&masm, kNumberOfEntries, type);
(...skipping 348 matching lines...) Expand 10 before | Expand all | Expand 10 after
1287 } 1247 }
1288 frames_to_skip--; 1248 frames_to_skip--;
1289 } 1249 }
1290 } 1250 }
1291 1251
1292 UNREACHABLE(); 1252 UNREACHABLE();
1293 } 1253 }
1294 1254
1295 1255
1296 } } // namespace v8::internal 1256 } } // namespace v8::internal
OLDNEW
« no previous file with comments | « src/deoptimizer.h ('k') | src/frames.h » ('j') | src/frames.h » ('J')

Powered by Google App Engine
This is Rietveld 408576698