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"})); } } }
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
Leave a comment: