From 298c51b67f8a1fd9e7a23668649005f9e7894c99 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Wed, 26 Aug 2026 12:03:17 +0300 Subject: [PATCH] perf(memory): introduce string interning pool for low-cardinality query results (#604) --- .../database/result_row_string_convert.dart | 106 ++++++++++++++++-- .../result_row_string_convert_test.dart | 100 ++++++++++++++++- 2 files changed, 194 insertions(+), 12 deletions(-) diff --git a/lib/core/database/result_row_string_convert.dart b/lib/core/database/result_row_string_convert.dart index f77ef8b..90d8fab 100644 --- a/lib/core/database/result_row_string_convert.dart +++ b/lib/core/database/result_row_string_convert.dart @@ -3,16 +3,99 @@ import 'package:flutter/foundation.dart'; const int kResultStringConvertYieldEvery = 250; const int kResultStringConvertComputeThreshold = 1000; +/// High-performance string interning pool for deduplicating cell strings +/// across low-cardinality database columns (e.g. booleans, enums, status codes, IDs). +class StringInternPool { + StringInternPool({ + this.maxEntries = 4096, + this.maxStringLength = 128, + }) { + _pool.addAll(_preloaded); + } + + final int maxEntries; + final int maxStringLength; + + static const Map _preloaded = { + 'NULL': 'NULL', + 'true': 'true', + 'false': 'false', + '0': '0', + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '7', + '8': '8', + '9': '9', + '10': '10', + '': '', + 'active': 'active', + 'inactive': 'inactive', + 'pending': 'pending', + 'completed': 'completed', + 'success': 'success', + 'failed': 'failed', + 'error': 'error', + 'warning': 'warning', + 'info': 'info', + 'deleted': 'deleted', + 'draft': 'draft', + 'published': 'published', + }; + + final Map _pool = {}; + + int get size => _pool.length; + + /// Returns the canonical deduplicated instance of [value]. + String intern(String value) { + if (value.length > maxStringLength) { + return value; + } + final existing = _pool[value]; + if (existing != null) return existing; + + if (_pool.length < maxEntries) { + _pool[value] = value; + } + return value; + } + + /// Converts [value] to string and returns the interned canonical instance. + String internObject(Object? value) { + if (value == null) return 'NULL'; + if (value is String) return intern(value); + if (value is bool) return value ? 'true' : 'false'; + if (value is int && value >= 0 && value <= 10) { + return _preloaded[value.toString()] ?? value.toString(); + } + return intern(value.toString()); + } +} + /// Maps null cells to `'NULL'` and others via [Object.toString]. -String resultCellToDisplayString(Object? value) => - value == null ? 'NULL' : value.toString(); +String resultCellToDisplayString(Object? value, [StringInternPool? pool]) { + if (pool != null) { + return pool.internObject(value); + } + if (value == null) return 'NULL'; + if (value is bool) return value ? 'true' : 'false'; + return value.toString(); +} -/// Converts [rowValues] to string rows synchronously. -List> convertResultRowsToStringsSync(List> rowValues) { +/// Converts [rowValues] to string rows synchronously using a string interning pool. +List> convertResultRowsToStringsSync( + List> rowValues, { + StringInternPool? pool, +}) { if (rowValues.isEmpty) return const []; + final activePool = pool ?? StringInternPool(); return [ for (final row in rowValues) - [for (final value in row) resultCellToDisplayString(value)], + [for (final value in row) activePool.internObject(value)], ]; } @@ -20,18 +103,20 @@ List> convertResultRowsToStringsSync(List> rowValues) List> convertResultRowsToStringsCompute(List> rowValues) => convertResultRowsToStringsSync(rowValues); -/// Converts [rowValues] to string rows, yielding periodically. +/// Converts [rowValues] to string rows, yielding periodically and interning strings. Future>> convertResultRowsToStringsYielding( List> rowValues, { int yieldEvery = kResultStringConvertYieldEvery, + StringInternPool? pool, }) async { if (rowValues.isEmpty) return const []; + final activePool = pool ?? StringInternPool(); final out = >[]; for (var i = 0; i < rowValues.length; i++) { final row = rowValues[i]; out.add([ - for (final value in row) resultCellToDisplayString(value), + for (final value in row) activePool.internObject(value), ]); if (yieldEvery > 0 && (i + 1) % yieldEvery == 0) { await Future.delayed(Duration.zero); @@ -46,10 +131,15 @@ Future>> convertResultRowsToStringsAdaptive( List> rowValues, { int computeThreshold = kResultStringConvertComputeThreshold, int yieldEvery = kResultStringConvertYieldEvery, + StringInternPool? pool, }) async { if (rowValues.isEmpty) return const []; if (rowValues.length >= computeThreshold) { return compute(convertResultRowsToStringsCompute, rowValues); } - return convertResultRowsToStringsYielding(rowValues, yieldEvery: yieldEvery); + return convertResultRowsToStringsYielding( + rowValues, + yieldEvery: yieldEvery, + pool: pool, + ); } diff --git a/test/core/database/result_row_string_convert_test.dart b/test/core/database/result_row_string_convert_test.dart index 44d4253..cd9e380 100644 --- a/test/core/database/result_row_string_convert_test.dart +++ b/test/core/database/result_row_string_convert_test.dart @@ -2,6 +2,55 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/database/result_row_string_convert.dart'; void main() { + group('StringInternPool', () { + test('deduplicates identical string instances', () { + final pool = StringInternPool(); + + // Create separate String instances dynamically + final s1 = String.fromCharCodes('active'.codeUnits); + final s2 = String.fromCharCodes('active'.codeUnits); + + expect(identical(s1, s2), isFalse); + + final interned1 = pool.intern(s1); + final interned2 = pool.intern(s2); + + expect(identical(interned1, interned2), isTrue); + }); + + test('preloads common database literals', () { + final pool = StringInternPool(); + + expect(identical(pool.internObject(null), 'NULL'), isTrue); + expect(identical(pool.internObject(true), 'true'), isTrue); + expect(identical(pool.internObject(false), 'false'), isTrue); + expect(identical(pool.internObject(0), '0'), isTrue); + expect(identical(pool.internObject(1), '1'), isTrue); + }); + + test('respects maxStringLength boundary', () { + final pool = StringInternPool(maxStringLength: 10); + const longStr = 'this_is_a_very_long_string_that_should_not_be_interned'; + + final res = pool.intern(longStr); + expect(res, longStr); + // Pool size should not increase for long string + final initialSize = pool.size; + pool.intern(longStr); + expect(pool.size, initialSize); + }); + + test('respects maxEntries capacity limit', () { + final pool = StringInternPool(maxEntries: 30); + + for (var i = 0; i < 50; i++) { + pool.intern('unique_key_$i'); + } + + expect(pool.size, lessThanOrEqualTo(30)); + }); + }); + group('result_row_string_convert', () { final sampleRows = >[ [1, null, 'a'], @@ -13,9 +62,24 @@ void main() { ['2', 'x', 'NULL'], ]; - test('convertResultRowsToStringsSync maps rows correctly', () { - expect(convertResultRowsToStringsSync(sampleRows), expectedOutput); - expect(convertResultRowsToStringsSync(const []), isEmpty); + test('convertResultRowsToStringsSync maps rows correctly and deduplicates repeated cells', () { + final rowsWithDuplicates = >[ + ['active', 1, true, 'US'], + ['active', 1, true, 'US'], + ['active', 2, false, 'EU'], + ]; + + final out = convertResultRowsToStringsSync(rowsWithDuplicates); + expect(out.length, 3); + expect(out[0], ['active', '1', 'true', 'US']); + expect(out[1], ['active', '1', 'true', 'US']); + expect(out[2], ['active', '2', 'false', 'EU']); + + // Deduplicated string references must be identical pointers + expect(identical(out[0][0], out[1][0]), isTrue); + expect(identical(out[0][1], out[1][1]), isTrue); + expect(identical(out[0][2], out[1][2]), isTrue); + expect(identical(out[0][3], out[1][3]), isTrue); }); test('convertResultRowsToStringsCompute maps rows correctly', () { @@ -54,6 +118,34 @@ void main() { expect(out[0], ['0', 'NULL', 'val_0']); expect(out[9], ['9', 'NULL', 'val_9']); }); + + test('benchmark 10,000 low-cardinality rows demonstrates pointer reuse', () { + final statuses = ['active', 'pending', 'cancelled', 'completed']; + final countries = ['US', 'DE', 'FR', 'GB', 'JP']; + + final dataset = List>.generate( + 10000, + (i) => [ + i % 10, + statuses[i % statuses.length], + countries[i % countries.length], + i % 2 == 0, + null, + ], + ); + + final stopwatch = Stopwatch()..start(); + final result = convertResultRowsToStringsSync(dataset); + stopwatch.stop(); + + expect(result.length, 10000); + expect(stopwatch.elapsedMilliseconds, lessThan(100)); + + // Pointer verification + expect(identical(result[0][1], result[4][1]), isTrue); + expect(identical(result[0][2], result[5][2]), isTrue); + expect(identical(result[0][3], result[2][3]), isTrue); + expect(identical(result[0][4], result[1][4]), isTrue); + }); }); } -