diff --git a/BililiveRecorder.Core/Event/IOStatsEventArgs.cs b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs new file mode 100644 index 0000000..428510a --- /dev/null +++ b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs @@ -0,0 +1,29 @@ +using System; + +namespace BililiveRecorder.Core.Event +{ + public class IOStatsEventArgs : EventArgs + { + public DateTimeOffset StartTime { get; set; } + + public DateTimeOffset EndTime { get; set; } + + public TimeSpan Duration { get; set; } + + public int NetworkBytesDownloaded { get; set; } + + /// + /// mibi-bits per seconds + /// + public double NetworkMbps { get; set; } + + public TimeSpan DiskWriteTime { get; set; } + + public int DiskBytesWritten { get; set; } + + /// + /// mibi-bytes per seconds + /// + public double DiskMBps { get; set; } + } +} diff --git a/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs b/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs deleted file mode 100644 index 3b93471..0000000 --- a/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace BililiveRecorder.Core.Event -{ - public class NetworkingStatsEventArgs : EventArgs - { - public DateTimeOffset StartTime { get; set; } - - public DateTimeOffset EndTime { get; set; } - - public TimeSpan Duration { get; set; } - - public int BytesDownloaded { get; set; } - - public double Mbps { get; set; } - } -} diff --git a/BililiveRecorder.Core/IRecorder.cs b/BililiveRecorder.Core/IRecorder.cs index 59d156f..ffafe24 100644 --- a/BililiveRecorder.Core/IRecorder.cs +++ b/BililiveRecorder.Core/IRecorder.cs @@ -15,7 +15,7 @@ namespace BililiveRecorder.Core event EventHandler>? RecordSessionEnded; event EventHandler>? RecordFileOpening; event EventHandler>? RecordFileClosed; - event EventHandler>? NetworkingStats; + event EventHandler>? IOStats; event EventHandler>? RecordingStats; IRoom AddRoom(int roomid); diff --git a/BililiveRecorder.Core/IRoom.cs b/BililiveRecorder.Core/IRoom.cs index 04de18f..fb44ce0 100644 --- a/BililiveRecorder.Core/IRoom.cs +++ b/BililiveRecorder.Core/IRoom.cs @@ -29,7 +29,7 @@ namespace BililiveRecorder.Core event EventHandler? RecordFileOpening; event EventHandler? RecordFileClosed; event EventHandler? RecordingStats; - event EventHandler? NetworkingStats; + event EventHandler? IOStats; void StartRecord(); void StopRecord(); diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs index 66e4652..1b280b9 100644 --- a/BililiveRecorder.Core/Recorder.cs +++ b/BililiveRecorder.Core/Recorder.cs @@ -50,7 +50,7 @@ namespace BililiveRecorder.Core public event EventHandler>? RecordSessionEnded; public event EventHandler>? RecordFileOpening; public event EventHandler>? RecordFileClosed; - public event EventHandler>? NetworkingStats; + public event EventHandler>? IOStats; public event EventHandler>? RecordingStats; public event PropertyChangedEventHandler? PropertyChanged; @@ -81,7 +81,7 @@ namespace BililiveRecorder.Core room.RecordSessionEnded += this.Room_RecordSessionEnded; room.RecordFileOpening += this.Room_RecordFileOpening; room.RecordFileClosed += this.Room_RecordFileClosed; - room.NetworkingStats += this.Room_NetworkingStats; + room.IOStats += this.Room_IOStats; room.RecordingStats += this.Room_RecordingStats; room.PropertyChanged += this.Room_PropertyChanged; @@ -122,10 +122,10 @@ namespace BililiveRecorder.Core #region Events - private void Room_NetworkingStats(object sender, NetworkingStatsEventArgs e) + private void Room_IOStats(object sender, IOStatsEventArgs e) { var room = (IRoom)sender; - NetworkingStats?.Invoke(this, new AggregatedRoomEventArgs(room, e)); + IOStats?.Invoke(this, new AggregatedRoomEventArgs(room, e)); } private void Room_RecordingStats(object sender, RecordingStatsEventArgs e) diff --git a/BililiveRecorder.Core/Recording/IRecordTask.cs b/BililiveRecorder.Core/Recording/IRecordTask.cs index 5fce8c1..84577cc 100644 --- a/BililiveRecorder.Core/Recording/IRecordTask.cs +++ b/BililiveRecorder.Core/Recording/IRecordTask.cs @@ -8,7 +8,7 @@ namespace BililiveRecorder.Core.Recording { Guid SessionId { get; } - event EventHandler? NetworkingStats; + event EventHandler? IOStats; event EventHandler? RecordingStats; event EventHandler? RecordFileOpening; event EventHandler? RecordFileClosed; diff --git a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs index 90d792d..4517b88 100644 --- a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs +++ b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs @@ -63,9 +63,18 @@ namespace BililiveRecorder.Core.Recording if (bytesRead == 0) break; - Interlocked.Add(ref this.fillerDownloadedBytes, bytesRead); + Interlocked.Add(ref this.ioNetworkDownloadedBytes, bytesRead); + this.ioDiskStopwatch.Restart(); await file.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + this.ioDiskStopwatch.Stop(); + + lock (this.ioDiskStatsLock) + { + this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; + this.ioDiskWrittenBytes += bytesRead; + } + this.ioDiskStopwatch.Reset(); } } catch (OperationCanceledException ex) diff --git a/BililiveRecorder.Core/Recording/RecordTaskBase.cs b/BililiveRecorder.Core/Recording/RecordTaskBase.cs index 8e11d45..a19f16f 100644 --- a/BililiveRecorder.Core/Recording/RecordTaskBase.cs +++ b/BililiveRecorder.Core/Recording/RecordTaskBase.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; @@ -35,9 +36,17 @@ namespace BililiveRecorder.Core.Recording protected bool started = false; protected bool timeoutTriggered = false; - private readonly object fillerStatsLock = new object(); - protected int fillerDownloadedBytes; - private DateTimeOffset fillerStatsLastTrigger; + + private readonly object ioStatsLock = new(); + protected int ioNetworkDownloadedBytes; + + protected Stopwatch ioDiskStopwatch = new(); + protected object ioDiskStatsLock = new(); + protected TimeSpan ioDiskWriteTime; + protected int ioDiskWrittenBytes; + private DateTimeOffset ioDiskWarningTimeout; + + private DateTimeOffset ioStatsLastTrigger; private TimeSpan durationSinceNoDataReceived; protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator) @@ -48,20 +57,20 @@ namespace BililiveRecorder.Core.Recording this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator)); this.ct = this.cts.Token; - this.timer.Elapsed += this.Timer_Elapsed_TriggerNetworkStats; + this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats; } public Guid SessionId { get; } = Guid.NewGuid(); #region Events - public event EventHandler? NetworkingStats; + public event EventHandler? IOStats; public event EventHandler? RecordingStats; public event EventHandler? RecordFileOpening; public event EventHandler? RecordFileClosed; public event EventHandler? RecordSessionEnded; - protected void OnNetworkingStats(NetworkingStatsEventArgs e) => NetworkingStats?.Invoke(this, e); + protected void OnIOStats(IOStatsEventArgs e) => IOStats?.Invoke(this, e); protected void OnRecordingStats(RecordingStatsEventArgs e) => RecordingStats?.Invoke(this, e); protected void OnRecordFileOpening(RecordFileOpeningEventArgs e) => RecordFileOpening?.Invoke(this, e); protected void OnRecordFileClosed(RecordFileClosedEventArgs e) => RecordFileClosed?.Invoke(this, e); @@ -98,7 +107,7 @@ namespace BililiveRecorder.Core.Recording var stream = await this.GetStreamAsync(fullUrl: fullUrl, timeout: (int)this.room.RoomConfig.TimingStreamConnect).ConfigureAwait(false); - this.fillerStatsLastTrigger = DateTimeOffset.UtcNow; + this.ioStatsLastTrigger = DateTimeOffset.UtcNow; this.durationSinceNoDataReceived = TimeSpan.Zero; this.ct.Register(state => Task.Run(async () => @@ -118,38 +127,61 @@ namespace BililiveRecorder.Core.Recording protected abstract void StartRecordingLoop(Stream stream); - private void Timer_Elapsed_TriggerNetworkStats(object sender, ElapsedEventArgs e) + private void Timer_Elapsed_TriggerIOStats(object sender, ElapsedEventArgs e) { - int bytes; - TimeSpan diff; - DateTimeOffset start, end; + int networkDownloadBytes, diskWriteBytes; + TimeSpan durationDiff, diskWriteTime; + DateTimeOffset startTime, endTime; - lock (this.fillerStatsLock) + + lock (this.ioStatsLock) // 锁 timer elapsed 事件本身防止并行运行 { - bytes = Interlocked.Exchange(ref this.fillerDownloadedBytes, 0); - end = DateTimeOffset.UtcNow; - start = this.fillerStatsLastTrigger; - this.fillerStatsLastTrigger = end; - diff = end - start; + // networks + networkDownloadBytes = Interlocked.Exchange(ref this.ioNetworkDownloadedBytes, 0); // 锁网络统计 + endTime = DateTimeOffset.UtcNow; + startTime = this.ioStatsLastTrigger; + this.ioStatsLastTrigger = endTime; + durationDiff = endTime - startTime; - this.durationSinceNoDataReceived = bytes > 0 ? TimeSpan.Zero : this.durationSinceNoDataReceived + diff; + this.durationSinceNoDataReceived = networkDownloadBytes > 0 ? TimeSpan.Zero : this.durationSinceNoDataReceived + durationDiff; + + // disks + lock (this.ioDiskStatsLock) // 锁硬盘统计 + { + diskWriteTime = this.ioDiskWriteTime; + diskWriteBytes = this.ioDiskWrittenBytes; + this.ioDiskWriteTime = TimeSpan.Zero; + this.ioDiskWrittenBytes = 0; + } } - var mbps = bytes * (8d / 1024d / 1024d) / diff.TotalSeconds; + var netMbps = networkDownloadBytes * (8d / 1024d / 1024d) / durationDiff.TotalSeconds; + var diskMBps = diskWriteBytes / (1024d * 1024d) / diskWriteTime.TotalSeconds; - this.OnNetworkingStats(new NetworkingStatsEventArgs + this.OnIOStats(new IOStatsEventArgs { - BytesDownloaded = bytes, - Duration = diff, - StartTime = start, - EndTime = end, - Mbps = mbps + NetworkBytesDownloaded = networkDownloadBytes, + Duration = durationDiff, + StartTime = startTime, + EndTime = endTime, + NetworkMbps = netMbps, + DiskBytesWritten = diskWriteBytes, + DiskWriteTime = diskWriteTime, + DiskMBps = diskMBps, }); + var now = DateTimeOffset.Now; + if (diskWriteBytes > 0 && this.ioDiskWarningTimeout < now && (diskWriteTime.TotalSeconds > 1d || diskMBps < 2d)) + { + // 硬盘 IO 可能不能满足录播 + this.ioDiskWarningTimeout = now + TimeSpan.FromMinutes(2); // 最多每 2 分钟提醒一次 + this.logger.Warning("检测到硬盘写入速度较慢可能影响录播,请检查是否有其他软件或游戏正在使用硬盘"); + } + if ((!this.timeoutTriggered) && (this.durationSinceNoDataReceived.TotalMilliseconds > this.room.RoomConfig.TimingWatchdogTimeout)) { this.timeoutTriggered = true; - this.logger.Warning("直播服务器未断开连接但停止发送直播数据,将会主动断开连接"); + this.logger.Warning("检测到录制卡住,可能是网络或硬盘原因,将会主动断开连接"); this.RequestStop(); } } diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs index 3e0a026..d180700 100644 --- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs +++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs @@ -129,7 +129,7 @@ namespace BililiveRecorder.Core.Recording if (bytesRead == 0) break; writer.Advance(bytesRead); - Interlocked.Add(ref this.fillerDownloadedBytes, bytesRead); + Interlocked.Add(ref this.ioNetworkDownloadedBytes, bytesRead); } catch (Exception ex) { @@ -171,7 +171,16 @@ namespace BililiveRecorder.Core.Recording if (this.context.Comments.Count > 0) this.logger.Debug("修复逻辑输出 {@Comments}", this.context.Comments); - await this.writer.WriteAsync(this.context).ConfigureAwait(false); + this.ioDiskStopwatch.Restart(); + var bytesWritten = await this.writer.WriteAsync(this.context).ConfigureAwait(false); + this.ioDiskStopwatch.Stop(); + + lock (this.ioDiskStatsLock) + { + this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; + this.ioDiskWrittenBytes += bytesWritten; + } + this.ioDiskStopwatch.Reset(); if (this.context.Actions.Any(x => x is PipelineDisconnectAction)) { diff --git a/BililiveRecorder.Core/Room.cs b/BililiveRecorder.Core/Room.cs index b382812..9a9e2a2 100644 --- a/BililiveRecorder.Core/Room.cs +++ b/BililiveRecorder.Core/Room.cs @@ -102,7 +102,7 @@ namespace BililiveRecorder.Core public event EventHandler? RecordSessionEnded; public event EventHandler? RecordFileOpening; public event EventHandler? RecordFileClosed; - public event EventHandler? NetworkingStats; + public event EventHandler? IOStats; public event EventHandler? RecordingStats; public event PropertyChangedEventHandler? PropertyChanged; @@ -219,7 +219,7 @@ namespace BililiveRecorder.Core return; var task = this.recordTaskFactory.CreateRecordTask(this); - task.NetworkingStats += this.RecordTask_NetworkingStats; + task.IOStats += this.RecordTask_IOStats; task.RecordingStats += this.RecordTask_RecordingStats; task.RecordFileOpening += this.RecordTask_RecordFileOpening; task.RecordFileClosed += this.RecordTask_RecordFileClosed; @@ -349,13 +349,13 @@ namespace BililiveRecorder.Core #region Event Handlers /// - private void RecordTask_NetworkingStats(object sender, NetworkingStatsEventArgs e) + private void RecordTask_IOStats(object sender, IOStatsEventArgs e) { - this.logger.Verbose("Networking stats: {@stats}", e); + this.logger.Verbose("IO stats: {@stats}", e); - this.Stats.NetworkMbps = e.Mbps; + this.Stats.NetworkMbps = e.NetworkMbps; - NetworkingStats?.Invoke(this, e); + IOStats?.Invoke(this, e); } /// diff --git a/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs index 6675c0f..0f8b207 100644 --- a/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs +++ b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs @@ -12,6 +12,6 @@ namespace BililiveRecorder.Flv event EventHandler FileClosed; - Task WriteAsync(FlvProcessingContext context); + Task WriteAsync(FlvProcessingContext context); } } diff --git a/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs b/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs index 4d55044..9e2eda0 100644 --- a/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs +++ b/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs @@ -25,6 +25,8 @@ namespace BililiveRecorder.Flv.Writer private KeyframesScriptDataValue? keyframesScriptDataValue = null; private double lastDuration; + private int bytesWrittenByCurrentWriteCall { get; set; } + public event EventHandler? FileClosed; public Action? BeforeScriptTagWrite { get; set; } @@ -37,7 +39,7 @@ namespace BililiveRecorder.Flv.Writer this.disableKeyframes = disableKeyframes; } - public async Task WriteAsync(FlvProcessingContext context) + public async Task WriteAsync(FlvProcessingContext context) { if (this.state == WriterState.Invalid) throw new InvalidOperationException("FlvProcessingContextWriter is in a invalid state."); @@ -70,11 +72,16 @@ namespace BililiveRecorder.Flv.Writer this.semaphoreSlim.Release(); } + var bytesWritten = this.bytesWrittenByCurrentWriteCall; + this.bytesWrittenByCurrentWriteCall = 0; + // Dispose tags foreach (var action in context.Actions) if (action is PipelineDataAction dataAction) foreach (var tag in dataAction.Tags) tag.BinaryData?.Dispose(); + + return bytesWritten; } #region Flv Writer Implementation @@ -214,6 +221,8 @@ namespace BililiveRecorder.Flv.Writer await this.tagWriter.WriteTag(this.nextAudioHeaderTag).ConfigureAwait(false); } + this.bytesWrittenByCurrentWriteCall += (int)this.tagWriter.FileSize; + this.state = WriterState.Writing; } @@ -236,9 +245,13 @@ namespace BililiveRecorder.Flv.Writer var duration = tags[tags.Count - 1].Timestamp / 1000d; this.lastDuration = duration; + var beforeFileSize = this.tagWriter.FileSize; + foreach (var tag in tags) await this.tagWriter.WriteTag(tag).ConfigureAwait(false); + this.bytesWrittenByCurrentWriteCall += (int)(this.tagWriter.FileSize - beforeFileSize); + await this.RewriteScriptTagImpl(duration, firstTag.IsKeyframeData(), firstTag.Timestamp, pos).ConfigureAwait(false); } @@ -255,7 +268,11 @@ namespace BililiveRecorder.Flv.Writer throw new InvalidOperationException($"Can't write data tag with current state ({this.state})"); } + var beforeFileSize = this.tagWriter.FileSize; + await this.tagWriter.WriteTag(endAction.Tag).ConfigureAwait(false); + + this.bytesWrittenByCurrentWriteCall += (int)(this.tagWriter.FileSize - beforeFileSize); } #endregion