Skip to content

Commit b9f21d2

Browse files
authored
Improve indexing performance (#60)
* fix a lot of analyzer warnings and suggestions * performance optimisations and benchmarking * make indexing async * add a progress bar for indexing
1 parent c96afe5 commit b9f21d2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+354
-208
lines changed

AvaGui/Models/IndexObjectHeader.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.

AvaGui/Models/ObjectEditorModel.cs

Lines changed: 29 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public class ObjectEditorModel
5454
public ObjectEditorModel()
5555
{
5656
Logger = new Logger();
57-
LoggerObservableLogs = new ObservableCollection<LogLine>(((Logger)Logger).Logs);
57+
LoggerObservableLogs = [];
58+
Logger.LogAdded += (sender, laea) => LoggerObservableLogs.Add(laea.Log);
5859

5960
LoadSettings(SettingsFile, Logger);
6061
}
@@ -76,15 +77,13 @@ public void LoadSettings(string settingsFile, ILogger? logger)
7677

7778
Settings = settings!;
7879

79-
if (!ValidateSettings(Settings, logger))
80+
if (ValidateSettings(Settings, logger))
8081
{
81-
return;
82-
}
83-
84-
if (File.Exists(Settings.GetObjDataFullPath(Settings.IndexFileName)))
85-
{
86-
logger?.Info($"Loading header index from \"{Settings.IndexFileName}\"");
87-
LoadObjDirectory(Settings.ObjDataDirectory, new Progress<float>(), true);
82+
if (File.Exists(Settings.GetObjDataFullPath(Settings.IndexFileName)))
83+
{
84+
Logger?.Info($"Loading header index from \"{Settings.IndexFileName}\"");
85+
LoadObjDirectoryAsync(Settings.ObjDataDirectory, new Progress<float>(), true).Wait();
86+
}
8887
}
8988
}
9089

@@ -159,70 +158,29 @@ public bool TryLoadObject(string filename, out UiLocoFile? uiLocoFile)
159158
}
160159

161160
// this method loads every single object entirely. it takes a long time to run
162-
void CreateIndex(string[] allFiles, IProgress<float>? progress)
161+
async Task CreateIndex(string[] allFiles, IProgress<float> progress)
163162
{
164163
Logger?.Info($"Creating index on {allFiles.Length} files");
165164

166-
ConcurrentDictionary<string, IndexObjectHeader> ccHeaderIndex = new(); // key is full path/filename
167-
168-
var count = 0;
169-
ConcurrentDictionary<string, TimeSpan> timePerFile = new();
170-
171165
var sw = new Stopwatch();
172166
sw.Start();
173167

174168
var fileCount = allFiles.Length;
175-
176-
_ = Parallel.ForEach(allFiles, new ParallelOptions() { MaxDegreeOfParallelism = 16 }, (filename)
177-
=> count = LoadAndIndexFile(count, filename));
178-
179-
HeaderIndex = ccHeaderIndex.OrderBy(kvp => kvp.Key).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
169+
var index = await SawyerStreamReader.FastIndexAsync(allFiles, progress);
170+
171+
HeaderIndex = index.ToDictionary(
172+
x => x.filename,
173+
x => new ObjectIndexModel(
174+
x.s5.Name,
175+
DatFileType.Object,
176+
x.s5.ObjectType,
177+
x.s5.SourceGame,
178+
x.s5.Checksum,
179+
x.VehicleType)
180+
);
180181

181182
sw.Stop();
182-
Logger?.Info("Finished creating index");
183-
Logger?.Debug($"Time time={sw.Elapsed}");
184-
185-
if (timePerFile.IsEmpty)
186-
{
187-
_ = timePerFile.TryAdd("<no items>", TimeSpan.Zero);
188-
}
189-
190-
var slowest = timePerFile.MaxBy(x => x.Value.Ticks);
191-
Logger?.Debug($"Slowest file={slowest.Key} Time={slowest.Value}");
192-
193-
var average = timePerFile.Average(x => x.Value.TotalMilliseconds);
194-
Logger?.Debug($"Average time={average}ms");
195-
196-
var median = timePerFile.OrderBy(x => x.Value).Skip(timePerFile.Count / 2).Take(1).Single();
197-
Logger?.Debug($"Median time={median.Value}ms");
198-
199-
int LoadAndIndexFile(int count, string filename)
200-
{
201-
var startTime = sw.Elapsed;
202-
var loadResult = TryLoadObject(filename, out var uiLocoFile);
203-
var elapsed = sw.Elapsed - startTime;
204-
205-
if (loadResult && uiLocoFile != null)
206-
{
207-
_ = ccHeaderIndex.TryAdd(filename, new IndexObjectHeader(
208-
uiLocoFile.DatFileInfo.S5Header.Name,
209-
DatFileType.Object,
210-
uiLocoFile.DatFileInfo.S5Header.ObjectType,
211-
uiLocoFile.DatFileInfo.S5Header.SourceGame,
212-
uiLocoFile.DatFileInfo.S5Header.Checksum,
213-
uiLocoFile.LocoObject.Object is VehicleObject veh ? veh.Type : null));
214-
215-
_ = timePerFile.TryAdd(uiLocoFile.DatFileInfo.S5Header.Name, elapsed);
216-
}
217-
else
218-
{
219-
Logger?.Error($"Failed to load \"{filename}\"");
220-
}
221-
222-
_ = Interlocked.Increment(ref count);
223-
progress?.Report((float)count / fileCount);
224-
return count;
225-
}
183+
Logger?.Info($"Indexed {fileCount} in {sw.Elapsed}");
226184
}
227185

228186
public void SaveFile(string path, UiLocoFile obj)
@@ -275,7 +233,7 @@ void LoadKnownData(HashSet<string> allFilesInDir, HashSet<string> knownFilenames
275233

276234
//LoadPalette(); // update palette from g1
277235

278-
SaveSettings();
236+
//await SaveSettings();
279237

280238
return true;
281239
}
@@ -286,19 +244,14 @@ void LoadKnownData(HashSet<string> allFilesInDir, HashSet<string> knownFilenames
286244
// var allFiles = Directory.GetFiles(directory, "*.dat|*.sv5|*.sc5", SearchOption.AllDirectories);
287245
//}
288246

289-
public async Task LoadObjDirectoryAsync(string directory, IProgress<float>? progress, bool useExistingIndex)
290-
{
291-
await Task.Run(() => LoadObjDirectory(directory, progress, useExistingIndex));
292-
await Task.Run(SaveSettings);
293-
}
294-
295-
public void LoadObjDirectory(string directory)
296-
=> LoadObjDirectory(directory, null, true);
247+
//public void LoadObjDirectory(string directory)
248+
// => LoadObjDirectory(directory, new Progress<float>(), true);
297249

298-
public void LoadObjDirectory(string directory, IProgress<float>? progress, bool useExistingIndex)
250+
public async Task LoadObjDirectoryAsync(string directory, IProgress<float> progress, bool useExistingIndex)
299251
{
300-
if (!Directory.Exists(directory))
252+
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory) || progress == null)
301253
{
254+
Logger?.Error($"Couldn't start loading obj dir: {directory}");
302255
return;
303256
}
304257

@@ -324,7 +277,7 @@ public void LoadObjDirectory(string directory, IProgress<float>? progress, bool
324277
else
325278
{
326279
Logger?.Info("Recreating index file");
327-
CreateIndex(allFiles, progress); // do we need the array?
280+
await CreateIndex(allFiles, progress); // do we need the array?
328281
SerialiseHeaderIndexToFile(Settings.GetObjDataFullPath(Settings.IndexFileName), HeaderIndex, GetOptions());
329282
}
330283

AvaGui/Models/ObjectIndexModel.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using OpenLoco.ObjectEditor.Data;
2+
using OpenLoco.ObjectEditor.Headers;
3+
using OpenLoco.ObjectEditor.Objects;
4+
5+
namespace AvaGui.Models
6+
{
7+
public record ObjectIndexModel(string Name, DatFileType DatFileType, ObjectType ObjectType, SourceGame SourceGame, uint32_t Checksum, VehicleType? VehicleType);
8+
}

AvaGui/Models/ObjectTypeToMaterialIconConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace AvaGui.Models
99
{
1010
public class ObjectTypeToMaterialIconConverter : IValueConverter
1111
{
12-
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
12+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
1313
{
1414
if (Enum.TryParse<DatFileType>(value as string, out var datType) && DatTypeMapping.TryGetValue(datType, out var datIcon))
1515
{

AvaGui/ViewLocator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ namespace AvaGui
77
{
88
public class ViewLocator : IDataTemplate
99
{
10-
public Control Build(object data)
10+
public Control Build(object? data)
1111
{
12+
if (data == null)
13+
{
14+
return new TextBlock { Text = "<object passed in was null>" };
15+
}
16+
1217
var name = data.GetType().FullName!.Replace("ViewModel", "View");
1318
var type = Type.GetType(name);
1419

@@ -20,7 +25,7 @@ public Control Build(object data)
2025
return new TextBlock { Text = "Not Found: " + name };
2126
}
2227

23-
public bool Match(object data)
28+
public bool Match(object? data)
2429
=> data is ViewModelBase;
2530
}
2631
}

AvaGui/ViewModels/DatTypes/ObjectEditorViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class ObjectEditorViewModel : ReactiveObject, ILocoFileViewModel
2828
public UiLocoFile? CurrentObject { private set; get; }
2929

3030
[Reactive]
31-
public FileSystemItemBase CurrentFile { get; set; }
31+
public FileSystemItemBase? CurrentFile { get; set; }
3232

3333
public ObjectEditorViewModel(ObjectEditorModel model)
3434
{

AvaGui/ViewModels/FolderTreeViewModel.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using OpenLoco.ObjectEditor.Data;
1010
using ReactiveUI.Fody.Helpers;
1111
using System.Reactive;
12+
using System.Threading.Tasks;
13+
using System.Diagnostics;
1214

1315
namespace AvaGui.ViewModels
1416
{
@@ -19,23 +21,28 @@ public class FolderTreeViewModel : ReactiveObject
1921
public FolderTreeViewModel(ObjectEditorModel model)
2022
{
2123
Model = model;
24+
Progress = new();
25+
Progress.ProgressChanged += (a, b) =>
26+
{
27+
IndexingProgress = b;
28+
};
2229

23-
RecreateIndex = ReactiveCommand.Create(() => LoadObjDirectory(CurrentDirectory, false));
30+
RecreateIndex = ReactiveCommand.Create(async () => await LoadObjDirectoryAsync(CurrentDirectory, false));
2431

2532
_ = this.WhenAnyValue(o => o.CurrentDirectory)
26-
.Subscribe(_ => LoadObjDirectory(CurrentDirectory, true));
33+
.Subscribe(async _ => await LoadObjDirectoryAsync(CurrentDirectory, true));
2734
_ = this.WhenAnyValue(o => o.DisplayVanillaOnly)
28-
.Subscribe(_ => LoadObjDirectory(CurrentDirectory, true));
35+
.Subscribe(async _ => await LoadObjDirectoryAsync(CurrentDirectory, true));
2936
_ = this.WhenAnyValue(o => o.FilenameFilter)
3037
.Throttle(TimeSpan.FromMilliseconds(500))
31-
.Subscribe(_ => LoadObjDirectory(CurrentDirectory, true));
38+
.Subscribe(async _ => await LoadObjDirectoryAsync(CurrentDirectory, true));
3239
_ = this.WhenAnyValue(o => o.DirectoryItems)
3340
.Subscribe(_ => this.RaisePropertyChanged(nameof(DirectoryFileCount)));
3441

3542
// loads the last-viewed folder
3643
CurrentDirectory = Model.Settings.ObjDataDirectory;
3744
}
38-
public ReactiveCommand<Unit, Unit> RecreateIndex { get; }
45+
public ReactiveCommand<Unit, Task> RecreateIndex { get; }
3946

4047
[Reactive]
4148
public string CurrentDirectory { get; set; } = string.Empty;
@@ -50,36 +57,43 @@ public FolderTreeViewModel(ObjectEditorModel model)
5057
public bool DisplayVanillaOnly { get; set; }
5158

5259
[Reactive]
53-
public ObservableCollection<FileSystemItemBase> DirectoryItems { get; }
60+
public ObservableCollection<FileSystemItemBase> DirectoryItems { get; private set; }
61+
62+
Progress<float> Progress { get; set; }
63+
64+
[Reactive]
65+
public float IndexingProgress { get; set; }
5466

55-
private void LoadObjDirectory(string directory, bool useExistingIndex)
67+
private async Task LoadObjDirectoryAsync(string directory, bool useExistingIndex)
5668
{
57-
DirectoryItems = new(LoadObjDirectoryCore(directory, useExistingIndex));
69+
DirectoryItems = new(await LoadObjDirectoryCoreAsync(directory, useExistingIndex));
5870

5971
// really just for debugging - puts all dat file types in the collection, even if they don't have anything in them
6072
//foreach (var dat in Enum.GetValues<DatFileType>().Except(DirectoryItems.Select(x => ((FileSystemDatGroup)x).DatFileType)))
6173
//{
6274
// DirectoryItems.Add(new FileSystemDatGroup(string.Empty, dat, new ObservableCollection<FileSystemItemBase>()));
6375
//}
6476

65-
IEnumerable<FileSystemItemBase> LoadObjDirectoryCore(string directory, bool useExistingIndex)
77+
async Task<List<FileSystemItemBase>> LoadObjDirectoryCoreAsync(string directory, bool useExistingIndex)
6678
{
79+
var result = new List<FileSystemItemBase>();
80+
6781
if (string.IsNullOrEmpty(directory))
6882
{
69-
yield break;
83+
return result;
7084
}
7185

7286
var dirInfo = new DirectoryInfo(directory);
7387

7488
if (!dirInfo.Exists)
7589
{
76-
yield break;
90+
return result;
7791
}
7892

7993
// todo: load each file
8094
// check if its object, scenario, save, landscape, g1, sfx, tutorial, etc
8195

82-
Model.LoadObjDirectory(directory, null, useExistingIndex);
96+
await Model.LoadObjDirectoryAsync(directory, Progress, useExistingIndex);
8397

8498
var groupedDatObjects = Model.HeaderIndex
8599
.Where(o => (string.IsNullOrEmpty(FilenameFilter) || o.Value.Name.Contains(FilenameFilter, StringComparison.CurrentCultureIgnoreCase)) && (!DisplayVanillaOnly || o.Value.SourceGame == SourceGame.Vanilla))
@@ -135,11 +149,13 @@ IEnumerable<FileSystemItemBase> LoadObjDirectoryCore(string directory, bool useE
135149
subNodes));
136150
}
137151

138-
yield return new FileSystemDatGroup(
152+
result.Add(new FileSystemDatGroup(
139153
string.Empty,
140154
datObjGroup.Key,
141-
groups);
155+
groups));
142156
}
157+
158+
return result;
143159
}
144160
}
145161

AvaGui/ViewModels/MainWindowViewModel.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
global using HeaderIndex = System.Collections.Generic.Dictionary<string, AvaGui.Models.IndexObjectHeader>;
1+
global using HeaderIndex = System.Collections.Generic.Dictionary<string, AvaGui.Models.ObjectIndexModel>;
22
using Avalonia;
33
using AvaGui.Models;
44
using ReactiveUI;
@@ -62,10 +62,10 @@ public class MainWindowViewModel : ViewModelBase
6262
public string WindowTitle => $"{ObjectEditorModel.ApplicationName} - {ApplicationVersion} ({LatestVersionText})";
6363

6464
[Reactive]
65-
Version ApplicationVersion { get; }
65+
public Version ApplicationVersion { get; set; }
6666

6767
[Reactive]
68-
string LatestVersionText { get; } = "Up-to-date";
68+
public string LatestVersionText { get; set; } = "Up-to-date";
6969

7070
public MainWindowViewModel()
7171
{
@@ -190,7 +190,9 @@ public async Task SelectNewFolder()
190190
var dirPath = dir.Path.LocalPath;
191191
if (Directory.Exists(dirPath) && !Model.Settings.ObjDataDirectories.Contains(dirPath))
192192
{
193-
await Model.LoadObjDirectoryAsync(dirPath, null, false);
193+
FolderTreeViewModel.CurrentDirectory = dirPath; // this will cause the reindexing
194+
//var progress = new Progress<float>();
195+
//await Model.LoadObjDirectoryAsync(dirPath, progress, false);
194196
var menuItem = new MenuItemModel(
195197
dirPath,
196198
ReactiveCommand.Create(() => FolderTreeViewModel.CurrentDirectory = dirPath)

0 commit comments

Comments
 (0)