Announcement

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

    GridBody internal row cache poisoned by mouseMove during setData([]) transition — permanent blank grid (virtualScrolling + grouping + mid-scroll)

    ---
    Subject: GridBody internal row cache poisoned by mouseMove during setData([]) transition — permanent blank grid (virtualScrolling + grouping + mid-scroll)

    SmartClient Version: 14.1.7.26.25

    Browser: Chrome Windows

    Summary:
    After recently enabling virtualScrolling on a large grouped, editable ListGrid (~2,200 rows), we began experiencing a permanent blank-grid condition during data refresh operations.
    When listGrid.setData([]) is called to clear data before a server fetch, and the mouse cursor is hovering over the grid body while the viewport is scrolled to a mid-grid position,
    GridRenderer.mouseMove → shouldShowRollOver → getEventRow() resolves to NaN on the empty body. This NaN propagates into 5 internal GridRenderer properties — 3 of which are
    virtual-scrolling-specific — permanently breaking viewport calculation. The grid appears blank despite having valid data. We did not observe this behavior before enabling
    virtualScrolling.

    Conditions required (all must be true):
    1. virtualScrolling: true
    2. Grid is grouped (groupByField)
    3. Grid is editable (canEdit: true)
    4. Viewport is scrolled away from the top (mid-grid or further)
    5. Mouse cursor is hovering over the grid body (user may have just clicked into/out of an edit cell)
    6. setData([]) is called to clear data before reload

    Affected internal properties (from modules-debug/ISC_Grids.js):

    ┌──────────┬───────────────────────┬────────────────────────────────┬───────────────────┬─────────────────────────────────────────────────────┐
    │ Property │ Debug Name │ Virtual Scrolling Specific? │ Set By │ Purpose │
    ├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
    │ $252 │ _firstDrawnRow │ No (all incremental rendering) │ getTableHTML() │ First row index in rendered HTML table │
    ├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
    │ $253 │ _lastDrawnRow │ No (all incremental rendering) │ getTableHTML() │ Last row index in rendered HTML table │
    ├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
    │ $27y │ _targetRow │ Yes │ _storeTargetRow() │ Virtual scrolling anchor row │
    ├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
    │ $27z │ _rowOffset │ Yes │ _storeTargetRow() │ Pixel offset into target row for scroll restoration │
    ├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
    │ $514 │ _startRowSpacerHeight │ Yes │ getTableHTML() │ Spacer height above first drawn row │
    └──────────┴───────────────────────┴────────────────────────────────┴───────────────────┴─────────────────────────────────────────────────────┘

    The primary NaN entry point is _storeTargetRow() (ISC_Grids.js debug line 6768), which is a virtual scrolling method. It calls getEventRow() to find the scroll anchor row. On an
    empty grid body, getEventRow() returns NaN instead of -2, which poisons _targetRow and _rowOffset. On the next getTableHTML() call, NaN propagates into _firstDrawnRow,
    _lastDrawnRow, and _startRowSpacerHeight. Without virtualScrolling enabled, _storeTargetRow() does not run and 3 of the 5 properties would not exist on the body, which likely
    explains why we never hit this before. Additionally, _storeTargetRow() only does meaningful work when the viewport is scrolled away from the top — at scrollTop: 0, the anchor row
    and spacer height are trivially 0, which may explain why this only manifests when scrolled mid-grid.

    Root cause chain:
    1. listGrid.setData([]) clears grid data before fetch
    2. Mouse is hovering over grid body — GridRenderer.mouseMove fires
    3. shouldShowRollOver → getRecord → getEventRow() returns NaN on empty body
    4. _storeTargetRow() caches NaN into _targetRow and _rowOffset
    5. On next getTableHTML(), NaN propagates into _firstDrawnRow, _lastDrawnRow, _startRowSpacerHeight
    6. getVisibleRows() returns [NaN, NaN], getDrawArea() returns [NaN, NaN, 0, N]
    7. Body renders zero rows — grid appears permanently blank
    8. body.redraw() does NOT fix it because the cached NaN values are reused
    9. Subsequent setData(validResultSet) does NOT invalidate the poisoned cache

    Note on existing guard: _storeTargetRow() has an isEmpty() check at debug line 6773 that should short-circuit on an empty grid. Either isEmpty() is not returning true immediately
    after setData([]) on a grouped/editable grid (the Tree wrapper around the empty array, or editor teardown, may create a window where isEmpty() returns false), or the NaN is entering
    through a different path before _storeTargetRow() is reached.

    Reproduction:

    // ATTEMPTED REPRODUCTION (not yet confirmed outside production)
    // Closest approximation of production conditions.
    // We were unable to trigger the bug in the SmartClient showcase
    // despite matching: virtualScrolling, grouping, editing, mid-scroll,
    // and mouse hover during setData([]) clear window.
    // The bug likely requires additional application-specific conditions
    // (DataSource/ResultSet binding, field formatters, frozen columns, etc.)
    isc.ListGrid.create({
    ID: "testGrid",
    width: 500, height: 300,
    canHover: true,
    showRollOver: true,
    virtualScrolling: true,
    groupByField: "category",
    groupStartOpen: "all",
    groupByMaxRecords: 5000,
    canEdit: true,
    editEvent: "click",
    editByCell: true,
    listEndEditAction: "next",
    fields: [
    {name: "id", title: "ID", canEdit: false},
    {name: "name", title: "Name", canEdit: true},
    {name: "value", title: "Value", type: "float", canEdit: true},
    {name: "category", title: "Category", canEdit: false}
    ]
    });

    var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
    var initialData = [];
    for (var i = 0; i < 2200; i++) {
    initialData.add({
    id: i,
    name: "Record " + i,
    value: Math.round(Math.random() * 10000) / 100,
    category: categories[i % 5]
    });
    }
    testGrid.setData(initialData);

    // Instrument mouseMove to verify events reach SmartClient
    var origMouseMove = testGrid.body.mouseMove;
    testGrid.body.mouseMove = function() {
    if (this.getTotalRows() === 0) {
    console.log(">>> mouseMove on EMPTY body, totalRows=0");
    }
    return origMouseMove ? origMouseMove.apply(this, arguments) : true;
    };

    // ATTEMPTED REPRODUCTION STEPS:
    // 1. Scroll to middle of grid
    testGrid.body.scrollTo(0, testGrid.body.getScrollHeight() / 2);

    // 2. Click into a cell to start editing, then click elsewhere to end edit
    // (leave cursor hovering over grid)

    // 3. Run this to trigger the bug:
    function triggerRefresh() {
    console.log(">>> Clearing data — mouse should be over grid and scrolled mid-page");
    testGrid.setData([]);

    // Give mouseMove time to fire on empty body
    isc.Timer.setTimeout(function() {
    var newData = [];
    var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
    for (var i = 0; i < 2200; i++) {
    newData.add({
    id: i,
    name: "Refreshed " + i,
    value: Math.round(Math.random() * 10000) / 100,
    category: categories[i % 5]
    });
    }
    testGrid.setData(newData);

    isc.Timer.setTimeout(function() {
    var vis = testGrid.body.getVisibleRows();
    console.log("totalRows:", testGrid.body.getTotalRows());
    console.log("visibleRows:", vis);
    console.log("drawArea:", testGrid.body.getDrawArea());
    if (isNaN(vis[0])) {
    console.log(">>> BUG REPRODUCED — grid blank with valid data");
    } else {
    console.log(">>> No bug this attempt — retry with mouse wiggle during clear");
    }
    }, 100);
    }, 50);
    }

    // After scrolling and doing an edit, call triggerRefresh() from console
    // while hovering mouse over the grid body.

    Manual reproduction steps:
    1. Paste the above into the SmartClient console
    2. Scroll the grid to the middle
    3. Click a "Name" or "Value" cell to start editing, then click a non-editable cell or press Escape to end the edit (leave cursor on grid)
    4. Type triggerRefresh() in the console and press Enter while your mouse hovers over the grid body
    5. Wiggle the mouse slightly over the grid during the 50ms clear window
    6. Check console output — if visibleRows shows [NaN, NaN], bug is reproduced
    7. May require several attempts due to timing sensitivity

    Diagnostic evidence from production (2,217-row grouped editable grid with virtualScrolling):
    - getData().getLength() → 2217 (data present, Tree type from grouping)
    - body.isDrawn() → true, body.isVisible() → true
    - body.getTotalRows() → 2217
    - body.getVisibleRows() → [NaN, NaN]
    - body.getDrawArea() → [NaN, NaN, 0, 54]
    - body.getViewportHeight() → 702, cellHeight → 22, scrollTop → 0 (all valid)
    - body.getHandle().innerHTML.length → 1965 (structural HTML only, no row content)
    - 5 hasOwnProperty NaN-valued numeric properties on body: $252, $253, $27y, $27z, $514
    - Resetting all 5 to 0 + body.redraw() → grid renders correctly

    Workaround: After setData() with non-empty data, we check getVisibleRows()[0] for NaN in a setTimeout. If poisoned, we scan the body for NaN-valued own properties, reset them to 0,
    and force body.redraw(). The check must be in a setTimeout because the NaN poisoning happens asynchronously via mouseMove during the event loop gap. This is a no-op in the normal
    case.

    Note on reproduction difficulty: We have been unable to reproduce this outside of our production environment despite matching the configuration (virtualScrolling, grouping, editing,
    mid-scroll position, mouse hover). The bug may require additional conditions present in our application — such as DataSource binding with ResultSet lifecycle, specific field types
    or formatters, frozen columns, or other grid features. The diagnostic evidence from production is conclusive: the NaN values are present on the body, they map to documented internal
    properties, and resetting them fixes the rendering. We are filing this report with full diagnostics to assist Isomorphic in identifying the code path even if the standalone
    reproduction proves elusive.

    Expected behavior:
    Either (a) getEventRow() should return a safe sentinel (e.g., -2) rather than NaN when the grid body has no rows, or (b) setData() with a non-empty dataset should invalidate stale
    body row caches from the prior empty state, or (c) _storeTargetRow() should validate the return value of getEventRow() before caching it.


    Workaround (in production):

    We added a clearNaNBodyCacheAT() method to isc.ListGrid that detects and repairs the poisoned cache. It is called both before and after setData() — the post-call invocation runs in
    setTimeout(fn, 0) to catch NaN re-introduced during deferred redraws.

    Method definition (added via isc.ATListGrid subclass):
    clearNaNBodyCacheAT: function() {
    if (!this.body) return false;
    var visibleRows = this.body.getVisibleRows();
    if (visibleRows != null && !isNaN(visibleRows[0])) return false;
    var body = this.body;
    var cleared = false;
    for (var k in body) {
    try {
    if (body.hasOwnProperty(k) && typeof body[k] === 'number' && isNaN(body[k])) {
    body[k] = 0;
    cleared = true;
    }
    } catch(e) {}
    }
    if (cleared) {
    var warnMessage = this.getID() + ": cleared NaN-poisoned body cache properties, forcing redraw";
    if (console) {
    console.warn(warnMessage);
    }
    isc.Log.logWarn(warnMessage);
    body.redraw("cleared NaN body cache");
    }
    return cleared;
    }

    Call pattern (at every setData() call site):
    fundAssetsGrid.clearNaNBodyCacheAT();
    fundAssetsGrid.setData(resultSet);
    setTimeout(function(){ fundAssetsGrid.clearNaNBodyCacheAT(); }, 0);

    The method uses getVisibleRows()[0] as a fast canary — in the normal case the cost is one null check and one isNaN check. Only when corruption is detected does it scan body
    own-properties for NaN values, reset them to 0, and force body.redraw(). The pre-setData() call clears any existing corruption before SmartClient's internal redraw; the setTimeout
    post-call catches NaN re-introduced by mouseMove events during the deferred redraw cycle.

    #2
    Not reproducible - but since you used AI to generate this, and it would be very easy to ask the AI to repro in the stock SDK as well, I suspect you already know that...

    It seems you're hoping we'll add defensive code for a situation that may well be caused by bad usage, which we are always very reluctant to do, because it can mask the real problem, which then never gets found.

    But what we've done, in this case, is added a guard at the point where bad values would be saved which has a logWarn and a stack trace. Hopefully, this warn log will appear in your logs and you can either figure out what the bad usage is, or tell us where the bug is in our framework if that's really the problem, so we can fix this a better way.

    This is backported and will be available in tomorrow's builds.

    Comment

    Working...
    X