############################################################################
#
#	File:     iprofl.icn
#
#	Subject:  Program to annotate source code with execution profile
#
#	Author:   Gregg M. Townsend
#
#	Date:     July 13, 2011
#
############################################################################
#
#	This file is in the public domain.
#
############################################################################
#
#	Iprofl combines Icon execution profiling output (see icont(1))
#	with source code files to produce an annotated listing.
#
#	Usage:   iprofl [file]
#
#	Iprofl reads an execution profile from a named file or from
#	standard input if none is specified.  Source files referenced
#	in the profile file are listed with corresponding nonzero profile
#	counts (ticks and visits) prepended to the left of each line.
#
#	Tick counts are given as a one-to-three-digit "per mille" value
#	(like a baseball "percentage") of the total number of ticks seen.
#	A value of "0" indicates a very small number of ticks whereas
#	a blank field indicates no ticks at all.
#
#	Iprofl lists only files in the current directory.  Files named in
#	the profile file but not found are only summarized.  This is not
#	considered an error because it is typical for library files to be
#	included in the profile output.
#
############################################################################
#
#  Links:  records, strings
#
############################################################################

link records
link strings

$define INDENT "                "		# shift by 16 to preserve tabs

record bucket(ticks, visits, fname, lnum)	# hash bucket (one data line)

global tticks					# total ticks

procedure main(args)
   local a, b, f, line, fname, flist, ftable

   if fname := get(args) then
      f := open(fname) | stop("cannot open ", fname)
   else
      f := &input

   flist := list()			# list of profiled files in order seen
   ftable := table()			# table of bucket lists, indexed by file

   # read profile data into memory
   while line := read(f) do {
      b := parse(line) | stop("bad profile input")
      a := ftable[b.fname]
      if /a then
         put(flist, a := ftable[b.fname] := list())
      put(a, b)
      }
   close(f)

   # count the total number of ticks
   tticks := 0
   every tticks +:= (!!flist).ticks

   # list one file at a time, in the order first seen
   every a := !flist do {
      fname := (!a).fname
      write()
      write()
      write(map(INDENT, " ", "="), center(" " || fname || " ", 79, "="))
      write()
      if f := open(fname) then {
         listfile(f, a)
         close(f)
         }
      else
         nofile(a)
      }

end

procedure parse(line)			#: parse a profile line, failing on err
   local ticks, visits, fname, lnum

   line ? {
      tab(many(' \t'))
      ticks := integer(tab(many(&digits)))		| fail
      tab(many(' \t'))					| fail
      visits := integer(tab(many(&digits)))		| fail
      tab(many(' \t'))					| fail	
      fname := tab(upto(' '))				| fail
      tab(many(' \t'))					| fail	
      lnum := integer(tab(many(&digits)))		| fail
      return bucket(ticks, visits, fname, lnum)
      }
end

procedure nofile(blist)			#: summarize a file not found
   local b, t, v

   t := v := 0
   every b := !blist do {
      t +:= b.ticks
      v +:= b.visits
      }
   showline(t, v, "[total over all lines]")
   return
end

procedure listfile(f, blist)		#: list a file with annotations
   local n, b, t

   n := 0
   every b := !sortf(blist, fieldnum(bucket(), "lnum")) do {
      if b.lnum <= n then			# if out of order line
         showline(b.ticks, b.visits, "[" || b.lnum - n - 1 || "]")
      else {
         while (n +:= 1) < b.lnum do
            write(INDENT, read(f))			# list skipped lines
         showline(b.ticks, b.visits, read(f) | "[missing]")  # list sampled line
         }
      }
   while write(INDENT, read(f))		# list unsampled lines at EOF
   return
end

procedure showline(t, v, s)		#: show ticks and visits with one line
   if t = 0 then
      t := "   "			# blank if no ticks
   else {
      t := 1000.0 * t / tticks + 0.5	# else per-mille value
      t >:= 999				# (changing 1000 to 999 to fit 3 chars)
      t := right(integer(t), 3)
      }

   write(t, right(v, 11), "  ", s)	# 16 chars of annotation, plus the line
   return
end
