Skip to content

feat(io): add Resize API for efficient storage allocation#1571

Merged
LHT129 merged 1 commit intoantgroup:mainfrom
LHT129:io_resize
Feb 11, 2026
Merged

feat(io): add Resize API for efficient storage allocation#1571
LHT129 merged 1 commit intoantgroup:mainfrom
LHT129:io_resize

Conversation

@LHT129
Copy link
Collaborator

@LHT129 LHT129 commented Feb 9, 2026

  • Add Resize() method to basic_io.h with ResizeImpl support via SFINAE
  • Implement ResizeImpl for async_io, buffer_io, and memory_block_io
  • Replace Write-based expansion hack with Resize in datacell classes
  • Add null pointer check for memory_block_io allocation

- Add Resize() method to basic_io.h with ResizeImpl support via SFINAE
- Implement ResizeImpl for async_io, buffer_io, and memory_block_io
- Replace Write-based expansion hack with Resize in datacell classes
- Add null pointer check for memory_block_io allocation

Signed-off-by: LHT129 <tianlan.lht@antgroup.com>
@LHT129 LHT129 self-assigned this Feb 9, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @LHT129, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the I/O subsystem by introducing a dedicated and efficient Resize mechanism. This change streamlines storage allocation for various I/O types, moving away from an indirect Write-based approach, and improves overall code clarity and performance. Additionally, it bolsters memory safety by explicitly checking for allocation failures in memory-backed I/O.

Highlights

  • New Resize API: Introduced a new Resize API in basic_io.h to provide a standardized and efficient way to allocate or reallocate storage for I/O objects. This API leverages C++ SFINAE to allow specialized ResizeImpl methods in derived classes.
  • Specialized Resize Implementations: Implemented ResizeImpl for async_io, buffer_io, memory_block_io, mmap_io, and memory_io classes, providing optimized resizing logic for each specific I/O type, utilizing system calls like ftruncate and mremap where appropriate.
  • Datacell Refactoring: Refactored datacell classes (ExtraInfoDataCell, FlattenDataCell, GraphDataCell, SparseVectorDataCell) to utilize the new Resize API, replacing a previous workaround that involved using Write operations for storage expansion.
  • Memory Safety Improvement: Added a null pointer check in MemoryBlockIO::check_and_realloc to handle potential allocation failures gracefully, improving robustness during memory block allocation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/datacell/extra_info_datacell.h
    • Replaced the io_->Write call with this->io_->Resize for storage allocation.
  • src/datacell/flatten_datacell.h
    • Replaced the io_->Write call with this->io_->Resize for storage allocation.
  • src/datacell/graph_datacell.h
    • Replaced the io_->Write call with this->io_->Resize for storage allocation.
  • src/datacell/sparse_vector_datacell.h
    • Replaced this->io_->Write and this->offset_io_->Write calls with this->io_->Resize and this->offset_io_->Resize respectively for storage allocation.
  • src/io/async_io.cpp
    • Added AsyncIO::ResizeImpl which uses ftruncate (or ftruncate64) to resize the underlying file descriptor.
  • src/io/async_io.h
    • Declared ResizeImpl method in the AsyncIO class.
  • src/io/basic_io.h
    • Introduced a new Resize method that uses C++17 if constexpr and SFINAE (has_ResizeImpl) to either call a specialized ResizeImpl or provide a default Write-based resizing implementation.
    • Added GENERATE_HAS_MEMBER_FUNCTION(ResizeImpl, ...) to detect ResizeImpl in derived classes.
  • src/io/buffer_io.cpp
    • Added BufferIO::ResizeImpl which uses ftruncate (or ftruncate64) to resize the underlying file descriptor.
  • src/io/buffer_io.h
    • Declared ResizeImpl method in the BufferIO class.
  • src/io/memory_block_io.cpp
    • Added MemoryBlockIO::ResizeImpl which calls check_and_realloc to adjust memory blocks.
    • Added a null pointer check for the result of this->allocator_->Allocate during block allocation.
  • src/io/memory_block_io.h
    • Declared ResizeImpl method in the MemoryBlockIO class.
  • src/io/memory_io.cpp
    • Added MemoryIO::ResizeImpl which calls check_and_realloc to adjust memory.
  • src/io/memory_io.h
    • Declared ResizeImpl method in the MemoryIO class.
  • src/io/mmap_io.cpp
    • Added MMapIO::ResizeImpl which handles resizing memory-mapped files using ftruncate (or ftruncate64) and mmap/munmap (for Apple) or mremap (for Linux).
  • src/io/mmap_io.h
    • Declared ResizeImpl method in the MMapIO class.
Activity
  • No human activity recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Resize API for more efficient storage allocation, replacing a previous Write-based hack. This is a significant improvement in terms of code clarity and correctness. The changes across the datacell classes to adopt this new API are clean and appropriate. The implementations of ResizeImpl for the various IO backends are mostly solid, but I've identified a few areas for improvement regarding error handling, efficiency, and code structure, including a critical bug in mmap_io.cpp where the return value of mremap was not being checked. My review includes specific suggestions to address these points.

Comment on lines +109 to +160
MMapIO::ResizeImpl(uint64_t size) {
auto new_size = size;
auto old_size = this->size_;
if (old_size == 0) {
old_size = DEFAULT_INIT_MMAP_SIZE;
}
if (new_size > old_size) {
auto ret =
#ifdef __APPLE__
ftruncate(this->fd_, static_cast<off_t>(new_size));
#else
ftruncate64(this->fd_, static_cast<int64_t>(new_size));
#endif
if (ret == -1) {
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");
}

#ifdef __APPLE__
munmap(this->start_, old_size);
void* addr = mmap(nullptr, new_size, PROT_READ | PROT_WRITE, MAP_SHARED, this->fd_, 0);
if (addr == MAP_FAILED) {
throw VsagException(ErrorType::INTERNAL_ERROR, "mmap remap failed");
}
this->start_ = static_cast<uint8_t*>(addr);
#else
this->start_ =
static_cast<uint8_t*>(mremap(this->start_, old_size, new_size, MREMAP_MAYMOVE));
#endif
} else if (new_size < old_size) {
#ifdef __APPLE__
munmap(this->start_, old_size);
void* addr = mmap(nullptr, new_size, PROT_READ | PROT_WRITE, MAP_SHARED, this->fd_, 0);
if (addr == MAP_FAILED) {
throw VsagException(ErrorType::INTERNAL_ERROR, "mmap remap failed");
}
this->start_ = static_cast<uint8_t*>(addr);
#else
this->start_ =
static_cast<uint8_t*>(mremap(this->start_, old_size, new_size, MREMAP_MAYMOVE));
#endif
auto ret =
#ifdef __APPLE__
ftruncate(this->fd_, static_cast<off_t>(new_size));
#else
ftruncate64(this->fd_, static_cast<int64_t>(new_size));
#endif
if (ret == -1) {
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");
}
}
this->size_ = new_size;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation of ResizeImpl has a few issues:

  1. Critical Bug: The return value of mremap is not checked. It can return MAP_FAILED on error, which would lead to undefined behavior when the invalid pointer is used. This should be checked and an exception thrown on failure.
  2. Error Reporting: The error message for ftruncate is generic. Including strerror(errno) would provide more details on failure.
  3. Code Duplication: The logic for remapping memory (mremap or munmap/mmap) is duplicated for both growing and shrinking cases. This can be refactored to improve maintainability.

I've provided a suggested refactoring that addresses all these points by extracting the remap and truncate logic into lambdas, adding error checking, and improving error messages.

void
MMapIO::ResizeImpl(uint64_t size) {
    auto new_size = size;
    if (new_size == this->size_) {
        return;
    }

    auto old_size = this->size_;
    if (old_size == 0) {
        old_size = DEFAULT_INIT_MMAP_SIZE;
    }

    auto remap_memory = [&]() {
#ifdef __APPLE__
        munmap(this->start_, old_size);
        void* addr = mmap(nullptr, new_size, PROT_READ | PROT_WRITE, MAP_SHARED, this->fd_, 0);
        if (addr == MAP_FAILED) {
            throw VsagException(ErrorType::INTERNAL_ERROR, fmt::format("mmap remap failed: {}", strerror(errno)));
        }
        this->start_ = static_cast<uint8_t*>(addr);
#else
        void* new_addr = mremap(this->start_, old_size, new_size, MREMAP_MAYMOVE);
        if (new_addr == MAP_FAILED) {
            throw VsagException(ErrorType::INTERNAL_ERROR, fmt::format("mremap failed: {}", strerror(errno)));
        }
        this->start_ = static_cast<uint8_t*>(new_addr);
#endif
    };

    auto truncate_file = [&]() {
        auto ret =
#ifdef __APPLE__
            ftruncate(this->fd_, static_cast<off_t>(new_size));
#else
            ftruncate64(this->fd_, static_cast<int64_t>(new_size));
#endif
        if (ret == -1) {
            throw VsagException(ErrorType::INTERNAL_ERROR, fmt::format("ftruncate failed: {}", strerror(errno)));
        }
    };

    if (new_size > old_size) {
        truncate_file();
        remap_memory();
    } else if (new_size < old_size) {
        remap_memory();
        truncate_file();
    }
    this->size_ = new_size;
}

Comment on lines +225 to +236
} else {
if (size <= this->size_) {
return;
}
ByteBuffer buffer(SERIALIZE_BUFFER_SIZE, this->allocator_);
uint64_t offset = this->size_;
while (offset < size) {
auto cur_size = std::min(SERIALIZE_BUFFER_SIZE, size - offset);
this->Write(buffer.data, cur_size, offset);
offset += cur_size;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback implementation for Resize can be made more efficient. Instead of writing data in a loop to expand the underlying storage, a single Write call with a size of 0 at the target offset is sufficient. Most WriteImpl implementations will handle this by extending the storage to the specified offset without actually writing data, which is much faster.

        } else {
            if (size > this->size_) {
                // A single write of 0 bytes at the target size is enough to
                // trigger the size update in WriteImpl, which is more efficient
                // than writing data in a loop.
                this->Write(nullptr, 0, size);
            }
        }

auto ret = ftruncate64(this->wfd_, static_cast<int64_t>(size));
#endif
if (ret == -1) {
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message for ftruncate is a bit generic. Including the system error message using strerror(errno) would provide more context for debugging, which is a common practice elsewhere in this file.

Suggested change
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");
throw VsagException(ErrorType::INTERNAL_ERROR, fmt::format("ftruncate failed: {}", strerror(errno)));

auto ret = ftruncate64(this->fd_, static_cast<int64_t>(size));
#endif
if (ret == -1) {
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to async_io.cpp, the error message for ftruncate could be improved by including the system error string via strerror(errno). This will make debugging failures easier.

Suggested change
throw VsagException(ErrorType::INTERNAL_ERROR, "ftruncate failed");
throw VsagException(ErrorType::INTERNAL_ERROR, fmt::format("ftruncate failed: {}", strerror(errno)));

@codecov
Copy link

codecov bot commented Feb 9, 2026

Codecov Report

❌ Patch coverage is 52.54237% with 28 lines in your changes missing coverage. Please review.

❌ Your patch status has failed because the patch coverage (52.54%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

@@            Coverage Diff             @@
##             main    #1571      +/-   ##
==========================================
- Coverage   91.24%   91.06%   -0.18%     
==========================================
  Files         329      329              
  Lines       19396    19445      +49     
==========================================
+ Hits        17697    17708      +11     
- Misses       1699     1737      +38     
Flag Coverage Δ
cpp 91.06% <52.54%> (-0.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
common 85.81% <ø> (ø)
datacell 92.47% <48.14%> (-1.31%) ⬇️
index 90.64% <ø> (+0.02%) ⬆️
simd 100.00% <ø> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3dd2923...fdd32f5. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@LHT129 LHT129 marked this pull request as ready for review February 9, 2026 11:23
@wxyucs wxyucs added the kind/feature New feature or request label Feb 10, 2026
Copy link
Collaborator

@wxyucs wxyucs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Collaborator

@inabao inabao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@LHT129 LHT129 merged commit ca58677 into antgroup:main Feb 11, 2026
28 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants