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
3 // found in the LICENSE file.
5 #include "content/browser/gpu/browser_gpu_memory_buffer_manager.h"
7 #include "base/atomic_sequence_num.h"
9 #include "base/command_line.h"
10 #include "base/strings/stringprintf.h"
11 #include "base/synchronization/waitable_event.h"
12 #include "base/threading/thread_restrictions.h"
13 #include "base/trace_event/process_memory_dump.h"
14 #include "base/trace_event/trace_event.h"
15 #include "content/browser/gpu/gpu_process_host.h"
16 #include "content/common/child_process_host_impl.h"
17 #include "content/common/gpu/client/gpu_memory_buffer_impl.h"
18 #include "content/common/gpu/client/gpu_memory_buffer_impl_shared_memory.h"
19 #include "content/common/gpu/gpu_memory_buffer_factory_shared_memory.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "content/public/common/content_switches.h"
22 #include "gpu/GLES2/gl2extchromium.h"
24 #if defined(OS_MACOSX)
25 #include "content/common/gpu/gpu_memory_buffer_factory_io_surface.h"
28 #if defined(OS_ANDROID)
29 #include "content/common/gpu/gpu_memory_buffer_factory_surface_texture.h"
32 #if defined(USE_OZONE)
33 #include "content/common/gpu/gpu_memory_buffer_factory_ozone_native_pixmap.h"
39 void GpuMemoryBufferDeleted(
40 scoped_refptr
<base::SingleThreadTaskRunner
> destruction_task_runner
,
41 const GpuMemoryBufferImpl::DestructionCallback
& destruction_callback
,
43 destruction_task_runner
->PostTask(
44 FROM_HERE
, base::Bind(destruction_callback
, sync_point
));
47 bool IsGpuMemoryBufferFactoryConfigurationSupported(
48 gfx::GpuMemoryBufferType type
,
49 const GpuMemoryBufferFactory::Configuration
& configuration
) {
51 case gfx::SHARED_MEMORY_BUFFER
:
52 return GpuMemoryBufferFactorySharedMemory::
53 IsGpuMemoryBufferConfigurationSupported(configuration
.format
,
55 #if defined(OS_MACOSX)
56 case gfx::IO_SURFACE_BUFFER
:
57 return GpuMemoryBufferFactoryIOSurface::
58 IsGpuMemoryBufferConfigurationSupported(configuration
.format
,
61 #if defined(OS_ANDROID)
62 case gfx::SURFACE_TEXTURE_BUFFER
:
63 return GpuMemoryBufferFactorySurfaceTexture::
64 IsGpuMemoryBufferConfigurationSupported(configuration
.format
,
67 #if defined(USE_OZONE)
68 case gfx::OZONE_NATIVE_PIXMAP
:
69 return GpuMemoryBufferFactoryOzoneNativePixmap::
70 IsGpuMemoryBufferConfigurationSupported(configuration
.format
,
79 gfx::GpuMemoryBufferType
GetGpuMemoryBufferFactoryType() {
80 std::vector
<gfx::GpuMemoryBufferType
> supported_types
;
81 GpuMemoryBufferFactory::GetSupportedTypes(&supported_types
);
82 DCHECK(!supported_types
.empty());
84 // The GPU service will always use the preferred type.
85 return supported_types
[0];
88 std::vector
<GpuMemoryBufferFactory::Configuration
>
89 GetSupportedGpuMemoryBufferConfigurations(gfx::GpuMemoryBufferType type
) {
90 std::vector
<GpuMemoryBufferFactory::Configuration
> configurations
;
91 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
92 switches::kEnableNativeGpuMemoryBuffers
)) {
93 const GpuMemoryBufferFactory::Configuration kNativeConfigurations
[] = {
94 {gfx::BufferFormat::R_8
, gfx::BufferUsage::MAP
},
95 {gfx::BufferFormat::R_8
, gfx::BufferUsage::PERSISTENT_MAP
},
96 {gfx::BufferFormat::RGBA_4444
, gfx::BufferUsage::MAP
},
97 {gfx::BufferFormat::RGBA_4444
, gfx::BufferUsage::PERSISTENT_MAP
},
98 {gfx::BufferFormat::RGBA_8888
, gfx::BufferUsage::MAP
},
99 {gfx::BufferFormat::RGBA_8888
, gfx::BufferUsage::PERSISTENT_MAP
},
100 {gfx::BufferFormat::BGRA_8888
, gfx::BufferUsage::MAP
},
101 {gfx::BufferFormat::BGRA_8888
, gfx::BufferUsage::PERSISTENT_MAP
}};
102 for (auto& configuration
: kNativeConfigurations
) {
103 if (IsGpuMemoryBufferFactoryConfigurationSupported(type
, configuration
))
104 configurations
.push_back(configuration
);
108 #if defined(USE_OZONE) || defined(OS_MACOSX)
109 const GpuMemoryBufferFactory::Configuration kScanoutConfigurations
[] = {
110 {gfx::BufferFormat::BGRA_8888
, gfx::BufferUsage::SCANOUT
},
111 {gfx::BufferFormat::RGBX_8888
, gfx::BufferUsage::SCANOUT
}};
112 for (auto& configuration
: kScanoutConfigurations
) {
113 if (IsGpuMemoryBufferFactoryConfigurationSupported(type
, configuration
))
114 configurations
.push_back(configuration
);
118 return configurations
;
121 BrowserGpuMemoryBufferManager
* g_gpu_memory_buffer_manager
= nullptr;
123 // Global atomic to generate gpu memory buffer unique IDs.
124 base::StaticAtomicSequenceNumber g_next_gpu_memory_buffer_id
;
128 struct BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferRequest
{
129 AllocateGpuMemoryBufferRequest(const gfx::Size
& size
,
130 gfx::BufferFormat format
,
131 gfx::BufferUsage usage
,
134 : event(true, false),
138 client_id(client_id
),
139 surface_id(surface_id
) {}
140 ~AllocateGpuMemoryBufferRequest() {}
141 base::WaitableEvent event
;
143 gfx::BufferFormat format
;
144 gfx::BufferUsage usage
;
147 scoped_ptr
<gfx::GpuMemoryBuffer
> result
;
150 BrowserGpuMemoryBufferManager::BrowserGpuMemoryBufferManager(int gpu_client_id
)
151 : factory_type_(GetGpuMemoryBufferFactoryType()),
152 supported_configurations_(
153 GetSupportedGpuMemoryBufferConfigurations(factory_type_
)),
154 gpu_client_id_(gpu_client_id
),
156 DCHECK(!g_gpu_memory_buffer_manager
);
157 g_gpu_memory_buffer_manager
= this;
160 BrowserGpuMemoryBufferManager::~BrowserGpuMemoryBufferManager() {
161 g_gpu_memory_buffer_manager
= nullptr;
165 BrowserGpuMemoryBufferManager
* BrowserGpuMemoryBufferManager::current() {
166 return g_gpu_memory_buffer_manager
;
170 uint32
BrowserGpuMemoryBufferManager::GetImageTextureTarget(
171 gfx::BufferFormat format
,
172 gfx::BufferUsage usage
) {
173 gfx::GpuMemoryBufferType type
= GetGpuMemoryBufferFactoryType();
174 for (auto& configuration
: GetSupportedGpuMemoryBufferConfigurations(type
)) {
175 if (configuration
.format
!= format
|| configuration
.usage
!= usage
)
179 case gfx::SURFACE_TEXTURE_BUFFER
:
180 case gfx::OZONE_NATIVE_PIXMAP
:
181 // GPU memory buffers that are shared with the GL using EGLImages
182 // require TEXTURE_EXTERNAL_OES.
183 return GL_TEXTURE_EXTERNAL_OES
;
184 case gfx::IO_SURFACE_BUFFER
:
185 // IOSurface backed images require GL_TEXTURE_RECTANGLE_ARB.
186 return GL_TEXTURE_RECTANGLE_ARB
;
188 return GL_TEXTURE_2D
;
192 return GL_TEXTURE_2D
;
195 scoped_ptr
<gfx::GpuMemoryBuffer
>
196 BrowserGpuMemoryBufferManager::AllocateGpuMemoryBuffer(const gfx::Size
& size
,
197 gfx::BufferFormat format
,
198 gfx::BufferUsage usage
) {
199 return AllocateGpuMemoryBufferForSurface(size
, format
, usage
, 0);
202 scoped_ptr
<gfx::GpuMemoryBuffer
>
203 BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForScanout(
204 const gfx::Size
& size
,
205 gfx::BufferFormat format
,
207 DCHECK_GT(surface_id
, 0);
208 return AllocateGpuMemoryBufferForSurface(
209 size
, format
, gfx::BufferUsage::SCANOUT
, surface_id
);
212 void BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForChildProcess(
213 const gfx::Size
& size
,
214 gfx::BufferFormat format
,
215 gfx::BufferUsage usage
,
216 base::ProcessHandle child_process_handle
,
218 const AllocationCallback
& callback
) {
219 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
221 gfx::GpuMemoryBufferId new_id
= g_next_gpu_memory_buffer_id
.GetNext();
223 // Use service side allocation if this is a supported configuration.
224 if (IsGpuMemoryBufferConfigurationSupported(format
, usage
)) {
225 AllocateGpuMemoryBufferOnIO(new_id
, size
, format
, usage
, child_client_id
, 0,
230 // Early out if we cannot fallback to shared memory buffer.
231 if (!GpuMemoryBufferImplSharedMemory::IsFormatSupported(format
) ||
232 !GpuMemoryBufferImplSharedMemory::IsUsageSupported(usage
) ||
233 !GpuMemoryBufferImplSharedMemory::IsSizeValidForFormat(size
, format
)) {
234 callback
.Run(gfx::GpuMemoryBufferHandle());
238 BufferMap
& buffers
= clients_
[child_client_id
];
239 DCHECK(buffers
.find(new_id
) == buffers
.end());
241 // Allocate shared memory buffer as fallback.
243 BufferInfo(size
, gfx::SHARED_MEMORY_BUFFER
, format
, usage
, 0);
244 callback
.Run(GpuMemoryBufferImplSharedMemory::AllocateForChildProcess(
245 new_id
, size
, format
, child_process_handle
));
248 gfx::GpuMemoryBuffer
*
249 BrowserGpuMemoryBufferManager::GpuMemoryBufferFromClientBuffer(
250 ClientBuffer buffer
) {
251 return GpuMemoryBufferImpl::FromClientBuffer(buffer
);
254 void BrowserGpuMemoryBufferManager::SetDestructionSyncPoint(
255 gfx::GpuMemoryBuffer
* buffer
,
257 static_cast<GpuMemoryBufferImpl
*>(buffer
)
258 ->set_destruction_sync_point(sync_point
);
261 bool BrowserGpuMemoryBufferManager::OnMemoryDump(
262 base::trace_event::ProcessMemoryDump
* pmd
) {
263 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
265 for (const auto& client
: clients_
) {
266 int client_id
= client
.first
;
268 for (const auto& buffer
: client
.second
) {
269 if (buffer
.second
.type
== gfx::EMPTY_BUFFER
)
272 gfx::GpuMemoryBufferId buffer_id
= buffer
.first
;
273 base::trace_event::MemoryAllocatorDump
* dump
=
274 pmd
->CreateAllocatorDump(base::StringPrintf(
275 "gpumemorybuffer/client_%d/buffer_%d", client_id
, buffer_id
));
279 size_t buffer_size_in_bytes
= 0;
280 // Note: BufferSizeInBytes returns an approximated size for the buffer
281 // but the factory can be made to return the exact size if this
282 // approximation is not good enough.
283 bool valid_size
= GpuMemoryBufferImpl::BufferSizeInBytes(
284 buffer
.second
.size
, buffer
.second
.format
, &buffer_size_in_bytes
);
287 dump
->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize
,
288 base::trace_event::MemoryAllocatorDump::kUnitsBytes
,
289 buffer_size_in_bytes
);
291 // Create the cross-process ownership edge. If the client creates a
292 // corresponding dump for the same buffer, this will avoid to
293 // double-count them in tracing. If, instead, no other process will emit a
294 // dump with the same guid, the segment will be accounted to the browser.
295 const uint64 client_tracing_process_id
=
296 ChildProcessHostImpl::ChildProcessUniqueIdToTracingProcessId(
298 base::trace_event::MemoryAllocatorDumpGuid shared_buffer_guid
=
299 gfx::GetGpuMemoryBufferGUIDForTracing(client_tracing_process_id
,
301 pmd
->CreateSharedGlobalAllocatorDump(shared_buffer_guid
);
302 pmd
->AddOwnershipEdge(dump
->guid(), shared_buffer_guid
);
309 void BrowserGpuMemoryBufferManager::ChildProcessDeletedGpuMemoryBuffer(
310 gfx::GpuMemoryBufferId id
,
311 base::ProcessHandle child_process_handle
,
314 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
316 DestroyGpuMemoryBufferOnIO(id
, child_client_id
, sync_point
);
319 void BrowserGpuMemoryBufferManager::ProcessRemoved(
320 base::ProcessHandle process_handle
,
322 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
324 ClientMap::iterator client_it
= clients_
.find(client_id
);
325 if (client_it
== clients_
.end())
328 for (const auto& buffer
: client_it
->second
) {
329 // This might happen if buffer is currenlty in the process of being
330 // allocated. The buffer will in that case be cleaned up when allocation
332 if (buffer
.second
.type
== gfx::EMPTY_BUFFER
)
335 GpuProcessHost
* host
= GpuProcessHost::FromID(buffer
.second
.gpu_host_id
);
337 host
->DestroyGpuMemoryBuffer(buffer
.first
, client_id
, 0);
340 clients_
.erase(client_it
);
343 scoped_ptr
<gfx::GpuMemoryBuffer
>
344 BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForSurface(
345 const gfx::Size
& size
,
346 gfx::BufferFormat format
,
347 gfx::BufferUsage usage
,
349 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::IO
));
351 AllocateGpuMemoryBufferRequest
request(size
, format
, usage
, gpu_client_id_
,
353 BrowserThread::PostTask(
354 BrowserThread::IO
, FROM_HERE
,
356 &BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForSurfaceOnIO
,
357 base::Unretained(this), // Safe as we wait for result below.
358 base::Unretained(&request
)));
360 // We're blocking the UI thread, which is generally undesirable.
363 "BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForSurface");
364 base::ThreadRestrictions::ScopedAllowWait allow_wait
;
365 request
.event
.Wait();
366 return request
.result
.Pass();
369 bool BrowserGpuMemoryBufferManager::IsGpuMemoryBufferConfigurationSupported(
370 gfx::BufferFormat format
,
371 gfx::BufferUsage usage
) const {
372 for (auto& configuration
: supported_configurations_
) {
373 if (configuration
.format
== format
&& configuration
.usage
== usage
)
379 void BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferForSurfaceOnIO(
380 AllocateGpuMemoryBufferRequest
* request
) {
381 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
383 gfx::GpuMemoryBufferId new_id
= g_next_gpu_memory_buffer_id
.GetNext();
385 // Use service side allocation if this is a supported configuration.
386 if (IsGpuMemoryBufferConfigurationSupported(request
->format
,
388 // Note: Unretained is safe as this is only used for synchronous allocation
389 // from a non-IO thread.
390 AllocateGpuMemoryBufferOnIO(
391 new_id
, request
->size
, request
->format
, request
->usage
,
392 request
->client_id
, request
->surface_id
, false,
393 base::Bind(&BrowserGpuMemoryBufferManager::
394 GpuMemoryBufferAllocatedForSurfaceOnIO
,
395 base::Unretained(this), base::Unretained(request
)));
399 DCHECK(GpuMemoryBufferImplSharedMemory::IsFormatSupported(request
->format
))
400 << static_cast<int>(request
->format
);
401 DCHECK(GpuMemoryBufferImplSharedMemory::IsUsageSupported(request
->usage
))
402 << static_cast<int>(request
->usage
);
404 BufferMap
& buffers
= clients_
[request
->client_id
];
405 DCHECK(buffers
.find(new_id
) == buffers
.end());
407 // Allocate shared memory buffer as fallback.
408 buffers
[new_id
] = BufferInfo(request
->size
, gfx::SHARED_MEMORY_BUFFER
,
409 request
->format
, request
->usage
, 0);
410 // Note: Unretained is safe as IO thread is stopped before manager is
412 request
->result
= GpuMemoryBufferImplSharedMemory::Create(
413 new_id
, request
->size
, request
->format
,
415 &GpuMemoryBufferDeleted
,
416 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO
),
417 base::Bind(&BrowserGpuMemoryBufferManager::DestroyGpuMemoryBufferOnIO
,
418 base::Unretained(this), new_id
, request
->client_id
)));
419 request
->event
.Signal();
422 void BrowserGpuMemoryBufferManager::GpuMemoryBufferAllocatedForSurfaceOnIO(
423 AllocateGpuMemoryBufferRequest
* request
,
424 const gfx::GpuMemoryBufferHandle
& handle
) {
425 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
427 // Early out if factory failed to allocate the buffer.
428 if (handle
.is_null()) {
429 request
->event
.Signal();
433 // Note: Unretained is safe as IO thread is stopped before manager is
435 request
->result
= GpuMemoryBufferImpl::CreateFromHandle(
436 handle
, request
->size
, request
->format
, request
->usage
,
438 &GpuMemoryBufferDeleted
,
439 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO
),
440 base::Bind(&BrowserGpuMemoryBufferManager::DestroyGpuMemoryBufferOnIO
,
441 base::Unretained(this), handle
.id
, request
->client_id
)));
442 request
->event
.Signal();
445 void BrowserGpuMemoryBufferManager::AllocateGpuMemoryBufferOnIO(
446 gfx::GpuMemoryBufferId id
,
447 const gfx::Size
& size
,
448 gfx::BufferFormat format
,
449 gfx::BufferUsage usage
,
452 bool reused_gpu_process
,
453 const AllocationCallback
& callback
) {
454 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
456 BufferMap
& buffers
= clients_
[client_id
];
457 DCHECK(buffers
.find(id
) == buffers
.end());
459 GpuProcessHost
* host
= GpuProcessHost::FromID(gpu_host_id_
);
461 host
= GpuProcessHost::Get(GpuProcessHost::GPU_PROCESS_KIND_SANDBOXED
,
462 CAUSE_FOR_GPU_LAUNCH_GPU_MEMORY_BUFFER_ALLOCATE
);
464 LOG(ERROR
) << "Failed to launch GPU process.";
465 callback
.Run(gfx::GpuMemoryBufferHandle());
468 gpu_host_id_
= host
->host_id();
469 reused_gpu_process
= false;
471 if (reused_gpu_process
) {
472 // We come here if we retried to allocate the buffer because of a
473 // failure in GpuMemoryBufferAllocatedOnIO, but we ended up with the
474 // same process ID, meaning the failure was not because of a channel
475 // error, but another reason. So fail now.
476 LOG(ERROR
) << "Failed to allocate GpuMemoryBuffer.";
477 callback
.Run(gfx::GpuMemoryBufferHandle());
480 reused_gpu_process
= true;
483 // Note: Handling of cases where the client is removed before the allocation
484 // completes is less subtle if we set the buffer type to EMPTY_BUFFER here
485 // and verify that this has not changed when allocation completes.
486 buffers
[id
] = BufferInfo(size
, gfx::EMPTY_BUFFER
, format
, usage
, 0);
488 // Note: Unretained is safe as IO thread is stopped before manager is
490 host
->CreateGpuMemoryBuffer(
491 id
, size
, format
, usage
, client_id
, surface_id
,
492 base::Bind(&BrowserGpuMemoryBufferManager::GpuMemoryBufferAllocatedOnIO
,
493 base::Unretained(this), id
, client_id
, surface_id
,
494 gpu_host_id_
, reused_gpu_process
, callback
));
497 void BrowserGpuMemoryBufferManager::GpuMemoryBufferAllocatedOnIO(
498 gfx::GpuMemoryBufferId id
,
502 bool reused_gpu_process
,
503 const AllocationCallback
& callback
,
504 const gfx::GpuMemoryBufferHandle
& handle
) {
505 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
507 ClientMap::iterator client_it
= clients_
.find(client_id
);
509 // This can happen if client is removed while the buffer is being allocated.
510 if (client_it
== clients_
.end()) {
511 if (!handle
.is_null()) {
512 GpuProcessHost
* host
= GpuProcessHost::FromID(gpu_host_id
);
514 host
->DestroyGpuMemoryBuffer(handle
.id
, client_id
, 0);
516 callback
.Run(gfx::GpuMemoryBufferHandle());
520 BufferMap
& buffers
= client_it
->second
;
522 BufferMap::iterator buffer_it
= buffers
.find(id
);
523 DCHECK(buffer_it
!= buffers
.end());
524 DCHECK_EQ(buffer_it
->second
.type
, gfx::EMPTY_BUFFER
);
526 // If the handle isn't valid, that means that the GPU process crashed or is
528 bool valid_handle
= !handle
.is_null() && handle
.id
== id
;
530 // If we failed after re-using the GPU process, it may have died in the
531 // mean time. Retry to have a chance to create a fresh GPU process.
532 if (handle
.is_null() && reused_gpu_process
) {
533 DVLOG(1) << "Failed to create buffer through existing GPU process. "
534 "Trying to restart GPU process.";
535 // If the GPU process has already been restarted, retry without failure
536 // when GPU process host ID already exists.
537 if (gpu_host_id
!= gpu_host_id_
)
538 reused_gpu_process
= false;
539 gfx::Size size
= buffer_it
->second
.size
;
540 gfx::BufferFormat format
= buffer_it
->second
.format
;
541 gfx::BufferUsage usage
= buffer_it
->second
.usage
;
542 // Remove the buffer entry and call AllocateGpuMemoryBufferOnIO again.
543 buffers
.erase(buffer_it
);
544 AllocateGpuMemoryBufferOnIO(id
, size
, format
, usage
, client_id
,
545 surface_id
, reused_gpu_process
, callback
);
547 // Remove the buffer entry and run the allocation callback with an empty
548 // handle to indicate failure.
549 buffers
.erase(buffer_it
);
550 callback
.Run(gfx::GpuMemoryBufferHandle());
555 // Store the type and host id of this buffer so it can be cleaned up if the
556 // client is removed.
557 buffer_it
->second
.type
= handle
.type
;
558 buffer_it
->second
.gpu_host_id
= gpu_host_id
;
560 callback
.Run(handle
);
563 void BrowserGpuMemoryBufferManager::DestroyGpuMemoryBufferOnIO(
564 gfx::GpuMemoryBufferId id
,
567 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
568 DCHECK(clients_
.find(client_id
) != clients_
.end());
570 BufferMap
& buffers
= clients_
[client_id
];
572 BufferMap::iterator buffer_it
= buffers
.find(id
);
573 if (buffer_it
== buffers
.end()) {
574 LOG(ERROR
) << "Invalid GpuMemoryBuffer ID for client.";
578 // This can happen if a client managed to call this while a buffer is in the
579 // process of being allocated.
580 if (buffer_it
->second
.type
== gfx::EMPTY_BUFFER
) {
581 LOG(ERROR
) << "Invalid GpuMemoryBuffer type.";
585 GpuProcessHost
* host
= GpuProcessHost::FromID(buffer_it
->second
.gpu_host_id
);
587 host
->DestroyGpuMemoryBuffer(id
, client_id
, sync_point
);
589 buffers
.erase(buffer_it
);
592 } // namespace content