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

Side by Side Diff: third_party/WebKit/Source/bindings/core/v8/ScriptStreamerImpl.cpp

Issue 2830533002: Split ScriptStreamer and ScriptStreamerImpl, preparing for unit testing (Closed)
Patch Set: Rebase Created 3 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
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "bindings/core/v8/ScriptStreamer.h" 5 #include "bindings/core/v8/ScriptStreamerImpl.h"
6 6
7 #include <memory> 7 #include <memory>
8 #include "bindings/core/v8/ScriptStreamerThread.h" 8 #include "bindings/core/v8/ScriptStreamerThread.h"
9 #include "bindings/core/v8/V8ScriptRunner.h" 9 #include "bindings/core/v8/V8ScriptRunner.h"
10 #include "core/dom/Document.h" 10 #include "core/dom/Document.h"
11 #include "core/dom/Element.h" 11 #include "core/dom/Element.h"
12 #include "core/dom/PendingScript.h" 12 #include "core/dom/PendingScript.h"
13 #include "core/frame/Settings.h" 13 #include "core/frame/Settings.h"
14 #include "core/html/parser/TextResourceDecoder.h" 14 #include "core/html/parser/TextResourceDecoder.h"
15 #include "core/loader/resource/ScriptResource.h" 15 #include "core/loader/resource/ScriptResource.h"
(...skipping 162 matching lines...) Expand 10 before | Expand all | Expand 10 after
178 178
179 public: 179 public:
180 explicit SourceStream(RefPtr<WebTaskRunner> loading_task_runner) 180 explicit SourceStream(RefPtr<WebTaskRunner> loading_task_runner)
181 : v8::ScriptCompiler::ExternalSourceStream(), 181 : v8::ScriptCompiler::ExternalSourceStream(),
182 cancelled_(false), 182 cancelled_(false),
183 finished_(false), 183 finished_(false),
184 queue_lead_position_(0), 184 queue_lead_position_(0),
185 queue_tail_position_(0), 185 queue_tail_position_(0),
186 loading_task_runner_(std::move(loading_task_runner)) {} 186 loading_task_runner_(std::move(loading_task_runner)) {}
187 187
188 virtual ~SourceStream() override {} 188 ~SourceStream() override {}
189 189
190 // Called by V8 on a background thread. Should block until we can return 190 // Called by V8 on a background thread. Should block until we can return
191 // some data. 191 // some data.
192 size_t GetMoreData(const uint8_t** src) override { 192 size_t GetMoreData(const uint8_t** src) override {
193 DCHECK(!IsMainThread()); 193 DCHECK(!IsMainThread());
194 { 194 {
195 MutexLocker locker(mutex_); 195 MutexLocker locker(mutex_);
196 if (cancelled_) 196 if (cancelled_)
197 return 0; 197 return 0;
198 } 198 }
199 size_t length = 0; 199 size_t length = 0;
200 // This will wait until there is data. 200 // This will wait until there is data.
201 data_queue_.Consume(src, &length); 201 data_queue_.Consume(src, &length);
202 { 202 {
203 MutexLocker locker(mutex_); 203 MutexLocker locker(mutex_);
204 if (cancelled_) 204 if (cancelled_)
205 return 0; 205 return 0;
206 } 206 }
207 queue_lead_position_ += length; 207 queue_lead_position_ += length;
208 return length; 208 return length;
209 } 209 }
210 210
211 void DidFinishLoading() { 211 void DidFinishLoading() {
212 DCHECK(IsMainThread()); 212 DCHECK(IsMainThread());
213 finished_ = true; 213 finished_ = true;
214 data_queue_.Finish(); 214 data_queue_.Finish();
215 } 215 }
216 216
217 void DidReceiveData(ScriptStreamer* streamer) { 217 void DidReceiveData(ScriptStreamerImpl* streamer) {
218 DCHECK(IsMainThread()); 218 DCHECK(IsMainThread());
219 PrepareDataOnMainThread(streamer); 219 PrepareDataOnMainThread(streamer);
220 } 220 }
221 221
222 void Cancel() { 222 void Cancel() {
223 DCHECK(IsMainThread()); 223 DCHECK(IsMainThread());
224 // The script is no longer needed by the upper layers. Stop streaming 224 // The script is no longer needed by the upper layers. Stop streaming
225 // it. The next time GetMoreData is called (or woken up), it will return 225 // it. The next time GetMoreData is called (or woken up), it will return
226 // 0, which will be interpreted as EOS by V8 and the parsing will 226 // 0, which will be interpreted as EOS by V8 and the parsing will
227 // fail. ScriptStreamer::streamingComplete will be called, and at that 227 // fail. ScriptStreamer::streamingComplete will be called, and at that
228 // point we will release the references to SourceStream. 228 // point we will release the references to SourceStream.
229 { 229 {
230 MutexLocker locker(mutex_); 230 MutexLocker locker(mutex_);
231 cancelled_ = true; 231 cancelled_ = true;
232 } 232 }
233 data_queue_.Finish(); 233 data_queue_.Finish();
234 } 234 }
235 235
236 private: 236 private:
237 void PrepareDataOnMainThread(ScriptStreamer* streamer) { 237 void PrepareDataOnMainThread(ScriptStreamerImpl* streamer) {
238 DCHECK(IsMainThread()); 238 DCHECK(IsMainThread());
239 239
240 if (cancelled_) { 240 if (cancelled_) {
241 data_queue_.Finish(); 241 data_queue_.Finish();
242 return; 242 return;
243 } 243 }
244 244
245 // The Resource must still be alive; otherwise we should've cancelled 245 // The Resource must still be alive; otherwise we should've cancelled
246 // the streaming (if we have cancelled, the background thread is not 246 // the streaming (if we have cancelled, the background thread is not
247 // waiting). 247 // waiting).
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after
318 // queueLeadPosition: data we have handed off to the V8 thread. 318 // queueLeadPosition: data we have handed off to the V8 thread.
319 // queueTailPosition: end of data we have enqued in the queue. 319 // queueTailPosition: end of data we have enqued in the queue.
320 // bookmarkPosition: position of the bookmark. 320 // bookmarkPosition: position of the bookmark.
321 SourceStreamDataQueue data_queue_; // Thread safe. 321 SourceStreamDataQueue data_queue_; // Thread safe.
322 size_t queue_lead_position_; // Only used by v8 thread. 322 size_t queue_lead_position_; // Only used by v8 thread.
323 size_t queue_tail_position_; // Used by both threads; guarded by m_mutex. 323 size_t queue_tail_position_; // Used by both threads; guarded by m_mutex.
324 324
325 RefPtr<WebTaskRunner> loading_task_runner_; 325 RefPtr<WebTaskRunner> loading_task_runner_;
326 }; 326 };
327 327
328 size_t ScriptStreamer::small_script_threshold_ = 30 * 1024; 328 size_t ScriptStreamerImpl::small_script_threshold_ = 30 * 1024;
329 329
330 void ScriptStreamer::StartStreaming(PendingScript* script, 330 void ScriptStreamerImpl::StartStreaming(
331 Type script_type, 331 PendingScript* script,
332 Settings* settings, 332 Type script_type,
333 ScriptState* script_state, 333 Settings* settings,
334 RefPtr<WebTaskRunner> loading_task_runner) { 334 ScriptState* script_state,
335 RefPtr<WebTaskRunner> loading_task_runner) {
335 // We don't yet know whether the script will really be streamed. E.g., 336 // We don't yet know whether the script will really be streamed. E.g.,
336 // suppressing streaming for short scripts is done later. Record only the 337 // suppressing streaming for short scripts is done later. Record only the
337 // sure negative cases here. 338 // sure negative cases here.
338 bool started_streaming = 339 bool started_streaming =
339 StartStreamingInternal(script, script_type, settings, script_state, 340 StartStreamingInternal(script, script_type, settings, script_state,
340 std::move(loading_task_runner)); 341 std::move(loading_task_runner));
341 if (!started_streaming) 342 if (!started_streaming)
342 RecordStartedStreamingHistogram(script_type, 0); 343 RecordStartedStreamingHistogram(script_type, 0);
343 } 344 }
344 345
345 bool ScriptStreamer::ConvertEncoding( 346 bool ScriptStreamerImpl::ConvertEncoding(
346 const char* encoding_name, 347 const char* encoding_name,
347 v8::ScriptCompiler::StreamedSource::Encoding* encoding) { 348 v8::ScriptCompiler::StreamedSource::Encoding* encoding) {
348 // Here's a list of encodings we can use for streaming. These are 349 // Here's a list of encodings we can use for streaming. These are
349 // the canonical names. 350 // the canonical names.
350 if (strcmp(encoding_name, "windows-1252") == 0 || 351 if (strcmp(encoding_name, "windows-1252") == 0 ||
351 strcmp(encoding_name, "ISO-8859-1") == 0 || 352 strcmp(encoding_name, "ISO-8859-1") == 0 ||
352 strcmp(encoding_name, "US-ASCII") == 0) { 353 strcmp(encoding_name, "US-ASCII") == 0) {
353 *encoding = v8::ScriptCompiler::StreamedSource::ONE_BYTE; 354 *encoding = v8::ScriptCompiler::StreamedSource::ONE_BYTE;
354 return true; 355 return true;
355 } 356 }
356 if (strcmp(encoding_name, "UTF-8") == 0) { 357 if (strcmp(encoding_name, "UTF-8") == 0) {
357 *encoding = v8::ScriptCompiler::StreamedSource::UTF8; 358 *encoding = v8::ScriptCompiler::StreamedSource::UTF8;
358 return true; 359 return true;
359 } 360 }
360 // We don't stream other encodings; especially we don't stream two 361 // We don't stream other encodings; especially we don't stream two
361 // byte scripts to avoid the handling of endianness. Most scripts 362 // byte scripts to avoid the handling of endianness. Most scripts
362 // are Latin1 or UTF-8 anyway, so this should be enough for most 363 // are Latin1 or UTF-8 anyway, so this should be enough for most
363 // real world purposes. 364 // real world purposes.
364 return false; 365 return false;
365 } 366 }
366 367
367 bool ScriptStreamer::IsFinished() const { 368 bool ScriptStreamerImpl::IsFinished() const {
368 DCHECK(IsMainThread()); 369 DCHECK(IsMainThread());
369 return loading_finished_ && (parsing_finished_ || streaming_suppressed_); 370 return loading_finished_ && (parsing_finished_ || streaming_suppressed_);
370 } 371 }
371 372
372 void ScriptStreamer::StreamingCompleteOnBackgroundThread() { 373 void ScriptStreamerImpl::StreamingCompleteOnBackgroundThread() {
373 DCHECK(!IsMainThread()); 374 DCHECK(!IsMainThread());
374 375
375 // notifyFinished might already be called, or it might be called in the 376 // notifyFinished might already be called, or it might be called in the
376 // future (if the parsing finishes earlier because of a parse error). 377 // future (if the parsing finishes earlier because of a parse error).
377 loading_task_runner_->PostTask( 378 loading_task_runner_->PostTask(
378 BLINK_FROM_HERE, CrossThreadBind(&ScriptStreamer::StreamingComplete, 379 BLINK_FROM_HERE, CrossThreadBind(&ScriptStreamerImpl::StreamingComplete,
379 WrapCrossThreadPersistent(this))); 380 WrapCrossThreadPersistent(this)));
380 381
381 // The task might delete ScriptStreamer, so it's not safe to do anything 382 // The task might delete ScriptStreamerImpl, so it's not safe to do anything
382 // after posting it. Note that there's no way to guarantee that this 383 // after posting it. Note that there's no way to guarantee that this
383 // function has returned before the task is ran - however, we should not 384 // function has returned before the task is ran - however, we should not
384 // access the "this" object after posting the task. 385 // access the "this" object after posting the task.
385 } 386 }
386 387
387 void ScriptStreamer::Cancel() { 388 void ScriptStreamerImpl::Cancel() {
388 DCHECK(IsMainThread()); 389 DCHECK(IsMainThread());
389 // The upper layer doesn't need the script any more, but streaming might 390 // The upper layer doesn't need the script any more, but streaming might
390 // still be ongoing. Tell SourceStream to try to cancel it whenever it gets 391 // still be ongoing. Tell SourceStream to try to cancel it whenever it gets
391 // the control the next time. It can also be that V8 has already completed 392 // the control the next time. It can also be that V8 has already completed
392 // its operations and streamingComplete will be called soon. 393 // its operations and streamingComplete will be called soon.
393 detached_ = true; 394 detached_ = true;
394 resource_ = 0; 395 resource_ = 0;
395 if (stream_) 396 if (stream_)
396 stream_->Cancel(); 397 stream_->Cancel();
397 } 398 }
398 399
399 void ScriptStreamer::SuppressStreaming() { 400 void ScriptStreamerImpl::SuppressStreaming() {
400 DCHECK(IsMainThread()); 401 DCHECK(IsMainThread());
401 DCHECK(!loading_finished_); 402 DCHECK(!loading_finished_);
402 // It can be that the parsing task has already finished (e.g., if there was 403 // It can be that the parsing task has already finished (e.g., if there was
403 // a parse error). 404 // a parse error).
404 streaming_suppressed_ = true; 405 streaming_suppressed_ = true;
405 } 406 }
406 407
407 void ScriptStreamer::NotifyAppendData(ScriptResource* resource) { 408 void ScriptStreamerImpl::NotifyAppendData(ScriptResource* resource) {
408 DCHECK(IsMainThread()); 409 DCHECK(IsMainThread());
409 CHECK_EQ(resource_, resource); 410 CHECK_EQ(resource_, resource);
410 if (streaming_suppressed_) 411 if (streaming_suppressed_)
411 return; 412 return;
412 if (!have_enough_data_for_streaming_) { 413 if (!have_enough_data_for_streaming_) {
413 // Even if the first data chunk is small, the script can still be big 414 // Even if the first data chunk is small, the script can still be big
414 // enough - wait until the next data chunk comes before deciding whether 415 // enough - wait until the next data chunk comes before deciding whether
415 // to start the streaming. 416 // to start the streaming.
416 DCHECK(resource->ResourceBuffer()); 417 DCHECK(resource->ResourceBuffer());
417 if (resource->ResourceBuffer()->size() < small_script_threshold_) 418 if (resource->ResourceBuffer()->size() < small_script_threshold_)
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after
490 ScriptStreamerThread::Shared()->PostTask( 491 ScriptStreamerThread::Shared()->PostTask(
491 CrossThreadBind(&ScriptStreamerThread::RunScriptStreamingTask, 492 CrossThreadBind(&ScriptStreamerThread::RunScriptStreamingTask,
492 WTF::Passed(std::move(script_streaming_task)), 493 WTF::Passed(std::move(script_streaming_task)),
493 WrapCrossThreadPersistent(this))); 494 WrapCrossThreadPersistent(this)));
494 RecordStartedStreamingHistogram(script_type_, 1); 495 RecordStartedStreamingHistogram(script_type_, 1);
495 } 496 }
496 if (stream_) 497 if (stream_)
497 stream_->DidReceiveData(this); 498 stream_->DidReceiveData(this);
498 } 499 }
499 500
500 void ScriptStreamer::NotifyFinished(Resource* resource) { 501 void ScriptStreamerImpl::NotifyFinished(Resource* resource) {
501 DCHECK(IsMainThread()); 502 DCHECK(IsMainThread());
502 CHECK_EQ(resource_, resource); 503 CHECK_EQ(resource_, resource);
503 // A special case: empty and small scripts. We didn't receive enough data to 504 // A special case: empty and small scripts. We didn't receive enough data to
504 // start the streaming before this notification. In that case, there won't 505 // start the streaming before this notification. In that case, there won't
505 // be a "parsing complete" notification either, and we should not wait for 506 // be a "parsing complete" notification either, and we should not wait for
506 // it. 507 // it.
507 if (!have_enough_data_for_streaming_) { 508 if (!have_enough_data_for_streaming_) {
508 RecordNotStreamingReasonHistogram(script_type_, kScriptTooSmall); 509 RecordNotStreamingReasonHistogram(script_type_, kScriptTooSmall);
509 RecordStartedStreamingHistogram(script_type_, 0); 510 RecordStartedStreamingHistogram(script_type_, 0);
510 SuppressStreaming(); 511 SuppressStreaming();
511 } 512 }
512 if (stream_) 513 if (stream_)
513 stream_->DidFinishLoading(); 514 stream_->DidFinishLoading();
514 loading_finished_ = true; 515 loading_finished_ = true;
515 516
516 NotifyFinishedToClient(); 517 NotifyFinishedToClient();
517 } 518 }
518 519
519 ScriptStreamer::ScriptStreamer( 520 ScriptStreamerImpl::ScriptStreamerImpl(
520 PendingScript* script, 521 PendingScript* script,
521 Type script_type, 522 Type script_type,
522 ScriptState* script_state, 523 ScriptState* script_state,
523 v8::ScriptCompiler::CompileOptions compile_options, 524 v8::ScriptCompiler::CompileOptions compile_options,
524 RefPtr<WebTaskRunner> loading_task_runner) 525 RefPtr<WebTaskRunner> loading_task_runner)
525 : pending_script_(script), 526 : pending_script_(script),
526 resource_(script->GetResource()), 527 resource_(script->GetResource()),
527 detached_(false), 528 detached_(false),
528 stream_(0), 529 stream_(0),
529 loading_finished_(false), 530 loading_finished_(false),
530 parsing_finished_(false), 531 parsing_finished_(false),
531 have_enough_data_for_streaming_(false), 532 have_enough_data_for_streaming_(false),
532 streaming_suppressed_(false), 533 streaming_suppressed_(false),
533 compile_options_(compile_options), 534 compile_options_(compile_options),
534 script_state_(script_state), 535 script_state_(script_state),
535 script_type_(script_type), 536 script_type_(script_type),
536 script_url_string_(resource_->Url().Copy().GetString()), 537 script_url_string_(resource_->Url().Copy().GetString()),
537 script_resource_identifier_(resource_->Identifier()), 538 script_resource_identifier_(resource_->Identifier()),
538 // Unfortunately there's no dummy encoding value in the enum; let's use 539 // Unfortunately there's no dummy encoding value in the enum; let's use
539 // one we don't stream. 540 // one we don't stream.
540 encoding_(v8::ScriptCompiler::StreamedSource::TWO_BYTE), 541 encoding_(v8::ScriptCompiler::StreamedSource::TWO_BYTE),
541 loading_task_runner_(std::move(loading_task_runner)) {} 542 loading_task_runner_(std::move(loading_task_runner)) {}
542 543
543 ScriptStreamer::~ScriptStreamer() {} 544 ScriptStreamerImpl::~ScriptStreamerImpl() {}
544 545
545 DEFINE_TRACE(ScriptStreamer) { 546 DEFINE_TRACE(ScriptStreamerImpl) {
546 visitor->Trace(pending_script_); 547 visitor->Trace(pending_script_);
547 visitor->Trace(resource_); 548 visitor->Trace(resource_);
549 ScriptStreamer::Trace(visitor);
548 } 550 }
549 551
550 void ScriptStreamer::StreamingComplete() { 552 void ScriptStreamerImpl::StreamingComplete() {
551 // The background task is completed; do the necessary ramp-down in the main 553 // The background task is completed; do the necessary ramp-down in the main
552 // thread. 554 // thread.
553 DCHECK(IsMainThread()); 555 DCHECK(IsMainThread());
554 parsing_finished_ = true; 556 parsing_finished_ = true;
555 557
556 // It's possible that the corresponding Resource was deleted before V8 558 // It's possible that the corresponding Resource was deleted before V8
557 // finished streaming. In that case, the data or the notification is not 559 // finished streaming. In that case, the data or the notification is not
558 // needed. In addition, if the streaming is suppressed, the non-streaming 560 // needed. In addition, if the streaming is suppressed, the non-streaming
559 // code path will resume after the resource has loaded, before the 561 // code path will resume after the resource has loaded, before the
560 // background task finishes. 562 // background task finishes.
561 if (detached_ || streaming_suppressed_) 563 if (detached_ || streaming_suppressed_)
562 return; 564 return;
563 565
564 // We have now streamed the whole script to V8 and it has parsed the 566 // We have now streamed the whole script to V8 and it has parsed the
565 // script. We're ready for the next step: compiling and executing the 567 // script. We're ready for the next step: compiling and executing the
566 // script. 568 // script.
567 NotifyFinishedToClient(); 569 NotifyFinishedToClient();
568 } 570 }
569 571
570 void ScriptStreamer::NotifyFinishedToClient() { 572 void ScriptStreamerImpl::NotifyFinishedToClient() {
571 DCHECK(IsMainThread()); 573 DCHECK(IsMainThread());
572 // Usually, the loading will be finished first, and V8 will still need some 574 // Usually, the loading will be finished first, and V8 will still need some
573 // time to catch up. But the other way is possible too: if V8 detects a 575 // time to catch up. But the other way is possible too: if V8 detects a
574 // parse error, the V8 side can complete before loading has finished. Send 576 // parse error, the V8 side can complete before loading has finished. Send
575 // the notification after both loading and V8 side operations have 577 // the notification after both loading and V8 side operations have
576 // completed. 578 // completed.
577 if (!IsFinished()) 579 if (!IsFinished())
578 return; 580 return;
579 581
580 pending_script_->StreamingFinished(); 582 pending_script_->StreamingFinished();
581 } 583 }
582 584
583 bool ScriptStreamer::StartStreamingInternal( 585 bool ScriptStreamerImpl::StartStreamingInternal(
584 PendingScript* script, 586 PendingScript* script,
585 Type script_type, 587 Type script_type,
586 Settings* settings, 588 Settings* settings,
587 ScriptState* script_state, 589 ScriptState* script_state,
588 RefPtr<WebTaskRunner> loading_task_runner) { 590 RefPtr<WebTaskRunner> loading_task_runner) {
589 DCHECK(IsMainThread()); 591 DCHECK(IsMainThread());
590 DCHECK(script_state->ContextIsValid()); 592 DCHECK(script_state->ContextIsValid());
591 ScriptResource* resource = script->GetResource(); 593 ScriptResource* resource = script->GetResource();
592 if (resource->IsLoaded()) { 594 if (resource->IsLoaded()) {
593 RecordNotStreamingReasonHistogram(script_type, kAlreadyLoaded); 595 RecordNotStreamingReasonHistogram(script_type, kAlreadyLoaded);
(...skipping 14 matching lines...) Expand all
608 // to arrive: the Content-Length HTTP header is not sent for chunked 610 // to arrive: the Content-Length HTTP header is not sent for chunked
609 // downloads. 611 // downloads.
610 612
611 // Decide what kind of cached data we should produce while streaming. Only 613 // Decide what kind of cached data we should produce while streaming. Only
612 // produce parser cache if the non-streaming compile takes advantage of it. 614 // produce parser cache if the non-streaming compile takes advantage of it.
613 v8::ScriptCompiler::CompileOptions compile_option = 615 v8::ScriptCompiler::CompileOptions compile_option =
614 v8::ScriptCompiler::kNoCompileOptions; 616 v8::ScriptCompiler::kNoCompileOptions;
615 if (settings->GetV8CacheOptions() == kV8CacheOptionsParse) 617 if (settings->GetV8CacheOptions() == kV8CacheOptionsParse)
616 compile_option = v8::ScriptCompiler::kProduceParserCache; 618 compile_option = v8::ScriptCompiler::kProduceParserCache;
617 619
618 // The Resource might go out of scope if the script is no longer 620 // The Resource might go out of scope if the script is no longer needed.
619 // needed. This makes PendingScript notify the ScriptStreamer when it is 621 // This makes PendingScript notify the ScriptStreamerImpl when it is
620 // destroyed. 622 // destroyed.
621 script->SetStreamer(ScriptStreamer::Create(script, script_type, script_state, 623 script->SetStreamer(ScriptStreamerImpl::Create(
622 compile_option, 624 script, script_type, script_state, compile_option,
623 std::move(loading_task_runner))); 625 std::move(loading_task_runner)));
624 626
625 return true; 627 return true;
626 } 628 }
627 629
628 } // namespace blink 630 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698