Skip to content

[dotnet] Trim away CDP when publishing AOT apps #15217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 7, 2025

Conversation

RenderMichael
Copy link
Contributor

@RenderMichael RenderMichael commented Feb 2, 2025

Description

Removes unnecessary AOT warnings from the user's eyes, and trims away a lot of code unless it is directly used.

Motivation and Context

Size and warning savings

Sample app:

Console.WriteLine("Start");

Log.SetLevel(LogEventLevel.Trace);

var options = new ChromeOptions { UseWebSocketUrl = true, BrowserVersion = "131" };

using var driver = new ChromeDriver(options);

driver.Url = "https://www.google.com";

Thread.Sleep(2500);
driver.FindElements(By.CssSelector("div"));

Size before PR: 8.9 MB
image

  • 460 AOT warnings
    image

Size after PR: 6.5 MB
image

  • 2 warnings
    image

Size savings: 2.3 MB
image

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Enhancement


Description

  • Optimized the WebDriver class to reduce application size.

  • Deferred initialization of NetworkManager to save resources.

  • Removed unused or redundant code for cleaner implementation.


Changes walkthrough 📝

Relevant files
Enhancement
WebDriver.cs
Optimize and streamline `WebDriver` initialization             

dotnet/src/webdriver/WebDriver.cs

  • Removed direct initialization of NetworkManager.
  • Deferred NetworkManager initialization using a null-coalescing
    assignment.
  • Deleted unused Network property setter.
  • Cleaned up redundant code for better maintainability.
  • +1/-5     

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Sorry, something went wrong.

    @RenderMichael RenderMichael marked this pull request as ready for review February 2, 2025 03:45
    Copy link
    Contributor

    qodo-merge-pro bot commented Feb 2, 2025

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Thread Safety

    The lazy initialization of Network property using null-coalescing assignment is not thread-safe. Multiple threads accessing this property simultaneously could potentially create multiple NetworkManager instances.

    internal INetwork Network => this.network ??= new NetworkManager(this);

    Copy link
    Contributor

    qodo-merge-pro bot commented Feb 2, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Possible issue
    Add thread-safe initialization pattern

    Add null check before accessing this.network to prevent potential
    NullReferenceException in multi-threaded scenarios. The current implementation might
    have race conditions.

    dotnet/src/webdriver/WebDriver.cs [218]

    -internal INetwork Network => this.network ??= new NetworkManager(this);
    +internal INetwork Network
    +{
    +    get
    +    {
    +        if (this.network == null)
    +        {
    +            lock (this)
    +            {
    +                this.network ??= new NetworkManager(this);
    +            }
    +        }
    +        return this.network;
    +    }
    +}
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    Why: The suggestion addresses a critical thread-safety issue by implementing a double-checked locking pattern, which prevents potential race conditions during NetworkManager initialization in multi-threaded environments.

    8
    Learned
    best practice
    Use modern C# pattern matching instead of explicit null checks when checking interface implementation
    Suggestion Impact:The commit directly implemented the suggested change by replacing '(this as ISupportsLogs) != null' with 'this is ISupportsLogs' on line 33 of the diff, which is exactly what the suggestion recommended.

    code diff:

    -            if ((this as ISupportsLogs) != null)
    +            if (this is ISupportsLogs)
                 {

    While the PR introduces the null-coalescing assignment (??=) operator for the
    Network property, the null check for ISupportsLogs interface could be simplified
    using the is pattern matching operator for more concise and modern C# code.

    dotnet/src/webdriver/WebDriver.cs [81]

    -if ((this as ISupportsLogs) != null)
    +if (this is ISupportsLogs)
     {
    • Apply this suggestion
    6

    Sorry, something went wrong.

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy link
    Member

    @nvborisenko nvborisenko left a comment

    Choose a reason for hiding this comment

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

    Nice done!

    @RenderMichael
    Copy link
    Contributor Author

    Failures not related to changes

    //javascript/node/selenium-webdriver:test-bidi-browsingcontext-inspector-test.js-chrome
    //javascript/node/selenium-webdriver:test-bidi-browsingcontext-inspector-test.js-firefox
    //py:common-chrome-bidi-test/selenium/webdriver/common/bidi_tests.py
    //rb/spec/integration/selenium/webdriver:action_builder-chrome-remote
    //rb/spec/integration/selenium/webdriver:action_builder-firefox-remote
    //rb/spec/integration/selenium/webdriver:driver-chrome
    //rb/spec/integration/selenium/webdriver:driver-chrome-remote
    //rb/spec/integration/selenium/webdriver:select-chrome-remote

    @RenderMichael RenderMichael merged commit 44e35d2 into SeleniumHQ:trunk Mar 7, 2025
    9 of 10 checks passed
    @RenderMichael RenderMichael deleted the devtools-trim branch March 7, 2025 22:03
    @nvborisenko
    Copy link
    Member

    @RenderMichael Big brain is right about thread-safe: if we apply Lazy<T> or simple lock - is would be AOT friendly?

    @RenderMichael
    Copy link
    Contributor Author

    @nvborisenko Unfortunately Lazy is not too AOT-friendly, anything captured in the factory is stuck. It doesn’t matter too much usually (it’s usually not too complex, the Lazy is usually used somewhere anyway, etc). This was a very special case.

    @nvborisenko
    Copy link
    Member

    But in future if we really want to make some lazy property initialization thread-safe and AOT-friendly what coding pattern will be?

    @nvborisenko
    Copy link
    Member

    Like here in BiDi namespace: https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/src/webdriver/BiDi/BiDi.cs. I use Lazy<T>, do we have better options?

    @RenderMichael
    Copy link
    Contributor Author

    Hmm, that might actually be a good place to do the same trick. I would write a sample app that uses only a small part of the BiDi commands, and try replacing the Lazy field with a property that creates the value on initialization with ??=. Maybe it will allow a good amount of code to be trimmed away

    @nvborisenko
    Copy link
    Member

    I asked big brain:

    To replace Lazy in AOT (Ahead-of-Time) environments like Unity IL2CPP or .NET Native, you need a solution that avoids reflection. Here's an AOT-friendly approach using manual lazy initialization:

    1. Why Lazy Isn't AOT-Friendly
      The default constructor of Lazy uses Activator.CreateInstance, which relies on reflection and is incompatible with AOT compilation.

    Even when using Lazy with a factory delegate, some AOT environments may still restrict its use due to internal synchronization mechanisms.

    1. AOT-Friendly Replacement
      Implement a simple thread-safe lazy initializer using a Func delegate and explicit locking:
    public class AotFriendlyLazy<T>
    {
        private readonly Func<T> _factory;
        private T _value;
        private bool _isInitialized;
        private readonly object _lock = new object();
    
        public AotFriendlyLazy(Func<T> factory)
        {
            _factory = factory ?? throw new ArgumentNullException(nameof(factory));
        }
    
        public T Value
        {
            get
            {
                if (!_isInitialized)
                {
                    lock (_lock)
                    {
                        if (!_isInitialized)
                        {
                            _value = _factory();
                            _isInitialized = true;
                        }
                    }
                }
                return _value;
            }
        }
    }

    Usage Example:

    private readonly AotFriendlyLazy<ExpensiveResource> _resource =
        new AotFriendlyLazy<ExpensiveResource>(() => new ExpensiveResource());
    
    public ExpensiveResource Resource => _resource.Value;
    1. Key Features
      No Reflection: Explicitly initializes objects via a provided Func.

    Thread Safety: Uses double-check locking to ensure thread safety.

    AOT Compatibility: Avoids reflection and dynamic code generation.

    1. Alternatives
      Explicit Initialization with Lazy (if supported):
      If your AOT environment allows it, use Lazy with an explicit factory delegate (avoids Activator.CreateInstance):
    // Works if Lazy<T>'s non-reflection code paths are AOT-friendly
    private readonly Lazy<ExpensiveResource> _resource =
        new Lazy<ExpensiveResource>(() => new ExpensiveResource());

    LazyInitializer:
    Use LazyInitializer.EnsureInitialized for simpler scenarios (ensure it doesn’t use reflection in your target environment):

    private ExpensiveResource _resource;
    public ExpensiveResource Resource => 
        LazyInitializer.EnsureInitialized(ref _resource, () => new ExpensiveResource());
    1. When to Use This Approach
      Unity/IL2CPP: Required for compatibility.

    .NET Native/UWP: Avoids reflection restrictions.

    High-Performance Scenarios: Manual control over initialization logic.

    This pattern ensures AOT compatibility while retaining the "initialize-on-first-use" behavior of Lazy.

    @nvborisenko
    Copy link
    Member

    What I know: Lazy<T> is thread-safe. We really want to be thread-safe.

    @RenderMichael
    Copy link
    Contributor Author

    The big brain is not wrong there, about AOT compatibility. I think the normal Lazy is AOT-safe, this PR solves another issue: AOT rooting.

    Anything in the Lazy’s factory is rooted by the AOT compiler as “something which may be invoked”. This means AOT trimming cannot eliminate the NetworkManager constructor, and that constructor also roots the entire DevTools functionality. That’s megabytes of code that is never called.

    Lazy cannot do this. If we had a Lazy<INetwork> then we would not see any of those savings in size.

    @RenderMichael
    Copy link
    Contributor Author

    @nvborisenko I tried it out. Just like I thought, it didn't save anything. (11.0MB - 2.0kB)
    image

    image

    This PR was a special case where the constructor, by itself, was very heavy.

    sandeepsuryaprasad pushed a commit to sandeepsuryaprasad/selenium that referenced this pull request Mar 23, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    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.

    None yet

    2 participants