| 1 | ! ============================================================================== |
| 2 | ! Module: memory_profiler |
| 3 | ! Purpose: Track memory allocations and detect leaks |
| 4 | ! |
| 5 | ! Provides instrumentation similar to valgrind/AddressSanitizer |
| 6 | ! ============================================================================== |
| 7 | module memory_profiler |
| 8 | use iso_fortran_env, only: int64, real64, error_unit |
| 9 | implicit none |
| 10 | private |
| 11 | |
| 12 | public :: mem_track_alloc, mem_track_dealloc, mem_report |
| 13 | public :: mem_check_leaks, mem_enable_tracking, mem_disable_tracking |
| 14 | public :: mem_get_current_usage, mem_get_peak_usage |
| 15 | |
| 16 | ! Tracking record for each allocation |
| 17 | type :: alloc_record |
| 18 | integer(int64) :: address = 0 |
| 19 | integer(int64) :: size = 0 |
| 20 | character(len=256) :: location = '' |
| 21 | character(len=64) :: type_name = '' |
| 22 | integer :: line_number = 0 |
| 23 | logical :: active = .false. |
| 24 | integer(int64) :: alloc_time = 0 |
| 25 | end type alloc_record |
| 26 | |
| 27 | ! Site statistics for reporting |
| 28 | type :: site_stats |
| 29 | character(len=256) :: location = '' |
| 30 | integer :: count = 0 |
| 31 | integer(int64) :: total_size = 0 |
| 32 | end type site_stats |
| 33 | |
| 34 | ! Module state |
| 35 | integer, parameter :: MAX_TRACKED = 10000 |
| 36 | type(alloc_record) :: allocations(MAX_TRACKED) |
| 37 | integer :: num_allocations = 0 |
| 38 | integer :: num_deallocations = 0 |
| 39 | integer :: num_active = 0 |
| 40 | integer(int64) :: current_usage = 0 |
| 41 | integer(int64) :: peak_usage = 0 |
| 42 | logical :: tracking_enabled = .false. |
| 43 | integer(int64) :: start_time = 0 |
| 44 | |
| 45 | ! Leak detection thresholds |
| 46 | integer, parameter :: LEAK_WARNING_SIZE = 1048576 ! 1MB |
| 47 | integer, parameter :: LEAK_ERROR_SIZE = 10485760 ! 10MB |
| 48 | |
| 49 | contains |
| 50 | |
| 51 | ! Enable memory tracking |
| 52 | subroutine mem_enable_tracking() |
| 53 | tracking_enabled = .true. |
| 54 | call system_clock(start_time) |
| 55 | write(error_unit, '(a)') '=== Memory profiling ENABLED ===' |
| 56 | end subroutine mem_enable_tracking |
| 57 | |
| 58 | ! Disable memory tracking |
| 59 | subroutine mem_disable_tracking() |
| 60 | tracking_enabled = .false. |
| 61 | write(error_unit, '(a)') '=== Memory profiling DISABLED ===' |
| 62 | end subroutine mem_disable_tracking |
| 63 | |
| 64 | ! Track an allocation |
| 65 | subroutine mem_track_alloc(ptr_address, size, location, type_name, line_num) |
| 66 | integer(int64), intent(in) :: ptr_address |
| 67 | integer(int64), intent(in) :: size |
| 68 | character(len=*), intent(in) :: location |
| 69 | character(len=*), intent(in) :: type_name |
| 70 | integer, intent(in) :: line_num |
| 71 | integer :: i, slot |
| 72 | integer(int64) :: current_time |
| 73 | |
| 74 | if (.not. tracking_enabled) return |
| 75 | |
| 76 | ! Find empty slot |
| 77 | slot = 0 |
| 78 | do i = 1, MAX_TRACKED |
| 79 | if (.not. allocations(i)%active) then |
| 80 | slot = i |
| 81 | exit |
| 82 | end if |
| 83 | end do |
| 84 | |
| 85 | if (slot == 0) then |
| 86 | write(error_unit, '(a)') 'WARNING: Memory tracker full, cannot track allocation' |
| 87 | return |
| 88 | end if |
| 89 | |
| 90 | call system_clock(current_time) |
| 91 | |
| 92 | ! Record allocation |
| 93 | allocations(slot)%address = ptr_address |
| 94 | allocations(slot)%size = size |
| 95 | allocations(slot)%location = location |
| 96 | allocations(slot)%type_name = type_name |
| 97 | allocations(slot)%line_number = line_num |
| 98 | allocations(slot)%active = .true. |
| 99 | allocations(slot)%alloc_time = current_time |
| 100 | |
| 101 | ! Update statistics |
| 102 | num_allocations = num_allocations + 1 |
| 103 | num_active = num_active + 1 |
| 104 | current_usage = current_usage + size |
| 105 | if (current_usage > peak_usage) peak_usage = current_usage |
| 106 | |
| 107 | ! Warn on large allocations |
| 108 | if (size > LEAK_WARNING_SIZE) then |
| 109 | write(error_unit, '(a,i0,a,a,a,i0)') & |
| 110 | 'LARGE ALLOCATION: ', size, ' bytes at ', trim(location), ':', line_num |
| 111 | end if |
| 112 | |
| 113 | end subroutine mem_track_alloc |
| 114 | |
| 115 | ! Track a deallocation |
| 116 | subroutine mem_track_dealloc(ptr_address, location, line_num) |
| 117 | integer(int64), intent(in) :: ptr_address |
| 118 | character(len=*), intent(in) :: location |
| 119 | integer, intent(in) :: line_num |
| 120 | integer :: i |
| 121 | logical :: found |
| 122 | |
| 123 | if (.not. tracking_enabled) return |
| 124 | |
| 125 | found = .false. |
| 126 | do i = 1, MAX_TRACKED |
| 127 | if (allocations(i)%active .and. allocations(i)%address == ptr_address) then |
| 128 | found = .true. |
| 129 | current_usage = current_usage - allocations(i)%size |
| 130 | allocations(i)%active = .false. |
| 131 | num_deallocations = num_deallocations + 1 |
| 132 | num_active = num_active - 1 |
| 133 | exit |
| 134 | end if |
| 135 | end do |
| 136 | |
| 137 | if (.not. found) then |
| 138 | write(error_unit, '(a,z16,a,a,a,i0)') & |
| 139 | 'WARNING: Deallocating untracked pointer 0x', ptr_address, & |
| 140 | ' at ', trim(location), ':', line_num |
| 141 | end if |
| 142 | |
| 143 | end subroutine mem_track_dealloc |
| 144 | |
| 145 | ! Check for memory leaks |
| 146 | subroutine mem_check_leaks() |
| 147 | integer :: i, leak_count |
| 148 | integer(int64) :: leaked_bytes |
| 149 | character(len=512) :: leak_msg |
| 150 | |
| 151 | if (.not. tracking_enabled) return |
| 152 | |
| 153 | leak_count = 0 |
| 154 | leaked_bytes = 0 |
| 155 | |
| 156 | write(error_unit, '(a)') '' |
| 157 | write(error_unit, '(a)') '=== MEMORY LEAK CHECK ===' |
| 158 | |
| 159 | do i = 1, MAX_TRACKED |
| 160 | if (allocations(i)%active) then |
| 161 | leak_count = leak_count + 1 |
| 162 | leaked_bytes = leaked_bytes + allocations(i)%size |
| 163 | |
| 164 | write(leak_msg, '(a,i0,a,a,a,i0,a,a)') & |
| 165 | 'LEAK: ', allocations(i)%size, ' bytes from ', & |
| 166 | trim(allocations(i)%location), ':', allocations(i)%line_number, & |
| 167 | ' (', trim(allocations(i)%type_name), ')' |
| 168 | write(error_unit, '(a)') trim(leak_msg) |
| 169 | end if |
| 170 | end do |
| 171 | |
| 172 | if (leak_count > 0) then |
| 173 | write(error_unit, '(a,i0,a,i0,a)') & |
| 174 | 'TOTAL: ', leak_count, ' leaks, ', leaked_bytes, ' bytes leaked' |
| 175 | if (leaked_bytes > LEAK_ERROR_SIZE) then |
| 176 | write(error_unit, '(a)') 'ERROR: Severe memory leak detected!' |
| 177 | end if |
| 178 | else |
| 179 | write(error_unit, '(a)') 'No memory leaks detected' |
| 180 | end if |
| 181 | |
| 182 | write(error_unit, '(a)') '=========================' |
| 183 | write(error_unit, '()') |
| 184 | |
| 185 | end subroutine mem_check_leaks |
| 186 | |
| 187 | ! Generate memory report |
| 188 | subroutine mem_report() |
| 189 | integer :: i, bucket_counts(10), bucket_sizes(10) |
| 190 | integer(int64) :: total_time |
| 191 | real(real64) :: elapsed_seconds |
| 192 | |
| 193 | if (.not. tracking_enabled) return |
| 194 | |
| 195 | call system_clock(total_time) |
| 196 | elapsed_seconds = real(total_time - start_time) / 1000.0 |
| 197 | |
| 198 | write(error_unit, '(a)') '' |
| 199 | write(error_unit, '(a)') '=== MEMORY USAGE REPORT ===' |
| 200 | write(error_unit, '(a,f8.2,a)') 'Profiling duration: ', elapsed_seconds, ' seconds' |
| 201 | write(error_unit, '(a,i0)') 'Total allocations: ', num_allocations |
| 202 | write(error_unit, '(a,i0)') 'Total deallocations: ', num_deallocations |
| 203 | write(error_unit, '(a,i0)') 'Currently active: ', num_active |
| 204 | write(error_unit, '(a,i0,a)') 'Current usage: ', current_usage, ' bytes' |
| 205 | write(error_unit, '(a,i0,a)') 'Peak usage: ', peak_usage, ' bytes' |
| 206 | |
| 207 | ! Size distribution |
| 208 | bucket_counts = 0 |
| 209 | bucket_sizes = [16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, huge(1)] |
| 210 | |
| 211 | do i = 1, MAX_TRACKED |
| 212 | if (allocations(i)%active) then |
| 213 | if (allocations(i)%size <= bucket_sizes(1)) then |
| 214 | bucket_counts(1) = bucket_counts(1) + 1 |
| 215 | else if (allocations(i)%size <= bucket_sizes(2)) then |
| 216 | bucket_counts(2) = bucket_counts(2) + 1 |
| 217 | else if (allocations(i)%size <= bucket_sizes(3)) then |
| 218 | bucket_counts(3) = bucket_counts(3) + 1 |
| 219 | else if (allocations(i)%size <= bucket_sizes(4)) then |
| 220 | bucket_counts(4) = bucket_counts(4) + 1 |
| 221 | else if (allocations(i)%size <= bucket_sizes(5)) then |
| 222 | bucket_counts(5) = bucket_counts(5) + 1 |
| 223 | else if (allocations(i)%size <= bucket_sizes(6)) then |
| 224 | bucket_counts(6) = bucket_counts(6) + 1 |
| 225 | else if (allocations(i)%size <= bucket_sizes(7)) then |
| 226 | bucket_counts(7) = bucket_counts(7) + 1 |
| 227 | else if (allocations(i)%size <= bucket_sizes(8)) then |
| 228 | bucket_counts(8) = bucket_counts(8) + 1 |
| 229 | else if (allocations(i)%size <= bucket_sizes(9)) then |
| 230 | bucket_counts(9) = bucket_counts(9) + 1 |
| 231 | else |
| 232 | bucket_counts(10) = bucket_counts(10) + 1 |
| 233 | end if |
| 234 | end if |
| 235 | end do |
| 236 | |
| 237 | write(error_unit, '(a)') '' |
| 238 | write(error_unit, '(a)') 'Size distribution of active allocations:' |
| 239 | write(error_unit, '(a,i0)') ' 0-16 bytes: ', bucket_counts(1) |
| 240 | write(error_unit, '(a,i0)') ' 17-64 bytes: ', bucket_counts(2) |
| 241 | write(error_unit, '(a,i0)') ' 65-256 bytes: ', bucket_counts(3) |
| 242 | write(error_unit, '(a,i0)') ' 257-1K bytes: ', bucket_counts(4) |
| 243 | write(error_unit, '(a,i0)') ' 1-4K bytes: ', bucket_counts(5) |
| 244 | write(error_unit, '(a,i0)') ' 4-16K bytes: ', bucket_counts(6) |
| 245 | write(error_unit, '(a,i0)') ' 16-64K bytes: ', bucket_counts(7) |
| 246 | write(error_unit, '(a,i0)') ' 64-256K bytes: ', bucket_counts(8) |
| 247 | write(error_unit, '(a,i0)') ' 256K-1M bytes: ', bucket_counts(9) |
| 248 | write(error_unit, '(a,i0)') ' >1M bytes: ', bucket_counts(10) |
| 249 | |
| 250 | ! Top allocation sites |
| 251 | call report_top_allocators() |
| 252 | |
| 253 | write(error_unit, '(a)') '=========================' |
| 254 | write(error_unit, '()') |
| 255 | |
| 256 | end subroutine mem_report |
| 257 | |
| 258 | ! Report top allocation sites |
| 259 | subroutine report_top_allocators() |
| 260 | type(site_stats) :: sites(100) |
| 261 | integer :: num_sites, i, j |
| 262 | logical :: found |
| 263 | |
| 264 | num_sites = 0 |
| 265 | sites%count = 0 |
| 266 | sites%total_size = 0 |
| 267 | |
| 268 | ! Aggregate by location |
| 269 | do i = 1, MAX_TRACKED |
| 270 | if (allocations(i)%active) then |
| 271 | found = .false. |
| 272 | do j = 1, num_sites |
| 273 | if (sites(j)%location == allocations(i)%location) then |
| 274 | sites(j)%count = sites(j)%count + 1 |
| 275 | sites(j)%total_size = sites(j)%total_size + allocations(i)%size |
| 276 | found = .true. |
| 277 | exit |
| 278 | end if |
| 279 | end do |
| 280 | |
| 281 | if (.not. found .and. num_sites < 100) then |
| 282 | num_sites = num_sites + 1 |
| 283 | sites(num_sites)%location = allocations(i)%location |
| 284 | sites(num_sites)%count = 1 |
| 285 | sites(num_sites)%total_size = allocations(i)%size |
| 286 | end if |
| 287 | end if |
| 288 | end do |
| 289 | |
| 290 | ! Sort and display top sites |
| 291 | if (num_sites > 0) then |
| 292 | write(error_unit, '(a)') '' |
| 293 | write(error_unit, '(a)') 'Top allocation sites:' |
| 294 | call sort_sites_by_size(sites, num_sites) |
| 295 | do i = 1, min(5, num_sites) |
| 296 | write(error_unit, '(2x,a,a,i0,a,i0,a)') & |
| 297 | trim(sites(i)%location), ': ', & |
| 298 | sites(i)%count, ' allocations, ', & |
| 299 | sites(i)%total_size, ' bytes' |
| 300 | end do |
| 301 | end if |
| 302 | |
| 303 | end subroutine report_top_allocators |
| 304 | |
| 305 | ! Sort allocation sites by total size |
| 306 | subroutine sort_sites_by_size(sites, n) |
| 307 | type(site_stats), intent(inout) :: sites(:) |
| 308 | integer, intent(in) :: n |
| 309 | type(site_stats) :: temp |
| 310 | integer :: i, j |
| 311 | |
| 312 | do i = 1, n-1 |
| 313 | do j = i+1, n |
| 314 | if (sites(j)%total_size > sites(i)%total_size) then |
| 315 | temp = sites(i) |
| 316 | sites(i) = sites(j) |
| 317 | sites(j) = temp |
| 318 | end if |
| 319 | end do |
| 320 | end do |
| 321 | |
| 322 | end subroutine sort_sites_by_size |
| 323 | |
| 324 | ! Get current memory usage |
| 325 | function mem_get_current_usage() result(usage) |
| 326 | integer(int64) :: usage |
| 327 | usage = current_usage |
| 328 | end function mem_get_current_usage |
| 329 | |
| 330 | ! Get peak memory usage |
| 331 | function mem_get_peak_usage() result(usage) |
| 332 | integer(int64) :: usage |
| 333 | usage = peak_usage |
| 334 | end function mem_get_peak_usage |
| 335 | |
| 336 | end module memory_profiler |