Repository navigation
Conversation
Value#value= copied its string through sub even with nothing to clean; it now copies only when a trailing semicolon, !important or surrounding whitespace has to go, and freezes the result with -value. reduce and each_with_object over the declarations hash built a [key, value] pair per entry, casecmp? case-folded both strings, replace_declaration! rebuilt its hash through zip, and merge stripped already stripped values. A parsed declaration goes from 10.6 to 4.6 objects, declarations_to_s from 21 to 7; inlining our 336 emails with premailer allocates 32% fewer objects, with byte-for-byte identical output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of 3 runs The unfrozen-value test only used a value that needed cleaning, so sub copied it before -value ran; a clean one now goes straight through -value and must come out neither frozen nor mutated. GC.stat counts allocations of every thread, so the helper keeps the minimum of three runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| value = value.sub(TRAILING_SEMICOLON_RX, '') if value.include?(SEMICOLON) | ||
| self.important = value.include?('!') && value.match?(CssParser::IMPORTANT_IN_PROPERTY_RX) | ||
| value = value.sub(CssParser::IMPORTANT_IN_PROPERTY_RX, '') if important | ||
| value = value.strip if value.match?(SURROUNDING_WHITESPACE_RX) |
There was a problem hiding this comment.
is this not the same ?
| value = value.strip if value.match?(SURROUNDING_WHITESPACE_RX) | |
| value.strip! |
There was a problem hiding this comment.
Sadly not quite: when nothing was trimmed before, value is still the caller's string, so strip! would modify it (or raise on a frozen one). The new comment above the block says so, and test_unfrozen_values_are_neither_frozen_nor_mutated covers it.
| @value = value.freeze | ||
| @value = -value |
There was a problem hiding this comment.
is this not the same as before ?
There was a problem hiding this comment.
Almost! Before, value was always a fresh copy from sub, so freezing it was harmless. Now it can be the caller's own string, and -value freezes a copy instead of it. I added a one-line comment.
| value = value.to_s | ||
| value = value.sub(TRAILING_SEMICOLON_RX, '') if value.include?(SEMICOLON) | ||
| self.important = value.include?('!') && value.match?(CssParser::IMPORTANT_IN_PROPERTY_RX) | ||
| value = value.sub(CssParser::IMPORTANT_IN_PROPERTY_RX, '') if important |
There was a problem hiding this comment.
not a fan of this double-checking and hand-optimizing 😢
if it really makes a difference leave some inline comments splitting the logic up explaining why it needs to be weird to be more performance
There was a problem hiding this comment.
Fair point, thank you! I dropped the include? pre-checks: they made value= alone 15–19% faster but didn't show on a whole parse. I kept only the match? guards, which keep clean values at zero allocations (4.4 vs 6.4 objects per declaration), and explained them in a comment.
| property.strip! | ||
| value.strip! | ||
| next if property.empty? || value.empty? || value.casecmp?(IMPORTANT) | ||
| next if property.empty? || value.empty? || value.casecmp(IMPORTANT) == 0 |
There was a problem hiding this comment.
prefer to keep the ? ... can't imagine that being slower
There was a problem hiding this comment.
I was surprised too, but on Ruby 4.0.9 casecmp? case-folds both strings: 2 objects per call and 2x slower, which takes a parse from 4.4 back to 6.4 objects per declaration. I went with casecmp(IMPORTANT).zero? so it still reads as a predicate, with a short comment. Happy to switch back if you prefer!
grosser
left a comment
There was a problem hiding this comment.
most of these look fine, some even improve readability like the .reduce change
Indeed. I aim to perform a readability review aligned with the repository’s code style after addressing each issue ! |
|
Will rebase with main ASAP to fix the conflict. |
…-fewer-allocations # Conflicts: # CHANGELOG.md
Value#value= now guards sub and strip with the regexps themselves: same allocations, the include? short-cuts only made value= alone 15-19% faster, which does not show on a whole parse. Comments say why the guards and -value are there, and why casecmp? is avoided (it case-folds both strings: two objects per declaration). The rebuilt declarations hash reads with each_with_index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Done ! You can merge if you want :) |
Following up on #193 :) Profiling premailer on our emails again, most of what css_parser still allocated came from storing and printing declarations. Every
Valuecopied its string throughsubeven with nothing to clean.reduceandeach_with_objectbuilt a[key, value]pair per declaration,casecmp?case-folded both strings, andreplace_declaration!rebuilt its hash throughzip.This branch copies a value only when it really has a trailing semicolon, an
!importantor surrounding whitespace to remove, and freezes the cleaned copy with-value, so repeated values (0,#fff,bold…) share one String. Hashes are iterated with plaineach.casecmp(IMPORTANT).zero?replacescasecmp?: I checked every codepoint, none case-folds into!important. Thestrips inCssParser.mergeare gone too, stored values are already stripped.One trade-off to flag: when the value passed in is frozen and needs no cleaning,
Value#valueis now that String or its interned twin rather than a fresh copy. Unfrozen strings are still copied, and never frozen or mutated, whether they need cleaning or not. There's a test for both cases.Against today's master, which includes #193 and #195, a parsed declaration goes from 10.4 objects to 4.4 and
declarations_to_sfrom 15 to 3. Measured end to end, median of 11 interleaved runs, Ruby 4.0.9 on an Apple M5, premailer 1.32.0, nokogiri 1.19.4:rake benchmark, complex.cssAll three corpora give byte-for-byte identical output. A differential fuzz against today's master (50,000 seeded random values, blocks, merges and shorthand round-trips, about 300,000 output lines) found no difference. The new allocation test fails on master, and is skipped on JRuby. The suite passes on Ruby 3.3 and 4.0, and RuboCop is clean.
Sister PR: #195, for shorthands and media types, now merged into this branch.
Pre-Merge Checklist
🤖 Generated with Claude Code, but prompted, read, and verified by @czj, a caring human :)