support icon indicating copy to clipboard operation
support copied to clipboard

Selecting/deselecting all columns triggers excessive re-rendering

Open canonic-epicure opened this issue 9 months ago • 2 comments

Forum post

Performance profiling: Trace-20240515T143230.json

To reproduce place this content into Gantt/examples/advanced/app.js

import '../_shared/shared.js'; // not required, our example styling etc.
import Gantt from '../../lib/Gantt/view/Gantt.js';
import '../../lib/Gantt/column/AllColumns.js';
import '../../lib/Scheduler/feature/TimeRanges.js';
import '../../lib/SchedulerPro/feature/DependencyEdit.js';
import '../../lib/Gantt/feature/Baselines.js';
import '../../lib/Scheduler/feature/RowResize.js';
import '../../lib/Grid/feature/Filter.js';
import '../../lib/Gantt/feature/Labels.js';
import '../../lib/Gantt/feature/ParentArea.js';
import '../../lib/Gantt/feature/ProjectLines.js';
import '../../lib/Gantt/feature/ProgressLine.js';
import '../../lib/Gantt/feature/Rollups.js';
import '../../lib/Gantt/feature/TaskEdit.js';
import '../../lib/Grid/feature/CellCopyPaste.js';
import '../../lib/Grid/feature/FillHandle.js';

import './lib/GanttToolbar.js';
import Task from './lib/Task.js';
import './lib/StatusColumn.js';
import './lib/ComplexityColumn.js';

const gantt = new Gantt({
    appendTo : 'container',

    dependencyIdField : 'wbsCode',

    selectionMode : {
        cell       : true,
        dragSelect : true,
        rowNumber  : true
    },

    project : {
        // Let the Project know we want to use our own Task model with custom fields / methods
        taskModelClass : Task,
        transport      : {
            load : {
                url : '../_datasets/launch-saas.json'
            }
        },
        autoLoad  : true,
        taskStore : {
            wbsMode : 'auto'
        },
        // The State TrackingManager which the UndoRedo widget in the toolbar uses
        stm : {
            autoRecord : true
        },
        // Reset Undo / Redo after each load
        resetUndoRedoQueuesAfterLoad : true
    },

    // For best initial performance, configure startDate & endDate to determine the date range that needs to be
    // rendered initially
    startDate                     : '2019-01-12',
    endDate                       : '2019-03-24',
    resourceImageFolderPath       : '../_shared/images/users/',
    scrollTaskIntoViewOnCellClick : true,

    columns : [
        { type : 'wbs' },
        { type : 'name', width : 250 },
        { type : 'startdate' },
        { type : 'duration' },
        { type : 'resourceassignment', width : 120, showAvatars : true },
        { type : 'percentdone', showCircle : true, width : 70 },
        {
            type  : 'predecessor',
            width : 112
        },
        {
            type  : 'successor',
            width : 112
        },
        { type : 'schedulingmodecolumn' },
        { type : 'calendar' },
        { type : 'constrainttype' },
        { type : 'constraintdate' },
        { type : 'statuscolumn' },
        { type : 'complexitycolumn' },
        { type : 'deadlinedate' },
        { type : 'addnew' },
        { type : 'wbs' },
        { type : 'name', width : 250 },
        { type : 'startdate' },
        { type : 'duration' },
        { type : 'resourceassignment', width : 120, showAvatars : true },
        { type : 'percentdone', showCircle : true, width : 70 },
        {
            type  : 'predecessor',
            width : 112
        },
        {
            type  : 'successor',
            width : 112
        },
        { type : 'schedulingmodecolumn' },
        { type : 'calendar' },
        { type : 'constrainttype' },
        { type : 'constraintdate' },
        { type : 'statuscolumn' },
        { type : 'complexitycolumn' },
        { type : 'deadlinedate' },
        { type : 'addnew' },
        { type : 'wbs' },
        { type : 'name', width : 250 },
        { type : 'startdate' },
        { type : 'duration' },
        { type : 'resourceassignment', width : 120, showAvatars : true },
        { type : 'percentdone', showCircle : true, width : 70 },
        {
            type  : 'predecessor',
            width : 112
        },
        {
            type  : 'successor',
            width : 112
        },
        { type : 'schedulingmodecolumn' },
        { type : 'calendar' },
        { type : 'constrainttype' },
        { type : 'constraintdate' },
        { type : 'statuscolumn' },
        { type : 'complexitycolumn' },
        { type : 'deadlinedate' },
        { type : 'addnew' },
        { type : 'wbs' },
        { type : 'name', width : 250 },
        { type : 'startdate' },
        { type : 'duration' },
        { type : 'resourceassignment', width : 120, showAvatars : true },
        { type : 'percentdone', showCircle : true, width : 70 },
        {
            type  : 'predecessor',
            width : 112
        },
        {
            type  : 'successor',
            width : 112
        },
        { type : 'schedulingmodecolumn' },
        { type : 'calendar' },
        { type : 'constrainttype' },
        { type : 'constraintdate' },
        { type : 'statuscolumn' },
        { type : 'complexitycolumn' },
        { type : 'deadlinedate' },
        { type : 'addnew' },
    ],

    subGridConfigs : {
        locked : {
            flex : 3
        },
        normal : {
            flex : 4
        }
    },

    columnLines : false,

    // Shows a color field in the task editor and a color picker in the task menu.
    // Both lets the user select the Task bar's background color
    showTaskColorPickers : true,

    features : {
        baselines : {
            disabled : true
        },
        dependencies : {
            showLagInTooltip : true,
            // Soften up dependency line corners
            radius           : 3,
            // Make dependencies easier to reach using the mouse
            clickWidth       : 5
        },
        dependencyEdit : true,
        filter         : true,
        labels         : {
            left : {
                field  : 'name',
                editor : {
                    type : 'textfield'
                }
            }
        },
        parentArea : {
            disabled : true
        },
        progressLine : {
            disabled   : true,
            statusDate : new Date(2019, 0, 25)
        },
        rollups : {
            disabled : true
        },
        rowResize : {
            cellSelector : '.b-rownumber-cell'
        },
        rowReorder : {
            showGrip        : true,
            preserveSorters : true
        },
        timeRanges : {
            showCurrentTimeLine : true
        },
        fillHandle    : true,
        cellCopyPaste : true,
        taskCopyPaste : {
            useNativeClipboard : true
        },

        headerMenu : {
            processItems(props) {
                const { items : { columns }, feature : { client : grid } } = props;
                const items = [];
                let allSelected = true;
                grid.columns.allRecords.forEach(column => {
                    items.push({
                        grid,
                        column,
                        text     : column.text,
                        checked  : !column.hidden,
                        disabled : false,
                        cls      : ''
                    });
                    if (allSelected !== false) allSelected = !column.hidden;
                });

                items.unshift({
                    grid,
                    ref     : 'selectAll',
                    text    : 'Select All',
                    checked : allSelected
                });

                columns.menu.items = items;
            },
            items : {
                columns : {
                    text   : 'Columns',
                    cls    : 'b-separator',
                    icon   : 'b-fw-icon b-icon-columns',
                    weight : 120,
                    menu   : {},
                    onToggle(props) {
                        const { menu, item, checked } = props;

                        if (item.text === 'Select All') {
                            Object.keys(menu.widgetMap).forEach((key) => {
                                const col = menu.widgetMap[key];
                                col.checked = checked;
                            });
                        }
                        else {
                            item.column && (item.column.hidden = !checked);
                        }

                    }
                },
                // Hide the default column picker
                columnPicker : null
            }
        }
    },

    tbar : {
        type : 'gantttoolbar'
    }
});

Right click in the left sub-grid header, point to Columns -> Select All. Observe a significant delay in processing. It seems what happens is that after every change of the column "checked" state, a full re-rendering is performed.

Probably this "full re-rendering" can be buffered on 10ms to allow batch updates.

canonic-epicure avatar May 15 '24 10:05 canonic-epicure

What we recommend in similar cases (not involving columns) is to suspendRefresh(), perform all operations, resumeRefresh(). That will lead to a single redraw on resume.

But, columns change handling does not take refresh suspension into account. We can make it do that, but it is going to have to keep track of that columns has changed when resuming, since column changes require a more brutal update (renderContents() instead of refresh())

isglass avatar May 15 '24 11:05 isglass

An optimized version of onToggle handler:

                    onToggle(props) {
                        const { menu, item, checked } = props;

                        const gantt = menu.up('gantt');

                        if (item.text === 'Select All') {

                            gantt.columns.beginBatch();

                            gantt.suspendRefresh();

                            Object.keys(menu.items).forEach((key, index) => {
                                const col = menu.items[key];
                                col.checked = checked;
                            });

                            gantt.resumeRefresh();

                            gantt.columns.endBatch();
                        }
                        else {
                            item.column && (item.column.hidden = !checked);
                        }
                    }

It makes the refresh process instant, but events rendering is broken: image

canonic-epicure avatar May 15 '24 11:05 canonic-epicure