Visitar URL original
Reduce AOT file-size by changing LogFactory PurgeObsoleteLoggers to clone Dictionary instead of ToList by snakefoot · Pull Request #6177 · NLog/NLog · GitHub
Skip to content

Reduce AOT file-size by changing LogFactory PurgeObsoleteLoggers to clone Dictionary instead of ToList - #6177

Merged
snakefoot merged 1 commit into
NLog:devfrom
snakefoot:AotGenericCollections
May 17, 2026
Merged

snakefoot merged 1 commit into
NLog:devfrom
snakefoot:AotGenericCollections

Conversation

@snakefoot

@snakefoot snakefoot commented May 17, 2026 •

Copy link
Copy Markdown
Contributor

AOT file-size increase with every declared generic-type-instance, along with bonus code from System.Linq.

@coderabbitai

coderabbitai Bot commented May 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fdc42ad6-5001-4e38-ba02-9b90c005e040

📥 Commits

Reviewing files that changed from the base of the PR and between e54319d and b5938f1.

📒 Files selected for processing (11)
  • src/NLog/Conditions/ConditionRelationalExpression.cs
  • src/NLog/Config/ConfigurationItemFactory.cs
  • src/NLog/Config/LoggingConfiguration.cs
  • src/NLog/Config/XmlLoggingConfiguration.cs
  • src/NLog/Config/XmlParserConfigurationElement.cs
  • src/NLog/Internal/CollectionExtensions.cs
  • src/NLog/Internal/ReflectionHelpers.cs
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/LogFactory.cs
  • src/NLog/ScopeContext.cs
  • src/NLog/Targets/FileTarget.cs
✅ Files skipped from review due to trivial changes (2)
  • src/NLog/Config/XmlLoggingConfiguration.cs
  • src/NLog/ScopeContext.cs
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/NLog/Conditions/ConditionRelationalExpression.cs
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/Internal/ReflectionHelpers.cs
  • src/NLog/Config/ConfigurationItemFactory.cs
  • src/NLog/LogFactory.cs
  • src/NLog/Targets/FileTarget.cs
  • src/NLog/Config/XmlParserConfigurationElement.cs

Walkthrough

Refactors avoid mutating collections while enumerating: LogFactory defers logger-cache removals; FileTarget clones the open-file cache before iterating. Several small collection constructions switch from List/LINQ to preallocated arrays or literals; a ToArray extension was added.

Changes

AOT-Safe iteration and small collection changes

Layer / File(s) Summary
Logger cache deferred removal
src/NLog/LogFactory.cs
PurgeObsoleteLoggers collects obsolete cache entries into a temporary collection during iteration, then removes those keys in a second pass to avoid mutation while enumerating.
File target cache cloning
src/NLog/Targets/FileTarget.cs
CloseTarget() and PruneOpenFileCacheUsingTimeout() replace LINQ ToList() snapshots with explicit Dictionary clones (preserving comparer) and iterate the clone when closing or pruning appenders.
LoggingConfiguration targets and rules
src/NLog/Config/LoggingConfiguration.cs
Adjusted XML remarks; GetAllTargetsThreadSafe() now returns an IList<Target> backed by ToArray(); ConfiguredNamedTargets constructs a ReadOnlyCollection<Target> from that result; AllTargets seeded from GetAllTargetsThreadSafe() then augmented by iterating _configItems.
Array and snapshot conversions
src/NLog/Config/ConfigurationItemFactory.cs, src/NLog/Config/XmlLoggingConfiguration.cs, src/NLog/ScopeContext.cs
Multiple methods switch from List/LINQ snapshots to fixed arrays or ICollection.ToArray() calls to materialize collections without LINQ allocations.
Condition type-order array
src/NLog/Conditions/ConditionRelationalExpression.cs
BuildTypeOrderDictionary() uses an array literal for the ordered type list and updates loop bounds from Count to Length.
XML parser children array
src/NLog/Config/XmlParserConfigurationElement.cs
ParseChildren preallocates an XmlParserConfigurationElement[] sized to childElements.Count and assigns each child by index.
CollectionExtensions.ToArray
src/NLog/Internal/CollectionExtensions.cs
Adds public static TItem[] ToArray<TItem>(this ICollection<TItem> items) which allocates an array, CopyTo's the collection into it, and returns it.
Reflection parameter array
src/NLog/Internal/ReflectionHelpers.cs
BuildParameterList preallocates an Expression[] and fills it by index instead of accumulating into a List<Expression>.
StringBuilder zero-padded table
src/NLog/Internal/StringBuilderExt.cs
Replaces runtime LINQ generation of _zeroPaddedDigits with a hardcoded string[] initializer for values "00"–"59".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I nibble through code with careful paws,
I gather keys before I pause,
Arrays and clones snug in my lair,
No iterator tears to snare,
A clean commit — I hop with glee.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the changes, their rationale, and any testing performed to validate the AOT file-size reduction.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset—reducing AOT file-size by replacing ToList() calls with more efficient alternatives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/NLog/LogFactory.cs (1)

1180-1186: ⚡ Quick win

Prefer collecting only keys instead of key-value pairs.

The aotFriendlyRemoval dictionary stores both keys and values (WeakReference instances), but line 1190 only uses removeLogger.Key. The values are never accessed, making their storage wasteful.

♻️ Refactor to collect only keys
-                var aotFriendlyRemoval = new Dictionary<LoggerCacheKey, WeakReference>();
+                var aotFriendlyRemoval = new List<LoggerCacheKey>();
                 foreach (var item in _loggerCache)
                 {
                     var logger = Retrieve(item.Key);
                     if (logger is null)
-                        aotFriendlyRemoval[item.Key] = item.Value;
+                        aotFriendlyRemoval.Add(item.Key);
                 }
 
                 foreach (var removeLogger in aotFriendlyRemoval)
                 {
-                    _loggerCache.Remove(removeLogger.Key);
+                    _loggerCache.Remove(removeLogger);
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NLog/LogFactory.cs` around lines 1180 - 1186, The aotFriendlyRemoval
currently stores key/value pairs (Dictionary<LoggerCacheKey, WeakReference>) but
only the keys are used; change it to collect just keys (e.g.,
List<LoggerCacheKey> or HashSet<LoggerCacheKey>) instead of a Dictionary, add
item.Key to that collection when Retrieve(item.Key) returns null, and then
iterate that keys collection to remove entries from _loggerCache; update
references to aotFriendlyRemoval accordingly (variables: aotFriendlyRemoval,
LoggerCacheKey, _loggerCache, Retrieve, removeLogger.Key).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/NLog/LogFactory.cs`:
- Around line 1180-1186: The aotFriendlyRemoval currently stores key/value pairs
(Dictionary<LoggerCacheKey, WeakReference>) but only the keys are used; change
it to collect just keys (e.g., List<LoggerCacheKey> or HashSet<LoggerCacheKey>)
instead of a Dictionary, add item.Key to that collection when Retrieve(item.Key)
returns null, and then iterate that keys collection to remove entries from
_loggerCache; update references to aotFriendlyRemoval accordingly (variables:
aotFriendlyRemoval, LoggerCacheKey, _loggerCache, Retrieve, removeLogger.Key).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9647ffbb-a766-41f9-a975-50a26454df46

📥 Commits

Reviewing files that changed from the base of the PR and between b1ede3e and 169c1d0.

📒 Files selected for processing (2)
  • src/NLog/LogFactory.cs
  • src/NLog/Targets/FileTarget.cs

@snakefoot
snakefoot force-pushed the AotGenericCollections branch 2 times, most recently from 3bfa42f to 5815022 Compare May 17, 2026 11:11
@pull-request-size pull-request-size Bot added size/M and removed size/S labels May 17, 2026
@snakefoot
snakefoot force-pushed the AotGenericCollections branch 3 times, most recently from d230d33 to af59019 Compare May 17, 2026 11:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/NLog/LogFactory.cs (1)

1180-1191: ⚡ Quick win

Eliminate redundant dictionary lookups during purge.

Line 1183 calls Retrieve(item.Key), which performs a dictionary lookup to access the WeakReference, but the iteration already provides item.Value as the WeakReference. Directly checking item.Value.Target is null eliminates O(n) redundant lookups.

Additionally, since only keys are needed for removal, consider using List<LoggerCacheKey> instead of Dictionary<LoggerCacheKey, WeakReference> to simplify the code and reduce allocations.

♻️ Proposed refactor
 public void PurgeObsoleteLoggers()
 {
-    var aotFriendlyRemoval = new Dictionary<LoggerCacheKey, WeakReference>();
+    var aotFriendlyRemoval = new List<LoggerCacheKey>();
     foreach (var item in _loggerCache)
     {
-        var logger = Retrieve(item.Key);
-        if (logger is null)
-            aotFriendlyRemoval[item.Key] = item.Value;
+        if (item.Value.Target is null)
+            aotFriendlyRemoval.Add(item.Key);
     }

-    foreach (var removeLogger in aotFriendlyRemoval)
+    foreach (var removeKey in aotFriendlyRemoval)
     {
-        _loggerCache.Remove(removeLogger.Key);
+        _loggerCache.Remove(removeKey);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NLog/LogFactory.cs` around lines 1180 - 1191, The current purge loop
redundantly calls Retrieve(item.Key) causing extra dictionary lookups; instead
check the iterated WeakReference directly by testing item.Value.Target == null
and collect only keys to remove (use a List<LoggerCacheKey> removeKeys). Replace
the Dictionary<LoggerCacheKey, WeakReference> aotFriendlyRemoval with
List<LoggerCacheKey> removeKeys, add item.Key to removeKeys when
item.Value.Target is null, and then iterate removeKeys to call
_loggerCache.Remove(key); this eliminates redundant lookups and reduces
allocations while keeping the removal logic around _loggerCache and types
LoggerCacheKey and WeakReference intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/NLog/LogFactory.cs`:
- Around line 1180-1191: The current purge loop redundantly calls
Retrieve(item.Key) causing extra dictionary lookups; instead check the iterated
WeakReference directly by testing item.Value.Target == null and collect only
keys to remove (use a List<LoggerCacheKey> removeKeys). Replace the
Dictionary<LoggerCacheKey, WeakReference> aotFriendlyRemoval with
List<LoggerCacheKey> removeKeys, add item.Key to removeKeys when
item.Value.Target is null, and then iterate removeKeys to call
_loggerCache.Remove(key); this eliminates redundant lookups and reduces
allocations while keeping the removal logic around _loggerCache and types
LoggerCacheKey and WeakReference intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3e998665-a903-40ce-9d26-5c17d3e3aac3

📥 Commits

Reviewing files that changed from the base of the PR and between d230d33 and af59019.

📒 Files selected for processing (5)
  • src/NLog/Conditions/ConditionRelationalExpression.cs
  • src/NLog/Config/ConfigurationItemFactory.cs
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/LogFactory.cs
  • src/NLog/Targets/FileTarget.cs
✅ Files skipped from review due to trivial changes (2)
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/Conditions/ConditionRelationalExpression.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/NLog/Targets/FileTarget.cs
  • src/NLog/Config/ConfigurationItemFactory.cs

@snakefoot
snakefoot force-pushed the AotGenericCollections branch 5 times, most recently from 3db079f to aa0a7c8 Compare May 17, 2026 11:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/NLog/LogFactory.cs (1)

1180-1185: ⚡ Quick win

Consider using List instead of Dictionary for the removal collection.

The aotFriendlyRemoval dictionary stores WeakReference values that are never used—only the keys are needed in the removal loop at line 1189. Using List<LoggerCacheKey> would be more efficient and clearer.

♻️ Proposed refactor
-var aotFriendlyRemoval = new Dictionary<LoggerCacheKey, WeakReference>();
+var aotFriendlyRemoval = new List<LoggerCacheKey>();
 foreach (var item in _loggerCache)
 {
     if (item.Value.Target is null)
-        aotFriendlyRemoval[item.Key] = item.Value;
+        aotFriendlyRemoval.Add(item.Key);
 }

 foreach (var removeLogger in aotFriendlyRemoval)
 {
-    _loggerCache.Remove(removeLogger.Key);
+    _loggerCache.Remove(removeLogger);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NLog/LogFactory.cs` around lines 1180 - 1185, Replace the temporary
Dictionary aotFriendlyRemoval with a List<LoggerCacheKey> since only keys are
used: while iterating _loggerCache, add item.Key to the list when
item.Value.Target is null (use the existing LoggerCacheKey type), then iterate
that List<LoggerCacheKey> to remove entries from _loggerCache; update references
to aotFriendlyRemoval accordingly in the removal loop to improve clarity and
performance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/NLog/Internal/CollectionExtensions.cs`:
- Around line 44-49: The ToArray<TItem>(this ICollection<TItem> items) extension
lacks null-checking and will throw NullReferenceException; add a guard at the
start of the method that throws ArgumentNullException(nameof(items)) when items
is null, then proceed to allocate the array and call items.CopyTo as before so
callers receive a clear ArgumentNullException instead of a NRE.

---

Nitpick comments:
In `@src/NLog/LogFactory.cs`:
- Around line 1180-1185: Replace the temporary Dictionary aotFriendlyRemoval
with a List<LoggerCacheKey> since only keys are used: while iterating
_loggerCache, add item.Key to the list when item.Value.Target is null (use the
existing LoggerCacheKey type), then iterate that List<LoggerCacheKey> to remove
entries from _loggerCache; update references to aotFriendlyRemoval accordingly
in the removal loop to improve clarity and performance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0ba302ff-2993-4a63-adc5-10297862a7bf

📥 Commits

Reviewing files that changed from the base of the PR and between 3db079f and aa0a7c8.

📒 Files selected for processing (8)
  • src/NLog/Conditions/ConditionRelationalExpression.cs
  • src/NLog/Config/ConfigurationItemFactory.cs
  • src/NLog/Config/XmlParserConfigurationElement.cs
  • src/NLog/Internal/CollectionExtensions.cs
  • src/NLog/Internal/ReflectionHelpers.cs
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/LogFactory.cs
  • src/NLog/Targets/FileTarget.cs
✅ Files skipped from review due to trivial changes (3)
  • src/NLog/Targets/FileTarget.cs
  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/Internal/ReflectionHelpers.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/NLog/Config/XmlParserConfigurationElement.cs
  • src/NLog/Conditions/ConditionRelationalExpression.cs
  • src/NLog/Config/ConfigurationItemFactory.cs

Comment on lines +44 to +49
public static TItem[] ToArray<TItem>(this ICollection<TItem> items)
{
var array = new TItem[items.Count];
items.CopyTo(array, 0);
return array;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add null parameter validation.

The method accesses items.Count and calls items.CopyTo without validating that items is non-null, which will throw NullReferenceException if a null collection is passed.

🛡️ Proposed fix
 public static TItem[] ToArray<TItem>(this ICollection<TItem> items)
 {
+    if (items is null)
+        throw new ArgumentNullException(nameof(items));
     var array = new TItem[items.Count];
     items.CopyTo(array, 0);
     return array;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static TItem[] ToArray<TItem>(this ICollection<TItem> items)
{
var array = new TItem[items.Count];
items.CopyTo(array, 0);
return array;
}
public static TItem[] ToArray<TItem>(this ICollection<TItem> items)
{
if (items is null)
throw new ArgumentNullException(nameof(items));
var array = new TItem[items.Count];
items.CopyTo(array, 0);
return array;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NLog/Internal/CollectionExtensions.cs` around lines 44 - 49, The
ToArray<TItem>(this ICollection<TItem> items) extension lacks null-checking and
will throw NullReferenceException; add a guard at the start of the method that
throws ArgumentNullException(nameof(items)) when items is null, then proceed to
allocate the array and call items.CopyTo as before so callers receive a clear
ArgumentNullException instead of a NRE.

@snakefoot
snakefoot force-pushed the AotGenericCollections branch 6 times, most recently from 09a0e2c to 3498f75 Compare May 17, 2026 12:18
@snakefoot
snakefoot force-pushed the AotGenericCollections branch 2 times, most recently from e54319d to b5938f1 Compare May 17, 2026 12:21
@snakefoot snakefoot closed this May 17, 2026
@snakefoot snakefoot reopened this May 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

@snakefoot
snakefoot merged commit dd16d5e into NLog:dev May 17, 2026
5 of 6 checks passed
@snakefoot snakefoot added this to the 6.1.4 milestone Jul 8, 2026
@snakefoot snakefoot changed the title Reduce AOT file-size by skipping ToList clone for Dictionary Reduce AOT file-size by changing LogFactory PurgeObsoleteLoggers to clone Dictionary insted of ToList Jul 8, 2026
@snakefoot snakefoot changed the title Reduce AOT file-size by changing LogFactory PurgeObsoleteLoggers to clone Dictionary insted of ToList Reduce AOT file-size by changing LogFactory PurgeObsoleteLoggers to clone Dictionary instead of ToList Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant