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