fortplot_memory_backend.f90 Source File


Source Code

module fortplot_memory_backend
    !! Memory-only backend for Windows CI performance testing
    !!
    !! This module provides an in-memory alternative to file-based backends,
    !! specifically designed to eliminate file I/O bottlenecks in Windows CI
    !! environments (Issue #188).
    
    use iso_fortran_env, only: int32, int64, real64, int8
    implicit none
    private
    
    public :: memory_backend_t
    public :: memory_buffer_t
    public :: get_memory_backend
    public :: clear_memory_backend
    
    ! Memory buffer to hold rendered output
    type :: memory_buffer_t
        character(len=:), allocatable :: filename
        integer(int8), dimension(:), allocatable :: data
        integer(int64) :: size = 0
        character(len=16) :: format = ""
        real(real64) :: creation_time = 0.0_real64
    end type memory_buffer_t
    
    ! Memory backend manager
    type :: memory_backend_t
        type(memory_buffer_t), dimension(:), allocatable :: buffers
        integer :: buffer_count = 0
        integer :: max_buffers = 1000
        logical :: enabled = .false.
        real(real64) :: total_memory_used = 0.0_real64
    contains
        procedure :: initialize => memory_backend_initialize
        procedure :: save => memory_backend_save
        procedure :: get_buffer => memory_backend_get_buffer
        procedure :: exists => memory_backend_exists
        procedure :: get_size => memory_backend_get_size
        procedure :: clear => memory_backend_clear
        procedure :: get_stats => memory_backend_get_stats
    end type memory_backend_t
    
    ! Global memory backend instance
    type(memory_backend_t), save, target :: global_memory_backend
    logical, save :: backend_initialized = .false.
    
contains
    
    subroutine memory_backend_initialize(this, max_buffers)
        !! Initialize the memory backend
        class(memory_backend_t), intent(inout) :: this
        integer, intent(in), optional :: max_buffers
        
        if (present(max_buffers)) then
            this%max_buffers = max_buffers
        end if
        
        if (allocated(this%buffers)) deallocate(this%buffers)
        allocate(this%buffers(this%max_buffers))
        
        this%buffer_count = 0
        this%total_memory_used = 0.0_real64
        this%enabled = .true.
        
    end subroutine memory_backend_initialize
    
    subroutine memory_backend_save(this, filename, data, format)
        !! Save data to memory buffer instead of file
        class(memory_backend_t), intent(inout) :: this
        character(len=*), intent(in) :: filename
        integer(int8), dimension(:), intent(in) :: data
        character(len=*), intent(in), optional :: format
        
        integer :: i, existing_idx
        real(real64) :: current_time
        
        if (.not. this%enabled) return
        
        ! Check if buffer already exists for this filename
        existing_idx = 0
        do i = 1, this%buffer_count
            if (allocated(this%buffers(i)%filename)) then
                if (this%buffers(i)%filename == filename) then
                    existing_idx = i
                    exit
                end if
            end if
        end do
        
        call cpu_time(current_time)
        
        if (existing_idx > 0) then
            ! Update existing buffer
            if (allocated(this%buffers(existing_idx)%data)) then
                this%total_memory_used = this%total_memory_used - &
                    real(size(this%buffers(existing_idx)%data), real64)
                deallocate(this%buffers(existing_idx)%data)
            end if
            
            allocate(this%buffers(existing_idx)%data(size(data)))
            this%buffers(existing_idx)%data = data
            this%buffers(existing_idx)%size = size(data, kind=int64)
            this%buffers(existing_idx)%creation_time = current_time
            if (present(format)) this%buffers(existing_idx)%format = format
            
        else
            ! Create new buffer
            if (this%buffer_count >= this%max_buffers) then
                ! Reached maximum buffers, overwrite oldest
                existing_idx = 1
                do i = 2, this%buffer_count
                    if (this%buffers(i)%creation_time < &
                        this%buffers(existing_idx)%creation_time) then
                        existing_idx = i
                    end if
                end do
                
                if (allocated(this%buffers(existing_idx)%data)) then
                    this%total_memory_used = this%total_memory_used - &
                        real(size(this%buffers(existing_idx)%data), real64)
                    deallocate(this%buffers(existing_idx)%data)
                end if
                if (allocated(this%buffers(existing_idx)%filename)) then
                    deallocate(this%buffers(existing_idx)%filename)
                end if
            else
                this%buffer_count = this%buffer_count + 1
                existing_idx = this%buffer_count
            end if
            
            this%buffers(existing_idx)%filename = filename
            allocate(this%buffers(existing_idx)%data(size(data)))
            this%buffers(existing_idx)%data = data
            this%buffers(existing_idx)%size = size(data, kind=int64)
            this%buffers(existing_idx)%creation_time = current_time
            if (present(format)) this%buffers(existing_idx)%format = format
        end if
        
        this%total_memory_used = this%total_memory_used + real(size(data), real64)
        
    end subroutine memory_backend_save
    
    function memory_backend_get_buffer(this, filename) result(buffer)
        !! Retrieve a buffer by filename
        class(memory_backend_t), intent(in) :: this
        character(len=*), intent(in) :: filename
        type(memory_buffer_t) :: buffer
        
        integer :: i
        
        do i = 1, this%buffer_count
            if (allocated(this%buffers(i)%filename)) then
                if (this%buffers(i)%filename == filename) then
                    buffer = this%buffers(i)
                    return
                end if
            end if
        end do
        
        ! Not found - return empty buffer
        buffer%size = 0
        
    end function memory_backend_get_buffer
    
    function memory_backend_exists(this, filename) result(exists)
        !! Check if a file exists in memory backend
        class(memory_backend_t), intent(in) :: this
        character(len=*), intent(in) :: filename
        logical :: exists
        
        integer :: i
        
        exists = .false.
        if (.not. this%enabled) return
        
        do i = 1, this%buffer_count
            if (allocated(this%buffers(i)%filename)) then
                if (this%buffers(i)%filename == filename) then
                    exists = .true.
                    return
                end if
            end if
        end do
        
    end function memory_backend_exists
    
    function memory_backend_get_size(this, filename) result(size)
        !! Get size of a file in memory backend
        class(memory_backend_t), intent(in) :: this
        character(len=*), intent(in) :: filename
        integer(int64) :: size
        
        integer :: i
        
        size = -1
        if (.not. this%enabled) return
        
        do i = 1, this%buffer_count
            if (allocated(this%buffers(i)%filename)) then
                if (this%buffers(i)%filename == filename) then
                    size = this%buffers(i)%size
                    return
                end if
            end if
        end do
        
    end function memory_backend_get_size
    
    subroutine memory_backend_clear(this)
        !! Clear all memory buffers
        class(memory_backend_t), intent(inout) :: this
        
        integer :: i
        
        do i = 1, this%buffer_count
            if (allocated(this%buffers(i)%data)) deallocate(this%buffers(i)%data)
            if (allocated(this%buffers(i)%filename)) deallocate(this%buffers(i)%filename)
        end do
        
        this%buffer_count = 0
        this%total_memory_used = 0.0_real64
        
    end subroutine memory_backend_clear
    
    subroutine memory_backend_get_stats(this, buffer_count, total_memory, enabled)
        !! Get memory backend statistics
        class(memory_backend_t), intent(in) :: this
        integer, intent(out), optional :: buffer_count
        real(real64), intent(out), optional :: total_memory
        logical, intent(out), optional :: enabled
        
        if (present(buffer_count)) buffer_count = this%buffer_count
        if (present(total_memory)) total_memory = this%total_memory_used
        if (present(enabled)) enabled = this%enabled
        
    end subroutine memory_backend_get_stats
    
    function get_memory_backend() result(backend)
        !! Get global memory backend instance
        type(memory_backend_t), pointer :: backend
        
        if (.not. backend_initialized) then
            call global_memory_backend%initialize()
            backend_initialized = .true.
        end if
        
        backend => global_memory_backend
        
    end function get_memory_backend
    
    subroutine clear_memory_backend()
        !! Clear global memory backend
        
        if (backend_initialized) then
            call global_memory_backend%clear()
        end if
        
    end subroutine clear_memory_backend
    
end module fortplot_memory_backend