Skip to content

Commit 6da4dbf

Browse files
committed
"merge-perf"
2 parents f0a5612 + bcb36b1 commit 6da4dbf

11 files changed

Lines changed: 1494 additions & 1370 deletions

‎lib/core/services/diff_calculator.dart‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,42 @@ import 'package:localizer_app_main/data/models/app_settings.dart';
66
/// Progress callback for reporting diff calculation progress.
77
typedef DiffProgressCallback = void Function(int processed, int total);
88

9+
/// Cache for string similarity calculations to avoid redundant computation.
10+
/// This is especially important for Smart Match mode which can have O(n²) comparisons.
11+
class _SimilarityCache {
12+
final Map<String, double> _cache = {};
13+
static const int _maxSize = 50000;
14+
15+
String _makeKey(String a, String b) {
16+
// Ensure consistent key regardless of order
17+
return a.hashCode < b.hashCode ? '$a\n---\n$b' : '$b\n---\n$a';
18+
}
19+
20+
double? get(String a, String b) => _cache[_makeKey(a, b)];
21+
22+
void set(String a, String b, double similarity) {
23+
if (_cache.length >= _maxSize) {
24+
// Clear half the cache when full
25+
final keysToRemove = _cache.keys.take(_maxSize ~/ 2).toList();
26+
for (final key in keysToRemove) {
27+
_cache.remove(key);
28+
}
29+
}
30+
_cache[_makeKey(a, b)] = similarity;
31+
}
32+
33+
void clear() => _cache.clear();
34+
}
35+
936
class DiffCalculator {
37+
// Similarity cache for avoiding redundant calculations
38+
static final _similarityCache = _SimilarityCache();
39+
40+
/// Clears the similarity cache. Call when starting a new comparison.
41+
static void clearCache() {
42+
_similarityCache.clear();
43+
}
44+
1045
/// Calculates diff between two data maps based on the comparison mode.
1146
///
1247
/// - `Key-based`: Matches entries by key name (traditional approach).
@@ -20,6 +55,9 @@ class DiffCalculator {
2055
required AppSettings settings,
2156
DiffProgressCallback? onProgress,
2257
}) {
58+
// Clear cache at start of new comparison
59+
_similarityCache.clear();
60+
2361
final mode = settings.comparisonMode;
2462

2563
switch (mode) {
@@ -214,6 +252,7 @@ class DiffCalculator {
214252
.toList();
215253

216254
// Phase 3: Try to match by value similarity (detect renames)
255+
// Uses cached similarity to avoid O(n²) redundant calculations
217256
final usedKeys2 = <String>{};
218257
for (final key1 in unmatchedKeys1) {
219258
String? value1 =
@@ -227,7 +266,8 @@ class DiffCalculator {
227266
_normalizeValue(targetData[key2], ignoreCase, ignoreWhitespace);
228267

229268
if (value1 != null && value2 != null) {
230-
final similarity = value1.similarityTo(value2);
269+
// Use cached similarity calculation
270+
final similarity = _getCachedSimilarity(value1, value2);
231271
if (similarity >= similarityThreshold &&
232272
similarity > bestSimilarity) {
233273
bestSimilarity = similarity;
@@ -292,10 +332,24 @@ class DiffCalculator {
292332
} else if (value1 != null && value2 == null) {
293333
return ComparisonStatusDetail.removed();
294334
} else if (value1 != value2) {
295-
final similarityScore = value1!.similarityTo(value2!);
335+
final similarityScore = _getCachedSimilarity(value1!, value2!);
296336
return ComparisonStatusDetail.modified(similarityScore);
297337
} else {
298338
return ComparisonStatusDetail.identical();
299339
}
300340
}
341+
342+
/// Gets similarity score with caching to avoid redundant calculations.
343+
static double _getCachedSimilarity(String value1, String value2) {
344+
// Check cache first
345+
final cached = _similarityCache.get(value1, value2);
346+
if (cached != null) {
347+
return cached;
348+
}
349+
350+
// Calculate and cache
351+
final similarity = value1.similarityTo(value2);
352+
_similarityCache.set(value1, value2, similarity);
353+
return similarity;
354+
}
301355
}

‎lib/core/services/file_cache_service.dart‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import 'dart:io';
2+
import 'dart:collection';
23

34
class _CachedFile {
45
final Map<String, String> data;
56
final DateTime modifiedTime;
7+
DateTime lastAccessed;
68

7-
_CachedFile(this.data, this.modifiedTime);
9+
_CachedFile(this.data, this.modifiedTime) : lastAccessed = DateTime.now();
10+
11+
void touch() {
12+
lastAccessed = DateTime.now();
13+
}
814
}
915

16+
/// File cache service with LRU eviction policy.
17+
///
18+
/// Caches parsed file data to avoid re-parsing unchanged files.
19+
/// Uses LRU (Least Recently Used) eviction when cache is full.
1020
class FileCacheService {
1121
static final FileCacheService _instance = FileCacheService._internal();
1222
factory FileCacheService() => _instance;
1323
FileCacheService._internal();
1424

15-
final Map<String, _CachedFile> _cache = {};
16-
static const int _maxCacheSize = 10;
25+
// LinkedHashMap maintains insertion order for efficient LRU
26+
final LinkedHashMap<String, _CachedFile> _cache = LinkedHashMap();
27+
static const int _maxCacheSize = 50; // Increased from 10 for better hit rate
1728

1829
Future<Map<String, String>?> getCached(File file, {String? keySuffix}) async {
1930
final key = file.path + (keySuffix ?? '');
@@ -29,6 +40,10 @@ class FileCacheService {
2940
final stat = await file.stat();
3041
// Truncate to seconds for safety
3142
if (stat.modified.difference(cached.modifiedTime).abs().inSeconds < 2) {
43+
// Move to end (most recently used) for LRU
44+
_cache.remove(key);
45+
cached.touch();
46+
_cache[key] = cached;
3247
return cached.data;
3348
}
3449
} catch (e) {
@@ -41,15 +56,26 @@ class FileCacheService {
4156

4257
void cache(File file, Map<String, String> data, DateTime modifiedTime,
4358
{String? keySuffix}) {
44-
// Evict oldest if full
45-
if (_cache.length >= _maxCacheSize) {
59+
final key = file.path + (keySuffix ?? '');
60+
61+
// If key exists, remove it first to update position
62+
_cache.remove(key);
63+
64+
// Evict LRU entries if full (remove from front)
65+
while (_cache.length >= _maxCacheSize) {
4666
_cache.remove(_cache.keys.first);
4767
}
48-
final key = file.path + (keySuffix ?? '');
68+
4969
_cache[key] = _CachedFile(data, modifiedTime);
5070
}
5171

5272
void clear() {
5373
_cache.clear();
5474
}
75+
76+
/// Returns cache statistics for debugging
77+
Map<String, dynamic> get stats => {
78+
'size': _cache.length,
79+
'maxSize': _maxCacheSize,
80+
};
5581
}

‎lib/core/utils/diff_utils.dart‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,46 @@ import 'package:diff_match_patch/diff_match_patch.dart';
44
enum DiffRenderMode { unified, sourceOnly, targetOnly }
55

66
/// Utility for calculating and rendering granular diffs.
7+
///
8+
/// Includes caching of diff calculations to avoid redundant computation
9+
/// when the same text pairs are compared multiple times (e.g., during scrolling).
710
class DiffUtils {
811
static final _dmp = DiffMatchPatch();
912

13+
// Cache for diff results to avoid recomputation during scrolling/rebuilds
14+
static final Map<String, List<Diff>> _diffCache = {};
15+
static const int _maxCacheSize = 1000;
16+
17+
/// Clears the diff cache. Call when comparison data changes.
18+
static void clearCache() {
19+
_diffCache.clear();
20+
}
21+
22+
/// Gets or computes diffs between two texts with caching.
23+
static List<Diff> _getCachedDiffs(String text1, String text2) {
24+
final cacheKey = '$text1\n---DIFF---\n$text2';
25+
26+
final cached = _diffCache[cacheKey];
27+
if (cached != null) {
28+
return cached;
29+
}
30+
31+
final diffs = _dmp.diff(text1, text2);
32+
_dmp.diffCleanupSemantic(diffs);
33+
34+
// Manage cache size with simple eviction
35+
if (_diffCache.length >= _maxCacheSize) {
36+
// Remove oldest entries (first 200)
37+
final keysToRemove = _diffCache.keys.take(200).toList();
38+
for (final key in keysToRemove) {
39+
_diffCache.remove(key);
40+
}
41+
}
42+
43+
_diffCache[cacheKey] = diffs;
44+
return diffs;
45+
}
46+
1047
/// Calculates the difference between [text1] (old) and [text2] (new)
1148
/// and returns a list of [TextSpan]s for rendering.
1249
static List<TextSpan> getDiffSpans({
@@ -36,8 +73,7 @@ class DiffUtils {
3673
return [TextSpan(text: text2, style: baseStyle)];
3774
}
3875

39-
final diffs = _dmp.diff(text1, text2);
40-
_dmp.diffCleanupSemantic(diffs);
76+
final diffs = _getCachedDiffs(text1, text2);
4177

4278
final spans = <TextSpan>[];
4379
final addColor = addedColor ?? Colors.green.withValues(alpha: 0.25);
@@ -56,7 +92,6 @@ class DiffUtils {
5692
style: baseStyle.copyWith(
5793
backgroundColor: addColor,
5894
fontWeight: FontWeight.bold,
59-
// If unified, maybe underline?
6095
),
6196
),
6297
);
@@ -70,8 +105,7 @@ class DiffUtils {
70105
style: baseStyle.copyWith(
71106
backgroundColor: delColor,
72107
decoration: TextDecoration.lineThrough,
73-
// Make deleted text slightly lighter
74-
color: baseStyle.color?.withOpacity(0.7),
108+
color: baseStyle.color?.withValues(alpha: 0.7),
75109
),
76110
),
77111
);
Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// TODO: Implement with Hive for local persistence
2+
import 'dart:collection';
23
import 'package:flutter/foundation.dart';
34

45
abstract class TranslationCache {
@@ -9,38 +10,58 @@ abstract class TranslationCache {
910
Future<void> clearCache();
1011
}
1112

13+
/// In-memory translation cache with LRU eviction.
14+
///
15+
/// Caches translation results to avoid redundant API calls.
16+
/// Uses LRU (Least Recently Used) eviction when cache is full.
1217
class LocalTranslationCache implements TranslationCache {
13-
// This will use Hive for storage.
14-
// For MVP, we can use a simple in-memory map or return null.
15-
final Map<String, String> _cache =
16-
{}; // Key: "sourceLang:targetLang:sourceText"
18+
// LinkedHashMap maintains insertion order for efficient LRU
19+
final LinkedHashMap<String, String> _cache = LinkedHashMap();
20+
21+
/// Maximum number of cached translations (prevents unbounded memory growth)
22+
static const int _maxCacheSize = 5000;
1723

1824
@override
1925
Future<String?> getCachedTranslation(
2026
String sourceText, String sourceLang, String targetLang) async {
21-
// TODO: Implement Hive retrieval with expiration logic
2227
final key = '$sourceLang:$targetLang:$sourceText';
2328
final cached = _cache[key];
2429
if (cached != null) {
25-
debugPrint('Translation found in in-memory cache for key: $key');
26-
// TODO: Check timestamp if implementing expiration with in-memory for now
30+
// Move to end (most recently used) for LRU
31+
_cache.remove(key);
32+
_cache[key] = cached;
33+
debugPrint('Translation cache hit for: ${key.substring(0, key.length.clamp(0, 50))}...');
2734
}
2835
return cached;
2936
}
3037

3138
@override
3239
Future<void> cacheTranslation(String sourceText, String sourceLang,
3340
String targetLang, String translatedText) async {
34-
// TODO: Implement Hive storage with timestamp for expiration
3541
final key = '$sourceLang:$targetLang:$sourceText';
42+
43+
// Remove if exists to update position
44+
_cache.remove(key);
45+
46+
// Evict LRU entries if full
47+
while (_cache.length >= _maxCacheSize) {
48+
final oldestKey = _cache.keys.first;
49+
_cache.remove(oldestKey);
50+
}
51+
3652
_cache[key] = translatedText;
37-
debugPrint('Translation cached in-memory for key: $key');
53+
debugPrint('Translation cached (${_cache.length}/$_maxCacheSize entries)');
3854
}
3955

4056
@override
4157
Future<void> clearCache() async {
42-
// TODO: Implement Hive clear
4358
_cache.clear();
44-
debugPrint('In-memory translation cache cleared');
59+
debugPrint('Translation cache cleared');
4560
}
61+
62+
/// Returns cache statistics for debugging
63+
Map<String, dynamic> get stats => {
64+
'size': _cache.length,
65+
'maxSize': _maxCacheSize,
66+
};
4667
}

0 commit comments

Comments
 (0)