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

Side by Side Diff: cc/resources/video_resource_updater.cc

Issue 761903003: Update from https://crrev.com/306655 (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 6 years 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 | « cc/resources/video_resource_updater.h ('k') | cc/resources/video_resource_updater_unittest.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 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 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 "cc/resources/video_resource_updater.h" 5 #include "cc/resources/video_resource_updater.h"
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/debug/trace_event.h" 8 #include "base/debug/trace_event.h"
9 #include "cc/output/gl_renderer.h" 9 #include "cc/output/gl_renderer.h"
10 #include "cc/resources/resource_provider.h" 10 #include "cc/resources/resource_provider.h"
11 #include "gpu/GLES2/gl2extchromium.h" 11 #include "gpu/GLES2/gl2extchromium.h"
12 #include "gpu/command_buffer/client/gles2_interface.h" 12 #include "gpu/command_buffer/client/gles2_interface.h"
13 #include "media/base/video_frame.h" 13 #include "media/base/video_frame.h"
14 #include "media/filters/skcanvas_video_renderer.h" 14 #include "media/filters/skcanvas_video_renderer.h"
15 #include "third_party/khronos/GLES2/gl2.h" 15 #include "third_party/khronos/GLES2/gl2.h"
16 #include "third_party/khronos/GLES2/gl2ext.h" 16 #include "third_party/khronos/GLES2/gl2ext.h"
17 #include "ui/gfx/geometry/size_conversions.h" 17 #include "ui/gfx/geometry/size_conversions.h"
18 18
19 namespace cc { 19 namespace cc {
20 20
21 namespace { 21 namespace {
22 22
23 const ResourceFormat kYUVResourceFormat = LUMINANCE_8;
24 const ResourceFormat kRGBResourceFormat = RGBA_8888; 23 const ResourceFormat kRGBResourceFormat = RGBA_8888;
25 24
26 class SyncPointClientImpl : public media::VideoFrame::SyncPointClient { 25 class SyncPointClientImpl : public media::VideoFrame::SyncPointClient {
27 public: 26 public:
28 explicit SyncPointClientImpl(gpu::gles2::GLES2Interface* gl) : gl_(gl) {} 27 explicit SyncPointClientImpl(gpu::gles2::GLES2Interface* gl) : gl_(gl) {}
29 ~SyncPointClientImpl() override {} 28 ~SyncPointClientImpl() override {}
30 uint32 InsertSyncPoint() override { 29 uint32 InsertSyncPoint() override {
31 return GLC(gl_, gl_->InsertSyncPointCHROMIUM()); 30 return GLC(gl_, gl_->InsertSyncPointCHROMIUM());
32 } 31 }
33 void WaitSyncPoint(uint32 sync_point) override { 32 void WaitSyncPoint(uint32 sync_point) override {
34 GLC(gl_, gl_->WaitSyncPointCHROMIUM(sync_point)); 33 GLC(gl_, gl_->WaitSyncPointCHROMIUM(sync_point));
35 } 34 }
36 35
37 private: 36 private:
38 gpu::gles2::GLES2Interface* gl_; 37 gpu::gles2::GLES2Interface* gl_;
39 }; 38 };
40 39
41 } // namespace 40 } // namespace
42 41
42 VideoResourceUpdater::PlaneResource::PlaneResource(
43 unsigned int resource_id,
44 const gfx::Size& resource_size,
45 ResourceFormat resource_format,
46 gpu::Mailbox mailbox)
47 : resource_id(resource_id),
48 resource_size(resource_size),
49 resource_format(resource_format),
50 mailbox(mailbox),
51 frame_ptr(nullptr),
52 plane_index(0) {
53 }
54
55 bool VideoResourceUpdater::PlaneResourceMatchesUniqueID(
56 const PlaneResource& plane_resource,
57 const media::VideoFrame* video_frame,
58 int plane_index) {
59 return plane_resource.frame_ptr == video_frame &&
60 plane_resource.plane_index == plane_index &&
61 plane_resource.timestamp == video_frame->timestamp();
62 }
63
64 void VideoResourceUpdater::SetPlaneResourceUniqueId(
65 const media::VideoFrame* video_frame,
66 int plane_index,
67 PlaneResource* plane_resource) {
68 plane_resource->frame_ptr = video_frame;
69 plane_resource->plane_index = plane_index;
70 plane_resource->timestamp = video_frame->timestamp();
71 }
72
43 VideoFrameExternalResources::VideoFrameExternalResources() : type(NONE) {} 73 VideoFrameExternalResources::VideoFrameExternalResources() : type(NONE) {}
44 74
45 VideoFrameExternalResources::~VideoFrameExternalResources() {} 75 VideoFrameExternalResources::~VideoFrameExternalResources() {}
46 76
47 VideoResourceUpdater::VideoResourceUpdater(ContextProvider* context_provider, 77 VideoResourceUpdater::VideoResourceUpdater(ContextProvider* context_provider,
48 ResourceProvider* resource_provider) 78 ResourceProvider* resource_provider)
49 : context_provider_(context_provider), 79 : context_provider_(context_provider),
50 resource_provider_(resource_provider) { 80 resource_provider_(resource_provider) {
51 } 81 }
52 82
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
97 case media::VideoFrame::NV12: 127 case media::VideoFrame::NV12:
98 break; 128 break;
99 } 129 }
100 return false; 130 return false;
101 } 131 }
102 132
103 // For frames that we receive in software format, determine the dimensions of 133 // For frames that we receive in software format, determine the dimensions of
104 // each plane in the frame. 134 // each plane in the frame.
105 static gfx::Size SoftwarePlaneDimension( 135 static gfx::Size SoftwarePlaneDimension(
106 const scoped_refptr<media::VideoFrame>& input_frame, 136 const scoped_refptr<media::VideoFrame>& input_frame,
107 ResourceFormat output_resource_format, 137 bool software_compositor,
108 size_t plane_index) { 138 size_t plane_index) {
109 if (output_resource_format == kYUVResourceFormat) { 139 if (!software_compositor) {
110 return media::VideoFrame::PlaneSize( 140 return media::VideoFrame::PlaneSize(
111 input_frame->format(), plane_index, input_frame->coded_size()); 141 input_frame->format(), plane_index, input_frame->coded_size());
112 } 142 }
113
114 DCHECK_EQ(output_resource_format, kRGBResourceFormat);
115 return input_frame->coded_size(); 143 return input_frame->coded_size();
116 } 144 }
117 145
118 VideoFrameExternalResources VideoResourceUpdater::CreateForSoftwarePlanes( 146 VideoFrameExternalResources VideoResourceUpdater::CreateForSoftwarePlanes(
119 const scoped_refptr<media::VideoFrame>& video_frame) { 147 const scoped_refptr<media::VideoFrame>& video_frame) {
120 TRACE_EVENT0("cc", "VideoResourceUpdater::CreateForSoftwarePlanes"); 148 TRACE_EVENT0("cc", "VideoResourceUpdater::CreateForSoftwarePlanes");
121 media::VideoFrame::Format input_frame_format = video_frame->format(); 149 media::VideoFrame::Format input_frame_format = video_frame->format();
122 150
123 #if defined(VIDEO_HOLE) 151 #if defined(VIDEO_HOLE)
124 if (input_frame_format == media::VideoFrame::HOLE) { 152 if (input_frame_format == media::VideoFrame::HOLE) {
(...skipping 13 matching lines...) Expand all
138 if (input_frame_format != media::VideoFrame::YV12 && 166 if (input_frame_format != media::VideoFrame::YV12 &&
139 input_frame_format != media::VideoFrame::I420 && 167 input_frame_format != media::VideoFrame::I420 &&
140 input_frame_format != media::VideoFrame::YV12A && 168 input_frame_format != media::VideoFrame::YV12A &&
141 input_frame_format != media::VideoFrame::YV12J && 169 input_frame_format != media::VideoFrame::YV12J &&
142 input_frame_format != media::VideoFrame::YV16 && 170 input_frame_format != media::VideoFrame::YV16 &&
143 input_frame_format != media::VideoFrame::YV24) 171 input_frame_format != media::VideoFrame::YV24)
144 return VideoFrameExternalResources(); 172 return VideoFrameExternalResources();
145 173
146 bool software_compositor = context_provider_ == NULL; 174 bool software_compositor = context_provider_ == NULL;
147 175
148 ResourceFormat output_resource_format = kYUVResourceFormat; 176 ResourceFormat output_resource_format =
177 resource_provider_->yuv_resource_format();
149 size_t output_plane_count = media::VideoFrame::NumPlanes(input_frame_format); 178 size_t output_plane_count = media::VideoFrame::NumPlanes(input_frame_format);
150 179
151 // TODO(skaslev): If we're in software compositing mode, we do the YUV -> RGB 180 // TODO(skaslev): If we're in software compositing mode, we do the YUV -> RGB
152 // conversion here. That involves an extra copy of each frame to a bitmap. 181 // conversion here. That involves an extra copy of each frame to a bitmap.
153 // Obviously, this is suboptimal and should be addressed once ubercompositor 182 // Obviously, this is suboptimal and should be addressed once ubercompositor
154 // starts shaping up. 183 // starts shaping up.
155 if (software_compositor) { 184 if (software_compositor) {
156 output_resource_format = kRGBResourceFormat; 185 output_resource_format = kRGBResourceFormat;
157 output_plane_count = 1; 186 output_plane_count = 1;
158 } 187 }
159 188
160 int max_resource_size = resource_provider_->max_texture_size(); 189 int max_resource_size = resource_provider_->max_texture_size();
161 std::vector<PlaneResource> plane_resources; 190 std::vector<PlaneResource> plane_resources;
162 bool allocation_success = true; 191 bool allocation_success = true;
163 192
164 for (size_t i = 0; i < output_plane_count; ++i) { 193 for (size_t i = 0; i < output_plane_count; ++i) {
165 gfx::Size output_plane_resource_size = 194 gfx::Size output_plane_resource_size =
166 SoftwarePlaneDimension(video_frame, output_resource_format, i); 195 SoftwarePlaneDimension(video_frame, software_compositor, i);
167 if (output_plane_resource_size.IsEmpty() || 196 if (output_plane_resource_size.IsEmpty() ||
168 output_plane_resource_size.width() > max_resource_size || 197 output_plane_resource_size.width() > max_resource_size ||
169 output_plane_resource_size.height() > max_resource_size) { 198 output_plane_resource_size.height() > max_resource_size) {
170 allocation_success = false; 199 allocation_success = false;
171 break; 200 break;
172 } 201 }
173 202
174 ResourceProvider::ResourceId resource_id = 0;
175 gpu::Mailbox mailbox;
176
177 // Try recycle a previously-allocated resource. 203 // Try recycle a previously-allocated resource.
178 for (size_t i = 0; i < recycled_resources_.size(); ++i) { 204 auto recycled_it = recycled_resources_.end();
179 bool resource_matches = 205 for (auto it = recycled_resources_.begin(); it != recycled_resources_.end();
180 recycled_resources_[i].resource_format == output_resource_format && 206 ++it) {
181 recycled_resources_[i].resource_size == output_plane_resource_size; 207 const bool resource_matches =
182 bool not_in_use = 208 it->resource_format == output_resource_format &&
183 !software_compositor || !resource_provider_->InUseByConsumer( 209 it->resource_size == output_plane_resource_size;
184 recycled_resources_[i].resource_id); 210 const bool in_use = software_compositor &&
185 if (resource_matches && not_in_use) { 211 resource_provider_->InUseByConsumer(it->resource_id);
186 resource_id = recycled_resources_[i].resource_id; 212 if (resource_matches && !in_use) {
187 mailbox = recycled_resources_[i].mailbox; 213 // We found a recycled resource with the allocation size and format we
188 recycled_resources_.erase(recycled_resources_.begin() + i); 214 // are looking for.
189 break; 215 recycled_it = it;
216 // Keep looking for a recycled resource that also contains the data we
217 // are planning to put in it.
218 if (PlaneResourceMatchesUniqueID(*it, video_frame.get(), i))
219 break;
190 } 220 }
191 } 221 }
192 222
193 if (resource_id == 0) { 223 // Check if we can avoid allocating a new resource.
194 // TODO(danakj): Abstract out hw/sw resource create/delete from 224 if (recycled_it != recycled_resources_.end()) {
195 // ResourceProvider and stop using ResourceProvider in this class. 225 plane_resources.push_back(*recycled_it);
196 resource_id = resource_provider_->CreateResource( 226 recycled_resources_.erase(recycled_it);
197 output_plane_resource_size, 227 continue;
198 GL_CLAMP_TO_EDGE,
199 ResourceProvider::TextureHintImmutable,
200 output_resource_format);
201
202 DCHECK(mailbox.IsZero());
203
204 if (!software_compositor) {
205 DCHECK(context_provider_);
206
207 gpu::gles2::GLES2Interface* gl = context_provider_->ContextGL();
208
209 GLC(gl, gl->GenMailboxCHROMIUM(mailbox.name));
210 ResourceProvider::ScopedWriteLockGL lock(resource_provider_,
211 resource_id);
212 GLC(gl,
213 gl->ProduceTextureDirectCHROMIUM(
214 lock.texture_id(), GL_TEXTURE_2D, mailbox.name));
215 }
216
217 if (resource_id)
218 all_resources_.push_back(resource_id);
219 } 228 }
220 229
230 // TODO(danakj): Abstract out hw/sw resource create/delete from
231 // ResourceProvider and stop using ResourceProvider in this class.
232 const ResourceProvider::ResourceId resource_id =
233 resource_provider_->CreateResource(
234 output_plane_resource_size, GL_CLAMP_TO_EDGE,
235 ResourceProvider::TextureHintImmutable, output_resource_format);
221 if (resource_id == 0) { 236 if (resource_id == 0) {
222 allocation_success = false; 237 allocation_success = false;
223 break; 238 break;
224 } 239 }
240 all_resources_.push_back(resource_id);
241
242 gpu::Mailbox mailbox;
243 if (!software_compositor) {
244 DCHECK(context_provider_);
245
246 gpu::gles2::GLES2Interface* gl = context_provider_->ContextGL();
247
248 GLC(gl, gl->GenMailboxCHROMIUM(mailbox.name));
249 ResourceProvider::ScopedWriteLockGL lock(resource_provider_, resource_id);
250 GLC(gl, gl->ProduceTextureDirectCHROMIUM(lock.texture_id(), GL_TEXTURE_2D,
251 mailbox.name));
252 }
225 253
226 DCHECK(software_compositor || !mailbox.IsZero()); 254 DCHECK(software_compositor || !mailbox.IsZero());
227 plane_resources.push_back(PlaneResource(resource_id, 255 plane_resources.push_back(PlaneResource(resource_id,
228 output_plane_resource_size, 256 output_plane_resource_size,
229 output_resource_format, 257 output_resource_format,
230 mailbox)); 258 mailbox));
231 } 259 }
232 260
233 if (!allocation_success) { 261 if (!allocation_success) {
234 for (size_t i = 0; i < plane_resources.size(); ++i) 262 for (size_t i = 0; i < plane_resources.size(); ++i)
235 DeleteResource(plane_resources[i].resource_id); 263 DeleteResource(plane_resources[i].resource_id);
236 return VideoFrameExternalResources(); 264 return VideoFrameExternalResources();
237 } 265 }
238 266
239 VideoFrameExternalResources external_resources; 267 VideoFrameExternalResources external_resources;
240 268
241 if (software_compositor) { 269 if (software_compositor) {
242 DCHECK_EQ(plane_resources.size(), 1u); 270 DCHECK_EQ(plane_resources.size(), 1u);
243 DCHECK_EQ(plane_resources[0].resource_format, kRGBResourceFormat); 271 DCHECK_EQ(plane_resources[0].resource_format, kRGBResourceFormat);
244 DCHECK(plane_resources[0].mailbox.IsZero()); 272 DCHECK(plane_resources[0].mailbox.IsZero());
245 273
246 if (!video_renderer_) 274 if (!PlaneResourceMatchesUniqueID(plane_resources[0], video_frame.get(),
247 video_renderer_.reset(new media::SkCanvasVideoRenderer); 275 0)) {
276 // We need to transfer data from |video_frame| to the plane resource.
277 if (!video_renderer_)
278 video_renderer_.reset(new media::SkCanvasVideoRenderer);
248 279
249 {
250 ResourceProvider::ScopedWriteLockSoftware lock( 280 ResourceProvider::ScopedWriteLockSoftware lock(
251 resource_provider_, plane_resources[0].resource_id); 281 resource_provider_, plane_resources[0].resource_id);
252 SkCanvas canvas(lock.sk_bitmap()); 282 SkCanvas canvas(lock.sk_bitmap());
253 video_renderer_->Copy(video_frame, &canvas); 283 video_renderer_->Copy(video_frame, &canvas);
284 SetPlaneResourceUniqueId(video_frame.get(), 0, &plane_resources[0]);
254 } 285 }
255 286
256 RecycleResourceData recycle_data = {
257 plane_resources[0].resource_id,
258 plane_resources[0].resource_size,
259 plane_resources[0].resource_format,
260 gpu::Mailbox()
261 };
262 external_resources.software_resources.push_back( 287 external_resources.software_resources.push_back(
263 plane_resources[0].resource_id); 288 plane_resources[0].resource_id);
264 external_resources.software_release_callback = 289 external_resources.software_release_callback =
265 base::Bind(&RecycleResource, AsWeakPtr(), recycle_data); 290 base::Bind(&RecycleResource, AsWeakPtr(), plane_resources[0]);
266 external_resources.type = VideoFrameExternalResources::SOFTWARE_RESOURCE; 291 external_resources.type = VideoFrameExternalResources::SOFTWARE_RESOURCE;
267 292
268 return external_resources; 293 return external_resources;
269 } 294 }
270 295
271 for (size_t i = 0; i < plane_resources.size(); ++i) { 296 for (size_t i = 0; i < plane_resources.size(); ++i) {
272 // Update each plane's resource id with its content. 297 // Update each plane's resource id with its content.
273 DCHECK_EQ(plane_resources[i].resource_format, kYUVResourceFormat); 298 DCHECK_EQ(plane_resources[i].resource_format,
299 resource_provider_->yuv_resource_format());
274 300
275 const uint8_t* input_plane_pixels = video_frame->data(i); 301 if (!PlaneResourceMatchesUniqueID(plane_resources[i], video_frame.get(),
302 i)) {
303 // We need to transfer data from |video_frame| to the plane resource.
304 const uint8_t* input_plane_pixels = video_frame->data(i);
276 305
277 gfx::Rect image_rect(0, 306 gfx::Rect image_rect(0, 0, video_frame->stride(i),
278 0, 307 plane_resources[i].resource_size.height());
279 video_frame->stride(i), 308 gfx::Rect source_rect(plane_resources[i].resource_size);
280 plane_resources[i].resource_size.height()); 309 resource_provider_->SetPixels(plane_resources[i].resource_id,
281 gfx::Rect source_rect(plane_resources[i].resource_size); 310 input_plane_pixels, image_rect, source_rect,
282 resource_provider_->SetPixels(plane_resources[i].resource_id, 311 gfx::Vector2d());
283 input_plane_pixels, 312 SetPlaneResourceUniqueId(video_frame.get(), i, &plane_resources[i]);
284 image_rect, 313 }
285 source_rect,
286 gfx::Vector2d());
287
288 RecycleResourceData recycle_data = {
289 plane_resources[i].resource_id,
290 plane_resources[i].resource_size,
291 plane_resources[i].resource_format,
292 plane_resources[i].mailbox
293 };
294 314
295 external_resources.mailboxes.push_back( 315 external_resources.mailboxes.push_back(
296 TextureMailbox(plane_resources[i].mailbox, GL_TEXTURE_2D, 0)); 316 TextureMailbox(plane_resources[i].mailbox, GL_TEXTURE_2D, 0));
297 external_resources.release_callbacks.push_back( 317 external_resources.release_callbacks.push_back(
298 base::Bind(&RecycleResource, AsWeakPtr(), recycle_data)); 318 base::Bind(&RecycleResource, AsWeakPtr(), plane_resources[i]));
299 } 319 }
300 320
301 external_resources.type = VideoFrameExternalResources::YUV_RESOURCE; 321 external_resources.type = VideoFrameExternalResources::YUV_RESOURCE;
302 return external_resources; 322 return external_resources;
303 } 323 }
304 324
305 // static 325 // static
306 void VideoResourceUpdater::ReturnTexture( 326 void VideoResourceUpdater::ReturnTexture(
307 base::WeakPtr<VideoResourceUpdater> updater, 327 base::WeakPtr<VideoResourceUpdater> updater,
308 const scoped_refptr<media::VideoFrame>& video_frame, 328 const scoped_refptr<media::VideoFrame>& video_frame,
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
355 mailbox_holder->texture_target, 375 mailbox_holder->texture_target,
356 mailbox_holder->sync_point)); 376 mailbox_holder->sync_point));
357 external_resources.release_callbacks.push_back( 377 external_resources.release_callbacks.push_back(
358 base::Bind(&ReturnTexture, AsWeakPtr(), video_frame)); 378 base::Bind(&ReturnTexture, AsWeakPtr(), video_frame));
359 return external_resources; 379 return external_resources;
360 } 380 }
361 381
362 // static 382 // static
363 void VideoResourceUpdater::RecycleResource( 383 void VideoResourceUpdater::RecycleResource(
364 base::WeakPtr<VideoResourceUpdater> updater, 384 base::WeakPtr<VideoResourceUpdater> updater,
365 RecycleResourceData data, 385 PlaneResource data,
366 uint32 sync_point, 386 uint32 sync_point,
367 bool lost_resource, 387 bool lost_resource,
368 BlockingTaskRunner* main_thread_task_runner) { 388 BlockingTaskRunner* main_thread_task_runner) {
369 if (!updater.get()) { 389 if (!updater.get()) {
370 // Resource was already deleted. 390 // Resource was already deleted.
371 return; 391 return;
372 } 392 }
373 393
374 ContextProvider* context_provider = updater->context_provider_; 394 ContextProvider* context_provider = updater->context_provider_;
375 if (context_provider && sync_point) { 395 if (context_provider && sync_point) {
376 GLC(context_provider->ContextGL(), 396 GLC(context_provider->ContextGL(),
377 context_provider->ContextGL()->WaitSyncPointCHROMIUM(sync_point)); 397 context_provider->ContextGL()->WaitSyncPointCHROMIUM(sync_point));
378 } 398 }
379 399
380 if (lost_resource) { 400 if (lost_resource) {
381 updater->DeleteResource(data.resource_id); 401 updater->DeleteResource(data.resource_id);
382 return; 402 return;
383 } 403 }
384 404
385 // Drop recycled resources that are the wrong format. 405 // Drop recycled resources that are the wrong format.
386 while (!updater->recycled_resources_.empty() && 406 while (!updater->recycled_resources_.empty() &&
387 updater->recycled_resources_.back().resource_format != 407 updater->recycled_resources_.back().resource_format !=
388 data.resource_format) { 408 data.resource_format) {
389 updater->DeleteResource(updater->recycled_resources_.back().resource_id); 409 updater->DeleteResource(updater->recycled_resources_.back().resource_id);
390 updater->recycled_resources_.pop_back(); 410 updater->recycled_resources_.pop_back();
391 } 411 }
392 412
393 PlaneResource recycled_resource(data.resource_id, 413 updater->recycled_resources_.push_back(data);
394 data.resource_size,
395 data.resource_format,
396 data.mailbox);
397 updater->recycled_resources_.push_back(recycled_resource);
398 } 414 }
399 415
400 } // namespace cc 416 } // namespace cc
OLDNEW
« no previous file with comments | « cc/resources/video_resource_updater.h ('k') | cc/resources/video_resource_updater_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698