From d68191c6525d97491daace8e57c2b426089b0548 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 30 Aug 2026 20:21:31 +0300 Subject: [PATCH] perf(workspace): harden memory lifecycle, staging buffer disposal, and grid row cache cleanup (close #670) --- .../extensions/extension_table_view.dart | 4 ++++ lib/features/mysql/mysql_sql_workspace.dart | 7 ++++++- .../postgresql/postgres_sql_workspace.dart | 7 ++++++- lib/features/sqlite/sqlite_sql_workspace.dart | 7 ++++++- .../workspace/data_grid_staging_buffer.dart | 8 ++++++++ lib/features/workspace/result_grid_view.dart | 3 +++ lib/features/workspace/results_tab.dart | 8 ++++++++ .../data_grid_staging_buffer_test.dart | 17 +++++++++++++++++ 8 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/features/extensions/extension_table_view.dart b/lib/features/extensions/extension_table_view.dart index 91b459c..9070da9 100644 --- a/lib/features/extensions/extension_table_view.dart +++ b/lib/features/extensions/extension_table_view.dart @@ -124,6 +124,7 @@ class _ExtensionTableViewState extends material.State { _totalRows = null; _filterController.clear(); _filterActive = false; + _stagingBuffer?.dispose(); _stagingBuffer = null; unawaited(_loadPage(refreshCount: true)); } @@ -131,6 +132,8 @@ class _ExtensionTableViewState extends material.State { @override void dispose() { + _stagingBuffer?.dispose(); + _stagingBuffer = null; _filterController.dispose(); super.dispose(); } @@ -208,6 +211,7 @@ class _ExtensionTableViewState extends material.State { _columns = dataResult.columns; _rows = dataResult.rows; _loading = false; + _stagingBuffer?.dispose(); if (!widget.isView && (_capabilities?.supportsMutations == true)) { _stagingBuffer = DataGridStagingBuffer(columns: _columns, rows: _rows); diff --git a/lib/features/mysql/mysql_sql_workspace.dart b/lib/features/mysql/mysql_sql_workspace.dart index bfbd55c..e1f1f3f 100644 --- a/lib/features/mysql/mysql_sql_workspace.dart +++ b/lib/features/mysql/mysql_sql_workspace.dart @@ -149,6 +149,8 @@ class _MysqlSqlWorkspaceState extends material.State { ); } _lease?.release(); + _stagingBuffer?.dispose(); + _stagingBuffer = null; _sqlController.dispose(); super.dispose(); } @@ -244,6 +246,7 @@ class _MysqlSqlWorkspaceState extends material.State { _rows = outRows; _affectedRows = affected; _lastExecutedSql = userSql; + _stagingBuffer?.dispose(); _stagingBuffer = cols.isNotEmpty ? DataGridStagingBuffer(columns: cols, rows: outRows) : null; @@ -325,8 +328,10 @@ class _MysqlSqlWorkspaceState extends material.State { } if (!mounted) return; + final newRows = _stagingBuffer!.effectiveRows; + _stagingBuffer?.dispose(); setState(() { - _rows = _stagingBuffer!.effectiveRows; + _rows = newRows; _stagingBuffer = DataGridStagingBuffer(columns: _columns, rows: _rows); _savingChanges = false; }); diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index b732363..90a9ac6 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -296,6 +296,8 @@ class _PostgresSqlWorkspaceState extends material.State { ); } _dropLease(); + _stagingBuffer?.dispose(); + _stagingBuffer = null; _sqlController.dispose(); super.dispose(); } @@ -388,6 +390,7 @@ class _PostgresSqlWorkspaceState extends material.State { _rows = outRows; _affectedRows = result.affectedRows; _lastExecutedSql = userSql; + _stagingBuffer?.dispose(); _stagingBuffer = cols.isNotEmpty ? DataGridStagingBuffer(columns: cols, rows: outRows) : null; @@ -478,8 +481,10 @@ class _PostgresSqlWorkspaceState extends material.State { await conn.execute(txSql, timeout: to); if (!mounted) return; + final newRows = _stagingBuffer!.effectiveRows; + _stagingBuffer?.dispose(); setState(() { - _rows = _stagingBuffer!.effectiveRows; + _rows = newRows; _stagingBuffer = DataGridStagingBuffer(columns: _columns, rows: _rows); _savingChanges = false; }); diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 737f708..605957d 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -125,6 +125,8 @@ class _SqliteSqlWorkspaceState extends material.State { .removeListener(_appSettingsListener); _topFraction.dispose(); _lease?.release(); + _stagingBuffer?.dispose(); + _stagingBuffer = null; _sqlController.dispose(); super.dispose(); } @@ -206,6 +208,7 @@ class _SqliteSqlWorkspaceState extends material.State { _rows = outRows; _affectedRows = null; _lastExecutedSql = userSql; + _stagingBuffer?.dispose(); _stagingBuffer = cols.isNotEmpty ? DataGridStagingBuffer(columns: cols, rows: outRows) : null; @@ -285,8 +288,10 @@ class _SqliteSqlWorkspaceState extends material.State { } if (!mounted) return; + final newRows = _stagingBuffer!.effectiveRows; + _stagingBuffer?.dispose(); setState(() { - _rows = _stagingBuffer!.effectiveRows; + _rows = newRows; _stagingBuffer = DataGridStagingBuffer(columns: _columns, rows: _rows); _savingChanges = false; }); diff --git a/lib/features/workspace/data_grid_staging_buffer.dart b/lib/features/workspace/data_grid_staging_buffer.dart index bae5c99..85471d3 100644 --- a/lib/features/workspace/data_grid_staging_buffer.dart +++ b/lib/features/workspace/data_grid_staging_buffer.dart @@ -315,5 +315,13 @@ class DataGridStagingBuffer extends ChangeNotifier { } return result; } + + @override + void dispose() { + _modifiedCells.clear(); + _insertedRows.clear(); + _deletedRowIndices.clear(); + super.dispose(); + } } diff --git a/lib/features/workspace/result_grid_view.dart b/lib/features/workspace/result_grid_view.dart index 6732d6c..80afd80 100644 --- a/lib/features/workspace/result_grid_view.dart +++ b/lib/features/workspace/result_grid_view.dart @@ -470,6 +470,9 @@ class _VirtualResultGridState extends material.State { _horizontalController.dispose(); _verticalController.dispose(); _focusNode.dispose(); + _sortedRows = const []; + _columnWidths = const []; + _columnOffsets = const [0]; super.dispose(); } diff --git a/lib/features/workspace/results_tab.dart b/lib/features/workspace/results_tab.dart index 20bd286..3d0b4b6 100644 --- a/lib/features/workspace/results_tab.dart +++ b/lib/features/workspace/results_tab.dart @@ -101,6 +101,14 @@ class _ResultsTabState extends material.State { return filteredRows; } + @override + void dispose() { + _memoColumns = null; + _memoEffectiveRows = null; + _cachedFilteredRows = const []; + super.dispose(); + } + @override Widget build(BuildContext context) { return QueryaFadeSlide( diff --git a/test/features/workspace/data_grid_staging_buffer_test.dart b/test/features/workspace/data_grid_staging_buffer_test.dart index 1406643..1849ae0 100644 --- a/test/features/workspace/data_grid_staging_buffer_test.dart +++ b/test/features/workspace/data_grid_staging_buffer_test.dart @@ -154,5 +154,22 @@ void main() { expect(plan.statements[2].type, MutationType.delete); expect(plan.statements[2].sql, 'DELETE FROM "public"."users" WHERE "id" = 3'); }); + + test('dispose clears internal collections and prevents further listener notifications', () { + buffer.setCell(0, 1, 'Alice Modified'); + buffer.addRow(['4', 'Diana', 'diana@test.com']); + buffer.toggleDeleteRow(2); + + expect(buffer.isDirty, isTrue); + expect(buffer.changeCount, 3); + + buffer.dispose(); + + expect(buffer.isDirty, isFalse); + expect(buffer.changeCount, 0); + expect(buffer.modifiedCellCount, 0); + expect(buffer.insertedRowCount, 0); + expect(buffer.deletedRowCount, 0); + }); }); }