Skip to content

Commit 64b75aa

Browse files
authored
Merge pull request #11712 from SubtitleEdit/perf/mkv-read-speed-and-progress
Speed up large MKV reading and add open progress (#6772)
2 parents 3216005 + 592db55 commit 64b75aa

4 files changed

Lines changed: 102 additions & 11 deletions

File tree

src/libse/ContainerFormats/Matroska/MatroskaFile.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,17 @@ public sealed class MatroskaFile : IDisposable
3434
public MatroskaFile(string path)
3535
{
3636
Path = path;
37-
_stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 65536);
37+
38+
// Subtitle/track extraction walks the entire cluster structure but only reads a
39+
// tiny fraction of a (potentially multi-GB) file: it reads each block's small header
40+
// and then seeks past the large video/audio payloads. A big read buffer is
41+
// counter-productive here - because the forward skips are usually smaller than the
42+
// buffer, FileStream keeps refilling contiguously and ends up pulling almost the
43+
// whole file off disk. A small 4 KB (one page) buffer makes the skips fall outside
44+
// the buffer so the skipped payloads are never read, cutting cold-open disk I/O by
45+
// ~4x (e.g. ~700 MB -> ~175 MB on a 1 GB file) and open time several-fold.
46+
// Memory-mapping was measured to be slower here (synchronous page-fault stalls).
47+
_stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096);
3848

3949
// read header
4050
var headerElement = ReadElement();

src/ui/Features/Main/MainViewModel.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14653,20 +14653,20 @@ private async Task<bool> LoadMatroskaSubtitle(
1465314653

1465414654
if (matroskaSubtitleInfo.CodecId.Equals("S_HDMV/TEXTST", StringComparison.OrdinalIgnoreCase))
1465514655
{
14656-
return LoadTextSTFromMatroska(matroskaSubtitleInfo, matroska);
14656+
return await LoadTextSTFromMatroska(matroskaSubtitleInfo, matroska);
1465714657
}
1465814658

1465914659
if (matroskaSubtitleInfo.CodecId.Equals("S_DVBSUB", StringComparison.OrdinalIgnoreCase))
1466014660
{
14661-
return LoadDvbFromMatroska(matroskaSubtitleInfo, matroska, fileName);
14661+
return await LoadDvbFromMatroska(matroskaSubtitleInfo, matroska, fileName);
1466214662
}
1466314663

1466414664
if (matroskaSubtitleInfo.CodecId.Equals("S_VOBSUB", StringComparison.OrdinalIgnoreCase))
1466514665
{
1466614666
return await LoadVobSubFromMatroska(matroskaSubtitleInfo, matroska, fileName);
1466714667
}
1466814668

14669-
var sub = matroska.GetSubtitle(matroskaSubtitleInfo.TrackNumber, null);
14669+
var sub = await ExtractMatroskaSubtitleAsync(matroska, matroskaSubtitleInfo.TrackNumber);
1467014670
var subtitle = new Subtitle();
1467114671
var format = Utilities.LoadMatroskaTextSubtitle(matroskaSubtitleInfo, matroska, sub, subtitle);
1467214672
VideoCloseFile();
@@ -14684,10 +14684,10 @@ private static bool IsImageSubtitleTrack(MatroskaTrackInfo track) =>
1468414684
track.CodecId.Equals("S_DVBSUB", StringComparison.OrdinalIgnoreCase) ||
1468514685
track.CodecId.Equals("S_VOBSUB", StringComparison.OrdinalIgnoreCase);
1468614686

14687-
private bool LoadDvbFromMatroska(MatroskaTrackInfo matroskaSubtitleInfo, MatroskaFile matroska, string fileName)
14687+
private async Task<bool> LoadDvbFromMatroska(MatroskaTrackInfo matroskaSubtitleInfo, MatroskaFile matroska, string fileName)
1468814688
{
1468914689
ShowStatus(Se.Language.Main.ParsingMatroskaFile);
14690-
var sub = matroska.GetSubtitle(matroskaSubtitleInfo.TrackNumber, MatroskaProgress);
14690+
var sub = await ExtractMatroskaSubtitleAsync(matroska, matroskaSubtitleInfo.TrackNumber);
1469114691

1469214692
_subtitle.Paragraphs.Clear();
1469314693
var subtitleImages = new List<DvbSubPes>();
@@ -14805,7 +14805,7 @@ await MessageBox.Show(Window!, Se.Language.General.Error, message, MessageBoxBut
1480514805
return false;
1480614806
}
1480714807

14808-
var sub = matroska.GetSubtitle(matroskaSubtitleInfo.TrackNumber, MatroskaProgress);
14808+
var sub = await ExtractMatroskaSubtitleAsync(matroska, matroskaSubtitleInfo.TrackNumber);
1480914809
_subtitle.Paragraphs.Clear();
1481014810

1481114811
List<VobSubMergedPack> mergedVobSubPacks = [];
@@ -14863,15 +14863,49 @@ await MessageBox.Show(Window!, Se.Language.General.Error, message, MessageBoxBut
1486314863
return false;
1486414864
}
1486514865

14866-
private void MatroskaProgress(long position, long total)
14866+
// Files smaller than this parse fast enough that a progress window would only
14867+
// flash; above it the extraction can take long enough to warrant feedback.
14868+
private const long MatroskaProgressWindowMinFileSize = 25 * 1024 * 1024; // 25 MB
14869+
14870+
/// <summary>
14871+
/// Extracts a Matroska track's blocks on a background thread so the UI stays
14872+
/// responsive, showing a determinate "Please wait..." window with percentage
14873+
/// progress for large files. Must be called on the UI thread.
14874+
/// </summary>
14875+
private async Task<List<MatroskaSubtitle>> ExtractMatroskaSubtitleAsync(MatroskaFile matroska, int trackNumber)
1486714876
{
14868-
// UpdateProgress(position, total, _language.ParsingMatroskaFile);
14877+
PleaseWaitViewModel? pleaseWaitVm = null;
14878+
long fileSize = 0;
14879+
try
14880+
{
14881+
fileSize = new FileInfo(matroska.Path).Length;
14882+
}
14883+
catch
14884+
{
14885+
// ignore - just means no size-based gating
14886+
}
14887+
14888+
if (fileSize >= MatroskaProgressWindowMinFileSize)
14889+
{
14890+
pleaseWaitVm = _windowService.ShowWindow<PleaseWaitWindow, PleaseWaitViewModel>(Window!);
14891+
pleaseWaitVm.StatusText = Se.Language.Main.ParsingMatroskaFile;
14892+
}
14893+
14894+
try
14895+
{
14896+
var vm = pleaseWaitVm;
14897+
return await Task.Run(() => matroska.GetSubtitle(trackNumber, (pos, total) => vm?.ReportProgress(pos, total)));
14898+
}
14899+
finally
14900+
{
14901+
pleaseWaitVm?.Close();
14902+
}
1486914903
}
1487014904

14871-
private bool LoadTextSTFromMatroska(MatroskaTrackInfo matroskaSubtitleInfo, MatroskaFile matroska)
14905+
private async Task<bool> LoadTextSTFromMatroska(MatroskaTrackInfo matroskaSubtitleInfo, MatroskaFile matroska)
1487214906
{
1487314907
ShowStatus(Se.Language.Main.ParsingMatroskaFile);
14874-
var sub = matroska.GetSubtitle(matroskaSubtitleInfo.TrackNumber, MatroskaProgress);
14908+
var sub = await ExtractMatroskaSubtitleAsync(matroska, matroskaSubtitleInfo.TrackNumber);
1487514909

1487614910
VideoCloseFile();
1487714911
_subtitle.Paragraphs.Clear();

src/ui/Features/Shared/PleaseWaitViewModel.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ namespace Nikse.SubtitleEdit.Features.Shared;
1515
public partial class PleaseWaitViewModel : ObservableObject
1616
{
1717
[ObservableProperty] private string _statusText;
18+
[ObservableProperty] private double _progressValue;
19+
[ObservableProperty] private bool _isIndeterminate = true;
20+
21+
private int _lastPercent = -1;
1822

1923
public Window? Window { get; set; }
2024

@@ -23,6 +27,45 @@ public PleaseWaitViewModel()
2327
_statusText = Se.Language.General.PleaseWait;
2428
}
2529

30+
/// <summary>
31+
/// Switches the progress bar to determinate mode and updates it (0-100).
32+
/// Safe to call from a background thread; updates are throttled to whole
33+
/// percent changes so a tight progress callback does not flood the UI thread.
34+
/// </summary>
35+
public void ReportProgress(long position, long total, string? statusText = null)
36+
{
37+
if (total <= 0)
38+
{
39+
return;
40+
}
41+
42+
var percent = (int)(position * 100 / total);
43+
if (percent < 0)
44+
{
45+
percent = 0;
46+
}
47+
else if (percent > 100)
48+
{
49+
percent = 100;
50+
}
51+
52+
if (percent == _lastPercent)
53+
{
54+
return;
55+
}
56+
57+
_lastPercent = percent;
58+
Dispatcher.UIThread.Post(() =>
59+
{
60+
IsIndeterminate = false;
61+
ProgressValue = percent;
62+
if (statusText != null)
63+
{
64+
StatusText = statusText;
65+
}
66+
});
67+
}
68+
2669
public void Close()
2770
{
2871
Dispatcher.UIThread.Post(() => Window?.Close());

src/ui/Features/Shared/PleaseWaitWindow.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ public PleaseWaitWindow(PleaseWaitViewModel vm)
3838
var progressBar = new ProgressBar
3939
{
4040
IsIndeterminate = true,
41+
Minimum = 0,
42+
Maximum = 100,
4143
HorizontalAlignment = HorizontalAlignment.Stretch,
4244
Height = 8,
4345
};
46+
progressBar.Bind(ProgressBar.IsIndeterminateProperty, new Binding(nameof(vm.IsIndeterminate)));
47+
progressBar.Bind(ProgressBar.ValueProperty, new Binding(nameof(vm.ProgressValue)));
4448

4549
Content = new StackPanel
4650
{

0 commit comments

Comments
 (0)