Skip to content

Commit e7ea9b2

Browse files
committed
logging: avoid try/catch when the message is a simple
This should avoid the try/catch in some cases, where the message is simply a constant value or simple variable. It also tries to slightly reduce required specialization in some other cases too.
1 parent e87cb40 commit e7ea9b2

File tree

5 files changed

+150
-64
lines changed

5 files changed

+150
-64
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Standard library changes
8888
* `RegexMatch` objects can now be probed for whether a named capture group exists within it through `haskey()` ([#36717]).
8989
* For consistency `haskey(r::RegexMatch, i::Integer)` has also been added and returns if the capture group for `i` exists ([#37300]).
9090
* A new standard library `TOML` has been added for parsing and printing [TOML files](https://toml.io) ([#37034]).
91+
* Logging (such as `@warn`) no longer catches exceptions in the logger itself ([#36600]).
9192

9293
#### LinearAlgebra
9394
* New method `LinearAlgebra.issuccess(::CholeskyPivoted)` for checking whether pivoted Cholesky factorization was successful ([#36002]).

base/logging.jl

Lines changed: 112 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ _log_record_ids = Set{Symbol}()
254254
# across versions of the originating module, provided the log generating
255255
# statement itself doesn't change.
256256
function log_record_id(_module, level, message, log_kws)
257+
@nospecialize
257258
modname = _module === nothing ? "" : join(fullname(_module), "_")
258259
# Use an arbitrarily chosen eight hex digits here. TODO: Figure out how to
259260
# make the id exactly the same on 32 and 64 bit systems.
@@ -274,48 +275,112 @@ end
274275

275276
default_group(file) = Symbol(splitext(basename(file))[1])
276277

278+
function issimple(@nospecialize val)
279+
val isa String && return true
280+
val isa Symbol && return true
281+
val isa QuoteNode && return true
282+
val isa Number && return true
283+
val isa Char && return true
284+
if val isa Expr
285+
val.head === :quote && issimple(val[1]) && return true
286+
val.head === :inert && return true
287+
end
288+
return false
289+
end
290+
function issimplekw(@nospecialize val)
291+
if val isa Expr
292+
if val.head === :kw
293+
val = val.args[2]
294+
if val isa Expr && val.head === :escape
295+
issimple(val.args[1]) && return true
296+
end
297+
end
298+
end
299+
return false
300+
end
301+
277302
# Generate code for logging macros
278303
function logmsg_code(_module, file, line, level, message, exs...)
304+
@nospecialize
279305
log_data = process_logmsg_exs(_module, file, line, level, message, exs...)
280-
quote
281-
let
282-
level = $level
283-
std_level = convert(LogLevel, level)
284-
if std_level >= getindex(_min_enabled_level)
285-
group = $(log_data._group)
286-
_module = $(log_data._module)
287-
logger = current_logger_for_env(std_level, group, _module)
288-
if !(logger === nothing)
289-
id = $(log_data._id)
290-
# Second chance at an early bail-out (before computing the message),
291-
# based on arbitrary logger-specific logic.
292-
if _invoked_shouldlog(logger, level, _module, group, id)
293-
file = $(log_data._file)
294-
line = $(log_data._line)
295-
try
296-
msg = $(esc(message))
297-
handle_message(
306+
if !isa(message, Symbol) && issimple(message) && isempty(log_data.kwargs)
307+
logrecord = quote
308+
msg = $(message)
309+
kwargs = (;)
310+
true
311+
end
312+
elseif issimple(message) && all(issimplekw, log_data.kwargs)
313+
# if message and kwargs are just values and variables, we can avoid try/catch
314+
# complexity by adding the code for testing the UndefVarError by hand
315+
checkerrors = nothing
316+
for kwarg in reverse(log_data.kwargs)
317+
if isa(kwarg.args[2].args[1], Symbol)
318+
checkerrors = Expr(:if, Expr(:isdefined, kwarg.args[2]), checkerrors, Expr(:call, Expr(:core, :UndefVarError), QuoteNode(kwarg.args[2].args[1])))
319+
end
320+
end
321+
if isa(message, Symbol)
322+
message = esc(message)
323+
checkerrors = Expr(:if, Expr(:isdefined, message), checkerrors, Expr(:call, Expr(:core, :UndefVarError), QuoteNode(message.args[1])))
324+
end
325+
logrecord = quote
326+
let err = $checkerrors
327+
if err === nothing
328+
msg = $(message)
329+
kwargs = (;$(log_data.kwargs...))
330+
true
331+
else
332+
logging_error(logger, level, _module, group, id, file, line, err, false)
333+
false
334+
end
335+
end
336+
end
337+
else
338+
logrecord = quote
339+
try
340+
msg = $(esc(message))
341+
kwargs = (;$(log_data.kwargs...))
342+
true
343+
catch err
344+
logging_error(logger, level, _module, group, id, file, line, err, true)
345+
false
346+
end
347+
end
348+
end
349+
return quote
350+
let
351+
level = $level
352+
std_level = convert(LogLevel, level)
353+
if std_level >= getindex(_min_enabled_level)
354+
group = $(log_data._group)
355+
_module = $(log_data._module)
356+
logger = current_logger_for_env(std_level, group, _module)
357+
if !(logger === nothing)
358+
id = $(log_data._id)
359+
# Second chance at an early bail-out (before computing the message),
360+
# based on arbitrary logger-specific logic.
361+
if _invoked_shouldlog(logger, level, _module, group, id)
362+
file = $(log_data._file)
363+
line = $(log_data._line)
364+
local msg, kwargs
365+
$(logrecord) && handle_message(
298366
logger, level, msg, _module, group, id, file, line;
299-
$(log_data.kwargs...)
300-
)
301-
catch err
302-
logging_error(logger, level, _module, group, id, file, line, err)
367+
kwargs...)
303368
end
304369
end
305370
end
371+
nothing
306372
end
307-
nothing
308-
end
309373
end
310374
end
311375

312376
function process_logmsg_exs(_orig_module, _file, _line, level, message, exs...)
377+
@nospecialize
313378
local _group, _id
314379
_module = _orig_module
315380
kwargs = Any[]
316381
for ex in exs
317382
if ex isa Expr && ex.head === :(=) && ex.args[1] isa Symbol
318-
k,v = ex.args
383+
k, v = ex.args
319384
if !(k isa Symbol)
320385
throw(ArgumentError("Expected symbol for key in key value pair `$ex`"))
321386
end
@@ -352,6 +417,7 @@ function process_logmsg_exs(_orig_module, _file, _line, level, message, exs...)
352417
end
353418

354419
function default_group_code(file)
420+
@nospecialize
355421
if file isa String && isdefined(Base, :basename)
356422
QuoteNode(default_group(file)) # precompute if we can
357423
else
@@ -361,39 +427,34 @@ function default_group_code(file)
361427
end
362428

363429

364-
# Report an error in log message creation (or in the logger itself).
430+
# Report an error in log message creation
365431
@noinline function logging_error(logger, level, _module, group, id,
366-
filepath, line, @nospecialize(err))
432+
filepath, line, @nospecialize(err), real::Bool)
433+
@nospecialize
367434
if !_invoked_catch_exceptions(logger)
368-
rethrow(err)
369-
end
370-
try
371-
msg = "Exception while generating log record in module $_module at $filepath:$line"
372-
handle_message(
373-
logger, Error, msg, _module, :logevent_error, id, filepath, line;
374-
exception=(err,catch_backtrace())
375-
)
376-
catch err2
377-
try
378-
# Give up and write to stderr, in three independent calls to
379-
# increase the odds of it getting through.
380-
print(stderr, "Exception handling log message: ")
381-
println(stderr, err)
382-
println(stderr, " module=$_module file=$filepath line=$line")
383-
println(stderr, " Second exception: ", err2)
384-
catch
385-
end
435+
real ? rethrow(err) : throw(err)
386436
end
437+
msg = try
438+
"Exception while generating log record in module $_module at $filepath:$line"
439+
catch ex
440+
"Exception handling log message: $ex"
441+
end
442+
bt = real ? catch_backtrace() : backtrace()
443+
handle_message(
444+
logger, Error, msg, _module, :logevent_error, id, filepath, line;
445+
exception=(err,bt))
387446
nothing
388447
end
389448

390449
# Log a message. Called from the julia C code; kwargs is in the format
391450
# Any[key1,val1, ...] for simplicity in construction on the C side.
392451
function logmsg_shim(level, message, _module, group, id, file, line, kwargs)
393-
real_kws = Any[(kwargs[i],kwargs[i+1]) for i in 1:2:length(kwargs)]
452+
@nospecialize
453+
real_kws = Any[(kwargs[i], kwargs[i+1]) for i in 1:2:length(kwargs)]
394454
@logmsg(convert(LogLevel, level), message,
395455
_module=_module, _id=id, _group=group,
396456
_file=String(file), _line=line, real_kws...)
457+
nothing
397458
end
398459

399460
# Global log limiting mechanism for super fast but inflexible global log limiting.
@@ -574,8 +635,10 @@ min_enabled_level(logger::SimpleLogger) = logger.min_level
574635
catch_exceptions(logger::SimpleLogger) = false
575636

576637
function handle_message(logger::SimpleLogger, level, message, _module, group, id,
577-
filepath, line; maxlog=nothing, kwargs...)
578-
if maxlog !== nothing && maxlog isa Integer
638+
filepath, line; kwargs...)
639+
@nospecialize
640+
maxlog = get(kwargs, :maxlog, nothing)
641+
if maxlog isa Integer
579642
remaining = get!(logger.message_limits, id, maxlog)
580643
logger.message_limits[id] = remaining - 1
581644
remaining > 0 || return
@@ -589,6 +652,7 @@ function handle_message(logger::SimpleLogger, level, message, _module, group, id
589652
println(iob, "", msglines[i])
590653
end
591654
for (key, val) in kwargs
655+
key === :maxlog && continue
592656
println(iob, "", key, " = ", val)
593657
end
594658
println(iob, "└ @ ", something(_module, "nothing"), " ",

stdlib/Logging/src/ConsoleLogger.jl

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function default_logcolor(level)
5858
end
5959

6060
function default_metafmt(level, _module, group, id, file, line)
61+
@nospecialize
6162
color = default_logcolor(level)
6263
prefix = (level == Warn ? "Warning" : string(level))*':'
6364
suffix = ""
@@ -96,38 +97,42 @@ function termlength(str)
9697
end
9798

9899
function handle_message(logger::ConsoleLogger, level, message, _module, group, id,
99-
filepath, line; maxlog=nothing, kwargs...)
100-
if maxlog !== nothing && maxlog isa Integer
100+
filepath, line; kwargs...)
101+
@nospecialize
102+
hasmaxlog = haskey(kwargs, :maxlog) ? 1 : 0
103+
maxlog = get(kwargs, :maxlog, nothing)
104+
if maxlog isa Integer
101105
remaining = get!(logger.message_limits, id, maxlog)
102106
logger.message_limits[id] = remaining - 1
103107
remaining > 0 || return
104108
end
105109

106110
# Generate a text representation of the message and all key value pairs,
107111
# split into lines.
108-
msglines = [(indent=0,msg=l) for l in split(chomp(string(message)), '\n')]
112+
msglines = [(indent=0, msg=l) for l in split(chomp(string(message)), '\n')]
109113
dsize = displaysize(logger.stream)::Tuple{Int,Int}
110-
if !isempty(kwargs)
114+
if length(kwargs) > hasmaxlog
111115
valbuf = IOBuffer()
112-
rows_per_value = max(1, dsize[1]÷(length(kwargs)+1))
116+
rows_per_value = max(1, dsize[1] ÷ (length(kwargs) + 1 - hasmaxlog))
113117
valio = IOContext(IOContext(valbuf, logger.stream),
114-
:displaysize => (rows_per_value,dsize[2]-5),
118+
:displaysize => (rows_per_value, dsize[2] - 5),
115119
:limit => logger.show_limited)
116-
for (key,val) in pairs(kwargs)
120+
for (key, val) in kwargs
121+
key === :maxlog && continue
117122
showvalue(valio, val)
118123
vallines = split(String(take!(valbuf)), '\n')
119124
if length(vallines) == 1
120-
push!(msglines, (indent=2,msg=SubString("$key = $(vallines[1])")))
125+
push!(msglines, (indent=2, msg=SubString("$key = $(vallines[1])")))
121126
else
122-
push!(msglines, (indent=2,msg=SubString("$key =")))
123-
append!(msglines, ((indent=3,msg=line) for line in vallines))
127+
push!(msglines, (indent=2, msg=SubString("$key =")))
128+
append!(msglines, ((indent=3, msg=line) for line in vallines))
124129
end
125130
end
126131
end
127132

128133
# Format lines as text with appropriate indentation and with a box
129134
# decoration on the left.
130-
color,prefix,suffix = logger.meta_formatter(level, _module, group, id, filepath, line)::Tuple{Union{Symbol,Int},String,String}
135+
color, prefix, suffix = logger.meta_formatter(level, _module, group, id, filepath, line)::Tuple{Union{Symbol,Int},String,String}
131136
minsuffixpad = 2
132137
buf = IOBuffer()
133138
iob = IOContext(buf, logger.stream)
@@ -136,11 +141,11 @@ function handle_message(logger::ConsoleLogger, level, message, _module, group, i
136141
(isempty(suffix) ? 0 : length(suffix)+minsuffixpad)
137142
justify_width = min(logger.right_justify, dsize[2])
138143
if nonpadwidth > justify_width && !isempty(suffix)
139-
push!(msglines, (indent=0,msg=SubString("")))
144+
push!(msglines, (indent=0, msg=SubString("")))
140145
minsuffixpad = 0
141146
nonpadwidth = 2 + length(suffix)
142147
end
143-
for (i,(indent,msg)) in enumerate(msglines)
148+
for (i, (indent, msg)) in enumerate(msglines)
144149
boxstr = length(msglines) == 1 ? "[ " :
145150
i == 1 ? "" :
146151
i < length(msglines) ? "" :

test/choosetests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ function choosetests(choices = [])
4949
"euler", "show", "client",
5050
"errorshow", "sets", "goto", "llvmcall", "llvmcall2", "ryu",
5151
"some", "meta", "stacktraces", "docs",
52-
"misc", "threads", "stress",
52+
"misc", "threads", "stress", "atexit",
5353
"enums", "cmdlineargs", "int", "interpreter",
5454
"checked", "bitset", "floatfuncs", "precompile",
5555
"boundscheck", "error", "ambiguous", "cartesian", "osutils",
5656
"channels", "iostream", "secretbuffer", "specificity",
57-
"reinterpretarray", "syntax", "logging", "missing", "asyncmap", "atexit"
57+
"reinterpretarray", "syntax", "corelogging", "missing", "asyncmap"
5858
]
5959

6060
tests = []

test/logging.jl renamed to test/corelogging.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,28 @@ end
8787

8888
@testset "Log message exception handling" begin
8989
# Exceptions in message creation are caught by default
90-
@test_logs (Error,Test.Ignored(),Test.Ignored(),:logevent_error) catch_exceptions=true @info "foo $(1÷0)"
90+
@test_logs (Error, Test.Ignored(), Test.Ignored(), :logevent_error) catch_exceptions=true @info "foo $(1÷0)"
9191
# Exceptions propagate if explicitly disabled for the logger (by default
9292
# for the test logger)
9393
@test_throws DivideError collect_test_logs() do
9494
@info "foo $(1÷0)"
9595
end
96+
# trivial expressions create the errors explicitly instead of throwing them (to avoid try/catch)
97+
for i in 1:2
98+
local msg, x, y
99+
logmsg = (function() @info msg x=y end,
100+
function() @info msg x=y z=1+1 end)[i]
101+
@test_logs (Error, Test.Ignored(), Test.Ignored(), :logevent_error) catch_exceptions=true logmsg()
102+
@test_throws UndefVarError(:msg) collect_test_logs(logmsg)
103+
@test (only(collect_test_logs(logmsg, catch_exceptions=true)[1]).kwargs[:exception]::Tuple{UndefVarError, Vector})[1] === UndefVarError(:msg)
104+
msg = "the msg"
105+
@test_logs (Error, Test.Ignored(), Test.Ignored(), :logevent_error) catch_exceptions=true logmsg()
106+
@test_throws UndefVarError(:y) collect_test_logs(logmsg)
107+
@test (only(collect_test_logs(logmsg, catch_exceptions=true)[1]).kwargs[:exception]::Tuple{UndefVarError, Vector})[1] === UndefVarError(:y)
108+
y = "the y"
109+
@test_logs (Info,"the msg") logmsg()
110+
@test only(collect_test_logs(logmsg)[1]).kwargs[:x] === "the y"
111+
end
96112
end
97113

98114
@testset "Special keywords" begin

0 commit comments

Comments
 (0)