Skip to content

Load yaml only when a YAML layout or object format is used - #254

Open
tas50 wants to merge 1 commit into
TwP:masterfrom
tas50:perf/defer-yaml-require
Open

Load yaml only when a YAML layout or object format is used#254
tas50 wants to merge 1 commit into
TwP:masterfrom
tas50:perf/defer-yaml-require

Conversation

@tas50

@tas50 tas50 commented Aug 28, 2026

Copy link
Copy Markdown

Problem

logging.rb requires yaml at the top of the file, but YAML is only reachable through two opt-in paths:

  • Layout#try_yaml, used when :obj_format is :yaml
  • the format method generated by Parseable.create_yaml_format_method, used by Parseable.yaml layouts

Everyone else pays for psych, which drags date and stringio along with it. It is the single largest item in the require graph:

   42.16  logging
   13.19    yaml            <-- largest single item
   12.79      psych
    1.93        date
    5.54    fileutils
    3.53    multi_json
    3.30    logging/appenders
    5.33    logging/layouts

Fix

Move the require into the two funnels.

Note the second one is placed in create_yaml_format_method rather than at the point of use: that method builds a format method whose body calls #to_yaml, so yaml has to be loaded before the generated method runs, not merely before it is defined.

Measurements

Ruby 4.0.6 (arm64-darwin), best of seven runs:

require 'logging' files loaded
before 33.21 ms 85
after 21.83 ms 46
−11.38 ms (34%) −39

Confirmed directly:

require "logging"                      # yaml loaded? => false
Logging::Layouts::Parseable.yaml       # yaml loaded? => true, layout formats correctly
Logging::Layouts::Basic.new(obj_format: :yaml)  # formats correctly

Two things I looked at and deliberately left alone

  • socket (3.59 ms) — Socket.gethostname is evaluated while building the Parseable::DIRECTIVE_TABLE constant, so socket really is needed at load time. Deferring it would mean making the hostname lazy, which is a behavior change, not a require move.
  • fileutils (5.54 ms) — logging/utils.rb reopens module FileUtils to add concat. Without fileutils already loaded that would silently define an empty FileUtils module and copy_file/touch would go missing. Not worth the fragility.

Tests

Full suite unchanged: 202 tests, 3 failures, all three pre-existing on Ruby 4.0.6 and unrelated to YAML (two syslog frozen-string-literal warnings, one test_method_name assertion). With the added tests: 206 tests, same 3 failures.

The four added tests cover requiring the gem, a YAML layout, the :yaml object format, and YAML event formatting. The first fails against the previous code.

logging.rb requires yaml at the top, but YAML is only reachable through
two opt-in paths: Layout#try_yaml, used when :obj_format is :yaml, and
the format method generated by Parseable.create_yaml_format_method,
used by Parseable.yaml layouts.

Everyone else pays for psych, and psych pulls date and stringio along
with it. That is the single largest item in the require graph.

Measured on Ruby 4.0.6 (arm64-darwin), best of seven runs:

              require 'logging'   files loaded
  before      33.21 ms            85
  after       21.83 ms            46
              -11.38 ms           -39   (34% faster)

The require is placed in the two funnels rather than at the point of
use. create_yaml_format_method builds a format method whose body calls
#to_yaml, so yaml has to be loaded before that generated method runs,
not merely before it is defined.

socket and fileutils were considered and left alone. Socket.gethostname
is evaluated while building Parseable::DIRECTIVE_TABLE, so socket is
genuinely needed at load time, and logging/utils.rb reopens FileUtils
to add concat, which would silently define an empty module if fileutils
were not already loaded.

Test suite unchanged at 202 tests, 3 failures, all three pre-existing
on Ruby 4.0.6 and unrelated to YAML (two syslog frozen-string-literal
warnings, one log event method_name assertion).

The four added tests cover requiring the gem, a YAML layout, the :yaml
object format, and YAML event formatting. The first fails against the
previous code.

Signed-off-by: Tim Smith <tsmith84@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant