Skip to content

Avoid excessive memory usage and slow TrackBar control #8060

Merged
lonitra merged 30 commits intodotnet:mainfrom
conorgee:semester_of_code
Dec 27, 2022
Merged

Avoid excessive memory usage and slow TrackBar control #8060
lonitra merged 30 commits intodotnet:mainfrom
conorgee:semester_of_code

Conversation

@conorgee
Copy link
Contributor

@conorgee conorgee commented Oct 28, 2022

Allow users to set large a range of values on the trackbar without excessive memory consumption.

Fixes #329

Proposed changes

  • Solving the issue of Memory consumption for the trackbar for large range of values on trackbar.

  • Turning TBS_AUTOTICKS off when the the tick range is larger then the physical limitations of the trackbar by checking this with the Boolean method ShouldAutoDrawTicks(). This sets the flag _autoDrawTicks in CreateParams.

  • The fix will be put behind a feature switch called TrackBarModernRendering in LocalAppContextSwitches because of the significant risk - primarily due to the use of RecreateHandle() method. If they opt out of the feature the ShouldAutoDrawTicks() then it will always return true. This means that the way of rendering ticks will be reverted back to the old way e.g. using TBS_AUTOTICKS and setting tick frequency with the TBM_SETTICFREQ message.

         /// <summary>
         /// This checks all the use cases that we potentially might want to keep `TBS_AUTOTICKS`.
         /// </summary>
         private bool ShouldAutoDrawTicks()
         {
             // If the user decides to opt out of TrackBarModernRendering for drawing ticks,
             // by returning true autoticks will be set to true which will
             // use the old way of rendering ticks on the trackbar.
             // TBS_AUTOTICKS will be enabled and sending
             // the TBM_SETTICFREQ message to set tick frequency.
             if (!LocalAppContextSwitches.TrackBarModernRendering)
             {
                 return true;
             }
    
             if (TickStyle == TickStyle.None)
             {
                 return true;
             }
    
             int size = Orientation == Orientation.Horizontal ? Size.Width : Size.Height;
             if (size == 0)
             {
                 return true;
             }
    
             uint range = (uint)(_maximum - _minimum);
             return range <= (size / 2);
         }
  • The method DrawTicks() replaces the message TBM_SETTICFREQ It gets called everywhere TBM_SETTICFREQ would have been used.

  • It checks whether the tickstyle is None (return) or _autoDrawTicks is true which sets the _tickFrequency using the TBM_SETTICFREQ message then return .

  • When the tick range is greater then physical limitaions of the size of the trackbar it calculates if the users set _tickFrequency will fit on the trackbar.

  • If the value is greater then the physical limiations we have then we set drawnTickFrequency to range / maxTickCount.

  • Using a for loop we maually draw the ticks based on the drawnTickFrequency set.

       private void DrawTicks()
       {
           if (_tickStyle == TickStyle.None)
           {
               return;
           }

           int drawnTickFrequency = _tickFrequency;
           // Will be true if they opt out TrackBarModernRendering.
           if (_autoDrawTicks)
           {
               PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETTICFREQ, (WPARAM)drawnTickFrequency);
               return;
           }

           // Divide by 2 because otherwise the ticks appear as a solid line.
           int maxTickCount = (Orientation == Orientation.Horizontal ? Size.Width : Size.Height) / 2;
           uint range = (uint)(_maximum - _minimum);
           if (range > maxTickCount && maxTickCount != 0)
           {
               int calculatedTickFrequency = (int)(range / maxTickCount);
               if (calculatedTickFrequency > drawnTickFrequency)
               {
                   drawnTickFrequency = calculatedTickFrequency;
               }
           }

           PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_CLEARTICS, (WPARAM)1, (LPARAM)0);
           for (int i = _minimum + drawnTickFrequency; i < _maximum - drawnTickFrequency; i += drawnTickFrequency)
           {
               LRESULT lresult = PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETTIC, lParam: (IntPtr)i);
               Debug.Assert((bool)(BOOL)lresult);
           }
       }
  • Before Recreating Handle check to see If it needs to be recreated by using ShouldRecreateHandle() to set the recreateHandle flag in places where DrawTicks() gets called
       /// <summary>
       /// Determine if the decision of whether the ticks drawing,
       /// is performed by the native control or the Windows Forms runtime
       /// is still valid. If it's no longer valid, we'll need to recreate the native control.
       /// If user opts out of TrackBarModernRendering then this will always return false.
       /// </summary>
       private bool ShouldRecreateHandle() => IsHandleCreated && _autoDrawTicks != ShouldAutoDrawTicks();
  • DrawTicks() Is refrenced in three places first is when the user wants to change the TickFrequency and the second place is SetRange() when max or min is changed and OnHandleCreated() .
  • If recreateHandle is true then DrawTicks() will not be called in either of the first two places it will be called in OnHandleCreated()
       /// <summary>
       ///  Indicates just how many ticks will be drawn. For a TrackBar with a
       ///  range of 0..100, it might be impractical to draw all 100 ticks for a
       ///  very small control. Passing in a value of 5 here would only draw
       ///  20 ticks -- i.e. each tick would represent 5 units in the TrackBars
       ///  range of values.
       /// </summary>
      public int TickFrequency
       {
           ....

               // Determine if the decision of whether the ticks drawing,
               // is performed by the native control or the Windows Forms runtime
               // is still valid. If it's no longer valid, we'll need to recreate the native control.
               bool recreateHandle = ShouldRecreateHandle();
               if (IsHandleCreated)
               {
                   PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETTICFREQ, (WPARAM)value);
                   // If user opts out of TrackBarModernRendering then recreateHandle
                   // will always be false.
                   if (recreateHandle)
                   {
                       RecreateHandle();
                   }
                   else
                   {
                       DrawTicks();
                       Invalidate();
                   }
               }
           }
       }
       /// <summary>
       ///  Lets you set the entire range for the TrackBar control at once.
       ///  The values passed are both the lower and upper limits to the range
       ///  with which the control will work.
       /// </summary>
       public void SetRange(int minValue, int maxValue)
       {
           ....

               // Determine if the decision of whether the ticks drawing,
               // is performed by the native control or the Windows Forms runtime
               // is still valid. If it's no longer valid, we'll need to recreate the native control.
               bool recreateHandle = ShouldRecreateHandle();
               // If user opts out of TrackBarModernRendering then recreateHandle
               // will always be false.
               if (IsHandleCreated && !recreateHandle)
               {
                   PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETRANGEMIN, (WPARAM)(BOOL)false, (LPARAM)_minimum);
                   PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETRANGEMAX, (WPARAM)(BOOL)true, (LPARAM)_maximum);
                   DrawTicks();
                   Invalidate();
               }

              ....

               // If user opts out of TrackBarModernRendering then recreateHandle
               // will always be false.
               if (recreateHandle)
               {
                   RecreateHandle();
               }
               else
               {
                   SetTrackBarPosition();
               }
           }
       }
  • If the trackbar has been created or RecreateHandle() is excuted then we excute DrawTicks(); here
    protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);

            if (!IsHandleCreated)
            {
                return;
            }

            Debug.Assert(_autoDrawTicks == ShouldAutoDrawTicks());
            PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETRANGEMIN, (WPARAM)(BOOL)false, (LPARAM)_minimum);
            PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETRANGEMAX, (WPARAM)(BOOL)false, (LPARAM)_maximum);
            DrawTicks();
            PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETPAGESIZE, (WPARAM)0, (LPARAM)_largeChange);
            PInvoke.SendMessage(this, (User32.WM)PInvoke.TBM_SETLINESIZE, (WPARAM)0, (LPARAM)_smallChange);
            SetTrackBarPosition();
            AdjustSize();
        }

Customer Impact

  • Solves the issue of ticks slowing down performance via large memory consumption.

Regression?

  • No.

Risk

  • Significant risk - primarily due to the use of RecreateHandle() method.

Screenshots

Before

image

After

image

Test methodology

Test Feature Switch

  • With TrackBarModernRendering set to false

image

  • With TrackBarModernRendering set to true

image

Test int.MinValue

  • For min int value – trackbar breaks and is unuseable seems to be issue with the trackbar range cannot be greater then the positive value for an integer e.g. -2,147,483,648 would create a range larger then 2,147,483,647 .
  • This is for both horizontal and vertical orientation
  • set a trackbar width to 588px, orientation horizontal, min=int.MinValue, max=0, tick freq=1 -> expect memory consumption to be low. 0 ticks drawn on trackbar. Autoticks disabled

image

image

  • set a trackbar width to 364px, orientation vertical, min=-int.MinValue, max=0, tick freq=1 -> expect memory consumption to be low. 0 ticks drawn on trackbar. Autoticks disabled

image

Test int.MaxValue

  • For max int value – trackbar works for both horizontal and vertical orientation
  • set a trackbar width to 588px, orientation horizontal, min=0, max=int.MaxValue, tick freq=1 -> expect memory consumption to be low. 294 ticks drawn on trackbar. Autoticks disabled

image

  • set a trackbar width to 364px, orientation vertical, min=0, max=int.MaxValue, tick freq=1 -> expect memory consumption to be low. 182 ticks drawn on trackbar. Autoticks disabled

image

Basic Tests

  • Testing for a large value range to turn off TBS_AUTOTICKS then setting to a small range for TBS_AUTOTICKS to be turned on , works for both horizontal and vertical orientation.

  • set a trackbar width to 588px, orientation horizontal, min=-100, max=1000, tick freq=1 -> expect memory consumption to be low e.g 20mb. expect 294 ticks, autoTicks disabled ticks manually drawn

image

  • set a trackbar width to 588px, orientation horizontal, min=-100, max=100, tick freq=1 -> expect memory consumption to be low e.g 20mb. expect 200 ticks, autoTicks enabled

image

  • set a trackbar width to 588px, orientation horizontal, min=-100, max=100, tick freq=1 , Tickstyle set to none, -> expect memory consumption to be low e.g 20mb. expect 0 ticks, autoTicks disabled

image

Trackbar Size tests

  • works the same for both horizontal and vertical orientation.
  • set a trackbar width to 0px, orientation vertical, min=-100, max=100, tick freq=10 -> expect memory consumption to be low e.g 20mb.expect 0 ticks, autoTicks disabled
    image
  • set a trackbar width to 1,000px, orientation horizontal, min=-100, max=100, tick freq=1 -> expect memory consumption to be low e.g 20mb. expect 200 ticks, autoTicks enabled

image

  • set a trackbar width to 1,000px, orientation horizontal, min=-1000, max=1000, tick freq=1 -> expect memory consumption to be low e.g 20mb. expect 500 ticks, autoTicks disabled
    image

  • set a trackbar width to 1,000px, orientation horizontal, min=-1000, max=1000, tick freq=100 -> expect memory consumption to be low e.g 20mb. expect 20 ticks, autoTicks disabled
    image

Tickfrequency tests

  • works the same for both horizontal and vertical orientation.

  • set a trackbar width to 588px, orientation horizontal, min=-100, max=100, tick freq=100 -> expect memory consumption to be low e.g 20mb. expect 3 ticks, autoTicks enabled

image

  • set a trackbar width to 364px, orientation vertical, min=-10000, max=10000, tick freq=1000 -> expect memory consumption to be low e.g 20mb. expect 20 ticks, autoTicks disabled

image

  • TODO: Add unit tests.
  • TODO: Add UIIntegration tests.

Accessibility testing

TODO

Test environment(s)

.NET 8.0.0 - alpha.1.22605.1

Microsoft Reviewers: Open in CodeFlow

@ghost ghost assigned conorgee Oct 28, 2022
@dnfadmin
Copy link

dnfadmin commented Oct 28, 2022

CLA assistant check
All CLA requirements met.

@ghost ghost added the draft draft PR label Oct 28, 2022
@RussKie RussKie self-assigned this Oct 31, 2022
@RussKie RussKie changed the title Semester of code Avoid excessive memory consumption by TrackBar control Oct 31, 2022
@elachlan
Copy link
Contributor

elachlan commented Nov 1, 2022

You probably should have rebased this instead of pulling all the changes in from main. It makes it very difficult for the reviewers if you don't.

@RussKie
Copy link
Contributor

RussKie commented Nov 2, 2022

Hi @conorgee, it looks like instead of rebasing you pulled completely unrelated changes in.
I suggest you do the following to fix this:

  1. Hard reset your branch back to fc60e82 commit. There are few ways of doing this - either via the command line (git reset --hard fc60e82) or via git UI (in Git Extensions right click on fc60e82 commit in the revision grid > "Reset current branch to here..." > Hard reset),
  2. Fetch the upstream trunk (git fetch --all from cli) or via git UI
  3. Rebase your branch on top of the latest upstream/main (something like git -c rebase.autoSquash=false rebase upstream/main) or via in git UI (in Git Extensions right click on the upstream/main commit in the revision grid > "Rebase current branch on" > "Selected commit")
  4. After that's done (and any merge conflicts resolved, if any), you'll need to re-apply these changes - either manually, or by cherry-picking those in:
    image

@conorgee conorgee changed the title Avoid excessive memory consumption by TrackBar control Avoid laggy and slow TrackBar control Nov 10, 2022
@elachlan
Copy link
Contributor

What if you set the frequency prior to the min/max values via PInvoke? Did you try that? Does it improve the memory usage?

@conorgee
Copy link
Contributor Author

Changing the the frequency before changing max or min has no effect on the memory consumption , The memory just shoots up as soon as you excute PInvoke.

image

@conorgee conorgee force-pushed the semester_of_code branch 2 times, most recently from 8425ccd to f47e12d Compare November 17, 2022 11:45
Copy link
Member

@lonitra lonitra left a comment

Choose a reason for hiding this comment

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

LGTM, thank you so much for all your effort 🥳

@lonitra lonitra added the ready-to-merge PRs that are ready to merge but worth notifying the internal team. label Dec 20, 2022
@conorgee
Copy link
Contributor Author

No problem at all, thank you for the opportunity to work on this. It has been a truly remarkable learning experience for me and everyone has been very helpful . At the beginining the issue looked alot simpler then it turned out to be but luckily with the help of @RussKie and @elachlan we could figure this one out. Hats off to the WinForms team. This has been an extraordinary experience for me, thank you.

@elachlan
Copy link
Contributor

Thanks for persisting with this.

@lonitra lonitra merged commit 3634f49 into dotnet:main Dec 27, 2022
@ghost ghost added this to the 8.0 Preview1 milestone Dec 27, 2022
@lonitra
Copy link
Member

lonitra commented Dec 27, 2022

@merriemcgaw my assumption is that this fix will need to be documented somewhere so that users are aware of the switch?

@conorgee conorgee deleted the semester_of_code branch December 29, 2022 22:51
@dreddy-work
Copy link
Member

@merriemcgaw my assumption is that this fix will need to be documented somewhere so that users are aware of the switch?

This should go with other switches we are introducing/introduced. #8442

@ghost ghost locked as resolved and limited conversation to collaborators Feb 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-merge PRs that are ready to merge but worth notifying the internal team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting the Maximum value on a Trackbar to a very large number results in excessive memory usage

7 participants