@@ -12,7 +12,7 @@ module Herb
1212 class Project
1313 include Colors
1414
15- attr_accessor :project_path , :output_file , :no_log_file , :no_timing , :silent , :verbose , :isolate , :validate_ruby , :file_paths
15+ attr_accessor :project_path , :output_file , :no_log_file , :no_timing , :silent , :verbose , :isolate , :validate_ruby , :file_paths , :arena_stats
1616
1717 # Known error types that indicate issues in the user's template, not bugs in the parser.
1818 TEMPLATE_ERRORS = [
@@ -256,6 +256,10 @@ def analyze!
256256
257257 log_problem_file_details ( results , log )
258258
259+ if arena_stats
260+ print_arena_summary ( file_results )
261+ end
262+
259263 unless no_log_file
260264 puts "\n #{ separator } "
261265 puts "\n #{ dimmed ( "Results saved to #{ output_file } " ) } "
@@ -339,6 +343,10 @@ def process_file_direct(file_path)
339343 file_content = File . read ( file_path )
340344 result = { file_path : file_path }
341345
346+ if arena_stats
347+ result [ :arena_stats ] = capture_arena_stats ( file_content )
348+ end
349+
342350 Timeout . timeout ( 1 ) do
343351 parse_result = Herb . parse ( file_content )
344352
@@ -353,17 +361,17 @@ def process_file_direct(file_path)
353361
354362 result
355363 rescue Timeout ::Error
356- { file_path : file_path , status : :timeout , file_content : file_content ,
357- log : "⏱️ Parsing #{ file_path } timed out after 1 second" }
364+ result . merge ( status : :timeout , file_content : file_content ,
365+ log : "⏱️ Parsing #{ file_path } timed out after 1 second" )
358366 rescue StandardError => e
359367 file_content ||= begin
360368 File . read ( file_path )
361369 rescue StandardError
362370 nil
363371 end
364372
365- { file_path : file_path , status : :failed , file_content : file_content ,
366- log : "⚠️ Error processing #{ file_path } : #{ e . message } " }
373+ result . merge ( status : :failed , file_content : file_content ,
374+ log : "⚠️ Error processing #{ file_path } : #{ e . message } " )
367375 end
368376
369377 def process_file_isolated ( file_path )
@@ -878,5 +886,84 @@ def format_duration(seconds)
878886 "#{ minutes } m #{ remaining_seconds . round ( 2 ) } s"
879887 end
880888 end
889+
890+ def capture_arena_stats ( file_content )
891+ stats = Herb . arena_stats ( file_content )
892+
893+ {
894+ pages : stats [ :pages ] ,
895+ bytes : stats [ :total_used ] ,
896+ allocations : stats [ :allocations ] ,
897+ lines : file_content . count ( "\n " ) + 1 ,
898+ length : file_content . bytesize ,
899+ }
900+ rescue StandardError
901+ { pages : 0 , bytes : 0 , allocations : 0 , lines : 0 , length : 0 }
902+ end
903+
904+ def print_arena_summary ( file_results )
905+ stats = file_results . filter_map { |result |
906+ next unless result [ :arena_stats ] && result [ :arena_stats ] [ :bytes ] . positive?
907+
908+ { file : result [ :file_path ] , **result [ :arena_stats ] }
909+ }
910+
911+ return if stats . empty?
912+
913+ stats . sort_by! { |stat | -stat [ :bytes ] }
914+
915+ puts "\n #{ separator } "
916+ puts "\n "
917+ puts " #{ bold ( "Arena memory usage:" ) } "
918+ puts ""
919+
920+ relatives = stats . map { |stat | relative_path ( stat [ :file ] ) }
921+ used_strings = stats . map { |stat | format_bytes ( stat [ :bytes ] ) }
922+ length_strings = stats . map { |stat | format_bytes ( stat [ :length ] ) }
923+ used_width = [ used_strings . max_by ( &:length ) . length , 4 ] . max
924+ pages_width = [ stats . max_by { |stat | stat [ :pages ] } [ :pages ] . to_s . length , 5 ] . max
925+ allocs_width = [ stats . max_by { |stat | stat [ :allocations ] } [ :allocations ] . to_s . length , 6 ] . max
926+ lines_width = [ stats . max_by { |stat | stat [ :lines ] } [ :lines ] . to_s . length , 5 ] . max
927+ length_width = [ length_strings . max_by ( &:length ) . length , 4 ] . max
928+ total_width = pages_width + used_width + allocs_width + lines_width + length_width + 11
929+
930+ puts format ( " %#{ lines_width } s %#{ length_width } s %#{ pages_width } s %#{ used_width } s %#{ allocs_width } s %s" , "Lines" , "Size" , "Pages" , "Used" , "Allocs" , "File" )
931+ puts " #{ "-" * ( total_width + relatives . max_by ( &:length ) . length ) } "
932+
933+ stats . each_with_index do |stat , index |
934+ relative = relatives [ index ]
935+ used = used_strings [ index ]
936+ length = length_strings [ index ]
937+ color = stat [ :pages ] > 1 ? :yellow : :green
938+ colored_used = send ( color , used )
939+ padding = colored_used . length - used . length
940+ puts format ( " %#{ lines_width } d %#{ length_width } s %#{ pages_width } d %#{ used_width + padding } s %#{ allocs_width } d %s" , stat [ :lines ] , length , stat [ :pages ] , colored_used , stat [ :allocations ] , relative )
941+ end
942+
943+ total_bytes = stats . sum { |stat | stat [ :bytes ] }
944+ max = stats . first
945+
946+ puts ""
947+ puts " #{ label ( "Total" ) } #{ cyan ( format_bytes ( total_bytes ) ) } across #{ cyan ( "#{ stats . size } #{ pluralize ( stats . size , "file" ) } " ) } "
948+ puts " #{ label ( "Largest" ) } #{ cyan ( relative_path ( max [ :file ] ) ) } (#{ cyan ( format_bytes ( max [ :bytes ] ) ) } , #{ cyan ( "#{ max [ :pages ] } #{ pluralize ( max [ :pages ] , "page" ) } " ) } )"
949+
950+ thresholds = { "16 KB" => 16 * 1024 , "64 KB" => 64 * 1024 , "128 KB" => 128 * 1024 , "256 KB" => 256 * 1024 , "512 KB" => 512 * 1024 }
951+
952+ puts ""
953+ thresholds . each do |label_text , threshold |
954+ count = stats . count { |stat | stat [ :bytes ] > threshold }
955+ puts " #{ label ( " > #{ label_text } " ) } #{ count } #{ pluralize ( count , "file" ) } "
956+ end
957+ end
958+
959+ def format_bytes ( bytes )
960+ if bytes >= 1024 * 1024
961+ "#{ ( bytes / ( 1024.0 * 1024.0 ) ) . round ( 1 ) } MB"
962+ elsif bytes >= 1024
963+ "#{ ( bytes / 1024.0 ) . round ( 0 ) } KB"
964+ else
965+ "#{ bytes } B"
966+ end
967+ end
881968 end
882969end
0 commit comments