Compare commits
23 Commits
4085b0d99c
...
write-asyn
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c2cce59e4 | |||
| 3aae468e65 | |||
| 1ceaf30061 | |||
| 87204dab8e | |||
| 05d40ce3b6 | |||
| 309db7e5f1 | |||
| fb71ce64cf | |||
| d87e3830a5 | |||
| baf50eeab0 | |||
| 32104bbfb8 | |||
| f77d2a47d1 | |||
| f6e43f466d | |||
| f889d51857 | |||
| 45c35d7442 | |||
| 9ebacf87e5 | |||
| dc1b97fed4 | |||
| 9bfe091024 | |||
| bb5cb77b7b | |||
| 933eec0f7f | |||
| 3a4ada50c6 | |||
| 05a22a0b29 | |||
| 6797028cc1 | |||
| 77e52fa11e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -482,3 +482,6 @@ $RECYCLE.BIN/
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
|
||||
# Tailwind auto-generated stylesheet
|
||||
output.css
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class AboutInfo
|
||||
{
|
||||
public required string Introduction { get; set; }
|
||||
|
||||
public required string Description { get; set; }
|
||||
|
||||
public required string AvatarImage { get; set; }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
|
||||
{
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument document = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
IEnumerable<IElement> preElements = from e in document.All
|
||||
where e.LocalName == "pre"
|
||||
select e;
|
||||
|
||||
foreach (IElement element in preElements)
|
||||
{
|
||||
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
|
||||
}
|
||||
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(CodeBlockPostRenderProcessor);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
public class TablePostRenderProcessor: IPostRenderProcessor
|
||||
{
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext browsingContext = new(Configuration.Default);
|
||||
IDocument document = await browsingContext.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
|
||||
where item.LocalName == "table"
|
||||
select item as IHtmlTableElement;
|
||||
|
||||
foreach (IHtmlTableElement element in tableElements)
|
||||
{
|
||||
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
|
||||
divElement.InnerHtml = element.OuterHtml;
|
||||
divElement.ClassList.Add("py-2", "table-wrapper");
|
||||
|
||||
element.Replace(divElement);
|
||||
}
|
||||
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(TablePostRenderProcessor);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public sealed class BlogHotReloadService(
|
||||
RendererService rendererService,
|
||||
IEssayContentService essayContentService,
|
||||
BlogChangeWatcher watcher,
|
||||
ILogger<BlogHotReloadService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("BlogHotReloadService is starting.");
|
||||
|
||||
await rendererService.RenderAsync();
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug("Watching file changes...");
|
||||
string? changFile = await watcher.WaitForChange(stoppingToken);
|
||||
|
||||
if (changFile is null)
|
||||
{
|
||||
logger.LogInformation("BlogHotReloadService is stopping.");
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogInformation("{} changed, re-rendering.", changFile);
|
||||
essayContentService.Clear();
|
||||
await rendererService.RenderAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||
<PackageReference Include="Markdig" Version="0.34.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
|
||||
@@ -29,10 +27,6 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
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();
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IEssayScanService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.CommandLine.Binding;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.CommandLine.Binding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
using System.CommandLine;
|
||||
using YaeBlog.Commands.Binders;
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Core.Extensions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Commands;
|
||||
|
||||
public static class CommandExtensions
|
||||
public sealed class YaeBlogCommand
|
||||
{
|
||||
public static void AddServeCommand(this RootCommand rootCommand)
|
||||
private readonly RootCommand _rootCommand = new("YaeBlog Cli");
|
||||
|
||||
public YaeBlogCommand()
|
||||
{
|
||||
AddServeCommand(_rootCommand);
|
||||
AddWatchCommand(_rootCommand);
|
||||
AddListCommand(_rootCommand);
|
||||
AddNewCommand(_rootCommand);
|
||||
AddPublishCommand(_rootCommand);
|
||||
AddScanCommand(_rootCommand);
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(string[] args)
|
||||
{
|
||||
return _rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static void AddServeCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command serveCommand = new("serve", "Start http server.");
|
||||
rootCommand.AddCommand(serveCommand);
|
||||
@@ -21,7 +38,6 @@ public static class CommandExtensions
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddServer();
|
||||
|
||||
@@ -40,7 +56,7 @@ public static class CommandExtensions
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddWatchCommand(this RootCommand rootCommand)
|
||||
private static void AddWatchCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -52,7 +68,6 @@ public static class CommandExtensions
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddWatcher();
|
||||
|
||||
@@ -71,7 +86,7 @@ public static class CommandExtensions
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddNewCommand(this RootCommand rootCommand)
|
||||
private static void AddNewCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command newCommand = new("new", "Create a new blog file and image directory.");
|
||||
rootCommand.AddCommand(newCommand);
|
||||
@@ -101,7 +116,7 @@ public static class CommandExtensions
|
||||
new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddListCommand(this RootCommand rootCommand)
|
||||
private static void AddListCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("list", "List all blogs");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -124,7 +139,7 @@ public static class CommandExtensions
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddScanCommand(this RootCommand rootCommand)
|
||||
private static void AddScanCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("scan", "Scan unused and not found images.");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -165,7 +180,7 @@ public static class CommandExtensions
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||
}
|
||||
|
||||
public static void AddPublishCommand(this RootCommand rootCommand)
|
||||
private static void AddPublishCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("publish", "Publish a new blog file.");
|
||||
rootCommand.AddCommand(command);
|
||||
9
YaeBlog/Components/Anchor.razor
Normal file
9
YaeBlog/Components/Anchor.razor
Normal file
@@ -0,0 +1,9 @@
|
||||
<a href="@Address" class="text-blue-600" target="@(NewPage ? "_blank" : "_self")">@Text</a>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Address { get; set; }
|
||||
|
||||
[Parameter] public string? Text { get; set; }
|
||||
|
||||
[Parameter] public bool NewPage { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
@@ -7,22 +7,14 @@
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||
<link rel="icon" href="images/favicon.ico"/>
|
||||
<link rel="stylesheet" href="bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
|
||||
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
|
||||
<link rel="stylesheet" href="globals.css"/>
|
||||
<link rel="stylesheet" href="output.css"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes/>
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="bootstrap.bundle.min.js"></script>
|
||||
<script src="clipboard.min.js"></script>
|
||||
<script>
|
||||
const clipboard = new ClipboardJS('.btn');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,57 +1,49 @@
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject BlogOptions Options
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto p-4">
|
||||
<Image Src="images/avatar.png" Alt="Ricardo's avatar"/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="p-10">
|
||||
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full"/>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center p-3">
|
||||
<div class="col-auto fs-4">
|
||||
<div class="px-10 py-2 text-xl">
|
||||
“奇奇怪怪东西的聚合地”
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
<div class="flex flex-row justify-between px-6 py-2 text-xl">
|
||||
<div>
|
||||
文章
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/archives">
|
||||
@(Contents.Essays.Count)
|
||||
<a href="/blog/archives/">
|
||||
<div>
|
||||
@(Contents.Count)
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
<div class="flex flex-row justify-between px-6 py-2 text-xl">
|
||||
<div>
|
||||
标签
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/tags">
|
||||
<a href="/blog/tags/">
|
||||
<div>
|
||||
@(Contents.Tags.Count)
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-start fs-5" style="padding-top: 2em">
|
||||
<div class="col-auto">
|
||||
<div class="text-xl px-2 py-2">
|
||||
广而告之
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p style="text-indent: 2em">
|
||||
<div class="px-6">
|
||||
<p class="text-lg">
|
||||
@(Options.Announcement)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Models
|
||||
|
||||
<div class="container p-3">
|
||||
<div class="row fs-2 fw-bold py-2 essay-title">
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="text-3xl font-bold py-2">
|
||||
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
|
||||
</div>
|
||||
|
||||
<div class="row p-2 justify-content-start">
|
||||
<div class="col-auto fw-light">
|
||||
<div class="p-2 flex flex-row justify-content-start gap-2">
|
||||
<div class="font-light">
|
||||
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
@foreach (string key in Essay.Tags)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<div class="text-sky-600">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
|
||||
# @key
|
||||
</a>
|
||||
@@ -21,20 +21,11 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="p-2">
|
||||
@(Essay.Description)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col border-bottom">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required BlogEssay Essay { get; set; }
|
||||
[Parameter] public required BlogEssay Essay { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
<div class="row align-items-end text-center">
|
||||
<div class="row">
|
||||
<p class="fs-6">
|
||||
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>,
|
||||
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。
|
||||
<div class="flex flex-col text-center py-2">
|
||||
<div>
|
||||
<p class="text-md">
|
||||
2021 - @(DateTimeOffset.Now.Year) ©
|
||||
<Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
|
||||
,由
|
||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||
驱动。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-6">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
|
||||
<div>
|
||||
<p class="text-md">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private string DotnetVersion => $".NET {Environment.Version}";
|
||||
}
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@using YaeBlog.Models
|
||||
@inject BlogOptions Options
|
||||
|
||||
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle">
|
||||
<div class="col">
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
|
||||
</div>
|
||||
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
|
||||
<div class="flex flex-col gap-3 text-md">
|
||||
<div>
|
||||
文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
|
||||
</div>
|
||||
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
<div>
|
||||
文章地址:
|
||||
<a href="/blog/essays/@(EssayAddress)" target="_blank">
|
||||
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
|
||||
<a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
|
||||
@($"https://rrricardo.top/blog/essays/{EssayFilename}")
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
<div>
|
||||
版权声明:本博客所有文章除特别声明外,均采用
|
||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a>
|
||||
许可协议,转载请注明来自
|
||||
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>。
|
||||
</div>
|
||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="text-blue-600">
|
||||
CC BY-NC-SA 4.0
|
||||
</a>
|
||||
许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
|
||||
<a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
|
||||
Ricardo's Blog
|
||||
</a>”。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string? EssayAddress { get; set; }
|
||||
[Parameter] public string? EssayFilename { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.copyright {
|
||||
}
|
||||
22
YaeBlog/Components/PageAnchor.razor
Normal file
22
YaeBlog/Components/PageAnchor.razor
Normal file
@@ -0,0 +1,22 @@
|
||||
@if (Selected)
|
||||
{
|
||||
<div class="border rounded-lg shadow-neutral-500 bg-sky-400 w-8 h-8 inline-block leading-8 text-center">
|
||||
<span class="text-white">@(Text)</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="@Address">
|
||||
<div class="border rounded-lg shadow-neutral-500 w-8 h-8 inline-block leading-8 text-center">
|
||||
<span>@(Text)</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Address { get; set; }
|
||||
|
||||
[Parameter] public string? Text { get; set; }
|
||||
|
||||
[Parameter] public bool Selected { get; set; }
|
||||
}
|
||||
46
YaeBlog/Components/Pagination.razor
Normal file
46
YaeBlog/Components/Pagination.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="flex flex-row justify-center gap-3">
|
||||
@if (Page != 1)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="<"/>
|
||||
}
|
||||
|
||||
@if (Page == 1)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(1)" Text="1" Selected="@true"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(2)" Text="2"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(3)" Text="3"/>
|
||||
}
|
||||
else if (Page == PageCount)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(PageCount - 2)" Text="@($"{PageCount - 2}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(PageCount - 1)" Text="@($"{PageCount - 1}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(PageCount)" Text="@($"{PageCount}")" Selected="@true"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page + 1}")"/>
|
||||
}
|
||||
|
||||
@if (Page != PageCount)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page + 1)" Text=">"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? BaseUrl { get; set; }
|
||||
|
||||
[Parameter] public int PageCount { get; set; }
|
||||
|
||||
[Parameter] public int Page { get; set; }
|
||||
|
||||
private string GenerateAddress(int page) => $"{BaseUrl}?page={page}";
|
||||
}
|
||||
@@ -9,12 +9,20 @@ public class FilesController : ControllerBase
|
||||
[HttpGet("{*filename}")]
|
||||
public IActionResult Images(string filename)
|
||||
{
|
||||
// 这里疑似有点太愚蠢了
|
||||
string contentType = "image/png";
|
||||
|
||||
if (filename.EndsWith("jpg") || filename.EndsWith("jpeg"))
|
||||
{
|
||||
contentType = "image/jpeg";
|
||||
}
|
||||
|
||||
if (filename.EndsWith("svg"))
|
||||
{
|
||||
contentType = "image/svg+xml";
|
||||
}
|
||||
|
||||
|
||||
FileInfo imageFile = new(filename);
|
||||
|
||||
if (!imageFile.Exists)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
|
||||
WORKDIR /app
|
||||
COPY bin/Release/net8.0/publish/ ./
|
||||
COPY bin/Release/net9.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
COPY appsettings.json .
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@@ -1,13 +1,11 @@
|
||||
using AngleSharp;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Processors;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Processors;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
@@ -19,14 +17,13 @@ public static class WebApplicationBuilderExtensions
|
||||
|
||||
builder.Services.AddMarkdig();
|
||||
builder.Services.AddYamlParser();
|
||||
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
||||
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default);
|
||||
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
||||
builder.Services.AddSingleton<RendererService>();
|
||||
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
|
||||
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
||||
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
|
||||
builder.Services.AddTransient<TablePostRenderProcessor>();
|
||||
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
|
||||
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
|
||||
builder.Services.AddTransient<BlogOptions>(provider =>
|
||||
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Processors;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Processors;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static void UseYaeBlog(this WebApplication application)
|
||||
{
|
||||
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<TablePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
|
||||
}
|
||||
|
||||
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
||||
@@ -2,60 +2,41 @@
|
||||
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<main class="container">
|
||||
<div class="row d-none d-xl-flex" style="height: 80px">
|
||||
<div class="px-2 col-9">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h4>Ricardo's Blog</h4>
|
||||
<main class="container mx-auto flex flex-col min-h-screen">
|
||||
<div class="grid grid-cols-3 mx-3">
|
||||
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
|
||||
<a href="/blog/">
|
||||
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-3 d-flex justify-content-around align-items-center">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h5>首页</h5>
|
||||
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
|
||||
<div>
|
||||
<a href="/blog/archives/">
|
||||
<span class="text-xl text-blue-600">归档</span>
|
||||
</a>
|
||||
|
||||
<a href="/blog/archives/" class="p-2">
|
||||
<h5>归档</h5>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/blog/tags/">
|
||||
<span class="text-xl text-blue-600">标签</span>
|
||||
</a>
|
||||
|
||||
<a href="/blog/tags/" class="p-2">
|
||||
<h5>标签</h5>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/about/" target="_blank">
|
||||
<span class="text-xl text-blue-600">关于</span>
|
||||
</a>
|
||||
|
||||
<a href="/blog/about/" class="p-2">
|
||||
<h5>关于</h5>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/friends/" target="_blank">
|
||||
<span class="text-xl text-blue-600">友链</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-xl-none">
|
||||
<div class="px-2 col-12">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h4>Ricardo's Blog</h4>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="px-2 col-12 justify-content-end d-flex">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h5>首页</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/archives/" class="p-2">
|
||||
<h5>归档</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/tags/" class="p-2">
|
||||
<h5>标签</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/about/" class="p-2">
|
||||
<h5>关于</h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-2">
|
||||
<div class="px-4 py-2 flex-grow">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main class="container">
|
||||
<div class="row" style="height: 80px">
|
||||
<div class="px-2 col-8">
|
||||
<a href="/" class="p-2">
|
||||
<h4>Ricardo's Index</h4>
|
||||
<main class="container mx-auto min-h-screen flex flex-col">
|
||||
<div class="grid grid-cols-4">
|
||||
<div class="px-2 md:col-span-3 col-span-4 h-20 flex items-center">
|
||||
<a href="/" class="text-2xl">
|
||||
<h4 class="text-blue-600">Ricardo's Index</h4>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-4 d-flex justify-content-around align-items-center">
|
||||
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank">
|
||||
<h5>E-mail</h5>
|
||||
</a>
|
||||
<div class="md:col-span-1 col-span-4 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
|
||||
<Anchor
|
||||
Address="/blog/"
|
||||
Text="博客"
|
||||
NewPage="@(true)"/>
|
||||
|
||||
<Anchor
|
||||
Address="/about/"
|
||||
Text="关于"
|
||||
NewPage="@(true)"/>
|
||||
|
||||
<Anchor
|
||||
Address="/friends"
|
||||
Text="友链"
|
||||
NewPage="@(true)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 center">
|
||||
<div class="px-4 mx-auto flex-grow">
|
||||
<div class="py-2">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
max-width: 48em;
|
||||
min-height: calc(100vh - 80px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogContent
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogEssay : IComparable<BlogEssay>
|
||||
{
|
||||
@@ -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()
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogOptions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
@@ -1,141 +1,74 @@
|
||||
@page "/blog/about"
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject BlogOptions Options
|
||||
@page "/about"
|
||||
|
||||
<PageTitle>
|
||||
关于
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>关于</h1>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">关于</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-2">
|
||||
把字刻在石头上!(・’ω’・)
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<span class="italic">把字刻在石头上!(・’ω’・)</span>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于我</h3>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="pb-2">
|
||||
<h3 class="text-2xl">关于我</h3>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-2">
|
||||
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
|
||||
一般在互联网上使用<span class="fst-italic">初冬的朝阳</span>或者<span class="fst-italic">jackfiled</span>的名字活动。
|
||||
<span class="text-decoration-line-through">都是ICP备案过的人了,网名似乎没有太大的用处(</span>
|
||||
</div>
|
||||
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span class="italic">jackfiled</span>的名字活动。
|
||||
<span class="line-through">都是ICP备案过的人了,网名似乎没有太大的用处(</span>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-2">
|
||||
主要是一个C#程序员,目前也在尝试写一点Rust。
|
||||
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
|
||||
前后端分离的项目本当上手。
|
||||
常常因为现实的压力而写一些C/C++。
|
||||
<span class="text-decoration-line-through">对于Java和Go的评价很低。</span>
|
||||
<span class="line-through">对于Java和Go的评价很低。</span>
|
||||
日常使用ArchLinux。
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。
|
||||
兴趣活动是读书和看番,目前在玩原神和三角洲。
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-4">
|
||||
常常被人批评没有梦想,这里就随便瞎编一下。
|
||||
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
|
||||
完成第一个不是课程设计的个人开源项目。
|
||||
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
|
||||
<span class="text-decoration-line-through">什么时候天上才能掉美少女?</span>
|
||||
</div>
|
||||
<span class="line-through">什么时候天上才能掉美少女?</span>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-2">
|
||||
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
|
||||
也可以试试在各大平台搜索上面提到的名字。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="pb-2">
|
||||
<h3 class="text-2xl">关于本站</h3>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于本站</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-2">
|
||||
本站肇始于2021年下半年,在开始的两年中个人网站和博客是分别的两个网站,个人网站是裸HTML写的,博客是用
|
||||
<a href="https://hexo.io">Hexo</a>渲染的。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<div class="py-2">
|
||||
2024年,我们决定使用.NET技术完全重构两个网站,合二为一。虽然目前这个版本还是一个半成品,但是我们一定会努力的~(确信。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>友链</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col fst-italic">
|
||||
欢迎所有人联系我添加友链!(´。✪ω✪。`)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
@foreach (FriendLink link in Options.Links)
|
||||
{
|
||||
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||
<a href="@(link.Link)" target="_blank" class="m-3">
|
||||
<div class="row link-item">
|
||||
<div class="col-4">
|
||||
<Image Src="@(link.AvatarImage)" Alt="@(link.Name)" Style="border-radius: 50%"/>
|
||||
</div>
|
||||
|
||||
<div class="col-8">
|
||||
<div class="row">
|
||||
<div class="col-auto fs-5">
|
||||
@(link.Name)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto fst-italic">
|
||||
@(link.Description)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<div class="py-2">
|
||||
2025年,我们将使用的样式库从Bootstrap迁移到Tailwind CSS,将现代的前端技术同Blazor结合起来。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.link-item {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog/archives"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
|
||||
@@ -8,68 +8,56 @@
|
||||
归档
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>归档</h1>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">归档</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-4">
|
||||
<div class="py-4">
|
||||
<span class="italic">
|
||||
时光图书馆,黑历史集散地。(๑◔‿◔๑)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
|
||||
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>@(group.Key.Year)</h3>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h3 class="text-xl">@(group.Key.Year)</h3>
|
||||
</div>
|
||||
|
||||
<div class="container px-3 py-2">
|
||||
@foreach (KeyValuePair<string, BlogEssay> essay in group)
|
||||
<div class="px-4 py-4 flex flex-col">
|
||||
@foreach (BlogEssay essay in group)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
@(essay.Value.PublishTime.ToString("MM-dd"))
|
||||
<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日"))
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/essays/@(essay.Key)">
|
||||
@(essay.Value.Title)
|
||||
<div>
|
||||
<span class="text-blue-600">
|
||||
@(essay.Title)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -9,79 +9,18 @@
|
||||
Ricardo's Blog
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-9">
|
||||
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
|
||||
<div>
|
||||
<div class="grid grid-cols-4">
|
||||
<div class="col-span-4 md:col-span-3">
|
||||
@foreach (BlogEssay essay in _essays)
|
||||
{
|
||||
<EssayCard Essay="@(pair.Value)"/>
|
||||
<EssayCard Essay="@(essay)"/>
|
||||
}
|
||||
|
||||
<div class="row align-items-center justify-content-center p-3">
|
||||
@if (_page == 1)
|
||||
{
|
||||
<div class="col-auto fw-light">上一页</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page - 1)">上一页</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_page == 1)
|
||||
{
|
||||
<div class="col-auto">
|
||||
1
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=2">2</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=3">3</a>
|
||||
</div>
|
||||
}
|
||||
else if (_page == _pageCount)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@(_pageCount)
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@(_page)
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_page == _pageCount)
|
||||
{
|
||||
<div class="col-auto fw-light">
|
||||
下一页
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page + 1)">下一页</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<div class="col-span-4 md:col-span-1">
|
||||
<BlogInformationCard/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,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;
|
||||
@@ -99,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));
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.read-more a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/essays/{BlogKey}"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,86 +10,78 @@
|
||||
@(_essay!.Title)
|
||||
</PageTitle>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="flex flex-col py-8">
|
||||
<div>
|
||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||
<div class="col-auto">
|
||||
<h1 id="title">@(_essay!.Title)</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-1">
|
||||
<div class="col-auto fw-light">
|
||||
<div class="px-6 pt-4 pb-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="font-light">
|
||||
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
@foreach (string tag in _essay!.Tags)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<div class="text-sky-500">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||
# @(tag)
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-1">
|
||||
<div class="col-auto fw-light">
|
||||
<div class="px-6 pt-2 pb-4">
|
||||
<div class="font-light">
|
||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-12">
|
||||
<div class="grid grid-cols-3">
|
||||
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
|
||||
<div>
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
|
||||
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="row sticky-lg-top justify-content-center">
|
||||
<div class="col-auto">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
|
||||
文章目录
|
||||
</h3>
|
||||
<div>
|
||||
<LicenseDisclaimer EssayFilename="@BlogKey"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-left: 10px">
|
||||
<div class="col-auto">
|
||||
<div class="col-span-3 md:col-span-1">
|
||||
<div class="flex flex-col sticky top-0 px-8">
|
||||
<div>
|
||||
<h3 class="text-2xl">文章目录</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@foreach (BlogHeadline level2 in _headline!.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
|
||||
</div>
|
||||
<div class="py-2 pl-3">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
|
||||
Text="@(level2.Title)"/>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level3 in level2.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
|
||||
@(level3.Title)
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-2 pl-6">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
|
||||
Text="@(level3.Title)"/>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level4 in level3.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
|
||||
@(level4.Title)
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-2 pl-9">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
|
||||
Text="@(level4.Title)"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_headline!.Children.Count == 0)
|
||||
{
|
||||
@@ -102,7 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -123,7 +114,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
|
||||
if (!Contents.TryGetEssay(BlogKey, out _essay))
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFound");
|
||||
}
|
||||
|
||||
49
YaeBlog/Pages/Friends.razor
Normal file
49
YaeBlog/Pages/Friends.razor
Normal file
@@ -0,0 +1,49 @@
|
||||
@page "/friends"
|
||||
@using YaeBlog.Models
|
||||
@inject BlogOptions Options
|
||||
|
||||
<PageTitle>
|
||||
友链
|
||||
</PageTitle>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">
|
||||
友链
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
欢迎所有人联系我添加友链!(´。✪ω✪。`)
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 g-4 p-2">
|
||||
@foreach (FriendLink link in Options.Links)
|
||||
{
|
||||
<div>
|
||||
<a href="@(link.Link)" target="_blank" class="mx-5">
|
||||
<div class="flex flex-row">
|
||||
<div class="basis-1/3">
|
||||
<img src="@(link.AvatarImage)" alt="@($"Avatar of {link.Name}")"
|
||||
class="w-full h-auto rounded-full">
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col basis-2/3 px-2">
|
||||
<div class="text-lg">
|
||||
@(link.Name)
|
||||
</div>
|
||||
|
||||
<div class="text-sm italic">
|
||||
@(link.Description)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -4,28 +4,28 @@
|
||||
Ricardo's Index
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row py-4">
|
||||
<div class="col-lg-4 col-12 p-5 p-lg-0">
|
||||
<Image Src="images/avatar.png" Alt="Ricardo's Avatar"/>
|
||||
<div class="mx-20">
|
||||
<div class="grid grid-cols-3 py-4">
|
||||
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
|
||||
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-12">
|
||||
<div class="container px-3">
|
||||
<div class="row">
|
||||
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
<div class="flex flex-col px-3 gap-y-3">
|
||||
<div class="">
|
||||
<div class="text-3xl font-bold">初冬的朝阳 (Ricardo Ren)</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">a.k.a jackfiled</p>
|
||||
<div class="">
|
||||
<p class="text-lg">a.k.a jackfiled</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
|
||||
<div class="">
|
||||
<p class="text-lg italic">世界很大,时间很长。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
<div class="">
|
||||
<p class="text-lg">
|
||||
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
|
||||
</p>
|
||||
</div>
|
||||
@@ -33,20 +33,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-top: 80px">
|
||||
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||
<div class="py-5">
|
||||
<p class="text-lg">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
|
||||
<div>
|
||||
<p class="text-lg py-1">
|
||||
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <Anchor Address="/blog/" Text="博客"/> 看看。
|
||||
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
|
||||
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>,
|
||||
<Anchor Address="https://jackfiled.github.io/wiki/" Text="我的学习笔记"/>,
|
||||
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
|
||||
如果您想批判一下我的代码,在 <a href="https://github.com/jackfiled" target="_blank">Github</a> 和
|
||||
<a href="https://git.rrricardo.top/jackfiled/" target="_blank">Gitea</a> 都可以找到。
|
||||
如果您想批判一下我的代码,在
|
||||
<Anchor Address="https://github.com/jackfiled/" Text="Github"/> 和
|
||||
<Anchor Address="https://git.rrricardo.top/jackfiled/" Text="Gitea"/>
|
||||
都可以找到。
|
||||
</p>
|
||||
<p class="fs-5">
|
||||
<p class="text-lg py-1">
|
||||
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
啊~ 页面走丢啦~
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<h3>NotFound!</h3>
|
||||
<div>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/tags/"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,24 +10,22 @@
|
||||
@(TagName ?? "标签")
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1>标签</h1>
|
||||
<h1 class="text-4xl">标签</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2>@(TagName)</h2>
|
||||
<h2 class="text-2xl">@(TagName)</h2>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-4">
|
||||
<div class="py-4">
|
||||
<span class="italic">
|
||||
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (TagName is null)
|
||||
@@ -38,19 +36,17 @@
|
||||
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
||||
{
|
||||
<li class="p-2">
|
||||
<div class="flex flex-row">
|
||||
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
||||
<div class="container fs-5">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div class="text-sky-600 text-lg">
|
||||
# @(pair.Key.TagName)
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="col-auto tag-count">
|
||||
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
|
||||
@(pair.Value.Count)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.tag-count {
|
||||
background: var(--bs-secondary-bg);
|
||||
border-radius: 5px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
102
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal file
102
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
|
||||
/// </summary>
|
||||
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
||||
{
|
||||
public string Name => nameof(EssayStylesPostRenderProcessor);
|
||||
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument document = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
ApplyGlobalCssStyles(document);
|
||||
BeatifyTable(document);
|
||||
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, string> _globalCssStyles = new()
|
||||
{
|
||||
{ "pre", "p-4 bg-slate-300 rounded-sm overflow-x-auto" },
|
||||
{ "h2", "text-3xl font-bold py-4" },
|
||||
{ "h3", "text-2xl font-bold py-3" },
|
||||
{ "h4", "text-xl font-bold py-2" },
|
||||
{ "h5", "text-lg font-bold py-1" },
|
||||
{ "p", "p-2" },
|
||||
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
|
||||
{ "ul", "list-disc pl-2" }
|
||||
};
|
||||
|
||||
private void ApplyGlobalCssStyles(IDocument document)
|
||||
{
|
||||
foreach ((string tag, string style) in _globalCssStyles)
|
||||
{
|
||||
foreach (IElement element in document.GetElementsByTagName(tag))
|
||||
{
|
||||
element.ClassList.Add(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeatifyTable(IDocument document)
|
||||
{
|
||||
foreach (IElement element in from e in document.All
|
||||
where e.LocalName == "table"
|
||||
select e)
|
||||
{
|
||||
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
|
||||
|
||||
// thead元素
|
||||
foreach (IElement headElement in from e in element.Children
|
||||
where e.LocalName == "thead"
|
||||
select e)
|
||||
{
|
||||
headElement.ClassList.Add("bg-slate-200");
|
||||
|
||||
// tr in thead
|
||||
foreach (IElement trElement in from e in headElement.Children
|
||||
where e.LocalName == "tr"
|
||||
select e)
|
||||
{
|
||||
trElement.ClassList.Add("border border-slate-300");
|
||||
|
||||
// th in tr
|
||||
foreach (IElement thElement in from e in trElement.Children
|
||||
where e.LocalName == "th"
|
||||
select e)
|
||||
{
|
||||
thElement.ClassList.Add("px-4 py-1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tbody元素
|
||||
foreach (IElement bodyElement in from e in element.Children
|
||||
where e.LocalName == "tbody"
|
||||
select e)
|
||||
{
|
||||
// tr in tbody
|
||||
foreach (IElement trElement in from e in bodyElement.Children
|
||||
where e.LocalName == "tr"
|
||||
select e)
|
||||
{
|
||||
foreach (IElement tdElement in from e in trElement.Children
|
||||
where e.LocalName == "td"
|
||||
select e)
|
||||
{
|
||||
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
public class HeadlinePostRenderProcessor(
|
||||
IConfiguration angleConfiguration,
|
||||
AngleSharp.IConfiguration angleConfiguration,
|
||||
IEssayContentService essayContentService,
|
||||
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
|
||||
{
|
||||
@@ -1,24 +1,21 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||
IOptions<BlogOptions> options)
|
||||
: IPostRenderProcessor
|
||||
{
|
||||
private static readonly IConfiguration s_configuration = Configuration.Default;
|
||||
|
||||
private readonly BlogOptions _options = options.Value;
|
||||
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(s_configuration);
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument html = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
@@ -34,7 +31,6 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||
logger.LogDebug("Found image link: '{}'", attr.Value);
|
||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
|
||||
}
|
||||
element.ClassList.Add("essay-image");
|
||||
}
|
||||
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
|
||||
}
|
||||
@@ -1,13 +1,4 @@
|
||||
using System.CommandLine;
|
||||
using YaeBlog.Commands;
|
||||
|
||||
RootCommand rootCommand = new("YaeBlog CLI");
|
||||
|
||||
rootCommand.AddServeCommand();
|
||||
rootCommand.AddNewCommand();
|
||||
rootCommand.AddListCommand();
|
||||
rootCommand.AddWatchCommand();
|
||||
rootCommand.AddScanCommand();
|
||||
rootCommand.AddPublishCommand();
|
||||
|
||||
await rootCommand.InvokeAsync(args);
|
||||
YaeBlogCommand command = new();
|
||||
await command.RunAsync(args);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class BlogChangeWatcher : IDisposable
|
||||
{
|
||||
@@ -1,7 +1,4 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public class BlogHostedService(
|
||||
ILogger<BlogHostedService> logger,
|
||||
@@ -9,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;
|
||||
}
|
||||
}
|
||||
41
YaeBlog/Services/BlogHotReloadService.cs
Normal file
41
YaeBlog/Services/BlogHotReloadService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using YaeBlog.Abstraction;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class BlogHotReloadService(
|
||||
RendererService rendererService,
|
||||
IEssayContentService essayContentService,
|
||||
BlogChangeWatcher watcher,
|
||||
ILogger<BlogHotReloadService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
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(true);
|
||||
|
||||
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
|
||||
await Task.WhenAll(reloadTasks);
|
||||
}
|
||||
|
||||
private async Task FileWatchTask(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
logger.LogInformation("Watching file changes...");
|
||||
string? changeFile = await watcher.WaitForChange(token);
|
||||
|
||||
if (changeFile is null)
|
||||
{
|
||||
logger.LogInformation("File watcher is stopping.");
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogInformation("{} changed, re-rendering.", changeFile);
|
||||
essayContentService.Clear();
|
||||
await rendererService.RenderAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,36 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public partial class EssayScanService(
|
||||
ISerializer yamlSerializer,
|
||||
@@ -23,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)
|
||||
@@ -61,10 +60,11 @@ 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()
|
||||
where file.Extension == ".md"
|
||||
where file.Extension == ".md" && !file.Name.StartsWith('.')
|
||||
select file;
|
||||
|
||||
ConcurrentBag<(string, string)> fileContents = [];
|
||||
@@ -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)
|
||||
@@ -132,13 +133,12 @@ public partial class EssayScanService(
|
||||
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
|
||||
DirectoryInfo root)
|
||||
{
|
||||
Regex imageRegex = ImageRegex();
|
||||
ConcurrentBag<FileInfo> unusedImage = [];
|
||||
ConcurrentBag<FileInfo> notFoundImage = [];
|
||||
|
||||
Parallel.ForEach(contents, content =>
|
||||
{
|
||||
MatchCollection result = imageRegex.Matches(content.FileContent);
|
||||
MatchCollection result = ImagePattern.Matches(content.FileContent);
|
||||
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
|
||||
|
||||
Dictionary<string, bool> usedDictionary;
|
||||
@@ -181,7 +181,7 @@ public partial class EssayScanService(
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
|
||||
private static partial Regex ImageRegex();
|
||||
private static partial Regex ImagePattern { get; }
|
||||
|
||||
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||
{
|
||||
@@ -3,12 +3,11 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public partial class RendererService(
|
||||
ILogger<RendererService> logger,
|
||||
@@ -22,18 +21,21 @@ 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)
|
||||
{
|
||||
uint wordCount = GetWordCount(content);
|
||||
@@ -41,6 +43,7 @@ public partial class RendererService(
|
||||
{
|
||||
Title = content.Metadata.Title ?? content.FileName,
|
||||
FileName = content.FileName,
|
||||
IsDraft = content.IsDraft,
|
||||
Description = GetDescription(content),
|
||||
WordCount = wordCount,
|
||||
ReadTime = CalculateReadTime(wordCount),
|
||||
@@ -55,7 +58,6 @@ public partial class RendererService(
|
||||
|
||||
essays.Add(essay);
|
||||
}
|
||||
});
|
||||
|
||||
ConcurrentBag<BlogEssay> postProcessEssays = [];
|
||||
Parallel.ForEach(essays, essay =>
|
||||
@@ -67,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();
|
||||
@@ -118,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)
|
||||
@@ -127,16 +140,18 @@ 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(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
||||
private static partial Regex DescriptionPattern();
|
||||
// private static partial Regex DescriptionPattern();
|
||||
private static partial Regex DescriptionPattern { get; }
|
||||
|
||||
private string GetDescription(BlogContent content)
|
||||
{
|
||||
@@ -152,7 +167,7 @@ public partial class RendererService(
|
||||
}
|
||||
|
||||
string rawContent = content.FileContent[..pos];
|
||||
MatchCollection matches = DescriptionPattern().Matches(rawContent);
|
||||
MatchCollection matches = DescriptionPattern.Matches(rawContent);
|
||||
|
||||
StringBuilder builder = new();
|
||||
foreach (Match match in matches)
|
||||
@@ -1,18 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0"/>
|
||||
<PackageReference Include="Markdig" Version="0.38.0"/>
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
|
||||
<Message Importance="low" Text="Ensure pnpm is installed..."/>
|
||||
<Exec Command="pnpm --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||
</Exec>
|
||||
|
||||
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
|
||||
|
||||
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
|
||||
<Exec Command="pnpm install"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
|
||||
<Message Importance="normal" Text="Generate css files using tailwind..."/>
|
||||
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorBootstrap
|
||||
@using YaeBlog
|
||||
@using YaeBlog.Components
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Tailwind": {
|
||||
"InputFile": "wwwroot/input.css",
|
||||
"OutputFile": "wwwroot/output.css"
|
||||
},
|
||||
"Blog": {
|
||||
"Root": "source",
|
||||
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
|
||||
@@ -24,10 +28,16 @@
|
||||
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Chenxu",
|
||||
"Name": "不会写程序的晨旭",
|
||||
"Description": "一个普通大学生",
|
||||
"Link": "https://chenxutalk.top",
|
||||
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
|
||||
},
|
||||
{
|
||||
"Name": "万木长风",
|
||||
"Description": "世界渲染中...",
|
||||
"Link": "https://ryohai.fun",
|
||||
"AvatarImage": "https://ryohai.fun/icon.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
YaeBlog/package.json
Normal file
12
YaeBlog/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "YaeBlog",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.4.16"
|
||||
}
|
||||
}
|
||||
836
YaeBlog/pnpm-lock.yaml
generated
Normal file
836
YaeBlog/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,836 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
tailwindcss:
|
||||
specifier: ^3.4.16
|
||||
version: 3.4.16
|
||||
|
||||
packages:
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/set-array@1.2.1':
|
||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@nodelib/fs.stat@2.0.5':
|
||||
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@nodelib/fs.walk@1.2.8':
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.1.0:
|
||||
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
camelcase-css@2.0.1:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
foreground-child@3.3.0:
|
||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
glob-parent@6.0.2:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-core-module@2.16.0:
|
||||
resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jiti@1.21.6:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pirates@4.0.6:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.0.0
|
||||
|
||||
postcss-js@4.0.1:
|
||||
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
|
||||
engines: {node: ^12 || ^14 || >= 16}
|
||||
peerDependencies:
|
||||
postcss: ^8.4.21
|
||||
|
||||
postcss-load-config@4.0.2:
|
||||
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
|
||||
engines: {node: '>= 14'}
|
||||
peerDependencies:
|
||||
postcss: '>=8.0.9'
|
||||
ts-node: '>=9.0.0'
|
||||
peerDependenciesMeta:
|
||||
postcss:
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
|
||||
postcss-nested@6.2.0:
|
||||
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.2.14
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.4.49:
|
||||
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
resolve@1.22.9:
|
||||
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
|
||||
hasBin: true
|
||||
|
||||
reusify@1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.0:
|
||||
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
sucrase@3.35.0:
|
||||
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
tailwindcss@3.4.16:
|
||||
resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yaml@2.6.1:
|
||||
resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/set-array@1.2.1': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
run-parallel: 1.2.0
|
||||
|
||||
'@nodelib/fs.stat@2.0.5': {}
|
||||
|
||||
'@nodelib/fs.walk@1.2.8':
|
||||
dependencies:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.17.1
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.1.0: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
braces: 3.0.3
|
||||
glob-parent: 5.1.2
|
||||
is-binary-path: 2.1.0
|
||||
is-glob: 4.0.3
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fastq@1.17.1:
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
foreground-child@3.3.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob-parent@6.0.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob@10.4.5:
|
||||
dependencies:
|
||||
foreground-child: 3.3.0
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
|
||||
is-core-module@2.16.0:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jiti@1.21.6: {}
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
mz@2.7.0:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pirates@4.0.6: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.4.49):
|
||||
dependencies:
|
||||
postcss: 8.4.49
|
||||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.9
|
||||
|
||||
postcss-js@4.0.1(postcss@8.4.49):
|
||||
dependencies:
|
||||
camelcase-css: 2.0.1
|
||||
postcss: 8.4.49
|
||||
|
||||
postcss-load-config@4.0.2(postcss@8.4.49):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
yaml: 2.6.1
|
||||
optionalDependencies:
|
||||
postcss: 8.4.49
|
||||
|
||||
postcss-nested@6.2.0(postcss@8.4.49):
|
||||
dependencies:
|
||||
postcss: 8.4.49
|
||||
postcss-selector-parser: 6.1.2
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.49:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
resolve@1.22.9:
|
||||
dependencies:
|
||||
is-core-module: 2.16.0
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
reusify@1.0.4: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.0:
|
||||
dependencies:
|
||||
ansi-regex: 6.1.0
|
||||
|
||||
sucrase@3.35.0:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.6
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tailwindcss@3.4.16:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
arg: 5.0.2
|
||||
chokidar: 3.6.0
|
||||
didyoumean: 1.2.2
|
||||
dlv: 1.1.3
|
||||
fast-glob: 3.3.2
|
||||
glob-parent: 6.0.2
|
||||
is-glob: 4.0.3
|
||||
jiti: 1.21.6
|
||||
lilconfig: 3.1.3
|
||||
micromatch: 4.0.8
|
||||
normalize-path: 3.0.0
|
||||
object-hash: 3.0.0
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.4.49
|
||||
postcss-import: 15.1.0(postcss@8.4.49)
|
||||
postcss-js: 4.0.1(postcss@8.4.49)
|
||||
postcss-load-config: 4.0.2(postcss@8.4.49)
|
||||
postcss-nested: 6.2.0(postcss@8.4.49)
|
||||
postcss-selector-parser: 6.1.2
|
||||
resolve: 1.22.9
|
||||
sucrase: 3.35.0
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
|
||||
thenify@3.3.1:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
yaml@2.6.1: {}
|
||||
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
@@ -0,0 +1,884 @@
|
||||
---
|
||||
title: async/await究竟是如何工作的?
|
||||
tags:
|
||||
- dotnet
|
||||
- 技术笔记
|
||||
- 译文
|
||||
---
|
||||
|
||||
### 译者按
|
||||
|
||||
如何正确而快速的编写异步运行的代码一直是软件工程界的难题,而C#提出的`async/await`范式无疑是探索道路上的先行者。本篇文章便是翻译自.NET开发者博客上一篇名为“How async/await really works in C#”的文章,希望能够让读者在阅读之后明白`async/await`编程范式的前世今生和`.NET`实现方式。另外,.Net开发者中文博客也翻译了[这篇文章](https://devblogs.microsoft.com/dotnet-ch/async-await%e5%9c%a8-c%e8%af%ad%e8%a8%80%e4%b8%ad%e6%98%af%e5%a6%82%e4%bd%95%e5%b7%a5%e4%bd%9c%e7%9a%84/),一并供读者参考。
|
||||
|
||||
---
|
||||
|
||||
数周前,[.NET开发者博客](https://devblogs.microsoft.com/dotnet/)发布了一篇题为[什么是.NET,为什么你应该选择.NET](https://devblogs.microsoft.com/dotnet/why-dotnet/)的文章。文章中从宏观上概览了整个`dotnet`生态系统,总结了系统中的各个部分和其中的设计决定;文章还承诺在未来推出一系列的深度文章介绍涉及到的方方面面。这篇文章便是这系列文章中的第一篇,深入介绍C#和.NET中`async/await`的历史、设计决定和实现细节。
|
||||
|
||||
对于`async/await`的支持大约在十年前就提供了。在这段时间里,`async/await`语法大幅改变了编写可扩展.NET代码的方式,同时该语法使得在不了解`async/await`工作原理的情况下使用它提供的功能编写异步代码也是十分容易和常见的。以下面的**同步**方法为例:(因为这个方法的调用者在整个操作完成之前、将控制权返回给它之前都不能进行任何操作,所以这个方法被称为**同步**)
|
||||
|
||||
```csharp
|
||||
// 将数据同步地从源复制到目的地
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个方法的基础上,你只需要修改几个关键词、改变几个方法的名称,就可以得到一个**异步**的方法(因为这个方法将很快,往往实在所有的工作完成之前,就会将控制权返回给它的调用者,所以被称作异步方法)。
|
||||
|
||||
```csharp
|
||||
// 将数据异步地从源复制到目的地
|
||||
public async Task CopyStreamToStreamAsync(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有着几乎相同的语法,类似的控制流结构,但是现在这个方法在执行过程中不会阻塞,有着完全不同的底层执行模型,而且C#编译器和核心库帮你完成所有这些复杂的工作。
|
||||
|
||||
尽管在不了解底层原理的基础上使用这类技术是十分普遍的,但是我们坚持认为了解这些事务的运行原理将会帮助我们更好的利用它们。之于`async/await`,了解这些原理将在你需要深入探究时十分有用,例如当你需要调试一段错误的代码或者优化某段正确运行代码的运行效率时。在这篇文章中,我们将深入了解`async/await`具体如何在语言、编译器和库层面运行,然后你将更好地利用这些优秀的设计。
|
||||
|
||||
为了更好的理解这一切,我们将回到没有`async/await`的时代,看看在没有它们的情况下最优秀的异步代码是如何编写的。平心而论,这些代码看上去并不好。
|
||||
|
||||
### 原初的历史
|
||||
|
||||
回到.NET框架1.0时代,当时流行的异步编程范式是**异步编程模型**,“Asynchronous Programming Model”,也被称作`APM`范式、`Being/End`范式或者`IAsyncResult`范式。从宏观上来看,这种范式是相当简单的。例如对于一个同步操作`DoStuff`:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
}
|
||||
```
|
||||
|
||||
在这种编程模型下会存在两个相关的方法:一个`BeginStuff`方法和一个`EndStuff`方法:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
|
||||
public int EndDoStuff(IAsyncResult asyncResult);
|
||||
}
|
||||
```
|
||||
|
||||
`BeginStuff`方法首先会接受所有`DoStuff`方法会接受的参数,同时其会接受一个`AsyncCallback`回调和一个**不透明**的状态对象`state`,而且这两个参数都可以为空。这个“开始”方法将负责异步操作的初始化,而且如果提供了回调函数,这个函数还会负责在异步操作完成之后调用这个回调函数,因此这个回调函数也常常被称为初始化操作的“下一步”。开始方法还会负责构建一个实现了`IAsyncResult`接口的对象,这个对象中的`AsyncState`属性由可选的`state`参数提供:
|
||||
|
||||
```csharp
|
||||
namespace System
|
||||
{
|
||||
public interface IAsyncResult
|
||||
{
|
||||
object? AsyncState { get; }
|
||||
WaitHandle AsyncWaitHandle { get; }
|
||||
bool IsCompleted { get; }
|
||||
bool CompletedSynchronously { get; }
|
||||
}
|
||||
|
||||
public delegate void AsyncCallback(IAsyncResult ar);
|
||||
}
|
||||
```
|
||||
|
||||
这个`IAsynResult`实例将会被开始方法返回,在调用`AsyncCallback`时这个实例也会被传递过去。当准备好使用该异步操作的结果时,调用者也会将这个`IAsyncResult`实例传递给结束方法,同时结束方法也会负责保证这个异步操作完成,如果没有完成该方法就会阻塞代码的运行直到完成。结束方法会返回异步操作的结果,异步操作过程中引发的各种错误和异常也会通过该方法传递出来。因此,对于下面这种同步的操作:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int i = handler.DoStuff(arg);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 在这里处理DoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
可以使用开始/结束方法改写为异步运行的形式:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // 处理从EndDoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
}, handler);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 处理从同步调用BeginDoStuff方法引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
对于熟悉使用含有回调`API`语言的开发者来说,这样的代码应该会显得相当眼熟。
|
||||
|
||||
但是事情在这里变得更加复杂了。例如,这段代码存在“栈堆积”`stack dive`的问题。栈堆积就是代码在重复的调用方法中使得栈越来越深,直到发生栈溢出的现象。如果“异步”操作同步完成,开始方法将会使同步的调用回调方法,这就意味着对于开始方法的调用就会直接调用回调方法。同时考虑到“异步”方法同步完成却是一种非常常见的现象,它们只是承诺会异步的完成操作而不是只被允许异步的完成。例如一个对于某个网络操作的异步操作,比如读取一个套接字,如果你只需要从一次操作中读取少量的数据,例如在一次回答中只需要读取少量响应头的数据,你可能会直接读取大量数据存储在缓冲区中。相比于每次使用都使用系统调用但是只读取少量的数据,你一次读取了大量数据在缓冲区中,并在缓冲区失效之前都是从缓冲区中读取,这样就减少了需要调用昂贵的系统调用来和套接字交互的次数。像这样的缓冲区可能在你进行任何异步调用之后存在,例如第一次操作异步的完成对于缓冲区的填充,之后的若干次“异步”操作都不需要同I/O进行任何交互而直接通过与缓冲区的同步交互完成,直到缓冲区失效之后再次异步的填充缓冲区。因此当开始方法进行上述的一次调用时,开始方法会发现操作同步地完成了,因此开始方法同步地调用回调方法。此时,你有一个调用了开始方法的栈帧和一个调用了回调方法的栈帧。想想看如果回调方法再次调用了开始方法会发生什么?如果开始方法和回调方法都是被同步调用的,现在你就会在站上得到多个重复的栈帧,如此重复下去直到将栈上的空间耗尽。
|
||||
|
||||
这并不是杞人忧天,使用下面这段代码就可以很容易的复现这个问题:
|
||||
|
||||
```csharp
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
listener.Listen();
|
||||
|
||||
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
client.Connect(listener.LocalEndPoint!);
|
||||
|
||||
using Socket server = listener.Accept();
|
||||
_ = server.SendAsync(new byte[100_000]);
|
||||
|
||||
var mres = new ManualResetEventSlim();
|
||||
byte[] buffer = new byte[1];
|
||||
|
||||
var stream = new NetworkStream(client);
|
||||
|
||||
void ReadAgain()
|
||||
{
|
||||
stream.BeginRead(buffer, 0, 1, iar =>
|
||||
{
|
||||
if (stream.EndRead(iar) != 0)
|
||||
{
|
||||
ReadAgain(); // uh oh!
|
||||
}
|
||||
else
|
||||
{
|
||||
mres.Set();
|
||||
}
|
||||
}, null);
|
||||
};
|
||||
ReadAgain();
|
||||
|
||||
mres.Wait();
|
||||
```
|
||||
|
||||
在代码中我们建立一个简单的客户端套接字和一个简单的服务端套接字并让它们连接。服务端会向客户端发送十万字节的信息,而客户端会使用开始/结束方法尝试去“异步的”接收这些信息(需要注意这样做是十分低效的,在教学实例之外的地方都不应该这样编写代码)。传递给`BeingRead`的回调函数通过调用`EndRead`方法停止读取,如果在读取过程中读取到数据(意味着还没有读取完成),就通过对于本地方法`ReadAgain`的递归调用来再次调用`BeingRead`方法继续读取。值得指出的是,在.NET Core中套接字操作比原来在.NET Framework中的版本快上许多,同时如果操作系统可以同步的完成这些操作,那么.NET Core中的操作也会同步完成(需要注意操作系统内核也有一个缓冲区来完成套接字接收操作)。因此,运行这段代码就会出现栈溢出。
|
||||
|
||||
鉴于这个问题非常容易出现,因此`APM`模型中内建了缓解这个问题的方法。容易想到有两种方法可以缓解这个问题:
|
||||
|
||||
1. 不允许`AsyncCallback`被同步调用。如果该回调方法始终都是被异步调用的,即使操作是异步完成的,栈堆叠的方法也就不存在了。但是这样做会降低性能,因为同步完成的操作(或者快到难以注意到的操作)是相当的常见的,强制这些操作的回调排队完成会增加相当可观的开销。
|
||||
2. 引入一个机制让调用者而不是回调函数在工作异步完成时完成剩余的工作。在这种情况下,我们就避免了引入额外的栈帧,在不增加栈深度的情况下完成了余下的工作。
|
||||
|
||||
`APM`模型使用了第二种方法。为了实现这个方法,`IAsyncResult`接口提供了另外两个成员:`IsCompleted`和`CompletedSynchronusly`。`IsCompeleted`成员告诉我们操作是否完成,在程序中可以反复检查这个成员直到它从`false`变成`true`。相对的,`CompletedSynchronously`在运行过程中不会变化,(或者它存在一个还未被发现的`bug`会导致这个值变化,笑),这个值的主要作用是判断后续的工作是应该由开始方法的调用者还是`AsyncCallback`来进行。如果`CompletedSynchronously`的值是`false`,说明这个操作是异步进行的,所有后续的工作应该由回调函数来进行处理;毕竟,如果工作是异步完成的,开始方法的调用者不能知道工作是何时完成的(如果开始方法的调用者调用了结束方法,那么结束方法就会阻塞直到工作完成)。反之,如果`CompletedSynchronously`的值是`true`,如果此时使用回调方法处理后续的工作就会引发栈堆叠问题,因为此时回调方法会在栈上比开始它更低的位置上进行后续的操作。因此任何在意栈堆叠问题的实现需要关注`CompletedSynchronously`的值,当为真的时候,让开始方法的调用者处理后续的工作,而回调方法在此时不应处理任何工作。这也是为什么`CompletedSynchronously`的值不能改变——开始方法的调用者和回调方法需要相同的值来保证后续工作在任何情况下都进行且只进行一次。
|
||||
|
||||
因此我们之前的`DoStuff`实例就需要被修改为:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
IAsyncResult ar = handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // handle exceptions from EndDoStuff and Use
|
||||
}
|
||||
}
|
||||
}, handler);
|
||||
if (ar.CompletedSynchronously)
|
||||
{
|
||||
int i = handler.EndDoStuff(ar);
|
||||
Use(i);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // handle exceptions that emerge synchronously from BeginDoStuff and possibly EndDoStuff/Use
|
||||
}
|
||||
```
|
||||
|
||||
这里的代码已经~~显得冗长~~,而且我们还只研究了如何使用这种范式,还没有涉及如何实现这种范式。尽管大部分的开发者并不需要在这些子调用(例如实现`Socket.BeginReceive/EndReceive`这些方法去和操作系统交互),但是很多开发者需要组合这些操作(从一个“较大的”的异步操作调用多个异步操作),而这不仅需要使用其他的开始/结束方法,还需要自行实现你自己的开始/结束方法,这样你才能在其他的地方使用这个操作。同时,你还会注意到在上述的`DoStuff`范例中没有任何的控制流代码。如果需要引入一些控制流代码——即使是一个简单的循环——这也会立刻变成~~抖M才会编写的代码~~,同时也给无数的博客作者提供水`CSDN`的好题材。
|
||||
|
||||
所以让我们现在就来写一篇`CSDN`,给出一个完成的实例。在文章的开头我展示了一个`CopyStreamToStream`方法,这个方式会将一个流中的数据复制到另外一个流中(就是`Stream.CopyTo`方法所完成的工作,但是为了说明,让我们假设这个方法并不存在):
|
||||
|
||||
```csharp
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
直白的说,我们只需要不停的从一个流中读取数据然后写入到另外一个流中,直到我们没法从第一个流中读取到任何数据。现在让我们使用`APM`模型使用这个操作的异步模式吧:
|
||||
|
||||
```csharp
|
||||
public IAsyncResult BeginCopyStreamToStream(
|
||||
Stream source, Stream destination,
|
||||
AsyncCallback callback, object state)
|
||||
{
|
||||
var ar = new MyAsyncResult(state);
|
||||
var buffer = new byte[0x1000];
|
||||
|
||||
Action<IAsyncResult?> readWriteLoop = null!;
|
||||
readWriteLoop = iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (bool isRead = iar == null; ; isRead = !isRead)
|
||||
{
|
||||
if (isRead)
|
||||
{
|
||||
iar = source.BeginRead(buffer, 0, buffer.Length, static readResult =>
|
||||
{
|
||||
if (!readResult.CompletedSynchronously)
|
||||
{
|
||||
((Action<IAsyncResult?>)readResult.AsyncState!)(readResult);
|
||||
}
|
||||
}, readWriteLoop);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int numRead = source.EndRead(iar!);
|
||||
if (numRead == 0)
|
||||
{
|
||||
ar.Complete(null);
|
||||
callback?.Invoke(ar);
|
||||
return;
|
||||
}
|
||||
|
||||
iar = destination.BeginWrite(buffer, 0, numRead, writeResult =>
|
||||
{
|
||||
if (!writeResult.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination.EndWrite(writeResult);
|
||||
readWriteLoop(null);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destination.EndWrite(iar);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
};
|
||||
|
||||
readWriteLoop(null);
|
||||
|
||||
return ar;
|
||||
}
|
||||
|
||||
public void EndCopyStreamToStream(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is not MyAsyncResult ar)
|
||||
{
|
||||
throw new ArgumentException(null, nameof(asyncResult));
|
||||
}
|
||||
|
||||
ar.Wait();
|
||||
}
|
||||
|
||||
private sealed class MyAsyncResult : IAsyncResult
|
||||
{
|
||||
private bool _completed;
|
||||
private int _completedSynchronously;
|
||||
private ManualResetEvent? _event;
|
||||
private Exception? _error;
|
||||
|
||||
public MyAsyncResult(object? state) => AsyncState = state;
|
||||
|
||||
public object? AsyncState { get; }
|
||||
|
||||
public void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_completed = true;
|
||||
_error = error;
|
||||
_event?.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Wait()
|
||||
{
|
||||
WaitHandle? h = null;
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
h = _event ??= new ManualResetEvent(false);
|
||||
}
|
||||
|
||||
h.WaitOne();
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
}
|
||||
|
||||
public WaitHandle AsyncWaitHandle
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _event ??= new ManualResetEvent(_completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompletedSynchronously
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completedSynchronously == 0)
|
||||
{
|
||||
_completedSynchronously = _completed ? 1 : -1;
|
||||
}
|
||||
|
||||
return _completedSynchronously == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
~~Yowsers~~。即使写完了这些繁文缛节,这实际上仍然不是一个完美的实现。例如,`IAsyncResult`的实现会在每次操作时上锁,而不是在任何可能的时候都使用无锁的实现;异常也是以原始的模型存储,如果使用`ExceptionDispatchInfo`可以让异常在传播的过程中含有调用栈的信息,在每次操作中都分配了大量的空间来存储变量(例如在每次`BeingWrite`调用时都会分配一片空间来存储委托),如此等等。现在想象这就是你每次编写方法时需要做的工作,每次当你需要编写一个可重用的异步方法来使用另外一个异步方法时,你需要自己完成上述所有的工作。而且如果你需要编写使用多个不同的`IAsyncResult`的可重用代码——就像在`async/await`范式中`Task.WhenAll`所完成的那样,难度又上升了一个等级;每个不同操作都会实现并暴露针对相关的`API`,这让编写一套逻辑代码并简单的复用它们也变得不可能(尽管一些库作者可能会通过提供一层针对回调方法的新抽象来方便开发者编写需要访问暴露`API`的回调方法)。
|
||||
|
||||
上述这些复杂性也说明只有很少的一部分人尝试过这样编写代码,而且对于这些人来说,`bug`也往往如影随形。而且这并不是一个`APM`范式的黑点,这是所有使用基于回调的异步方法都具有的缺点。我们已经十分习惯现代语言都有的控制流结构所带来的强大和便利,因此使用会破坏这种结构的基于回调的异步方式会带来大量的复杂性也是可以理解的。同时,也没有任何主流的语言提供了更好的替代。
|
||||
|
||||
我们需要一种更好的办法,一个既继承了我们在`APM`范式中所学习到所有经验也规避了其所有的各种缺点的方式。一个有趣的点是,`APM`范式只是一种编程范式,运行时、核心库和编译器在使用或者实现这种范式的过程中没有提供任何协助。
|
||||
|
||||
### 基于事件的异步范式
|
||||
|
||||
在.NET Framework 2.0中提供了一系列的`API`来实现一种不同的异步编程范式,当时设想这种范式的主要应用场景是客户端应用程序。这种基于事件的异步范式,也被称作`EAP`范式,也是以提供一系列成员的方式提供的,包含一个用于初始化异步操作的方式和一个监听异步操作是否完成的事件。因此上述的`DoStuff`示例可能会暴露如下的一系列成员:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
首先通过`DoStuffCompleted`事件注册需要在完成异步操作时进行的工作然后调用`DoStuff`方法,这个方法将初始化异步操作,一旦异步操作完成,`DoStuffCompleted`事件将会被调用者引发。已经注册的回调方法可以运行剩余的工作,例如验证提供的`userToken`是否是期望的`userToken`,同时我们可以注册多个回调方法在异步操作完成的时候运行。
|
||||
|
||||
这个范式确实让一系列用例的编写更好编写,同时也让一系列用例变得更加复杂(例如上述的`CopyStreamToStream`例子)。这种范式的影响范围并不大,只在一次.NET Framework的更新中引入便匆匆地消失了,除了留下了一系列为了支持这种范式而实现的`API`,例如:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
但是这种编程范式确实在`APM`范式所没有注意到的地方前进了一大步,并且这一点还保留到了我们今天所介绍的模型中:[同步上下文](https://github.com/dotnet/runtime/blob/967a59712996c2cdb8ce2f65fb3167afbd8b01f3/src/libraries/System.Private.CoreLib/src/System/Threading/SynchronizationContext.cs#L6) (`SynchronizationContext`)。
|
||||
|
||||
同步上下文作为一个对于通用调度器的实现,也是在.NET Framework中引入的。在实践中,同步上下文最常用的方法是`Post`,这个方法将一个工作实现传递给上下文所代表的一种调度器。举例来说,一个基础的同步上下文实现是一个线程池`ThreadPool`,因此`Post`方法的典型实现就是`ThreadPool.QueueUserWorkItem`方法,这个方法将让线程池在池中任意的线程上以指定的状态调用指定的委托。然而,同步上下文的巧妙之处不仅在于提供了对于不同调度器的支持,而是提供了一种针对不同的应用模型使用不同调度方法的抽象能力。
|
||||
|
||||
考虑像Windows Forms之类的`UI`框架。对于大多数工作在Windows上的`UI`框架来说,控件往往关联到一个特定的线程,这个线程负责运行一个消息管理中心,这个中心用来运行那些需要同控件交互的工作:只有这个控件有能力来修改控件,任何其他试图同控件进行交互的线程都需要发送消息到这个消息控制中心。Windows Forms通过一系列方法来实现这一点,例如`Control.BeingInvoke`,这类方法将会把提供的委托和参数传递给同这个控件相关联的线程来运行。你可以写出如下的代码:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.BeginInvoke(() =>
|
||||
{
|
||||
button1.Text = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
这段代码首先将`ComputeMessage`方法交给线程池中的一个线程运行(这样可以保证该方法在运行时`UI`界面不会卡死),当上述工作完成之后,再将一个更新`button1`标签的委托传递给关联到`button1`的线程运行。简单而易于理解。在`WPF`框架中也是类似的逻辑,使用一个被称为`Dispatcher`的类型:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
button1.Content = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`.NET MAUI`亦然。但是如果我想将这部分的逻辑封装到一个独立的辅助函数中,例如下面这种:
|
||||
|
||||
```csharp
|
||||
// 调用ComputeMessage然后触发更新逻辑
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }
|
||||
```
|
||||
|
||||
这样我就可以直接:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ComputeMessageAndInvokeUpdate(message => button1.Text = message);
|
||||
}
|
||||
```
|
||||
|
||||
但是`ComputerMessageAndInvokeUpdate`应该如何实现才能适配各种类型的应用程序呢?难道需要硬编码所有可能涉及的`UI`框架吗?这就是`SynchronizationContext`大显神威的地方,我们可以这样实现这个方法:
|
||||
|
||||
```csharp
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update)
|
||||
{
|
||||
SynchronizationContext? sc = SynchronizationContext.Current;
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
if (sc is not null)
|
||||
{
|
||||
sc.Post(_ => update(message), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
update(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
在这个实现中将`SynchronizationContext`作为同`UI`进行交互的调度器之抽象。任何应用程序模型都需要保证在`SynchronizationContext.Current`属性上注册一个继承了`SynchronizationContext`的类,这个就会完成调度相关的工作。例如在`Windows Forms`中:
|
||||
|
||||
```csharp
|
||||
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state) =>
|
||||
_controlToSendTo?.BeginInvoke(d, new object?[] { state });
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
在`WPF`中有:
|
||||
|
||||
```
|
||||
public sealed class DispatcherSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, Object state) =>
|
||||
_dispatcher.BeginInvoke(_priority, d, state);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`ASP.NET`*曾经*也有过一个实现,尽管Web框架实际上并不关心是哪个线程在运行指定的工作,但是非常关心指定工作和那个请求相关,因此该实现主要负责保证多个线程不会在同时访问同一个`HttpContext`。
|
||||
|
||||
```csharp
|
||||
internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase
|
||||
{
|
||||
public override void Post(SendOrPostCallback callback, Object state) =>
|
||||
_state.Helper.QueueAsynchronous(() => callback(state));
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这个概念也并不局限于像上面的主流应用程序模型。例如在[xunit](https://github.com/xunit/xunit),一个流行的单元测试框架(`.NET`核心代码仓库也使用了)中也实现了需要自定义的`SynchronizationContext`。例如限制同步运行单元测试时同时运行单元测试数量就可以用`SynchroniaztionContext`实现:
|
||||
|
||||
```
|
||||
public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state)
|
||||
{
|
||||
var context = ExecutionContext.Capture();
|
||||
workQueue.Enqueue((d, state, context));
|
||||
workReady.Set();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`MaxConcurrentSyncContext`中的`Post`方法只是将需要完成的工作压入其内部的工作队列中,这样就能够控制同时多少工作能够并行的运行。
|
||||
|
||||
那么同步上下文这个概念时如何同基于事件的异步范式关联起来的呢?`EAP`范式和同步上下文都是在同一时间引入的,而`EAP`范式要求当异步操作启动的时候,完成事件需要由当前`SynchronizationContext`进行调度。为了简化这个过程(可能反而引入多余的复杂性),在`System.ComponentModel`命名控件中引入了一些帮助程序,具体来说是`AsyncOperation`和`AsyncOperationManager`。其中前者是一个由用户提供的状态对象和捕获到的`SynchronizationContext`组成的元组,后者是一个捕获`SynchronizationContext`和创建`AsyncOperation`对象的工厂类。`EAP`范式会在实现中使用上述帮助类,例如`Ping.SendAsync`会首先调用`AsyncOperationManager.CreateOperationi`来捕获同步上下文,然后当异步操作完成的时候调用`AsyncOperation.PostOperationCompleted`方法来调用捕获到的`SynchronizationContext.Post`方法。
|
||||
|
||||
`SynchronizationContext`还提供了其他一些后面会用到的小工具。这个类暴露了`OperationStarted`和`OperationCompleted`两个方法。这个虚方法在基类中的实现都是空的,并不完成任何工作。但是继承其的实现可能会重载这些来了解运行中的操作。`EAP`的实现就会在每个操作开始和结束的时候调用`OperationStarted`和`OperationCompleted`,来方便可能存在的同步上下文跟踪工作的进度。鉴于在`EAP`范式中启动异步操作的方法往往不会返回任何东西,不能指望可以获得任何帮助你跟踪工作进度的东西,因而可能获得工作进度的同步上下文就显得很有价值了。
|
||||
|
||||
综上所说,我们需要一些比`APM`编程范式更好的东西,而`EAP`范式引入了一些新的东西,但是没有解决我们面对的核心问题,我们仍然需要一些更好的东西。
|
||||
|
||||
### 进入Task时代
|
||||
|
||||
在.NET Framework 4.0中引入了`System.Threading.Tasks.Task`类型。当时`Task`类型还只代表某些异步操作的最终完成(在其他编程框架中可能成称为`promise`或者`future`)。当一个操作开始时,创建一个`Task`来表示这个操作,当这个操作完成之后,操作的结果就会被保存在这个`Task`中。简单而明确。但是`Task`相较于`IAsyncResult`提供的重要特点是其蕴含了一个任务在持续运行的状态。这个特点让你能够随意找到一个`Task`,让它在异步操作完成的时候异步的通知你,而不用你关注任务当前是处在已经完成、没有完成、正在完成等各种状态。为什么这点非常重要?首先想想`APM`范式中存在的两个主要问题:
|
||||
|
||||
1. 你需要对每个操作实现一个自定义的`IAsycResult`实现:库中没有任何内置开箱即用的`IAsycResult`实现。
|
||||
2. 你需要在调用开始方法之前就知道在操作结束的时候需要做什么。这让编写使用任意异步操作的组合代码或者通用运行时非常困难。
|
||||
|
||||
相对的,`Task`提供了一个通用的接口让你在启动一个异步操作之后“接触”这个操作,还提供了针对“持续”的抽象,这样你就不需要为启动异步操作的方法提供一个持续性。任何需要进行异步操作的人都可以产生一个`Task`,任何人需要使用异步操作的人都可以使用一个`Task`,在这个过程中不用自定义任何东西,`Task`成为了沟通异步操作的生产者和消费者之间最重要的桥梁。这一点大大改变了.NET框架。
|
||||
|
||||
现在让我们深入理解`Task`所带来的重要意义。与其直接去研究错综复杂的`Task`源代码,我们将尝试去实现一个`Task`的简单版本。这不会是一个完善的实现,只会完成基础的功能来让我们更好的理解什么是`Task`,即一个负责协调设置和存储完成信号的数据结构。
|
||||
|
||||
开始时`Task`中只有很少的字段:
|
||||
|
||||
```csharp
|
||||
class MyTask
|
||||
{
|
||||
private bool _completed;
|
||||
private Exception? _error;
|
||||
private Action<MyTask>? _continuation;
|
||||
private ExecutionContext? _ec;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
我们首先需要一个字段告诉我们任务是否完成`_completed`,一个字段存储造成任务执行失败的错误`_error`;如果我们需要实现一个泛型的`MyTask<TResult>`,还需要一个`private TResult _result`字段来存储操作运行完成之后的结果。到目前为止的实现和`IAsyncResult`相关的实现非常类似(当然这不是一个巧合)。`_continuation`字段时实现中最重要的字段。在这个简单的实现中,我们只支持一个简单的后续过程,在真正的`Task`实现中是一个`object`类型的字段,这样既可以是一个独立的后续过程,也可以是一个后续过程的列表。这个委托会在任务完成的时候调用。
|
||||
|
||||
让我们继续深入。如上所述,`Task`相较于之前的异步执行模型一个基础的优势是在异步操作开始之后再提供后续需要完成的工作。因此我们需要一个方法来实现这个功能:
|
||||
|
||||
```csharp
|
||||
public void ContinueWith(Action<MyTask> action)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ => action(this));
|
||||
}
|
||||
else if (_continuation is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation = action;
|
||||
_ec = ExecutionContext.Capture();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果在调用`ContinueWith`的时候异步操作已经完成,那么就直接将该委托的执行加入执行队列。反之,这个方法就会将存储这个委托,当异步任务完成的时候进行执行(这个方法同时也存储一个被称为`ExecutionContext`的对象,会在后续调用委托的涉及到,我们后续会继续介绍)。
|
||||
|
||||
然后我们需要能够在异步过程完成的时候标记任务已经完成。我们将添加两个方法,一个负责标记任务成功完成,一个负责标记任务报错退出。
|
||||
|
||||
```csharp
|
||||
public void SetResult() => Complete(null);
|
||||
|
||||
public void SetException(Exception error) => Complete(error);
|
||||
|
||||
private void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
throw new InvalidOperationException("Already completed");
|
||||
}
|
||||
|
||||
_error = error;
|
||||
_completed = true;
|
||||
|
||||
if (_continuation is not null)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
if (_ec is not null)
|
||||
{
|
||||
ExecutionContext.Run(_ec, _ => _continuation(this), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们会存储任何的错误、标记任务已经完成,如果已经注册的任何的后续过程,我们也会引发其进行执行。
|
||||
|
||||
最后我们还需要一个方法将在工作中发生的任何传递出来,(如果是泛型类型,还需要将执行结果返回),为了方便某些特定的场景,我们将允许这个方法阻塞直到异步操作完成(通过调用`ContinueWith`注册一个`ManualResetEventSlim`实现)。
|
||||
|
||||
```csharp
|
||||
public void Wait()
|
||||
{
|
||||
ManualResetEventSlim? mres = null;
|
||||
lock (this)
|
||||
{
|
||||
if (!_completed)
|
||||
{
|
||||
mres = new ManualResetEventSlim();
|
||||
ContinueWith(_ => mres.Set());
|
||||
}
|
||||
}
|
||||
|
||||
mres?.Wait();
|
||||
if (_error is not null)
|
||||
{
|
||||
ExceptionDispatchInfo.Throw(_error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这就是一个基础的`Task`实现。当然需要指出的是实际的`Task`会复杂很多:
|
||||
|
||||
- 支持设置任意数量的后续工作;
|
||||
- 支持配置其的工作行为(例如配置后续工作是应该进入工作队列等待执行还是作为任务完成的一部分同步被调用);
|
||||
- 支持存储多个错误;
|
||||
- 支持取消异步操作;
|
||||
- 一系列的帮助函数(例如`Task.Run`创建一个代表在线程池上运行委托的`Task`)。
|
||||
|
||||
但是这些内容中没有什么奥秘,核心工作原理和我们自行实现的是一样的。
|
||||
|
||||
你可以会注意到我们自行实现的`MyTask`直接公开了`SetResult/SetException`方法,而`Task`没有;这是因为`Task`是以`internal`声明了上述两个方法,同时`System.Threading.Tasks.TaskCompletionSource`类型负责作为一个独立的`Task`生产者和管理任务的完成。这样做的目的并不是出于技术目的,只是将负责控制完成的方法从消费`Task`的方法中分离出来。这样你就可以通过保留`TaskCompletionSource`对象来控制`Task`的完成,不必担心你创建的`Task`在你不知道的地方被完成。(`CancellationToken`和`CanellationTokenSource`也是处于同样的设计考虑,`CancellationToken`是一个包装`CancellationTokenSource`的结构,只暴露了和接受消费信号相关的结构而缺少产生一个取消信号的能力,这样就限制只有`CancellationToeknSource`可以产生取消信号。)
|
||||
|
||||
当前我们也可以像`Task`一样为我们自己的`MyTask`添加各种工具函数。例如我们添加一个`MyTask.WhenAll`:
|
||||
|
||||
```csharp
|
||||
public static MyTask WhenAll(MyTask t1, MyTask t2)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
int remaining = 2;
|
||||
Exception? e = null;
|
||||
|
||||
Action<MyTask> continuation = completed =>
|
||||
{
|
||||
e ??= completed._error; // just store a single exception for simplicity
|
||||
if (Interlocked.Decrement(ref remaining) == 0)
|
||||
{
|
||||
if (e is not null) t.SetException(e);
|
||||
else t.SetResult();
|
||||
}
|
||||
};
|
||||
|
||||
t1.ContinueWith(continuation);
|
||||
t2.ContinueWith(continuation);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
然后是一个`MyTask.Run`的示例:
|
||||
|
||||
```csharp
|
||||
public static MyTask Run(Action action)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
t.SetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
t.SetException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
还有一个简单的`MyTask.Delay`:
|
||||
|
||||
```csharp
|
||||
public static MyTask Delay(TimeSpan delay)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
var timer = new Timer(_ => t.SetResult());
|
||||
timer.Change(delay, Timeout.InfiniteTimeSpan);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
在`Task`横空出世之后,之前的所有异步编程范式都成为了过去式。任何使用过去的编程范式暴露的异步`API`,现在都提供了返回`Task`的方法。
|
||||
|
||||
### 添加Value Task
|
||||
|
||||
直到现在,`Task`都是.NET异步编程中的主力军,在每次新版本发布或者社区发布的新`API`都会返回`Task`或者`Task<TResult>`。但是,`Task`是一个类,而每次创建一个类是都需要分配一次内存。在大多数情况下,为一个会长期存在的异步操作进行一次内存分配时无关紧要的,并不会操作明显的性能影响。但是正如之前所说的,同步完成的异步操作十分创建。例如,`Stream.ReadAsync`会返回一个`Task<int>`,但是如果是在一个类似与`BufferedStream`的实现上调用该方法,那么你的调用由很大概率就会是同步完成的,因为大多数读取只需要从内存中的缓冲区中读取数据而不需要通过系统调用访问`I/O`。在这种情况下还需要分配一个额外的对象显然是不划算的(而且在`APM`范式中也存在这个问题)。对于返回非泛型类型的方法来说,还可以通过返回一个预先分配的已完成单例来缓解这个问题,而且`Task`也提供了一个`Task.CompletedTask`。但是对于泛型的`Task<TResult>`则不行,因为不可能针对每个不同的`TResult`都创建一个对应的单例。那么我们可以如何让这个同步操作更快呢?
|
||||
|
||||
我们可以试图缓存一个常见的`Task<TResult>`。例如`Task<bool>`就非常的常见,而且也只存在两种需要缓存的情况:当结果为真时的一个对象和结果为假时的一个对象。同样的,尽管我们可能不想尝试(也不太可能)去缓存数亿个`Task<int>`对象以覆盖所有可能出现的值,但是鉴于很小的`Int32`值时非常常见的,我们可以尝试去缓存给一些较小的结果,例如从-1到8的结果。 而且对于其他任意的类型来说,`default`就是一个常常出现的值,因此缓存一个结果是`default(TResult)`的`Task`。而且 在最近的.NET版本中添加了一个称作`Task.FromResult`辅助函数,该函数就会完成与上述类似的工作,如果存在可以重复使用的`Task<Result>`单例就返回该单例,反之再创建一个新的`Task`对象。对于其他常常出现的值也也可以设计方法进行缓存。还是以`Stream.ReadAsync`为例子,这个方法常常会在同一个流上调用多次,而且每次读取的值都是允许读取的字节数量`count`。再考虑到使用者往往只需要读取到这个`count`值,因此`Stream.ReadAsync`操作常常会重复返回有着相同`int`值的`Task`对象。为了避免在这种情况下重复的内存分配,许多`Stream`的实现(例如`MemoryStream`)会缓存上一次成功缓存的`Task<int>`对象,如果下一次读取仍然是同步返回的且返回了相同的数值,该方法就会返回上一次读取创建的`Task<int>`对象。但是仍然会存在许多无法覆盖的其他情况,能不能找到一种更加优雅的解决方案来来避免在异步操作同步完成的时候避免创建新的对象,尤其是在性能非常重要的场景下。
|
||||
|
||||
这就是`ValueTask<TResult>`诞生的背景([这篇博客](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)详细测试了`ValueTask<TResult>`的性能)。`ValueTask<TResult>`在诞生之初是`TResult`和`Task<TResult>`的歧视性联合。在这些争论尘埃落定之后,`ValueTask<TResult>`便不是一个立刻可以返回的结果就是一个对未来结果的承诺:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly Task<TResult>? _task;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
一个方法可以通过返回`ValueTask<TResult>`来避免在`TResult`已知的情况下创建新的`Task<Result>`对象,当然返回的类型会更大、返回的结果更加不直接。
|
||||
|
||||
当然,实际应用中也存在对性能需求相当高的场合,甚至你会想在操作异步完成的时候也避免`Task<TResult>`对象的分配。例如`Socket`作为整个网络栈的最底层,对于网络中的大多数服务来说`SendAsync`和`ReceiveAsync`都是绝对的热点代码路径,不论是同步操作还是异步操作都是非常常见的(鉴于内核中的缓存,大多数发送请求都会同步完成,部分接受请求会同步完成)。因此对于像`Socket`这类的工具,如果我们可以在异步我弄成和同步完成的情况下都实现无内存分配的调用是十分有意义的。
|
||||
|
||||
这就是`System.Threading.Tasks.Sources.IValueTaskSource<TResult>`产生的背景:
|
||||
|
||||
```csharp
|
||||
public interface IValueTaskSource<out TResult>
|
||||
{
|
||||
ValueTaskSourceStatus GetStatus(short token);
|
||||
void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
|
||||
TResult GetResult(short token);
|
||||
}
|
||||
```
|
||||
|
||||
该接口允许自行为`ValueTask<TResult>`实现一个“背后“的对象,并且让这个对象提供了获得操作结构的`GetResult`方法和设置操作后续工作的`OnCompleted`。在这个接口出现之后,`ValueTask<TResult>`也小小修改了定义,`Task<TResult>? _task`字段被一个`object? _obj`字段替换了:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly object? _obj;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
现在`_obj`字段就可以存储一个`IValueTaskSource<TReuslt>`对象了。而且相较于`Task<TResult>`在完成之后就只能保持完成的状态,不能变回未完成的状态,`IValueTaskSource<TResult>`的实现有着完全的控制权,可以在已完成和未完成的状态之间双向变化。但是`ValueTask<TResult>`要求一个特定的实例只能被使用一次,不能观察到这个实例在使用之后的任何变化,这也是分析规则[CA2012](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012)存在的意义。这就让让类似于`Socket`的工具为重复的调用建立一个`IValueTaskSource<TResult>`对象池。从实现上来说,`Socket`会至多缓存两个类似的实例,一个用于读取操作一个用于写入操作,因为在99.999%的情况下同时只会有一个发送请求和一个接受请求。
|
||||
|
||||
值得说明的是我只提到了`ValueTask<TResult>`却没有提到`ValueTask`。因为如果只是为了在操作同步完成的时候避免内存分配,非泛型类型的`ValueTask`指挥提供很少的性能提升,因为在同样的条件下可以使用`Task.CompletedTask`。但是如果要考虑在异步完成的时候通过缓存对象避免内存分配,非泛型类型也有作用。因而,在引入`IValueTaskSource<TResult>`的同时,`IValueTaskSource`和`ValueTask`也被引入了。
|
||||
|
||||
到目前我们,我们已经可以利用`Task`,`Task<TResult>`,`ValueTask`,`ValueTask<TResult>`表示各种各样的异步操作,并注册在操作完成之前和之后注册后续的操作。
|
||||
|
||||
但是这些后续操作仍然是回调方法,我们仍然陷入了基于回调的异步控制流程。该怎么办?
|
||||
|
||||
### 迭代器成为大救星
|
||||
|
||||
解决方案的先声实际上在`Task`诞生之前就出现了,在C# 2.0引入迭代器语法的时候。
|
||||
|
||||
你可能会问,迭代器就是`IEnumerable<T>`吗?这是其中的一个。迭代器是一个让编译器将你编写的方法自动实现`IEnumerable<T>`或者`IEnumertor<T>`的语法。例如我可以用迭代器语法编写一个产生斐波那契数列的可遍历对象:
|
||||
|
||||
```csharp
|
||||
public static IEnumerable<int> Fib()
|
||||
{
|
||||
int prev = 0, next = 1;
|
||||
yield return prev;
|
||||
yield return next;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int sum = prev + next;
|
||||
yield return sum;
|
||||
prev = next;
|
||||
next = sum;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个方法可以直接用`foreach`遍历,也可以和`System.Linq.Enumerable`中提供的各种方法组合,也可以直接用一个`IEnumerator<T>`对象遍历。
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib())
|
||||
{
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib().Take(12))
|
||||
{
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
using IEnumerator<int> e = Fib().GetEnumerator();
|
||||
while (e.MoveNext())
|
||||
{
|
||||
int i = e.Current;
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
89
YaeBlog/source/posts/2024-final.md
Normal file
89
YaeBlog/source/posts/2024-final.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: 2024年年终总结
|
||||
date: 2025-01-16T17:15:05.8634370+08:00
|
||||
tags:
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
---
|
||||
|
||||
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
|
||||
|
||||
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
|
||||
|
||||
所以年终总结再次跳票,红豆泥私密马赛!
|
||||
|
||||
<!--more-->
|
||||
|
||||
### 压力
|
||||
|
||||
本年度的第一个关键词,我会选择压力。这一年总是被不同的压力笼罩着,先是有形的压力,然后是无形的压力,在不同的时间阶段有着不同的来源。
|
||||
|
||||
1月份起始的两周就是大三学年秋季学期的期末考试周,而鄙人在下不才我,在本学期面临着计算机科学四幻神的考验——老师不知所云之操作系统、抽象概念无法理解之编译原理、全英语授课之数据库系统原理和智商不够无法战胜之算法导论。挣扎在保研线上的我,刚刚被上一学期的离散数学(下)的~~75分~~74分和数据结构的79分拷打,面对着如此沉重的考试压力(加起来一共12学分呢),可耻的失眠了。
|
||||
|
||||
过完年回来的三月份,就是同论文奋斗的一个月。虽然只是一篇6页的EI检索论文,但对于一个**纯洁**的本科生来说还是有点太困难了。这个过程就像是你先拉了一坨大的,然后在上面细细的涂上巧克力,在最后发表的过程中,需要在众人的面前大嚼这一坨东西,并且称赞“真是一道美食啊”!还没有开始的学术生涯就已经留下永恒的污点力(悲)。
|
||||
|
||||
搞完论文的四月和五月则是和大作业搏斗的两个月。首先是无法战胜的“编译原理课程设计”,内容是设计一个Pascal-S到C语言的源到源编译器。这一大作业的主要压力来源是大作业本身的难度,直到最后提交的时候,全部95个测试点也没有能够完全通过,然而其他人在祖传代码上缝缝补补却过来,哭。虽然考虑到我们是全手写的编译器,没有使用任何的编译器构建工具,提出的解决方案也称不上是墨守成规,老师给了我一个还算是可以的分数,算是压力中的小小慰藉。
|
||||
|
||||
然后是风波不断的软件工程大作业,明明只是一个相对简单的Web前后端开发,但是我们前后进行了三次验收才通过,一直拖到了学期的第16周。老师设计的联合验收制度给我们结结实实的上了一课,要求联合验收小组的不同前后端需要能够任意组合使用,导致我们为了适配另外一组的逻辑,几乎是把核心代码写了两遍。虽然我不喜欢在背后攻击别人,但是我不得不说,这一年中最有压力的时刻,往往不是自己的事情搞不定时,而是看着别人搞砸事情你却无能为力的时候。
|
||||
|
||||
这两个月还夹杂这一个意义不明的专业实习,明明是计算机科学与技术专业的牛马,为什么会被中兴通讯的老师培训通信项目的项目管理?
|
||||
|
||||
应付完上面这些杂七杂八的内容,便是本科生生涯中的最后三场考试:人称计算机领域的政治之《软件工程》,通信领域科普课程之《现代交换原理》和永远的神之《计算机系统结构》。
|
||||
|
||||
不得不说《软件工程》,~~或者人们常说的肖概~~确实不愧于计算机领域的政治之称。毕竟政治的主要课题就是研究如何组织和动员人群以完成一个特定的目标,《软件工程》不过是将人员限制为了软件的开发人员,领域限制为了软件开发领域,基本的道理还是相通的。
|
||||
|
||||
《现代交换原理》则是一门在现有的课程体系下非常尴尬的一门课程,显然这门课的保留还是为了凸显“计算机+通信”的学科特色,但是大量前置知识的缺失和同其他课程的脱节使得这门课就显得非常的“脱节”。而且相对来说,通信技术的发展速度远远不如互联网的迭代技术,这门课也被同学们戏称为“古代交换原理”。令人最难受的,虽然知识古代,但是却一点都不简单,很多内容只能说是听了个概念,幸好最后的考试不难,靠死记硬背通过了考试。
|
||||
|
||||
《计算机系统结构》就是核心课中的核心课了。课程内容和《计算机组成原理》衔接的非常紧密,~~虽然我组成原理就学的很垃圾~~,主要围绕着如何最大限度的并行化运行程序进行,从指令级的并行一直到多机并行,可以说是压力最大的一门考试。在准备的过程中做了很多套往年题,博客上也发布了一部分的复习笔记,最终幸好低空飞过。唯一的吐槽是实验什么时候可以从MIPS改成为RISC-V呢。
|
||||
|
||||
三门课的考试一结束,这些死线明确的、有形的压力便消失了,但是无形的压力——对于是否能保研的焦虑——便笼罩下来。
|
||||
|
||||
7月和8月都是在这种不安和恐慌中度过,这种氛围在9月份保研名单出炉之前达到了顶峰。保研的流程开始之后则是通知推着人走,各种交材料,各种准备答辩,各种等待公示,直到最后的保研名单出炉。
|
||||
|
||||
不过现在回想起来,最后名单出炉,获得保研资格,复试通过之后,并没有一种如释重负的感觉,或者说终于实现了既定目标的快感。反而是一种“啊,结束了”的空落感,只想回去睡一觉。
|
||||
|
||||
然后新的~~风暴~~压力已经出现,在度过一个短短的国庆假期之后便正式进组,作为一个研究生的社畜生涯就此开始。
|
||||
|
||||
### 经历
|
||||
|
||||
虽然2024年的第一个关键词已经选择为“压力”,但是众所周知,高压锅里往往能压出好吃的。人也是这样。所以我将2024年的第二个关键词定为“经历”,人生如逆旅,我亦是行人,各式各样的经历便是风格迥异的景点。
|
||||
|
||||
人生第一篇学术论文的撰写和发表无疑是今年最难忘的经历。虽然我在前面称之为“学术生涯上的污点”,但是污点也好过一片空白不是,还非常的引人夺目。而且这是一个完整的撰写-发表流程,从开始的选题、实验、撰写、投稿,到最后的接受、提交、发表、报销等等数个环节我均参加。这个过程不仅让我对于学术论文的诞生流程有力较为清晰的认识,也对学校的各种发表和报销流程有了深入的了解。
|
||||
|
||||
两个大作业,编译原理课程设计和软件工程大作业,也是非常难忘的经历。这两个项目的代码都已经整理好开源在Github上了。前者代表了目前我软件开发的最高水平,而后者则是我本科阶段唯一一个差点失败的软件开发项目。这种冰火两重天的对比实在是很难令人忘记。
|
||||
|
||||
这两个项目中的收获有非常技术性的。相较于2023年面对各种大作业时的略显底气不足,这次我在各种技术栈的选择上更加游刃有余,选择了完全倒向.NET和React,摈弃了之前的Java和Vue。各类现代软件开发技术也得到了充分的应用,例如由Gitea Actions驱动的DevOps实践,完全基于合并请求的多人协作流程。事实证明,这些协作流程确实在一定程度上加速了项目的开发。
|
||||
|
||||
但是,“软件工程里没有银弹”,先进技术的堆叠并不能保证软件项目成功。虽然我这里~~自吹自擂~~有非常多新技术的帮助,软件工程大作业的差点失败的确说明了软件工程实际上还是人的工程,猪队友永远比凶恶的敌人更可怕。当然也不能将所有的锅都扔给别人,我在项目失控的过程中也没有能够采取有力的措施挽救整个项目,~~负有不可推卸的领导责任~~。
|
||||
|
||||
今年最后一个难忘的经历便是去横店镇参加CNCC 2024,也单独出过[博客](https://rrricardo.top/blog/essays/cncc-2024)。虽然之前学术论文发表的过程中也是在学术会议上做过口头报告,不过是线上参加的,并没有特别的实感。现在线下参加,也不需要自己上去发表,顿感旅游真好玩,~~也有可能是因为CNCC比较水~~。
|
||||
|
||||
### 匆匆
|
||||
|
||||
2024年的第三个关键词我想定为”匆匆“,虽然想找一个更加”有文化“的词汇,奈何自己的文化造纸实在不够,故定为”匆匆“。
|
||||
|
||||
可2024年确实是非常忙碌的一年,现在回想起来,几乎每一个月都是在为了某一件特定的事情而奔走着。还记得在新年伊始的时间里,我还制订了各种各样的读书计划和补番计划,现在看来,定计划的目的不是为了实现,而是为了安心。
|
||||
|
||||
不过匆匆之中还是读了几本书。首先是久负盛名的《置身事内——中国政府与经济发展》,这本书的开篇即言:“这本书是写给大学生和对经济话题感兴趣的读者”,细读下来也确实如此。然后是一本我从小便着迷的二战军史相关话题《美国陷阱:橙色计划始末》,其中若干的政治与军事细节之于我不过是走马观花,不过其中表达出的长期战略实在令人敬佩。
|
||||
|
||||
至于补番计划,我则是表现出了同电子ED一样的症状,对于新番没有兴趣,对于补早就下载安装好的老番更是兴趣缺缺。反倒是电视剧,由于12月韩国的惊天一变,我又重新下载了《第五共和国》,忠!诚!
|
||||
|
||||
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站,鬼知道你匆匆在哪了~~。
|
||||
|
||||

|
||||
|
||||
### 未来
|
||||
|
||||
> 定计划的目的不是为了实现,而是为了安心。
|
||||
|
||||
站在年关,已经可以预见到2025年将会是更为繁忙的一年,从一月份到十月份都已经有了或多或少的安排,现在无法多言,只能希望都能有良好的结果。
|
||||
|
||||
还是多说点可以说的罢。
|
||||
|
||||
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目,一整本上总结下来也能有超过50本,其中不乏超过一千页的大部头,说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍:
|
||||
|
||||
- 陆风,《光变:一个企业及其工业史》
|
||||
- 吴军,《浪潮之巅》
|
||||
|
||||
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。
|
||||
|
||||
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
Normal file
Binary file not shown.
407
YaeBlog/source/posts/aspnet-authorization.md
Normal file
407
YaeBlog/source/posts/aspnet-authorization.md
Normal file
@@ -0,0 +1,407 @@
|
||||
---
|
||||
title: 在ASP.NET Core中集成认证和授权流程
|
||||
date: 2024-09-08T22:27:17.0328669+08:00
|
||||
tags:
|
||||
- ASP.NET Core
|
||||
- 技术笔记
|
||||
---
|
||||
|
||||
以[Martina](https://github.com/post-guard/Martina)为例,记录如何典型的ASP.NET Core应用中集成认证和授权的流程。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 业务需求概述
|
||||
|
||||
[Martina](https://github.com/post-guard/Martina)系统是一个酒店的空调和入住管理系统,项目中对于认证和授权的要求是一个典型的多权限、多用户模式,具体来说:
|
||||
|
||||
- 系统中所有的接口均需要在登录之后才能调用;
|
||||
- 系统中安装不同管理领域将用户的权限划分为一大类、三小类:一个超级管理员权限和客房、空调、账单三个领域管理员权限;
|
||||
- 普通用户的权限有时间和使用房间的要求:只能在入住时间段内访问入住房间的空调相关接口。
|
||||
|
||||
可以看出,上述这些要求基本上覆盖了一个常见系统的中所有关于认证和授权的使用场景,因此本篇便以该系统为例介绍如何在ASP.NET Core框架中实现上述业务要求。
|
||||
|
||||
## 身份认证和授权的基础知识
|
||||
|
||||
身份认证是指由用户提供凭据,然后将其与存储在操作系统、数据库、应用和资源中的凭据进行比较的过程。而授权过程发生在身份认证成功之后:在凭据匹配成功之后,用户身份验证成功,可执行已向其授权的操作。授权就是判断允许用户执行操作的过程。
|
||||
|
||||
在ASPNET.Core中,这是通过两个**中间件**,`UseAuthenication`和`UseAuthorization`来完成的,还是来看这张经典的中间件工作流程:
|
||||
|
||||

|
||||
|
||||
可以看到在中间件的管道中,认证中间价将在授权中间件运行之前运行——这两个顺序是不能颠倒的,如果授权中间件在认证中间件运行之前运行,那授权中间件就无法为用户授予任何权限,所有需要权限的接口均会返回401错误码。
|
||||
|
||||
> 为什么我知道的如此清楚捏?
|
||||
>
|
||||
> 因为我真的写反过,最后还是在框架代码里面打断点才发现授权中间件拿不到用户登录的信息,当时还在GitHub的工单里面翻找相关的bug,感觉可以评选为人生十大傻逼bug之一。
|
||||
|
||||
概览完认证和授权之后,首先来谈谈认证。认证的基本过程就是一个开锁的过程:用户提供一个凭据,也就是钥匙,系统验证凭据的有效性,就是锁的工作。这里主要的问题就是这个钥匙的形状长什么样子,也就是凭据的表现形式。常见的凭据表现形式有`Cookies`和`JWT`两种。
|
||||
|
||||
`Cookies`是一种服务器发送到用户浏览器并保存在本地上的一小块文本文件,用户浏览器在保存这些文本文件之后会在每次向同一服务器发送请求时在请求体中携带一些文本文件信息。`Cookies`是一种非常古老的技术,这种技术使得无状态的HTTP协议可以记录稳定的状态信息,因此在这个技术常被应用来认证网络用户的身份。
|
||||
|
||||
`JWT`的全称是JSON Web Token,是一种使用JSON对象表示格式在两方之前安全且有效的传输信息的方法,使用该方法的信息可以使用指定的密钥或者是公钥-私钥对验证信息的有效性。因此`JWT`作为一种通用的、可验证的令牌格式用来完成网络中认证的过程。在服务器验证某一个用户的身份之后(例如通过验证账号密码、通过第三方的验证)可以签发一个`JWT`令牌给用户浏览器,浏览器可以使用`localstorage`等技术将该令牌存储在用户浏览器中并在每次向服务器发送请求的过程中将该令牌携带在一个特定的请求头`Authorization`中。
|
||||
|
||||
> 在`Authorization`请求头中常常会以`Bearer <JWT>`的格式进行,这其中的`Bearer`是指定的身份认证的模式(Scheme),这里的详细解释可以见[MDN文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)。
|
||||
|
||||
谈完认证之后,再来看看授权。授权的实现是一个和业务逻辑高度相关的过程,一个常见的业务逻辑是用户分为不同的层级——例如普通用户和管理员,而不同层级的用户可以调用的接口不同,这就是**基于策略的授权模式**的典型应用场景,该模式允许为每个接口指定一个或者多个认证策略。另外一个常见的业务逻辑是用户只能访问自己所拥有的资源——例如用户只能删除自己创建的记录,这就是**基于资源的授权模式**的典型应用场景,该模式允许为一种资源编写一段授权逻辑,并通过依赖注入的方式供服务器或者控制器使用。
|
||||
|
||||
## 身份认证和授权的实践
|
||||
|
||||
在本个系统中,身份认证将采用`JWT`令牌,而授权的部分将会覆盖到上文中提到的两种典型模式,通过研究本系统的实现可以理解在ASP.NET Core中集成身份认证和授权的流程。
|
||||
|
||||
在ASP.NET Core系统中集成`JWT`令牌的认证方式需要先安装一个包`Microsoft.AspNetCore.Authentication.JwtBearer`。
|
||||
|
||||
### 身份认证部分
|
||||
|
||||
身份认证部分主要分为令牌签发和令牌验证两个部分,令牌认证的部分主要在于使用`AddAuthentication`向主机容器中注入服务,而令牌签发的部分则通常是实现一个接口,在验证用户输入的账号和密码之后生成该用户对于的令牌。这两个过程是高度关联的,在签发过程中设置的令牌信息需要在验证令牌的过程设置对应的部分,否则签发的令牌就无法验证。因此先介绍签发令牌的部分。
|
||||
|
||||
签发令牌之前先介绍一下`JWT`令牌的组成,一个兼容的`JWT`令牌一般有三个部分组成:
|
||||
|
||||
- 头部`Header`:头部在一般情况下只有两个字段组成,一个`tpy`字段存储固定值为`JWT`指定这是一个`JWT`令牌,一个`alg`字段指定验证该令牌的算法是`HMCA SHA256`还是`RSA`:
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
- 负载`Payload`:包含各种关于实体(用户)的宣称列表。宣称可以分成三种类型,已注册的类型、公开的类型和私有的类型,这三种的类型的区别可以从[RFC7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1)中具体查看,简而言之就是已注册的类型就是推荐在签发令牌时设置的,包括签发者和到期时间等的内容,公开的类型是公开注册可以共享的名称,而私有的就是自行指定的。
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"admin": true
|
||||
}
|
||||
```
|
||||
|
||||
- 签名`signature`:验证令牌的签名部分,在使用`HMCA SHA256`算法的情况下,签名的计算公示如下所示:
|
||||
|
||||
```
|
||||
HMACSHA256(
|
||||
base64UrlEncode(header) + "." +
|
||||
base64UrlEncode(payload),
|
||||
secret)
|
||||
```
|
||||
|
||||
在学习了这些`JWT`的基础知识之后就可以很容易的写出如下的令牌生成代码:
|
||||
|
||||
```csharp
|
||||
public string GenerateJsonWebToken(User user)
|
||||
{
|
||||
List<Claim> claims =
|
||||
[
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
new Claim(ClaimTypes.NameIdentifier, user.UserId)
|
||||
];
|
||||
|
||||
JwtSecurityToken token = new(
|
||||
issuer: _option.Issuer,
|
||||
audience: user.UserId,
|
||||
notBefore: DateTime.Now,
|
||||
expires: DateTime.Now.AddDays(7),
|
||||
claims: claims,
|
||||
signingCredentials: _signingCredentials
|
||||
);
|
||||
|
||||
return _jwtSecurityTokenHandler.WriteToken(token);
|
||||
}
|
||||
```
|
||||
|
||||
签发令牌的凭据使用下面的方式创建:
|
||||
|
||||
```csharp
|
||||
private readonly SigningCredentials _signingCredentials =
|
||||
new(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.Value.JsonWebTokenKey)),
|
||||
SecurityAlgorithms.HmacSha256);
|
||||
```
|
||||
|
||||
签发的过程中部分重要的参数使用配置的方式提供,例如签发者和密钥,配置实体类如下所示:
|
||||
|
||||
```csharp
|
||||
public class JsonWebTokenOption
|
||||
{
|
||||
public const string OptionName = "JWT";
|
||||
|
||||
/// <summary>
|
||||
/// JWT令牌的签发者
|
||||
/// </summary>
|
||||
public required string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JWT令牌的签发密钥
|
||||
/// </summary>
|
||||
public required string JsonWebTokenKey { get; set; }
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
签发好令牌之后就可以编写验证令牌的部分了:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
|
||||
options =>
|
||||
{
|
||||
JsonWebTokenOption? jsonWebTokenOption = builder.Configuration.GetSection(JsonWebTokenOption.OptionName)
|
||||
.Get<JsonWebTokenOption>();
|
||||
|
||||
if (jsonWebTokenOption is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to get JWT options");
|
||||
}
|
||||
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jsonWebTokenOption.Issuer,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.JsonWebTokenKey)),
|
||||
ValidAlgorithms = [SecurityAlgorithms.HmacSha256]
|
||||
};
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
在验证令牌的部分,指定验证令牌的签发者和签名。
|
||||
|
||||
编写完上述代码之后就可以增加身份验证和授权的中间件验证上述代码的正确性了。
|
||||
|
||||
```csharp
|
||||
application.UseAuthentication();
|
||||
application.UseAuthorization();
|
||||
```
|
||||
|
||||
### 授权的部分
|
||||
|
||||
#### 按照策略进行授权
|
||||
|
||||
系统中一个典型的场景就是不同级别的用户能访问的接口不同,例如在本系统中用户的级别分为:
|
||||
|
||||
```csharp
|
||||
[Flags]
|
||||
public enum Roles
|
||||
{
|
||||
User = 0b_0000_0000,
|
||||
RoomAdministrator = 0b_0000_0001,
|
||||
AirConditionerAdministrator = 0b_0000_0010,
|
||||
BillAdministrator = 0b_0000_0100,
|
||||
Administrator = 0b_0000_1000
|
||||
}
|
||||
```
|
||||
|
||||
为了方便给不同的接口指定不同的访问策略,首先创建一个对用户级别的要求(Requirement):
|
||||
|
||||
```csharp
|
||||
public class HotelRoleRequirement(Roles hotelRole) : IAuthorizationRequirement
|
||||
{
|
||||
public Roles HotelRole { get; } = hotelRole;
|
||||
}
|
||||
```
|
||||
|
||||
然后实现一个处理该要求的验证程序:
|
||||
|
||||
```csharp
|
||||
public class HotelRoleHandler(MartinaDbContext dbContext) : AuthorizationHandler<HotelRoleRequirement>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
HotelRoleRequirement requirement)
|
||||
{
|
||||
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
|
||||
|
||||
if (userId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
User? user = await dbContext.Users
|
||||
.Include(u => u.Permission)
|
||||
.Where(u => u.UserId == userId.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果要求的权限是超级管理员
|
||||
// 则判断是否是超级管理员
|
||||
if ((requirement.HotelRole & Roles.Administrator) == Roles.Administrator)
|
||||
{
|
||||
if (user.Permission.IsAdministrator)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
// 剩下的权限
|
||||
// 如果用户是超级管理员则直接有权限
|
||||
if (user.Permission.IsAdministrator)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((requirement.HotelRole & Roles.BillAdministrator) == Roles.BillAdministrator)
|
||||
{
|
||||
if (user.Permission.BillAdminstrator)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
if ((requirement.HotelRole & Roles.RoomAdministrator) == Roles.RoomAdministrator)
|
||||
{
|
||||
if (user.Permission.RoomAdministrator)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
if ((requirement.HotelRole & Roles.AirConditionerAdministrator) == Roles.AirConditionerAdministrator)
|
||||
{
|
||||
if (user.Permission.AirConditionorAdministrator)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架要求在处理程序使用依赖注入到主机的容器中,这里因为在验证的过程中使用了数据库的服务`DbContext`因此被注册为一个范围内(Scope)服务。
|
||||
|
||||
```csharp
|
||||
builder.Services.AddScoped<IAuthorizationHandler, HotelRoleHandler>();
|
||||
```
|
||||
|
||||
为了方便在`[Authorize]`注解中使用字符串指定不同的授权策略,在`AddAuthoriztion`进行配置:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("Administrator", policy =>
|
||||
{
|
||||
policy.AddRequirements(new HotelRoleRequirement(Roles.Administrator));
|
||||
});
|
||||
|
||||
options.AddPolicy("RoomAdministrator", policy =>
|
||||
policy.AddRequirements(new HotelRoleRequirement(Roles.RoomAdministrator)));
|
||||
|
||||
options.AddPolicy("AirConditionerAdministrator", policy =>
|
||||
policy.AddRequirements(new HotelRoleRequirement(Roles.AirConditionerAdministrator)));
|
||||
|
||||
options.AddPolicy("BillAdministrator", policy =>
|
||||
policy.AddRequirements(new HotelRoleRequirement(Roles.BillAdministrator)));
|
||||
});
|
||||
```
|
||||
|
||||
使用该方法注册之后就可以直接在`[Authorize]`注解中指定需要使用的授权策略:
|
||||
|
||||
```csharp
|
||||
[HttpGet("revenue")]
|
||||
[Authorize(policy: "BillAdministrator")]
|
||||
[ProducesResponseType<ExceptionMessage>(400)]
|
||||
[ProducesResponseType<RevenueTrend>(200)]
|
||||
public async Task<IActionResult> QueryRevenueTrend([FromQuery] DateTimeOffset begin, [FromQuery] DateTimeOffset end)
|
||||
{
|
||||
if (begin >= end)
|
||||
{
|
||||
return BadRequest(new ExceptionMessage("开始时间不能晚于结束时间"));
|
||||
}
|
||||
|
||||
RevenueTrend trend = new()
|
||||
{
|
||||
TotalUsers = await managerService.QueryCurrentUser(),
|
||||
TotalCheckin = await managerService.QueryCurrentCheckin(),
|
||||
DailyRevenues = await managerService.QueryDailyRevenue(begin, end)
|
||||
};
|
||||
|
||||
return Ok(trend);
|
||||
}
|
||||
```
|
||||
|
||||
#### 按照资源进行授权
|
||||
|
||||
系统中一个典型的需求就是一个用户只能修改资源池中部分自己拥有权限的资源,在本系统中就是用户只能开启和关闭当前入住房间中的空调。
|
||||
|
||||
按照资源进行授权的总体流程和安装策略进行授权总体上差别不大,除了无法在注解中设置需要使用的策略。首先仍然是设计一个授权的要求:
|
||||
|
||||
```csharp
|
||||
public class CheckinRequirement : IAuthorizationRequirement;
|
||||
```
|
||||
|
||||
然后为该要求实现一个授权处理程序,注意在这里集成泛型基类`AuthorizationHandler`时除了需要指定要求类还需要指定资源类型:
|
||||
|
||||
```csharp
|
||||
public class CheckinHandler(
|
||||
RoomService roomService,
|
||||
MartinaDbContext dbContext)
|
||||
: AuthorizationHandler<CheckinRequirement, Room>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
CheckinRequirement requirement,
|
||||
Room resource)
|
||||
{
|
||||
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
|
||||
|
||||
if (userId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
User? user = await dbContext.Users.AsNoTracking()
|
||||
.Where(u => u.UserId == userId.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (user is { Permission.IsAdministrator: true } || user is { Permission.AirConditionorAdministrator: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
CheckinRecord? record = await roomService.QueryUserCurrentStatus(userId.Value);
|
||||
|
||||
if (record?.RoomId == resource.Id)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在使用该授权方法时,通过依赖注入获得一个`IAuthorizationService`的接口对象并调用对应的授权接口进行验证,传入需要访问的资源和当前`HttpContext`中的用户`User`,这个`User`实际上就是`JWT`令牌中的负载部分。
|
||||
|
||||
```csharp
|
||||
AuthorizationResult result = await authorizationService.AuthorizeAsync(User, room, [new CheckinRequirement()]);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
if (!airConditionerManageService.VolidateAirConditionerRequest(roomObjectId, request, out string? message))
|
||||
{
|
||||
return BadRequest(new ExceptionMessage(message));
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过清晰的定义身份认证和授权两个环节,并提供了一个要求——处理程序的授权模型,ASP.NET Core提供了一套简单易用、扩展性高的接口安全系统。
|
||||
@@ -0,0 +1,324 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 706.35 390">
|
||||
<defs>
|
||||
<symbol id="New_Symbol_125" data-name="New Symbol 125" viewBox="0 0 32 16">
|
||||
<path d="M31,8c0,3.85-3.6,7-8,7H9c-4.4,0-8-3.15-8-7S4.6,1,9,1H23C27.4,1,31,4.15,31,8Z" fill="#fff"/>
|
||||
<path d="M23,16H9c-5,0-9-3.59-9-8S4,0,9,0H23c5,0,9,3.59,9,8S28,16,23,16ZM9,2C5.14,2,2,4.69,2,8s3.14,6,7,6H23c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#0072c6"/>
|
||||
</symbol>
|
||||
</defs>
|
||||
<g id="Shapes">
|
||||
<rect width="706.35" height="390" fill="#fff"/>
|
||||
<g>
|
||||
<rect x="376.35" y="287.58" width="215" height="85" fill="#fff"/>
|
||||
<rect x="376.35" y="287.58" width="215" height="85" fill="#3c3c41" opacity="0.05"/>
|
||||
<rect x="376.35" y="287.58" width="215" height="85" fill="none" stroke="#3c3c41" stroke-miterlimit="10" stroke-width="0.25"/>
|
||||
</g>
|
||||
<use width="32" height="16" transform="translate(73.58 22.58)" xlink:href="#New_Symbol_125"/>
|
||||
<g>
|
||||
<path d="M47.15,70.41c0,3.85-3.6,7-8,7h-14c-4.4,0-8-3.15-8-7s3.6-7,8-7h14C43.55,63.41,47.15,66.56,47.15,70.41Z" fill="#fff"/>
|
||||
<path d="M39.15,78.41h-14c-5,0-9-3.59-9-8s4-8,9-8h14c5,0,9,3.59,9,8S44.11,78.41,39.15,78.41Zm-14-14c-3.86,0-7,2.69-7,6s3.14,6,7,6h14c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#76bc2d"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="79.78" y="57.58" width="110" height="25" fill="#fff"/>
|
||||
<rect x="79.78" y="57.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="89.78" y1="39.15" x2="89.78" y2="52.2" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="86.04 51.1 89.78 57.58 93.52 51.1 86.04 51.1" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="104.78" y="97.58" width="110" height="25" fill="#fff"/>
|
||||
<rect x="104.78" y="97.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="114.78" y1="82.89" x2="114.78" y2="91.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="111.04 90.85 114.78 97.33 118.52 90.85 111.04 90.85" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="79.78" y1="70.41" x2="50.91" y2="70.41" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M57.06,66.09a.44.44,0,0,1-.13.62l-5.81,3.7,5.81,3.7a.44.44,0,0,1,.13.62.45.45,0,0,1-.62.14L50,70.79a.44.44,0,0,1-.21-.38A.47.47,0,0,1,50,70L56.44,66a.55.55,0,0,1,.24-.07A.45.45,0,0,1,57.06,66.09Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="129.78" y="137.27" width="110" height="25" fill="#fff"/>
|
||||
<rect x="129.78" y="137.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="139.78" y1="122.58" x2="139.78" y2="131.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="136.04 130.53 139.78 137.01 143.52 130.53 136.04 130.53" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M129.78,148.93h-10a5,5,0,0,1-5-5V123.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M119.09,129.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,119.09,129.83Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M104.35,108.93h-10a5,5,0,0,1-5-5V83.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M93.67,89.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.7,5.8a.45.45,0,1,1-.76-.48L89,82.81a.45.45,0,0,1,.76,0l4.08,6.4a.42.42,0,0,1,.07.24A.45.45,0,0,1,93.67,89.83Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="154.78" y="177.58" width="110" height="25" fill="#fff"/>
|
||||
<rect x="154.78" y="177.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="164.78" y1="162.89" x2="164.78" y2="171.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="161.04 170.85 164.78 177.32 168.52 170.85 161.04 170.85" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M154.78,189.24h-10a5,5,0,0,1-5-5V164" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M144.09,170.15a.45.45,0,0,1-.62-.14l-3.7-5.81L136.08,170a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="179.78" y="217.27" width="110" height="25" fill="#fff"/>
|
||||
<rect x="179.78" y="217.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="189.78" y1="202.58" x2="189.78" y2="211.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="186.04 210.53 189.78 217.01 193.52 210.53 186.04 210.53" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M179.78,228.93h-10a5,5,0,0,1-5-5V203.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M169.09,209.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,169.09,209.83Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="204.78" y="257.58" width="110" height="25" fill="#fff"/>
|
||||
<rect x="204.78" y="257.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="214.78" y1="242.89" x2="214.78" y2="251.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="211.04 250.85 214.78 257.32 218.52 250.85 211.04 250.85" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M204.78,269.24h-10a5,5,0,0,1-5-5V244" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M194.09,250.15a.45.45,0,0,1-.62-.14l-3.7-5.81L186.08,250a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="229.78" y="297.89" width="110" height="25" fill="#fff"/>
|
||||
<rect x="229.78" y="297.89" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="239.78" y1="283.21" x2="239.78" y2="292.26" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="236.04 291.16 239.78 297.64 243.52 291.16 236.04 291.16" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M229.78,309.55h-10a5,5,0,0,1-5-5V284.31" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M219.09,290.46a.45.45,0,0,1-.62-.14l-3.7-5.81-3.69,5.81a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.44.44,0,0,1,219.09,290.46Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="254.78" y="337.58" width="110" height="25" fill="#fff"/>
|
||||
<rect x="254.78" y="337.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#fff"/>
|
||||
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#fff"/>
|
||||
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#fff"/>
|
||||
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="264.78" y1="322.89" x2="264.78" y2="331.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="261.04 330.85 264.78 337.32 268.52 330.85 261.04 330.85" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="476.92" y1="349.24" x2="485.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="484.87 352.98 491.35 349.24 484.87 345.5 484.87 352.98" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="364.78" y1="349.24" x2="380.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="379.87 352.98 386.35 349.24 379.87 345.5 379.87 352.98" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="581.35" y1="349.24" x2="600.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
|
||||
<polygon points="599.87 352.98 606.35 349.24 599.87 345.5 599.87 352.98" fill="#005ba1"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M254.78,349.24h-10a5,5,0,0,1-5-5V324" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M244.09,330.15a.45.45,0,0,1-.62-.14l-3.7-5.81L236.08,330a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M432.92,362.58v15a5,5,0,0,1-5,5H269.78a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M269.1,368.48a.45.45,0,0,1-.63-.13l-3.69-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.44.44,0,0,1,.38-.21.47.47,0,0,1,.38.21l4.07,6.4a.46.46,0,0,1-.13.62Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M656.35,362.08v15.5a5,5,0,0,1-5,5h-110a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M540.67,368.48a.44.44,0,0,1-.62-.13l-3.7-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.45.45,0,0,1,.76,0l4.08,6.4a.44.44,0,0,1,.07.24A.45.45,0,0,1,540.67,368.48Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M536.35,337.58v-10a5,5,0,0,0-5-5H437.92a5,5,0,0,0-5,5v8.92" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
|
||||
<path d="M428.6,330.35a.45.45,0,0,1,.63.13l3.69,5.81,3.7-5.81a.44.44,0,0,1,.62-.13.45.45,0,0,1,.14.62l-4.08,6.4a.44.44,0,0,1-.38.21.47.47,0,0,1-.38-.21l-4.07-6.4a.46.46,0,0,1,.13-.62Z" fill="#4f4f4f"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Text">
|
||||
<g>
|
||||
<path d="M75.51,16.47H74.34l-1.41-2.35a5,5,0,0,0-.37-.56,2.27,2.27,0,0,0-.38-.38,1.07,1.07,0,0,0-.41-.21,1.49,1.49,0,0,0-.49-.07h-.81v3.57h-1V8.07H72a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,1.84,1.84,0,0,1,.53.7,2.28,2.28,0,0,1,.2,1,2.53,2.53,0,0,1-.13.8,2.15,2.15,0,0,1-.38.66,2.37,2.37,0,0,1-.59.49,2.82,2.82,0,0,1-.77.31v0a2.13,2.13,0,0,1,.37.22,2.56,2.56,0,0,1,.3.28c.09.11.18.24.27.37s.2.3.31.49ZM70.47,9v3h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.83,1.83,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05A1.88,1.88,0,0,0,71.82,9Z" fill="#1e1e1e"/>
|
||||
<path d="M81,13.71H76.77a2.19,2.19,0,0,0,.54,1.55,1.86,1.86,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.33,3.33,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.56,2.56,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76A3.19,3.19,0,0,1,81,13.21Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M87.58,19.23h-1V15.44h0a2.14,2.14,0,0,1-2,1.17,2.25,2.25,0,0,1-1.81-.8,3.27,3.27,0,0,1-.68-2.2,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.89,1.89,0,0,1,1.78,1h0v-.84h1Zm-1-5.46V12.9a1.73,1.73,0,0,0-.49-1.25,1.61,1.61,0,0,0-1.22-.51,1.66,1.66,0,0,0-1.37.64A2.85,2.85,0,0,0,83,13.59a2.48,2.48,0,0,0,.49,1.63,1.55,1.55,0,0,0,1.25.59,1.73,1.73,0,0,0,1.35-.58A2.17,2.17,0,0,0,86.62,13.77Z" fill="#1e1e1e"/>
|
||||
<path d="M94.38,16.47h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55V10.47h.95v3.44c0,1.26.48,1.9,1.45,1.9A1.49,1.49,0,0,0,93,15.29a2,2,0,0,0,.45-1.36V10.47h1Z" fill="#1e1e1e"/>
|
||||
<path d="M101.14,13.71H96.9a2.28,2.28,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.5,3.5,0,0,1-2.09.57,2.56,2.56,0,0,1-2-.81,3.37,3.37,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.58,2.58,0,0,1,2-.88,2.27,2.27,0,0,1,1.82.76,3.2,3.2,0,0,1,.65,2.12Zm-1-.81a1.91,1.91,0,0,0-.4-1.3,1.39,1.39,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M102.23,16.26v-1a2.88,2.88,0,0,0,1.73.58c.84,0,1.26-.29,1.26-.85a.7.7,0,0,0-.11-.41,1,1,0,0,0-.29-.29,2.18,2.18,0,0,0-.43-.23l-.54-.22a5.16,5.16,0,0,1-.7-.32,1.74,1.74,0,0,1-.5-.36,1.29,1.29,0,0,1-.31-.46,1.57,1.57,0,0,1-.1-.6,1.44,1.44,0,0,1,.19-.75,1.58,1.58,0,0,1,.52-.54,2.45,2.45,0,0,1,.73-.34,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.78,2.78,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.06,1.06,0,0,0-.37.18.66.66,0,0,0-.24.26.7.7,0,0,0-.09.35.8.8,0,0,0,.09.39.82.82,0,0,0,.25.28,1.67,1.67,0,0,0,.4.22l.53.22a6.85,6.85,0,0,1,.71.31,2.44,2.44,0,0,1,.54.37,1.41,1.41,0,0,1,.35.46,1.54,1.54,0,0,1,.12.63,1.5,1.5,0,0,1-.2.77,1.74,1.74,0,0,1-.53.55,2.43,2.43,0,0,1-.75.32,3.76,3.76,0,0,1-.9.1A3.49,3.49,0,0,1,102.23,16.26Z" fill="#1e1e1e"/>
|
||||
<path d="M110.47,16.41a1.82,1.82,0,0,1-.9.19c-1,0-1.58-.58-1.58-1.76V11.29h-1v-.82h1V9l1-.31v1.77h1.52v.82H109v3.38a1.41,1.41,0,0,0,.21.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M14.05,55.66H12.88L11.47,53.3a4.53,4.53,0,0,0-.37-.56,2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,1.89,1.89,0,0,0-.5-.07H9v3.58H8v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7A2.61,2.61,0,0,1,13,50.3a2,2,0,0,1-.38.65,2.13,2.13,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48ZM9,48.15v3h1.34a1.87,1.87,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.32,1.32,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.35,1.35,0,0,0-.44-1.06,1.88,1.88,0,0,0-1.26-.37Z" fill="#1e1e1e"/>
|
||||
<path d="M19.55,52.9H15.31a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.91,2.91,0,0,0,1.86-.67v.9A3.43,3.43,0,0,1,17,55.8,2.53,2.53,0,0,1,15,55a3.35,3.35,0,0,1-.72-2.3,3.28,3.28,0,0,1,.79-2.28,2.54,2.54,0,0,1,2-.88,2.24,2.24,0,0,1,1.82.76,3.17,3.17,0,0,1,.65,2.11Zm-1-.82a1.9,1.9,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.46,1.56,1.56,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M20.64,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L22.26,53c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.78.78,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6,6,0,0,1,.71.32,2,2,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.68,1.68,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,20.64,55.44Z" fill="#1e1e1e"/>
|
||||
<path d="M27.08,54.79h0v3.63h-1V49.66h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,27.08,54.79Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.86,2.49,2.49,0,0,0-.46-1.57,1.56,1.56,0,0,0-1.26-.56,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,27.05,52.37Z" fill="#1e1e1e"/>
|
||||
<path d="M35.66,55.8A2.79,2.79,0,0,1,33.53,55a3.12,3.12,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.29,3.29,0,0,1,.75,2.29,3.21,3.21,0,0,1-.81,2.3A2.82,2.82,0,0,1,35.66,55.8Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.46,2.46,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.77,1.77,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.74,1.74,0,0,0,35.73,50.33Z" fill="#1e1e1e"/>
|
||||
<path d="M45.16,55.66h-1V52.24c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.18,2.18,0,0,1,2-1.13,1.85,1.85,0,0,1,1.51.63A2.84,2.84,0,0,1,45.16,52Z" fill="#1e1e1e"/>
|
||||
<path d="M46.61,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L48.23,53a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.62.62,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6.81,6.81,0,0,1,.71.32,2.42,2.42,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.37,3.37,0,0,1,46.61,55.44Z" fill="#1e1e1e"/>
|
||||
<path d="M56.88,52.9H52.65a2.21,2.21,0,0,0,.54,1.55A1.85,1.85,0,0,0,54.6,55a2.94,2.94,0,0,0,1.87-.67v.9a3.45,3.45,0,0,1-2.09.58,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.17,3.17,0,0,1,.64,2.11Zm-1-.82a2,2,0,0,0-.4-1.29,1.37,1.37,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M93.79,74.58H89.34v-8.4H93.6v.89H90.32v2.79h3v.89h-3v2.94h3.47Z" fill="#1e1e1e"/>
|
||||
<path d="M99.66,68.58l-2,3,2,3H98.51l-1.18-2c-.08-.12-.16-.27-.27-.45h0l-.27.45-1.21,2h-1.1l2-2.94-2-3.06h1.11l1.16,2c.09.15.17.31.26.47h0l1.5-2.52Z" fill="#1e1e1e"/>
|
||||
<path d="M104.88,74.3a3.14,3.14,0,0,1-1.65.42,2.72,2.72,0,0,1-2.07-.84,3,3,0,0,1-.78-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.26-.9,3.27,3.27,0,0,1,1.4.29v1a2.4,2.4,0,0,0-1.43-.47,1.92,1.92,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.55,1.66,1.91,1.91,0,0,0,1.49.61,2.43,2.43,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
|
||||
<path d="M111.15,71.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48,2.24,2.24,0,0,0-.58,1.28Z" fill="#1e1e1e"/>
|
||||
<path d="M113.59,73.71h0v3.63h-1V68.58h1v1h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,113.59,73.71Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.45,2.45,0,0,0-.46-1.57,1.54,1.54,0,0,0-1.26-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,113.56,71.29Z" fill="#1e1e1e"/>
|
||||
<path d="M122.44,74.52a1.76,1.76,0,0,1-.89.19c-1.06,0-1.58-.59-1.58-1.76V69.4h-1v-.82h1V67.11l1-.31v1.78h1.51v.82h-1.51v3.38a1.37,1.37,0,0,0,.21.86.8.8,0,0,0,.67.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M124.22,67.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.64.64,0,0,1,.45.18.6.6,0,0,1,.18.44.59.59,0,0,1-.18.44A.61.61,0,0,1,124.22,67.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M129.14,74.72a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.68,2.68,0,0,1,2.09.82,3.76,3.76,0,0,1-.05,4.6A2.88,2.88,0,0,1,129.14,74.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.63,2.63,0,0,0-.53,1.73,2.47,2.47,0,0,0,.54,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,129.21,69.25Z" fill="#1e1e1e"/>
|
||||
<path d="M138.64,74.58h-1V71.16c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.84,1.84,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M146.9,74.58h-1V70.75h-4.34v3.83h-1v-8.4h1v3.68h4.34V66.18h1Z" fill="#1e1e1e"/>
|
||||
<path d="M153.27,74.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a2.94,2.94,0,0,0-2,.74V69a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.9.9,0,0,0,.32.71,1.16,1.16,0,0,0,.83.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M160.06,74.58h-1V71.16c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M167,74.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V65.7h1Zm-1-2.71V71a1.73,1.73,0,0,0-.48-1.23,1.78,1.78,0,0,0-2.6.14,2.81,2.81,0,0,0-.51,1.78,2.51,2.51,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6,1.64,1.64,0,0,0,1.3-.58A2.17,2.17,0,0,0,166,71.87Z" fill="#1e1e1e"/>
|
||||
<path d="M169.9,74.58h-1V65.7h1Z" fill="#1e1e1e"/>
|
||||
<path d="M176.66,71.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
|
||||
<path d="M181.25,69.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M153.53,113.58h-1v-3.83H148.2v3.83h-1v-8.4h1v3.68h4.35v-3.68h1Z" fill="#1e1e1e"/>
|
||||
<path d="M155.34,113.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.39,3.39,0,0,0,.61.15,3.09,3.09,0,0,0,.58.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2,2,0,0,1-.35-2.44,2.1,2.1,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,155.34,113.24Z" fill="#1e1e1e"/>
|
||||
<path d="M167.08,106.07h-2.43v7.51h-1v-7.51h-2.42v-.89h5.83Z" fill="#1e1e1e"/>
|
||||
<path d="M167.77,113.24v-1.16a2.23,2.23,0,0,0,.48.32,4.51,4.51,0,0,0,1.2.38,3.09,3.09,0,0,0,.58.06,2.27,2.27,0,0,0,1.35-.34,1.15,1.15,0,0,0,.45-1,1.1,1.1,0,0,0-.15-.59,1.76,1.76,0,0,0-.41-.46,4.43,4.43,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.2,3.2,0,0,1-.66-.51,2.1,2.1,0,0,1-.45-.62,2.07,2.07,0,0,1-.16-.82,2,2,0,0,1,.25-1,2.13,2.13,0,0,1,.67-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.42,1.42,0,0,0-.41.4,1.07,1.07,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.31,1.31,0,0,0,.36.43,3.31,3.31,0,0,0,.57.37c.22.12.48.26.78.4s.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.42,2.42,0,0,1,.48.67,1.84,1.84,0,0,1,.18.83,2.14,2.14,0,0,1-.24,1.05,1.93,1.93,0,0,1-.66.7,2.8,2.8,0,0,1-1,.39,5.24,5.24,0,0,1-1.14.12l-.49,0c-.19,0-.39-.06-.6-.1a4.74,4.74,0,0,1-.57-.15A2,2,0,0,1,167.77,113.24Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M147.86,153.26h-1v-3.83h-4.35v3.83h-1v-8.4h1v3.69h4.35v-3.69h1Z" fill="#1e1e1e"/>
|
||||
<path d="M152.72,153.21a1.87,1.87,0,0,1-.89.18c-1.06,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.37,1.37,0,0,0,.21.86.81.81,0,0,0,.68.26,1,1,0,0,0,.62-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M156.79,153.21a2,2,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M159.06,152.4h0V156h-1v-8.76h1v1.06h0a2.26,2.26,0,0,1,2.07-1.2,2.19,2.19,0,0,1,1.81.81,3.37,3.37,0,0,1,.65,2.16,3.71,3.71,0,0,1-.73,2.41,2.46,2.46,0,0,1-2,.91A2,2,0,0,1,159.06,152.4Zm0-2.42v.84a1.77,1.77,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3.09,3.09,0,0,0,.5-1.86,2.41,2.41,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.71,1.71,0,0,0-1.35.59A2.11,2.11,0,0,0,159,150Z" fill="#1e1e1e"/>
|
||||
<path d="M164.76,153.05v-1a2.9,2.9,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.85a.72.72,0,0,0-.11-.4,1,1,0,0,0-.29-.3,2.27,2.27,0,0,0-.44-.23l-.53-.22a6.87,6.87,0,0,1-.7-.31,2,2,0,0,1-.51-.37,1.41,1.41,0,0,1-.3-.46,1.57,1.57,0,0,1-.11-.6,1.45,1.45,0,0,1,.2-.75,1.65,1.65,0,0,1,.51-.54,2.26,2.26,0,0,1,.74-.33,3,3,0,0,1,.85-.12,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.44,1.75,1.75,0,0,0-.48.06,1.34,1.34,0,0,0-.38.18.85.85,0,0,0-.24.26.82.82,0,0,0-.08.35.74.74,0,0,0,.33.67,1.89,1.89,0,0,0,.4.22l.53.22c.27.1.51.21.72.31a2.44,2.44,0,0,1,.54.37,1.38,1.38,0,0,1,.34.46,1.54,1.54,0,0,1,.12.63,1.47,1.47,0,0,1-.19.77,1.85,1.85,0,0,1-.53.55,2.54,2.54,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,164.76,153.05Z" fill="#1e1e1e"/>
|
||||
<path d="M176.37,153.26H175.2l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.5a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2.22,2.22,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,2,2,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38c.09.13.19.3.3.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
|
||||
<path d="M181.87,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.24,2.24,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M188.44,153.26h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.32,3.32,0,0,1-.68-2.19,3.6,3.6,0,0,1,.75-2.39,2.48,2.48,0,0,1,2-.9,1.94,1.94,0,0,1,1.8,1h0v-3.72h1Zm-1-2.71v-.88a1.7,1.7,0,0,0-.48-1.23,1.61,1.61,0,0,0-1.22-.51,1.64,1.64,0,0,0-1.38.65,2.79,2.79,0,0,0-.51,1.78,2.56,2.56,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6A1.66,1.66,0,0,0,187,152,2.2,2.2,0,0,0,187.48,150.55Z" fill="#1e1e1e"/>
|
||||
<path d="M190.88,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,190.88,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M196.42,148.24a1.18,1.18,0,0,0-.73-.2,1.22,1.22,0,0,0-1,.58,2.68,2.68,0,0,0-.42,1.59v3h-1v-6h1v1.24h0a2.08,2.08,0,0,1,.62-1,1.49,1.49,0,0,1,.95-.35,1.65,1.65,0,0,1,.57.08Z" fill="#1e1e1e"/>
|
||||
<path d="M202.29,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.26,2.26,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M207.83,153a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.33,3.33,0,0,1,.85-2.39,3,3,0,0,1,2.27-.9,3.09,3.09,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.39,2.39,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.45,2.45,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
|
||||
<path d="M212.07,153.21a2,2,0,0,1-.9.18c-1,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.52v.83h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M213.84,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.62.62,0,0,1,.63.63.6.6,0,0,1-.18.44A.61.61,0,0,1,213.84,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M218.76,153.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,218.76,153.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,218.83,147.93Z" fill="#1e1e1e"/>
|
||||
<path d="M228.27,153.26h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.88,2.88,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M183,193.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.29,2.29,0,0,0,1.36-.34,1.14,1.14,0,0,0,.44-1,1.2,1.2,0,0,0-.14-.59,1.64,1.64,0,0,0-.42-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4,4,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,1.83,1.83,0,0,0-.57.22,1.31,1.31,0,0,0-.41.4,1,1,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.41,1.41,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.59.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.49,0c-.2,0-.4-.06-.6-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,183,193.24Z" fill="#1e1e1e"/>
|
||||
<path d="M192,193.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78H192v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M197.6,193.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.92.92,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M202.19,193.52a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M204,186.06a.59.59,0,0,1-.44-.18.61.61,0,0,1-.18-.45.6.6,0,0,1,.18-.44.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,204,186.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M210.47,193.3a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.23,3.23,0,0,1,1.39.29v1a2.38,2.38,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.56,1.66,1.89,1.89,0,0,0,1.48.61,2.39,2.39,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
|
||||
<path d="M219.6,186.07h-3.28V189h3v.89h-3v3.72h-1v-8.4h4.26Z" fill="#1e1e1e"/>
|
||||
<path d="M221.56,186.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M224.94,193.58h-1V184.7h1Z" fill="#1e1e1e"/>
|
||||
<path d="M231.7,190.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
|
||||
<path d="M232.79,193.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.55,2.55,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.59,3.59,0,0,1,2.25.16v1a2.71,2.71,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.15,1.15,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.8.8,0,0,0,.24.28,1.91,1.91,0,0,0,.4.23l.54.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.5,1.5,0,0,1-.19.78,1.61,1.61,0,0,1-.53.54,2.3,2.3,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,232.79,193.36Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M221.16,233.26H220l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.51a3.62,3.62,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2,2,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,1.58,1.58,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38l.31.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.43,1.43,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
|
||||
<path d="M224.34,233.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,224.34,233.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74A2.46,2.46,0,0,0,223,232a1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,224.41,227.93Z" fill="#1e1e1e"/>
|
||||
<path d="M233.72,233.26h-1v-.94h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.86-2.14-2.56v-3.59h.95v3.44c0,1.26.49,1.9,1.46,1.9a1.48,1.48,0,0,0,1.15-.52,2,2,0,0,0,.46-1.36v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M238.44,233.21a1.92,1.92,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V225.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M240.22,225.74a.62.62,0,0,1-.44-.17.65.65,0,0,1,0-.9.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,240.22,225.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M247.61,233.26h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.52,1.52,0,0,0-1.2.54,2.05,2.05,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.82,2.82,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M254.54,232.78q0,3.32-3.16,3.31a4.31,4.31,0,0,1-1.95-.42v-1a4.06,4.06,0,0,0,1.94.56c1.47,0,2.21-.79,2.21-2.36v-.65h0a2.43,2.43,0,0,1-3.87.35,3.24,3.24,0,0,1-.68-2.15,3.73,3.73,0,0,1,.74-2.43,2.44,2.44,0,0,1,2-.91,2,2,0,0,1,1.8,1h0v-.84h1Zm-1-2.23v-.88a1.73,1.73,0,0,0-.48-1.23,1.6,1.6,0,0,0-1.21-.51,1.68,1.68,0,0,0-1.39.65,2.86,2.86,0,0,0-.5,1.81,2.51,2.51,0,0,0,.48,1.61,1.56,1.56,0,0,0,1.28.6,1.63,1.63,0,0,0,1.31-.58A2.12,2.12,0,0,0,253.58,230.55Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M251.74,273.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
|
||||
<path d="M256.52,273.72a3.66,3.66,0,0,1-2.86-1.18,4.33,4.33,0,0,1-1.08-3.06,4.66,4.66,0,0,1,1.1-3.24,3.83,3.83,0,0,1,3-1.2,3.6,3.6,0,0,1,2.8,1.17,4.37,4.37,0,0,1,1.07,3.06,4.66,4.66,0,0,1-1.09,3.25A3.76,3.76,0,0,1,256.52,273.72Zm.07-7.79a2.73,2.73,0,0,0-2.15.95,4.24,4.24,0,0,0,0,5,2.61,2.61,0,0,0,2.1.95,2.77,2.77,0,0,0,2.18-.91,3.68,3.68,0,0,0,.79-2.52,3.81,3.81,0,0,0-.77-2.57A2.63,2.63,0,0,0,256.59,265.93Z" fill="#1e1e1e"/>
|
||||
<path d="M268.2,273.58H267l-1.41-2.36c-.13-.21-.25-.4-.37-.56a2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,2,2,0,0,0-.5-.06h-.81v3.57h-1v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7,2.33,2.33,0,0,1,.19,1,2.28,2.28,0,0,1-.13.8,2,2,0,0,1-.38.65,2,2,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48Zm-5-7.51v3h1.34a1.9,1.9,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.28,1.28,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.32,1.32,0,0,0-.44-1.05,1.83,1.83,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
|
||||
<path d="M269,273.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,269,273.24Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M253.45,313.89h-1.09l-.89-2.35H247.9l-.84,2.35H246l3.22-8.4h1Zm-2.31-3.24-1.32-3.58a3.36,3.36,0,0,1-.12-.56h0a3.13,3.13,0,0,1-.13.56l-1.31,3.58Z" fill="#1e1e1e"/>
|
||||
<path d="M259.4,313.89h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1v3.44c0,1.26.48,1.9,1.45,1.9a1.49,1.49,0,0,0,1.16-.52,2,2,0,0,0,.45-1.36v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M264.13,313.83a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.2.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M270.39,313.89h-1v-3.45c0-1.25-.46-1.88-1.39-1.88a1.53,1.53,0,0,0-1.19.54,2,2,0,0,0-.48,1.39v3.4h-1V305h1v3.88h0a2.17,2.17,0,0,1,2-1.14c1.36,0,2,.82,2,2.45Z" fill="#1e1e1e"/>
|
||||
<path d="M277,311.13h-4.23a2.19,2.19,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.41.55,3,3,0,0,0,1.87-.67v.9a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.34,3.34,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.55,2.55,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76,3.19,3.19,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M283.46,313.89h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.51.64,2.86,2.86,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M288.05,313.83a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.58-1.57-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.49,1.49,0,0,0,.2.87.83.83,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M289.83,306.37a.59.59,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,289.83,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M296.33,313.62a3.1,3.1,0,0,1-1.64.41,2.68,2.68,0,0,1-2.07-.83,3,3,0,0,1-.79-2.17,3.32,3.32,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.06,3.06,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.41,2.41,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.4,2.4,0,0,0,1.48-.53Z" fill="#1e1e1e"/>
|
||||
<path d="M302.08,313.89h-1V313h0a2,2,0,0,1-1.84,1.07,1.93,1.93,0,0,1-1.4-.47,1.65,1.65,0,0,1-.51-1.26c0-1.12.66-1.77,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a3,3,0,0,0-2,.74v-1a3.68,3.68,0,0,1,2-.57c1.41,0,2.12.75,2.12,2.24Zm-1-3-1.45.2a2.35,2.35,0,0,0-1,.33,1,1,0,0,0-.34.84.93.93,0,0,0,.32.72,1.22,1.22,0,0,0,.83.28,1.52,1.52,0,0,0,1.18-.51,1.77,1.77,0,0,0,.47-1.26Z" fill="#1e1e1e"/>
|
||||
<path d="M306.67,313.83a1.76,1.76,0,0,1-.89.19c-1,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.21.87.81.81,0,0,0,.68.25.94.94,0,0,0,.62-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M308.45,306.37a.6.6,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,308.45,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M313.37,314a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.26,3.26,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.09.83,3.75,3.75,0,0,1-.05,4.59A2.83,2.83,0,0,1,313.37,314Zm.07-5.47a1.8,1.8,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.85,1.85,0,0,0,1.46.62,1.76,1.76,0,0,0,1.43-.61,2.58,2.58,0,0,0,.5-1.72,2.62,2.62,0,0,0-.5-1.73A1.73,1.73,0,0,0,313.44,308.56Z" fill="#1e1e1e"/>
|
||||
<path d="M322.87,313.89h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.82,1.82,0,0,1,1.5.64,2.8,2.8,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M281.47,353.58h-1.09l-.89-2.36h-3.56l-.84,2.36H274l3.23-8.4h1Zm-2.3-3.24-1.32-3.58a3.65,3.65,0,0,1-.13-.56h0a3.49,3.49,0,0,1-.14.56l-1.31,3.58Z" fill="#1e1e1e"/>
|
||||
<path d="M287.43,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h.95V351c0,1.27.49,1.9,1.46,1.9a1.45,1.45,0,0,0,1.15-.52,1.94,1.94,0,0,0,.46-1.35v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M292.15,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M298.41,353.58h-1v-3.46c0-1.25-.46-1.87-1.39-1.87a1.51,1.51,0,0,0-1.18.54,2,2,0,0,0-.48,1.39v3.4h-1V344.7h1v3.88h0a2.18,2.18,0,0,1,2-1.14c1.35,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
|
||||
<path d="M302.73,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,302.73,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.79,1.79,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.76,1.76,0,0,0,302.8,348.25Z" fill="#1e1e1e"/>
|
||||
<path d="M310.39,348.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.64,2.64,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
|
||||
<path d="M311.92,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M318.52,347.85,315,352.76h3.51v.82h-4.92v-.3l3.55-4.88h-3.22v-.82h4.63Z" fill="#1e1e1e"/>
|
||||
<path d="M324.06,353.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.65,1.65,0,0,1-.51-1.26c0-1.13.67-1.78,2-2l1.79-.25q0-1.53-1.23-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.44.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.56,1.56,0,0,0,1.18-.5,1.79,1.79,0,0,0,.46-1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M328.65,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M330.42,346.06a.62.62,0,0,1-.44-.18.61.61,0,0,1-.18-.45A.6.6,0,0,1,330,345a.62.62,0,0,1,.44-.18.61.61,0,0,1,.45.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,330.42,346.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M335.35,353.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,335.35,353.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,335.42,348.25Z" fill="#1e1e1e"/>
|
||||
<path d="M344.85,353.58h-1v-3.42c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M414.56,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
|
||||
<path d="M421,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h1V351c0,1.27.49,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52A2,2,0,0,0,420,351v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M422.57,353.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.29,2.29,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.41,3.41,0,0,1,.85-.11,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.33,1.33,0,0,0-.38.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.91,1.91,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.42,1.42,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.41,3.41,0,0,1,422.57,353.36Z" fill="#1e1e1e"/>
|
||||
<path d="M430.81,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H429.3v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M434.6,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,434.6,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.17,3.17,0,0,0,0-3.45A1.73,1.73,0,0,0,434.67,348.25Z" fill="#1e1e1e"/>
|
||||
<path d="M447.65,353.58h-1v-3.45a2.66,2.66,0,0,0-.3-1.44,1.19,1.19,0,0,0-1-.44,1.25,1.25,0,0,0-1,.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.76,1.76,0,0,1,1.08.34,1.73,1.73,0,0,1,.62.9,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
|
||||
<path d="M452.73,353.58h-1v-7.26a1.74,1.74,0,0,1-.33.25,5.06,5.06,0,0,1-.48.29,5.85,5.85,0,0,1-.56.26,5,5,0,0,1-.58.2v-1a5.46,5.46,0,0,0,.68-.23c.23-.1.46-.22.69-.34a5.88,5.88,0,0,0,.65-.39,4.87,4.87,0,0,0,.53-.39h.36Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M517.25,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.5,4.5,0,0,1,1.21-3.26,4.12,4.12,0,0,1,3.08-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
|
||||
<path d="M523.69,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1V351c0,1.27.48,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M525.26,353.36v-1a2.82,2.82,0,0,0,1.73.58c.84,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.49-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.35,1.35,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.68,2.68,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,525.26,353.36Z" fill="#1e1e1e"/>
|
||||
<path d="M533.5,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H532v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M537.29,353.72a2.77,2.77,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.71,2.71,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,537.29,353.72Zm.07-5.47a1.81,1.81,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,537.36,348.25Z" fill="#1e1e1e"/>
|
||||
<path d="M550.33,353.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.16,1.16,0,0,0-1-.44,1.28,1.28,0,0,0-1,.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.42,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
|
||||
<path d="M555.77,353.71a.63.63,0,0,1-.46-.2.6.6,0,0,1-.19-.46.62.62,0,0,1,.19-.46.63.63,0,0,1,.46-.2.65.65,0,0,1,.47.2.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.65.65,0,0,1,555.77,353.71Z" fill="#1e1e1e"/>
|
||||
<path d="M558.37,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.64.64,0,0,1,.93,0,.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.63.63,0,0,1,558.37,353.71Z" fill="#1e1e1e"/>
|
||||
<path d="M561,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.67.67,0,0,1,1.13.46.61.61,0,0,1-.2.46A.63.63,0,0,1,561,353.71Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M434.72,307.23a4.92,4.92,0,0,1-2.32.49,3.72,3.72,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.45,4.45,0,0,1,1.21-3.26,4.11,4.11,0,0,1,3.08-1.24,5,5,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
|
||||
<path d="M441.15,307.58h-1v-.95h0a2,2,0,0,1-1.86,1.09c-1.42,0-2.14-.85-2.14-2.55v-3.59h1V305c0,1.27.48,1.9,1.45,1.9a1.47,1.47,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
|
||||
<path d="M442.73,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23l-.54-.21a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.37,1.37,0,0,1,.47,1.09,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,442.73,307.36Z" fill="#1e1e1e"/>
|
||||
<path d="M451,307.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.59-1.58-1.76V302.4h-1v-.82h1v-1.47l1-.31v1.78H451v.82h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
<path d="M454.76,307.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,454.76,307.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,454.83,302.25Z" fill="#1e1e1e"/>
|
||||
<path d="M467.8,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.46-1.77-1.37-1.77a1.24,1.24,0,0,0-1,.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.87-1.09,1.71,1.71,0,0,1,1.07.34,1.81,1.81,0,0,1,.63.9,2.13,2.13,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
|
||||
<path d="M481.42,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.45-1.77-1.36-1.77a1.27,1.27,0,0,0-1.05.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
|
||||
<path d="M483.73,300.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M491.27,307.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.7,1.7,0,0,0-.49-1.23,1.61,1.61,0,0,0-1.21-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.59,1.59,0,0,0,1.3.6,1.61,1.61,0,0,0,1.3-.58A2.13,2.13,0,0,0,490.31,304.87Z" fill="#1e1e1e"/>
|
||||
<path d="M498.33,307.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.73,1.73,0,0,0-.48-1.23,1.64,1.64,0,0,0-1.22-.5,1.66,1.66,0,0,0-1.38.64,2.81,2.81,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.57,1.57,0,0,0,1.3.6,1.63,1.63,0,0,0,1.3-.58A2.17,2.17,0,0,0,497.37,304.87Z" fill="#1e1e1e"/>
|
||||
<path d="M501.24,307.58h-1V298.7h1Z" fill="#1e1e1e"/>
|
||||
<path d="M508,304.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.5,3.5,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.36,3.36,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.28,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.65,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
|
||||
<path d="M517,301.58l-1.8,6h-1l-1.23-4.3a2.84,2.84,0,0,1-.1-.55h0a2.44,2.44,0,0,1-.12.54l-1.35,4.31h-1l-1.81-6h1l1.24,4.51a3.12,3.12,0,0,1,.08.54H511a2.29,2.29,0,0,1,.1-.55l1.38-4.5h.88l1.25,4.52a5.29,5.29,0,0,1,.08.54h0a2.61,2.61,0,0,1,.1-.54l1.22-4.52Z" fill="#1e1e1e"/>
|
||||
<path d="M522.43,307.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.64,1.64,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
|
||||
<path d="M527.37,302.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
|
||||
<path d="M533.23,304.82H529a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48A2.24,2.24,0,0,0,529,304Z" fill="#1e1e1e"/>
|
||||
<path d="M534.32,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.1-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.22,1.22,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.67,1.67,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.57,1.57,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,534.32,307.36Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M638,353.58h-4.46v-8.4h4.27v.89h-3.28v2.79h3v.89h-3v2.94H638Z" fill="#1e1e1e"/>
|
||||
<path d="M644.48,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M651.42,353.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.67-2.19,3.57,3.57,0,0,1,.75-2.38,2.46,2.46,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V344.7h1Zm-1-2.71V350a1.73,1.73,0,0,0-.48-1.23,1.65,1.65,0,0,0-1.22-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.73,1.73,0,0,0,2.6,0A2.13,2.13,0,0,0,650.46,350.87Z" fill="#1e1e1e"/>
|
||||
<path d="M654.35,352.71h0v3.63h-1v-8.76h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.21,2.21,0,0,1,1.81.8,3.37,3.37,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,654.35,352.71Zm0-2.42v.84a1.73,1.73,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.39,2.39,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,654.32,350.29Z" fill="#1e1e1e"/>
|
||||
<path d="M662.92,353.72a2.79,2.79,0,0,1-2.12-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,662.92,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.84,1.84,0,0,0,1.45.61,1.77,1.77,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.75,1.75,0,0,0,663,348.25Z" fill="#1e1e1e"/>
|
||||
<path d="M667.94,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
|
||||
<path d="M675.33,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
|
||||
<path d="M679.93,353.52a1.82,1.82,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.38,1.38,0,0,0,.2.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 55 KiB |
232
YaeBlog/source/posts/build-dotnet-from-source.md
Normal file
232
YaeBlog/source/posts/build-dotnet-from-source.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
title: 交叉编译.NET到RISC-V平台
|
||||
date: 2024-08-25T15:41:05.9519941+08:00
|
||||
tags:
|
||||
- dotnet
|
||||
- 技术笔记
|
||||
---
|
||||
|
||||
|
||||
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
|
||||
|
||||
<!--more-->
|
||||
|
||||
这次我们打算在`x86_64`平台上交叉编译`.NET`到`riscv64`平台上。
|
||||
|
||||
首先从相关的[进度跟踪页面](https://github.com/dotnet/runtime/issues/84834)显示,.NET移植到RISC-V的进度还远远没有完成,但是在整个SDK中除了AOT编译器的部分都可以在RISC-V平台上编译了。
|
||||
|
||||
## 环境准备
|
||||
|
||||
我们构建的环境是Arch Linux,因此依赖包的安装使用`pacman`进行。综合[.NET官方文档](https://github.com/dotnet/runtime/blob/main/docs/workflow/requirements/linux-requirements.md)给出的信息和Arch Linux官方打包的脚本,所需要安装的软件包如下:
|
||||
|
||||
| 包名 | 备注 |
|
||||
| ------------- | ------------------------------------------------------------ |
|
||||
| bash | |
|
||||
| clang | |
|
||||
| lld | |
|
||||
| cmake | |
|
||||
| git | |
|
||||
| icu | 第一次看见这个名词就想吐槽,谁TM想得到重症监护室会是一个全球化支持库,, |
|
||||
| inetutils | 常见的网络工具库,官方文档没有但是构建脚本有 |
|
||||
| krb5 | 一个网络通信认证库?不懂 |
|
||||
| libgit2 | |
|
||||
| libunwind | 解析程序运行堆栈的魔法工具 |
|
||||
| libxml2 | |
|
||||
| lldb | |
|
||||
| llvm | |
|
||||
| lttng-ust2.12 | 又是一个跟踪运行的魔法工具 |
|
||||
| openssl | |
|
||||
| systemd | |
|
||||
| zlib | |
|
||||
|
||||
### 交叉编译工具链
|
||||
|
||||
在正式开始编译.NET之前,先学习如何搭建一套C/C++的交叉编译工具链。
|
||||
|
||||
通常一份GNU工具链只能针对一个平台进行编译,但是LLVM工具链是一套先天的交叉编译工具链,例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
|
||||
|
||||

|
||||
|
||||
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
|
||||
|
||||
但是直接使用`clang++ --target=riscv64-linux-gnu hello.cpp -o hello`时会爆出一个奇怪的找不到头文件错误:
|
||||
|
||||
```cpp
|
||||
// File: hello.cpp
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << "Hello, world!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译,因此尝试在本地自行编译一套LLVM工具链。
|
||||
|
||||
首先从[Github Release](https://github.com/llvm/llvm-project/releases)上下载最新的`llvm-project`源代码并解压到本地文件夹中。这里126M的压缩文件可以解压出一个1.8G大小的源代码文件夹。创建一个`build`文件夹,在该文件夹使用如下的配置进行编译,在配置中使用`LLVM_TARGETS_TO_BUILD`选择启用`X86`和`RISCV`的支持。
|
||||
|
||||
```bash
|
||||
cmake ../llvm-project.src/llvm \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
|
||||
-DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra"
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前,因此调用时将会优先调用我们自行编译的LLVM工具链,而不是系统中安装的LLVM工具链。
|
||||
|
||||

|
||||
|
||||
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装LLVM工具链的问题。仔细一想也确实,这里提示找不到对应的头文件应该是找不到RISC-V架构之下的头文件——这里的也是交叉编译的主要问题所在:虽然LLVM工具链宣称自己是原生支持交叉编译的,但是没人宣称说标准库和头文件是原生的。这里我们就需要一个根文件系统来提供这些头文件和各种库文件。
|
||||
|
||||
### 生成根文件系统
|
||||
|
||||
在.NET的构建文档中提供了一个自动生成头文件的脚本,但是这个脚本似乎强依赖某个U开头的发行版,身为Arch神教信徒的我似乎没有办法使用。直接使用预构建好的镜像又屏蔽了太多的技术细节,感觉也不太好。因此打算尝试使用[arch-riscv](https://mirror.iscas.ac.cn/archriscv/)提供的移植Arch Linux系统作为根文件系统。
|
||||
|
||||
首先使用移植之后的根文件系统构建一个`archriscv`镜像:
|
||||
|
||||
```Dockerfile
|
||||
FROM archriscv AS bootstrap
|
||||
|
||||
COPY etc /rootfs
|
||||
COPY bootstrap/pacstrap-docker /usr/local/bin/
|
||||
RUN pacstrap-docker /rootfs base
|
||||
RUN rm /rootfs/var/lib/pacman/sync/*
|
||||
|
||||
FROM scratch AS root
|
||||
|
||||
COPY --from=bootstrap /rootfs /
|
||||
COPY etc /etc
|
||||
|
||||
LABEL org.opencontainers.image.title="Arch Linux RISC-V"
|
||||
LABEL org.opencontainers.image.description="This is an Arch Linux port to the RISC-V architecture."
|
||||
|
||||
ENV LANG=en_US.UTF-8
|
||||
RUN ldconfig && locale-gen
|
||||
RUN pacman-key --init && \
|
||||
pacman-key --populate && \
|
||||
bash -c "rm -rf etc/pacman.d/gnupg/{openpgp-revocs.d/,private-keys-v1.d/,pubring.gpg~,gnupg.S.}*"
|
||||
|
||||
CMD ["/usr/bin/bash"]
|
||||
```
|
||||
|
||||
虽然这个镜像是一个自举的镜像,给出这个构建文件似乎没有什么用处(笑)。再在这个镜像的基础上新建一层镜像安装各种.NET的依赖项。
|
||||
|
||||
```dockerfile
|
||||
FROM archriscv
|
||||
|
||||
RUN pacman -Syyu --noconfirm bash clang cmake git icu inetutils \
|
||||
krb5 libgit2 libunwind libxml2 lldb llvm lttng-ust2.12 \
|
||||
openssl systemd zlib
|
||||
```
|
||||
|
||||
构建这个镜像,再将这个镜像根目录下的所有文件拷贝出来。
|
||||
|
||||
```bash
|
||||
docker build . --platform linux/riscv64 -t archriscv:base-devel
|
||||
mkdir rootfs
|
||||
cid=$(docker run -d --platform linux/riscv64 archriscv:base-devel)
|
||||
sudo docker cp $cid:/ rootfs
|
||||
sudo chown $USER:$USER -R rootfs
|
||||
```
|
||||
|
||||
新建一个`runtime-build`文件夹,使用下面的指令在`rootfs`文件系统中构建`libcxx`和`compiler-rt`。
|
||||
|
||||
> `libcxx`和`compiler-rt`不是常规交叉编译需要的,而是编译.NET所需要的。
|
||||
|
||||
```bash
|
||||
export TARGET_TRIPLE="riscv64-linux-gnu"
|
||||
export CLANG_MAJOR_VERSION=18
|
||||
export ROOTFS_DIR=<ROOTFS>
|
||||
cmake -S ../llvm-project.src/runtimes \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_COMPILER=clang \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_ASM_COMPILER_TARGET="$TARGET_TRIPLE" \
|
||||
-DCMAKE_C_COMPILER_TARGET="$TARGET_TRIPLE" \
|
||||
-DCMAKE_CXX_COMPILER_TARGET="$TARGET_TRIPLE" \
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_SYSROOT="$ROOTFS_DIR" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
|
||||
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" \
|
||||
-DLLVM_USE_LINKER=lld \
|
||||
-DLLVM_ENABLE_RUNTIMES="libcxx;compiler-rt" \
|
||||
-DLIBCXX_ENABLE_SHARED=OFF \
|
||||
-DLIBCXX_CXX_ABI=libstdc++ \
|
||||
-DLIBCXX_CXX_ABI_INCLUDE_PATHS="$ROOTFS_DIR/usr/include/c++/14.2.1/;$ROOTFS_DIR/usr/include/c++/14.2.1/riscv64-unknown-linux-gnu/" \
|
||||
-DCOMPILER_RT_CXX_LIBRARY="libcxx" \
|
||||
-DCOMPILER_RT_STATIC_CXX_LIBRARY=ON \
|
||||
-DCOMPILER_RT_BUILD_SANITIZERS=OFF \
|
||||
-DCOMPILER_RT_BUILD_MEMPROF=OFF \
|
||||
-DCOMPILER_RT_BUILD_LIBFUZZER=OFF \
|
||||
-DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
|
||||
-DCOMPILER_RT_INSTALL_PATH="/usr/local/lib/clang/$CLANG_MAJOR_VERSION"
|
||||
make -j20
|
||||
sudo cmake --install . --prefix "$ROOTFS_DIR/usr"
|
||||
```
|
||||
|
||||
在构建指令中需要根据安装的`gcc`版本调整`_DLIBCXX_CXX_ABI_INCLUDE_PATHS`的路径。
|
||||
|
||||
完成所有上述的工作之后,回到我们最开始的你好世界样例,使用下面这行神秘的代码进行编译:
|
||||
|
||||
```bash
|
||||
clang++ --target=riscv64-linux-gnu --sysroot=$ROOTFS_DIR -fuse-ld=lld hello.cpp -o hello
|
||||
```
|
||||
|
||||
这次编译不会出现问题,上面指定的三个参数依次为指定目标三元组、指定根文件系统的位置和指定使用`lld`作为链接器。使用Docker镜像进行测试确认编译之后的二进制文件可以正常运行。
|
||||
|
||||
### 复盘
|
||||
|
||||
在正式开始下一步之前,我们先复盘一下在搭建交叉编译环境时我们都做了什么:
|
||||
|
||||
- 使用`LLVM_TARGETS_TO_BUILD`编译了一套新的LLVM,
|
||||
- 将安装了基础依赖包的`archriscv`导出作为根文件系统,
|
||||
- 使用该根文件系统在该根文件系统中编译了`libcxx`和`compiler-rt`两个库。
|
||||
|
||||
这三步也带来了三个问题:
|
||||
|
||||
1. Arch Linux自带的LLVM工具链难道不能交叉编译吗?
|
||||
2. Arch Linux 官方提供的`riscv64-linux-gnu-gcc`包能够作为根文件系统吗?
|
||||
3. 能够在上述的根文件系统中安装我们需要的`libcxx`和`compiler-rt`两个库吗?
|
||||
|
||||
第一个问题的回答是Arch Linux安装的LLVM工具是可以交叉编译的。虽然在Arch Linux官方构建LLVM工具链的[构建脚本](https://gitlab.archlinux.org/archlinux/packaging/packages/clang/-/blob/main/PKGBUILD?ref_type=heads)中没有使用`LLVM_TARGETS_TO_BUILD`参数,但是这个参数的默认值是`all`。这一点我们也可以通过实验来验证。
|
||||
|
||||
于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`。
|
||||
|
||||
第二个问题的回答可以使用实验来验证,首先安装`riscv64-linux-gnu-gcc`,然后将根文件系统的位置设置为`/usr/riscv64-linux-gnu`,重新编译上面的你好世界样例。编译之后可以正常执行。
|
||||
|
||||
第三个问题的回答是还是新建一个根文件系统罢,随便往系统目录里面写东西感觉是一个不太好的习惯。
|
||||
|
||||
## 正式编译
|
||||
|
||||
首先进入克隆代码的目录,运行初始化脚本。
|
||||
|
||||
```bash
|
||||
cd dotnet
|
||||
./prep-source-build.sh
|
||||
```
|
||||
|
||||
设置根文件系统的目录,这里仍然使用从安装了`base-devel`的Docker容器中导出并自行编译了`compiler-rt`和`libcxx`的根文件系统。
|
||||
|
||||
```bash
|
||||
export ROOTFS_DIR=<rootfs>
|
||||
```
|
||||
|
||||
然后使用下面这条神秘的命令开始交叉编译:
|
||||
|
||||
```bash
|
||||
./build.sh -sb --clean-while-building /p:TargetOS=linux /p:TargetArchitecture=riscv64 /p:Crossbuild=true /p:BuildArgs="/p:BundleNativeAotCompiler=false"
|
||||
```
|
||||
|
||||
上面的第一个参数是指定了`source-build`选项,第二个参数指定了在编译的过程中清理不需要的文件以节省硬盘空间,后面的几个MSBUILD参数则是指定为RISC-V架构上的Linux系统构建,并且不构建AOT编译器。
|
||||
|
||||
但是现在的.NET在RISC-V平台上还是废物一个,甚至连`dotnet new`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
|
||||
|
||||

|
||||
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
Normal file
Binary file not shown.
67
YaeBlog/source/posts/cncc-2024.md
Normal file
67
YaeBlog/source/posts/cncc-2024.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: 2024中国计算机大会
|
||||
date: 2024-11-03T14:06:36.4212070+08:00
|
||||
tags:
|
||||
- 杂谈
|
||||
---
|
||||
|
||||
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办,而鄙人在下不才我,有幸受到实验室资助前去参观学习。
|
||||
|
||||
<!--more-->
|
||||
|
||||
首先开幕式镇楼。
|
||||
|
||||

|
||||
|
||||
## 学术上
|
||||
|
||||
大会每天的日程是上午的大会特邀报告和大会论坛,下午的各个分论坛讨论。老实说,大会上午的报告和论坛我都没有特别感兴趣,因此这里将重点放在我参加的三个分论坛上。
|
||||
|
||||
### AI时代的异构融合操作系统:聚散终有时,融合亦有期
|
||||
|
||||
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴,从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路:通过三层架构,基于互联池化技术,构建AI时代的融合算力系统。系统中的三层包括:(1)池化基础底层,包括多设备的融合和池化设备虚拟化;(2)异构融合核心子系统,例如异构融合调度系统、异构融合内存和异构融合存储系统;(3)异构核心服务。总的来说,这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能,但是显然这一操作系统的实现还存在着明显的困难。
|
||||
|
||||

|
||||
|
||||
下面一个报告是较为有干货的报告,北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统,是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性,指出分布式共享内存(Distributed Shared Memory)可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意,但是现在总结的时候才发现这个报告的思路似乎有点混乱,尤其是最后一点和内存管理系统并没有什么直接的关系,而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
|
||||
|
||||

|
||||
|
||||
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此,提出了一个智能模型训练并行任务划分方法:(1)基于符号算子的计算图定义方法;(2)面向Transformer模型的流水线并行任务划分方法;(3)异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题,提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术,针对现有重计算技术(re-computing)和存储交换(swapping)技术存在的问题,提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`。
|
||||
|
||||
最后一个报告是上海交通大学杜冬冬老师的《软硬芯异构融合操作系统的多个维度》。报告伊始,杜老师就抛出一个问题:操作系统的演进应该是提供新的抽象还是兼容现有的抽象?在回答这个问题之前,杜老师首先介绍他们一个异构融合操作系统的设计思路:层OS架构的思路,通过设置两个层次——全局OS和本地OS,全局OS在本地OS的基础上提供一层跨`XPU`的能力。杜老师设计的这个系统称作`XPU-Shim`,在设计这个系统时就面对着前面的问题,是提供新的抽象还是兼容现有的抽象。`XPU-Shim`的回答是兼容现有的抽象,在底层的CXL、UB等内存语义总线的基础上实现了传统的Socket抽象,提供了低时延、高吞吐的协同能力。在操作系统的抽象问题之外,杜老师还就云上GPU应用的启动时延问题进行了讨论,深入解释了通过状态复用完全跳过初始化阶段从而加速应用冷启动过程的思路。
|
||||
|
||||
Plane讨论没有参加。
|
||||
|
||||
### 编译系统前沿技术与应用
|
||||
|
||||
第一个报告是清华大学陈文光老师的《神经网络全同态编译器》。这个报告可以说证明了“编译技术的人才活跃在各行各业”,报告中的主要内容就是编译技术如何助力机密计算中的全同态加密应用在神经网络的推理中。全同态加密算法实现了“数据可用不可见”的概念,允许程序直接在密文上进行乘法和加法运算,但是限制也是只能进行加法和乘法运算,而且过多的乘法操作会造成计算之后解密失败。该编译器成为`ANT-ACE`,首先通过设计新的五层中间表示(IR)实现了自动化全同态加密程序生成和面向性能的优化设计,在实现基本的编译工作之外,`ANT-ACE`提供了一定的调试支持,通过部分支持对于模型的部分加密支持和运行时校验为解决加密之后程序推理准确率下降的问题。
|
||||
|
||||
接下来三个报告都是关于如何将人工智能技术同编译技术解决起来。计算所冯晓兵老师的报告《人工智能编译领域的应用探索》,介绍了大模型同编译后端的两个结合方向:(1)使用大模型生成编译器的后端代码;(2)使用大模型替换编译器的后端,直接利用大模型生成汇编代码。华为毕昇编译器架构师魏伟的报告《AI for Compiler的技术探索和应用实践》则是介绍了毕昇编译器的自动调优器`Autotuner`,这个一个自动寻找最优化的编译参数组合工具。复旦大学张为华老师的报告《基于学习的编译优化技术》也是一个类似的工作,利用机器学习技术挖掘已有的编译系统中存在的相关知识来指导新的编译优化。
|
||||
|
||||
最后一个报告则是字节公司郑思泽研究员的《计算通信融合中的编译器设计》,该报告主要聚焦于如何实现在深度学习算子层的计算通信融合,这个报告主要由搞`MLIR`的同学听,我就摸鱼了。
|
||||
|
||||
### 智能终端操作系统OpenHarmony前沿研究
|
||||
|
||||
虽然名字叫作OpenHarmony,但是感觉内容实际上和鸿蒙系统没有什么太大的关系。
|
||||
|
||||
第一个报告是软件所武延军老师的《万物智联时代基础软件如何驯服碎片化》。报告的标题非常的高大上,但是实际上就讲了两件事情:(1)RISCV架构,或者说RISCV这个可扩展的思想,是解决架构碎片化的思路;(2)`openEular`系统可以作为系统软件适配的一个基线操作系统。总结一下,这其实就是一个广告,希望大家做基础软件的都来和大家一起做。
|
||||
|
||||
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》,同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力,而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景,仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言,但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛,例如`triton`。
|
||||
|
||||

|
||||
|
||||
第三个报告是在存算一体的芯片上做数据库的加速,第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。
|
||||
|
||||
最后一个又是上交杜冬冬老师的报告,《面向下一代智能终端操作系统的渲染服务研究与挑战》。这是一个我感觉还挺有趣的报告,报告中介绍的主要背景是随着终端设备上屏幕刷新率的提高和操作系统动画变得更加精致复杂,用户会发现终端系统上的显示卡顿越来越多、越明显。这是因为目前的终端显示刷新机制是同步的,显示屏会按照当前刷新的频率从操纵系统中读取下一帧的画面,但是操作系统面对这越来越短的刷新时延和越来越复杂的动画常常不能按时把下一帧的画面渲染好。于是我们的杜冬冬老师就提出了一种动态、异步的渲染机制,考虑到系统中显示动画的时间还是占少部分的,于是就可以借用这些系统不繁忙的时间预先渲染(削峰填谷)。但是这种方式需要预知到系统后面会显示的内容,这使得这套技术只能在确定性的场景和部分简单交互场景下使用。
|
||||
|
||||
> 这里插入一个杜冬冬老师的八卦,杜老师改过一次名字,之前的名字是杜东(Dong Du),在查找论文的时候使用后面的名字会更好一些(在[IPADS](https://ipads.se.sjtu.edu.cn/zh/members/)和[dblp](https://dblp.org/pid/48/331-3.html)上面都还没有改过来)。
|
||||
|
||||
## 其他
|
||||
|
||||
首先我要锐评一下浙江省金华市东阳市横店镇。横店镇感觉完全没有为一个旅游目的地做过准备,虽然说镇子上面的酒店还是挺多的,但是不管是吃的还是玩的感觉都非常少。而且镇上的交通简直就是一坨,尤其是我们从酒店到会议举办地圆明新园的一段路,完全被大货车摧残的不成样子,在上面坐车堪比过山车。
|
||||
|
||||
然后我要锐评一下会议的举办地横店圆明新园。在去之前听说这里是1:1复刻了被八国联军烧毁的圆明园,结果去了才发现圆明新园分成春苑、夏苑和秋苑,其中春苑是复刻的圆明园,但是会议的举办地是在夏苑和秋苑,感觉有点的被诈骗了。夏苑里面只复刻了圆明园长春园的部分景观,比如海岳开襟、谐奇趣和大水法等,而且还增设了英、法、美、俄、日、德、意和奥等国的特色建筑,而会议就主要在这些特色建筑中进行,属实感觉有点奇怪了。
|
||||
|
||||
最后我要锐评一下CNCC会议。名义上看这个会议有涵盖数十个方向的130余场论坛,上万名注册参会者的大型会议,但是这个会议却选在了一个看上去基本上不适合召开大型会议的横店镇圆明新园。同时会议进行的非常寒酸,中午的午餐是横店提供给剧组的盒饭,在主会场发给我们之后只能自己端着吃,下午的茶歇更是少的可怜,除了第三天有好哥们分了我一块蛋挞,三天的茶歇我愣是一点都没见到(有可能是第三天的人最少,提高了我获得茶歇的概率)。
|
||||
|
||||
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
Normal file
Binary file not shown.
625
YaeBlog/source/posts/dotnet-performance-8.md
Normal file
625
YaeBlog/source/posts/dotnet-performance-8.md
Normal file
@@ -0,0 +1,625 @@
|
||||
---
|
||||
title: 从.NET 6到.NET 8中的性能提升综述
|
||||
date: 2024-08-31T18:51:06.9233598+08:00
|
||||
tags:
|
||||
- dotnet
|
||||
- 技术笔记
|
||||
- 编译原理
|
||||
---
|
||||
|
||||
JIT编译就一定比AOT编译慢吗?
|
||||
|
||||
<!--more-->
|
||||
|
||||
长期以来,我们已经习惯了调侃Java较慢的运行速度,并将其原因归咎于Java使用了字节码加虚拟机的JIT编译方式。但是对于同样采用了这样方式的.NET,微软的开发人员却认为——"(虽然这样说很难让人信服,)但是许多人都认为托管应用程序的性能实际上超过了非托管应用程序。有许多原因使我们对此深信不疑。例如,当JIT编译器在运行时将IL代码编译成本地代码时,编译器对于执行环境的认识比非托管编译器更深刻。"(摘自Jeffrey Richter的《CLR via C#》)。
|
||||
|
||||
既然微软的开发人员对此深信不疑,同时在.NET Core之后,.NET的内部开发流程逐渐公开化,在[Github](https://github.com/dotnet/runtime)和[.NET 官方博客](https://devblogs.microsoft.com/dotnet/)上都能看到。那么我们就在本篇文章中梳理一下.NET平台从.NET 6到.NET 8三个版本中所有主要的性能提升,主要聚焦于JIT编译器、内存管理等少数几个部分。
|
||||
|
||||
## JIT
|
||||
|
||||
JIT(Just In Time,即时编译)编译器是运行时中的基础,负责将前端编译器生成的IL(Intermediate Language,就是一套微软规定的中间表示形式)转换为汇编语言,在AOT(Ahead Of Time,提前编译)编译时也是调用的该编译器。这里可以解释一下.NET代码执行的三种模型:
|
||||
|
||||
- JIT编译执行:最为“传统“的执行模型,所有的IL代码都需要在执行前通过JIT编译为本机代码再执行。
|
||||
- 即时运行(ReadyToRun, R2R):在程序编译阶段先调用JIT编译器将IL代码编译为本机代码,在程序运行时首先运行编译好的本机代码以提高应用启动的速度,在运行过程中再次调用JIT编译对热点代码进行优化编译。为了提高启动速度,.NET中的所有核心库都以R2R的形式提供,程序员可以自行决定编写的代码是否使用R2R的方式运行。
|
||||
- 提前编译:在程序编译阶段直接调用JIT编译器将IL代码编译为本机代码,程序运行时就执行这一套代码。
|
||||
|
||||
### 分层编译、栈上替换和PGO
|
||||
|
||||
在.NET从6到8的版本演进过程中,最为重磅的性能更新莫过于在.NET 6便引入的动态PGO(dynamic Profile-Guided Optimization)在.NET 8中终于默认启用了。为了介绍动态PGO,我们必须首先理解JIT对于IL代码的分层编译机制。
|
||||
|
||||
#### 分层编译
|
||||
|
||||
在JIT编译器最初的设计模型中,每个方法只会被编译一次:每个方法只会在调用被编译为汇编代码,该代码被缓存起来以备下次调用。但是这种设计却导致许多矛盾:一个根本性的矛盾就是JIT编译花费在编译优化上的时间同从优化中能得到的效果之间的矛盾。在编译过程中对代码进行优化几乎是编译器工作过程中最耗时的部分,尤其是对于一个JIT编译器来说,编译的时间几乎直接决定了应用启动的时间,如果对一个方法进行优化需要耗费一秒钟的时间,但是仅能使该方法的运行时间从10毫秒下降到1毫秒,在该方法在运行过程只会调用一次的情况下,编译器引入该优化只会让程序的运行时间增加。因此,编译器必须要在程序运行时的效率和启动时间之间做取舍。尤其是考虑到程序的**空间局部性**原理:程序中的大多数函数只会在运行时被调用少数几次,对于这些函数在启动时耗费大量的优化时间是纯纯的浪费。
|
||||
|
||||
**分层编译**的引入从根本上解决了这个问题:该编译策略允许一个方法在运行时被编译多次。
|
||||
|
||||
在第一次调用时,方法会被编译到第0层(Tier 0)。在这个编译层级上只会应用少数的编译优化策略,这些编译优化策略被称为最小优化策略(Minimal Optimization,Min Opts)。需要指出的这些策略实际上也不少,包含了那些可以使JIT编译器更快运行的优化策略,例如可以生成更少量的本机代码。在优化的同时JIT编译器还会注入一些短短的代码片段(stub),这些代码片段使得运行时可以统计每个方法的调用次数。
|
||||
|
||||
运行时可以监控这些方法的调用次数,当某个方法的调用次数超过某个预先设定的阈值时,这个方法将被加入重新编译的队列。这次编译将会把方法编译到第1层(Tier 1),JIT编译器将会在编译的过程中应用所有可能的优化策略。在整个程序的运行过程中,只有少数被多次调用的方法会编译到第1层。同时编译器也可以通过收集方法在第0层的运行过程中的信息来进行第1层编译过程中的优化。例如对于`static readonly`类型的变量,当方法在第0层执行之后,这些类型的变量已经完成初始化且无法再发生更改,此时编译器就可以将这些变量当作是`const`类型的常量,将所有应用于常量的优化策略扩展到该类型的变量上进行应用。
|
||||
|
||||
在大多数情况下,使用分层编译可以使用程序同时获得良好的启动速度和运行效率,除了某些特定的情况。这些特定情况的一个典型例子就是运行时间非常长的方法:在上述的优化策略中只重视了调用次数非常多的方法,但是运行时间非常长的方法也对于效率有着非常明显的影响。而在分层编译的情况下,这些长运行时间但是少调用次数的函数将会只被编译到第0层,这会造成明显的性能下降。因此在.NET 7之前,所有含有回溯分支的方法都会直接编译到第1层。
|
||||
|
||||
.NET 7引入的栈上替换改进了这一点。
|
||||
|
||||
>这里可能有人会争论:对于少数运行时间长的方法在启动时多施加一些优化策略真的会导致明显的启动时间增加吗,有必要引入更复杂的策略针对这点蚊子腿进行优化吗?
|
||||
>
|
||||
>的确,对这点启动时间进行优化很可能是不明显的,但是别忘了编译器可以在第0层的运行过程中收集信息进行第1层的优化,这实际上也是动态PGO机制引入的基础之一。
|
||||
|
||||
#### 栈上替换
|
||||
|
||||
分层编译很好,除了在面对运行时间长的方法时。例如对于下面这个包含一万次循环的方法:
|
||||
|
||||
```csharp
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
while (true)
|
||||
{
|
||||
sw.Restart();
|
||||
for (int trial = 0; trial < 10_000; trial++)
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < char.MaxValue; i++)
|
||||
if (IsAsciiDigit((char)i))
|
||||
count++;
|
||||
}
|
||||
sw.Stop();
|
||||
Console.WriteLine(sw.Elapsed);
|
||||
}
|
||||
|
||||
static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当在.NET 6平台运行时,我们可以比较在启用对方法的分层编译和不启用的情况下的性能对比。
|
||||
|
||||
| 编号 | 启用分层编译 | 不启用 |
|
||||
| ---- | ---------------- | ---------------- |
|
||||
| 1 | 00:00:01.2841397 | 00:00:00.5734352 |
|
||||
| 2 | 00:00:01.2693485 | 00:00:00.5526667 |
|
||||
| 3 | 00:00:01.2755646 | 00:00:00.5675267 |
|
||||
| 4 | 00:00:01.2656678 | 00:00:00.5588724 |
|
||||
| 5 | 00:00:01.2679925 | 00:00:00.5616028 |
|
||||
|
||||
栈上替换(On Stack Replacement)就是为了解决这个问题而引入的:没人规定说一个方法执行的本机代码只能在执行的间隙被替换,在执行的过程中也可以替换掉方法执行的本机代码,也就是当方法还在运行栈上时执行替换。在第0层编译时,编译器不仅可以为函数的调用生成统计调用次数的片段代码,也可以为循环的执行生成运行次数的片段代码。当运行时监控到某一个循环的执行次数超过设定的阈值时,编译器就可以将该方法编译到第1层,运行时将会把方法此时调用的所有寄存器和本地变量复制到一个新的方法调用中,而新的调用使用的本机代码已经是优化之后的本机代码了。
|
||||
|
||||
在分层编译和栈上替换的协作下,程序的启动实现和运行性能之前就可以达到一个较好的平衡了。当然,分层编译和栈上替换的能力并不仅限于优化应用的启动时间,在动态PGO中这两者将会发挥更大的作用。
|
||||
|
||||
#### 动态PGO
|
||||
|
||||
采样制导的优化(Profile-Guided Optimization)并不是一个新鲜的概念,在数十年前就出现,并在多种编程语言和运行时中得到的应用。PGO的一个典型工作流程一般如下:
|
||||
|
||||
1. 在插入一些特定指令的情况下构建应用程序;
|
||||
2. 将应用程序放在典型的应用场景下进行运行,并通过这些特定指令收集运行的信息;
|
||||
3. 在这些信息的指导下重新构建应用程序,得到针对运行场景的特定优化。
|
||||
|
||||
这种工作流程被称作是静态的PGO,这些工作流往往额外的应用知识、特定的工具和构建-上线流程的反复执行。
|
||||
|
||||
回到.NET的执行过程中,既然分层编译已经可以将程序生成为第0层和第1层两个版本,为什么不在第0层程序的运行过程中收集一些有用的信息输入到第1层的编译过程中呢,这样编译器还可以生成更加优化的第1层本机代码。这个过程中传统静态PGO流程中的构建-运行-再构建流程完全一致,不过现在优化的层级可以聚焦在方法上,而不是针对整个程序进行优化,以及最为重要的是,这一切都在程序运行的过程中由JIT编译器自动的进行,不需要任何额外的开发工作或者是针对性的构建流程。
|
||||
|
||||
在.NET 6到.NET 8整整三个大版本对于动态PGO的迭代过程中引入了大量的优化,这里仅能介绍一小部分。
|
||||
|
||||
首先是为了更好发挥动态PGO的性能,JIT编译器中为分层编译引入了更多的编译层数。需要引入更多编译层数的原因主要有两点。第一,插入各种采样的指令和代码是需要代价的,考虑到第0层编译的主要目标是为了降低编译的时间,提高应用的启动速度,在第0层编译过程中就不能插入太多的采样指令。因此编译器首先增加了一个新的编译层——采样第0层来解决这个问题。大部分的方法将在第一次运行时编译到缺少优化、缺少采样指令的第0层,在运行时发现该方法被调用了多次之后,JIT编译器将这个方法重新编译到采样第0层,再经过一系列的调用之后,JIT编译器将利用采样得到的信息对该方法重新进行编译并优化。第二,在原始编译器模型中使用即时运行(R2R)方法编译的代码不能参加到动态PGO中,尤其是考虑到几乎所有应用程序都会调用的核心库代码是采用R2R的方式进行运行的,如果这部分的代码不能参加动态PGO将不能够完全发挥动态PGO的效果,虽然核心库在提前编译的过程中会使用静态PGO进行一部分的优化。因此JIT编译器为R2R编译好的代码增加了一个新的编译器,在运行时发现这部分代码被调用多次之后将会被JIT编译器编译到含有优化和采样代码的采样第1层,随着调用次数的增加这部分的代码将可以利用采样得到的信息进行优化。下面这张图展现了不同编译方法在运行过程中可能达到的编译层级。
|
||||
|
||||

|
||||
|
||||
JIT编译器也在第0层编译的过程中引入了更多的优化。虽然第0层编译的目的是缩短编译的时间,但是许多的优化可以通过减少需要生成的代码数量来达到这个目的。常量折叠(Constant Folding)就是一个很好的例子。虽然这会让JIT编译器在第0层编译时花费更多的时间同运行时中的虚拟机交互来解析各种变量的类型,但是这可以大量的减少JIT编译器需要生成的代码量,尤其是对于下面这种涉及到类型判断的例子。
|
||||
|
||||
```csharp
|
||||
MaybePrint(42.0);
|
||||
|
||||
static void MaybePrint<T>(T value)
|
||||
{
|
||||
if (value is int)
|
||||
{
|
||||
Console.WriteLine(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
现在在第0层编译的过程中,JIT编译器可以发现`MaybePrint`方法在运行过程中不会运行任何实际的代码路径,因此可以直接优化掉这段代码。
|
||||
|
||||
```assembly
|
||||
; Assembly listing for method Program:<<Main>$>g__MaybePrint|0_0[double](double) (Tier0)
|
||||
; Emitting BLENDED_CODE for X64 with AVX - Windows
|
||||
; Tier0 code
|
||||
; rbp based frame
|
||||
; partially interruptible
|
||||
|
||||
G_M000_IG01: ;; offset=0x0000
|
||||
push rbp
|
||||
mov rbp, rsp
|
||||
vmovsd qword ptr [rbp+0x10], xmm0
|
||||
|
||||
G_M000_IG02: ;; offset=0x0009
|
||||
|
||||
G_M000_IG03: ;; offset=0x0009
|
||||
pop rbp
|
||||
ret
|
||||
|
||||
; Total bytes of code 11
|
||||
```
|
||||
|
||||
插入的采样代码片段也会造成一些性能上的问题。为了优化JIT编译器往往需要统计各种方法和分支的调用和运行次数,但是问题是这些统计调用次数的代码应该如何编写?尤其是考虑到代码片段是一个静态的“数据”,会在各种不同的运行线程之间共享,如何设计一个线程安全同时高效的统计方法?
|
||||
|
||||
最初的统计方式是设计一个朴素、没有线程同步的方法,例如`_branches[branchId]++`。虽然这种方法没有在运行时引入大量的同步开销,但是这也意味着在某个方法被多个线程同时调用时会损失掉大量的统计数据,这会造成一个本应该提前进入动态PGO的方法得到优化的时间严重滞后。这方面一个容易想到的方式是使用同步的方法进行统计,例如给数据加锁或者是使用原子指令(`Interlocked.Add`)。但是这种方式会严重的导致性能下降。为了解决这个问题,开发者们设计了一种非常巧妙的解决方法,这种方法的C#实现如下所示。
|
||||
|
||||
```csharp
|
||||
static void Count(ref uint sharedCounter)
|
||||
{
|
||||
uint currentCount = sharedCounter, delta = 1;
|
||||
if (currentCount > 0)
|
||||
{
|
||||
int logCount = 31 - (int)uint.LeadingZeroCount(currentCount);
|
||||
if (logCount >= 13)
|
||||
{
|
||||
delta = 1u << (logCount - 12);
|
||||
uint random = (uint)Random.Shared.NextInt64(0, uint.MaxValue + 1L);
|
||||
if ((random & (delta - 1)) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Add(ref sharedCounter, delta);
|
||||
}
|
||||
```
|
||||
|
||||
在计数器的值没有超过8192时,计数逻辑直接使用原子指令进行统计。当计数器的数值超过8192之后,计数逻辑将采用一个随机的增加策略。首先按照50%的概率给计数器增加2,然后按照25%的概率增加4,然后按照12.5%的概率增加8,依次类推。随着计数器值的增加,但是需要调用原子指令的频率也就越低。
|
||||
|
||||
为了验证该计数逻辑的有效性,可以使用下面的代码进行验证。
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics;
|
||||
|
||||
uint counter = 0;
|
||||
const int ItersPerThread = 1_000_000_00;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Run("Interlock", _ =>
|
||||
{
|
||||
for (int i = 0; i < ItersPerThread; i++) Interlocked.Increment(ref counter);
|
||||
});
|
||||
Run("Racy ", _ =>
|
||||
{
|
||||
for (int i = 0; i < ItersPerThread; i++) counter++;
|
||||
});
|
||||
Run("Scalable ", _ =>
|
||||
{
|
||||
for (int i = 0; i < ItersPerThread; i++) Count(ref counter);
|
||||
});
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
void Run(string name, Action<int> body)
|
||||
{
|
||||
counter = 0;
|
||||
long start = Stopwatch.GetTimestamp();
|
||||
Parallel.For(0, Environment.ProcessorCount, body);
|
||||
long end = Stopwatch.GetTimestamp();
|
||||
Console.WriteLine(
|
||||
$"{name} => Expected: {Environment.ProcessorCount * ItersPerThread:N0}, Actual: {counter,13:N0}, Elapsed: {Stopwatch.GetElapsedTime(start, end).TotalMilliseconds}ms");
|
||||
}
|
||||
```
|
||||
|
||||
运行得到的数据如下所示:
|
||||
|
||||
| 类型 | 期望数值 | 实际数值 | 运行时间 |
|
||||
| -------- | ------------- | ------------- | ------------ |
|
||||
| 原子指令 | 2,000,000,000 | 2,000,000,000 | 22241.9848ms |
|
||||
| 朴素 | 2,000,000,000 | 220,525,235 | 277.3435ms |
|
||||
| 随机 | 2,000,000,000 | 2,024,587,268 | 527.5323ms |
|
||||
|
||||
从数据上就可以发现,新方法可以在和朴素方法接近的运行时间下获得和使用原子指令接近的实际数值,而且运行时间会随着数值的增加进一步的减少,逐渐逼近朴素方法的运行时间。
|
||||
|
||||
如何准确而低成本的技术并不是采样过程中唯一的问题。另一个问题是如何统计在接口或者是虚拟方法调用时哪个类型是最可能被调用到的类型,如果JIT能够得到这种信息,就可以为该类型生成一条更加快速的调用路径。正如上一个算法所揭示的,准确统计每一个类型被调用的次数是非常昂贵的,因此在这里开发者引入了一种被称作蓄水池采样(Reservoir Sampling)的方法进行统计。例如对于一个含有60%的`'a'`、30%的`'b'`和10%的`‘c'`的字符序列,如何快速而准确的统计其中哪个字符出现的频率最高?利用蓄水池采样算法,可以写出如下的统计代码:
|
||||
|
||||
> 蓄水池采样算法设计的目的是为了解决这样一个问题:给出一个数据流,这个数据流的长度很大或者是未知,并且对于该数据流中的数据只能访问一次。请设计一个随机选择算法,使得数据里中所有数据被选中的概率相等。
|
||||
|
||||
```csharp
|
||||
// Create random input for testing, with 60% a, 30% b, 10% c
|
||||
char[] chars = new char[1_000_000];
|
||||
Array.Fill(chars, 'a', 0, 600_000);
|
||||
Array.Fill(chars, 'b', 600_000, 300_000);
|
||||
Array.Fill(chars, 'c', 900_000, 100_000);
|
||||
Random.Shared.Shuffle(chars);
|
||||
|
||||
for (int trial = 0; trial < 5; trial++)
|
||||
{
|
||||
// Reservoir sampling
|
||||
char[] reservoir = new char[32]; // same reservoir size as the JIT
|
||||
int next = 0;
|
||||
for (int i = 0; i < reservoir.Length && next < chars.Length; i++, next++)
|
||||
{
|
||||
reservoir[i] = chars[i];
|
||||
}
|
||||
for (; next < chars.Length; next++)
|
||||
{
|
||||
int r = Random.Shared.Next(next + 1);
|
||||
if (r < reservoir.Length)
|
||||
{
|
||||
reservoir[r] = chars[next];
|
||||
}
|
||||
}
|
||||
|
||||
// Print resulting percentages
|
||||
Console.WriteLine($"a: {reservoir.Count(c => c == 'a') * 100.0 / reservoir.Length}");
|
||||
Console.WriteLine($"b: {reservoir.Count(c => c == 'b') * 100.0 / reservoir.Length}");
|
||||
Console.WriteLine($"c: {reservoir.Count(c => c == 'c') * 100.0 / reservoir.Length}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
```
|
||||
|
||||
程序的输出是5次次采样统计的结果:
|
||||
|
||||

|
||||
|
||||
需要指出的是,虽然在上面的代码中使用和运行时代码中一样的“蓄水池”大小,但是在运行时并没有提前获得所有需要统计的数据,调用的统计数据是由多个不同的运行线程同时写入蓄水池中的。从结果中可以看出,虽然数值上并不准确,但是该算法准确的统计出了各个字符的出现趋势。
|
||||
|
||||
在上述两个例子中,算法中都引入了随机数的概念进行统计,这就导致每次运行的结果都在一定程度上有着不同,同时这也会导致在每次程序运行的过程中,动态PGO所做的优化都会有轻微的不同。有的开发者可能会担心这些随机的引入是否会造成程序运行行为的不可确定性从而导致程序的调试变得困难,但是实际上在引入这些随机数之后这些代码路径已经就有一定的不确定性(例如那个朴素的调用次数统计算法),同时开发过程中已经有大量的数据证实这些代码的行为是总体上稳定且可重现的。
|
||||
|
||||
本篇文章中介绍动态PGO的部分就大致到这里,但是文章后续的部分中仍然可以在各个地方中看到动态PGO的身影,这也可以侧面看出动态PGO对于整个优化的巨大作用。
|
||||
|
||||
### 函数内联
|
||||
|
||||
函数内联是JIT编译器能完成的重要优化之一,其的运行逻辑是取消对于某个方法的直接调用,而是将该方法的执行代码直接插入到当前的控制流中。函数内联最显而易见的优化是减小了调用函数过程中压栈和弹栈带来的开销,但除了对于某些在热点路径上的小型方法,这点减少的开销实际上并不是函数内联实际上带来的主要优化。
|
||||
|
||||
函数内联带来的主要优化是其将被调用者的逻辑暴露给了调用者,或者反过来。例如,当调用者将一个常数作为参数传递给被调用的方法时,如果被调用的方法没有进行内联,对该方法进行编译时编译器就无从得知一个常数被传递了过来,但是如果该方法被内联了,进行编译的编译器就可以应用一切对于常数可以应用的优化,包括删除死代码、分支预测、常量折叠等等。
|
||||
|
||||
按照这个逻辑分析,那么在编译的时候应该应内联尽内联,但是内联有可能会增加编译之后的指令条数。而指令条数的增加可能会造成指令缓存效率的下降——当需要读取内存的次数越多时,缓存的效率就会越低。例如考虑一个方法,这个方法在整个程序中被内联了100次,而这一百次都内联编译为一份不同的本机代码序列,这一百次调用就完全不能高效的利用指令缓存,而如果对于这个方法没有进行内联,这一百次调用都可以指向同一个内存地址,这就让指令缓存感到非常舒适。因此在JIT编译器编译一个方法时,如果编译器聪明到可以判断出内联之后编译得到的指令序列将少于直接调用得到的指令序列那么编译器就可以执行内联操作,反之编译器就需要衡量内联方法得到的吞吐量提高和增长的指令序列造成的运行效率了。
|
||||
|
||||
因此就需要JIT编译器合理的判断哪些方法在编译过程中需要进行内联,哪些方法在编译过程中进行内联。这方面编译器做出的主要更新是让内联更好的能够判断需要被内联方法的内容,尤其是在方法没有被分层编译或者是方法直接跳过了第0层编译的情况下。再考虑到在运行时库中引入的大量可以低成本调用的硬件加速指令方法,这些方法也可以有效的进行内联。
|
||||
|
||||
### 去虚拟化
|
||||
|
||||
在调用一个接口类型的变量上的方法时,运行时需要做的一个重要工作就是判断实际上应该调用哪个类型的对象上的方法,这在对于接口、虚拟成员方法、泛型方法和委托类型的调用上都是适用的。
|
||||
|
||||
因此JIT编译器引入一种被称为保险去虚拟化(Guarded Devirtualization,GDV)的机制进行优化,这种机制也是在动态PGO的帮助下引入的。具体地说,在运行时将会统计具体被调用的类型或者方法的频率,然后在进行优化编译时为最常出现的类型提供一条快速调用的路径。对于下面这种例子来说:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
internal interface IValueProducer
|
||||
{
|
||||
int GetValue();
|
||||
}
|
||||
|
||||
class Producer : IValueProducer
|
||||
{
|
||||
public int GetValue() => 42;
|
||||
}
|
||||
|
||||
private IValueProducer _valueProducer;
|
||||
private int _factor = 2;
|
||||
|
||||
public void Setup() => _valueProducer = new Producer42();
|
||||
|
||||
public int GetValue() => _valueProducer.GetValue() * _factor;
|
||||
}
|
||||
```
|
||||
|
||||
对于其中的`GetValue`方法,在没有动态PGO和GDV的参与下,这个方法中将会被编译为一种普通的接口方法调用。但是在启用了动态PGO的环境下,编译器将会注意到对于`IValueProducer`最常见的实现是`Producer`,这样JIT编译器就可以为`Producer`生成一条快速路径,对应与下面的C#实现:
|
||||
|
||||
```csharp
|
||||
int result = _valueProducer.GetType() == typeof(Producer) ?
|
||||
Unsafe.As<Producer>(_valueProducer).GetValue() :
|
||||
_valueProducer.GetValue();
|
||||
return result * _factor;
|
||||
```
|
||||
|
||||
.NET中实现的GDV优化可以支持生成多个GDV,也就是在进行接口调用同时为多个类型生成快速路径。但是这个默认的运行条件下是关闭,需要用户通过一个特定的环境变量进行设置`DOTNET_JitGuardedDevirutalizationMaxTypeChecks`。这一优化在使用AOT编译器直接编译到本机代码时还有一个非常有趣的效果,考虑到在进行AOT编译时会对程序集进行裁剪,也就是删除掉最终的应用程序中没有用到的类型,这就让编译器可以在编译时知道实现了某一特定接口的类型总共有哪些,并且在这些类型的数量较少时直接为这些类型都生成调用时的快速路径而完全避免在运行时进行判断。
|
||||
|
||||
在上文中已经提到GDV不仅可以在调用接口上定义方法时使用,也可以在调用委托的时候使用。这使用GDV在和循环克隆(Loop Cloning)等优化技术配合时能够发挥出更大的功能,例如对于下面这个例子:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
private readonly Func<int, int> _func = i => i + 1;
|
||||
|
||||
public int Sum() => Sum(_func);
|
||||
|
||||
private static int Sum(Func<int, int> func)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
sum += func(i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在上面的示例代码的循环中调用了一个委托`func`,在动态PGO和GDV的参与下,编译器可以知道这个委托最常见的实现(其实是唯一的)是一个固定的Lambda函数(暂且称之为Known Lambda),因此编译器可以将`Sum`函数的编译器为如下的等价C#代码:
|
||||
|
||||
```csharp
|
||||
private static int Sum(Func<int, int> func)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
sum += func.Method == KnownLambda ? i + 1 : func(i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
```
|
||||
|
||||
> 这里需要注意的是,这些代码都是**等价**C#代码,实际上编译器并不是先编译为一种C#形式的代码,而是直接生成为汇编代码。
|
||||
|
||||
显然,在循环内部反复的进行一个相同的判断并不是一个理想的状态。因此在变量提升(hoisting)优化技术的帮助下,编译器可以将循环内部一个相同的判断提升到循环外部执行,这将产生如下的等价代码。
|
||||
|
||||
```csharp
|
||||
private static int Sum(Func<int, int> func)
|
||||
{
|
||||
int sum = 0;
|
||||
bool isAdd = func.Method == KnownLambda;
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
sum += isAdd ? i + 1 : func(i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
```
|
||||
|
||||
这还不是优化的极限,注意到在每个循环中还有个重复的三元表达式,这个的结果在各次循环之前也应该是稳定的,因此在循环克隆优化的指导下,编译器将生成如下的等价代码。
|
||||
|
||||
```csharp
|
||||
private static int Sum(Func<int, int> func)
|
||||
{
|
||||
int sum = 0;
|
||||
if (func.Method == KnownLambda)
|
||||
{
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
sum += i + 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
sum += func(i);
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
```
|
||||
|
||||
这可以说,在动态PGO和GDV优化策略的加持下,一些“传统的”优化策略又被编译器榨出了新的潜能,从实际的跑分上也可以验证这惊人的优化。
|
||||
|
||||
| 方法 | 条件 | 平均运行时间 |
|
||||
| ---- | ---------------- | ------------ |
|
||||
| Sum | 开启动态PGO和GDV | 2.320us |
|
||||
| Sum | 关闭动态PGO和GDV | 16.546us |
|
||||
|
||||
### 分支
|
||||
|
||||
分支代码几乎是所有的代码片段中都会涉及到的模式,包括各种循环、判断和三元表达式种种。但是考虑到现代处理器都是多发射的超标量流水线处理器,而各种分支代码往往会打断这些高速运行的流水线,尽管处理器的设计者会通过分支预测器等技术进行猜测,而且往往还猜得很准,但是如果预测出错就需要清空流水线重新运行。因此如何减少代码中的分支是编译器优化的重要课题。
|
||||
|
||||
删除重复的分支判断是一个常见的分支优化,尤其常见与用户代码和库代码进行交互的过程中。例如对于下面这个例子:
|
||||
|
||||
```csharp
|
||||
public ReadOnlySpan<char> SliceOrDefault(ReadOnlySpan<char> span, int i)
|
||||
{
|
||||
if ((uint)i < (uint)span.Length)
|
||||
{
|
||||
return span.Slice(i);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
```
|
||||
|
||||
这段代码中首先判断索引起始的位置是否小于切片的长度再调用对应的切片方法,但是在`ReadOnlySpan<char>.Slice`的源代码中还有一个几乎一致的判断:
|
||||
|
||||
```csharp
|
||||
public ReadOnlySpan<T> Slice(int start)
|
||||
{
|
||||
if ((uint)start > (uint)_length)
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException();
|
||||
|
||||
return new ReadOnlySpan<T>(ref Unsafe.Add(ref _reference, (nint)(uint)start /* force zero-extension */), _length - start);
|
||||
}
|
||||
```
|
||||
|
||||
这就让生成的本机代码中出现两个冗余的判断。编译器可以针对这种冗余的判断进行检查并删除这些重复判断。这种类似的分支删除后面还会在“消除边界检查”章节中提到。
|
||||
|
||||
灵活的应用各种位运算也是一种常见的分支优化策略。例如对于下面这种对于一个有符号整数的判断`i >= o && j >= 0`可以直接被优化为`(i | j) >= 0`,通过引入一个位运算就减少了一个分支判断。除了灵活的应用位运算之外,使用指令集提供的各种条件移动指令也是一种有效的分支优化策略,比如`x86/64`指令集中提供的`cmov`指令和`arm`指令集中提供的`csel`指令,这些指令都将一个条件判断封装到一条指令中。
|
||||
|
||||
C#编译器也可以在分支消除中贡献一份属于自己的力量。考虑.NET中非常常见的一个类型`System.Boolean`,在使用中这个类型是一个两值类型,有且仅有两个取值`true`和`false`。但是实际上在运行时中会使用一个字节大小的空间来存储一个类型,这意味实际上该类型有着256个取值,并且将0视为`false`,将`[1,255]`视为`true`。当然开发者可以使用`unsafe`代码绕过一些编译器的限制,但是“普通的”的开发者和核心库都只会给这个类型的赋予0或者1两个值。因此,在设计一类特殊的算法——无分支判断算法时,开发者可能会写出如下的代码:
|
||||
|
||||
```csharp
|
||||
static int ConditionalSelect(bool condition, int whenTrue, int whenFalse) =>
|
||||
(whenTrue * condition) +
|
||||
(whenFalse * !condition);
|
||||
```
|
||||
|
||||
但是上述的代码并不能被C#接受,因为C#编译器限制不能让布尔类型参加运算,因此这类算法的开发者不得不因此引入两个多余的分支判断:
|
||||
|
||||
```csharp
|
||||
static int ConditionalSelect(bool condition, int whenTrue, int whenFalse) =>
|
||||
(whenTrue * (condition ? 1 : 0)) +
|
||||
(whenFalse * (condition ? 0 : 1));
|
||||
```
|
||||
|
||||
但是现在C#编译器可以消除掉这两个多余的分支判断,因为在.NET世界中编译器可以确保布尔变量的取值只能有1或者0两种情况。
|
||||
|
||||
#### 消除边界检查
|
||||
|
||||
.NET提供的一种特性就是运行时安全,这其中重要的一点就是对于数组、字符串和切片在运行时进行边界检查。但是这些边界检查就会在实际生成的代码中生成大量的分支判断,这会导致程序运行的效率严重下降。因此如何让编译器在能够保证访问安全的情况下消除掉部分不必要的边界检查是编译器优化中的一个重要课题。
|
||||
|
||||
例如在一个常用数据结构——哈希表中,通常的实现是计算键的哈希值,并利用该哈希值作为下标在数组中获得存储的对象。考虑到哈希值是一个`int`类型的变量,但是哈希表中很少需要存储高达21亿对象,因此往往需要对哈希值取模之后再作为数组的下标,此时取模的值常常就是数组的长度。也就是说,在这种情况下对于数组的访问是不可能出现越界的情况下。因此编译器可以为类似与如下的代码取消访问数组时的边界检查:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
private readonly int[] _array = new int[7];
|
||||
|
||||
public int GetBucket() => GetBucket(_array, 42);
|
||||
|
||||
private static int GetBucket(int[] buckets, int hashcode) =>
|
||||
buckets[(uint)hashcode % buckets.Length];
|
||||
}
|
||||
```
|
||||
|
||||
同样的,对于下面这些代码,编译器也可以取消访问数组时的边界检查:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
private readonly string _s = "\"Hello, World!\"";
|
||||
|
||||
public bool IsQuoted() => IsQuoted(_s);
|
||||
|
||||
private static bool IsQuoted(string s) =>
|
||||
s.Length >= 2 && s[0] == '"' && s[^1] == '"';
|
||||
}
|
||||
```
|
||||
|
||||
### 常量折叠
|
||||
|
||||
常量折叠(Constant Folding)同样是一个编译器在生成代码时可以进行的重要优化,这让编译器在计算在编译器时就可以确定的值,而不是让他们留到运行时进行。最朴素的常量折叠——例如计算一个数学表达式的值——在这里不在赘述。在上面介绍函数内联时也涉及到了常量折叠的内容,分层编译的引入也会使得常量折叠的应用范围变广,这些都不在这里重复。
|
||||
|
||||
进行常量折叠优化时一个重要的问题是“教会”编译器哪些变量是常量。这方面编译器得到的提升有:
|
||||
|
||||
- 可以将一个字面值字符串的长度视为一个常数;
|
||||
- 在进行空安全的检查时字面值字符串是必定不为空的;
|
||||
- 编译器在编译时除了可以进行一些简单的数学运算,现在整个`System.Math`命名空间中提供的算法都可以在编译时进行运算;
|
||||
- `static readonly`类型的字符串和数组长度被视为一个常数;
|
||||
- `obj.GetType()`现在在JIT编译器明确了解类型的情况下可以被替换为一个常量;
|
||||
- `DateTime`等时间类型初始化时可以在编译期计算内存存储的时间。例如对于`new DateTime(2023, 9, 1)`将会直接被编译到`new DateTime(0x8DBAA7E629B4000)`。
|
||||
|
||||
上述这些并不能完全覆盖在.NET 6到.NET 8三个大版本之中引入的所有JIT编译器优化,但是从中也可以一窥编译器优化的精巧之处。首先,编译器的优化并不是一个个独立优化策略的组合,而且各种优化策略的有机组合。方法的内联就是一个典型例子,通过将被调用方法的内容暴露给调用者(或者反过来)让其他的各种优化策略发挥更大的作用。其次,JIT编译器在编译优化方面可以发挥更伟大的作用。通过在程序运行时对于运行环境和程序本身有着更加深刻的理解,JIT编译器可以在运行时发挥出更高的性能。
|
||||
|
||||
## 内存管理
|
||||
|
||||
.NET中的垃圾回收器(GC)负责管理应用程序的内存分配和释放。每当有对象新建时,运行时都会将从托管堆为对象分配内存,主要托管堆中还有地址空间,运行时就会从托管堆为对象分配内存。不过内存并不是无限的,垃圾回收器就负责执行垃圾回收来释放一些内存。垃圾回收器的优化引擎会根据所执行的分配来确定执行收回的最佳时机。
|
||||
|
||||
.NET中内存管理中的一个显著变更为将内存的抽象从段(Segment)修改为区域(Region)。段和区域之前最明显的区别是大小,段是较大的内存——在64位的机器上一个段的大小万网是1GB、2GB或者是4GB,而区域是非常小的单元,在默认情况下只有4MB的大小。从宏观上来说,之前的GC是为每个代的堆维持一个GB级别的内存范围,而现在GC则是维持了许多个较小的内存区域,这些内存区域可以被分配给各个代的堆(或者其他可能涉及的堆)使用。
|
||||
|
||||
垃圾回收器中还有两个引人注意的特性增加。第一个是动态的代提升和下降(Dynamic Promotion and Demotion,`DPAD`),第二个是动态适应应用程序大小(Dynamic Adaptive To Application Size,`DATAS`)。`DPAD`特性允许GC在工作的过程中动态的设置一个区域的代数,例如直接将一个可能存活时间非常长的对象配置为第2代,而这在之前的GC模型中需要通过两次垃圾回收才能实现。而第二个特性`DATAS`旨在适应应用程序的内存要求,即应用程序堆的大小和长期数据大小大致成正比,即使在不同规格的计算机上执行相同的工作时,运行时中堆的大小也是类似的。相比如下,传统的服务器模式下的GC旨在提高程序的吞吐量,允许内存的分配量基于吞吐量而不是应用程序的大小。`DATAS`对于各种突发类型的工作负载是非常有利的,同时通过允许堆大小按照工作负载的要求进行调整,这将让一些内存首先的环境直接受益。
|
||||
|
||||
### 无垃圾回收的堆
|
||||
|
||||
在程序中大量会涉及到使用常量字符串的情形,例如下面这个例子:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
public string GetPrefix() => "https://";
|
||||
}
|
||||
```
|
||||
|
||||
在.NET 7平台上这个方法会被JIT编译器编译之后得到下面这段本机代码:
|
||||
|
||||
```assembly
|
||||
; Tests.GetPrefix()
|
||||
mov rax,126A7C01498
|
||||
mov rax,[rax]
|
||||
ret
|
||||
; Total bytes of code 14
|
||||
```
|
||||
|
||||
在这段代码中使用了两个`mov`指令,其中第一个指令加载存储这个字符串对象地址的地址,第二个读取该地址。从这段本机代码可以看见,尽管已经是在处理一个常量的字符串,但是编译器和运行时仍然需要为这个字符串在堆上分配一个`string`对象:因为一个在堆上分配的对象在GC的控制下会在内存中发生移动,编译器就不能为这个对象使用一个固定的内存地址,需用从一个指定的地址读取该对象所在的地址。如果能让这个常量字符串分配在不会移动的内存区域中就能从编译器和GC两个方面上提高程序运行的效率。
|
||||
|
||||
为了优化这种生成周期和程序一致对象的内存管理,.NET 8中引入了一个新的堆——没有内存管理的堆。JIT编译器将会保证这些常量类型的对象将会被分配在这个堆中,这种没有GC管理的堆也意味着JIT编译器可以为这些对象使用一个固定的内存地址,在使用时避免掉了一次内存读取。
|
||||
|
||||

|
||||
|
||||
将上述提高的示例代码使用.NET 8版本进行编译得到的代码如下,从中也可以看出JIT编译器生成的代码只有一条`mov`指令,避免了一次内存访问。
|
||||
|
||||
```assembly
|
||||
; Tests.GetPrefix()
|
||||
mov rax,227814EAEA8
|
||||
ret
|
||||
; Total bytes of code 11
|
||||
```
|
||||
|
||||
这个没有内存管理的堆引入还可以让其他的类型受益。例如对于`typeof(T)`返回的类型对象,容易想到一个程序集中所有类型对象的生命周期应该是和程序一致的,因此也可以在这个堆上分配所有这些类型对象。`Array.Empty<T>`也可以利用类似的思路分配在这个堆上。
|
||||
|
||||
### 值类型
|
||||
|
||||
因为可以避免在堆上分配内存,值类型已经在.NET的高性能代码中得到了广泛的应用,虽然频繁的内存拷贝可能带来额外的性能开销。因此编译器对于值类型的各种优化就显得至关重要。
|
||||
|
||||
这部分优化中一个引人注目的点是值类型的“推广”(promotion)这里的推广意味着将一个结果划分为组成它的各种字段来区别对待。可以利用下面这个示例代码进行理解:
|
||||
|
||||
```csharp
|
||||
public class Tests
|
||||
{
|
||||
private ParsedStat _stat;
|
||||
|
||||
[Benchmark]
|
||||
public ulong GetTime()
|
||||
{
|
||||
ParsedStat stat = _stat;
|
||||
return stat.utime + stat.stime;
|
||||
}
|
||||
|
||||
internal struct ParsedStat
|
||||
{
|
||||
internal int pid;
|
||||
internal string comm;
|
||||
internal char state;
|
||||
internal int ppid;
|
||||
internal int session;
|
||||
internal ulong utime;
|
||||
internal ulong stime;
|
||||
internal long nice;
|
||||
internal ulong starttime;
|
||||
internal ulong vsize;
|
||||
internal long rss;
|
||||
internal ulong rsslim;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这段代码中有一个较大的结构类型,其的大小是80个字节。在没有启用推广的条件下进行运行,`GetTime`方法编译得到的本机代码如下所示。在汇编代码中将下载栈上分配一片88字节的空间,再将整个结构体直接复制到当前方法的栈上,在复制完成之后计算两个字段的和并返回。
|
||||
|
||||
```assembly
|
||||
; Tests.GetTime()
|
||||
push rdi
|
||||
push rsi
|
||||
sub rsp,58
|
||||
lea rsi,[rcx+8]
|
||||
lea rdi,[rsp+8]
|
||||
mov ecx,0A
|
||||
rep movsq
|
||||
mov rax,[rsp+10]
|
||||
add rax,[rsp+18]
|
||||
add rsp,58
|
||||
pop rsi
|
||||
pop rdi
|
||||
ret
|
||||
; Total bytes of code 40
|
||||
```
|
||||
|
||||
而在打开推广的情况下运行得到的本机代码如下所示:
|
||||
|
||||
```assembly
|
||||
; Tests.GetTime()
|
||||
add rcx,8
|
||||
mov rax,[rcx+8]
|
||||
mov rcx,[rcx+10]
|
||||
add rax,rcx
|
||||
ret
|
||||
; Total bytes of code 16
|
||||
```
|
||||
|
||||
在这段汇编代码中,JIT编译器只复制了两个需要使用的字段到当前方法的栈上,这就大幅减少了值类型在方法调用之前产生内存复制开销。
|
||||
|
||||
## 还有更多……
|
||||
|
||||
行文至此,本篇已经字数超过一万字了,毫无疑问这将成为博客历史上最长的一篇文章。在这点字数中我们还只是**简略**的介绍了一下.NET平台过去的几个版本中涉及到的优化,还主要聚焦于JIT编译器和内存管理的部分,在这两个部分之后还有一个线程管理部分也是影响性能的关键组件,同时.NET还提供了一个由数千个API组成的运行库,这些类型中无论是基元类型还是泛型集合类型都获得了若干提升,这些部分共同组成了这几个版本的性能奇迹。
|
||||
|
||||
本篇文章中的主要内容来自于.NET运行时仓库中的[Book of the Runtime](https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/README.md)和微软开发者博客上的[Performance Improvements in .NET 6](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/)、[Performance Improvements in .NET 7](https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/)和[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#whats-next)等几篇文章,上述没有覆盖到的内容推荐读者这些文章。同时算算时间,.NET 9版本引入的性能提升文章应该也要发布了。
|
||||
|
||||
回到文章最开始时的问题:JIT编译就一定比AOT编译慢吗?从启动速度上来说,JIT编译当然是完败AOT编译,但是在程序长时间运行,各项设备(JIT编译器、运行时和GC等)预热完成之后,则是鹿死谁手,犹未可知了。
|
||||
|
||||
BIN
YaeBlog/source/posts/dotnet-performance-8/HeapsWhereNetObjectsLive.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/dotnet-performance-8/HeapsWhereNetObjectsLive.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/dotnet-performance-8/image-20240828135354598.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/dotnet-performance-8/image-20240828135354598.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/dotnet-performance-8/image-20240828155556375.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/dotnet-performance-8/image-20240828155556375.png
(Stored with Git LFS)
Normal file
Binary file not shown.
875
YaeBlog/source/posts/heterogeneous-programming-model.md
Normal file
875
YaeBlog/source/posts/heterogeneous-programming-model.md
Normal file
@@ -0,0 +1,875 @@
|
||||
---
|
||||
title: 异构编程模型的昨天、今天与明天
|
||||
date: 2024-11-04T22:20:41.2571467+08:00
|
||||
tags:
|
||||
- 编译原理
|
||||
- 组会汇报
|
||||
---
|
||||
|
||||
|
||||
随着摩尔定律的逐渐失效,将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中,上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
|
||||
|
||||
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## 异构并行编程模型概述
|
||||
|
||||
异构并行编程模型是沟通上层应用和下层异构系统之间的桥梁,其的设计需要处理好下面五个问题:任务划分、任务映射、数据分布、同步和通信。
|
||||
|
||||
### 异构并行编程模型面临的技术挑战
|
||||
|
||||
异构并行编程模型面临的技术挑战主要是由两方面带来的:首先异构架构本身为编程模型带来的挑战,其次是上层应用带来的挑战。
|
||||
|
||||
异构并行编程模型需要解决的一个重要问题就是为上层应用的程序员提供一个合理的硬件平台抽象,使得其在编程是可以充分释放异构资源带来的计算能力,同时不需要考虑复杂的硬件细节。但是异构系统中各个计算设备在内部体系结构、设备间互联架构上的复杂性和多样性使得异构并行编程模型在提供建立统一的平台抽象上遇到了巨大的困难。具体来说,主要体现下述三点。
|
||||
|
||||
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中,比如多核CPU中,虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务,但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统,例如CPU、GPU和FPGA组成的异构系统,不同设备的微架构具有本质差异,其并行计算的模式和能力都完全不同,设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的,而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点,但是却给异构并行计算模型的设计带来了巨大的困难。
|
||||
|
||||

|
||||
|
||||
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中,CPU片内的存储是对于软件透明的缓存架构,在片外则是一个共享内存模型,因此在这类系统中,数据仅可能分布在片外的共享存储中,具有存储位置单一的特点,也不需要进行显式的通信操作。但是在异构系统中,不仅在单个加速设备内部可能有软件可分配的快速局部存储,设备之间的连接方式差异也很大。目前,大多个加速设备都是通过PCIe总线的方式同CPU进行连接,这使得加速设备无法通过和CPU相同的方式完成地址映射,存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置,不仅使得编程模型的数据分布问题变得十分复杂,设备间的通信文件也可能需要显式进行。
|
||||
|
||||

|
||||
|
||||
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题:在异构系统中数据可能分布在不同位置的条件下,同步操作需要在众多的位置上保证共享数据的一致性,这使得同步操作的范围变得十分复杂。同时,在一些特定的加速设备中,例如GPU,可能还会有局部的硬件同步机制,这更加提高了在异构系统的同步操作的设计和实现难度。
|
||||
|
||||
上层应用带来的挑战主要集中在缺少良好的异构抽象和统一的编程接口上。例如在CPU上进行编程时通常使用Java、Python等高级语言,而在进行GPU编程时则使用各种C语言的变体,其中的核心计算函数(Kernel Function)则通常只支持一个C语言的子集,而FPGA这些硬件设备又需要使用硬件描述语言进行编程。
|
||||
|
||||
### 异构并行编程接口和编译/运行时支持机制
|
||||
|
||||
异构并行编程接口是编程模型暴露给程序员使用的界面,它既需要为程序员提供合理的异构架构抽象,使程序员可以对异构计算资源加以合理利用,又需要保证接口的易用性,避免程序员陷入复杂的硬件细节中。编译/运行时系统是异构并行编程模型的软件工具层,它将程序员编写的加速器代码编译为可执行文件,并通过运行时系统完成任务的加速执行。
|
||||
|
||||
在任务划分、任务映射、数据分布、通信和同步这五个关键任务中,程序员往往只需要关注所编写应用程序的特点,因此显示的任务划分机制对应程序员来说可能是必不可少的,而其他的数据分布、通信和同步等任务只会加剧程序员开发应用程序的负担,但是这些任务通过接口暴露出来也为后续进行深度优化提供了空间。异构编译/运行时支持机制的主要任务就是保障任务映射,即明确任务将具体在哪个设备或者计算单元上执行,以何种顺序执行,同时在当程序员没有显式处理数据分布、通信和同步问题时进行自动处理并进行全系统级别的优化。
|
||||
|
||||
## 异构并行编程接口的研究
|
||||
|
||||
异构并行编程接口一般可以划分成两类:新设计的异构编程语言和现有语言的异构并行扩展。对于现有语言进行的异构并行扩展一般通过库(Library)或者是制导(Directive)的方法进行。
|
||||
|
||||
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
|
||||
|
||||

|
||||
|
||||
### 异构任务划分机制研究
|
||||
|
||||
在同构的并行编程语言中,并行编程接口需要提供一种面向单一设备的并行任务划分机制,这种并行任务划分机制有**任务并行**和**数据并行**等。数据并行指的是对源集合或者数组的元素同时执行相同操作的场景,一个数据并行的典型例子如下面计算两个矩阵的乘积:
|
||||
|
||||
```csharp
|
||||
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
|
||||
{
|
||||
int matACols = matA.GetLength(1);
|
||||
int matBCols = matB.GetLength(1);
|
||||
int matARows = matA.GetLength(0);
|
||||
|
||||
// A basic matrix multiplication.
|
||||
// Parallelize the outer loop to partition the source array by rows.
|
||||
Parallel.For(0, matARows, i =>
|
||||
{
|
||||
for (int j = 0; j < matBCols; j++)
|
||||
{
|
||||
double temp = 0;
|
||||
for (int k = 0; k < matACols; k++)
|
||||
{
|
||||
temp += matA[i, k] * matB[k, j];
|
||||
}
|
||||
result[i, j] = temp;
|
||||
}
|
||||
}); // Parallel.For
|
||||
}
|
||||
```
|
||||
|
||||
任务并行的概念一般是指一个或者多个独立的任务同时运行,是一种比数据并行更高的抽象层级。
|
||||
|
||||
```csharp
|
||||
public class Result
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
|
||||
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
|
||||
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
|
||||
|
||||
var results = new Double[taskArray.Length];
|
||||
Double sum = 0;
|
||||
|
||||
for (int i = 0; i < taskArray.Length; i++) {
|
||||
results[i] = taskArray[i].Result;
|
||||
Console.Write("{0:N1} {1}", results[i],
|
||||
i == taskArray.Length - 1 ? "= " : "+ ");
|
||||
sum += results[i];
|
||||
}
|
||||
Console.WriteLine("{0:N1}", sum);
|
||||
}
|
||||
|
||||
private static Double DoComputation(Double start)
|
||||
{
|
||||
Double sum = 0;
|
||||
for (var value = start; value <= start + 10; value += .1)
|
||||
sum += value;
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不论是高级或者是低级的异构并行编程接口都需要提供一种异构并行任务的划分机制。同传统的同构并行编程接口只需要提供面向单一设备的并行任务划分机制不同,异构并行编程接口还需要提供描述任务在不同设备间分配的机制。因此,异构并行编程接口的任务划分机制需要包括两个维度:异构特征描述和并行性表达两个维度。
|
||||
|
||||
一种典型异构任务划分机制是由`BrookGPU`编程语言提出的。该编程语言采用特殊函数`kernel`标记需要在GPU上执行的代码段,`kernel`函数必须作用在流上。这个流(Stream)在并行性表达方面表达了细粒度的数据并行。后面的OpenCL和CUDA在C语言的基础上提供了异构扩展,这种扩展的任务划分机制和`BrookGPU`的十分类似。但是OpenCL和CUDA在并行行表达的层面上支持了SPMD计算模型,这个`BrookGPU`编程语言采用的流式编程模型不同。OpenCL在数据并行之外还提供了任务并行的机制。
|
||||
|
||||
`Lime`则是一门完全新的异构并行编程语言,通过语言结构为程序提供了丰富的操作符用于任务的划分。同时在异构特征描述方面,`Lime`也没有任何显式的接口,同`BrookGPU`等一系列需要手动指定设备代码段的编程模型完全不同,这也是因为`Lime`采用了基于任务的并行划分方式。同时在任务并行之外,`Lime`也通过`MapReduce`操作符提供了中粒度的数据并行机制。
|
||||
|
||||
`Merge`还是一门新的异构并行编程语言,基于Intel提出的异构多核多线程系统编程环境`EXOCHI`。在并行性表达上,`Merge`使用`MapReduce`思想。而在异构特征描述方面,`Merge`则提供了一种成为平台变体(Target Variant)的机制,程序员需要为异构系统中的不同设备提供不同版本的代码实现。
|
||||
|
||||
### 异构数据分布和通信机制
|
||||
|
||||
异构数据分布和通信机制主要分成显式和隐式两种,其中`OpenCL/CUDA`等使用了显式的数据分布的通信机制,为程序员提供了丰富的异构数据分布与通信接口。而`Lime`和`Merge`等语言则使用了隐式机制,运行时系统代为完成这部分的工作。
|
||||
|
||||
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制,例如GPU的全局内存访存合并机制,这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程,会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法,例如`CUDA-lite`,这个运行时允许程序元在CUDA程序中加入简单的制导语句,数据分布的相关工作使用`CUDA-lite`的运行时系统完成,降低了CUDA程序的编写难度。
|
||||
|
||||

|
||||
|
||||
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
|
||||
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
|
||||
- 异构数据分布和通知机制在传统并行编程模型的基础上增加了"设备内数据多层分布"和"设备间显式通信"接口;
|
||||
- 异构同步机制在传统并行编程模型的基础上增加了"设备间同步"的机制。
|
||||
|
||||
## 异步编译/运行时的研究
|
||||
|
||||
### 异构任务映射机制
|
||||
|
||||
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
|
||||
|
||||

|
||||
|
||||
### 异构编译/运行时优化
|
||||
|
||||
与同构平台类似,异构编译/运行时优化有两条路径:
|
||||
|
||||
- 平台相关的优化,其核心是挖掘系统的硬件优势;
|
||||
- 应用导向的优化,其核心是实施特定领域的优化并解决应用的输入敏感问题。
|
||||
|
||||
在平台优化上,异构系统通常具有复杂且多变的硬件结构, 因此程序员仅负责编写正确实现程序功能的代码、由编译/运行时系统完成面向加速设备结构特点的优化是比较合理的方式, 这样也有利于程序在不同异构系统中获得良好的性能。
|
||||
|
||||
## 异构并行编程模型的研究方向
|
||||
|
||||
- 面向普通用户的异构并行编程接口
|
||||
- 面向多种加速设备的异构编译/运行时优化
|
||||
- 面向异构集群的异构并行编程模型
|
||||
|
||||
## 异构并行编程模型调研
|
||||
|
||||
为了调研各个异构并行编程模型的不同,使用不同的编程模型实现一个通用矩阵乘法算法,并通过计算`2048*2048`大小的矩阵乘法时间来比较各个模型的加速效果。
|
||||
|
||||
辅助计算的`Calculator`类如下所示:
|
||||
|
||||
```cpp
|
||||
#define MATRIX_SIZE 2048
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
|
||||
class Calculator
|
||||
{
|
||||
public:
|
||||
static void validate_matrix(const std::vector<std::vector<int>>& a, const std::vector<std::vector<int>>& b)
|
||||
{
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
if (a[i][j] != b[i][j])
|
||||
{
|
||||
std::cout << "Two matrix must be the same." << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<int>> calculate(const std::string& method,
|
||||
const std::function<std::vector<std::vector<int>>(
|
||||
const std::vector<std::vector<int>>&,
|
||||
const std::vector<std::vector<int>>&)>& calculator) const
|
||||
{
|
||||
std::cout << "Calculator '" << method << "' start." << std::endl;
|
||||
const auto start_time = std::chrono::high_resolution_clock::now();
|
||||
const auto result = calculator(a, b);
|
||||
const auto end_time = std::chrono::high_resolution_clock::now();
|
||||
const auto span = end_time - start_time;
|
||||
|
||||
std::cout << "Calculator '" << method << "' end, time is " << std::chrono::duration_cast<
|
||||
std::chrono::milliseconds>(span).count() << " ms." << std::endl;
|
||||
|
||||
return result;
|
||||
}
|
||||
private:
|
||||
std::vector<std::vector<int>> a = initialize_matrix();
|
||||
std::vector<std::vector<int>> b = initialize_matrix();
|
||||
|
||||
static std::vector<std::vector<int>> initialize_matrix()
|
||||
{
|
||||
std::vector<std::vector<int>> matrix;
|
||||
std::random_device seed;
|
||||
std::ranlux48 engine(seed());
|
||||
std::uniform_int_distribution distribute(0, 102400);
|
||||
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
std::vector row(MATRIX_SIZE, 0);
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
row[j] = distribute(engine);
|
||||
}
|
||||
|
||||
matrix.emplace_back(row);
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
作为对比,一个使用CPU单线程计算的例子如下:
|
||||
|
||||
```cpp
|
||||
inline std::vector<int> cpuMatrixMultiply(
|
||||
const std::vector<int>& a,
|
||||
const std::vector<int>& b)
|
||||
{
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
int temp = 0;
|
||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||
{
|
||||
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
|
||||
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
|
||||
}
|
||||
result[i * MATRIX_SIZE + j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### OpenMP
|
||||
|
||||
OpenMP是`Open MultiProcessing`的缩写,是一个使用编译器制导(Directives)来进行共享内存平行计算的框架,在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口,让程序员能够充分释放多核和多处理器系统性能。
|
||||
|
||||
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型,但是第一,OpenMP作为一个经典的并行编程框架,研究价值还是非常高的,其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速,似乎也能算是一个异构并行编程模型。
|
||||
|
||||
使用OpenMP进行并行加速的代码如下:
|
||||
|
||||
```C++
|
||||
std::vector<std::vector<int>> omp_matrix_multiply(
|
||||
const std::vector<std::vector<int>>& a,
|
||||
const std::vector<std::vector<int>>& b)
|
||||
{
|
||||
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
|
||||
|
||||
#pragma omp parallel for shared(a, b, result) default(none)
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
int temp = 0;
|
||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||
{
|
||||
temp += a[i][k] * b[k][j];
|
||||
}
|
||||
result[i][j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
加速的结果如下:
|
||||
|
||||
| 运行方法 | 运行时间 | 比率 |
|
||||
| ------------ | -------- | ---- |
|
||||
| SingleThread | 21685 ms | 1.00 |
|
||||
| OpenMP | 2268 ms | 0.10 |
|
||||
|
||||
### CUDA
|
||||
|
||||
CUDA是NVIDIA公司设计的一套GPU加速应用程序的编程框架,是将NVIDIA GPU作为GPGPU使用的官方解决方案。
|
||||
|
||||
CUDA的异构编程接口是经典的Device-Host两元结构,程序员需要编写两部分代码,Device代码是实际运行在GPU上的逻辑部分,而Host代码则负责将数据从内存中复制到GPU上的显存和复制回来等准备工作,并负责以特定的参数调用GPU上的Device代码。
|
||||
|
||||
一个使用GPU的矩阵乘法程序如下所示:
|
||||
|
||||
```c++
|
||||
template <typename T>
|
||||
void check(T result, char const* const func, const char* const file, int const line)
|
||||
{
|
||||
if (result)
|
||||
{
|
||||
std::cerr << "CUDA error at " << file << ":" << line << "code = " << result << "(" << cudaGetErrorString(result)
|
||||
<< ") '" << func << "'" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__)
|
||||
|
||||
__global__ void cudaMatrixMultiply(const int* a, const int* b, int* c)
|
||||
{
|
||||
const int totalSize = MATRIX_SIZE * MATRIX_SIZE;
|
||||
int threadId = threadIdx.x + blockIdx.x * blockDim.x;
|
||||
|
||||
while (threadId < totalSize)
|
||||
{
|
||||
const int x = threadId / MATRIX_SIZE;
|
||||
const int y = threadId % MATRIX_SIZE;
|
||||
|
||||
int result = 0;
|
||||
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
|
||||
}
|
||||
|
||||
c[MATRIX_SIZE * x + y] = result;
|
||||
threadId += gridDim.x * blockDim.x;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<int>>& a,
|
||||
const std::vector<std::vector<int>>& b)
|
||||
{
|
||||
constexpr unsigned int matrixSize = sizeof(int) * MATRIX_SIZE * MATRIX_SIZE;
|
||||
|
||||
// 在host上为a, b, c分配空间
|
||||
int *hostA, *hostB, *hostC;
|
||||
checkCudaErrors(cudaMallocHost(&hostA, matrixSize));
|
||||
checkCudaErrors(cudaMallocHost(&hostB, matrixSize));
|
||||
checkCudaErrors(cudaMallocHost(&hostC, matrixSize));
|
||||
|
||||
// 将数据复制到host上
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
hostA[i * MATRIX_SIZE + j] = a[i][j];
|
||||
hostB[i * MATRIX_SIZE + j] = b[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// 在device上分配空间
|
||||
int *deviceA, *deviceB, *deviceC;
|
||||
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceA), matrixSize));
|
||||
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceB), matrixSize));
|
||||
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceC), matrixSize));
|
||||
|
||||
cudaStream_t stream;
|
||||
|
||||
checkCudaErrors(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking));
|
||||
|
||||
// 将数据从host复制到device
|
||||
checkCudaErrors(cudaMemcpyAsync(deviceA, hostA, matrixSize, cudaMemcpyHostToDevice, stream));
|
||||
checkCudaErrors(cudaMemcpyAsync(deviceB, hostB, matrixSize, cudaMemcpyHostToDevice, stream));
|
||||
|
||||
constexpr int threadSize = 32 * 32;
|
||||
constexpr int grid = MATRIX_SIZE * MATRIX_SIZE / threadSize;
|
||||
|
||||
cudaEvent_t start, stop;
|
||||
cudaEventCreate(&start);
|
||||
cudaEventCreate(&stop);
|
||||
|
||||
cudaStreamSynchronize(stream);
|
||||
cudaEventRecord(start, stream);
|
||||
|
||||
cudaMatrixMultiply<<<grid, threadSize, 0, stream>>>(deviceA, deviceB, deviceC);
|
||||
|
||||
cudaEventRecord(stop, stream);
|
||||
cudaEventSynchronize(stop);
|
||||
|
||||
float cudaRunTime = 0;
|
||||
cudaEventElapsedTime(&cudaRunTime, start, stop);
|
||||
std::cout << "CUDA actual run time is " << cudaRunTime << " ms" << std::endl;
|
||||
|
||||
// 将数据从device复制到host
|
||||
cudaMemcpyAsync(hostC, deviceC, matrixSize, cudaMemcpyDeviceToHost, stream);
|
||||
cudaStreamSynchronize(stream);
|
||||
|
||||
std::vector<std::vector<int>> result;
|
||||
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
std::vector<int> row;
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
row.emplace_back(hostC[i * MATRIX_SIZE + j]);
|
||||
}
|
||||
result.emplace_back(row);
|
||||
}
|
||||
|
||||
// 释放内存
|
||||
cudaFreeHost(hostA);
|
||||
cudaFreeHost(hostB);
|
||||
cudaFreeHost(hostC);
|
||||
cudaFree(deviceA);
|
||||
cudaFree(deviceB);
|
||||
cudaFree(deviceC);
|
||||
cudaEventDestroy(start);
|
||||
cudaEventDestroy(stop);
|
||||
cudaStreamDestroy(stream);
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
加速的结果如下所示:
|
||||
|
||||
| 类型 | 运行时间 | 比率 |
|
||||
| ---- | -------- | ----- |
|
||||
| CPU | 22059ms | 1.000 |
|
||||
| GPU | 32ms | 0.001 |
|
||||
|
||||
需要注意的是,上面编写的CUDA代码还没有完全利用GPU的并行计算能力。
|
||||
|
||||
> 这里我遇到的一个非常奇怪的问题是,相同的CPU计算代码,在运行完OpenMP测试之后再运行就会比在CUDA运行之后再运行慢上一倍,而且可复现性极高。这里我给出一个典型的运行时间比较:CUDA计算的时间是323毫秒,CUDA之后的CPU计算时间是38602毫秒,OpenMP的计算时间是8721毫秒,OpenMP之后的计算时间是76598毫秒。
|
||||
>
|
||||
> 针对这个比较奇怪的情况我觉得可以做出三个猜想:
|
||||
>
|
||||
> - 考虑到我使用的CPU是Intel的i7-13600K,这是一个有性能核和效率核组成的大小核异构系统,可能在两次计算的过程中调度到了不同的核上;
|
||||
> - 在进行CUDA计算的过程中提高了缓存的亲和性;
|
||||
> - 在测试中没有设计热身(Warm up)的过程,而在CUDA计算的过程中部分起到了这个作用。
|
||||
>
|
||||
> 针对上面三个猜测做个两个实验:
|
||||
>
|
||||
> - 首先是换了一台没有大小核异构设计的计算机进行实验,发现这下两次使用CPU计算的时间差异不大;
|
||||
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
|
||||
>
|
||||
> 综上所述,可以认为此现象和异构CPU之间存在着明显的关联,但是缺乏直接证据。
|
||||
>
|
||||
> 在我们调整了矩阵的数据布局之后,这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的,而在修改为使用一维数组(也就是现在提供的代码)之后,相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
|
||||
|
||||
### OpenCL
|
||||
|
||||
OpenCL是目前最为典型、发展最好的异构并行编程模型,毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
|
||||
|
||||

|
||||
|
||||
从上图的OpenCL工作原理中可以看出,OpenCL和CUDA类似,也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写,编译之后在CPU上执行,而设备代码使用一个特定的C语言方言OpenCL C进行编写,这个方言针对并行编程进行了扩展,并提供了一系列封装好的数学计算函数。
|
||||
|
||||
设备代码上的编译方法有两种:在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码,离线编译则有两种表现形式,第一种是在线编译的扩展版,由驱动编译得到的可执行程序可以通过API获取并保存下来,当下一需要在同一设备上调用时可以直接使用而不是再次编译,第二种则是完全独立的编译过程,在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
|
||||
|
||||

|
||||
|
||||
在提出离线编译之后,为了让驱动编译好的二进制文件可以在不同的设备之间复用,同时也是支持更为丰富的编译器生态系统,OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
|
||||
|
||||
下面是一个使用OpenCL进行矩阵计算的例子。
|
||||
|
||||
```cpp
|
||||
struct ComputationContext
|
||||
{
|
||||
cl_platform_id platform;
|
||||
cl_device_id device;
|
||||
};
|
||||
|
||||
static std::unique_ptr<ComputationContext> selectDevice()
|
||||
{
|
||||
cl_uint platformCount;
|
||||
checkOpenCLError(clGetPlatformIDs(0, nullptr, &platformCount));
|
||||
std::cout << "Platform count: " << platformCount << std::endl;
|
||||
|
||||
std::vector<cl_platform_id> platforms(platformCount);
|
||||
checkOpenCLError(clGetPlatformIDs(platformCount, platforms.data(), nullptr));
|
||||
|
||||
std::unique_ptr<ComputationContext> selectedDevice = nullptr;
|
||||
|
||||
for (const auto& platform : platforms)
|
||||
{
|
||||
cl_uint deviceCount = 0;
|
||||
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &deviceCount));
|
||||
|
||||
std::vector<cl_device_id> devices(deviceCount);
|
||||
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, deviceCount, devices.data(), nullptr));
|
||||
|
||||
for (const auto& device : devices)
|
||||
{
|
||||
size_t deviceNameLength;
|
||||
checkOpenCLError(clGetDeviceInfo(device, CL_DEVICE_NAME, 0, nullptr, &deviceNameLength));
|
||||
|
||||
std::vector<char> deviceNameArray(deviceNameLength);
|
||||
checkOpenCLError(
|
||||
clGetDeviceInfo(device, CL_DEVICE_NAME, deviceNameLength, deviceNameArray.data(), nullptr));
|
||||
|
||||
std::string deviceName(deviceNameArray.data(), deviceNameArray.size() - 1);
|
||||
|
||||
std::cout << "Found device: " << deviceName << std::endl;
|
||||
|
||||
if (deviceName.find("4060") != std::string::npos)
|
||||
{
|
||||
std::cout << "Select device '" << deviceName << "' as runner." << std::endl;
|
||||
selectedDevice = std::make_unique<ComputationContext>();
|
||||
selectedDevice->platform = platform;
|
||||
selectedDevice->device = device;
|
||||
}
|
||||
else
|
||||
{
|
||||
clReleaseDevice(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedDevice == nullptr)
|
||||
{
|
||||
std::cout << "Failed to find the target device." << std::endl;
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
std::vector<int> clCalculateMatrix(const std::vector<int>& a,
|
||||
const std::vector<int>& b)
|
||||
{
|
||||
cl_int error;
|
||||
|
||||
const std::unique_ptr<ComputationContext> computationContext = selectDevice();
|
||||
// A key-value list ends with 0
|
||||
// See also https://www.khronos.org/registry/OpenCL/specs/3.0-unified/html/OpenCL_API.html#context-properties-table
|
||||
std::array<cl_context_properties, 3> properties = {
|
||||
CL_CONTEXT_PLATFORM,
|
||||
reinterpret_cast<cl_context_properties>(computationContext->platform),
|
||||
0
|
||||
};
|
||||
|
||||
cl_context context = clCreateContext(properties.data(), 1, &computationContext->device, nullptr, nullptr,
|
||||
&error);
|
||||
checkOpenCLError(error);
|
||||
cl_command_queue queue = clCreateCommandQueueWithProperties(context, computationContext->device, nullptr,
|
||||
&error);
|
||||
checkOpenCLError(error);
|
||||
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
constexpr size_t matrixSize = MATRIX_SIZE * MATRIX_SIZE * sizeof(int);
|
||||
|
||||
cl_mem deviceA = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
|
||||
checkOpenCLError(error);
|
||||
cl_mem deviceB = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
|
||||
checkOpenCLError(error);
|
||||
cl_mem deviceC = clCreateBuffer(context, CL_MEM_READ_WRITE, matrixSize, nullptr, &error);
|
||||
checkOpenCLError(error);
|
||||
|
||||
checkOpenCLError(
|
||||
clEnqueueWriteBuffer(queue, deviceA, CL_TRUE, 0, matrixSize, a.data(), 0, nullptr,
|
||||
nullptr));
|
||||
checkOpenCLError(
|
||||
clEnqueueWriteBuffer(queue, deviceB, CL_TRUE, 0, matrixSize, b.data(), 0, nullptr,
|
||||
nullptr));
|
||||
// Copy result to erase the previous result
|
||||
checkOpenCLError(
|
||||
clEnqueueWriteBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
|
||||
nullptr, nullptr
|
||||
));
|
||||
|
||||
auto source = R"(
|
||||
#define MATRIX_SIZE 2048
|
||||
|
||||
__kernel void calculate(const __global int* a, const __global int* b, __global int* c)
|
||||
{
|
||||
const int x = get_global_id(0);
|
||||
const int y = get_global_id(1);
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
|
||||
}
|
||||
|
||||
c[x * MATRIX_SIZE + y] = result;
|
||||
})";
|
||||
|
||||
cl_program program = clCreateProgramWithSource(context, 1, &source, nullptr, &error);
|
||||
checkOpenCLError(error);
|
||||
checkOpenCLError(clBuildProgram(program, 0, nullptr, "", nullptr, nullptr));
|
||||
|
||||
size_t messageSize;
|
||||
checkOpenCLError(
|
||||
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, 0, nullptr, &messageSize));
|
||||
std::vector<char> messageArray(messageSize);
|
||||
checkOpenCLError(
|
||||
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, messageSize, messageArray.data(
|
||||
), nullptr));
|
||||
std::string message(messageArray.data(), messageSize - 1);
|
||||
std::cout << "Build log: " << message << std::endl;
|
||||
|
||||
cl_kernel kernel = clCreateKernel(program, "calculate", &error);
|
||||
checkOpenCLError(error);
|
||||
|
||||
checkOpenCLError(clSetKernelArg(kernel, 0, sizeof(cl_mem), &deviceA));
|
||||
checkOpenCLError(clSetKernelArg(kernel, 1, sizeof(cl_mem), &deviceB));
|
||||
checkOpenCLError(clSetKernelArg(kernel, 2, sizeof(cl_mem), &deviceC));
|
||||
|
||||
cl_event event;
|
||||
constexpr std::size_t globalSize[2] = {MATRIX_SIZE, MATRIX_SIZE};
|
||||
checkOpenCLError(clEnqueueNDRangeKernel(queue, kernel, 2, nullptr,
|
||||
globalSize, nullptr, 0, nullptr, &event));
|
||||
|
||||
checkOpenCLError(clWaitForEvents(1, &event));
|
||||
|
||||
checkOpenCLError(
|
||||
clEnqueueReadBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
|
||||
nullptr, nullptr));
|
||||
|
||||
clReleaseMemObject(deviceA);
|
||||
clReleaseMemObject(deviceB);
|
||||
clReleaseMemObject(deviceC);
|
||||
|
||||
clReleaseKernel(kernel);
|
||||
clReleaseProgram(program);
|
||||
clReleaseCommandQueue(queue);
|
||||
clReleaseContext(context);
|
||||
clReleaseDevice(computationContext->device);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
从上面的代码中可以看出两点:
|
||||
|
||||
- OpenCL的编程比CUDA的更为繁琐,因为OpenCL支持的设备种类更多,在主机代码上还需要多出一块选择运行设备的代码;
|
||||
- OpenCL在主机代码和核函数的解耦更为彻底,核函数直接以字符串的形式存在于主机代码中,而各个厂商提供的驱动才是真正的编译器。
|
||||
|
||||
测试的运行结果如下:
|
||||
|
||||
| 类型 | 运行时间 | 比率 |
|
||||
| ----------------------------- | -------- | ---- |
|
||||
| NVIDIA 4060 Ti OpenCL | 173ms | 0.01 |
|
||||
| Intel UHD Graphics 770 OpenCL | 1020ms | 0.04 |
|
||||
| CPU | 21255ms | 1.00 |
|
||||
|
||||
### SYCL
|
||||
|
||||
SYCL是一个使用标准C++编写在各种异构计算设备上运行核函数的抽象层,并提供了一套新的API来查找各种设备并管理这些设备上的内存资源和代码执行。这个标准是开发、无版税、跨平台的抽象标准。同时也是因为这是一个**标准**,因此需要寻找支持这个标准的编译器才能使用这个标准。按照官网上的说明,我们选择了两个看上去还在活跃开发的项目,Intel的[oneAPI](https://www.intel.com/content/www/us/en/developer/tools/oneapi/overview.html)和开源的[AdaptiveCpp](https://github.com/AdaptiveCpp/AdaptiveCpp)进行调研,考虑到在后文中还将继续介绍oneAPI相关的工作,因此这里将重点放在AdaptiveCpp上。
|
||||
|
||||
AdaptiveCpp由四个部分组成,分别在不同的C++命名空间中提供。
|
||||
|
||||
- SYCL Interface:实现了SYCL标准中规定的各种类和函数,是实际上同用户交互的接口。这些接口实际上可以仍然可以分成主机API和核函数库两个部分。主机API是普通的C++代码,负责任务调度、任务管理和平台射别管理等。核函数库包括了这种在编写核函数时可以使用的类和函数,这些接口暴露一些后端特定的功能,其中的一些甚至需要使用后端特定的方言来编写,例如CUDA。
|
||||
|
||||
- AdaptiveCpp Runtime:运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能,运行时负责同各种支持后端的运行时交互来实现上述的功能。
|
||||
|
||||

|
||||
|
||||
- Compiler:考虑到在用户编写的代码中可能使用一些特定后端的方言,因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的,这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
|
||||
|
||||
- Glue:将上述的各个部分连接在一起的胶水代码。一种典型的胶水代码是内核函数的启动代码`kernel launcher`,由于启动器中往往涉及到一些后端特定的方言,例如CUDA中的`<<<>>>`或者OpenMP中的各种`pragma`,因此这些代码通常需要使用特定的编译器进行编译,所以这些胶水代码直接以头文件的方式提供,以方便在编译时被特定的编译器处理。这些胶水代码将会把核函数包裹为一个合法的C++函数对象,这样运行时就可以获得这个函数对象并控制代码在设备上的运行。
|
||||
|
||||
AdaptiveCpp同时支持多种不同的编译流程。
|
||||
|
||||
1. 一种通用的一遍编译流程,将核函数编译到一种统一的中间表示形式,这种中间表示形式将在运行时被编译到特定的后端架构上。这种编译流程提供了高度的可移植性和较快的编译速度。这种编译设施支持的后端有:通过`PTX`在NVIDIA的GPU上运行,通过`amdgcn`在AMD的GPU上运行,通过`SPIR-V`在Intel的GPU上运行,通过`SPIR-V`在任何支持OpenCL驱动的设备上运行,也可以通过LLVM直接在CPU上运行。
|
||||
2. 一种为互操作性优化的多遍编译流程,在这个流程中AdaptiveCpp将聚合现有的各种LLVM/Clang的编译工具链,使得用户可以在单个代码文件中混合编写SYCL和各种特定的编程模型,例如CUDA和HIP。使用这个编译流程的好处有亮点:(1)在这种编译流程中可以直接在SYCL代码使用各个特定编译模型中提供最新设备内部优化(Intrinsics),不用等待SYCL标准的支持;(2)在这种编译流程中可以使用各个厂商提供的优化模板库,例如`rocPRIM`和`CUB`。这种编译流程是提供聚合`CUDA`的clang前端和`ROCm`的clang前端来实现的。
|
||||
3. 一种只将AdaptiveCpp作为函数使用的编程流程。在这种情况AdaptiveCpp作为一个三方库被引入其他的编译器编译流程中。
|
||||
|
||||
第一种通用的编译流程显然是泛用性最广的一种编译流程,同时也是AdaptiveCpp推荐的编译流程。
|
||||
|
||||

|
||||
|
||||
下面是一段使用SYCL进行矩阵乘法加速的代码:
|
||||
|
||||
```cpp
|
||||
struct CustomDeviceSelector
|
||||
{
|
||||
explicit CustomDeviceSelector(std::string vendorName) : _vendorName(std::move(vendorName))
|
||||
{
|
||||
}
|
||||
|
||||
int operator()(const sycl::device& d) const
|
||||
{
|
||||
int deviceRating = 0;
|
||||
|
||||
if (d.is_gpu() && d.get_info<sycl::info::device::name>().find(_vendorName) != std::string::npos)
|
||||
{
|
||||
deviceRating = 3;
|
||||
}
|
||||
else if (d.is_cpu())
|
||||
{
|
||||
deviceRating = 1;
|
||||
}
|
||||
|
||||
return deviceRating;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _vendorName;
|
||||
};
|
||||
|
||||
static std::vector<int> syclCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b,
|
||||
const std::string& hint)
|
||||
{
|
||||
const CustomDeviceSelector selector(hint);
|
||||
sycl::queue queue(selector);
|
||||
|
||||
const std::string deviceName = queue.get_device().get_info<sycl::info::device::name>();
|
||||
std::cout << "Select device: " << deviceName << std::endl;
|
||||
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
|
||||
sycl::buffer aBuffer(a);
|
||||
sycl::buffer bBuffer(b);
|
||||
sycl::buffer resultBuffer(result);
|
||||
|
||||
queue.submit([&](sycl::handler& h)
|
||||
{
|
||||
const sycl::accessor aBufferAccessor(aBuffer, h, sycl::read_only);
|
||||
const sycl::accessor bBufferAccessor(bBuffer, h, sycl::read_only);
|
||||
const sycl::accessor resultBufferAccessor(resultBuffer, h, sycl::write_only);
|
||||
|
||||
h.parallel_for(sycl::nd_range<2>({MATRIX_SIZE, MATRIX_SIZE}, {16, 16}), [=](const sycl::nd_item<2>& item)
|
||||
{
|
||||
const size_t x = item.get_global_id(0);
|
||||
const size_t y = item.get_global_id(1);
|
||||
|
||||
int temp = 0;
|
||||
for (size_t k = 0; k < MATRIX_SIZE; ++k)
|
||||
{
|
||||
temp += aBufferAccessor[x * MATRIX_SIZE + k] * bBufferAccessor[k * MATRIX_SIZE + y];
|
||||
}
|
||||
resultBufferAccessor[x * MATRIX_SIZE + y] = temp;
|
||||
});
|
||||
});
|
||||
|
||||
sycl::host_accessor resultHostAccessor(resultBuffer, sycl::read_only);
|
||||
|
||||
for (size_t i = 0; i < MATRIX_SIZE; ++i)
|
||||
{
|
||||
for (size_t j = 0; j < MATRIX_SIZE; ++j)
|
||||
{
|
||||
result[i * MATRIX_SIZE + j] = resultHostAccessor[i * MATRIX_SIZE + j];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
测试之后的运行结果如下所示:
|
||||
|
||||
| 类型 | 运行时间 | 比率 |
|
||||
| --------------------------- | -------- | ----- |
|
||||
| Intel UHD Graphics 770 SYCL | 488ms | 0.023 |
|
||||
| NVIDIA 4060 Ti SYCL | 180ms | 0.008 |
|
||||
| OpenMP SYCL | 1591ms | 0.076 |
|
||||
| CPU | 20930ms | 1.000 |
|
||||
|
||||
### OpenACC
|
||||
|
||||
OpenACC是一个通过编译器制导来在代码中表达并行性并利用并行编译器为多个并行加速器生成代码的编程模型。为了保证OpenACC可以适配于各种计算架构的加速设备,OpenACC设计了一个各种并行层次和有着不同速度和寻址方式内存的编程模型。同时OpenACC主要的功能即是支持同时将计算和数据卸载到一个加速设备上,考虑到加速设备可能有着同宿主设备完全不同的内存架构,OpenACC编译器和运行时将会自动分析代码并负责加速器上内存的管理和加速器和主机之间的数据传输。
|
||||
|
||||
作为一个高等级、平台独立的加速器编程框架,使用OpenACC进行开发能够使开发人员将一个源代码编译到一系列设备上运行并实现一个相对较好的性能,但是这个简易性和移植性也在一定程度上造成使用OpenACC编程无法完全利用加速设备上的算力。
|
||||
|
||||
OpenACC是作为一个标准的形式提供的,实现了该标准的编译器有:
|
||||
|
||||
| 编译器名称 | 情况 |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| NVIDIA HPC SDK | 支持在NVIDIA GPU和多核CPU上的OpenACC并行编程 |
|
||||
| Sourcery CodeBench Lite | OpenACC官网上说支持针对AMD GPU的编译,但是官网页面似乎改版了,没有找到相关的内容 |
|
||||
| GCC 12 | 支持到OpenACC 2.6 |
|
||||
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
|
||||
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
|
||||
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器,看上去还做了不少工作的样子,就是OpenACC官网上的链接已经失效了找起来比较麻烦,而且宣称是一个开源编译器,但是获取源代码和二进制文件需要联系他们(美国橡树岭国家实验室)创建账户,这看去对于我们这些Foreign Adversary有些抽象了。 |
|
||||
|
||||
在试验OpenACC时遇到了巨大的困难,不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速,多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
|
||||
|
||||
实验中编写的OpenACC加速代码如下:
|
||||
|
||||
```cpp
|
||||
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
|
||||
{
|
||||
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
|
||||
|
||||
const auto aBuffer = new int[length];
|
||||
const auto bBuffer = new int[length];
|
||||
const auto cBuffer = new int[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
aBuffer[i] = a[i];
|
||||
bBuffer[i] = b[i];
|
||||
cBuffer[i] = 0;
|
||||
}
|
||||
|
||||
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
|
||||
#pragma acc enter data create(bBuffer[0:length])
|
||||
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
|
||||
{
|
||||
#pragma acc kernels loop independent
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
#pragma acc loop independent
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
int temp = 0;
|
||||
#pragma acc loop independent reduction(+:temp)
|
||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||
{
|
||||
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
|
||||
}
|
||||
cBuffer[i * MATRIX_SIZE + j] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma acc exit data copyout(cBuffer[0:length])
|
||||
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
|
||||
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
|
||||
for (int i = 0; i < length; ++i)
|
||||
{
|
||||
result[i] = cBuffer[i];
|
||||
}
|
||||
|
||||
delete[] aBuffer;
|
||||
delete[] bBuffer;
|
||||
delete[] cBuffer;
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
|
||||
|
||||
| 编译器 | 类型 | 运行时间 |
|
||||
| -------------- | ------- | -------- |
|
||||
| NVIDIA HPC SDK | OpenACC | 19315ms |
|
||||
| NVIDIA HPC SDK | CPU | 22942ms |
|
||||
| GCC | OpenACC | 19999ms |
|
||||
| GCC | CPU | 22623ms |
|
||||
|
||||
### oneAPI
|
||||
|
||||
oneAPI是Intel公司提出的一套异构并行编程框架,该框架致力于达成如下几个目标:(1)定义一个跨架构、跨制造商的统一开放软件平台;(2)允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行;(3)提供一套覆盖多个编程领域的库API。为了实现这些目标,oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作,oneAPI也提供了一个SYCL的编译器和运行时;同时oneAPI也提供了一系列API库,包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
|
||||
|
||||

|
||||
|
||||
我对于oneAPI的理解就是Intel用来对标NVIDIA的CUDA的一套高性能编程工具箱。首先为了和NVIDIA完全闭源的CUDA形成鲜明的对比,Intel选择了OpenCL合作同时开发SYCL,当时也有可能是Intel知道自己的显卡技不如人,如果不兼容市面上其他的部件是没有出路的,同时为了和CUDA丰富的生态竞争,Intel再开发并开源了一系列的`oneXXX`。
|
||||
|
||||
这里我就把上面SYCL写的例子用Intel提供的`DPC++`编译运行一下,看看在效率上会不会有所变化。
|
||||
|
||||
| 类型 | 运行时间 | 比率 |
|
||||
| ----------------------------- | -------- | ----- |
|
||||
| Intel UHD Graphics 770 oneAPI | 429ms | 0.023 |
|
||||
| NVIDIA 4060 Ti oneAPI | 191ms | 0.010 |
|
||||
| Intel i5-13600K oneAPI | 198ms | 0.011 |
|
||||
| CPU | 18643ms | 1.000 |
|
||||
|
||||
在显卡上的计算时间没有明显的变化,但是我们Intel的编译器却在选择到使用Intel CPU进行计算时展现了不俗的实力。
|
||||
|
||||
|
||||
## 参考文献
|
||||
|
||||
1. 刘颖,吕方,王蕾,陈莉,崔慧敏,冯晓兵.异构并行编程模型研究与进展.软件学报,2014,25(7):1459-1475. [http://www.jos.org.cn/1000-9825/4608.htm](http://www.jos.org.cn/1000-9825/4608.htm)
|
||||
2. AdaptiveCpp官方文档. [https://adaptivecpp.github.io/AdaptiveCpp/](https://adaptivecpp.github.io/AdaptiveCpp/)
|
||||
3. Exploring the performance of SGEMM in OpenCL on NVIDIA GPUs. [https://github.com/CNugteren/myGEMM](https://github.com/CNugteren/myGEMM)
|
||||
4. OpenACC Programming and Best Practices Guide. [https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html](https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html)
|
||||
5. oneAPI What is it?. [https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html](https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html)
|
||||
|
||||
BIN
YaeBlog/source/posts/heterogeneous-programming-model/83ee1d254d638536d0fb4197ff63e758.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/heterogeneous-programming-model/83ee1d254d638536d0fb4197ff63e758.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/heterogeneous-programming-model/9eb06d8be92ddef3db33e040163c67a7.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/heterogeneous-programming-model/9eb06d8be92ddef3db33e040163c67a7.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/heterogeneous-programming-model/Screenshot_20241016_214139.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/heterogeneous-programming-model/Screenshot_20241016_214139.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user