update nest: use dispatcher to get lower allocs(4.3.5-rc.0)

This commit is contained in:
wjqserver 2025-09-14 10:05:25 +08:00
parent 3adc110298
commit 3d05902824
7 changed files with 299 additions and 86 deletions

View file

@ -2,14 +2,78 @@ package proxy
import (
"bufio"
"bytes"
"fmt"
"ghproxy/config"
"io"
"strings"
"sync"
"github.com/infinite-iroha/touka"
)
var (
prefixGithub = []byte("https://github.com")
prefixRawUser = []byte("https://raw.githubusercontent.com")
prefixRaw = []byte("https://raw.github.com")
prefixGistUser = []byte("https://gist.githubusercontent.com")
prefixGist = []byte("https://gist.github.com")
prefixAPI = []byte("https://api.github.com")
prefixHTTP = []byte("http://")
prefixHTTPS = []byte("https://")
)
func EditorMatcherBytes(rawPath []byte, cfg *config.Config) (bool, error) {
if bytes.HasPrefix(rawPath, prefixGithub) {
return true, nil
}
if bytes.HasPrefix(rawPath, prefixRawUser) {
return true, nil
}
if bytes.HasPrefix(rawPath, prefixRaw) {
return true, nil
}
if bytes.HasPrefix(rawPath, prefixGistUser) {
return true, nil
}
if bytes.HasPrefix(rawPath, prefixGist) {
return true, nil
}
if cfg.Shell.RewriteAPI {
if bytes.HasPrefix(rawPath, prefixAPI) {
return true, nil
}
}
return false, nil
}
func modifyURLBytes(url []byte, host []byte, cfg *config.Config) []byte {
matched, err := EditorMatcherBytes(url, cfg)
if err != nil || !matched {
return url
}
var u []byte
if bytes.HasPrefix(url, prefixHTTPS) {
u = url[len(prefixHTTPS):]
} else if bytes.HasPrefix(url, prefixHTTP) {
u = url[len(prefixHTTP):]
} else {
u = url
}
newLen := len(prefixHTTPS) + len(host) + 1 + len(u)
newURL := make([]byte, newLen)
written := 0
written += copy(newURL[written:], prefixHTTPS)
written += copy(newURL[written:], host)
written += copy(newURL[written:], []byte("/"))
copy(newURL[written:], u)
return newURL
}
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
@ -64,87 +128,126 @@ func modifyURL(url string, host string, cfg *config.Config) string {
return url
}
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// processLinksStreamingInternal is a link processing function that reads the input line by line.
// It is memory-safe for large files but less performant due to numerous small allocations.
func processLinksStreamingInternal(input io.ReadCloser, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe()
readerOut = pipeReader
go func() { // 在 Goroutine 中执行写入操作
go func() {
defer func() {
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
c.Errorf("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
c.Errorf("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
}
}
if err != nil {
pipeWriter.CloseWithError(err)
} else {
pipeWriter.Close()
}
}()
defer input.Close()
defer func() {
if err := input.Close(); err != nil {
c.Errorf("input close failed: %v", err)
}
bufReader := bufio.NewReader(input)
bufWriter := bufio.NewWriterSize(pipeWriter, 4096)
defer bufWriter.Flush()
}()
var bufReader *bufio.Reader
bufReader = bufio.NewReader(input)
var bufWriter *bufio.Writer
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
//确保writer关闭
defer func() {
if flushErr := bufWriter.Flush(); flushErr != nil {
c.Errorf("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
for {
line, readErr := bufReader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break // 文件结束
}
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
return // Goroutine 中使用 return 返回错误
if readErr != nil && readErr != io.EOF {
err = fmt.Errorf("read error: %w", readErr)
return
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
return modifyURL(originalURL, host, cfg)
})
n, writeErr := bufWriter.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if writeErr != nil {
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
return // Goroutine 中使用 return 返回错误
var n int
n, err = bufWriter.WriteString(modifiedLine)
written += int64(n)
if err != nil {
err = fmt.Errorf("write error: %w", err)
return
}
}
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush但这里再加一次确保及时刷新)
if flushErr := bufWriter.Flush(); flushErr != nil {
if err == nil { // 避免覆盖之前的错误
err = flushErr
if readErr == io.EOF {
break
}
return // Goroutine 中使用 return 返回错误
}
}()
return readerOut, written, nil // 返回 reader 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
return readerOut, written, nil
}
// processLinks acts as a dispatcher, choosing the best processing strategy based on file size.
// It uses a memory-safe streaming approach for large or unknown-size files,
// and a high-performance buffered approach for smaller files.
func processLinks(input io.ReadCloser, host string, cfg *config.Config, c *touka.Context, bodySize int) (readerOut io.Reader, written int64, err error) {
const sizeThreshold = 256 * 1024 // 256KB
// Use streaming for large or unknown size files to prevent OOM
if bodySize == -1 || bodySize > sizeThreshold {
c.Debugf("Using streaming processor for large/unknown size file (%d bytes)", bodySize)
return processLinksStreamingInternal(input, host, cfg, c)
} else {
c.Debugf("Using buffered processor for small file (%d bytes)", bodySize)
return processLinksBufferedInternal(input, host, cfg, c)
}
}
// processLinksBufferedInternal a link processing function that reads the entire content into a buffer.
// It is optimized for performance on smaller files but carries an OOM risk for large files.
func processLinksBufferedInternal(input io.ReadCloser, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe()
readerOut = pipeReader
hostBytes := []byte(host)
go func() {
// 在 goroutine 退出时, 根据 err 是否为 nil, 带错误或正常关闭 pipeWriter
defer func() {
if closeErr := input.Close(); closeErr != nil {
c.Errorf("input close failed: %v", closeErr)
}
}()
defer func() {
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil {
c.Errorf("pipeWriter close with error failed: %v", closeErr)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil {
c.Errorf("pipeWriter close failed: %v", closeErr)
}
}
}()
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
// 将全部输入读入复用的缓冲区
if _, err = buf.ReadFrom(input); err != nil {
err = fmt.Errorf("reading input failed: %w", err)
return
}
// 使用 ReplaceAllFunc 和字节版本辅助函数, 实现准零分配
modifiedBytes := urlPattern.ReplaceAllFunc(buf.Bytes(), func(originalURL []byte) []byte {
return modifyURLBytes(originalURL, hostBytes, cfg)
})
// 将处理后的字节写回管道
var n int
n, err = pipeWriter.Write(modifiedBytes)
if err != nil {
err = fmt.Errorf("writing to pipe failed: %w", err)
return
}
written = int64(n)
}()
return readerOut, written, nil
}