Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions benchmark/grid_perf_bench.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Frame-timing benchmark for VirtualResultGrid. Run in profile mode:
// flutter run --profile -d linux -t benchmark/grid_perf_bench.dart --dart-define=MODE=scroll
// MODE: scroll (horizontal pan) | edit (staged cell edits on a sorted grid)
// | select (mouse-drag selection through ResultsTab, incl. stats)
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
Expand All @@ -15,6 +16,7 @@ import 'package:flutter/scheduler.dart';
import 'package:querya_desktop/core/theme/querya_theme.dart';
import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart';
import 'package:querya_desktop/features/workspace/result_grid_view.dart';
import 'package:querya_desktop/features/workspace/results_tab.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:vm_service/vm_service.dart' as vms;
import 'package:vm_service/vm_service_io.dart' as vms_io;
Expand Down Expand Up @@ -55,14 +57,23 @@ void main() {
runApp(ShadcnApp(
theme: td,
home: material.Scaffold(
body: ListenableBuilder(
listenable: buffer,
builder: (_, __) => VirtualResultGrid(
columns: columns,
rows: buffer.effectiveRows,
stagingBuffer: buffer,
),
),
body: mode == 'select'
? ListenableBuilder(
listenable: buffer,
builder: (_, __) => ResultsTab(
columns: columns,
rows: rows,
stagingBuffer: buffer,
),
)
: ListenableBuilder(
listenable: buffer,
builder: (_, __) => VirtualResultGrid(
columns: columns,
rows: buffer.effectiveRows,
stagingBuffer: buffer,
),
),
),
));
WidgetsBinding.instance.addPostFrameCallback((_) async {
Expand Down Expand Up @@ -97,6 +108,18 @@ Future<void> _tapText(String text) async {

Future<void> _run(DataGridStagingBuffer buffer) async {
if (mode == 'edit') await _tapText('col_0'); // sort ascending
Offset? dragStart;
if (mode == 'select') {
final e = _find((e) => e.widget is Text && (e.widget as Text).data == 'v2_1');
final box = e!.renderObject! as RenderBox;
dragStart = box.localToGlobal(box.size.center(Offset.zero));
GestureBinding.instance.handlePointerEvent(PointerDownEvent(
position: dragStart,
pointer: 7,
kind: PointerDeviceKind.mouse,
buttons: kPrimaryButton,
));
}
if (profile) {
await _connectVm();
await _vm?.clearCpuSamples(_isolateId!);
Expand All @@ -121,11 +144,20 @@ Future<void> _run(DataGridStagingBuffer buffer) async {
final next = p.pixels + dir * 14;
if (next > p.maxScrollExtent || next < 0) dir = -dir;
p.jumpTo((p.pixels + dir * 14).clamp(0.0, p.maxScrollExtent));
} else if (mode == 'select') {
_dragStep(dragStart!, i++);
} else {
buffer.setCell((i * 37) % rowCount, 1 + i % 5, 'edit$i');
i++;
}
}
if (mode == 'select') {
GestureBinding.instance.handlePointerEvent(PointerUpEvent(
position: dragStart!,
pointer: 7,
kind: PointerDeviceKind.mouse,
));
}
await Future<void>.delayed(const Duration(seconds: 1));
_report();
if (profile) await _reportCpu(startMicros);
Expand Down Expand Up @@ -198,3 +230,21 @@ Future<void> _reportCpu(int startMicros) async {
top('self time', self);
top('inclusive time', incl);
}

/// Moves the held mouse pointer to a new cell each frame, zig-zagging down and
/// across, so the drag selection (and its statistics) changes on every frame.
Offset? _lastDrag;

void _dragStep(Offset start, int i) {
final rowsDown = 1 + (i % 40); // sweep 1..40 rows
final colsAcross = 1 + ((i ~/ 40) % 6);
final pos = start + Offset(colsAcross * 110.0, rowsDown * 28.0);
GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
position: pos,
delta: pos - (_lastDrag ?? start),
pointer: 7,
kind: PointerDeviceKind.mouse,
buttons: kPrimaryButton,
));
_lastDrag = pos;
}
175 changes: 110 additions & 65 deletions lib/features/workspace/results_tab.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:async' show unawaited;
import 'dart:async' show Timer, unawaited;

import 'package:flutter/material.dart' as material;
import 'package:flutter/services.dart';
Expand All @@ -22,6 +22,23 @@ enum ResultViewMode {
}

/// Query output: grid, loading, error, or placeholder.
/// Pause after the last selection change before aggregates are recomputed.
const kSelectionStatsDebounce = Duration(milliseconds: 60);

/// The cell that has keyboard focus, shown in the value inspector panel.
@immutable
class _FocusedCell {
const _FocusedCell({
required this.columnName,
required this.value,
required this.rowIndex,
});

final String columnName;
final String value;
final int? rowIndex;
}

class ResultsTab extends material.StatefulWidget {
const ResultsTab({
super.key,
Expand Down Expand Up @@ -59,18 +76,25 @@ class ResultsTab extends material.StatefulWidget {
}

class _ResultsTabState extends material.State<ResultsTab> {
int? _selectedRowIndex;
/// Selection / focus state lives in notifiers so a navigation step rebuilds
/// only the widgets that show it (toolbar, value panel, calc bar), not the
/// whole tab and its grid.
final _selectedRowIndex = material.ValueNotifier<int?>(null);
final _focusedCell = material.ValueNotifier<_FocusedCell?>(null);
final _selectionStats =
material.ValueNotifier<GridCalcStats>(GridCalcStats.empty);
Timer? _statsDebounce;

/// Bumped for every selection change; a stats result is applied only if it
/// still matches, so a slow older aggregation cannot overwrite a newer one.
int _statsSeq = 0;

ResultViewMode _viewMode = ResultViewMode.grid;

bool _showFilterBar = false;
String _filterText = '';

bool _showValuePanel = false;
String? _focusedColumnName;
String? _focusedCellValue;
int? _focusedRowIndex;

GridCalcStats _selectionStats = GridCalcStats.empty;

String? _memoFilterText;
List<String>? _memoColumns;
Expand Down Expand Up @@ -191,8 +215,35 @@ class _ResultsTabState extends material.State<ResultsTab> {
}
}

/// Selection aggregates are recomputed after a short pause, so holding an
/// arrow key or dragging a selection does not run the maths on every step.
void _onSelectionValues(List<String> values) {
final seq = ++_statsSeq;
_statsDebounce?.cancel();
if (values.isEmpty) {
_selectionStats.value = GridCalcStats.empty;
return;
}
_statsDebounce = Timer(kSelectionStatsDebounce, () {
if (values.length < GridSelectionCalcEngine.computeThreshold) {
_selectionStats.value = GridSelectionCalcEngine.compute(values);
return;
}
unawaited(
GridSelectionCalcEngine.computeAdaptive(values).then((stats) {
if (mounted && seq == _statsSeq) _selectionStats.value = stats;
}),
);
});
}

@override
void dispose() {
_statsDebounce?.cancel();
_statsSeq++;
_selectedRowIndex.dispose();
_focusedCell.dispose();
_selectionStats.dispose();
DataGridCommandBridge.instance.unregister();
_memoColumns = null;
_memoEffectiveRows = null;
Expand Down Expand Up @@ -319,11 +370,14 @@ class _ResultsTabState extends material.State<ResultsTab> {
QueryaAnimatedExpand(
expanded: widget.stagingBuffer != null,
child: widget.stagingBuffer != null
? DataGridStagingToolbar(
stagingBuffer: widget.stagingBuffer!,
selectedRowIndex: _selectedRowIndex,
onApplyChanges: widget.onApplyChanges,
isSaving: widget.isSaving,
? material.ValueListenableBuilder<int?>(
valueListenable: _selectedRowIndex,
builder: (_, selectedRow, __) => DataGridStagingToolbar(
stagingBuffer: widget.stagingBuffer!,
selectedRowIndex: selectedRow,
onApplyChanges: widget.onApplyChanges,
isSaving: widget.isSaving,
),
)
: const material.SizedBox.shrink(),
),
Expand Down Expand Up @@ -497,32 +551,14 @@ class _ResultsTabState extends material.State<ResultsTab> {
stagingBuffer: widget.stagingBuffer,
columnDataTypes: widget.columnDataTypes,
rowIndicesMapping: _cachedFilteredIndices,
onRowSelected: (row) => setState(() => _selectedRowIndex = row),
onSelectionValuesChanged: (values) {
if (values.isEmpty) {
setState(() => _selectionStats = GridCalcStats.empty);
return;
}
if (values.length < GridSelectionCalcEngine.computeThreshold) {
setState(() {
_selectionStats = GridSelectionCalcEngine.compute(values);
});
} else {
unawaited(
GridSelectionCalcEngine.computeAdaptive(values).then((stats) {
if (mounted) {
setState(() => _selectionStats = stats);
}
}),
);
}
},
onRowSelected: (row) => _selectedRowIndex.value = row,
onSelectionValuesChanged: _onSelectionValues,
onCellFocused: (colName, cellVal, rowIdx) {
setState(() {
_focusedColumnName = colName;
_focusedCellValue = cellVal;
_focusedRowIndex = rowIdx;
});
_focusedCell.value = _FocusedCell(
columnName: colName,
value: cellVal,
rowIndex: rowIdx,
);
},
onFilterRequested: (filterExpr) {
setState(() {
Expand All @@ -540,34 +576,40 @@ class _ResultsTabState extends material.State<ResultsTab> {
),

// Value Inspector Panel
material.AnimatedSize(
duration: context.motionDuration(QueryaMotion.standard),
curve: context.motionCurve(QueryaMotion.enter),
alignment: material.Alignment.centerRight,
child: (_showValuePanel &&
_focusedColumnName != null &&
_focusedCellValue != null)
? DataGridValuePanel(
columnName: _focusedColumnName!,
cellValue: _focusedCellValue!,
rowIndex: _focusedRowIndex,
onClose: () => setState(() => _showValuePanel = false),
onUpdateValue: widget.stagingBuffer != null &&
_focusedRowIndex != null &&
_focusedColumnName != null
? (newVal) {
final colIdx = widget.columns.indexOf(_focusedColumnName!);
if (colIdx != -1) {
widget.stagingBuffer!.setCell(
_focusedRowIndex!,
colIdx,
newVal,
);
material.ValueListenableBuilder<_FocusedCell?>(
valueListenable: _focusedCell,
builder: (context, focused, _) => material.AnimatedSize(
duration:
context.motionDuration(QueryaMotion.standard),
curve: context.motionCurve(QueryaMotion.enter),
alignment: material.Alignment.centerRight,
child: (_showValuePanel && focused != null)
? DataGridValuePanel(
columnName: focused.columnName,
cellValue: focused.value,
rowIndex: focused.rowIndex,
onClose: () =>
setState(() => _showValuePanel = false),
onUpdateValue: widget.stagingBuffer != null &&
focused.rowIndex != null
? (newVal) {
final colIdx = widget.columns
.indexOf(focused.columnName);
if (colIdx != -1) {
widget.stagingBuffer!.setCell(
focused.rowIndex!,
colIdx,
newVal,
);
}
}
}
: null,
)
: const material.SizedBox(width: 0, height: double.infinity),
: null,
)
: const material.SizedBox(
width: 0,
height: double.infinity,
),
),
),
],
),
Expand All @@ -576,7 +618,10 @@ class _ResultsTabState extends material.State<ResultsTab> {

// Calc Bar Footer
if (_viewMode == ResultViewMode.grid)
DataGridCalcBar(stats: _selectionStats),
material.ValueListenableBuilder<GridCalcStats>(
valueListenable: _selectionStats,
builder: (_, stats, __) => DataGridCalcBar(stats: stats),
),
],
),
);
Expand Down
Loading
Loading