blob: 421858e19bc3a78b090b1f4c41aeb46c123a4ef2 [file] [log] [blame] [edit]
//
// Copyright 2025 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.
//
// frame_capture_binary_data.cpp:
// Common code for the ANGLE trace replay large trace binary data definition.
//
#ifdef UNSAFE_BUFFERS_BUILD
# pragma allow_unsafe_buffers
#endif
#define USE_SYSTEM_ZLIB
#include "compression_utils_portable.h"
#include "common/mathutil.h"
#include "frame_capture_binary_data.h"
#include <array>
#include <string>
namespace angle
{
// Return current size of all binary data
size_t FrameCaptureBinaryData::totalSize() const
{
return ((mBlockCount - 1) * mDataBlockSize) + mCurrentBlockOffset;
}
// Determine if any blocks have been saved to disk, i.e., if we have run out of resident
// blocks
bool FrameCaptureBinaryData::isSwapMode() const
{
return (mStoredBlocks > 0);
}
void FrameCaptureBinaryData::storeResidentBlocks()
{
// Write out all resident binary data blocks by calling storeBlock on each, deleting
// front() from vector
if (!isSwapMode())
{
while (mData.size() > 1)
{
storeBlock();
mData.erase(mData.begin());
}
}
storeBlock();
}
void FrameCaptureBinaryData::updateGetDataCache(size_t blockId)
{
const ReplayBlockDescription &desc = mReplayBlockDescriptions[blockId];
mCacheBlockId = blockId;
mCacheBlockBeginOffset = desc.beginDataOffset;
mCacheBlockEndOffset = desc.endDataOffset;
mCacheBlockBaseAddress = desc.residentAddress;
// The location for the swap block differs for load and store. For store it will ultimately be
// zero as it's unnecessary to utilize the full BinaryDataSize. For load, it will end up
// as the last of the resident blocks.
if (blockId >= mMaxResidentBlockIndex)
{
mCurrentTransientLoadedBlockId = blockId;
}
}
// Resident blocks will have a valid memory address at residentAddress
bool FrameCaptureBinaryData::isBlockResident(size_t blockId) const
{
return (mReplayBlockDescriptions[blockId].residentAddress != nullptr);
}
void FrameCaptureBinaryData::setBlockResident(size_t blockId, uint8_t *address)
{
mReplayBlockDescriptions[blockId].residentAddress = address;
}
void FrameCaptureBinaryData::setBlockNonResident(size_t blockId)
{
mReplayBlockDescriptions[blockId].residentAddress = nullptr;
}
void FrameCaptureBinaryData::setBlockSize(size_t blockSize)
{
if (!gl::isPow2(blockSize))
{
FATAL() << "Binary Data File Blocksize specified is not a power of 2: " << blockSize;
}
mDataBlockSize = blockSize;
}
void FrameCaptureBinaryData::setBinaryDataSize(size_t binaryDataSize)
{
if (!gl::isPow2(binaryDataSize))
{
FATAL() << "Binary Data File Binary Data Size specified is not a power of 2: "
<< binaryDataSize;
}
mMaxResidentBinarySize = binaryDataSize;
}
std::vector<uint8_t> &FrameCaptureBinaryData::prepareStoreBlock(size_t blockId)
{
// Ensure mData has enough vectors up to and including the target index
if (!isSwapMode())
{
mData.resize(mData.size() + 1);
}
mBlockCount = blockId + 1;
mData.back().resize(mDataBlockSize);
mCurrentBlockOffset = 0;
return mData.back();
}
std::vector<uint8_t> &FrameCaptureBinaryData::prepareLoadBlock(size_t blockId)
{
size_t destBlockIndex = std::min(blockId, mMaxResidentBlockIndex);
// Ensure mData has enough vectors up to the target index
if (destBlockIndex >= mData.size())
{
mData.resize(destBlockIndex + 1);
}
if (isSwapBlock(destBlockIndex))
{
// If not the same block, mark previous block occupying swap slot as non-resident
if (blockId != mCurrentTransientLoadedBlockId)
{
// Since this is the swap block, we aren't actually freeing any memory. But we need
// a way to indicate whether a transient block is loaded. This way each logical
// block knows whether it is resident, and where.
setBlockNonResident(mCurrentTransientLoadedBlockId);
}
// Track which logical block is now in the swap slot
mCurrentTransientLoadedBlockId = blockId;
}
mData.back().resize(mDataBlockSize);
mCurrentBlockOffset = 0;
return mData.back();
}
// Write file index entries to the end of compressed binary data files
BinaryFileIndexInfo FrameCaptureBinaryData::appendFileIndex()
{
BinaryFileIndexInfo indexInfo;
indexInfo.version = kLongTraceVersionId;
indexInfo.blockSize = mDataBlockSize;
indexInfo.blockCount = mBlockCount;
indexInfo.residentSize = mMaxResidentBinarySize;
indexInfo.indexOffset = 0;
if (mIsBinaryDataCompressed)
{
size_t indexDataOffset = mFileStream->getPosition();
// Copy index entries (index data trailer) to end of compressed data file
for (auto &entry : mFileIndex)
{
mFileStream->write(reinterpret_cast<const uint8_t *>(&entry), sizeof(FileBlockInfo));
}
indexInfo.indexOffset = indexDataOffset;
}
// Return index information for saving in JSON file
return indexInfo;
}
// Read in file index data from a compressed file and construct an access index
void FrameCaptureBinaryData::constructBlockDescIndex(size_t indexOffset)
{
if (mIsBinaryDataCompressed)
{
// Move to the beginning of the index data in the compressed file
mFileStream->seek(indexOffset, kSeekBegin);
// Populate the replay block description array
for (size_t i = 0; i < mBlockCount; i++)
{
// Read in block's information data
FileBlockInfo blockInfo;
mFileStream->read(reinterpret_cast<uint8_t *>(&blockInfo), sizeof(FileBlockInfo));
// Create and save a block description from the block information
ReplayBlockDescription blockDesc = {};
blockDesc.fileOffset = blockInfo.fileOffset;
blockDesc.beginDataOffset = blockInfo.dataOffset;
blockDesc.endDataOffset = blockInfo.dataOffset + blockInfo.dataSize - 1;
blockDesc.dataSize = blockInfo.dataSize;
mReplayBlockDescriptions.push_back(blockDesc);
}
}
else
{
// Create block descriptions from fixed size calculations
mFileStream->seek(0, kSeekEnd);
size_t size = mFileStream->getPosition();
mFileStream->seek(0, kSeekBegin);
size_t remaining = size;
while (remaining > 0)
{
// The final block is typically smaller than mDataBlockSize
size_t dataSize = std::min(remaining, mDataBlockSize);
size_t offset = size - remaining;
// Create and save a block description
ReplayBlockDescription blockDesc = {};
blockDesc.fileOffset = offset;
blockDesc.beginDataOffset = offset;
blockDesc.endDataOffset = offset + dataSize - 1;
blockDesc.dataSize = dataSize;
mReplayBlockDescriptions.push_back(blockDesc);
remaining -= dataSize;
}
}
}
size_t FrameCaptureBinaryData::append(const void *data, size_t size)
{
if (mData.empty())
{
prepareStoreBlock(0);
mBlockCount = 1;
}
ASSERT(totalSize() % kBinaryAlignment == 0);
size_t startingOffset = totalSize();
const size_t sizeToIncrease = rx::roundUpPow2(size, kBinaryAlignment);
// If the requested data size will not fit into the current block, allocate
// a new block
if (mCurrentBlockOffset + sizeToIncrease > mDataBlockSize)
{
size_t newBlockId = (startingOffset + sizeToIncrease) / mDataBlockSize;
if (!isSwapMode())
{
if (newBlockId > mMaxResidentBlockIndex)
{
// All resident blocks are full, store them to disk
storeResidentBlocks();
}
else
{
// Resident blocks available, no need to store to disk
}
}
else
{
// Resident blocks have been saved, write this block to disk
storeBlock();
}
prepareStoreBlock(newBlockId);
startingOffset = totalSize();
}
memcpy(mData.back().data() + mCurrentBlockOffset, data, size);
mCurrentBlockOffset += sizeToIncrease;
return startingOffset;
}
const uint8_t *FrameCaptureBinaryData::getData(size_t offset)
{
// This is the fastpath for this function, misses should be negligible
if (offset >= mCacheBlockBeginOffset && offset < mCacheBlockEndOffset)
{
return (mCacheBlockBaseAddress + (offset - mCacheBlockBeginOffset));
}
// Calculate new block id for binary data to be loaded
size_t newBlockId = offset / mDataBlockSize;
// Swap block into memory if it is nonresident
if (!isBlockResident(newBlockId))
{
loadBlock(newBlockId);
}
// Update the fastpath cache variables
updateGetDataCache(newBlockId);
return (mCacheBlockBaseAddress + (offset - mCacheBlockBeginOffset));
}
void FrameCaptureBinaryData::clear()
{
mCurrentBlockOffset = 0;
mFileIndex.clear();
mReplayBlockDescriptions.clear();
mData.clear();
}
// Helper class for compression/decompression operations
class ZLibHelper
{
// See the following file for details on these variables and helpers:
// https://chromium.googlesource.com/chromium/src/+/master/third_party/zlib/google/compression_utils_portable.cc
static constexpr int kZlibMemoryLevel = 8;
static constexpr int kWindowBitsToGetGzipHeader = 16;
public:
ZLibHelper(FrameCaptureBinaryData::Mode mode) : mMode(mode), mStream(), mInitialized(false)
{
int ret = 0;
mStream.zalloc = Z_NULL;
mStream.zfree = Z_NULL;
mStream.opaque = Z_NULL;
mStream.avail_in = 0;
mStream.next_in = Z_NULL;
if (mMode == FrameCaptureBinaryData::Mode::Load)
{
ret = inflateInit2(&mStream, MAX_WBITS + kWindowBitsToGetGzipHeader);
}
else if (mMode == FrameCaptureBinaryData::Mode::Store)
{
ret = deflateInit2(&mStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
MAX_WBITS + kWindowBitsToGetGzipHeader, kZlibMemoryLevel,
Z_DEFAULT_STRATEGY);
}
else
{
FATAL() << "Invalid Mode Enum in ZLibHelper";
}
if (ret != Z_OK)
{
FATAL() << "Zlib helper initialization failed: " << ret;
}
mInitialized = true;
}
~ZLibHelper()
{
if (mInitialized)
{
if (mMode == FrameCaptureBinaryData::Mode::Load)
{
inflateEnd(&mStream);
}
else if (mMode == FrameCaptureBinaryData::Mode::Store)
{
deflateEnd(&mStream);
}
else
{
FATAL() << "Invalid Mode Enum in ZLibHelper";
}
}
}
z_stream *getStream() { return &mStream; }
ZLibHelper(const ZLibHelper &) = delete;
ZLibHelper &operator=(const ZLibHelper &) = delete;
ZLibHelper(ZLibHelper &&) = delete;
ZLibHelper &operator=(ZLibHelper &&) = delete;
private:
FrameCaptureBinaryData::Mode mMode;
z_stream mStream;
bool mInitialized;
};
// Configure binary data output parameters and prepare file for writing
void FrameCaptureBinaryData::initializeBinaryDataStore(bool compression,
const std::string &outDir,
const std::string &fileName)
{
std::string binaryDataFileName = outDir + fileName;
mStoredBlocks = 0;
mIsBinaryDataCompressed = compression;
if ((mMaxResidentBinarySize / mDataBlockSize) <= 1)
{
FATAL() << "Error,insufficient resident memory specified or available";
}
mMaxResidentBlockIndex = (mMaxResidentBinarySize / mDataBlockSize) - 1;
mFileStream = new FileStream(binaryDataFileName, Mode::Store);
}
// Optionally compress and then write a single data block to disk
void FrameCaptureBinaryData::storeBlock()
{
std::vector<uint8_t> &storeBlock = mData.front();
// The last block to be saved will be resized to fit used data
if (mCaptureComplete && mData.size() == 1)
{
storeBlock.resize(mCurrentBlockOffset);
}
if (mIsBinaryDataCompressed)
{
// Use zlib library, based on example/doc here: https://zlib.net/zlib_how.html
ZLibHelper compressor(Mode::Store);
z_stream *zStream = compressor.getStream();
int deflateStatus = 0;
using ZlibBuffer = std::array<unsigned char, kZlibBufferSize>;
std::unique_ptr<ZlibBuffer> compressBuffer(new ZlibBuffer());
FileBlockInfo fileIndexEntry;
fileIndexEntry.fileOffset = mFileStream->getPosition(); // CompressedFileOffset
fileIndexEntry.dataOffset = mStoredBlocks * mDataBlockSize; // UncompressedOffset
fileIndexEntry.dataSize = storeBlock.size(); // Size of block
// Save file index data
mFileIndex.push_back(fileIndexEntry);
const unsigned char *uncompressedDataPtr = storeBlock.data();
size_t remainingBytesToCompress = storeBlock.size();
while (remainingBytesToCompress > 0)
{
size_t bytesToCompress =
std::min(remainingBytesToCompress, static_cast<size_t>(kZlibBufferSize));
zStream->avail_in = static_cast<uInt>(bytesToCompress);
zStream->next_in = const_cast<unsigned char *>(uncompressedDataPtr);
do
{
zStream->avail_out = kZlibBufferSize;
zStream->next_out = compressBuffer->data();
int flushMode = Z_NO_FLUSH;
if (remainingBytesToCompress <= kZlibBufferSize)
{
flushMode = Z_FINISH;
}
deflateStatus = deflate(zStream, flushMode);
if (deflateStatus == Z_STREAM_ERROR)
{
FATAL() << "Error during deflate: Z_STREAM_ERROR";
}
// This is the compressed data size about to be written
unsigned bytesCompressed = kZlibBufferSize - zStream->avail_out;
mFileStream->write(compressBuffer->data(), bytesCompressed);
} while (zStream->avail_out == 0);
uncompressedDataPtr += bytesToCompress;
remainingBytesToCompress -= bytesToCompress;
}
}
else
{
mFileStream->write(storeBlock.data(), storeBlock.size());
}
mStoredBlocks++;
}
BinaryFileIndexInfo FrameCaptureBinaryData::closeBinaryDataStore()
{
mCaptureComplete = true;
storeResidentBlocks();
BinaryFileIndexInfo indexInfo;
indexInfo = appendFileIndex();
clear();
return indexInfo;
}
// Sets up binary data loader with config data from the trace fixture
void FrameCaptureBinaryData::configureBinaryDataLoader(bool compression,
size_t blockCount,
size_t blockSize,
size_t residentSize,
size_t indexOffset,
const std::string &fileName)
{
mIsBinaryDataCompressed = compression;
mFileName = fileName;
mMaxResidentBinarySize = residentSize;
mDataBlockSize = blockSize;
mBlockCount = blockCount;
mMaxResidentBlockIndex = (mMaxResidentBinarySize / mDataBlockSize) - 1;
mCurrentTransientLoadedBlockId = mMaxResidentBlockIndex;
mIndexOffset = indexOffset;
}
// Setup binary data file access, init index and preload data blocks up to limit
void FrameCaptureBinaryData::initializeBinaryDataLoader()
{
// Create file stream manager
mFileStream = new FileStream(mFileName.c_str(), Mode::Load);
// Assemble binary data file/cache index
constructBlockDescIndex(mIndexOffset);
// Preload binary data blocks up to limit
size_t blocksToPreload =
std::min(mReplayBlockDescriptions.size(), (mMaxResidentBlockIndex + 1));
for (size_t i = 0; i < blocksToPreload; i++)
{
loadBlock(i);
}
// Initialize getData cache
updateGetDataCache(0);
}
// Load a single data block into memory
void FrameCaptureBinaryData::loadBlock(size_t blockId)
{
std::vector<uint8_t> &uncompressedDataBlock = prepareLoadBlock(blockId);
// Move to start of this data block in the data file
mFileStream->seek(mReplayBlockDescriptions[blockId].fileOffset, kSeekBegin);
if (mIsBinaryDataCompressed)
{
// Use zlib library, based on example/doc here: https://zlib.net/zlib_how.html
ZLibHelper decompressor(Mode::Load);
z_stream *zStream = decompressor.getStream();
int inflateStatus = 0;
size_t bytesDecompressed = 0;
using ZlibBuffer = std::array<unsigned char, kZlibBufferSize>;
std::unique_ptr<ZlibBuffer> compressedDataBuffer(new ZlibBuffer());
zStream->avail_out = static_cast<uInt>(mDataBlockSize);
zStream->next_out = uncompressedDataBlock.data();
do
{
if (zStream->avail_in == 0)
{
zStream->avail_in = static_cast<uInt>(
mFileStream->read(compressedDataBuffer->data(), kZlibBufferSize));
zStream->next_in = compressedDataBuffer->data();
}
do
{
int availableOutputSpace = static_cast<int>(mDataBlockSize - mCurrentBlockOffset);
zStream->avail_out = availableOutputSpace;
zStream->next_out = uncompressedDataBlock.data() + mCurrentBlockOffset;
inflateStatus = inflate(zStream, Z_NO_FLUSH);
ASSERT(inflateStatus != Z_STREAM_ERROR);
if (inflateStatus == Z_NEED_DICT || inflateStatus == Z_DATA_ERROR ||
inflateStatus == Z_MEM_ERROR)
{
FATAL() << "Zlib inflate failed: " << inflateStatus;
}
bytesDecompressed = availableOutputSpace - zStream->avail_out;
mCurrentBlockOffset += bytesDecompressed;
} while (zStream->avail_out == 0 && mCurrentBlockOffset < mDataBlockSize);
} while (inflateStatus != Z_STREAM_END && mCurrentBlockOffset != mDataBlockSize);
}
else
{
mCurrentBlockOffset = mFileStream->read(uncompressedDataBlock.data(), mDataBlockSize);
}
// Except for the last block this resize will be a no-op
uncompressedDataBlock.resize(mCurrentBlockOffset);
// Indicate that this block is now loaded
setBlockResident(blockId, uncompressedDataBlock.data());
}
void FrameCaptureBinaryData::closeBinaryDataLoader()
{
clear();
}
int FileStreamSeek(FILE *stream, long long offset, int whence)
{
#if defined(ANGLE_PLATFORM_WINDOWS)
return _fseeki64(stream, offset, whence);
#else
return fseeko(stream, static_cast<off_t>(offset), whence);
#endif
}
long long FileStreamTell(FILE *stream)
{
#if defined(ANGLE_PLATFORM_WINDOWS)
return _ftelli64(stream);
#else
return ftello(stream);
#endif
}
void FileStream::write(const uint8_t *data, size_t size)
{
if (fwrite(data, 1, size, mFile) != size)
{
if (ferror(mFile))
{
FATAL() << "Error writing " << size << " bytes to binary data file.";
}
}
if (fflush(mFile) != 0)
{
FATAL() << "Error flushing data to binary data file.";
}
}
size_t FileStream::read(uint8_t *buffer, size_t size)
{
size_t readBytes = fread(buffer, 1, size, mFile);
if (readBytes < size && ferror(mFile))
{
FATAL() << "Error reading from binary data file.";
}
return readBytes;
}
void FileStream::seek(long long offset, int whence)
{
if (FileStreamSeek(mFile, offset, whence) != 0)
{
FATAL() << "Error seeking in binary data file with offset " << offset << " and whence "
<< whence;
}
}
size_t FileStream::getPosition()
{
long long offset = FileStreamTell(mFile);
if (offset == -1)
{
FATAL() << "Error getting position in binary data file " << mFilePath;
}
angle::CheckedNumeric<size_t> checkedOffset(offset);
size_t safeOffset = 0;
if (!checkedOffset.AssignIfValid(&safeOffset))
{
FATAL() << "ANGLE file seek position offset out of range";
}
return safeOffset;
}
} // namespace angle