luat-cbk.lua /size: 12 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['luat-cbk'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.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
9local insert, remove, concat = table.insert, table.remove, table.concat
10local find, format = string.find, string.format
11local collectgarbage, type, next = collectgarbage, type, next
12local round = math.round
13local sortedhash, sortedkeys, tohash = table.sortedhash, table.sortedkeys, table.tohash
14
15-- Callbacks are the real asset of LuaTeX. They permit you to hook your own code
16-- into the TeX engine. Here we implement a few handy auxiliary functions. Watch
17-- out, there are diferences between LuateX and LuaMetaTeX.
18
19callbacks       = callbacks or { }
20local callbacks = callbacks
21
22-- When you (temporarily) want to install a callback function, and after a while
23-- wants to revert to the original one, you can use the following two functions.
24-- This only works for non-frozen ones.
25
26local trace_callbacks   = false  trackers.register("system.callbacks", function(v) trace_callbacks = v end)
27local trace_calls       = false  -- only used when analyzing performance and initializations
28local trace_checking    = false  trackers.register("memory.checking", function(v) trace_checking = v end)
29
30local report_system     = logs.reporter("system")
31local report_callbacks  = logs.reporter("system","callbacks")
32local report_memory     = logs.reporter("system","memory")
33
34local register_callback = callback.register
35local find_callback     = callback.find
36local list_callbacks    = callback.list
37local register_usercall = false
38local original_register = register_callback
39
40local frozen            = { }
41local stack             = { }
42local list              = callbacks.list
43local permit_overloads  = false
44local block_overloads   = false
45
46-- By now most callbacks are frozen and most provide a way to plug in your own code.
47-- For instance all node list handlers provide before/after namespaces and the file
48-- handling code can be extended by adding schemes and if needed I can add more
49-- hooks. So there is no real need to overload a core callback function. It might be
50-- ok for quick and dirty testing but anyway you're on your own if you permanently
51-- overload callback functions.
52
53-- This might become a configuration file only option when it gets abused too much.
54
55directives.register("system.callbacks.permitoverloads", function(v)
56    if block_overloads or permit_overloads then
57        -- once bad news, always bad news
58    elseif v then
59        permit_overloads = { }
60        report_system()
61        report_system("The callback system has been brought in an unprotected state. As a result of directly")
62        report_system("setting of callbacks subsystems of ConTeXt can stop working. There is no support for")
63        report_system("bugs resulting from this state. It's better to use the official extension mechanisms.")
64        report_system()
65    end
66end)
67
68sandbox.initializer {
69    category = "functions",
70    action   = function()
71        block_overloads = true
72    end
73}
74
75if not list then -- otherwise counters get reset
76
77    list = utilities.storage.allocate(list_callbacks())
78
79    local supported = { }
80
81    for k in next, list do
82        list[k]      = 0
83        supported[k] = true
84    end
85
86    callbacks.list      = list
87    callbacks.supported = supported
88
89end
90
91local delayed = tohash {
92    "buildpage_filter",
93}
94
95if trace_calls then
96
97    local functions = { }
98
99    register_callback = function(name,func)
100        if type(func) == "function" then
101            if functions[name] then
102                functions[name] = func
103                return find_callback(name)
104            else
105                functions[name] = func
106                local cnuf = function(...)
107                    list[name] = list[name] + 1
108                    return functions[name](...)
109                end
110                return original_register(name,cnuf)
111            end
112        else
113            return original_register(name,func)
114        end
115    end
116
117end
118
119-- temporary, not public:
120
121callbacks.functions = { }
122
123-- till here
124
125local reported = { }
126
127local function register_usercall(what,name,func)
128    if list[name] then
129        if trace_callbacks or not reported[name] then
130            report_system()
131            report_system("disabling core code by %s user function into callback '%s' (reported only once)",what,name)
132            report_system()
133            reported[name] = true
134        end
135        permit_overloads[name] = true
136        return original_register(name,function(...)
137            if trace_callbacks then
138                report_callbacks("calling user function from '%s'",name)
139            end
140            return func(...)
141        end)
142    else
143        report_callbacks("not %s function into invalid callback '%s'",name)
144        return nil, format("unknown callback '%s'",name)
145    end
146end
147
148local function frozen_callback(name)
149    report_callbacks("not %s frozen %a","registering",name)
150    return nil, format("callback '%s' is frozen",name) -- no formatter yet
151end
152
153local function state(name)
154    local f = find_callback(name)
155    if f == false then
156        return "disabled"
157    elseif f then
158        return "enabled"
159    else
160        return "undefined"
161    end
162end
163
164function callbacks.known(name)
165    return list[name]
166end
167
168function callbacks.report()
169    for name, _ in sortedhash(list) do
170        local str = frozen[name]
171        if str then
172            report_callbacks("%s: %s -> %s",state(name),name,str)
173        else
174            report_callbacks("%s: %s",state(name),name)
175        end
176    end
177end
178
179function callbacks.freeze(name,freeze)
180    if not permit_overloads then
181        freeze = type(freeze) == "string" and freeze
182        if find(name,"*",1,true) then
183            local pattern = name
184            for name, _ in next, list do
185                if find(name,pattern) then
186                    frozen[name] = freeze or frozen[name] or "frozen"
187                end
188            end
189        else
190            frozen[name] = freeze or frozen[name] or "frozen"
191        end
192    end
193end
194
195function callbacks.register(name,func,freeze)
196    if frozen[name] then
197        if permit_overloads then
198            return register_usercall("registering",name,func)
199        else
200            return frozen_callback(name)
201        end
202    elseif freeze then
203        frozen[name] = type(freeze) == "string" and freeze or "registered"
204    end
205    if delayed[name] and environment.initex then
206        return nil
207    end
208    return register_callback(name,func)
209end
210
211function callback.register(name,func) -- original
212    if not frozen[name] then
213        return register_callback(name,func)
214    elseif permit_overloads then
215        return register_usercall("registering",name,func)
216    else
217        return frozen_callback(name)
218    end
219end
220
221function callbacks.push(name,func)
222    if not frozen[name] or permit_overloads then
223        local sn = stack[name]
224        if not sn then
225            sn = { }
226            stack[name] = sn
227        end
228        insert(sn,find_callback(name))
229        if permit_overloads then
230            register_usercall("pushing",name,func)
231        else
232            register_callback(name,func)
233        end
234    else
235        report_callbacks("not %s frozen %a","pushing",name)
236    end
237end
238
239function callbacks.pop(name)
240    if not frozen[name] or permit_overloads then
241        local sn = stack[name]
242        if not sn or #sn == 0 then
243            -- some error
244            register_callback(name,nil) -- ! really needed
245        else
246         -- this fails: register_callback(name, remove(stack[name]))
247            local func = remove(sn)
248            register_callback(name,func)
249        end
250    end
251end
252
253if trace_calls then
254    statistics.register("callback details", function()
255        local t = { } -- todo: pass function to register and quit at nil
256        for name, n in sortedhash(list) do
257            if n > 0 then
258                t[#t+1] = format("%s -> %s",name,n)
259            end
260        end
261        return concat(t," ")
262    end)
263end
264
265statistics.register("callbacks overloaded by user", function()
266    if permit_overloads then
267        return concat(sortedkeys(permit_overloads)," ")
268    end
269end)
270
271-- -- somehow crashes later on
272--
273-- callbacks.freeze("find_.*_file","finding file")
274-- callbacks.freeze("read_.*_file","reading file")
275-- callbacks.freeze("open_.*_file","opening file")
276
277-- The simple case is to remove the callback:
278--
279--   callbacks.push('linebreak_filter')
280--   ... some actions ...
281--   callbacks.pop('linebreak_filter')
282--
283-- Often, in such case, another callback or a macro call will pop the original.
284--
285-- In practice one will install a new handler, like in:
286--
287--   callbacks.push('linebreak_filter', function(...)
288--       return something_done(...)
289--   end)
290--
291-- Even more interesting is:
292--
293--  callbacks.push('linebreak_filter', function(...)
294--      callbacks.pop('linebreak_filter')
295--      return something_done(...)
296--  end)
297--
298-- This does a one-shot.
299--
300-- Callbacks may result in Lua doing some hard work which takes time and above all
301-- resourses. Sometimes it makes sense to disable or tune the garbage collector in
302-- order to keep the use of resources acceptable.
303--
304-- At some point in the development we did some tests with counting nodes (in this
305-- case 121049).
306--
307--   setstepmul  seconds  megabytes
308--      200       24.0      80.5
309--      175       21.0      78.2
310--      150       22.0      74.6
311--      160       22.0      74.6
312--      165       21.0      77.6
313--      125       21.5      89.2
314--      100       21.5      88.4
315--
316-- The following code is kind of experimental. In the documents that describe the
317-- development of LuaTeX we report on speed tests. One observation is that it
318-- sometimes helps to restart the collector. Okay, experimental code has been
319-- removed, because messing around with the gc is too unpredictable.
320--
321-- For the moment we keep this here and not in util-gbc.lua or so.
322
323utilities                  = utilities or { }
324utilities.garbagecollector = utilities.garbagecollector or { }
325local garbagecollector     = utilities.garbagecollector
326
327garbagecollector.enabled   = false -- could become a directive
328garbagecollector.criterium = 4*1024*1024
329
330-- garbagecollector.enabled   = true
331
332-- Lua allocates up to 12 times the amount of memory needed for
333-- handling a string, and for large binary chunks (like chinese otf
334-- files) we get a prominent memory consumption. Even when a variable
335-- is nilled, there is some delay in freeing the associated memory (the
336-- hashed string) because if we do the same thing directly afterwards,
337-- we see only a slight increase in memory. For that reason it makes
338-- sense to do a collector pass after a huge file.
339--
340-- test file:
341--
342-- function test()
343--     local b = collectgarbage("count")
344--     local s = io.loaddata("some font table, e.g. a big tmc file")
345--     local a = collectgarbage("count")
346--     print(">>> STATUS",b,a,a-b,#s,1000*(a-b)/#s)
347-- end
348--
349-- test() test() test() test() collectgarbage("collect") test() test() test() test()
350--
351-- As a result of this, LuaTeX now uses an optimized version of f:read("*a"),
352-- one that does not use the 4K allocations but allocates in one step.
353
354function garbagecollector.check(size,criterium)
355    if garbagecollector.enabled then
356        criterium = criterium or garbagecollector.criterium
357        if not size or (criterium and criterium > 0 and size > criterium) then
358            if trace_checking then
359                local b = collectgarbage("count")
360                collectgarbage("collect")
361                local a = collectgarbage("count")
362                report_memory("forced sweep, collected: %s MB, used: %s MB",round((b-a)/1000),round(a/1000))
363            else
364                collectgarbage("collect")
365            end
366        end
367    end
368end
369
370-- this will move to a module
371
372commands = commands or { }
373
374function commands.showcallbacks()
375    local NC, NR, verbatim = context.NC, context.NR, context.type
376    context.starttabulate { "|l|l|p|" }
377    for name, _ in sortedhash(list) do
378        NC() verbatim(name) NC() verbatim(state(name)) NC() context(frozen[name] or "") NC() NR()
379    end
380    context.stoptabulate()
381end
382