Feat: improve thumbnails proformance and GC for local policy (#1044)

* thumb generating improvement

Replace "github.com/nfnt/resize" with "golang.org/x/image/draw". Add thumb task queue to avoid oom when batch thumb operation

* thumb improvement

* Add some tests for thumbnail generation
This commit is contained in:
kikoqiu
2021-11-11 17:45:22 +08:00
committed by GitHub
parent 4d7b8685b9
commit 54ed7e43ca
9 changed files with 160 additions and 15 deletions

View File

@@ -70,9 +70,13 @@ type redis struct {
// 缩略图 配置
type thumb struct {
MaxWidth uint
MaxHeight uint
FileSuffix string `validate:"min=1"`
MaxWidth uint
MaxHeight uint
FileSuffix string `validate:"min=1"`
MaxTaskCount int
EncodeMethod string `validate:"eq=jpg|eq=png"`
EncodeQuality int `validate:"gte=1,lte=100"`
GCAfterGen bool
}
// 跨域配置

View File

@@ -51,9 +51,13 @@ var CORSConfig = &cors{
// ThumbConfig 缩略图配置
var ThumbConfig = &thumb{
MaxWidth: 400,
MaxHeight: 300,
FileSuffix: "._thumb",
MaxWidth: 400,
MaxHeight: 300,
FileSuffix: "._thumb",
MaxTaskCount: -1,
EncodeMethod: "jpg",
GCAfterGen: false,
EncodeQuality: 85,
}
// SlaveConfig 从机配置

View File

@@ -4,6 +4,9 @@ import (
"context"
"fmt"
"strconv"
"sync"
"runtime"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
@@ -35,18 +38,53 @@ func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentR
ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, [2]uint{w, h})
ctx = context.WithValue(ctx, fsctx.FileModelCtx, fs.FileTarget[0])
res, err := fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName)
if err == nil && conf.SystemConfig.Mode == "master" {
res.MaxAge = model.GetIntSetting("preview_timeout", 60)
}
// 本地存储策略出错时重新生成缩略图
if err != nil && fs.Policy.Type == "local" {
fs.GenerateThumbnail(ctx, &fs.FileTarget[0])
res, err = fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName)
}
if err == nil && conf.SystemConfig.Mode == "master" {
res.MaxAge = model.GetIntSetting("preview_timeout", 60)
}
return res, err
}
// thumbPool 要使用的任务池
var thumbPool *Pool
var once sync.Once
// Pool 带有最大配额的任务池
type Pool struct {
// 容量
worker chan int
}
// Init 初始化任务池
func getThumbWorker() *Pool {
once.Do(func() {
maxWorker := conf.ThumbConfig.MaxTaskCount
if maxWorker <= 0 {
maxWorker = runtime.GOMAXPROCS(0)
}
thumbPool = &Pool{
worker: make(chan int, maxWorker),
}
util.Log().Debug("初始化Thumb任务队列WorkerNum = %d", maxWorker)
})
return thumbPool
}
func (pool *Pool) addWorker() {
pool.worker <- 1
util.Log().Debug("Thumb任务队列addWorker")
}
func (pool *Pool) releaseWorker() {
util.Log().Debug("Thumb任务队列releaseWorker")
<-pool.worker
}
// GenerateThumbnail 尝试为本地策略文件生成缩略图并获取图像原始大小
// TODO 失败时,如果之前还有图像信息,则清除
func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
@@ -65,6 +103,8 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
return
}
defer source.Close()
getThumbWorker().addWorker()
defer getThumbWorker().releaseWorker()
image, err := thumb.NewThumbFromFile(source, file.Name)
if err != nil {
@@ -79,6 +119,12 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
image.GetThumb(fs.GenerateThumbnailSize(w, h))
// 保存到文件
err = image.Save(util.RelativePath(file.SourceName + conf.ThumbConfig.FileSuffix))
image = nil
if conf.ThumbConfig.GCAfterGen {
util.Log().Debug("GenerateThumbnail runtime.GC")
runtime.GC()
}
if err != nil {
util.Log().Warning("无法保存缩略图:%s", err)
return

View File

@@ -38,3 +38,12 @@ func TestFileSystem_GetThumb(t *testing.T) {
asserts.EqualValues(50, res.MaxAge)
}
}
func TestFileSystem_ThumbWorker(t *testing.T) {
asserts := assert.New(t)
asserts.NotPanics(func() {
getThumbWorker().addWorker()
getThumbWorker().releaseWorker()
})
}

View File

@@ -12,9 +12,11 @@ import (
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/nfnt/resize"
//"github.com/nfnt/resize"
"golang.org/x/image/draw"
)
// Thumb 缩略图
@@ -58,7 +60,8 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) {
// GetThumb 生成给定最大尺寸的缩略图
func (image *Thumb) GetThumb(width, height uint) {
image.src = resize.Thumbnail(width, height, image.src, resize.Lanczos3)
//image.src = resize.Thumbnail(width, height, image.src, resize.Lanczos3)
image.src = Thumbnail(width, height, image.src)
}
// GetSize 获取图像尺寸
@@ -75,12 +78,59 @@ func (image *Thumb) Save(path string) (err error) {
return err
}
defer out.Close()
switch conf.ThumbConfig.EncodeMethod {
case "png":
err = png.Encode(out, image.src)
default:
err = jpeg.Encode(out, image.src, &jpeg.Options{Quality: conf.ThumbConfig.EncodeQuality})
}
err = png.Encode(out, image.src)
return err
}
// Thumbnail will downscale provided image to max width and height preserving
// original aspect ratio and using the interpolation function interp.
// It will return original image, without processing it, if original sizes
// are already smaller than provided constraints.
func Thumbnail(maxWidth, maxHeight uint, img image.Image) image.Image {
origBounds := img.Bounds()
origWidth := uint(origBounds.Dx())
origHeight := uint(origBounds.Dy())
newWidth, newHeight := origWidth, origHeight
// Return original image if it have same or smaller size as constraints
if maxWidth >= origWidth && maxHeight >= origHeight {
return img
}
// Preserve aspect ratio
if origWidth > maxWidth {
newHeight = uint(origHeight * maxWidth / origWidth)
if newHeight < 1 {
newHeight = 1
}
newWidth = maxWidth
}
if newHeight > maxHeight {
newWidth = uint(newWidth * maxHeight / newHeight)
if newWidth < 1 {
newWidth = 1
}
newHeight = maxHeight
}
return Resize(newWidth, newHeight, img)
}
func Resize(newWidth, newHeight uint, img image.Image) image.Image {
// Set the expected size that you want:
dst := image.NewRGBA(image.Rect(0, 0, int(newWidth), int(newHeight)))
// Resize:
draw.BiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Src, nil)
return dst
}
// CreateAvatar 创建头像
func (image *Thumb) CreateAvatar(uid uint) error {
// 读取头像相关设定
@@ -92,7 +142,8 @@ func (image *Thumb) CreateAvatar(uid uint) error {
// 生成头像缩略图
src := image.src
for k, size := range []int{s, m, l} {
image.src = resize.Resize(uint(size), uint(size), src, resize.Lanczos3)
//image.src = resize.Resize(uint(size), uint(size), src, resize.Lanczos3)
image.src = Resize(uint(size), uint(size), src)
err := image.Save(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k)))
if err != nil {
return err

View File

@@ -86,6 +86,30 @@ func TestThumb_GetThumb(t *testing.T) {
})
}
func TestThumb_Thumbnail(t *testing.T) {
asserts := assert.New(t)
{
img := image.NewRGBA(image.Rect(0, 0, 500, 200))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 40))
}
{
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 100))
}
{
img := image.NewRGBA(image.Rect(0, 0, 500, 500))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 100))
}
{
img := image.NewRGBA(image.Rect(0, 0, 200, 500))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 40, 100))
}
}
func TestThumb_Save(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()