Visitar URL original
Reduce AOT file-size by changing LoggingConfiguration CheckUnusedTargets to use HashSet instead of Linq by snakefoot · Pull Request #6195 · NLog/NLog · GitHub
Skip to content

Reduce AOT file-size by changing LoggingConfiguration CheckUnusedTargets to use HashSet instead of Linq - #6195

Merged
snakefoot merged 1 commit into
NLog:devfrom
snakefoot:CheckUnusedTargets
May 25, 2026
Merged

snakefoot merged 1 commit into
NLog:devfrom
snakefoot:CheckUnusedTargets

Conversation

@snakefoot

@snakefoot snakefoot commented May 25, 2026 •

Copy link
Copy Markdown
Contributor

After:
ProcessPath: 'C:\projects\nlog\Tests\TestTrimPublish\bin\release\net8.0\win-X64\publish\TestTrimPublish.exe' with FileSize: 4774912
Before:
ProcessPath: 'C:\projects\nlog\Tests\TestTrimPublish\bin\release\net8.0\win-X64\publish\TestTrimPublish.exe' with FileSize: 4850688
NLog v6.1.3
ProcessPath: 'C:\projects\nlog\tests\TestTrimPublish\bin\release\net8.0\win-x64\publish\TestTrimPublish.exe' with FileSize: 5486592

@coderabbitai

coderabbitai Bot commented May 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR changes GetAllTargetsThreadSafe() to return a Target[] snapshot, adds an ICollection.Select extension that yields results lazily with an early-empty fast path, and refactors CheckUnusedTargets() to detect unused targets using a wrapping-target lookup for wrapper/compound relationships.

Changes

Unused Target Detection Refactoring

Layer / File(s) Summary
Target enumeration contract and list utility
src/NLog/Config/LoggingConfiguration.cs, src/NLog/Internal/CollectionExtensions.cs
GetAllTargetsThreadSafe() now returns a Target[] snapshot. A new Select<TSource, TResult> extension on ICollection<TSource> yields transformed items lazily and returns ArrayHelper.Empty<TResult>() when empty.
Unused target detection with wrapper tracking
src/NLog/Config/LoggingConfiguration.cs
CheckUnusedTargets() now builds a HashSet of target names referenced by rules, constructs a wrapping-lookup via BuildWrappingTargetsLookup() that maps wrapped/inner targets to their wrapper/compound parents, and uses a local IsUnusedInList predicate to determine and log unused targets (debug/counts now use array length).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through targets, wrapped and neat,
Arrays for snapshots beneath my feet,
Select yields softly, empty paths clear,
Wrappers now mapped — unused ones appear! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.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 description was provided by the author, making it impossible to evaluate content relevance to the changeset. Add a pull request description explaining the purpose, motivation, and scope of the LINQ removal refactoring.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: refactoring CheckUnusedTargets to eliminate LINQ dependency in LoggingConfiguration.
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.

@snakefoot
snakefoot force-pushed the CheckUnusedTargets branch from e003aaf to 41143f0 Compare May 25, 2026 13:07
@snakefoot
snakefoot force-pushed the CheckUnusedTargets branch from 41143f0 to c00c477 Compare May 25, 2026 13:12

@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

🤖 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/Config/LoggingConfiguration.cs`:
- Around line 912-915: The loop building targetNamesAtRules incorrectly
enumerates rule.Targets directly; change it to use the thread-safe enumerator
rule.GetTargetsThreadSafe() (consistent with GetAllTargetsToFlush) so concurrent
modifications won't throw or produce inconsistent results—update the foreach
that currently reads "foreach (var t in rule.Targets)" to "foreach (var t in
rule.GetTargetsThreadSafe())" and keep adding t.Name to targetNamesAtRules.
🪄 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: b9020103-e65f-4bd4-86a4-086f7df342a9

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee2f84 and 41143f0.

📒 Files selected for processing (2)
  • src/NLog/Config/LoggingConfiguration.cs
  • src/NLog/Internal/CollectionExtensions.cs

Comment thread src/NLog/Config/LoggingConfiguration.cs Outdated
Comment on lines +912 to +915
var targetNamesAtRules = new HashSet<string>();
foreach (var rule in GetLoggingRulesThreadSafe())
foreach (var t in rule.Targets)
targetNamesAtRules.Add(t.Name);

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 | 🟠 Major | ⚡ Quick win

Use thread-safe access for rule targets.

This iterates rule.Targets directly, but other methods in this file (e.g., GetAllTargetsToFlush at line 713-720) use rule.GetTargetsThreadSafe() for thread-safe enumeration. If the rule's target list is modified concurrently, this could throw during enumeration or yield inconsistent results.

Proposed fix
 var targetNamesAtRules = new HashSet<string>();
 foreach (var rule in GetLoggingRulesThreadSafe())
-    foreach (var t in rule.Targets)
+    foreach (var t in rule.GetTargetsThreadSafe())
         targetNamesAtRules.Add(t.Name);
📝 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
var targetNamesAtRules = new HashSet<string>();
foreach (var rule in GetLoggingRulesThreadSafe())
foreach (var t in rule.Targets)
targetNamesAtRules.Add(t.Name);
var targetNamesAtRules = new HashSet<string>();
foreach (var rule in GetLoggingRulesThreadSafe())
foreach (var t in rule.GetTargetsThreadSafe())
targetNamesAtRules.Add(t.Name);
🤖 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/Config/LoggingConfiguration.cs` around lines 912 - 915, The loop
building targetNamesAtRules incorrectly enumerates rule.Targets directly; change
it to use the thread-safe enumerator rule.GetTargetsThreadSafe() (consistent
with GetAllTargetsToFlush) so concurrent modifications won't throw or produce
inconsistent results—update the foreach that currently reads "foreach (var t in
rule.Targets)" to "foreach (var t in rule.GetTargetsThreadSafe())" and keep
adding t.Name to targetNamesAtRules.

@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

♻️ Duplicate comments (1)
src/NLog/Config/LoggingConfiguration.cs (1)

912-913: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use thread-safe target enumeration in rule scan.

CheckUnusedTargets() still enumerates rule.Targets directly. This can race with concurrent mutations and produce inconsistent/throwing traversal; use rule.GetTargetsThreadSafe() here.

🔧 Proposed fix
-            var targetNamesAtRules = new HashSet<string>(GetLoggingRulesThreadSafe().SelectMany(r => r.Targets).Select(t => t.Name));
+            var targetNamesAtRules = new HashSet<string>();
+            foreach (var rule in GetLoggingRulesThreadSafe())
+            {
+                foreach (var target in rule.GetTargetsThreadSafe())
+                {
+                    targetNamesAtRules.Add(target.Name);
+                }
+            }
🤖 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/Config/LoggingConfiguration.cs` around lines 912 - 913,
CheckUnusedTargets() currently enumerates rule.Targets which can race with
concurrent mutations; change the enumeration to use the thread-safe accessor by
calling rule.GetTargetsThreadSafe() (and keep using GetLoggingRulesThreadSafe())
so the HashSet construction uses GetLoggingRulesThreadSafe().SelectMany(r =>
r.GetTargetsThreadSafe()).Select(t => t.Name) instead of r.Targets to avoid
inconsistent/throwing traversal.
🤖 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/Config/LoggingConfiguration.cs`:
- Around line 916-927: The current IsUnusedInList logic treats a wrapperTarget
as used as soon as it has any parent wrapper
(targets.ContainsKey(wrapperTarget)), which falsely marks whole chains as used;
change this to traverse the wrapper chain (follow targets[wrapperTarget]) and
only treat a wrapperTarget as used if its Name is in targetNamesAtRules or any
ancestor in the wrapper chain is referenced, using a visited/HashSet to avoid
cycles; update the same pattern where targets.ContainsKey(wrapperTarget) is used
(also at the later check around the 939-940 area) so inner targets are only
considered used when an actual referenced ancestor exists.

---

Duplicate comments:
In `@src/NLog/Config/LoggingConfiguration.cs`:
- Around line 912-913: CheckUnusedTargets() currently enumerates rule.Targets
which can race with concurrent mutations; change the enumeration to use the
thread-safe accessor by calling rule.GetTargetsThreadSafe() (and keep using
GetLoggingRulesThreadSafe()) so the HashSet construction uses
GetLoggingRulesThreadSafe().SelectMany(r => r.GetTargetsThreadSafe()).Select(t
=> t.Name) instead of r.Targets to avoid inconsistent/throwing traversal.
🪄 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: 73c74090-b01b-40f6-bb12-d3664a354513

📥 Commits

Reviewing files that changed from the base of the PR and between 41143f0 and c00c477.

📒 Files selected for processing (2)
  • src/NLog/Config/LoggingConfiguration.cs
  • src/NLog/Internal/CollectionExtensions.cs

Comment on lines +916 to 927
bool IsUnusedInList(Target target1, Dictionary<Target, List<Target>> targets)
{
if (targets.Contains(target1))
if (targets.TryGetValue(target1, out var wrappers))
{
foreach (var wrapperTarget in targets[target1])
foreach (var wrapperTarget in wrappers)
{
if (targetNamesAtRules.Contains(wrapperTarget.Name))
return false;

if (wrappedTargets.Contains(wrapperTarget))
return false;

if (compoundTargets.Contains(wrapperTarget))
if (targets.ContainsKey(wrapperTarget))
return false;
}

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 | 🟠 Major | ⚡ Quick win

Traverse wrapper chains before deciding “used”.

The targets.ContainsKey(wrapperTarget) early return marks inner targets as used just because they have a parent wrapper, even when no ancestor wrapper is referenced by any rule. This misses genuinely unused chains.

🔧 Proposed fix
-            bool IsUnusedInList(Target target1, Dictionary<Target, List<Target>> targets)
+            bool IsUsedByRuleOrWrapperChain(Target target1, Dictionary<Target, List<Target>> targets)
             {
-                if (targets.TryGetValue(target1, out var wrappers))
+                var pendingTargets = new Stack<Target>();
+                var visited = new HashSet<Target>(SingleItemOptimizedHashSet<Target>.ReferenceEqualityComparer.Default);
+                pendingTargets.Push(target1);
+
+                while (pendingTargets.Count > 0)
                 {
+                    var current = pendingTargets.Pop();
+                    if (!targets.TryGetValue(current, out var wrappers))
+                        continue;
+
                     foreach (var wrapperTarget in wrappers)
                     {
                         if (targetNamesAtRules.Contains(wrapperTarget.Name))
-                            return false;
+                            return true;
 
-                        if (targets.ContainsKey(wrapperTarget))
-                            return false;
+                        if (visited.Add(wrapperTarget))
+                            pendingTargets.Push(wrapperTarget);
                     }
                 }
 
-                return true;
+                return false;
             }
@@
-                if (!IsUnusedInList(target, wrappingTargets))
+                if (IsUsedByRuleOrWrapperChain(target, wrappingTargets))
                     continue;

Also applies to: 939-940

🤖 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/Config/LoggingConfiguration.cs` around lines 916 - 927, The current
IsUnusedInList logic treats a wrapperTarget as used as soon as it has any parent
wrapper (targets.ContainsKey(wrapperTarget)), which falsely marks whole chains
as used; change this to traverse the wrapper chain (follow
targets[wrapperTarget]) and only treat a wrapperTarget as used if its Name is in
targetNamesAtRules or any ancestor in the wrapper chain is referenced, using a
visited/HashSet to avoid cycles; update the same pattern where
targets.ContainsKey(wrapperTarget) is used (also at the later check around the
939-940 area) so inner targets are only considered used when an actual
referenced ancestor exists.

@sonarqubecloud

Copy link
Copy Markdown

@snakefoot
snakefoot merged commit 8cf58a8 into NLog:dev May 25, 2026
5 of 6 checks passed
@snakefoot snakefoot added this to the 6.1.4 milestone Jul 8, 2026
@snakefoot snakefoot changed the title LoggingConfiguration - CheckUnusedTargets without Linq Reduce AOT file-size by changing LoggingConfiguration CheckUnusedTargets to use HashSet instead of Linq 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