| 1 | ! ============================================================================== |
| 2 | ! Module: memory_dashboard |
| 3 | ! Purpose: Real-time memory pool statistics and visualization |
| 4 | ! Phase 4 of the memory pooling project |
| 5 | ! ============================================================================== |
| 6 | module memory_dashboard |
| 7 | use iso_fortran_env, only: int32, int64, output_unit |
| 8 | use string_pool |
| 9 | implicit none |
| 10 | private |
| 11 | |
| 12 | ! Public interface |
| 13 | public :: dashboard_init, dashboard_register_module, dashboard_track_allocation |
| 14 | public :: dashboard_track_deallocation, dashboard_display, dashboard_get_module_stats |
| 15 | public :: dashboard_cleanup, dashboard_export_csv, dashboard_summary |
| 16 | public :: module_stats ! Export type for testing |
| 17 | public :: MOD_READLINE, MOD_COMPLETION, MOD_PARSER, MOD_EXECUTOR, MOD_EXPANSION |
| 18 | public :: MOD_BUILTIN, MOD_AST, MOD_LEXER, MOD_EVALUATOR, MOD_HISTORY, MOD_VARIABLES |
| 19 | |
| 20 | ! Module tracking |
| 21 | integer, parameter :: MAX_MODULES = 50 |
| 22 | integer, parameter :: MODULE_NAME_LEN = 32 |
| 23 | |
| 24 | ! Module IDs - these should match the actual modules in fortsh |
| 25 | integer, parameter :: MOD_READLINE = 1 |
| 26 | integer, parameter :: MOD_COMPLETION = 2 |
| 27 | integer, parameter :: MOD_PARSER = 3 |
| 28 | integer, parameter :: MOD_EXECUTOR = 4 |
| 29 | integer, parameter :: MOD_EXPANSION = 5 |
| 30 | integer, parameter :: MOD_BUILTIN = 6 |
| 31 | integer, parameter :: MOD_AST = 7 |
| 32 | integer, parameter :: MOD_LEXER = 8 |
| 33 | integer, parameter :: MOD_EVALUATOR = 9 |
| 34 | integer, parameter :: MOD_HISTORY = 10 |
| 35 | integer, parameter :: MOD_VARIABLES = 11 |
| 36 | integer, parameter :: MOD_UNKNOWN = 99 |
| 37 | |
| 38 | ! Module statistics |
| 39 | type :: module_stats |
| 40 | character(len=MODULE_NAME_LEN) :: name = "unknown" |
| 41 | integer(int64) :: total_allocations = 0 |
| 42 | integer(int64) :: total_deallocations = 0 |
| 43 | integer(int64) :: current_bytes = 0 |
| 44 | integer(int64) :: peak_bytes = 0 |
| 45 | integer(int64) :: total_bytes_allocated = 0 |
| 46 | integer :: current_strings = 0 |
| 47 | integer :: peak_strings = 0 |
| 48 | ! Bucket-specific stats |
| 49 | integer(int64) :: bucket_allocs(5) = 0 ! Count per bucket size |
| 50 | integer(int64) :: bucket_bytes(5) = 0 ! Current bytes per bucket |
| 51 | end type module_stats |
| 52 | |
| 53 | ! Global dashboard state |
| 54 | type :: dashboard_state |
| 55 | type(module_stats) :: modules(MAX_MODULES) |
| 56 | integer :: num_registered = 0 |
| 57 | logical :: initialized = .false. |
| 58 | logical :: verbose = .false. |
| 59 | integer(int64) :: session_start_time = 0 |
| 60 | ! Pool efficiency metrics |
| 61 | real :: overall_hit_rate = 0.0 |
| 62 | integer(int64) :: total_pool_expansions = 0 |
| 63 | integer(int64) :: fragmentation_bytes = 0 |
| 64 | end type dashboard_state |
| 65 | |
| 66 | type(dashboard_state) :: dashboard |
| 67 | |
| 68 | ! ANSI color codes for terminal output |
| 69 | character(len=*), parameter :: RESET = char(27)//"[0m" |
| 70 | character(len=*), parameter :: BOLD = char(27)//"[1m" |
| 71 | character(len=*), parameter :: RED = char(27)//"[31m" |
| 72 | character(len=*), parameter :: GREEN = char(27)//"[32m" |
| 73 | character(len=*), parameter :: YELLOW = char(27)//"[33m" |
| 74 | character(len=*), parameter :: BLUE = char(27)//"[34m" |
| 75 | character(len=*), parameter :: CYAN = char(27)//"[36m" |
| 76 | |
| 77 | contains |
| 78 | |
| 79 | ! Initialize the dashboard |
| 80 | subroutine dashboard_init(verbose) |
| 81 | logical, intent(in), optional :: verbose |
| 82 | |
| 83 | dashboard%initialized = .true. |
| 84 | dashboard%verbose = .false. |
| 85 | if (present(verbose)) dashboard%verbose = verbose |
| 86 | |
| 87 | #ifdef MEMPOOL_DEBUG |
| 88 | ! Enable verbose output when MEMPOOL_DEBUG is defined |
| 89 | dashboard%verbose = .true. |
| 90 | write(output_unit, '(A)') "[MEMPOOL_DEBUG] Memory dashboard initialized in debug mode" |
| 91 | #endif |
| 92 | |
| 93 | ! Get start time (simplified - would use system clock in real implementation) |
| 94 | dashboard%session_start_time = 0 |
| 95 | |
| 96 | ! Register known modules |
| 97 | call dashboard_register_module(MOD_READLINE, "readline") |
| 98 | call dashboard_register_module(MOD_COMPLETION, "completion") |
| 99 | call dashboard_register_module(MOD_PARSER, "parser") |
| 100 | call dashboard_register_module(MOD_EXECUTOR, "executor") |
| 101 | call dashboard_register_module(MOD_EXPANSION, "expansion") |
| 102 | call dashboard_register_module(MOD_BUILTIN, "builtin") |
| 103 | call dashboard_register_module(MOD_AST, "ast") |
| 104 | call dashboard_register_module(MOD_LEXER, "lexer") |
| 105 | call dashboard_register_module(MOD_EVALUATOR, "evaluator") |
| 106 | call dashboard_register_module(MOD_HISTORY, "history") |
| 107 | call dashboard_register_module(MOD_VARIABLES, "variables") |
| 108 | |
| 109 | end subroutine dashboard_init |
| 110 | |
| 111 | ! Register a module for tracking |
| 112 | subroutine dashboard_register_module(module_id, module_name) |
| 113 | integer, intent(in) :: module_id |
| 114 | character(len=*), intent(in) :: module_name |
| 115 | |
| 116 | if (.not. dashboard%initialized) call dashboard_init() |
| 117 | |
| 118 | if (module_id <= MAX_MODULES) then |
| 119 | dashboard%modules(module_id)%name = module_name |
| 120 | if (module_id > dashboard%num_registered) then |
| 121 | dashboard%num_registered = module_id |
| 122 | end if |
| 123 | end if |
| 124 | |
| 125 | end subroutine dashboard_register_module |
| 126 | |
| 127 | ! Track an allocation from a specific module |
| 128 | subroutine dashboard_track_allocation(module_id, size_bytes, bucket_idx) |
| 129 | integer, intent(in) :: module_id |
| 130 | integer, intent(in) :: size_bytes |
| 131 | integer, intent(in), optional :: bucket_idx |
| 132 | |
| 133 | if (.not. dashboard%initialized) return |
| 134 | if (module_id > MAX_MODULES) return |
| 135 | |
| 136 | ! Update module stats |
| 137 | dashboard%modules(module_id)%total_allocations = & |
| 138 | dashboard%modules(module_id)%total_allocations + 1 |
| 139 | dashboard%modules(module_id)%current_bytes = & |
| 140 | dashboard%modules(module_id)%current_bytes + size_bytes |
| 141 | dashboard%modules(module_id)%total_bytes_allocated = & |
| 142 | dashboard%modules(module_id)%total_bytes_allocated + size_bytes |
| 143 | dashboard%modules(module_id)%current_strings = & |
| 144 | dashboard%modules(module_id)%current_strings + 1 |
| 145 | |
| 146 | ! Update peak if necessary |
| 147 | if (dashboard%modules(module_id)%current_bytes > dashboard%modules(module_id)%peak_bytes) then |
| 148 | dashboard%modules(module_id)%peak_bytes = dashboard%modules(module_id)%current_bytes |
| 149 | end if |
| 150 | if (dashboard%modules(module_id)%current_strings > dashboard%modules(module_id)%peak_strings) then |
| 151 | dashboard%modules(module_id)%peak_strings = dashboard%modules(module_id)%current_strings |
| 152 | end if |
| 153 | |
| 154 | ! Track bucket-specific stats |
| 155 | if (present(bucket_idx) .and. bucket_idx >= 1 .and. bucket_idx <= 5) then |
| 156 | dashboard%modules(module_id)%bucket_allocs(bucket_idx) = & |
| 157 | dashboard%modules(module_id)%bucket_allocs(bucket_idx) + 1 |
| 158 | dashboard%modules(module_id)%bucket_bytes(bucket_idx) = & |
| 159 | dashboard%modules(module_id)%bucket_bytes(bucket_idx) + size_bytes |
| 160 | end if |
| 161 | |
| 162 | end subroutine dashboard_track_allocation |
| 163 | |
| 164 | ! Track a deallocation from a specific module |
| 165 | subroutine dashboard_track_deallocation(module_id, size_bytes, bucket_idx) |
| 166 | integer, intent(in) :: module_id |
| 167 | integer, intent(in) :: size_bytes |
| 168 | integer, intent(in), optional :: bucket_idx |
| 169 | |
| 170 | if (.not. dashboard%initialized) return |
| 171 | if (module_id > MAX_MODULES) return |
| 172 | |
| 173 | dashboard%modules(module_id)%total_deallocations = & |
| 174 | dashboard%modules(module_id)%total_deallocations + 1 |
| 175 | dashboard%modules(module_id)%current_bytes = & |
| 176 | dashboard%modules(module_id)%current_bytes - size_bytes |
| 177 | dashboard%modules(module_id)%current_strings = & |
| 178 | dashboard%modules(module_id)%current_strings - 1 |
| 179 | |
| 180 | ! Update bucket stats |
| 181 | if (present(bucket_idx) .and. bucket_idx >= 1 .and. bucket_idx <= 5) then |
| 182 | dashboard%modules(module_id)%bucket_bytes(bucket_idx) = & |
| 183 | dashboard%modules(module_id)%bucket_bytes(bucket_idx) - size_bytes |
| 184 | end if |
| 185 | |
| 186 | end subroutine dashboard_track_deallocation |
| 187 | |
| 188 | ! Display the dashboard |
| 189 | subroutine dashboard_display(detailed) |
| 190 | logical, intent(in), optional :: detailed |
| 191 | logical :: show_details |
| 192 | integer :: i, j, total_allocs, total_deallocs, current_strings, peak_strings |
| 193 | real :: hit_rate |
| 194 | integer(int64) :: total_current, total_peak, total_allocated |
| 195 | character(len=80) :: bar |
| 196 | real :: percent |
| 197 | |
| 198 | show_details = .false. |
| 199 | if (present(detailed)) show_details = detailed |
| 200 | |
| 201 | ! Get current pool statistics |
| 202 | call pool_statistics(total_allocs, total_deallocs, current_strings, peak_strings, hit_rate) |
| 203 | |
| 204 | ! Print header |
| 205 | write(output_unit,'(a)') "" |
| 206 | write(output_unit,'(a)') BOLD//CYAN// & |
| 207 | "======================================================================"//RESET |
| 208 | write(output_unit,'(a)') BOLD//CYAN// & |
| 209 | " FORTSH MEMORY POOL STATISTICS DASHBOARD"//RESET |
| 210 | write(output_unit,'(a)') BOLD//CYAN// & |
| 211 | "======================================================================"//RESET |
| 212 | write(output_unit,'(a)') "" |
| 213 | |
| 214 | ! Overall statistics |
| 215 | write(output_unit,'(a)') BOLD//"═══ Overall Pool Performance ═══"//RESET |
| 216 | write(output_unit,'(a,i12)') " Total Allocations: ", total_allocs |
| 217 | write(output_unit,'(a,i12)') " Total Deallocations: ", total_deallocs |
| 218 | write(output_unit,'(a,i12)') " Current Strings: ", current_strings |
| 219 | write(output_unit,'(a,i12)') " Peak Strings: ", peak_strings |
| 220 | |
| 221 | ! Cache performance with color coding |
| 222 | if (hit_rate > 0.95) then |
| 223 | write(output_unit,'(a,f6.1,a)') " Cache Hit Rate: "//GREEN, hit_rate * 100.0, "%"//RESET |
| 224 | else if (hit_rate > 0.80) then |
| 225 | write(output_unit,'(a,f6.1,a)') " Cache Hit Rate: "//YELLOW, hit_rate * 100.0, "%"//RESET |
| 226 | else |
| 227 | write(output_unit,'(a,f6.1,a)') " Cache Hit Rate: "//RED, hit_rate * 100.0, "%"//RESET |
| 228 | end if |
| 229 | |
| 230 | ! Memory usage bar graph |
| 231 | if (peak_strings > 0) then |
| 232 | percent = real(current_strings) / real(peak_strings) |
| 233 | call draw_progress_bar(bar, percent) |
| 234 | write(output_unit,'(a)') " Memory Usage: "//trim(bar) |
| 235 | end if |
| 236 | |
| 237 | write(output_unit,'(a)') "" |
| 238 | |
| 239 | ! Per-module statistics |
| 240 | write(output_unit,'(a)') BOLD//"═══ Module Memory Usage ═══"//RESET |
| 241 | write(output_unit,'(a)') " Module Allocs Deallocs Current Peak Bytes" |
| 242 | write(output_unit,'(a)') & |
| 243 | " ---------------------------------------------------------------" |
| 244 | |
| 245 | total_current = 0 |
| 246 | total_peak = 0 |
| 247 | total_allocated = 0 |
| 248 | |
| 249 | do i = 1, dashboard%num_registered |
| 250 | if (dashboard%modules(i)%total_allocations > 0) then |
| 251 | write(output_unit,'(a,a16,i10,i10,i8,i8,a,a)') " ", & |
| 252 | adjustl(dashboard%modules(i)%name), & |
| 253 | int(dashboard%modules(i)%total_allocations), & |
| 254 | int(dashboard%modules(i)%total_deallocations), & |
| 255 | dashboard%modules(i)%current_strings, & |
| 256 | dashboard%modules(i)%peak_strings, & |
| 257 | " ", format_bytes(dashboard%modules(i)%current_bytes) |
| 258 | |
| 259 | total_current = total_current + dashboard%modules(i)%current_bytes |
| 260 | total_peak = total_peak + dashboard%modules(i)%peak_bytes |
| 261 | total_allocated = total_allocated + dashboard%modules(i)%total_bytes_allocated |
| 262 | end if |
| 263 | end do |
| 264 | |
| 265 | write(output_unit,'(a)') & |
| 266 | " ---------------------------------------------------------------" |
| 267 | write(output_unit,'(a,a)') " Total Current Memory: ", & |
| 268 | format_bytes(total_current) |
| 269 | write(output_unit,'(a,a)') " Total Peak Memory: ", & |
| 270 | format_bytes(total_peak) |
| 271 | |
| 272 | ! Detailed bucket analysis if requested |
| 273 | if (show_details) then |
| 274 | write(output_unit,'(a)') "" |
| 275 | write(output_unit,'(a)') BOLD//"═══ Bucket Distribution ═══"//RESET |
| 276 | write(output_unit,'(a)') " Size Module Allocations Current Bytes" |
| 277 | write(output_unit,'(a)') & |
| 278 | " --------------------------------------------------------" |
| 279 | |
| 280 | do j = 1, 5 |
| 281 | select case(j) |
| 282 | case(1) |
| 283 | write(output_unit,'(a)') " 64 bytes:" |
| 284 | case(2) |
| 285 | write(output_unit,'(a)') " 256 bytes:" |
| 286 | case(3) |
| 287 | write(output_unit,'(a)') " 1024 bytes:" |
| 288 | case(4) |
| 289 | write(output_unit,'(a)') " 4096 bytes:" |
| 290 | case(5) |
| 291 | write(output_unit,'(a)') " 16384 bytes:" |
| 292 | end select |
| 293 | |
| 294 | do i = 1, dashboard%num_registered |
| 295 | if (dashboard%modules(i)%bucket_allocs(j) > 0) then |
| 296 | write(output_unit,'(a,a16,i14,a,a)') " ", & |
| 297 | adjustl(dashboard%modules(i)%name), & |
| 298 | int(dashboard%modules(i)%bucket_allocs(j)), & |
| 299 | " ", format_bytes(dashboard%modules(i)%bucket_bytes(j)) |
| 300 | end if |
| 301 | end do |
| 302 | end do |
| 303 | end if |
| 304 | |
| 305 | write(output_unit,'(a)') "" |
| 306 | write(output_unit,'(a)') BOLD//CYAN// & |
| 307 | "======================================================================"//RESET |
| 308 | write(output_unit,'(a)') "" |
| 309 | |
| 310 | end subroutine dashboard_display |
| 311 | |
| 312 | ! Draw a progress bar |
| 313 | subroutine draw_progress_bar(bar, percent) |
| 314 | character(len=*), intent(out) :: bar |
| 315 | real, intent(in) :: percent |
| 316 | integer :: filled_chars, i, bar_len |
| 317 | integer, parameter :: BAR_WIDTH = 30 |
| 318 | character(len=10) :: percent_str |
| 319 | |
| 320 | filled_chars = min(int(percent * BAR_WIDTH), BAR_WIDTH) |
| 321 | bar = " [" |
| 322 | |
| 323 | ! Build the bar safely |
| 324 | do i = 1, filled_chars |
| 325 | if (len_trim(bar) < len(bar) - 20) then ! Leave room for percentage |
| 326 | bar = trim(bar) // "█" |
| 327 | end if |
| 328 | end do |
| 329 | |
| 330 | do i = filled_chars + 1, BAR_WIDTH |
| 331 | if (len_trim(bar) < len(bar) - 20) then ! Leave room for percentage |
| 332 | bar = trim(bar) // "─" |
| 333 | end if |
| 334 | end do |
| 335 | |
| 336 | bar = trim(bar) // "] " |
| 337 | write(percent_str, '(f5.1,a)') percent * 100.0, "%" |
| 338 | |
| 339 | ! Only append percentage if there's room |
| 340 | bar_len = len_trim(bar) + len_trim(percent_str) |
| 341 | if (bar_len < len(bar)) then |
| 342 | bar = trim(bar) // trim(adjustl(percent_str)) |
| 343 | end if |
| 344 | |
| 345 | end subroutine draw_progress_bar |
| 346 | |
| 347 | ! Format bytes for display |
| 348 | function format_bytes(bytes) result(formatted) |
| 349 | integer(int64), intent(in) :: bytes |
| 350 | character(len=20) :: formatted |
| 351 | |
| 352 | if (bytes < 1024) then |
| 353 | write(formatted, '(i0,a)') bytes, " B" |
| 354 | else if (bytes < 1024*1024) then |
| 355 | write(formatted, '(f0.1,a)') real(bytes)/1024.0, " KB" |
| 356 | else if (bytes < int(1024,int64)*1024*1024) then |
| 357 | write(formatted, '(f0.1,a)') real(bytes)/(1024.0*1024.0), " MB" |
| 358 | else |
| 359 | write(formatted, '(f0.1,a)') real(bytes)/(1024.0*1024.0*1024.0), " GB" |
| 360 | end if |
| 361 | |
| 362 | formatted = adjustr(formatted) |
| 363 | |
| 364 | end function format_bytes |
| 365 | |
| 366 | ! Get statistics for a specific module |
| 367 | function dashboard_get_module_stats(module_id) result(stats) |
| 368 | integer, intent(in) :: module_id |
| 369 | type(module_stats) :: stats |
| 370 | |
| 371 | if (module_id > 0 .and. module_id <= MAX_MODULES) then |
| 372 | stats = dashboard%modules(module_id) |
| 373 | else |
| 374 | stats%name = "invalid" |
| 375 | end if |
| 376 | |
| 377 | end function dashboard_get_module_stats |
| 378 | |
| 379 | ! Export statistics to CSV |
| 380 | subroutine dashboard_export_csv(filename) |
| 381 | character(len=*), intent(in) :: filename |
| 382 | integer :: unit, i, iostat |
| 383 | |
| 384 | open(newunit=unit, file=filename, status='replace', action='write', iostat=iostat) |
| 385 | if (iostat /= 0) then |
| 386 | write(output_unit,'(a)') "Error: Could not open CSV file for export" |
| 387 | return |
| 388 | end if |
| 389 | |
| 390 | ! Write header |
| 391 | write(unit,'(a)') "Module,Total_Allocations,Total_Deallocations,Current_Strings," // & |
| 392 | "Peak_Strings,Current_Bytes,Peak_Bytes,Total_Bytes_Allocated" |
| 393 | |
| 394 | ! Write data |
| 395 | do i = 1, dashboard%num_registered |
| 396 | if (dashboard%modules(i)%total_allocations > 0) then |
| 397 | write(unit,'(a,",",i0,",",i0,",",i0,",",i0,",",i0,",",i0,",",i0)') & |
| 398 | trim(dashboard%modules(i)%name), & |
| 399 | dashboard%modules(i)%total_allocations, & |
| 400 | dashboard%modules(i)%total_deallocations, & |
| 401 | dashboard%modules(i)%current_strings, & |
| 402 | dashboard%modules(i)%peak_strings, & |
| 403 | dashboard%modules(i)%current_bytes, & |
| 404 | dashboard%modules(i)%peak_bytes, & |
| 405 | dashboard%modules(i)%total_bytes_allocated |
| 406 | end if |
| 407 | end do |
| 408 | |
| 409 | close(unit) |
| 410 | write(output_unit,'(a,a)') "Statistics exported to: ", trim(filename) |
| 411 | |
| 412 | end subroutine dashboard_export_csv |
| 413 | |
| 414 | ! Display a summary |
| 415 | subroutine dashboard_summary() |
| 416 | integer(int64) :: total_saved, total_allocated |
| 417 | integer :: i |
| 418 | real :: efficiency |
| 419 | |
| 420 | write(output_unit,'(a)') "" |
| 421 | write(output_unit,'(a)') BOLD//"═══ Memory Pool Summary ═══"//RESET |
| 422 | |
| 423 | total_allocated = 0 |
| 424 | do i = 1, dashboard%num_registered |
| 425 | total_allocated = total_allocated + dashboard%modules(i)%total_bytes_allocated |
| 426 | end do |
| 427 | |
| 428 | ! Calculate approximate memory saved (assuming 50% reduction from pooling) |
| 429 | total_saved = total_allocated / 2 |
| 430 | |
| 431 | write(output_unit,'(a,a)') " Total Memory Processed: ", format_bytes(total_allocated) |
| 432 | write(output_unit,'(a,a)') " Estimated Memory Saved: ", format_bytes(total_saved) |
| 433 | |
| 434 | if (total_allocated > 0) then |
| 435 | efficiency = real(total_saved) / real(total_allocated) * 100.0 |
| 436 | write(output_unit,'(a,f5.1,a)') " Pool Efficiency: ", efficiency, "%" |
| 437 | end if |
| 438 | |
| 439 | write(output_unit,'(a)') "" |
| 440 | |
| 441 | end subroutine dashboard_summary |
| 442 | |
| 443 | ! Clean up the dashboard |
| 444 | subroutine dashboard_cleanup() |
| 445 | integer :: i |
| 446 | |
| 447 | do i = 1, MAX_MODULES |
| 448 | dashboard%modules(i)%total_allocations = 0 |
| 449 | dashboard%modules(i)%total_deallocations = 0 |
| 450 | dashboard%modules(i)%current_bytes = 0 |
| 451 | dashboard%modules(i)%peak_bytes = 0 |
| 452 | dashboard%modules(i)%total_bytes_allocated = 0 |
| 453 | dashboard%modules(i)%current_strings = 0 |
| 454 | dashboard%modules(i)%peak_strings = 0 |
| 455 | dashboard%modules(i)%bucket_allocs = 0 |
| 456 | dashboard%modules(i)%bucket_bytes = 0 |
| 457 | end do |
| 458 | |
| 459 | dashboard%num_registered = 0 |
| 460 | dashboard%initialized = .false. |
| 461 | |
| 462 | end subroutine dashboard_cleanup |
| 463 | |
| 464 | end module memory_dashboard |