Skip to content

System.Net.WebProxy Bypass regex list reconstructed for every call to IsBypassed #73703

Closed

Description

@onehourlate

Description

System.Net.WebProxy takes a list of strings for Uris that should bypass the proxy.
This list of strings is compiled into a list of Regex by the method UpdateRegexList

This method is called in the constructor and in the BypassList property setter.
This is normal.

However, this method is also called in IsMatchInBypassList, which is called by IsBypassed
This feels wrong. I don't see why it needs to systematically refresh the regex list in this call.
It re-allocates the array and all the regex, for no reason (as far as I can see anyway).

Regression?

This behavior seems to be copy-pasted from the .Net Framework code, which is also wrong.
https://referencesource.microsoft.com/#system/net/system/Net/webproxy.cs

Analysis

The following code shows a significant difference between the proxy1 et proxy2 execution time
(typically ~230ms vs ~2100ms)
Since the call to IsBypassed returns true for the first regex, the length of the Bypasslist should not matter.

Uri uri = new Uri("https://a.com/");

System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
bool r1 = true;
System.Net.WebProxy proxy1 = new System.Net.WebProxy("http://localhost:9999", true, new string[] { "a" });
sw.Restart();
for (int i = 0; i < 100000; i++) {
    r1 &= proxy1.IsBypassed(uri);
}
sw.Stop();
Console.WriteLine($"Proxy1 : {sw.ElapsedMilliseconds}ms ({r1})");

bool r2 = true;
System.Net.WebProxy proxy2 = new System.Net.WebProxy("http://localhost:9999", true, new string[] { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" });
sw.Restart();
for (int i = 0; i < 100000; i++) {
    r2 &= proxy2.IsBypassed(uri);

}
sw.Stop();
Console.WriteLine($"Proxy2 : {sw.ElapsedMilliseconds}ms ({r2})");

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Aug 10, 2022
  2. added
    help wanted[up-for-grabs] Good issue for external contributors
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Aug 10, 2022
  3. added this to the Future milestone on Aug 10, 2022
  4. MihaZupan commented on Aug 10, 2022

    @MihaZupan
    Member

    I also don't see any reason to rebuild the list in this case. Doing so looks like just a waste of compute.

    Should be a simple change - marking as up for grabs. @onehourlate would you be interested in contributing a fix for this?

  5. campersau commented on Aug 10, 2022

    @campersau
    Contributor

    It looks like the underlying _bypassList is publicly exposed as BypassArrayList which means that it could be modified from the outside:

    public ArrayList BypassArrayList => _bypassList ??= new ArrayList();

  6. removed
    help wanted[up-for-grabs] Good issue for external contributors
    on Aug 10, 2022
  7. MihaZupan commented on Aug 10, 2022

    @MihaZupan
    Member

    You're right... yay legacy APIs :)

    We can still do a lot better here than creating all Regex objects on every request though.
    At a minimum have a flag checking whether that property was ever accessed and skip UpdateRegexList on the hot path.

  8. onehourlate commented on Aug 10, 2022

    @onehourlate
    ContributorAuthor

    Or even subclass ArrayList for the BypassArrayList with something that keeps a dirty flag.
    Since all methods of an ArrayList that modify the list are virtual, that's easily done.
    (what's a bit frustrating is that the ArrayList does that for us already, via the version, but it's private)

    And then check the dirty flag to see if we need to rebuild the regex list.

    class DirtyTrackingArrayList : System.Collections.ArrayList {
    
        public bool IsDirty { get; private set; }
    
        public bool CheckDirty() {
            var dirty = IsDirty;
            IsDirty = false;
            return dirty;
        }
    
        public override object? this[int index] {
            get {
                return base[index];
            }
            set {
                IsDirty = true;
                base[index] = value;
            }
        }
    
    
        public override int Add(object? value) {
            IsDirty = true;
            return base.Add(value);
        }
    
        public override void AddRange(ICollection c) {
            IsDirty = true;
            base.AddRange(c);
        }
    
        public override void Insert(int index, object? value) {
            IsDirty = true;
            base.Insert(index, value);
        }
    
        public override void InsertRange(int index, ICollection c) {
            IsDirty = true;
            base.InsertRange(index, c);
        }
    
       //etc...
    }
    
  9. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Aug 11, 2022
  10. onehourlate commented on Aug 11, 2022

    @onehourlate
    ContributorAuthor

    Made a PR (#73803), if it helps
    (I just skimmed the guidelines, so sorry if I missed something ...)

    Changes in the BypassArrayList are tracked by a Dirty flag
    RegexList is only updated if Dirty flag is set
    Added a few unit tests (one unit test uses reflexion : not sure if it's "allowed")

  11. added a commit that references this issue on Aug 14, 2022
    f27b711
  12. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Aug 14, 2022
  13. modified the milestones: Future, 7.0.0 on Aug 15, 2022
  14. ghost locked as resolved and limited conversation to collaborators on Sep 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions