Announcement

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

    setVisibleWhen() performance problem

    Hi,

    I'm trying to use setVisibleWhen() functionality instead of other workarounds in my app. But I've come to massive performance problems.

    In my test case I've got a DataSource with around 20 fields and most of them have conditions setVisibleWhen() or setReadOnlyWhen().
    Now with ListGrid and connected DynamicForm switching between records can lead to delays even to couple of seconds. Without these conditions it works fine.

    I've been trying to isolate a problem to simple test case. First problem that I've encountered is multiple rule checking - test with logging set to INFO on ruleEngine and whenRules.
    Here's my test case:
    Code:
            DataSource ds = new DataSource();
            ds.setClientOnly(true);
    
            DataSourceIntegerField fieldId = new DataSourceIntegerField("id");
            fieldId.setPrimaryKey(true);
            ds.addField(fieldId);
            ds.addField(new DataSourceTextField("field1"));
            ds.addField(new DataSourceTextField("field2"));
    
            DataSourceTextField fieldStatus = new DataSourceTextField("status");
            fieldStatus.setValueMap("new", "progress", "finished", "canceled");
            ds.addField(fieldStatus);
    
            Record r1 = new Record();
            r1.setAttribute("id", 1);
            r1.setAttribute("field1", "Field 1");
            r1.setAttribute("status", "new");
    
            Record r2 = new Record();
            r2.setAttribute("id", 2);
            r2.setAttribute("field1", "Field 2");
            r2.setAttribute("status", "finished");
    
            ds.setTestData(r1, r2);
    
            HLayout layout = new HLayout(8);
    
            ListGrid listGrid = new ListGrid();
            listGrid.setDataSource(ds);
            listGrid.setWidth(300);
            listGrid.setHeight(300);
    
            listGrid.fetchData();
    
            layout.addMember(listGrid);
    
            DynamicForm form = new DynamicForm();
            form.setDataSource(ds);
    
            TextItem itemField1 = new TextItem("field1");
    // itemField1.setReadOnlyWhen(new AdvancedCriteria("status", OperatorId.IN_SET, new String[] {"finished", "canceled"}));
            itemField1.setVisibleWhen(new AdvancedCriteria("status", OperatorId.IN_SET, new String[] {"finished", "canceled"}));
    
            ButtonItem itemAdd = new ButtonItem("New");
            itemAdd.addClickHandler(event -> {
                form.editNewRecord();
            });
    
            form.setFields(
                    new TextItem("id"),
                    itemField1,
                    new TextItem("field2"),
                    new SelectItem("status"),
                    new SubmitItem(), itemAdd
            );
    
            layout.addMember(form);
    
            listGrid.addRecordClickHandler(event -> {
                form.editRecord(event.getRecord());
            });
    
            this.addChild(layout);
    In above example on start we've got these messages:
    Code:
    20:53:09.797:TMR7:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.798:TMR7:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.808:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.808:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.809:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.809:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.810:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.810:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.810:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.810:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.811:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.811:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:09.812:TMR8:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:09.812:TMR8:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    With just one click on record we've got another messages;
    Code:
    20:53:58.843:MDN2:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result null
    20:53:58.843:MDN2:INFO:whenRules:Hide FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.917:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.917:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.918:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.918:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.919:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.919:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.920:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.920:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.921:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.921:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.922:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.922:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.923:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.923:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.924:MUP5:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.924:MUP5:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.931:TMR7:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.932:TMR7:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.932:TMR7:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.932:TMR7:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.933:TMR7:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.933:TMR7:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    20:53:58.934:TMR7:INFO:whenRules:isc_RulesEngine_0:Applying visibility rule 'isc_DynamicForm_1_field1_visibility' for FormItem isc_DynamicForm_1.field1 with action result true
    20:53:58.934:TMR7:INFO:whenRules:Show FormItem 'isc_DynamicForm_1.isc_TextItem_4'
    It looks like there are multiple checks of this condition. In my base case there are around 100+ messages and chrome console can hang on this.
    I'm assuming it's not desired??
    Tested also on latest nightly from 2023-06-29.

    Best regards
    Mariusz Goch

    PS:
    As you can see in this example I'm using DataSourceTextField instead of DataSourceEnumField. It's because warning previously reported:
    https://forums.smartclient.com/forum...ation-warnings

    But switching do DataSourceEnumField it makes things even worse - probably due to this warnings.
    Attached Files

    #2
    A simple sample like this is not at all slow for us - it's likely only slow for you because of the combination of:

    1) enabling logs that are normally off: these logs are expensive to produce

    2) having development tools enabled: you may have browser development tools open, possibly in addition to using GWT "hosted mode"?

    If you can show a sample that runs slowly for you with default log settings and in compiled mode, we'd like to take a look.

    Comment


      #3
      This test case it's not slow at all. That's right. I've started dividing whole problem to small pieces and found out this multiple rule checking for only one condition and one field.

      So the my question for now is this correct that for this test case when I select record on ListGrid 13 times rules are checked when form.editRecord() is called??

      Case that is really slow contains around 20 fields with almost all of them using conditions. So if each condition is checked 13 times in normal case scenario there will be 260 checks for every record change. That's my first direction, so I want to clarify above.

      Performance issues have been reported on compiled version ready for production without any logging enabled or any other development tools, just plain Chrome.

      If this multiple checks are in fact correct and not a bug I will try to prepare minimal test case with performance issues.

      Best regards
      Mariusz Goch

      Comment


        #4
        You can get multiple seemingly redundant checks base on the order of your calls, and this can be worse in SmartGWT vs SmartClient, because SmartGWT puts a procedural facade over a superior declarative system, and that necessarily causes extra work.

        Bigger picture though, we would not want to dive into a test case that is performing fine and and spend a bunch of effort looking into and reducing redundant evaluations, only to find out this actually does nothing to the test case that really is slow, because in that case the problem is something completely different.. and perhaps our attempt to focus on redundant evaluations has made the real problem harder to solve!

        So please let us know if you can put together a test case that actually is slow, and we'll take a look.

        Comment


          #5
          Hi,
          I'm still working on minimal test case. Problem is it's not so minimal. So far I cannot figure out what event / widget causes this performance problem.

          Meanwhile could you take a look a this to profiles exported from Chrome Developer Console on Performance tab.
          These are just profiles when user clicks on new record. In both cases it's the same app and the same flow. But in larger one rules are enabled and on the other one they aren't.
          In one of them click is process in around 60 ms and in the other one it takes almost 2 seconds.

          What wonders me there are numerous DynamicForm_getValues calls with multiple NumberUtil_parseFloat and that take a lot of time.

          I will still work on minimal test case but for now it's just quite fast so it probably does not cover core problem.

          Please take a look at these profiles and maybe you could point me into the right direction.
          I've uploaded files on dropbox because profiles are larger than 2mb.

          https://www.dropbox.com/sh/i8646yi4a...9zeWNwmha?dl=0

          Best regards
          Mariusz Goch

          Comment


            #6
            We're not sure what tool makes these profiles easily browsable, and even with the right tool in hand, again, we're not really able to do deep analysis without a test case or at least a support contract.

            Given your description of what you're seeing a lot of:

            1) are as far NumberUtil_parseFloat(), perhaps you are in a non-English locale, or perhaps you have custom number formats enabled. Try switching to the English locale or removing custom number formats. If this fixes the problem, it will give you a clue as to how to put together a reproducible testcase that shows the slow down you're talking about

            2) you mention lots of calls to dynamicForm.getValues(). Aside from the built-in tools in browsers, you could use Log.traceLogMessage() to get a stack trace for every getValues() call, so you can see whether those are coming directly or indirectly from your application code

            Comment


              #7
              It's nothing fancy just plain Google Chrome with standard Dev Tools.
              It shows all traces on graph.

              I'm using Polish locale. I will try to disable all formatters and see if this is causing a problem.
              Attached Files

              Comment


                #8
                Hi,

                I've prepared test case that shows performance problems.

                Code:
                public void testRules() {
                        boolean whenRules = true;
                
                        DataSource ds = getRulesDataSource();
                
                        HLayout layout = new HLayout(8);
                
                        ListGrid listGrid = new ListGrid();
                        listGrid.setDataSource(ds);
                        listGrid.setWidth(300);
                        listGrid.setHeight(300);
                
                        listGrid.fetchData();
                
                        layout.addMember(listGrid);
                
                        ValuesManager vm = new ValuesManager();
                        vm.setDataSource(ds);
                
                        DynamicForm form1 = new DynamicForm();
                        BoxFormat.form(form1);
                        form1.setDataSource(ds);
                        form1.setValuesManager(vm);
                
                        ArrayList<FormItem> items1 = onForm1(whenRules);
                
                        if (whenRules) {
                            setReadOnly(items1);
                        }
                
                        form1.setFields(items1.toArray(new FormItem[items1.size()]));
                
                        layout.addMember(form1);
                
                        DynamicForm form2 = new DynamicForm();
                        BoxFormat.form(form2);
                        form2.setDataSource(ds);
                        form2.setValuesManager(vm);
                        form2.setNumCols(4);
                        form2.setColWidths("30%", "25%", "20%", "25%");
                
                        ArrayList<FormItem> items2 = onForm2(whenRules);
                        if (whenRules) {
                            setReadOnly(items2);
                        }
                
                        form2.setFields(items2.toArray(new FormItem[items2.size()]));
                
                        layout.addMember(form2);
                
                        listGrid.addRecordClickHandler(event -> {
                            long time = (new Date()).getTime();
                            vm.editRecord(event.getRecord());
                            long time2 = (new Date()).getTime();
                            console.log("editRecord time: ", time2 - time);
                        });
                
                        this.addChild(layout);
                    }
                
                    public DataSource getRulesDataSource() {
                        DataSource ds = new DataSource();
                        ds.setClientOnly(true);
                
                        DataSourceIntegerField fieldId = new DataSourceIntegerField("id");
                        fieldId.setPrimaryKey(true);
                        ds.addField(fieldId);
                
                        DataSourceTextField fieldType = new DataSourceTextField("type");
                        fieldType.setValueMap("ware", "service", "period");
                        ds.addField(fieldType);
                
                        DataSourceTextField fieldPeriod = new DataSourceTextField("period");
                        fieldPeriod.setValueMap("day", "week", "2week", "month", "2month");
                        ds.addField(fieldPeriod);
                
                        DataSourceTextField fieldStatus = new DataSourceTextField("status");
                        fieldStatus.setValueMap("new", "progress", "finished", "canceled");
                        ds.addField(fieldStatus);
                
                        DataSourceTextField fieldVat = new DataSourceTextField("vat");
                        ds.addField(fieldVat);
                
                        DataSourceFloatField fieldCost = new DataSourceFloatField("cost");
                        fieldCost.setFormat("0.00");
                        ds.addField(fieldCost);
                
                        DataSourceFloatField fieldPriceMargin = new DataSourceFloatField("price_margin");
                        fieldPriceMargin.setDecimalPad(2);
                        ds.addField(fieldPriceMargin);
                
                        DataSourceFloatField fieldPriceBaseNet = new DataSourceFloatField("price_base_net");
                        fieldPriceBaseNet.setFormat("0.00");
                        ds.addField(fieldPriceBaseNet);
                
                        DataSourceFloatField fieldPriceBase = new DataSourceFloatField("price_base");
                        fieldPriceBase.setFormat("0.00");
                        ds.addField(fieldPriceBase);
                
                        DataSourceFloatField fieldDiscount = new DataSourceFloatField("discount");
                        fieldDiscount.setDecimalPad(2);
                        ds.addField(fieldDiscount);
                
                        DataSourceFloatField fieldPriceNet = new DataSourceFloatField("price_net");
                        fieldPriceNet.setFormat("0.00");
                        fieldPriceNet.setRequired(true);
                        ds.addField(fieldPriceNet);
                
                        DataSourceFloatField fieldPrice = new DataSourceFloatField("price");
                        fieldPrice.setFormat("0.00");
                        fieldPrice.setRequired(true);
                        ds.addField(fieldPrice);
                
                        DataSourceFloatField fieldPriceProfit = new DataSourceFloatField("price_profit");
                        fieldPriceProfit.setFormat("0.00");
                        ds.addField(fieldPriceProfit);
                
                        Record r1 = new Record();
                        r1.setAttribute("id", 1);
                        r1.setAttribute("product_id", 1);
                        r1.setAttribute("type", "period");
                        r1.setAttribute("status", "new");
                        r1.setAttribute("quantity", "11");
                        r1.setAttribute("total_net", "11.12");
                        r1.setAttribute("total", "11.12");
                        r1.setAttribute("total_profit", "11.12");
                        r1.setAttribute("vat", "23");
                        r1.setAttribute("cost", "11.12");
                        r1.setAttribute("price_margin", "11.12");
                        r1.setAttribute("price_base_net", "11.12");
                        r1.setAttribute("price_base", "11.12");
                        r1.setAttribute("discount", "11.12");
                        r1.setAttribute("price_net", "11.12");
                        r1.setAttribute("price", "11.12");
                        r1.setAttribute("discount", "11.12");
                
                
                        Record r2 = new Record();
                        r2.setAttribute("id", 2);
                        r2.setAttribute("type", "ware");
                        r2.setAttribute("status", "progress");
                        r2.setAttribute("quantity", "11");
                        r2.setAttribute("total_net", "0.99");
                        r2.setAttribute("total", "0.99");
                        r2.setAttribute("total_profit", "0.99");
                        r2.setAttribute("vat", "23");
                        r2.setAttribute("cost", "0.99");
                        r2.setAttribute("price_margin", "0.99");
                        r2.setAttribute("price_base_net", "0.99");
                        r2.setAttribute("price_base", "0.99");
                        r2.setAttribute("discount", "0.99");
                        r2.setAttribute("price_net", "0.99");
                        r2.setAttribute("price", "0.99");
                        r2.setAttribute("discount", "0.99");
                
                        Record r3 = new Record();
                        r3.setAttribute("id", 3);
                        r3.setAttribute("product_id", 2);
                        r3.setAttribute("type", "period");
                        r3.setAttribute("status", "progress");
                
                        ds.setTestData(r1, r2, r3);
                
                        return ds;
                    }
                
                    public ArrayList<FormItem> onForm1(boolean whenRules) {
                        ArrayList<FormItem> items = new ArrayList<FormItem>();
                
                        items.add(new TextItem("id"));
                
                        AdvancedCriteria criteriaProduct = new AdvancedCriteria("product_id", OperatorId.NOT_NULL);
                
                        TextItem itemProductId = new TextItem("product_id");
                        if (whenRules) {
                            itemProductId.setVisibleWhen(criteriaProduct);
                        }
                        items.add(itemProductId);
                
                        TextItem itemName = new TextItem("name");
                        items.add(itemName);
                
                        TextItem itemCode = new TextItem("code");
                        items.add(itemCode);
                
                        RadioGroupItem itemType = new RadioGroupItem("type");
                        itemType.setDefaultValue("ware");
                        itemType.setVertical(false);
                        items.add(itemType);
                
                        SelectItem itemPeriod = new SelectItem("period");
                        itemPeriod.setDefaultValue("month");
                        if (whenRules) {
                            itemPeriod.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                        }
                        items.add(itemPeriod);
                
                        SpinnerItem itemPeriodQuantity = new SpinnerItem("period_quantity");
                        itemPeriodQuantity.setDefaultValue(1);
                        itemPeriodQuantity.setMin(1);
                        itemPeriodQuantity.setStep(1);
                        itemPeriodQuantity.setWriteStackedIcons(true);
                        if (whenRules) {
                            itemPeriodQuantity.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                        }
                        items.add(itemPeriodQuantity);
                
                        DateItem itemDateStart = new DateItem("date_start");
                        if (whenRules) {
                            itemDateStart.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                        }
                        itemDateStart.setDefaultValue(new Date());
                        items.add(itemDateStart);
                
                        DateItem itemDateEnd = new DateItem("date_end");
                        if (whenRules) {
                            itemDateEnd.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                            itemDateEnd.setReadOnlyWhen(new AdvancedCriteria("period", OperatorId.NOT_EQUAL, "other"));
                        }
                        items.add(itemDateEnd);
                
                        TextItem itemPKWiU = new TextItem("pkwiu");
                        itemPKWiU.setWidth(120);
                        items.add(itemPKWiU);
                
                        TextItem itemSerial = new TextItem("serial");
                        items.add(itemSerial);
                
                        items.add(new SelectItem("status"));
                
                        return items;
                    }
                
                    public ArrayList<FormItem> onForm2(boolean whenRules) {
                        ArrayList<FormItem> items = new ArrayList<FormItem>();
                
                        BooleanItem itemShowDiscount = new BooleanItem("showDiscount");
                        itemShowDiscount.setDefaultValue(false);
                        itemShowDiscount.setStartRow(false);
                        itemShowDiscount.setEndRow(true);
                        itemShowDiscount.setShowTitle(false);
                        itemShowDiscount.setColSpan(2);
                        itemShowDiscount.setAlign(Alignment.CENTER);
                
                        TextItem itemCost = new TextItem("cost");
                        itemCost.setDefaultValue(0);
                        itemCost.setEndRow(true);
                        itemCost.setWidth("*");
                        items.add(itemCost);
                
                        TextItem itemPriceMargin = new TextItem("price_margin");
                        itemPriceMargin.setWidth(70);
                        itemPriceMargin.setHint("%");
                        itemPriceMargin.setEndRow(true);
                        items.add(itemPriceMargin);
                
                        SelectItem itemVat = new SelectItem("vat");
                        itemVat.setDefaultValue("23");
                        itemVat.setWidth(70);
                        items.add(itemVat);
                
                        items.add(itemShowDiscount);
                
                        TextItem itemPriceBaseNet = new TextItem("price_base_net");
                        itemPriceBaseNet.setWidth("*");
                        items.add(itemPriceBaseNet);
                
                        TextItem itemPriceBase = new TextItem("price_base");
                        itemPriceBase.setWidth("*");
                        items.add(itemPriceBase);
                
                        TextItem itemDiscount = new TextItem("discount");
                        itemDiscount.setWidth(70);
                        itemDiscount.setHint("%");
                        itemDiscount.setEndRow(true);
                        FloatRangeValidator itemDiscountValidator = new FloatRangeValidator();
                        itemDiscountValidator.setMin(0);
                        itemDiscountValidator.setMax(100);
                        itemDiscount.setValidators(itemDiscountValidator);
                        items.add(itemDiscount);
                
                        CustomValidator validatorNoZero = new CustomValidator() {
                            @Override
                            protected boolean condition(Object value) {
                                try {
                                    return Float.valueOf(value + "") > 0;
                                } catch(NumberFormatException e) {} catch(NullPointerException e) {} catch(RuntimeException e) {};
                                return false;
                            }
                        };
                
                        TextItem itemPriceNet = new TextItem("price_net");
                        itemPriceNet.setEndRow(false);
                        itemPriceNet.setWidth("*");
                        items.add(itemPriceNet);
                
                
                        TextItem itemPrice = new TextItem("price");
                        itemPrice.setStartRow(false);
                        itemPrice.setWidth("*");
                        items.add(itemPrice);
                
                        SpinnerItem itemQuantity = new SpinnerItem("quantity");
                        itemQuantity.setDefaultValue(1);
                        itemQuantity.setMin(1);
                        itemQuantity.setStep(1);
                        itemQuantity.setWriteStackedIcons(true);
                        itemQuantity.setEndRow(true);
                        itemQuantity.setValidators(validatorNoZero);
                        items.add(itemQuantity);
                
                        StaticTextItem itemTotalNet = new StaticTextItem("total_net");
                        itemTotalNet.setDecimalPad(2);
                        itemTotalNet.setEndRow(false);
                        itemTotalNet.setWidth("*");
                        items.add(itemTotalNet);
                
                        StaticTextItem itemTotal = new StaticTextItem("total");
                        itemTotal.setDecimalPad(2);
                        itemTotal.setEndRow(true);
                        itemTotal.setWidth("*");
                        items.add(itemTotal);
                
                        StaticTextItem itemTotalProfit = new StaticTextItem("total_profit");
                        itemTotalProfit.setDecimalPad(2);
                        itemTotalProfit.setEndRow(true);
                        itemTotalProfit.setWidth("*");
                        items.add(itemTotalProfit);
                
                        return items;
                    }
                
                    public void setReadOnly(ArrayList<FormItem> items) {
                        for (FormItem item : items) {
                            if (item instanceof StaticTextItem || item instanceof BoxHeaderItem) {
                                continue;
                            }
                            if (item.getAttributeAsObject("readOnlyWhen") != null) {
                                AdvancedCriteria criteria = new AdvancedCriteria(OperatorId.OR, new Criterion[]{item.getReadOnlyWhen(), new AdvancedCriteria("status", OperatorId.IN_SET, new String[]{"finished", "canceled"})});
                                item.setReadOnlyWhen(criteria);
                            } else {
                                item.setReadOnlyWhen(new AdvancedCriteria("status", OperatorId.IN_SET, new String[]{"finished", "canceled"}));
                            }
                        }
                    }
                I've removed any handlers and ShowIf functions and left just grid with 2 forms connected with ValuesManager.

                whenRules flag can be used to disable all rules.

                Just run this code and click on record on grid.
                On console is returned time to perform just editRecord() on selected one in grid.
                On my machine this takes around 1000ms and without rules it takes around 10ms.

                I couldn't find single problem that causes this problem. That's why test case is not so simple.
                I think problem is scale.
                I've attached screenshots from chrome profiler on record click action.
                Profile_Rules-* are the same click with rules enabled
                Profile_NoRules-* without any rules

                As you can see whole process on both cases consists on 2 events:
                1. MouseUp - that leads to recordclick and editRecord
                2. Timer Fired - delayed redraw (probably called internally by DynamicForm)

                But in both cases these events are processed quite differently. Without rules it's quite simple and takes around 10ms and with rules there are multiple processRules stacks.

                Potential problems that I've found:
                1. Multiple rule checking. One call on editRecord() fires multiple setValue() on each item. Problem is that every time setValue() is called all rules are processed.

                2. Further on processRules method there are multiple calls on getValues()
                Don't know if I'm getting this right but it look like If values were cached during processRules it could speed up whole process.

                3. Further when redraw is processed there are multiple rule checks.

                Summing this up isc_RulesEngine_processRules is called 98 times on just one click.

                I don't know internal mechanizm but if used this process on similar problem some time ago:
                If processRules were scheduled to call on minimal delay - for example 50ms.
                And each schedule would cancel previous one. It would guarantee to only call processRules only ones per action.
                It would probably be more complicated than this in real life because there probably were multiple schedules depending on ruleContext but I think limiting processRules calls is right direction. Just a suggestion from what I've found.

                Hope that help to solve this problem. I've tested this also on testing laptop and this click with rules took 1800ms - 2400ms.
                Numbers are very similar to original times in my application so I think this test case should cover whole problem.

                Best regards
                Mariusz Goch
                Attached Files

                Comment


                  #9
                  Thanks for the clear test case. We're checking it out - we suspect we'll be able to eliminate most of the problem by running rules only once per editRecord() / setValues() call rather than responding to each individual formItem.setValue() call that is part of the overall editRecord() call.

                  We'll post here with updates.

                  Comment


                    #10
                    Changes have been made to reduce the number of times rules are processed when DynamicForms are initialized and for DynamicForm and ValuesManager editRecord() calls. This should improve your performance significantly based on your test code. These changes will be in 13.0 and later releases for builds starting July 8.

                    Please let us know your results.

                    Comment


                      #11
                      Hi,

                      I've checked today's build 2023-07-10 (first one after July 8).

                      1. Basically it's much better in both test case and original application. Looking at profiles during editRecord() processRules are called only ones.

                      2. But there is a scenario in my application that calls processRules multiple times.
                      From what is worse it's called from another processRules.

                      I've attached screenshot on this scenario.
                      As you can see on screenshot during processRules during getValues(), saveValue() is called which again triggers processRules.

                      That was not the case before. I've just compared to previous version and in older version I cannot populate this, so it's new.

                      In normal case on change selected record processRules are called 4 times - one time on editRecord() but in this particular scenario it's called 20 times and times are again jumping from 25ms to 450ms.

                      Strange thing it's not happening from the start.
                      It starts when I change BooleanItem value (even without saving) and then start to switch between records.

                      2. Another problem that still remains is multiple processRules calls when calculating other items.
                      I've got some handlers that calculates several other values and uses form.setValue() to update these items.
                      Is there a better way to do this??

                      3. As I see you took different approach than delaying processRules to perform after minimal timeout. Was there a particular implementation problem with my suggestion?

                      Best regards
                      Mariusz Goch
                      Attached Files

                      Comment


                        #12
                        1) can you show a test case in which this call chain happens?

                        2) call setValues() instead of individually calling setValue() each time. This is also more efficient even outside of *When rules

                        3) we're note sure what you mean about your suggestion. Are you saying why would we set an immediate timer vs wait 50ms? If so, what is the advantage of waiting 50ms?

                        Comment


                          #13
                          1. I'm working on it. I've got a potential hit. It's probably linked with calculations i'm doing.
                          I've found a place where into DataSourceFloatField is set empty string.
                          I will post a test case.

                          2. Ok. But I need to pass all values. So first getValues(), modify some, and then setValues() ??
                          But this Form is linked DataSource and according to docs editRecord() is suggested??

                          3. 50ms was just a guess. Probably 0 delay will work just fine - just to wait for current event flow finishes.
                          Example in JS probably will explain it better.
                          Code:
                          <script>
                            var processRulesTimeout;
                          
                            function processRules() {
                              console.log("processRules()");
                            }
                            function callEvent() {
                              if (processRulesTimeout) {
                                clearInterval(processRulesTimeout);
                              }
                              processRulesTimeout = setTimeout(() => {
                                processRules();
                              }, 0);
                            }
                          
                            function start() {
                              for (let i=0; i<20; i++) {
                                console.log('callEvent', i);
                                callEvent();
                              }
                            }
                          
                          start();
                          
                          </script>
                          Even when callEvent() was called 20 times processRules() will be called only ones after loop is ended.
                          This could probably limit processRules() just to 1 execution per event. Even when editRecord() calls it only one there are also some call on redraw(). So with this approach we could go down from 4 calls in previous test case to 1. This should also cover all scenarios not only the ones that have a workaround.

                          Best regards
                          Mariusz Goch

                          Comment


                            #14
                            1) good to hear

                            2) you can use editRecord() if what you want is the documented behavior of editRecord(), or setValues() if what you want is the documented behavior of setValues(). The point is, if you are going to modify multiple values at once, use an API that allows modifying multiple values at once, as this gives the framework the chance to optimize.

                            3) we already do something a bit more sophisticated

                            Comment


                              #15
                              Hi,
                              1. I've isolated test case.
                              Code:
                              public void testRules() {
                                      boolean whenRules = true;
                              
                                      DataSource ds = getRulesDataSource();
                              
                                      HLayout layout = new HLayout(8);
                              
                                      ListGrid listGrid = new ListGrid();
                                      listGrid.setDataSource(ds);
                                      listGrid.setWidth(300);
                                      listGrid.setHeight(300);
                              
                                      listGrid.fetchData();
                              
                                      layout.addMember(listGrid);
                              
                                      ValuesManager vm = new ValuesManager();
                                      vm.setDataSource(ds);
                              
                                      DynamicForm form1 = new DynamicForm();
                                      BoxFormat.form(form1);
                                      form1.setDataSource(ds);
                                      form1.setValuesManager(vm);
                              
                                      ArrayList<FormItem> items1 = onForm1(whenRules);
                              
                                      if (whenRules) {
                                          setReadOnly(items1);
                                      }
                              
                                      form1.setFields(items1.toArray(new FormItem[items1.size()]));
                              
                                      layout.addMember(form1);
                              
                                      DynamicForm form2 = new DynamicForm();
                                      BoxFormat.form(form2);
                                      form2.setDataSource(ds);
                                      form2.setValuesManager(vm);
                                      form2.setNumCols(4);
                                      form2.setColWidths("30%", "25%", "20%", "25%");
                              
                                      ArrayList<FormItem> items2 = onForm2(whenRules);
                                      if (whenRules) {
                                          setReadOnly(items2);
                                      }
                              
                                      form2.setFields(items2.toArray(new FormItem[items2.size()]));
                              
                                      layout.addMember(form2);
                              
                                      listGrid.addRecordClickHandler(event -> {
                                          long time = (new Date()).getTime();
                                          vm.editRecord(event.getRecord());
                                          long time2 = (new Date()).getTime();
                                          console.log("editRecord time: ", time2 - time);
                                      });
                              
                                      this.addChild(layout);
                                  }
                              
                                  public DataSource getRulesDataSource() {
                                      DataSource ds = new DataSource();
                                      ds.setClientOnly(true);
                              
                                      DataSourceIntegerField fieldId = new DataSourceIntegerField("id");
                                      fieldId.setPrimaryKey(true);
                                      ds.addField(fieldId);
                              
                                      DataSourceTextField fieldType = new DataSourceTextField("type");
                                      fieldType.setValueMap("ware", "service", "period");
                                      ds.addField(fieldType);
                              
                                      DataSourceTextField fieldPeriod = new DataSourceTextField("period");
                                      fieldPeriod.setValueMap("day", "week", "2week", "month", "2month");
                                      ds.addField(fieldPeriod);
                              
                                      DataSourceTextField fieldStatus = new DataSourceTextField("status");
                                      fieldStatus.setValueMap("new", "progress", "finished", "canceled");
                                      ds.addField(fieldStatus);
                              
                                      DataSourceTextField fieldVat = new DataSourceTextField("vat");
                                      ds.addField(fieldVat);
                              
                                      DataSourceFloatField fieldCost = new DataSourceFloatField("cost");
                                      fieldCost.setFormat("0.00");
                                      ds.addField(fieldCost);
                              
                                      DataSourceFloatField fieldPriceMargin = new DataSourceFloatField("price_margin");
                                      fieldPriceMargin.setDecimalPad(2);
                                      ds.addField(fieldPriceMargin);
                              
                                      DataSourceFloatField fieldPriceBaseNet = new DataSourceFloatField("price_base_net");
                                      fieldPriceBaseNet.setFormat("0.00");
                                      ds.addField(fieldPriceBaseNet);
                              
                                      DataSourceFloatField fieldPriceBase = new DataSourceFloatField("price_base");
                                      fieldPriceBase.setFormat("0.00");
                                      ds.addField(fieldPriceBase);
                              
                                      DataSourceFloatField fieldDiscount = new DataSourceFloatField("discount");
                                      fieldDiscount.setDecimalPad(2);
                                      ds.addField(fieldDiscount);
                              
                                      DataSourceFloatField fieldPriceNet = new DataSourceFloatField("price_net");
                                      fieldPriceNet.setFormat("0.00");
                                      fieldPriceNet.setRequired(true);
                                      ds.addField(fieldPriceNet);
                              
                                      DataSourceFloatField fieldPrice = new DataSourceFloatField("price");
                                      fieldPrice.setFormat("0.00");
                                      fieldPrice.setRequired(true);
                                      ds.addField(fieldPrice);
                              
                                      DataSourceFloatField fieldPriceProfit = new DataSourceFloatField("price_profit");
                                      fieldPriceProfit.setFormat("0.00");
                                      ds.addField(fieldPriceProfit);
                              
                                      Record r1 = new Record();
                                      r1.setAttribute("id", 1);
                                      r1.setAttribute("product_id", 1);
                                      r1.setAttribute("type", "period");
                                      r1.setAttribute("status", "new");
                                      r1.setAttribute("quantity", "11");
                                      r1.setAttribute("total_net", "11.12");
                                      r1.setAttribute("total", "11.12");
                                      r1.setAttribute("total_profit", "11.12");
                                      r1.setAttribute("vat", "23");
                                      r1.setAttribute("cost", "11.12");
                                      r1.setAttribute("price_margin", "11.12");
                                      r1.setAttribute("price_base_net", "11.12");
                                      r1.setAttribute("price_base", "11.12");
                                      r1.setAttribute("showDiscount", true);
                                      r1.setAttribute("discount", "11.12");
                                      r1.setAttribute("price_net", "11.12");
                                      r1.setAttribute("price", "11.12");
                                      r1.setAttribute("discount", "11.12");
                              
                              
                                      Record r2 = new Record();
                                      r2.setAttribute("id", 2);
                                      r2.setAttribute("type", "ware");
                                      r2.setAttribute("status", "progress");
                                      r2.setAttribute("quantity", "11");
                                      r2.setAttribute("total_net", "0.99");
                                      r2.setAttribute("total", "0.99");
                                      r2.setAttribute("total_profit", "0.99");
                                      r2.setAttribute("vat", "23");
                                      r2.setAttribute("cost", "0.99");
                                      r2.setAttribute("price_margin", "0.99");
                                      r2.setAttribute("price_base_net", "0.99");
                                      r2.setAttribute("price_base", "0.99");
                                      r2.setAttribute("showDiscount", true);
                                      r2.setAttribute("discount", "0.99");
                                      r2.setAttribute("price_net", "0.99");
                                      r2.setAttribute("price", "0.99");
                                      r2.setAttribute("discount", "0.99");
                              
                                      Record r3 = new Record();
                                      r3.setAttribute("id", 3);
                                      r3.setAttribute("product_id", 2);
                                      r3.setAttribute("type", "period");
                                      r3.setAttribute("showDiscount", false);
                                      r3.setAttribute("status", "progress");
                              
                                      ds.setTestData(r1, r2, r3);
                              
                                      return ds;
                                  }
                              
                                  public ArrayList<FormItem> onForm1(boolean whenRules) {
                                      ArrayList<FormItem> items = new ArrayList<FormItem>();
                              
                                      items.add(new TextItem("id"));
                              
                                      AdvancedCriteria criteriaProduct = new AdvancedCriteria("product_id", OperatorId.NOT_NULL);
                              
                                      TextItem itemProductId = new TextItem("product_id");
                                      if (whenRules) {
                                          itemProductId.setVisibleWhen(criteriaProduct);
                                      }
                                      items.add(itemProductId);
                              
                                      TextItem itemName = new TextItem("name");
                                      items.add(itemName);
                              
                                      TextItem itemCode = new TextItem("code");
                                      items.add(itemCode);
                              
                                      RadioGroupItem itemType = new RadioGroupItem("type");
                                      itemType.setDefaultValue("ware");
                                      itemType.setVertical(false);
                                      items.add(itemType);
                              
                                      SelectItem itemPeriod = new SelectItem("period");
                                      itemPeriod.setDefaultValue("month");
                                      if (whenRules) {
                                          itemPeriod.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                                      }
                                      items.add(itemPeriod);
                              
                                      SpinnerItem itemPeriodQuantity = new SpinnerItem("period_quantity");
                                      itemPeriodQuantity.setDefaultValue(1);
                                      itemPeriodQuantity.setMin(1);
                                      itemPeriodQuantity.setStep(1);
                                      itemPeriodQuantity.setWriteStackedIcons(true);
                                      if (whenRules) {
                                          itemPeriodQuantity.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                                      }
                                      items.add(itemPeriodQuantity);
                              
                                      DateItem itemDateStart = new DateItem("date_start");
                                      if (whenRules) {
                                          itemDateStart.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                                      }
                                      itemDateStart.setDefaultValue(new Date());
                                      items.add(itemDateStart);
                              
                                      DateItem itemDateEnd = new DateItem("date_end");
                                      if (whenRules) {
                                          itemDateEnd.setVisibleWhen(new AdvancedCriteria("type", OperatorId.EQUALS, "period"));
                                          itemDateEnd.setReadOnlyWhen(new AdvancedCriteria("period", OperatorId.NOT_EQUAL, "other"));
                                      }
                                      items.add(itemDateEnd);
                              
                                      TextItem itemPKWiU = new TextItem("pkwiu");
                                      itemPKWiU.setWidth(120);
                                      items.add(itemPKWiU);
                              
                                      TextItem itemSerial = new TextItem("serial");
                                      items.add(itemSerial);
                              
                                      items.add(new SelectItem("status"));
                              
                                      return items;
                                  }
                              
                                  public ArrayList<FormItem> onForm2(boolean whenRules) {
                                      ArrayList<FormItem> items = new ArrayList<FormItem>();
                              
                                      BooleanItem itemShowDiscount = new BooleanItem("showDiscount");
                                      itemShowDiscount.setDefaultValue(false);
                                      itemShowDiscount.setStartRow(false);
                                      itemShowDiscount.setEndRow(true);
                                      itemShowDiscount.setShowTitle(false);
                                      itemShowDiscount.setColSpan(2);
                                      itemShowDiscount.setAlign(Alignment.CENTER);
                                      itemShowDiscount.addChangedHandler(event -> {
                                          DynamicForm form = event.getForm();
                              
                                          form.setValue("price_base_net", "");
                                          form.setValue("price_base", "");
                                          form.setValue("discount", "");
                                      });
                              
                                      TextItem itemCost = new TextItem("cost");
                                      itemCost.setDefaultValue(0);
                                      itemCost.setEndRow(true);
                                      itemCost.setWidth("*");
                                      items.add(itemCost);
                              
                                      TextItem itemPriceMargin = new TextItem("price_margin");
                                      itemPriceMargin.setWidth(70);
                                      itemPriceMargin.setHint("%");
                                      itemPriceMargin.setEndRow(true);
                                      items.add(itemPriceMargin);
                              
                                      SelectItem itemVat = new SelectItem("vat");
                                      itemVat.setDefaultValue("23");
                                      itemVat.setWidth(70);
                                      items.add(itemVat);
                              
                                      items.add(itemShowDiscount);
                              
                                      TextItem itemPriceBaseNet = new TextItem("price_base_net");
                                      itemPriceBaseNet.setWidth("*");
                                      itemPriceBaseNet.setVisibleWhen(new AdvancedCriteria("showDiscount", OperatorId.EQUALS, true));
                                      items.add(itemPriceBaseNet);
                              
                                      TextItem itemPriceBase = new TextItem("price_base");
                                      itemPriceBase.setWidth("*");
                                      itemPriceBase.setVisibleWhen(new AdvancedCriteria("showDiscount", OperatorId.EQUALS, true));
                                      items.add(itemPriceBase);
                              
                                      TextItem itemDiscount = new TextItem("discount");
                                      itemDiscount.setWidth(70);
                                      itemDiscount.setHint("%");
                                      itemDiscount.setEndRow(true);
                                      itemDiscount.setVisibleWhen(new AdvancedCriteria("showDiscount", OperatorId.EQUALS, true));
                              
                                      FloatRangeValidator itemDiscountValidator = new FloatRangeValidator();
                                      itemDiscountValidator.setMin(0);
                                      itemDiscountValidator.setMax(100);
                                      itemDiscount.setValidators(itemDiscountValidator);
                                      items.add(itemDiscount);
                              
                                      CustomValidator validatorNoZero = new CustomValidator() {
                                          @Override
                                          protected boolean condition(Object value) {
                                              try {
                                                  return Float.valueOf(value + "") > 0;
                                              } catch(NumberFormatException e) {} catch(NullPointerException e) {} catch(RuntimeException e) {};
                                              return false;
                                          }
                                      };
                              
                                      TextItem itemPriceNet = new TextItem("price_net");
                                      itemPriceNet.setEndRow(false);
                                      itemPriceNet.setWidth("*");
                                      items.add(itemPriceNet);
                              
                              
                                      TextItem itemPrice = new TextItem("price");
                                      itemPrice.setStartRow(false);
                                      itemPrice.setWidth("*");
                                      items.add(itemPrice);
                              
                                      SpinnerItem itemQuantity = new SpinnerItem("quantity");
                                      itemQuantity.setDefaultValue(1);
                                      itemQuantity.setMin(1);
                                      itemQuantity.setStep(1);
                                      itemQuantity.setWriteStackedIcons(true);
                                      itemQuantity.setEndRow(true);
                                      itemQuantity.setValidators(validatorNoZero);
                                      items.add(itemQuantity);
                              
                                      StaticTextItem itemTotalNet = new StaticTextItem("total_net");
                                      itemTotalNet.setDecimalPad(2);
                                      itemTotalNet.setEndRow(false);
                                      itemTotalNet.setWidth("*");
                                      items.add(itemTotalNet);
                              
                                      StaticTextItem itemTotal = new StaticTextItem("total");
                                      itemTotal.setDecimalPad(2);
                                      itemTotal.setEndRow(true);
                                      itemTotal.setWidth("*");
                                      items.add(itemTotal);
                              
                                      StaticTextItem itemTotalProfit = new StaticTextItem("total_profit");
                                      itemTotalProfit.setDecimalPad(2);
                                      itemTotalProfit.setEndRow(true);
                                      itemTotalProfit.setWidth("*");
                                      items.add(itemTotalProfit);
                              
                                      return items;
                                  }
                              
                                  public void setReadOnly(ArrayList<FormItem> items) {
                                      for (FormItem item : items) {
                                          if (item instanceof StaticTextItem || item instanceof BoxHeaderItem) {
                                              continue;
                                          }
                                          if (item.getAttributeAsObject("readOnlyWhen") != null) {
                                              AdvancedCriteria criteria = new AdvancedCriteria(OperatorId.OR, new Criterion[]{item.getReadOnlyWhen(), new AdvancedCriteria("status", OperatorId.IN_SET, new String[]{"finished", "canceled"})});
                                              item.setReadOnlyWhen(criteria);
                                          } else {
                                              item.setReadOnlyWhen(new AdvancedCriteria("status", OperatorId.IN_SET, new String[]{"finished", "canceled"}));
                                          }
                                      }
                                  }
                              Steps to reproduce:
                              - Select first record
                              - Click twice on showDiscount BooleanItem
                              - Select third record

                              Or click ones on showDiscount and then switch records few times.

                              It's connected to this code in change handler:
                              Code:
                                          form.setValue("price_base_net", "");
                                          form.setValue("price_base", "");
                                          form.setValue("discount", "");
                              If I change this to:
                              Code:
                                          form.setValue("price_base_net", (String)null);
                                          form.setValue("price_base", (String)null);
                                          form.setValue("discount", (String)null);
                              it works fine. But I think it's worth checking.

                              By the way, not related but another surprising behavior is that this code is causing Exception even when item is FloatItem.
                              Code:
                              form.setValue("price_base_net", (Float)null);

                              3. I believe you did but as I'm testing it solved problems with editRecord() and not other scenarios when multiple calls to processRules are happening.
                              I think some kind of scheduler would help to solve this problem. I now code I send was not sufficient to solve the problem but I was only trying to demonstrate the idea.

                              Some other scenarios when multiple processRules are happening:

                              3a. In above scenario put from keyboard just one letter in name field.
                              processRules is called 4 times - see attached profile: Profile_One_Key.png

                              3b. Change field type to any other value: 5 times.

                              I now above test case works fast with this scenarios but rules can be much more.

                              3c. On my real life app changing showDiscount item still calls processRules 18 times - and this is after some optimization with setValues() instead of setValue().

                              3d. On base scenario with just click on change record on ListGrid processRules is called 5 times.
                              On real app it's still called 14 times - I've attached profile.
                              In my app I used Rules not only for DynamicForm items but also to buttons in ToolBar.

                              So I think there is still space for improvement. Functionality of whenRules is great and I've got some more ideas to optimize some processes in my applications but I worry that it will cause performance problems when they will be used in whole application.
                              Please consider creating some kind of scheduler.

                              Best regards
                              Mariusz Goch
                              Attached Files

                              Comment

                              Working...
                              X