Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Avoid memory allocation and improve performance, part 1 #522

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

Closed

Conversation

DaniilSokolyuk
Copy link
Contributor

@DaniilSokolyuk DaniilSokolyuk commented Mar 24, 2018

Hi @Daniel15, for more than a year we have been using this library, and recently noticed problems with allocations and object hits in Large Object Heap if the component is large or a large props is serialized

Goals

  1. Native implementation for IHtmlString without recopy and allocating new strings
  2. Reuse everything
  3. ComponentExistsChecks configuration property (default true), for disable "EnsureComponentExists" (avoid extra engine call, in production for example)

Branch with banchmarks here https://github.com/DaniilSokolyuk/React.NET/tree/bench
Note the difference between the nano seconds and microseconds

Before, Small objects

Method Mean Error StdDev Gen 0 Gen 1 Allocated
CreateComponent 12.97 us 0.0954 us 0.0845 us 1.8463 0.5035 10.9 KB
React 17.19 us 0.2015 us 0.1787 us 7.8430 1.1597 43.81 KB
ReactInitJavaScript 75.81 us 0.5654 us 0.5012 us 146.9727 - 602.56 KB

After, Small objects

Method Mean Error StdDev Gen 0 Gen 1 Allocated
CreateComponent 541.4 ns 10.55 ns 10.83 ns 0.0496 0.0143 312 B
React 12,973.0 ns 119.90 ns 100.12 ns 1.8005 0.4883 11320 B
ReactInitJavaScript 11,401.2 ns 72.78 ns 64.52 ns 0.1831 - 792 B

Before, Large objects

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
CreateComponent 35.76 us 0.2885 us 0.2558 us 5.6763 1.5259 - 34.8 KB
React 48.16 us 0.4108 us 0.3843 us 24.7803 3.5400 - 139.08 KB
ReactInitJavaScript 417.16 us 4.9953 us 4.6726 us 356.4453 142.5781 71.2891 1980.55 KB

After, Large objects

Method Mean Error StdDev Gen 0 Gen 1 Allocated
CreateComponent 551.0 ns 10.95 ns 9.710 ns 0.0496 0.0143 312 B
React 36,337.2 ns 130.36 ns 121.939 ns 5.0049 1.3428 31585 B
ReactInitJavaScript 51,691.7 ns 766.71 ns 717.185 ns 24.8413 0.5493 104398 B

@dustinsoftware
Copy link
Member

Thanks for starting this conversation. It looks like there are a few breaking changes (like making component disposable), which could be a problem. This PR is also quite large, it would be better to split up optimization’s into smaller PRs so we can discuss each change :)

Otherwise as is it’s going to be a bit before we can work through this one.

@DaniilSokolyuk
Copy link
Contributor Author

@dustinsoftware i think disposable component is the smallest problem :)
I do not think that there are so many changes here, backward compatibility is also completely left, I do not consider it necessary to split pr

@DaniilSokolyuk DaniilSokolyuk force-pushed the avoid-memory-allocation branch from 1cc32fb to 3b5e5c9 Compare March 24, 2018 16:58
@DaniilSokolyuk DaniilSokolyuk changed the title [WIP] Avoid memory allocation and improve performance Avoid memory allocation and improve performance Mar 24, 2018
@Daniel15
Copy link
Member

Daniel15 commented Mar 24, 2018

This is probably the largest pull request we've ever received :) Thanks for working on it.

There's a lot of changes that change #if NET40 to #if NET40 || NET451, is it possible to split those out into a separate pull request? As @dustinsoftware said, it's much easier to review multiple small PRs instead of one giant one.

@DaniilSokolyuk DaniilSokolyuk force-pushed the avoid-memory-allocation branch from bbffdde to 60d96bb Compare March 25, 2018 06:45
@DaniilSokolyuk DaniilSokolyuk changed the title Avoid memory allocation and improve performance Avoid memory allocation and improve performance, part 1 Mar 25, 2018
@DaniilSokolyuk
Copy link
Contributor Author

DaniilSokolyuk commented Mar 25, 2018

@Daniel15 Thanks you, splitted, after this PR I will create another PR

@DaniilSokolyuk
Copy link
Contributor Author

@Daniel15, @dustinsoftware what about this PR? I have already split this request and prepared the
second part
We are already using these changes and significantly reduced the hits in LOH and FullGC

@dustinsoftware
Copy link
Member

dustinsoftware commented Mar 28, 2018 via email

@DaniilSokolyuk
Copy link
Contributor Author

@dustinsoftware i am already split this PR, there are only 227 new lines here, and about 60% this lines is replace string.format to TextWriter.Write

@DaniilSokolyuk DaniilSokolyuk force-pushed the avoid-memory-allocation branch from 60d96bb to bdda3e6 Compare March 29, 2018 12:03
Copy link
Member

@dustinsoftware dustinsoftware left a comment

Choose a reason for hiding this comment

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

Thanks for submitting this PR! There are some good changes in here but we'll need to break this up into separate PR's even further before we can continue (if you don't have time to do that, let us know, and we can pick out some of these changes and test them individually when we have time).

From what I could tell, there are five separate improvements here:

  • Use new component ID generation instead of Guid.NewGuid
  • Using StringBuilder/TextWriter instead of string.Format to generate HTML
  • Cache component name validation
  • Optionally skip component exists checks
  • Update JSON.NET version

With each of these improvements it would be great to see how much these changes affected the benchmarks instead of testing all of them at once. It could be that some of these changes made a significant difference (such as using StringBuilder) and others not so much (such as changing how IDs are generated).

Let me know if you'd like some more clarification or help moving this PR forward!

if (Environment.Configuration.ScriptNonceProvider != null)
{
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
writer.Write(" nonce=\"");
writer.Write(Environment.Configuration.ScriptNonceProvider());
Copy link
Member

Choose a reason for hiding this comment

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

I see that we're manually constructing HTML tags in existing code.. as long as we're not passing user-generated data to these methods, this should be OK (although it would be nice to sanitize the data going into these attributes just in case)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -37,7 +37,7 @@
<PackageReference Include="JavaScriptEngineSwitcher.Msie" Version="2.4.9" />
<PackageReference Include="JSPool" Version="3.0.1" />
<PackageReference Include="MsieJavaScriptEngine" Version="2.2.2" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
Copy link
Member

Choose a reason for hiding this comment

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

Is bumping Json.NET required for this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, split PR fail, bumping JSON.NET need in part 2 for use ArrayPool, fixed

@@ -114,31 +101,61 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
/// <returns>HTML</returns>
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
public string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
Copy link
Member

Choose a reason for hiding this comment

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

Removing virtual is a breaking change, can this be avoided?

Copy link
Contributor Author

@DaniilSokolyuk DaniilSokolyuk Apr 7, 2018

Choose a reason for hiding this comment

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

we can return virtual but it does not make sense and can cause errors
because all methods and extensions uses overload with textwriter

{
var isValid = componentName.Split('.').All(segment => _identifierRegex.IsMatch(segment));
if (!isValid)
//TODO: cache
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove any TODOs before merging this in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
html = _environment.Execute<string>(reactRenderCommand);
stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look thread safe - multiple threads could call Write at the same time to this shared instance. Keeping StringWriter scoped to this method instead of using a static field would solve this.

Copy link
Contributor Author

@DaniilSokolyuk DaniilSokolyuk Apr 7, 2018

Choose a reason for hiding this comment

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

_sharedStringWriter is marked as ThreadStatic and this is unique for each thread. (corefx use similar strategy, https://github.com/dotnet/corefx/blob/master/src/Common/src/System/IO/StringBuilderCache.cs) , I am reuse evertything for reduce allocations.

Copy link
Member

Choose a reason for hiding this comment

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

Oh ok, did not know about ‘ThreadStatic’ :)

@DaniilSokolyuk
Copy link
Contributor Author

DaniilSokolyuk commented Apr 7, 2018

@dustinsoftware, yes you right, but previous ID generating is ugly, we are allocate GUID, after allocate byte array and 1-3 string... my implementation used 1 string,
removed Json.NET bump

{
EnsureComponentExists();
}

var html = string.Empty;
if (!renderContainerOnly)
{
var stringWriter = _sharedStringWriter;
Copy link
Member

Choose a reason for hiding this comment

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

This variable declaration is unnecessary - you can just use _sharedStringWriter here and below:

if (_sharedStringWriter == null)
  _sharedStringWriter = new StringWriter(new StringBuilder(512));

Copy link
Member

Choose a reason for hiding this comment

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

Also, the Write method on StringWriter just calls the wrapped StringBuilder, so it would be better to just use StringBuilder.

Copy link
Contributor Author

@DaniilSokolyuk DaniilSokolyuk Apr 8, 2018

Choose a reason for hiding this comment

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

  1. This variable is not unnecessary, because access to ThreadStatic variable is slow http://tips.x-tensive.com/2008/10/cost-of-threadstatic-attribute.html
  2. StringBuilder is not implement TextWriter, we cant use StringBuilder

{
_sharedStringWriter =
stringWriter =
new StringWriter(new StringBuilder(512));
Copy link
Member

Choose a reason for hiding this comment

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

This number seems unnecessarily large and might allocate more space than necessary for simple components, let's leave it unset unless there is a noticeable benefit to specifying a large value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In part 2 i will remove all the StringWriter, StringBuilders and threadStatic and replace them with my own buffer based on ArrayPool, https://github.com/DaniilSokolyuk/React.NET/blob/c271afeb369875194a31c18bbbe6b76fac947d51/src/React.Core/ReactComponent.cs#L150

@dustinsoftware
Copy link
Member

previous ID generating is ugly

There may be consuming code that depends on the current ID format, so this isn't a good reason to change it. We unfortunately don't know how consumers are using this project and want to avoid surprises between version updates.

@Daniel15
Copy link
Member

Daniel15 commented Apr 8, 2018

Thanks for reviewing this, @dustinsoftware!

@DaniilSokolyuk
Copy link
Contributor Author

DaniilSokolyuk commented Apr 8, 2018

@dustinsoftware , we can announce this change or publish major version

@dustinsoftware
Copy link
Member

That’s not the point I was trying to make... I am hesitant to merge in changes for minor improvements that are technically breaking changes.

The discussion on this PR is getting too long, please open a new one for each individual change (such as ones the bulleted list above) and we can discuss them one at a time :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants