2

JSS site is running in integrated mode on Sitecore 9.1

I have problem with the SSR node process, it's hanging around 18 min on load tests and results in error 500 for site.

For blind solution I turned cache for GraphQL queries on SSR side, which is not a good option but good for start. Only change was node hangs around 20 min of stress tests.

My findings:

  • When I turned off GraphQL, there was is no problem.
  • In Sitecore logs, GraphQL queries executes fast, since they are cached. Only from time to time they take 1s, mostly it's 50 - 200 ms.

So I decided to profile this piece, but:

  1. How can I turn on profiling on the node process for Sitecore SSR? Normally it's is allowed by start with --prof parameter i.e. node --prof server.js
  2. Any other ideas how to profile node SSR?

Best Regards


Update

Solution provided by @Adam Weber works like charm. Thanks, you saved me a lot of time :)

I have tuned your code a bit to make arguments configurable by NODE_ENV_ARGUMENTS variable:

class CustomHttpNodeInstance : HttpNodeInstance
{
    public CustomHttpNodeInstance(NodeServicesOptions options, int port = 0) : base(options, port)
    {
    }

    protected override ProcessStartInfo PrepareNodeProcessStartInfo(string entryPointFilename, string projectPath, string commandLineArguments,
        IDictionary<string, string> environmentVars, bool launchWithDebugging, int debuggingPort)
    {
        var startInfo = base.PrepareNodeProcessStartInfo(entryPointFilename, projectPath, commandLineArguments,
            environmentVars, launchWithDebugging, debuggingPort);

        Log.Debug(GetType().Name + " environmentVars: " + string.Join("; ", environmentVars.Select(kv => $"{kv.Key}={kv.Value}")), this);

        var arguments = string.Empty;
        if (environmentVars.TryGetValue("NODE_ENV_ARGUMENTS", out arguments) && !string.IsNullOrEmpty(arguments))
        {
            startInfo.Arguments = arguments + " " + startInfo.Arguments;
        }

        return startInfo;
    }
}

Patch file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <javaScriptServices>
      <renderEngines>
        <renderEngine name="nodejs" >
          <instance>
            <EnvironmentVariables>
              <var name="NODE_ENV_ARGUMENTS" value="--prof" />
            </EnvironmentVariables>
          </instance>
        </renderEngine>
      </renderEngines>
    </javaScriptServices>
  </sitecore>
</configuration>
5

I have some code to offer to start the node process(es) with custom arguments, e.g. --prof. However, I'd first like to highlight the following bits of information about GraphQL and JSS that might provide some insight.

Sitecore GraphQL includes some built-in diagnostics features that can help diagnose API performance issues: https://jss.sitecore.com/docs/techniques/graphql/graphql-overview#performance--diagnostics

When to use Integrated GraphQL queries: https://jss.sitecore.com/docs/fundamentals/services/graphql#integrated-graphql

vs When to use Connected GraphQL queries: https://jss.sitecore.com/docs/fundamentals/services/graphql#connected-graphql

For non-CM environments, you may want to consider using Headless SSR mode instead of Integrated Mode for high-traffic sites as recommended here: https://jss.sitecore.com/docs/fundamentals/application-modes#integrated-mode


Ok, with that out of the way, you need to extend some of the existing JSS Node invocation code. Admittedly, this is more difficult than it should be and perhaps is something we can look at addressing for the next release.

First, extend the Sitecore.JavaScriptServices.ViewEngine.NodeServices.HostingModels.Http.HttpNodeInstance type where you can override the method that creates the Node invocation information. This is where you can add the --prof argument.

public class CustomHttpNodeInstance : HttpNodeInstance
{
    public CustomHttpNodeInstance(NodeServicesOptions options, int port = 0) : base(options, port)
    {  
    }

    protected override ProcessStartInfo PrepareNodeProcessStartInfo(string entryPointFilename, string projectPath, string commandLineArguments,
        IDictionary<string, string> environmentVars, bool launchWithDebugging, int debuggingPort)
    {
        var startInfo = base.PrepareNodeProcessStartInfo(entryPointFilename, projectPath, commandLineArguments,
            environmentVars, launchWithDebugging, debuggingPort);

        // You might ask why you can't simply add custom arguments to the `commandLineArguments` parameter.
        // The answer is that the `commandLineArguments` value is added to the node.exe command _after_ the script name/path that will be executed by node.
        // However, the `--prof` flag must be placed _before_ the script name/path that will be executed by node.
        // Therefore, we invoke the base `PrepareNodeProcessStartInfo` method to generate the command arguments,
        // then prepend the arguments string with `--prof`.
        startInfo.Arguments = "--prof " + startInfo.Arguments;
        return startInfo;
    }
}

Next, create a custom class that implements the Sitecore.JavaScriptServices.ViewEngine.NodeServices.INodeServicesFactory interface. This is where you tell the Node services creation code to use your custom HttpNodeInstance (defined above).

public class CustomNodeServicesFactory : INodeServicesFactory
{
    public INodeServices CreateNodeServices(NodeServicesOptions options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        // Use our `CustomHttpNodeInstance` when creating Node instances.
        options.NodeInstanceFactory = () => new CustomHttpNodeInstance(options);
        return new DefaultNodeServices(options.NodeInstanceFactory);
    }
}

Next, you need to register your custom CustomNodeServicesFactory as the implementation of INodeServicesFactory that will be served by the DI container.

public class CustomRegisterDependencies : IServicesConfigurator
{
    public void Configure(IServiceCollection serviceCollection)
    {
        // Remove the existing `INodeServicesFactory` service
        var toRemove = serviceCollection.FirstOrDefault(d => d.ServiceType == typeof(INodeServicesFactory));
        serviceCollection.Remove(toRemove);

        // Add the custom service
        serviceCollection.AddTransient<INodeServicesFactory, CustomNodeServicesFactory>();
    }
}

And lastly, you need a config patch to tell Sitecore to include your custom DI registration (be sure to replace MyNamespace and MyLibrary with your relevant values).

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <services>
      <configurator
        type="MyNamespace.CustomRegisterDependencies, MyLibrary"
        patch:after="*[@type='Sitecore.JavaScriptServices.ViewEngine.Node.RegisterDependencies, Sitecore.JavaScriptServices.ViewEngine.Node']" />
    </services>
  </sitecore>
</configuration>

Now, when you browse to your JSS app in Integrated Mode, Node will generate a profiler log file. However, it will be empty/locked until the Node process(es) used by your JSS app exit. Therefore, you must kill the w3wp.exe process for your Sitecore instance or kill the node.exe process(es) attached to the w3wp.exe process in order for the profiler output to be finalized and written.

The profiler output file, e.g. isolate-xxxxx-v8.log, will be generated in the folder next to your server.bundle.js file.

Note: depending on your JSS Node pooling configuration, multiple Node instances may be used by your app when load testing. Therefore, there may be multiple profile log files.

  • Thanks for answer definitely I will try this. And yes, I have turned on performance diagnostic on Sitecore side, that's why I know there is no issue there. Strange i have not found information about commandLineArguments in documentation. How syntax looks like? – Jerzy Pikutin Jan 27 at 8:03
  • And it works :) – Jerzy Pikutin Jan 27 at 15:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.