trac-inf.lmt /size: 15 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['trac-inf'] = {
2    version   = 1.001,
3    comment   = "companion to trac-inf.mkiv",
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-- As we want to protect the global tables, we no longer store the timing
10-- in the tables themselves but in a hidden timers table so that we don't
11-- get warnings about assignments. This is more efficient than using rawset
12-- and rawget.
13
14local type, tonumber, select = type, tonumber, select
15local format, lower, find, gsub, rep = string.format, string.lower, string.find, string.gsub, string.rep
16local concat, sortedhash = table.concat, table.sortedhash
17local clock  = os.gettimeofday or os.clock -- should go in environment
18
19local setmetatableindex = table.setmetatableindex
20local serialize         = table.serialize
21local formatters        = string.formatters
22
23statistics              = statistics or { }
24local statistics        = statistics
25
26statistics.enable       = true
27statistics.threshold    = 0.01
28
29local statusinfo, n, registered, timers = { }, 0, { }, { }
30
31setmetatableindex(timers,function(t,k)
32    local v = { timing = 0, loadtime = 0, offset = 0 }
33    t[k] = v
34    return v
35end)
36
37local function hastiming(instance)
38    return instance and timers[instance]
39end
40
41local function resettiming(instance)
42    timers[instance or "notimer"] = { timing = 0, loadtime = 0, offset = 0 }
43end
44
45-- local ticks   = clock
46-- local seconds = function(n) return n or 0 end
47
48local ticks   = lua.getpreciseticks
49local seconds = lua.getpreciseseconds
50
51local function starttiming(instance,reset)
52    local timer = timers[instance or "notimer"]
53    local it = timer.timing
54    if reset then
55        it = 0
56        timer.loadtime = 0
57    end
58    if it == 0 then
59        timer.starttime = ticks()
60        if not timer.loadtime then
61            timer.loadtime = 0
62        end
63    end
64    timer.timing = it + 1
65end
66
67local function stoptiming(instance)
68    local timer = timers[instance or "notimer"]
69    local it = timer.timing
70    if it > 1 then
71        timer.timing = it - 1
72    else
73        local starttime = timer.starttime
74        if starttime and starttime > 0 then
75            local stoptime  = ticks()
76            local loadtime  = stoptime - starttime
77            timer.stoptime  = stoptime
78            timer.loadtime  = timer.loadtime + loadtime
79            timer.timing    = 0
80            timer.starttime = 0
81        end
82    end
83end
84
85local function benchmarktimer(instance)
86    local timer = timers[instance or "notimer"]
87    local it = timer.timing
88    if it > 1 then
89        timer.timing = it - 1
90    else
91        local starttime = timer.starttime
92        if starttime and starttime > 0 then
93            timer.offset = ticks() - starttime
94        else
95            timer.offset = 0
96        end
97    end
98end
99
100local function elapsed(instance)
101    if type(instance) == "number" then
102        return instance
103    else
104        local timer = timers[instance or "notimer"]
105        return timer and seconds(timer.loadtime - 2*(timer.offset or 0)) or 0
106    end
107end
108
109local function currenttime(instance)
110    if type(instance) == "number" then
111        return instance
112    else
113        local timer = timers[instance or "notimer"]
114        local it = timer.timing
115        if it > 1 then
116            -- whatever
117        else
118            local starttime = timer.starttime
119            if starttime and starttime > 0 then
120                return seconds(timer.loadtime + ticks() - starttime -  2*(timer.offset or 0))
121            end
122        end
123        return 0
124    end
125end
126
127local function elapsedtime(instance)
128    return format("%0.3f",elapsed(instance))
129end
130
131local function elapsedindeed(instance)
132    return elapsed(instance) > statistics.threshold
133end
134
135local function elapsedseconds(instance,rest) -- returns nil if 0 seconds
136    if elapsedindeed(instance) then
137        return format("%0.3f seconds %s", elapsed(instance),rest or "")
138    end
139end
140
141statistics.hastiming      = hastiming
142statistics.resettiming    = resettiming
143statistics.starttiming    = starttiming
144statistics.stoptiming     = stoptiming
145statistics.currenttime    = currenttime
146statistics.elapsed        = elapsed
147statistics.elapsedtime    = elapsedtime
148statistics.elapsedindeed  = elapsedindeed
149statistics.elapsedseconds = elapsedseconds
150statistics.benchmarktimer = benchmarktimer
151
152-- general function .. we might split this module
153
154function statistics.register(tag,fnc)
155    if statistics.enable and type(fnc) == "function" then
156        -- second load can overload:
157        local rt = registered[tag] or (#statusinfo + 1)
158        statusinfo[rt] = { tag, fnc }
159        registered[tag] = rt
160        if #tag > n then n = #tag end
161    end
162end
163
164local report = logs.reporter("mkiv lua stats")
165
166function statistics.show()
167    if statistics.enable then
168        -- this code will move
169        local register = statistics.register
170        register("used platform", function()
171            return format("%s, type: %s, binary subtree: %s",
172                os.platform or "unknown",os.type or "unknown", environment.texos or "unknown")
173        end)
174        register("used engine", function()
175            return format("%s version: %s, functionality level: %s, format id: %s, compiler: %s",
176                LUATEXENGINE, LUATEXVERBOSE, LUATEXFUNCTIONALITY, LUATEXFORMATID, status.used_compiler)
177        end)
178        register("tex properties", function()
179            local t = status.gethashstate()
180            local l = status.getlookupstate()
181            local m = status.gettexstate()
182            return format("%s hash slots used of %s, %s control sequences, approximate memory usage: %i MB",
183                t.top, t.max, l.ptr, m.approximate // (1024 * 1024))
184        end)
185        register("callbacks", statistics.callbacks)
186        -- so far
187        register("lua properties",function()
188         -- local hash = lua.gethashchars()
189            local mask = load([[τεχ = 1]]) and "utf" or "ascii"
190         -- return format("engine: %s %s, used memory: %s, hash chars: min(%i,40), symbol mask: %s (%s)",
191            return format("engine: lua %s, format %s, used memory: %s, symbol mask: %s (%s)",
192                LUAVERSION, LUAFORMAT, statistics.memused(), mask, mask == "utf" and "τεχ" or "tex")
193        end)
194        register("runtime",statistics.runtime)
195        logs.newline() -- initial newline
196        for i=1,#statusinfo do
197            local s = statusinfo[i]
198            local r = s[2]()
199            if r then
200                report("%s: %s",s[1],r)
201            end
202        end
203     -- logs.newline() -- final newline
204        statistics.enable = false
205    end
206end
207
208function statistics.memused() -- no math.round yet -)
209    local round    = math.round or math.floor
210    local luastate = status.getluastate()
211    return format("%s MB, ctx: %s MB, max: %s MB",
212        round(collectgarbage("count")//1024),
213        round(luastate.statebytes//1048576),
214        luastate.statebytesmax and round(luastate.statebytesmax//1048576) or "unknown"
215    )
216end
217
218starttiming(statistics)
219
220function statistics.formatruntime(runtime) -- indirect so it can be overloaded and
221    return format("%s seconds", runtime)   -- indeed that happens in cure-uti.lua
222end
223
224function statistics.runtime()
225    stoptiming(statistics)
226 -- stoptiming(statistics) -- somehow we can start the timer twice, but where
227    local runtime = lua.getruntime and lua.getruntime() or elapsedtime(statistics)
228    return statistics.formatruntime(runtime)
229end
230
231local report = logs.reporter("system")
232
233function statistics.timed(action,all)
234    starttiming("run")
235    action()
236    stoptiming("run")
237    local runtime = tonumber(elapsedtime("run"))
238    if all then
239        local alltime = tonumber(lua.getruntime and lua.getruntime() or elapsedtime(statistics))
240        if alltime and alltime > 0 then
241            report("total runtime: %0.3f seconds of %0.3f seconds",runtime,alltime)
242            return
243        end
244    end
245    report("total runtime: %0.3f seconds",runtime)
246end
247
248-- goodie
249
250function statistics.tracefunction(base,tag,...)
251    for i=1,select("#",...) do
252        local name = select(i,...)
253        local stat = { }
254        local func = base[name]
255        setmetatableindex(stat,function(t,k) t[k] = 0 return 0 end)
256        base[name] = function(n,k,v) stat[k] = stat[k] + 1 return func(n,k,v) end
257        statistics.register(formatters["%s.%s"](tag,name),function() return serialize(stat,"calls") end)
258    end
259end
260
261-- now here
262
263status.iocodes = setmetatableindex(tex.getiovalues(), function() return "unknown" end)
264
265local report = logs.reporter("system")
266
267local list   = {
268    "string", "pool", "hash", "lookup", "node", "token",
269    "buffer", "input", "file", "nest", "parameter", "save", "font", "language", "mark", "insert", "mvl",
270 -- "extra", "sparse",
271}
272
273local function show(data,fields)
274    local line    = rep("-",3+11*#list)
275    local columns = rep("%11s",#list)
276    report("")
277    report("%w%s",2,line)
278    report("%w"..columns,5,unpack(list))
279    report("%w%s",2,line)
280    for i=1,#fields do
281        local f = fields[i]
282        if f then
283            local t = { }
284            for i=1,#list do
285                local n = data[list[i].."state"][f]
286                t[i] = n < 0 and formatters["%w"](11) or formatters["%11i"](n)
287            end
288            report("  %3s"..columns,f,unpack(t))
289        else
290            report("")
291        end
292    end
293    report("%w%s",2,line)
294    report("")
295end
296
297function statistics.showmemory(when)
298    report("")
299    report("memory configuration")
300    show(status.list(), { "max", "min", "set", "stp" })
301end
302
303local registered = false
304local enabled    = false
305local finished   = false
306
307local status = status -- needed for --sandbox
308local mplib  = mplib  -- needed for --sandbox
309
310function statistics.showusage(when)
311    if finished and when == "finish" then
312        return
313    else
314        finished = true
315    end
316    local s = status  .list()
317    local c = status  .getcallbackstate() -- status.callbacks
318    local m = mplib   .getcallbackstate()
319    local b = backends.getcallbackstate()
320    local l = status  .getlinebreakstate()
321    local p = status  .getbalancestate()
322    local h = status  .gethyphenationstate()
323    do
324        --
325        local estatus = s.expandstate
326        local tstatus = s.texstate
327        local lstatus = s.luastate
328        local rstatus = s.readstate
329        --
330        local pstatus = logs.private.getpagetiming()
331        --
332        local iocode  = status.iocodes[rstatus.iocode]
333        --
334        local mpinstances, mpmemory = metapost.getstatistics(true)
335        --
336        report("")
337        if when == "finish" then
338            report("status after finishing run")
339        else
340            report("status after shipping out page %s",tex.getcount("realpageno"))
341        end
342        show(s, { "max", "min", "set", "stp", false, "mem", "itm", "ext", "all", false, "ini", "ptr", "top" })
343        report("  current input type    : %s", iocode)
344        if iocode == "file" then
345            report("  current file name     : %s", rstatus.filename or "")
346            report("  current line number   : %s", rstatus.linenumber)
347        end
348        report("")
349        report("  approximate memory    : %s (%s MB)", tstatus.approximate, tstatus.approximate // 1048576)
350        report("")
351        report("  expansion depth       : min: %s, max: %s, set: %s, top: %s", estatus.min, estatus.max, estatus.set, estatus.top)
352        report("")
353        report("  luabytecode registers : %s", lstatus.bytecodes)
354        report("  luabytecode bytes     : %s (%s MB)", lstatus.bytecodebytes, lstatus.bytecodebytes // 1048576)
355        report("  luastate bytes now    : %s (%s MB)", lstatus.statebytes,    lstatus.statebytes    // 1048576)
356        report("  luastate bytes max    : %s (%s MB)", lstatus.statebytesmax, lstatus.statebytesmax // 1048576)
357        report("")
358        report("  file callbacks        : %s", c.file)
359        report("  saved callbacks       : %s", c.saved)
360        report("  direct callbacks      : %s", c.direct)
361        report("  function callbacks    : %s", c["function"])
362        report("  value callbacks       : %s", c.value)
363        report("  message callbacks     : %s", c.message)
364        report("  bytecode callbacks    : %s", c.bytecode)
365        report("")
366        report("  mp instances          : %s", mpinstances)
367        report("  mp estimated memory   : %s (%s MB)", mpmemory, mpmemory // 1048576)
368        report("  mp file callbacks     : %s", m.file)
369        report("  mp text callbacks     : %s", m.text)
370        report("  mp script callbacks   : %s", m.script)
371        report("  mp log callbacks      : %s", m.log)
372        report("")
373        report("  total callbacks       : %s", c.count)
374        report("  mp total callbacks    : %s", m.count)
375        report("  backend callbacks     : %s", b.count)
376        report("")
377    end
378    do
379        local done = false
380        local round = math.round
381        local n_calls = 0
382        local n_first = 0
383        local n_second = 0
384        local n_final = 0
385        local n_specification = 0
386        local n_sub = 0
387        for k, v in sortedhash(l) do
388            if type(v) == "table" then
389                local calls = v.calls or 0
390                if v.calls > 0 then
391                    local calls, first, second, final, specification, sub = v.calls, v.first, v.second, v.final, v.specification,v.sub
392                    if not done then
393                        report("  linebreak statistics  : context      calls     first    second     final  passonly   subpass")
394                        report("")
395                        done = true
396                    end
397                    report("  linebreak statistics  : %-8s  %8i  %8i  %8i  %8i  %8i  %8i",k,calls,first,second,final,specification,sub)
398                    n_calls = n_calls + calls
399                    n_first = n_first+ first
400                    n_second = n_second + second
401                    n_final = n_final + final
402                    n_specification = n_specification + specification
403                    n_sub = n_sub + sub
404                end
405            end
406        end
407        if done then
408            report("")
409            report("  linebreak statistics  :           %8i  %8i  %8i  %8i  %8i  %8i",n_calls,n_first,n_second,n_final,n_specification,n_sub)
410        end
411    end
412    do
413        if p.calls > 0 then
414            report("")
415            for k, v in table.sortedhash(p) do
416                if v ~= 0 then
417                    report("  balance state         : %-14s  %7i",k,v)
418                end
419            end
420        end
421    end
422    do
423     -- if h.checked > 0 then
424            report("")
425            for k, v in table.sortedhash(h) do
426                if v ~= 0 then
427                    report("  hyphenation state     : %-10s  %7i",k,v)
428                    done = true
429                end
430            end
431     -- end
432    end
433    do
434        if pstatus then
435            report("")
436            report("  page numbers          : realpage %s, userpage %s, subpage %s",pstatus.page.real,pstatus.page.user,pstatus.page.sub)
437            report("  page timing           : total %0.03f, page %0.03f, average %0.03f",pstatus.time.elapsed,pstatus.time.page,pstatus.time.average)
438        end
439    end
440    report("")
441end
442
443trackers.register("system.usage", function(v)
444    if v and not registered then
445        logs.private.enablepagetiming()
446        if v ~= "summary" then
447            luatex.registerpageactions(function()
448                if enabled then
449                    statistics.showusage("page")
450                end
451            end)
452        end
453        luatex.registerstopactions(function()
454            if enabled then
455                statistics.showusage("finish")
456            end
457        end)
458        registered = true
459    end
460    enabled = v
461end)
462
463-- can't be done here:
464--
465-- luatex.registerstopactions(function()
466--     if not enabled then
467--         logs.push("logfile")
468--         statistics.showusage("finish")
469--         logs.pop()
470--     end
471-- end)
472