mtx-profile.lua /size: 7011 b    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['mtx-profile'] = {
2    version   = 1.000,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo: also line number
10-- todo: sort runtime as option
11-- todo: make it more efficient .. real old code
12
13local match, format, find, gsub = string.match, string.format, string.find, string.gsub
14
15local helpinfo = [[
16<?xml version="1.0"?>
17<application>
18 <metadata>
19  <entry name="name">mtx-profile</entry>
20  <entry name="detail">ConTeXt MkIV LuaTeX Profiler</entry>
21  <entry name="version">1.00</entry>
22 </metadata>
23 <flags>
24  <category name="basic">
25   <subcategory>
26    <flag name="analyze"><short>analyze lua calls</short></flag>
27    <flag name="trace"><short>analyze tex calls</short></flag>
28   </subcategory>
29  </category>
30 </flags>
31</application>
32]]
33
34local application = logs.application {
35    name     = "mtx-profile",
36    banner   = "ConTeXt MkIV LuaTeX Profiler 1.00",
37    helpinfo = helpinfo,
38}
39
40local report = application.report
41
42scripts          = scripts or { }
43scripts.profiler = scripts.profiler or { }
44
45local timethreshold    = 0
46local callthreshold    = 2500
47local countthreshold   = 2500
48
49local functiontemplate  = "%12s %03.4f %9i %s"
50local calltemplate      = "%9i %s"
51local totaltemplate     = "%i internal calls, %i function calls taking %3.4f seconds"
52local thresholdtemplate = "thresholds: %i internal calls, %i function calls, %i seconds"
53
54function scripts.profiler.analyze(filename)
55    local f = io.open(filename)
56    if f then
57        local times, counts, calls = { }, { }, { }
58        local totalruntime, totalcount, totalcalls = 0, 0, 0
59        for line in f:lines() do
60            if not find(line,"__index") and not find(line,"__newindex") then
61                local stacklevel, filename, functionname, linenumber, currentline, localtime, totaltime = match(line,"^(%d+)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)")
62                if not filename then
63                    -- next
64                elseif filename == "=[C]" then
65                    if not functionname:find("^%(") then
66                        calls[functionname] = (calls[functionname] or 0) + 1
67                    end
68                else
69                    local filename = match(filename,"^@(.*)$")
70                    if filename then
71                        local fi = times[filename]
72                        if not fi then fi = { } times[filename] = fi end
73                        fi[functionname] = (fi[functionname] or 0) + tonumber(localtime)
74                        counts[functionname] = (counts[functionname] or 0) + 1
75                    end
76                end
77            end
78        end
79        f:close()
80        print("")
81        local loaded = { }
82        local sorted = table.sortedkeys(times)
83        for i=1,#sorted do
84            local filename = sorted[i]
85            local functions = times[filename]
86            local sorted = table.sortedkeys(functions)
87            for i=1,#sorted do
88                local functionname = sorted[i]
89                local totaltime = functions[functionname]
90                local count = counts[functionname]
91                totalcount = totalcount + count
92                if totaltime > timethreshold or count > countthreshold then
93                    totalruntime = totalruntime + totaltime
94                    local functionfile, somenumber = match(functionname,"^@(.+):(.-)$")
95                    if functionfile then
96                        local number = tonumber(somenumber)
97                        if number then
98                            if not loaded[functionfile] then
99                                loaded[functionfile] = string.splitlines(io.loaddata(functionfile) or "")
100                            end
101                            functionname = loaded[functionfile][number] or functionname
102                            functionname = gsub(functionname,"^%s*","")
103                            functionname = gsub(functionname,"%s*%-%-.*$","")
104                            functionname = number .. ": " .. functionname
105                        end
106                    end
107                    filename = file.basename(filename)
108                    print(format(functiontemplate,filename,totaltime,count,functionname))
109                end
110            end
111        end
112        print("")
113        local sorted = table.sortedkeys(calls)
114        for i=1,#sorted do
115            local call = sorted[i]
116            local n = calls[call]
117            totalcalls = totalcalls + n
118            if n > callthreshold then
119                print(calltemplate:format(n,call))
120            end
121        end
122        print("")
123        print(totaltemplate:format(totalcalls,totalcount,totalruntime))
124        print("")
125        print(thresholdtemplate:format(callthreshold,countthreshold,timethreshold))
126    end
127end
128
129function scripts.profiler.x_analyze(filename)
130    local f = io.open(filename)
131    local calls = { }
132    local lines = 0
133    if f then
134        while true do
135            local line = f:read()
136            if line then
137                lines = lines + 1
138                local c = match(line,"\\([a-zA-Z%!%?@]+) *%->")
139                if c then
140                    local cc = calls[c]
141                    if not cc then
142                        calls[c] = 1
143                    else
144                        calls[c] = cc + 1
145                    end
146                end
147            else
148                break
149            end
150        end
151        f:close()
152        local noc = 0
153        local criterium = 100
154        for name, n in next, calls do
155            if n > criterium then
156                if find(name,"^@@[a-z][a-z]") then
157                    -- parameter
158                elseif find(name,"^[cvserft]%!") then
159                    -- variables and constants
160                elseif find(name,"^%?%?[a-z][a-z]$") then
161                    -- prefix
162                elseif find(name,"^%!%!") then
163                    -- reserved
164                elseif find(name,"^@.+@$") then
165                    -- weird
166                else
167                    noc = noc + n
168                    print(format("%6i: %s",n,name))
169                end
170            end
171        end
172        print("")
173        print(format("number of lines: %s",lines))
174        print(format("number of calls: %s",noc))
175        print(format("criterium calls: %s",criterium))
176    end
177end
178
179--~ scripts.profiler.analyze("t:/manuals/mk/mk-fonts-profile.lua")
180--~ scripts.profiler.analyze("t:/manuals/mk/mk-introduction-profile.lua")
181
182if environment.argument("analyze") then
183    scripts.profiler.analyze(environment.files[1] or "luatex-profile.log")
184elseif environment.argument("trace") then
185    scripts.profiler.analyze(environment.files[1] or "temp.log")
186elseif environment.argument("exporthelp") then
187    application.export(environment.argument("exporthelp"),environment.files[1])
188else
189    application.help()
190end
191