feat: Some performance tweak.

This commit is contained in:
jackfiled 2025-01-23 22:59:44 +08:00
parent 999284b91a
commit 24fb498d59
18 changed files with 112 additions and 207 deletions

View File

@ -5,7 +5,9 @@ namespace YaeBlog.Abstraction;
public interface IEssayContentService
{
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
public IEnumerable<BlogEssay> Essays { get; }
public int Count { get; }
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
@ -16,6 +18,8 @@ public interface IEssayContentService
public bool TryAdd(BlogEssay essay);
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
public void RefreshTags();
public void Clear();

View File

@ -19,7 +19,7 @@
</div>
<div>
@(Contents.Essays.Count)
@(Contents.Count)
</div>
</div>

View File

@ -22,7 +22,7 @@
}
else
{
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page - 1}")"/>
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>

View File

@ -1,12 +0,0 @@
namespace YaeBlog.Core.Exceptions;
public sealed class ProcessInteropException : Exception
{
public ProcessInteropException(string message) : base(message)
{
}
public ProcessInteropException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@ -1,10 +0,0 @@
namespace YaeBlog.Models;
public class AboutInfo
{
public required string Introduction { get; set; }
public required string Description { get; set; }
public required string AvatarImage { get; set; }
}

View File

@ -7,4 +7,6 @@ public class BlogContent
public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
}

View File

@ -6,6 +6,8 @@ public class BlogEssay : IComparable<BlogEssay>
public required string FileName { get; init; }
public required bool IsDraft { get; init; }
public required DateTime PublishTime { get; init; }
public required string Description { get; init; }
@ -24,6 +26,7 @@ public class BlogEssay : IComparable<BlogEssay>
{
Title = Title,
FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime,
Description = Description,
WordCount = WordCount,
@ -39,10 +42,16 @@ public class BlogEssay : IComparable<BlogEssay>
{
if (other is null)
{
return 1;
return -1;
}
return PublishTime.CompareTo(other.PublishTime);
// 草稿文章应当排在前面
if (IsDraft != other.IsDraft)
{
return IsDraft ? -1 : 1;
}
return other.PublishTime.CompareTo(PublishTime);
}
public override string ToString()

View File

@ -1,10 +0,0 @@
namespace YaeBlog.Models;
public class TailwindOptions
{
public const string OptionName = "Tailwind";
public required string InputFile { get; set; }
public required string OutputFile { get; set; }
}

View File

@ -19,7 +19,7 @@
</span>
</div>
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
{
<div class="p-2">
<div class="flex flex-col">
@ -28,9 +28,9 @@
</div>
<div class="px-4 py-4 flex flex-col">
@foreach ((String name, BlogEssay essay) in group)
@foreach (BlogEssay essay in group)
{
<a target="_blank" href="@($"/blog/essays/{name}")">
<a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
<div class="w-20">
@(essay.PublishTime.ToString("MM月dd日"))
@ -51,14 +51,13 @@
</div>
@code {
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
protected override void OnInitialized()
{
base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays
orderby essay.Value.PublishTime descending
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
}
}

View File

@ -12,9 +12,9 @@
<div>
<div class="grid grid-cols-4">
<div class="col-span-4 md:col-span-3">
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
@foreach (BlogEssay essay in _essays)
{
<EssayCard Essay="@(pair.Value)"/>
<EssayCard Essay="@(essay)"/>
}
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
@ -30,7 +30,7 @@
[SupplyParameterFromQuery] private int? Page { get; set; }
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
private readonly List<BlogEssay> _essays = [];
private const int EssaysPerPage = 8;
private int _pageCount = 1;
private int _page = 1;
@ -38,16 +38,15 @@
protected override void OnInitialized()
{
_page = Page ?? 1;
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
_pageCount = Contents.Count / EssaysPerPage + 1;
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
{
NavigationInstance.NavigateTo("/NotFount");
return;
}
_essays.AddRange(Contents.Essays
.OrderByDescending(p => p.Value.PublishTime)
.Skip((_page - 1) * EssaysPerPage)
.Take(EssaysPerPage));
}

View File

@ -110,7 +110,7 @@
return;
}
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
if (!Contents.TryGetEssay(BlogKey, out _essay))
{
NavigationInstance.NavigateTo("/NotFound");
}

View File

@ -6,14 +6,12 @@ public class BlogHostedService(
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Welcome to YaeBlog!");
logger.LogInformation("Failed to load cache, render essays.");
await rendererService.RenderAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
return Task.CompletedTask;
}
}

View File

@ -10,24 +10,32 @@ public sealed class BlogHotReloadService(
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("BlogHotReloadService is starting.");
logger.LogInformation("Hot reload is starting...");
logger.LogInformation("Change essays will lead to hot reload!");
logger.LogInformation("HINT: draft essays will be included.");
await rendererService.RenderAsync();
await rendererService.RenderAsync(true);
while (!stoppingToken.IsCancellationRequested)
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task FileWatchTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
logger.LogDebug("Watching file changes...");
string? changFile = await watcher.WaitForChange(stoppingToken);
logger.LogInformation("Watching file changes...");
string? changeFile = await watcher.WaitForChange(token);
if (changFile is null)
if (changeFile is null)
{
logger.LogInformation("BlogHotReloadService is stopping.");
logger.LogInformation("File watcher is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changFile);
logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear();
await rendererService.RenderAsync();
await rendererService.RenderAsync(true);
}
}
}

View File

@ -9,15 +9,28 @@ public class EssayContentService : IEssayContentService
{
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
private readonly List<BlogEssay> _sortedEssays = [];
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
public bool TryAdd(BlogEssay essay)
{
_sortedEssays.Add(essay);
return _essays.TryAdd(essay.FileName, essay);
}
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
public IEnumerable<BlogEssay> Essays => _sortedEssays;
public int Count => _sortedEssays.Count;
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
{
return _essays.TryGetValue(filename, out essay);
}
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;

View File

@ -22,8 +22,8 @@ public partial class EssayScanService(
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts),
await ScanContentsInternal(posts));
await ScanContentsInternal(drafts, true),
await ScanContentsInternal(posts, false));
}
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
@ -60,7 +60,7 @@ public partial class EssayScanService(
}
}
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
{
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
@ -97,7 +97,8 @@ public partial class EssayScanService(
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
});
}
catch (YamlException e)

View File

@ -1,65 +0,0 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using YaeBlog.Core.Exceptions;
namespace YaeBlog.Services;
public class ProcessInteropService(ILogger<ProcessInteropService> logger)
{
public Process StartProcess(string command, string arguments)
{
string commandName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !command.EndsWith(".exe"))
{
commandName = command + ".exe";
}
else
{
commandName = command;
}
try
{
ProcessStartInfo startInfo = new()
{
FileName = commandName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
Process? process = Process.Start(startInfo);
if (process is null)
{
throw new ProcessInteropException(
$"Failed to start process: {commandName}, the return process is null.");
}
process.OutputDataReceived += (_, data) =>
{
if (!string.IsNullOrEmpty(data.Data))
{
logger.LogInformation("Receive output from process '{}': '{}'", commandName, data.Data);
}
};
process.ErrorDataReceived += (_, data) =>
{
if (!string.IsNullOrEmpty(data.Data))
{
logger.LogWarning("Receive error from process '{}': '{}'", commandName, data.Data);
}
};
return process;
}
catch (Exception innerException)
{
throw new ProcessInteropException($"Failed to start process '{command}' with arguments '{arguments}",
innerException);
}
}
}

View File

@ -21,40 +21,43 @@ public partial class RendererService(
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
public async Task RenderAsync()
public async Task RenderAsync(bool includeDrafts = false)
{
_stopwatch.Start();
logger.LogInformation("Render essays start.");
BlogContents contents = await essayScanService.ScanContents();
List<BlogContent> posts = contents.Posts.ToList();
if (includeDrafts)
{
posts.AddRange(contents.Drafts);
}
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
List<BlogEssay> essays = [];
await Task.Run(() =>
foreach (BlogContent content in preProcessedContents)
{
foreach (BlogContent content in preProcessedContents)
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
}
essays.Add(essay);
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
}
});
essays.Add(essay);
}
ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay =>
@ -66,7 +69,16 @@ public partial class RendererService(
logger.LogDebug("Render markdown file {}.", newEssay);
});
await PostProcess(postProcessEssays);
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
foreach (BlogEssay essay in postProcessedEssays)
{
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
}
}
essayContentService.RefreshTags();
_stopwatch.Stop();
@ -117,8 +129,10 @@ public partial class RendererService(
return processedContents;
}
private async Task PostProcess(IEnumerable<BlogEssay> essays)
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
{
ConcurrentBag<BlogEssay> processedContents = [];
await Parallel.ForEachAsync(essays, async (essay, _) =>
{
foreach (IPostRenderProcessor processor in _postRenderProcessors)
@ -126,12 +140,13 @@ public partial class RendererService(
essay = await processor.ProcessAsync(essay);
}
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
processedContents.Add(essay);
});
List<BlogEssay> result = processedContents.ToList();
result.Sort();
return result;
}
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]

View File

@ -1,46 +0,0 @@
using System.Diagnostics;
using Microsoft.Extensions.Options;
using YaeBlog.Models;
namespace YaeBlog.Services;
/// <summary>
/// 在应用程序运行的过程中启动Tailwind watch
/// 在程序退出时自动结束进程
/// 只在Development模式下启动
/// </summary>
public sealed class TailwindRefreshService(
IOptions<TailwindOptions> options,
ProcessInteropService processInteropService,
IHostEnvironment hostEnvironment,
ILogger<TailwindRefreshService> logger) : IHostedService, IDisposable
{
private Process? _tailwindProcess;
public Task StartAsync(CancellationToken cancellationToken)
{
if (!hostEnvironment.IsDevelopment())
{
return Task.CompletedTask;
}
logger.LogInformation("Try to start tailwind watcher with input {} and output {}", options.Value.InputFile,
options.Value.OutputFile);
_tailwindProcess = processInteropService.StartProcess("pnpm",
$"tailwind -i {options.Value.InputFile} -o {options.Value.OutputFile} --watch");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_tailwindProcess?.Kill();
return Task.CompletedTask;
}
public void Dispose()
{
_tailwindProcess?.Dispose();
}
}