Announcement

Collapse
No announcement yet.
X
  • Filter
  • Time
Clear All
new posts

    js parsing performance

    SNAPSHOT_v13.1d_2023-11-01/Enterprise Deployment

    Chrome on MacOS

    Hello, when loading my largest (as number of screens) application, the browser takes a bit too much time in parsing the JavaScript code.
    Before I start adopting a different loading strategy from the current one (where I have a single assembly), I was trying to understand where most of the time is spent.
    I should note that I'm not an expert in this field.
    However, it seems that screens composed of SplitPanes are among those that take the most time. In particular, it appears that the initWidget of my SplitPane takes an average of 60ms.

    By running a test with the following code in the showcase:
    Code:
    isc.defineClass("MySplitPane", "SplitPane").addProperties({
        autoDraw: false,
        initWidget: function () {
            var startTime = window.performance.now();
            this.Super("initWidget", arguments);
            var duration = window.performance.now() - startTime;
            totalDuration += duration;
            isc.logEcho("SplitPane initWidget execution time: " + duration + " - total: " + totalDuration)
        }
    })
    
    var numIter = 30;
    var timeout = 100;
    for (var index = 1; index <= numIter; index++) {
        isc.Timer.setTimeout("isc.MySplitPane.create()", timeout * index * 3)
    }
    I notice a significant variance in execution times. Is this normal?
    The same code in my application is significantly slower, although not as slow as the actual code being parsed during loading.

    DynamicForms also seem to take a considerable amount of time to load.
    With the following code:
    Code:
    var totalDuration = 0;
    isc.defineClass("MyDynamicForm", "DynamicForm").addProperties({
        autoDraw: false,
        numItems: 1,
        initWidget: function () {
            var items = [];
            for (var index = this.numItems - 1; index >= 0; index--) {
                items.add({name: "field" + index, type: "float"})
            }
            var startTime = window.performance.now();
            this.Super("initWidget", arguments);
            this.setItems(items);
            var duration = window.performance.now() - startTime;
            totalDuration += duration;
            isc.logEcho(this.numItems + " items - DynamicForm initWidget execution time: " + duration + " - total: " + totalDuration)
        }
    })
    
    var numIter = 30;
    var timeout = 100;
    for (var index = 1; index <= numIter; index++) {
        isc.Timer.setTimeout("isc.MyDynamicForm.create({numItems: " + index + "})", timeout * index * 3)
    }
    I've observed the same significant variance as I see for SplitPane, and I don't see a correlation with the number of fields.
    This code also performs more slowly in my application compared to the showcase.
    I don't know if this could mean that there's some problem in my code.

    While I'm still trying to clarify my thoughts, do you have any advice?
    Am I perhaps wasting my time, and would it be better to split the JavaScript loading so that it's loaded only when needed?

    Edit: I just noticed that if I run those snippets in one of my smallest applications, they are even faster than in the showcase. Is it possible that the larger number of screens have this effect on performance?
    Last edited by claudiobosticco; 7 Nov 2023, 09:05.

    #2
    We're seeing far lower numbers, which may be because debug tools are enabled. Still, some small optimizations are probably still possible.

    However, it sounds like you have a very simple & straightforward major optimization you can do - it sounds like you are create()ing, and perhaps even drawing, all of your screens when the application first loads. There's no need to do that - you can just create/draw the screens when they are accessed. More detail here:

    https://smartclient.com/smartclient-...rtArchitecture

    Note that the JS parser per se is extremely fast. The core SmartClient runtime consists of megabytes of JavaScript source code, and the entire thing is parsed, with our classes and everything set up, in about 100ms. That's on an older laptop.

    Comment


      #3
      Thanks for your reply. Actually I create (almost) all screens (but not drawing).
      Before I didn't notice a big delay, but now this bigger application is actually a merge of other applications.
      I thought only of possibly segmenting (using the FileLoader) it, but you're right that I can also avoid the creation upfront. Thanks for pointing that out!

      Comment


        #4
        No problem.

        Note that we actually strongly recommend against "segmenting" in the sense of loading the JavaScript source code for screens only when they are accessed. This isn't worth doing because you can deliver dozens of screens in a single, compressed, small, cacheable JavaScript file. So long as you do not create() or draw() the components until they are needed, the parsing is nearly instantaneous, and the memory usage is very light (all that is created is a few JavaScript Function objects).

        When people start trying to load the JavaScript code for individual screens on demand, they end up with a bunch of complexity (synchronous operations become asynchronous, inter-screen dependencies like helper methods and helper classes need to be more intensively managed, etc) and very often end up actually degrading performance (there are more network requests for smaller files, reducing the effectiveness of compression, and caching / CDN / edge network delivery is often not used, plus end users are waiting for a network turnaround in a place where it could be handled instantaneously if the code were loaded in advance).

        So, definitely defer the create() calls and of course the draw() calls, but don't start down the path of trying to load the actual JavaScript code on demand. There's nothing good in that direction.

        Comment


          #5
          Hello, thanks for the clarification. I can confirm a huge improvement with just defering the create() calls. Also in memory consumption of the Chrome tab.

          Do you think it's worth postponing defineClasses too or not?

          Comment


            #6
            Probably not, unless you have hundreds of classes, or if it's trivially easy to defer the definitions.

            If you attempt to defer class definitions, watch out for things like other code calling class methods, or calling isc.isA.SomeClass().

            Comment


              #7
              After achieving great improvement with the deferred creation of objects, I am now wondering if delaying the loading of the data sources, which currently seem to have the most significant impact on performance, would be something worth trying or you think it isn't.

              Currently I use the pattern described in the doc for jsp environments, so I have (more than one) jsps to include them:

              Code:
              <%@taglib uri="/WEB-INF/iscTaglib.xml" prefix="isomorphic" %>
              <%@taglib uri="/WEB-INF/fmt.tld" prefix="fmt" %>
              
              <script type="text/javascript" charset="UTF-8">
              <isomorphic:loadDS ID="myDS"/>
              ...
              </script>

              Comment


                #8
                Probably not worth it, but with some caveats:

                1) if you have a very clear boundary in your app, such as a bunch of DataSources that are only needed for an application module that most users never visit (an admin area, for example), then it could be worth deferring that

                2) if you are not using Declarative Security at all, then DataSource definitions are actually always the same for every user, so you can just capture them as a plain .js file and load that.
                This removes the server-side processing part but has no impact on the client-side part.

                However, note also that if you looking at the server-side processing costs, make sure you load the app at least 3 times and look at the load time on the 3rd and later loads. There is a lot of caching/priming /JIT stuff that happens on the first request, and in some environments, things are as much as 10x faster on subsequent loads than on the first load.

                Note also that even if you are using Declarative Security, such that DataSource definitions differ for different users with different roles, you can of course pre-generate and cache all combinations of roles as JS files and load those. However, these kinds of techniques are for 100k+ concurrent user loads, or trying to eke out the last few percent of performance from dated hardware.

                3) if you do pre-generation as in #2 above, you can edit the DataSources file (which is now just a plain JS file) so that you surround certain isc.DataSources.create() calls in functions, which you then call when you need them. This allows you to defer creation of the DataSources while still loading all the DataSources in a single file up front. This results in better compression, and can be less complicated because the call to load DataSources is now synchronous rather than an asynchronous network call.

                4) finally, we're assuming you're using the approach suggested in the QuickStart Guide, of having a plain HTML login page (no SmartClient) which loads SmartClient & application resources in the background while the user is going through the login process. There is a further technique, which is to also go through all the component instantiation and even rendering while hidden. Then when login completes you just call show() and the UI instantaneously appears.

                This is so fast it's almost disorienting - the end-user experience is that they click the login button and the app is just suddenly there. However, it can only be applied if the first screen shown doesn't involve resources that require authentication or are user-role-specific, and only if you don't mind some information leakage (that is, the browser is loading the code for the application before the user is logged in).

                Comment


                  #9
                  Thanks for the throughout reply, for me there's definitely food for thought.
                  Regarding point 4, we are currently authenticating using microsoftonline login, so I don't think it's feasible.

                  Regarding the fact that I'm using this pattern:
                  Code:
                  <%@taglib uri="/WEB-INF/iscTaglib.xml" prefix="isomorphic" %>
                  <%@taglib uri="/WEB-INF/fmt.tld" prefix="fmt" %>
                  
                  <script type="text/javascript" charset="UTF-8">
                  <isomorphic:loadDS ID="myDS"/>
                  ...
                  </script>
                  I've just tried to use the DataSourceLoader, instead, and it seems way faster. Does this make sense? I wasn't expecting it.

                  Comment


                    #10
                    We would not expect the JSP tag to be slower than a call to DataSourceLoader servlet, as the two codepaths are 95% identical. What you may be seeing is that the HTML of the page is returned more quickly to the browser, since the processing has shifted from JSP execution to a separate servlet call.

                    However, in terms of how quickly the page actually appears (as in, rendered widgets) we would expect that to be basically identical, since none of your JavaScript code to create widgets and such will start to execute until after the DataSourceLoader call completes.

                    Are you actually seeing a faster first render? That could possibly indicate some other core servlets or JVM problem, like not having enough threads.

                    Comment


                      #11
                      Hello, I'm actually seeing a faster first render.

                      I'm trying to measure the time needed to load DataSources using the loadDS tag, I hope this makes sense, my compilated JSP become something like:

                      Code:
                      long start = System.currentTimeMillis();
                      
                            out.write('\n');
                            out.write('\n');
                            out.write('\n');
                            if (_jspx_meth_isomorphic_005floadDS_005f0(_jspx_page_context))
                              return;
                            out.write('\n');
                      
                          long end = System.currentTimeMillis();
                          System.out.println("loadDS : " + (end - start));
                          start = System.currentTimeMillis();
                      
                            out.write('\n');
                            if (_jspx_meth_isomorphic_005floadDS_005f1(_jspx_page_context))
                              return;
                            out.write('\n');
                      
                          end = System.currentTimeMillis();
                          System.out.println("loadDS : " + (end - start));
                          start = System.currentTimeMillis();
                      
                            out.write("\n");
                      where the methods like _jspx_meth_isomorphic_005floadDS_005f0 are like this:

                      Code:
                      private boolean _jspx_meth_isomorphic_005floadDS_005f0(javax.servlet.jsp.PageContext _jspx_page_context)
                                throws java.lang.Throwable {
                          javax.servlet.jsp.PageContext pageContext = _jspx_page_context;
                          javax.servlet.jsp.JspWriter out = _jspx_page_context.getOut();
                          // isomorphic:loadDS
                          com.isomorphic.taglib.LoadDSTag _jspx_th_isomorphic_005floadDS_005f0 = (com.isomorphic.taglib.LoadDSTag) _005fjspx_005ftagPool_005fisomorphic_005floadDS_0026_005flocale_005fID_005fnobody.get(com.isomorphic.taglib.LoadDSTag.class);
                          boolean _jspx_th_isomorphic_005floadDS_005f0_reused = false;
                          try {
                            _jspx_th_isomorphic_005floadDS_005f0.setPageContext(_jspx_page_context);
                            _jspx_th_isomorphic_005floadDS_005f0.setParent(null);
                            // /index.jsp(1282,0) name = ID type = null reqTime = true required = false fragment = false deferredValue = false expectedTypeName = null deferredMethod = false methodSignature = null
                            _jspx_th_isomorphic_005floadDS_005f0.setID("JASV_BI_xxxx_yyyyyyy");
                            // /index.jsp(1282,0) name = locale type = null reqTime = true required = false fragment = false deferredValue = false expectedTypeName = null deferredMethod = false methodSignature = null
                            _jspx_th_isomorphic_005floadDS_005f0.setLocale((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${param.locale}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null));
                            int _jspx_eval_isomorphic_005floadDS_005f0 = _jspx_th_isomorphic_005floadDS_005f0.doStartTag();
                            if (_jspx_th_isomorphic_005floadDS_005f0.doEndTag() == javax.servlet.jsp.tagext.Tag.SKIP_PAGE) {
                              return true;
                            }
                            _005fjspx_005ftagPool_005fisomorphic_005floadDS_0026_005flocale_005fID_005fnobody.reuse(_jspx_th_isomorphic_005floadDS_005f0);
                            _jspx_th_isomorphic_005floadDS_005f0_reused = true;
                          } finally {
                            org.apache.jasper.runtime.JspRuntimeLibrary.releaseTag(_jspx_th_isomorphic_005floadDS_005f0, _jsp_getInstanceManager(), _jspx_th_isomorphic_005floadDS_005f0_reused);
                          }
                          return false;
                        }
                      and what I see is times in the order of 40+ milliseconds for the bigger dataSources (more than a hundred fields), and 2-4 milliseconds with dataSources small as:

                      Code:
                      <DataSource xmlns:fmt="WEB-INF/" xmlns="http://www.smartclient.com/schema"
                                  schema="mySchema"
                                  dbName="myDB"
                                  tableName="JASV_BI_xxxx_yyyyyyy"
                                  ID="JASV_BI_xxxx_yyyyyyy"
                                  dataSourceVersion="1"
                                  serverType="sql"
                                  requiresAuthentication="true"
                                  dropExtraFields="false"
                      >
                          <fmt:bundle basename="i18nMessages" encoding="utf-8"/>
                          <fields>
                              <field name="ID_SQUADRA" type="integer" hidden="true"/>
                              <field name="NOME" type="text"/>
                              <field name="NOME_INTERNAZIONALE" type="text"/>
                              <field name="CATEGORIA" type="text"/>
                          </fields>
                      </DataSource>
                      which seems to much considering that with DataSourceLoader I see 1ms to load *all* dataSources.

                      Does this way of measuring makes sense? Is there something else I can investigate?
                      Last edited by claudiobosticco; 22 Dec 2023, 13:31.

                      Comment


                        #12
                        This sounds as though you are measuring the DataSourceLoader by looking at the amount of time that the JSP takes to write out the <script> tag. You would need to instead look at the network round-trip time for the DataSourceLoader URL using the browser's built-in Developer Tools.

                        Also, 40ms for a large DataSource is abnormal. As previously mentioned, you should make sure you are measuring the 3rd - 5th attempt to load DataSources in order to allow various kinds of caching and pooling to occur. You should also:

                        1) not run under a debugger when testing performance

                        2) make sure you do not have excessive logging turned on

                        3) make sure you haven't done something like disabled DataSource pooling entirely (you should see log messages related to DataSources being borrowed from pools, on the second and later load attempts)

                        Comment


                          #13
                          Thanks for the heads up, using the browser dev tools I see a similar round-trip time for the DataSourceLoader.

                          I confirm that I'm not using a debugger, and that I'm measuring after many page refresh. I'll check on the DataSource pooling, but actually the application is always the same, I'm just switching the two types of loading in my index.jsp.

                          I see a reload half a second faster with the DataSourceLoader. I'm testing with more than 500 DataSources.

                          Comment


                            #14
                            We're not sure how you're measuring the difference - seems like it would have to be with a stopwatch, where it's tough to measure a half-second difference - but there is a possible reason for it, since the number of DataSources being loaded is huge.

                            Basically, with the DataSources generated into the JSP, it's likely your browser doesn't see the first byte of the HTML file until the JSP is completely finished executing on the server.

                            When instead DataSources are loaded via a <script> tag, the JSP processing finishes quite quickly, and then the browser will load the SmartClient runtime (from cache) and execute it before execution is then blocked by waiting for the DataSourceLoader response.

                            Technically, this shouldn't be faster, because you've just reordered the phases, and in the HTML spec, the browser must load and execute a <script> block before looking at any DOM past that script block, since the script block might modify that DOM.

                            In practice, modern browsers do speculative execution much like modern CPUs. So if you were to compare timestamps, you would probably see that the browser speculatively kicked off the request to the DataSourceLoader while SmartClient was initializing. That effectively parallelizes SmartClient resource loading and initialization, as well as the loading and execution of anything else that appears before the DataSourceLoader <script> tag - that all happens concurrently with the long wait for the DataSourceLoader response. And there's your half-second.

                            Some points to cover again though:

                            1) your DataSourceLoader times are still abnormally high

                            2) you can remove the entire server-side processing of DataSourceLoader if you cache the generated DataSources (it's just JS logic). This does require that either your DataSources are the same for every type of user (that is, they don't vary by role or tenantId) or that you pre-generate and cache all variants by role

                            3) with 500 DataSources, there may be some natural ways to split up the loaded DataSources into sets so that some that aren't often used are only loaded on demand. But again, we would only look into this if your application has some clearly-defined "modules", because maintaining dynamic loading across a complex dependency tree usually isn't worth the hassle.

                            Comment


                              #15
                              about DataSource pooling: isn't it enabled by default?
                              I've tried to add in server.properties file:
                              datasources.pool.enabled = true
                              but I don't see a difference. There's something else which may disable pooling? How can I verify if it's actually enabled and working?

                              Comment

                              Working...
                              X