blob: 8e07640136ab5484654b192d14a7b265f62a58ac [file] [log] [blame]
//
// Copyright 2023 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// MemoryTracking.cpp:
// Implements the class methods in MemoryTracking.h.
//
#include "libANGLE/renderer/vulkan/MemoryTracking.h"
#include "common/debug.h"
#include "libANGLE/renderer/vulkan/vk_renderer.h"
// Consts
namespace
{
// This flag is used for memory allocation tracking using allocation size counters.
constexpr bool kTrackMemoryAllocationSizes = true;
#if defined(ANGLE_ENABLE_MEMORY_ALLOC_LOGGING)
// Flag used for logging memory allocations and deallocations.
constexpr bool kTrackMemoryAllocationDebug = true;
static_assert(kTrackMemoryAllocationSizes,
"kTrackMemoryAllocationSizes must be enabled to use kTrackMemoryAllocationDebug.");
#else
// Only the allocation size counters are used (if enabled).
constexpr bool kTrackMemoryAllocationDebug = false;
#endif
} // namespace
namespace rx
{
namespace
{
// Output memory log stream based on level of severity.
void OutputMemoryLogStream(std::stringstream &outStream, vk::MemoryLogSeverity severity)
{
if (!kTrackMemoryAllocationSizes)
{
return;
}
switch (severity)
{
case vk::MemoryLogSeverity::INFO:
INFO() << outStream.str();
break;
case vk::MemoryLogSeverity::WARN:
WARN() << outStream.str();
break;
default:
UNREACHABLE();
break;
}
}
// Check for currently allocated memory. It is used at the end of the renderer object and when
// there is an allocation error (from ANGLE_VK_TRY()).
void CheckForCurrentMemoryAllocations(vk::Renderer *renderer, vk::MemoryLogSeverity severity)
{
if (kTrackMemoryAllocationSizes)
{
for (uint32_t i = 0; i < vk::kMemoryAllocationTypeCount; i++)
{
if (renderer->getMemoryAllocationTracker()->getActiveMemoryAllocationsSize(i) == 0)
{
continue;
}
std::stringstream outStream;
outStream << "Currently allocated size for memory allocation type ("
<< vk::kMemoryAllocationTypeMessage[i] << "): "
<< renderer->getMemoryAllocationTracker()->getActiveMemoryAllocationsSize(i)
<< " | Count: "
<< renderer->getMemoryAllocationTracker()->getActiveMemoryAllocationsCount(i)
<< std::endl;
for (uint32_t heapIndex = 0;
heapIndex < renderer->getMemoryProperties().getMemoryHeapCount(); heapIndex++)
{
outStream
<< "--> Heap index " << heapIndex << ": "
<< renderer->getMemoryAllocationTracker()->getActiveHeapMemoryAllocationsSize(
i, heapIndex)
<< " | Count: "
<< renderer->getMemoryAllocationTracker()->getActiveHeapMemoryAllocationsCount(
i, heapIndex)
<< std::endl;
}
// Output the log stream based on the level of severity.
OutputMemoryLogStream(outStream, severity);
}
}
}
// In case of an allocation error, log pending memory allocation if the size in non-zero.
void LogPendingMemoryAllocation(vk::Renderer *renderer, vk::MemoryLogSeverity severity)
{
if (!kTrackMemoryAllocationSizes)
{
return;
}
vk::MemoryAllocationType allocInfo =
renderer->getMemoryAllocationTracker()->getPendingMemoryAllocationType();
VkDeviceSize allocSize =
renderer->getMemoryAllocationTracker()->getPendingMemoryAllocationSize();
uint32_t memoryTypeIndex = renderer->getMemoryAllocationTracker()->getPendingMemoryTypeIndex();
uint32_t memoryHeapIndex =
renderer->getMemoryProperties().getHeapIndexForMemoryType(memoryTypeIndex);
if (allocSize != 0)
{
std::stringstream outStream;
outStream << "Pending allocation size for memory allocation type ("
<< vk::kMemoryAllocationTypeMessage[ToUnderlying(allocInfo)]
<< ") for heap index " << memoryHeapIndex << " (type index " << memoryTypeIndex
<< "): " << allocSize;
// Output the log stream based on the level of severity.
OutputMemoryLogStream(outStream, severity);
}
}
void LogMemoryHeapStats(vk::Renderer *renderer, vk::MemoryLogSeverity severity)
{
if (!kTrackMemoryAllocationSizes)
{
return;
}
// Log stream for the heap information.
std::stringstream outStream;
// VkPhysicalDeviceMemoryProperties2 enables the use of memory budget properties if
// supported.
VkPhysicalDeviceMemoryProperties2KHR memoryProperties;
memoryProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2_KHR;
memoryProperties.pNext = nullptr;
VkPhysicalDeviceMemoryBudgetPropertiesEXT memoryBudgetProperties;
memoryBudgetProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT;
memoryBudgetProperties.pNext = nullptr;
if (renderer->getFeatures().supportsMemoryBudget.enabled)
{
vk::AddToPNextChain(&memoryProperties, &memoryBudgetProperties);
}
vkGetPhysicalDeviceMemoryProperties2(renderer->getPhysicalDevice(), &memoryProperties);
// Add memory heap information to the stream.
outStream << "Memory heap info" << std::endl;
outStream << std::endl << "* Available memory heaps:" << std::endl;
for (uint32_t i = 0; i < memoryProperties.memoryProperties.memoryHeapCount; i++)
{
outStream << std::dec << i
<< " | Heap size: " << memoryProperties.memoryProperties.memoryHeaps[i].size
<< " | Flags: 0x" << std::hex
<< memoryProperties.memoryProperties.memoryHeaps[i].flags << std::endl;
}
if (renderer->getFeatures().supportsMemoryBudget.enabled)
{
outStream << std::endl << "* Available memory budget and usage per heap:" << std::endl;
for (uint32_t i = 0; i < memoryProperties.memoryProperties.memoryHeapCount; i++)
{
outStream << std::dec << i << " | Heap budget: " << memoryBudgetProperties.heapBudget[i]
<< " | Heap usage: " << memoryBudgetProperties.heapUsage[i] << std::endl;
}
}
outStream << std::endl << "* Available memory types:" << std::endl;
for (uint32_t i = 0; i < memoryProperties.memoryProperties.memoryTypeCount; i++)
{
outStream << std::dec << i
<< " | Heap index: " << memoryProperties.memoryProperties.memoryTypes[i].heapIndex
<< " | Property flags: 0x" << std::hex
<< memoryProperties.memoryProperties.memoryTypes[i].propertyFlags << std::endl;
}
// Output the log stream based on the level of severity.
OutputMemoryLogStream(outStream, severity);
}
} // namespace
MemoryAllocationTracker::MemoryAllocationTracker(vk::Renderer *renderer)
: mRenderer(renderer), mMemoryAllocationID(0)
{}
void MemoryAllocationTracker::initMemoryTrackers()
{
// Allocation counters are initialized here to keep track of the size and count of the memory
// allocations.
for (size_t allocTypeIndex = 0; allocTypeIndex < mActiveMemoryAllocationsSize.size();
allocTypeIndex++)
{
mActiveMemoryAllocationsSize[allocTypeIndex] = 0;
mActiveMemoryAllocationsCount[allocTypeIndex] = 0;
// Per-heap allocation counters are initialized here.
for (size_t heapIndex = 0;
heapIndex < mRenderer->getMemoryProperties().getMemoryHeapCount(); heapIndex++)
{
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][heapIndex] = 0;
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][heapIndex] = 0;
}
}
resetPendingMemoryAlloc();
}
void MemoryAllocationTracker::onDestroy()
{
if (kTrackMemoryAllocationDebug)
{
CheckForCurrentMemoryAllocations(mRenderer, vk::MemoryLogSeverity::INFO);
}
}
void MemoryAllocationTracker::onDeviceInit()
{
if (kTrackMemoryAllocationDebug)
{
LogMemoryHeapStats(mRenderer, vk::MemoryLogSeverity::INFO);
}
}
void MemoryAllocationTracker::logMemoryStatsOnError()
{
CheckForCurrentMemoryAllocations(mRenderer, vk::MemoryLogSeverity::WARN);
LogPendingMemoryAllocation(mRenderer, vk::MemoryLogSeverity::WARN);
LogMemoryHeapStats(mRenderer, vk::MemoryLogSeverity::WARN);
}
void MemoryAllocationTracker::onMemoryAllocImpl(vk::MemoryAllocationType allocType,
VkDeviceSize size,
uint32_t memoryTypeIndex,
void *handle)
{
ASSERT(allocType != vk::MemoryAllocationType::InvalidEnum && size != 0);
if (kTrackMemoryAllocationDebug)
{
// If enabled (debug layers), we keep more details in the memory tracker, such as handle,
// and log the action to the output.
std::unique_lock<angle::SimpleMutex> lock(mMemoryAllocationMutex);
uint32_t allocTypeIndex = ToUnderlying(allocType);
uint32_t memoryHeapIndex =
mRenderer->getMemoryProperties().getHeapIndexForMemoryType(memoryTypeIndex);
mActiveMemoryAllocationsCount[allocTypeIndex]++;
mActiveMemoryAllocationsSize[allocTypeIndex] += size;
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][memoryHeapIndex]++;
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex] += size;
// Add the new allocation to the memory tracker.
vk::MemoryAllocationInfo memAllocLogInfo;
memAllocLogInfo.id = ++mMemoryAllocationID;
memAllocLogInfo.allocType = allocType;
memAllocLogInfo.memoryHeapIndex = memoryHeapIndex;
memAllocLogInfo.size = size;
memAllocLogInfo.handle = handle;
vk::MemoryAllocInfoMapKey memoryAllocInfoMapKey(memAllocLogInfo.handle);
mMemoryAllocationRecord[angle::getBacktraceInfo()].insert(
std::make_pair(memoryAllocInfoMapKey, memAllocLogInfo));
INFO() << "Memory allocation: (id " << memAllocLogInfo.id << ") for object "
<< memAllocLogInfo.handle << " | Size: " << memAllocLogInfo.size
<< " | Type: " << vk::kMemoryAllocationTypeMessage[allocTypeIndex]
<< " | Memory type index: " << memoryTypeIndex
<< " | Heap index: " << memAllocLogInfo.memoryHeapIndex;
resetPendingMemoryAlloc();
}
else if (kTrackMemoryAllocationSizes)
{
// Add the new allocation size to the allocation counter.
uint32_t allocTypeIndex = ToUnderlying(allocType);
mActiveMemoryAllocationsCount[allocTypeIndex]++;
mActiveMemoryAllocationsSize[allocTypeIndex] += size;
uint32_t memoryHeapIndex =
mRenderer->getMemoryProperties().getHeapIndexForMemoryType(memoryTypeIndex);
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][memoryHeapIndex].fetch_add(
1, std::memory_order_relaxed);
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex].fetch_add(
size, std::memory_order_relaxed);
resetPendingMemoryAlloc();
}
}
void MemoryAllocationTracker::onMemoryDeallocImpl(vk::MemoryAllocationType allocType,
VkDeviceSize size,
uint32_t memoryTypeIndex,
void *handle)
{
ASSERT(allocType != vk::MemoryAllocationType::InvalidEnum && size != 0);
if (kTrackMemoryAllocationDebug)
{
// If enabled (debug layers), we keep more details in the memory tracker, such as handle,
// and log the action to the output. The memory allocation tracker uses the backtrace info
// as key, if available.
for (auto &memInfoPerBacktrace : mMemoryAllocationRecord)
{
vk::MemoryAllocInfoMapKey memoryAllocInfoMapKey(handle);
MemoryAllocInfoMap &memInfoMap = memInfoPerBacktrace.second;
std::unique_lock<angle::SimpleMutex> lock(mMemoryAllocationMutex);
if (memInfoMap.find(memoryAllocInfoMapKey) != memInfoMap.end())
{
// Object found; remove it from the allocation tracker.
vk::MemoryAllocationInfo *memInfoEntry = &memInfoMap[memoryAllocInfoMapKey];
ASSERT(memInfoEntry->allocType == allocType && memInfoEntry->size == size);
uint32_t allocTypeIndex = ToUnderlying(memInfoEntry->allocType);
uint32_t memoryHeapIndex =
mRenderer->getMemoryProperties().getHeapIndexForMemoryType(memoryTypeIndex);
ASSERT(mActiveMemoryAllocationsCount[allocTypeIndex] != 0 &&
mActiveMemoryAllocationsSize[allocTypeIndex] >= size);
ASSERT(memoryHeapIndex == memInfoEntry->memoryHeapIndex &&
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][memoryHeapIndex] != 0 &&
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex] >=
size);
mActiveMemoryAllocationsCount[allocTypeIndex]--;
mActiveMemoryAllocationsSize[allocTypeIndex] -= size;
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][memoryHeapIndex]--;
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex] -= size;
INFO() << "Memory deallocation: (id " << memInfoEntry->id << ") for object "
<< memInfoEntry->handle << " | Size: " << memInfoEntry->size
<< " | Type: " << vk::kMemoryAllocationTypeMessage[allocTypeIndex]
<< " | Memory type index: " << memoryTypeIndex
<< " | Heap index: " << memInfoEntry->memoryHeapIndex;
memInfoMap.erase(memoryAllocInfoMapKey);
}
}
}
else if (kTrackMemoryAllocationSizes)
{
// Remove the allocation size from the allocation counter.
uint32_t allocTypeIndex = ToUnderlying(allocType);
ASSERT(mActiveMemoryAllocationsCount[allocTypeIndex] != 0 &&
mActiveMemoryAllocationsSize[allocTypeIndex] >= size);
mActiveMemoryAllocationsCount[allocTypeIndex]--;
mActiveMemoryAllocationsSize[allocTypeIndex] -= size;
uint32_t memoryHeapIndex =
mRenderer->getMemoryProperties().getHeapIndexForMemoryType(memoryTypeIndex);
ASSERT(mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex] >= size);
mActivePerHeapMemoryAllocationsCount[allocTypeIndex][memoryHeapIndex].fetch_add(
-1, std::memory_order_relaxed);
mActivePerHeapMemoryAllocationsSize[allocTypeIndex][memoryHeapIndex].fetch_add(
-size, std::memory_order_relaxed);
}
}
VkDeviceSize MemoryAllocationTracker::getActiveMemoryAllocationsSize(uint32_t allocTypeIndex) const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
ASSERT(allocTypeIndex < vk::kMemoryAllocationTypeCount);
return mActiveMemoryAllocationsSize[allocTypeIndex];
}
VkDeviceSize MemoryAllocationTracker::getActiveHeapMemoryAllocationsSize(uint32_t allocTypeIndex,
uint32_t heapIndex) const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
ASSERT(allocTypeIndex < vk::kMemoryAllocationTypeCount &&
heapIndex < mRenderer->getMemoryProperties().getMemoryHeapCount());
return mActivePerHeapMemoryAllocationsSize[allocTypeIndex][heapIndex];
}
uint64_t MemoryAllocationTracker::getActiveMemoryAllocationsCount(uint32_t allocTypeIndex) const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
ASSERT(allocTypeIndex < vk::kMemoryAllocationTypeCount);
return mActiveMemoryAllocationsCount[allocTypeIndex];
}
uint64_t MemoryAllocationTracker::getActiveHeapMemoryAllocationsCount(uint32_t allocTypeIndex,
uint32_t heapIndex) const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
ASSERT(allocTypeIndex < vk::kMemoryAllocationTypeCount &&
heapIndex < mRenderer->getMemoryProperties().getMemoryHeapCount());
return mActivePerHeapMemoryAllocationsCount[allocTypeIndex][heapIndex];
}
void MemoryAllocationTracker::compareExpectedFlagsWithAllocatedFlags(
VkMemoryPropertyFlags requiredFlags,
VkMemoryPropertyFlags preferredFlags,
VkMemoryPropertyFlags allocatedFlags,
void *handle)
{
if (!kTrackMemoryAllocationDebug)
{
return;
}
ASSERT((requiredFlags & ~allocatedFlags) == 0);
if (((preferredFlags | requiredFlags) & ~allocatedFlags) != 0)
{
INFO() << "Memory type index chosen for object " << handle
<< " lacks some of the preferred property flags.";
}
if ((~allocatedFlags & preferredFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0)
{
WARN() << "Device-local memory allocation fallback to system memory.";
}
}
void MemoryAllocationTracker::onExceedingMaxMemoryAllocationSize(VkDeviceSize size)
{
VkDeviceSize maxAllocationSize = mRenderer->getMaxMemoryAllocationSize();
ASSERT(size > maxAllocationSize);
WARN() << "Attempted allocation size (" << size
<< ") is greater than the maximum allocation size allowed (" << maxAllocationSize
<< ").";
}
VkDeviceSize MemoryAllocationTracker::getPendingMemoryAllocationSize() const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
return mPendingMemoryAllocationSize;
}
vk::MemoryAllocationType MemoryAllocationTracker::getPendingMemoryAllocationType() const
{
if (!kTrackMemoryAllocationSizes)
{
return vk::MemoryAllocationType::Unspecified;
}
return mPendingMemoryAllocationType;
}
uint32_t MemoryAllocationTracker::getPendingMemoryTypeIndex() const
{
if (!kTrackMemoryAllocationSizes)
{
return 0;
}
return mPendingMemoryTypeIndex;
}
void MemoryAllocationTracker::setPendingMemoryAlloc(vk::MemoryAllocationType allocType,
VkDeviceSize size,
uint32_t memoryTypeIndex)
{
if (!kTrackMemoryAllocationSizes)
{
return;
}
ASSERT(allocType != vk::MemoryAllocationType::InvalidEnum && size != 0);
mPendingMemoryAllocationType = allocType;
mPendingMemoryAllocationSize = size;
mPendingMemoryTypeIndex = memoryTypeIndex;
}
void MemoryAllocationTracker::resetPendingMemoryAlloc()
{
if (!kTrackMemoryAllocationSizes)
{
return;
}
mPendingMemoryAllocationType = vk::MemoryAllocationType::Unspecified;
mPendingMemoryAllocationSize = 0;
mPendingMemoryTypeIndex = kInvalidMemoryTypeIndex;
}
namespace vk
{
MemoryReport::MemoryReport()
: mCurrentTotalAllocatedMemory(0),
mMaxTotalAllocatedMemory(0),
mCurrentTotalImportedMemory(0),
mMaxTotalImportedMemory(0)
{}
void MemoryReport::processCallback(const VkDeviceMemoryReportCallbackDataEXT &callbackData,
bool logCallback)
{
std::unique_lock<angle::SimpleMutex> lock(mMemoryReportMutex);
VkDeviceSize size = 0;
std::string reportType;
switch (callbackData.type)
{
case VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT:
reportType = "Allocate";
if ((mUniqueIDCounts[callbackData.memoryObjectId] += 1) > 1)
{
break;
}
size = mSizesPerType[callbackData.objectType].allocatedMemory + callbackData.size;
mSizesPerType[callbackData.objectType].allocatedMemory = size;
if (mSizesPerType[callbackData.objectType].allocatedMemoryMax < size)
{
mSizesPerType[callbackData.objectType].allocatedMemoryMax = size;
}
mCurrentTotalAllocatedMemory += callbackData.size;
if (mMaxTotalAllocatedMemory < mCurrentTotalAllocatedMemory)
{
mMaxTotalAllocatedMemory = mCurrentTotalAllocatedMemory;
}
break;
case VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT:
reportType = "Free";
ASSERT(mUniqueIDCounts[callbackData.memoryObjectId] > 0);
mUniqueIDCounts[callbackData.memoryObjectId] -= 1;
size = mSizesPerType[callbackData.objectType].allocatedMemory - callbackData.size;
mSizesPerType[callbackData.objectType].allocatedMemory = size;
mCurrentTotalAllocatedMemory -= callbackData.size;
break;
case VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_IMPORT_EXT:
reportType = "Import";
if ((mUniqueIDCounts[callbackData.memoryObjectId] += 1) > 1)
{
break;
}
size = mSizesPerType[callbackData.objectType].importedMemory + callbackData.size;
mSizesPerType[callbackData.objectType].importedMemory = size;
if (mSizesPerType[callbackData.objectType].importedMemoryMax < size)
{
mSizesPerType[callbackData.objectType].importedMemoryMax = size;
}
mCurrentTotalImportedMemory += callbackData.size;
if (mMaxTotalImportedMemory < mCurrentTotalImportedMemory)
{
mMaxTotalImportedMemory = mCurrentTotalImportedMemory;
}
break;
case VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT:
reportType = "Un-Import";
ASSERT(mUniqueIDCounts[callbackData.memoryObjectId] > 0);
mUniqueIDCounts[callbackData.memoryObjectId] -= 1;
size = mSizesPerType[callbackData.objectType].importedMemory - callbackData.size;
mSizesPerType[callbackData.objectType].importedMemory = size;
mCurrentTotalImportedMemory -= callbackData.size;
break;
case VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATION_FAILED_EXT:
reportType = "allocFail";
break;
default:
UNREACHABLE();
return;
}
if (logCallback)
{
INFO() << std::right << std::setw(9) << reportType << ": size=" << std::setw(10)
<< callbackData.size << "; type=" << std::setw(15) << std::left
<< Renderer::GetVulkanObjectTypeName(callbackData.objectType)
<< "; heapIdx=" << callbackData.heapIndex << "; id=" << std::hex
<< callbackData.memoryObjectId << "; handle=" << std::hex
<< callbackData.objectHandle << ": Total=" << std::right << std::setw(10) << std::dec
<< size;
}
}
void MemoryReport::logMemoryReportStats() const
{
std::unique_lock<angle::SimpleMutex> lock(mMemoryReportMutex);
INFO() << std::right << "GPU Memory Totals: Allocated=" << std::setw(10)
<< mCurrentTotalAllocatedMemory << " (max=" << std::setw(10) << mMaxTotalAllocatedMemory
<< "); Imported=" << std::setw(10) << mCurrentTotalImportedMemory
<< " (max=" << std::setw(10) << mMaxTotalImportedMemory << ")";
INFO() << "Sub-Totals per type:";
for (const auto &it : mSizesPerType)
{
VkObjectType objectType = it.first;
MemorySizes memorySizes = it.second;
VkDeviceSize allocatedMemory = memorySizes.allocatedMemory;
VkDeviceSize allocatedMemoryMax = memorySizes.allocatedMemoryMax;
VkDeviceSize importedMemory = memorySizes.importedMemory;
VkDeviceSize importedMemoryMax = memorySizes.importedMemoryMax;
INFO() << std::right << "- Type=" << std::setw(15)
<< Renderer::GetVulkanObjectTypeName(objectType) << ": Allocated=" << std::setw(10)
<< allocatedMemory << " (max=" << std::setw(10) << allocatedMemoryMax
<< "); Imported=" << std::setw(10) << importedMemory << " (max=" << std::setw(10)
<< importedMemoryMax << ")";
}
}
} // namespace vk
} // namespace rx