You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Improve performance in DictConfigurator by optimizing logger lookup #132372
In logging.config.DictConfigurator.configure() (in Lib/logging/config.py), the current implementation uses a list called existing to store logger names:
This list is then used in both an if name in existing check and an existing.index(name) call:
ifnameinexisting:
i=existing.index(name)
Both operations are O(n) on a list. While this likely isn’t a major performance bottleneck for a small number of loggers, it could be avoided — and the change would be a net gain in efficiency.
One idea would be to build a set_existing = set(existing) for O(1) membership checks, and possibly a dict_existing = {name: idx} to avoid linear-time indexing, while keeping the original sorted list for the required prefix-based logic:
For a large number of loggers the implementation indeed looks not optimal. But how much is this a problem in applications? Unless there are benchmarks showing this part of the current implementation is a bottleneck I suspect a PR will have a low probability of being accepted.
Thanks! I ran a benchmark to evaluate the proposed optimization. The results show a measurable improvement in configuration time as the number of loggers increases.
Benchmark Results (500 runs each)
Logger Count
Before Optimization (avg)
After Optimization (avg)
10
3.074 ms
2.639 ms
50
61.889 ms
52.208 ms
100
503.351 ms
308.545 ms
Total Time
284.38 s
181.92 s
Tested on GitHub Codespaces with the following specs:
CPU: AMD EPYC 7763 (2 vCPUs)
RAM: 8 GB
OS: Fedora 40 (container)
Architecture: x86_64
Please let me know what you think about this benchmark. Happy to open a PR if this looks good!
Benchmark script
importloggingimportlogging.configimporttimeimportrandomimportstringENABLE_OPTIMIZED_CONFIG=True# copied from logging/config.py as isdef_clearExistingHandlers():
"""Clear and close existing handlers"""logging._handlers.clear()
logging.shutdown(logging._handlerList[:])
dellogging._handlerList[:]
# copied from logging/config.py as isdef_handle_existing_loggers(existing, child_loggers, disable_existing):
""" When (re)configuring logging, handle loggers which were in the previous configuration but are not in the new configuration. There's no point deleting them as other threads may continue to hold references to them; and by disabling them, you stop them doing any logging. However, don't disable children of named loggers, as that's probably not what was intended by the user. Also, allow existing loggers to NOT be disabled if disable_existing is false. """root=logging.rootforloginexisting:
logger=root.manager.loggerDict[log]
ifloginchild_loggers:
ifnotisinstance(logger, logging.PlaceHolder):
logger.setLevel(logging.NOTSET)
logger.handlers= []
logger.propagate=Trueelse:
logger.disabled=disable_existing# copied from logging/config.py as isdeforiginal_configure(self):
"""Do the configuration."""config=self.configif'version'notinconfig:
raiseValueError("dictionary doesn't specify a version")
ifconfig['version'] !=1:
raiseValueError("Unsupported version: %s"%config['version'])
incremental=config.pop('incremental', False)
EMPTY_DICT= {}
withlogging._lock:
ifincremental:
handlers=config.get('handlers', EMPTY_DICT)
fornameinhandlers:
ifnamenotinlogging._handlers:
raiseValueError('No handler found with ''name %r'%name)
else:
try:
handler=logging._handlers[name]
handler_config=handlers[name]
level=handler_config.get('level', None)
iflevel:
handler.setLevel(logging._checkLevel(level))
exceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) fromeloggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
try:
self.configure_logger(name, loggers[name], True)
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) fromeroot=config.get('root', None)
ifroot:
try:
self.configure_root(root, True)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeelse:
disable_existing=config.pop('disable_existing_loggers', True)
_clearExistingHandlers()
# Do formatters first - they don't refer to anything elseformatters=config.get('formatters', EMPTY_DICT)
fornameinformatters:
try:
formatters[name] =self.configure_formatter(
formatters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''formatter %r'%name) frome# Next, do filters - they don't refer to anything else, eitherfilters=config.get('filters', EMPTY_DICT)
fornameinfilters:
try:
filters[name] =self.configure_filter(filters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''filter %r'%name) frome# Next, do handlers - they refer to formatters and filters# As handlers can refer to other handlers, sort the keys# to allow a deterministic order of configurationhandlers=config.get('handlers', EMPTY_DICT)
deferred= []
fornameinsorted(handlers):
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
if' not configured yet'instr(e.__cause__):
deferred.append(name)
else:
raiseValueError('Unable to configure handler ''%r'%name) frome# Now do any that were deferredfornameindeferred:
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) frome# Next, do loggers - they refer to handlers and filters#we don't want to lose the existing loggers,#since other threads may have pointers to them.#existing is set to contain all existing loggers,#and as we go through the new configuration we#remove any which are configured. At the end,#what's left in existing is the set of loggers#which were in the previous configuration but#which are not in the new configuration.root=logging.rootexisting=list(root.manager.loggerDict.keys())
#The list needs to be sorted so that we can#avoid disabling child loggers of explicitly#named loggers. With a sorted list it is easier#to find the child loggers.existing.sort()
#We'll keep the list of existing loggers#which are children of named loggers here...child_loggers= []
#now set up the new ones...loggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
ifnameinexisting:
i=existing.index(name) +1# look after nameprefixed=name+"."pflen=len(prefixed)
num_existing=len(existing)
whilei<num_existing:
ifexisting[i][:pflen] ==prefixed:
child_loggers.append(existing[i])
i+=1existing.remove(name)
try:
self.configure_logger(name, loggers[name])
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) frome#Disable any old loggers. There's no point deleting#them as other threads may continue to hold references#and by disabling them, you stop them doing any logging.#However, don't disable children of named loggers, as that's#probably not what was intended by the user.#for log in existing:# logger = root.manager.loggerDict[log]# if log in child_loggers:# logger.level = logging.NOTSET# logger.handlers = []# logger.propagate = True# elif disable_existing:# logger.disabled = True_handle_existing_loggers(existing, child_loggers,
disable_existing)
# And finally, do the root loggerroot=config.get('root', None)
ifroot:
try:
self.configure_root(root)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') frome# copied from logging/config.py with some modificationsdefoptimized_configure(self):
"""Do the configuration."""config=self.configif'version'notinconfig:
raiseValueError("dictionary doesn't specify a version")
ifconfig['version'] !=1:
raiseValueError("Unsupported version: %s"%config['version'])
incremental=config.pop('incremental', False)
EMPTY_DICT= {}
withlogging._lock:
ifincremental:
handlers=config.get('handlers', EMPTY_DICT)
fornameinhandlers:
ifnamenotinlogging._handlers:
raiseValueError('No handler found with ''name %r'%name)
else:
try:
handler=logging._handlers[name]
handler_config=handlers[name]
level=handler_config.get('level', None)
iflevel:
handler.setLevel(logging._checkLevel(level))
exceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) fromeloggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
try:
self.configure_logger(name, loggers[name], True)
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) fromeroot=config.get('root', None)
ifroot:
try:
self.configure_root(root, True)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeelse:
disable_existing=config.pop('disable_existing_loggers', True)
_clearExistingHandlers()
# Do formatters first - they don't refer to anything elseformatters=config.get('formatters', EMPTY_DICT)
fornameinformatters:
try:
formatters[name] =self.configure_formatter(
formatters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''formatter %r'%name) frome# Next, do filters - they don't refer to anything else, eitherfilters=config.get('filters', EMPTY_DICT)
fornameinfilters:
try:
filters[name] =self.configure_filter(filters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''filter %r'%name) frome# Next, do handlers - they refer to formatters and filters# As handlers can refer to other handlers, sort the keys# to allow a deterministic order of configurationhandlers=config.get('handlers', EMPTY_DICT)
deferred= []
fornameinsorted(handlers):
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
if' not configured yet'instr(e.__cause__):
deferred.append(name)
else:
raiseValueError('Unable to configure handler ''%r'%name) frome# Now do any that were deferredfornameindeferred:
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) frome# Next, do loggers - they refer to handlers and filters#we don't want to lose the existing loggers,#since other threads may have pointers to them.#existing is set to contain all existing loggers,#and as we go through the new configuration we#remove any which are configured. At the end,#what's left in existing is the set of loggers#which were in the previous configuration but#which are not in the new configuration.root=logging.rootexisting=list(root.manager.loggerDict.keys())
existing_set=set(existing)
index_map= {name: idxforidx, nameinenumerate(existing)}
#The list needs to be sorted so that we can#avoid disabling child loggers of explicitly#named loggers. With a sorted list it is easier#to find the child loggers.existing.sort()
#We'll keep the list of existing loggers#which are children of named loggers here...child_loggers= []
#now set up the new ones...loggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
ifnameinexisting_set:
i=index_map[name] +1# look after nameprefixed=name+"."pflen=len(prefixed)
num_existing=len(existing)
whilei<num_existing:
ifexisting[i][:pflen] ==prefixed:
child_loggers.append(existing[i])
i+=1existing.remove(name)
try:
self.configure_logger(name, loggers[name])
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) frome#Disable any old loggers. There's no point deleting#them as other threads may continue to hold references#and by disabling them, you stop them doing any logging.#However, don't disable children of named loggers, as that's#probably not what was intended by the user.#for log in existing:# logger = root.manager.loggerDict[log]# if log in child_loggers:# logger.level = logging.NOTSET# logger.handlers = []# logger.propagate = True# elif disable_existing:# logger.disabled = True_handle_existing_loggers(existing, child_loggers,
disable_existing)
# And finally, do the root loggerroot=config.get('root', None)
ifroot:
try:
self.configure_root(root)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeifENABLE_OPTIMIZED_CONFIG:
logging.config.DictConfigurator.configure=optimized_configureifENABLE_OPTIMIZED_CONFIGelseoriginal_configuredefgenerate_log_config(num_loggers: int):
"""Generate a logging config dict with `num_loggers` named loggers."""loggers= {}
foriinrange(num_loggers):
logger_name=f"app.module_{i}_"+''.join(random.choices(string.ascii_lowercase, k=6))
loggers[logger_name] = {
"level": "DEBUG",
"handlers": ["console"],
"propagate": False,
}
config= {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
"stream": "ext://sys.stdout"
}
},
"loggers": loggers
}
returnconfigdefbenchmark_dict_config(num_loggers: int, runs: int=500):
durations= []
for_inrange(runs):
config=generate_log_config(num_loggers)
start=time.perf_counter()
logging.config.dictConfig(config)
end=time.perf_counter()
durations.append((end-start)*1000)
avg_duration=sum(durations) /runsprint(f"{num_loggers:>6} loggers → avg: {avg_duration:.6f} ms over {runs} runs")
returnavg_durationif__name__=="__main__":
overall_start=time.perf_counter()
print(f"Benchmarking logging.config.dictConfig() [{'After Optimization'ifENABLE_OPTIMIZED_CONFIGelse'Before Optimization'}]...\n")
forcountin [10, 50, 100]:
benchmark_dict_config(count)
overall_end=time.perf_counter()
print(f'Total time: {overall_end-overall_start} seconds')
I noticed the previous optimization didn’t perform as well for small logger counts (e.g. 1–3). To address this, I simplified the implementation by:
• Using a set instead of a list for existing logger names.
• Removing the sort and index logic.
• Iterating over the set directly to find child loggers.
This improves performance across all logger counts and simplifies the logic. Benchmark results (500 runs each):
Logger Count
Before Optimization (avg)
After Optimization (avg)
1
0.113 ms
0.097 ms
3
0.595 ms
0.432 ms
10
4.523 ms
3.222 ms
50
67.603 ms
50.915 ms
100
455.350 ms
293.010 ms
Total Time
264.31 s
174.06 s
Diff
diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 6a6a7f726f7..12679e64e14 100644
--- a/Lib/logging/config.py+++ b/Lib/logging/config.py@@ -632,12 +632,8 @@ def configure(self):
#which were in the previous configuration but
#which are not in the new configuration.
root = logging.root
- existing = list(root.manager.loggerDict.keys())- #The list needs to be sorted so that we can- #avoid disabling child loggers of explicitly- #named loggers. With a sorted list it is easier- #to find the child loggers.- existing.sort()+ root = logging.root+ existing = set(root.manager.loggerDict.keys())
#We'll keep the list of existing loggers
#which are children of named loggers here...
child_loggers = []
@@ -645,14 +641,11 @@ def configure(self):
loggers = config.get('loggers', EMPTY_DICT)
for name in loggers:
if name in existing:
- i = existing.index(name) + 1 # look after name
prefixed = name + "."
pflen = len(prefixed)
- num_existing = len(existing)- while i < num_existing:- if existing[i][:pflen] == prefixed:- child_loggers.append(existing[i])- i += 1+ for existing_logger in existing:+ if existing_logger[:pflen] == prefixed:+ child_loggers.append(existing_logger)
existing.remove(name)
try:
self.configure_logger(name, loggers[name])
Please let me know what you think about the changes.
here is the updated benchmark code:
importloggingimportlogging.configimporttimeimportrandomimportstringENABLE_OPTIMIZED_CONFIG=False# copied from logging/config.py as isdef_clearExistingHandlers():
"""Clear and close existing handlers"""logging._handlers.clear()
logging.shutdown(logging._handlerList[:])
dellogging._handlerList[:]
# copied from logging/config.py as isdef_handle_existing_loggers(existing, child_loggers, disable_existing):
""" When (re)configuring logging, handle loggers which were in the previous configuration but are not in the new configuration. There's no point deleting them as other threads may continue to hold references to them; and by disabling them, you stop them doing any logging. However, don't disable children of named loggers, as that's probably not what was intended by the user. Also, allow existing loggers to NOT be disabled if disable_existing is false. """root=logging.rootforloginexisting:
logger=root.manager.loggerDict[log]
ifloginchild_loggers:
ifnotisinstance(logger, logging.PlaceHolder):
logger.setLevel(logging.NOTSET)
logger.handlers= []
logger.propagate=Trueelse:
logger.disabled=disable_existing# copied from logging/config.py as isdeforiginal_configure(self):
"""Do the configuration."""config=self.configif'version'notinconfig:
raiseValueError("dictionary doesn't specify a version")
ifconfig['version'] !=1:
raiseValueError("Unsupported version: %s"%config['version'])
incremental=config.pop('incremental', False)
EMPTY_DICT= {}
withlogging._lock:
ifincremental:
handlers=config.get('handlers', EMPTY_DICT)
fornameinhandlers:
ifnamenotinlogging._handlers:
raiseValueError('No handler found with ''name %r'%name)
else:
try:
handler=logging._handlers[name]
handler_config=handlers[name]
level=handler_config.get('level', None)
iflevel:
handler.setLevel(logging._checkLevel(level))
exceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) fromeloggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
try:
self.configure_logger(name, loggers[name], True)
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) fromeroot=config.get('root', None)
ifroot:
try:
self.configure_root(root, True)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeelse:
disable_existing=config.pop('disable_existing_loggers', True)
_clearExistingHandlers()
# Do formatters first - they don't refer to anything elseformatters=config.get('formatters', EMPTY_DICT)
fornameinformatters:
try:
formatters[name] =self.configure_formatter(
formatters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''formatter %r'%name) frome# Next, do filters - they don't refer to anything else, eitherfilters=config.get('filters', EMPTY_DICT)
fornameinfilters:
try:
filters[name] =self.configure_filter(filters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''filter %r'%name) frome# Next, do handlers - they refer to formatters and filters# As handlers can refer to other handlers, sort the keys# to allow a deterministic order of configurationhandlers=config.get('handlers', EMPTY_DICT)
deferred= []
fornameinsorted(handlers):
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
if' not configured yet'instr(e.__cause__):
deferred.append(name)
else:
raiseValueError('Unable to configure handler ''%r'%name) frome# Now do any that were deferredfornameindeferred:
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) frome# Next, do loggers - they refer to handlers and filters#we don't want to lose the existing loggers,#since other threads may have pointers to them.#existing is set to contain all existing loggers,#and as we go through the new configuration we#remove any which are configured. At the end,#what's left in existing is the set of loggers#which were in the previous configuration but#which are not in the new configuration.root=logging.rootexisting=list(root.manager.loggerDict.keys())
#The list needs to be sorted so that we can#avoid disabling child loggers of explicitly#named loggers. With a sorted list it is easier#to find the child loggers.existing.sort()
#We'll keep the list of existing loggers#which are children of named loggers here...child_loggers= []
#now set up the new ones...loggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
ifnameinexisting:
i=existing.index(name) +1# look after nameprefixed=name+"."pflen=len(prefixed)
num_existing=len(existing)
whilei<num_existing:
ifexisting[i][:pflen] ==prefixed:
child_loggers.append(existing[i])
i+=1existing.remove(name)
try:
self.configure_logger(name, loggers[name])
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) frome#Disable any old loggers. There's no point deleting#them as other threads may continue to hold references#and by disabling them, you stop them doing any logging.#However, don't disable children of named loggers, as that's#probably not what was intended by the user.#for log in existing:# logger = root.manager.loggerDict[log]# if log in child_loggers:# logger.level = logging.NOTSET# logger.handlers = []# logger.propagate = True# elif disable_existing:# logger.disabled = True_handle_existing_loggers(existing, child_loggers,
disable_existing)
# And finally, do the root loggerroot=config.get('root', None)
ifroot:
try:
self.configure_root(root)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') frome# copied from logging/config.py with some modificationsdefoptimized_configure(self):
"""Do the configuration."""config=self.configif'version'notinconfig:
raiseValueError("dictionary doesn't specify a version")
ifconfig['version'] !=1:
raiseValueError("Unsupported version: %s"%config['version'])
incremental=config.pop('incremental', False)
EMPTY_DICT= {}
withlogging._lock:
ifincremental:
handlers=config.get('handlers', EMPTY_DICT)
fornameinhandlers:
ifnamenotinlogging._handlers:
raiseValueError('No handler found with ''name %r'%name)
else:
try:
handler=logging._handlers[name]
handler_config=handlers[name]
level=handler_config.get('level', None)
iflevel:
handler.setLevel(logging._checkLevel(level))
exceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) fromeloggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
try:
self.configure_logger(name, loggers[name], True)
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) fromeroot=config.get('root', None)
ifroot:
try:
self.configure_root(root, True)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeelse:
disable_existing=config.pop('disable_existing_loggers', True)
_clearExistingHandlers()
# Do formatters first - they don't refer to anything elseformatters=config.get('formatters', EMPTY_DICT)
fornameinformatters:
try:
formatters[name] =self.configure_formatter(
formatters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''formatter %r'%name) frome# Next, do filters - they don't refer to anything else, eitherfilters=config.get('filters', EMPTY_DICT)
fornameinfilters:
try:
filters[name] =self.configure_filter(filters[name])
exceptExceptionase:
raiseValueError('Unable to configure ''filter %r'%name) frome# Next, do handlers - they refer to formatters and filters# As handlers can refer to other handlers, sort the keys# to allow a deterministic order of configurationhandlers=config.get('handlers', EMPTY_DICT)
deferred= []
fornameinsorted(handlers):
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
if' not configured yet'instr(e.__cause__):
deferred.append(name)
else:
raiseValueError('Unable to configure handler ''%r'%name) frome# Now do any that were deferredfornameindeferred:
try:
handler=self.configure_handler(handlers[name])
handler.name=namehandlers[name] =handlerexceptExceptionase:
raiseValueError('Unable to configure handler ''%r'%name) frome# Next, do loggers - they refer to handlers and filters#we don't want to lose the existing loggers,#since other threads may have pointers to them.#existing is set to contain all existing loggers,#and as we go through the new configuration we#remove any which are configured. At the end,#what's left in existing is the set of loggers#which were in the previous configuration but#which are not in the new configuration.root=logging.rootexisting=set(root.manager.loggerDict.keys())
#We'll keep the list of existing loggers#which are children of named loggers here...child_loggers= []
#now set up the new ones...loggers=config.get('loggers', EMPTY_DICT)
fornameinloggers:
ifnameinexisting:
prefixed=name+"."pflen=len(prefixed)
forexisting_loggerinexisting:
ifexisting_logger[:pflen] ==prefixed:
child_loggers.append(existing_logger)
existing.remove(name)
try:
self.configure_logger(name, loggers[name])
exceptExceptionase:
raiseValueError('Unable to configure logger ''%r'%name) frome#Disable any old loggers. There's no point deleting#them as other threads may continue to hold references#and by disabling them, you stop them doing any logging.#However, don't disable children of named loggers, as that's#probably not what was intended by the user.#for log in existing:# logger = root.manager.loggerDict[log]# if log in child_loggers:# logger.level = logging.NOTSET# logger.handlers = []# logger.propagate = True# elif disable_existing:# logger.disabled = True_handle_existing_loggers(existing, child_loggers,
disable_existing)
# And finally, do the root loggerroot=config.get('root', None)
ifroot:
try:
self.configure_root(root)
exceptExceptionase:
raiseValueError('Unable to configure root ''logger') fromeifENABLE_OPTIMIZED_CONFIG:
logging.config.DictConfigurator.configure=optimized_configureifENABLE_OPTIMIZED_CONFIGelseoriginal_configuredefgenerate_log_config(num_loggers: int):
"""Generate a logging config dict with `num_loggers` named loggers."""loggers= {}
foriinrange(num_loggers):
logger_name=f"app.module_{i}_"+''.join(random.choices(string.ascii_lowercase, k=6))
loggers[logger_name] = {
"level": "DEBUG",
"handlers": ["console"],
"propagate": False,
}
config= {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
"stream": "ext://sys.stdout"
}
},
"loggers": loggers
}
returnconfigdefbenchmark_dict_config(num_loggers: int, runs: int=500):
durations= []
for_inrange(runs):
config=generate_log_config(num_loggers)
start=time.perf_counter()
logging.config.dictConfig(config)
end=time.perf_counter()
durations.append((end-start)*1000)
avg_duration=sum(durations) /runsprint(f"{num_loggers:>6} loggers → avg: {avg_duration:.6f} ms over {runs} runs")
returnavg_durationif__name__=="__main__":
overall_start=time.perf_counter()
print(f"Benchmarking logging.config.dictConfig() [{'After Optimization'ifENABLE_OPTIMIZED_CONFIGelse'Before Optimization'}]...\n")
forcountin [1, 3, 10, 50, 100]:
benchmark_dict_config(count)
overall_end=time.perf_counter()
print(f'Total time: {overall_end-overall_start} seconds')
Activity
eendebakpt commentedon Apr 10, 2025
For a large number of loggers the implementation indeed looks not optimal. But how much is this a problem in applications? Unless there are benchmarks showing this part of the current implementation is a bottleneck I suspect a PR will have a low probability of being accepted.
sobolevn commentedon Apr 10, 2025
Please, provide a benchmark :)
Only numbers can decide here.
allrob23 commentedon Apr 11, 2025
Thanks! I ran a benchmark to evaluate the proposed optimization. The results show a measurable improvement in configuration time as the number of loggers increases.
Benchmark Results (500 runs each)
Tested on GitHub Codespaces with the following specs:
Please let me know what you think about this benchmark. Happy to open a PR if this looks good!
Benchmark script
sobolevn commentedon Apr 11, 2025
Please, inclide
1and3as loggers counts, because I think that these are very realistic numbers for a lot of use-cases.allrob23 commentedon Apr 11, 2025
I noticed the previous optimization didn’t perform as well for small logger counts (e.g. 1–3). To address this, I simplified the implementation by:
• Using a set instead of a list for existing logger names.
• Removing the sort and index logic.
• Iterating over the set directly to find child loggers.
This improves performance across all logger counts and simplifies the logic. Benchmark results (500 runs each):
Diff
Please let me know what you think about the changes.
here is the updated benchmark code:
pythongh-132372: Speed up logging.config existing logger handling
pythongh-132372: Batch logging cache clearing
pythongh-132372: Batch logging cache clearing
pythongh-132372: Address logging config review comments
pythongh-132372: Use Logger.setLevel for preserved children
pythongh-132372: Speed up logging.config existing logger handling
pythongh-132372: Batch logging cache clearing
pythongh-132372: Address logging config review comments
pythongh-132372: Use Logger.setLevel for preserved children
gh-132372: Speed up logging.config existing logger handling (GH-150242)