⚡ Performance optimization
This commit is contained in:
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
|
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
|
||||||
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
|
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
|
||||||
<a href="https://goreportcard.com/"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
|
<a href="https://goreportcard.com/report/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square&logo=github" alt="License"></a>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache-green?style=flat-square&logo=github" alt="License"></a>
|
||||||
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
|
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -608,7 +608,7 @@ Contributions of code, issue reports, or suggestions are welcome! Please follow
|
|||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
|
<div align="center" style="margin-bottom: 20px; padding: 15px; border-radius: 10px;">
|
||||||
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
|
<a href="https://pkg.go.dev/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go-reference-blue?style=flat-square&logo=go" alt="Go Reference"></a>
|
||||||
<a href="https://goreportcard.com/"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
|
<a href="https://goreportcard.com/report/github.com/landaiqing/go-pixelnebula"><img src="https://img.shields.io/badge/go%20report-A+-brightgreen?style=flat-square&logo=go" alt="Go Report Card"></a>
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square&logo=github" alt="License"></a>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache-green?style=flat-square&logo=github" alt="License"></a>
|
||||||
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
|
<a href="https://github.com/landaiqing/go-pixelnebula/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square&logo=github" alt="Version"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -615,7 +615,7 @@ go test -bench=. -benchmem
|
|||||||
|
|
||||||
## 📄 许可证
|
## 📄 许可证
|
||||||
|
|
||||||
该项目采用 MIT 许可证 - 详情请查看 [LICENSE](LICENSE) 文件。
|
该项目采用 Apache 2.0 许可证 - 详情请查看 [LICENSE](LICENSE) 文件。
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
@@ -2,6 +2,23 @@ package animation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 字符串构建器对象池
|
||||||
|
animationBuilderPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(strings.Builder)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画映射对象池
|
||||||
|
animationMapPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make(map[string]string)
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnimationType 表示动画类型
|
// AnimationType 表示动画类型
|
||||||
@@ -26,6 +43,8 @@ type Animation interface {
|
|||||||
GenerateSVG() string
|
GenerateSVG() string
|
||||||
// GetTargetID 获取目标元素ID
|
// GetTargetID 获取目标元素ID
|
||||||
GetTargetID() string
|
GetTargetID() string
|
||||||
|
// GetType 获取动画类型
|
||||||
|
GetType() AnimationType
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseAnimation 基础动画结构,包含所有动画共有的属性
|
// BaseAnimation 基础动画结构,包含所有动画共有的属性
|
||||||
@@ -43,20 +62,20 @@ func (a *BaseAnimation) GetTargetID() string {
|
|||||||
return a.TargetID
|
return a.TargetID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetType 获取动画类型
|
||||||
|
func (a *BaseAnimation) GetType() AnimationType {
|
||||||
|
return a.Type
|
||||||
|
}
|
||||||
|
|
||||||
// Manager 动画管理器,负责管理所有动画
|
// Manager 动画管理器,负责管理所有动画
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
animations []Animation
|
animations []Animation
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnimations 获取所有动画
|
|
||||||
func (m *Manager) GetAnimations() []Animation {
|
|
||||||
return m.animations
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAnimationManager 创建一个新的动画管理器
|
// NewAnimationManager 创建一个新的动画管理器
|
||||||
func NewAnimationManager() *Manager {
|
func NewAnimationManager() *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
animations: make([]Animation, 0),
|
animations: make([]Animation, 0, 10), // 预分配容量
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,25 +84,63 @@ func (m *Manager) AddAnimation(animation Animation) {
|
|||||||
m.animations = append(m.animations, animation)
|
m.animations = append(m.animations, animation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAnimations 获取所有动画
|
||||||
|
func (m *Manager) GetAnimations() []Animation {
|
||||||
|
return m.animations
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnimationsByType 获取指定类型的动画
|
||||||
|
func (m *Manager) GetAnimationsByType(animType AnimationType) []Animation {
|
||||||
|
// 预分配足够容量
|
||||||
|
result := make([]Animation, 0, len(m.animations)/2)
|
||||||
|
|
||||||
|
for _, anim := range m.animations {
|
||||||
|
if anim.GetType() == animType {
|
||||||
|
result = append(result, anim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateSVGAnimations 生成SVG动画代码
|
// GenerateSVGAnimations 生成SVG动画代码
|
||||||
func (m *Manager) GenerateSVGAnimations() string {
|
func (m *Manager) GenerateSVGAnimations() string {
|
||||||
if len(m.animations) == 0 {
|
if len(m.animations) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
// 从对象池获取构建器
|
||||||
|
sb := animationBuilderPool.Get().(*strings.Builder)
|
||||||
|
sb.Reset()
|
||||||
|
sb.Grow(1024) // 预分配足够的容量
|
||||||
|
defer animationBuilderPool.Put(sb)
|
||||||
|
|
||||||
// 添加SVG命名空间声明
|
// 添加SVG命名空间声明
|
||||||
sb.WriteString("<defs>\n")
|
sb.WriteString("<defs>\n")
|
||||||
|
|
||||||
// 用于存储需要放在defs中的定义
|
// 获取定义内容构建器
|
||||||
var defsContent strings.Builder
|
defsContent := animationBuilderPool.Get().(*strings.Builder)
|
||||||
// 用于存储需要直接添加到SVG中的动画元素
|
defsContent.Reset()
|
||||||
var animationsContent strings.Builder
|
defsContent.Grow(512)
|
||||||
// 用于存储旋转动画的映射,键为目标元素ID
|
defer animationBuilderPool.Put(defsContent)
|
||||||
rotateAnimations := make(map[string]string)
|
|
||||||
|
|
||||||
// 处理所有动画
|
// 获取动画内容构建器
|
||||||
|
animationsContent := animationBuilderPool.Get().(*strings.Builder)
|
||||||
|
animationsContent.Reset()
|
||||||
|
animationsContent.Grow(512)
|
||||||
|
defer animationBuilderPool.Put(animationsContent)
|
||||||
|
|
||||||
|
// 获取旋转动画映射
|
||||||
|
rotateAnimations := animationMapPool.Get().(map[string]string)
|
||||||
|
defer func() {
|
||||||
|
// 清空映射并归还
|
||||||
|
for k := range rotateAnimations {
|
||||||
|
delete(rotateAnimations, k)
|
||||||
|
}
|
||||||
|
animationMapPool.Put(rotateAnimations)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 一次性处理所有动画以减少循环开销
|
||||||
for _, anim := range m.animations {
|
for _, anim := range m.animations {
|
||||||
svgCode := anim.GenerateSVG()
|
svgCode := anim.GenerateSVG()
|
||||||
if svgCode == "" {
|
if svgCode == "" {
|
||||||
@@ -91,16 +148,15 @@ func (m *Manager) GenerateSVGAnimations() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 根据动画类型决定放置位置
|
// 根据动画类型决定放置位置
|
||||||
switch a := anim.(type) {
|
switch anim.GetType() {
|
||||||
case *GradientAnimation:
|
case Gradient:
|
||||||
// 渐变定义需要放在defs中
|
// 渐变定义需要放在defs中
|
||||||
defsContent.WriteString(svgCode)
|
defsContent.WriteString(svgCode)
|
||||||
case *RotateAnimation:
|
case Rotate:
|
||||||
// 旋转动画需要包裹目标元素,先存储起来
|
// 旋转动画需要包裹目标元素,先存储起来
|
||||||
// 提取animateTransform标签
|
|
||||||
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
|
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
|
||||||
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
|
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
|
||||||
rotateAnimations[a.GetTargetID()] = svgCode[start : start+end+2]
|
rotateAnimations[anim.GetTargetID()] = svgCode[start : start+end+2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@@ -83,13 +83,33 @@ func BenchmarkCacheSizes(b *testing.B) {
|
|||||||
// BenchmarkCacheCompression 测试缓存压缩对性能的影响
|
// BenchmarkCacheCompression 测试缓存压缩对性能的影响
|
||||||
func BenchmarkCacheCompression(b *testing.B) {
|
func BenchmarkCacheCompression(b *testing.B) {
|
||||||
compressionLevels := []struct {
|
compressionLevels := []struct {
|
||||||
name string
|
name string
|
||||||
level int
|
options cache.CompressOptions
|
||||||
}{
|
}{
|
||||||
{"NoCompression", 0},
|
{
|
||||||
{"LowCompression", 3},
|
name: "NoCompression",
|
||||||
{"MediumCompression", 6},
|
options: cache.CompressOptions{
|
||||||
{"HighCompression", 9},
|
Enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DefaultCompression",
|
||||||
|
options: cache.CompressOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Level: 5, // 默认压缩级别
|
||||||
|
MinSize: 1024, // 最小压缩大小
|
||||||
|
CompressionRatio: 0.8, // 最小压缩比率
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BestCompression",
|
||||||
|
options: cache.CompressOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Level: 9, // 最高压缩级别
|
||||||
|
MinSize: 1024, // 最小压缩大小
|
||||||
|
CompressionRatio: 0.8, // 最小压缩比率
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cl := range compressionLevels {
|
for _, cl := range compressionLevels {
|
||||||
@@ -100,12 +120,8 @@ func BenchmarkCacheCompression(b *testing.B) {
|
|||||||
pn.WithSize(231, 231)
|
pn.WithSize(231, 231)
|
||||||
pn.WithDefaultCache()
|
pn.WithDefaultCache()
|
||||||
|
|
||||||
if cl.level > 0 {
|
if cl.options.Enabled {
|
||||||
pn.WithCompression(cache.CompressOptions{
|
pn.WithCompression(cl.options)
|
||||||
Enabled: true,
|
|
||||||
Level: cl.level,
|
|
||||||
MinSizeBytes: 100,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
|
110
cache/cache.go
vendored
110
cache/cache.go
vendored
@@ -2,10 +2,28 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 字符串构建器对象池
|
||||||
|
stringBuilderPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(strings.Builder)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存项对象池
|
||||||
|
cacheItemPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(CacheItem)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// CacheOptions 缓存配置选项
|
// CacheOptions 缓存配置选项
|
||||||
type CacheOptions struct {
|
type CacheOptions struct {
|
||||||
Enabled bool // 是否启用缓存
|
Enabled bool // 是否启用缓存
|
||||||
@@ -14,6 +32,8 @@ type CacheOptions struct {
|
|||||||
EvictionType string // 缓存淘汰策略,支持"lru"(最近最少使用)和"fifo"(先进先出)
|
EvictionType string // 缓存淘汰策略,支持"lru"(最近最少使用)和"fifo"(先进先出)
|
||||||
Compression CompressOptions // 压缩选项
|
Compression CompressOptions // 压缩选项
|
||||||
Monitoring MonitorOptions // 监控选项
|
Monitoring MonitorOptions // 监控选项
|
||||||
|
// 新增选项:是否启用预热
|
||||||
|
Preheating bool // 是否启用缓存预热
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCacheOptions 默认缓存配置
|
// DefaultCacheOptions 默认缓存配置
|
||||||
@@ -24,6 +44,7 @@ var DefaultCacheOptions = CacheOptions{
|
|||||||
EvictionType: "lru", // 默认使用LRU淘汰策略
|
EvictionType: "lru", // 默认使用LRU淘汰策略
|
||||||
Compression: DefaultCompressOptions, // 默认压缩选项
|
Compression: DefaultCompressOptions, // 默认压缩选项
|
||||||
Monitoring: DefaultMonitorOptions, // 默认监控选项
|
Monitoring: DefaultMonitorOptions, // 默认监控选项
|
||||||
|
Preheating: false, // 默认不启用预热
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheKey 缓存键结构
|
// CacheKey 缓存键结构
|
||||||
@@ -34,6 +55,37 @@ type CacheKey struct {
|
|||||||
Part int
|
Part int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String 返回缓存键的字符串表示
|
||||||
|
func (k CacheKey) String() string {
|
||||||
|
// 从对象池获取构建器
|
||||||
|
sb := stringBuilderPool.Get().(*strings.Builder)
|
||||||
|
sb.Reset()
|
||||||
|
|
||||||
|
// 预分配足够容量
|
||||||
|
sb.Grow(64)
|
||||||
|
|
||||||
|
// 构建键字符串
|
||||||
|
sb.WriteString(k.Id)
|
||||||
|
sb.WriteByte('_')
|
||||||
|
if k.SansEnv {
|
||||||
|
sb.WriteString("true")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("false")
|
||||||
|
}
|
||||||
|
sb.WriteByte('_')
|
||||||
|
sb.WriteString(strconv.Itoa(k.Theme))
|
||||||
|
sb.WriteByte('_')
|
||||||
|
sb.WriteString(strconv.Itoa(k.Part))
|
||||||
|
|
||||||
|
// 获取结果
|
||||||
|
result := sb.String()
|
||||||
|
|
||||||
|
// 归还构建器
|
||||||
|
stringBuilderPool.Put(sb)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// CacheItem 缓存项结构
|
// CacheItem 缓存项结构
|
||||||
type CacheItem struct {
|
type CacheItem struct {
|
||||||
SVG string // SVG内容
|
SVG string // SVG内容
|
||||||
@@ -43,6 +95,15 @@ type CacheItem struct {
|
|||||||
LastUsed time.Time // 最后使用时间
|
LastUsed time.Time // 最后使用时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset 重置缓存项以便重用
|
||||||
|
func (item *CacheItem) Reset() {
|
||||||
|
item.SVG = ""
|
||||||
|
item.Compressed = nil
|
||||||
|
item.IsCompressed = false
|
||||||
|
item.CreatedAt = time.Time{}
|
||||||
|
item.LastUsed = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
// PNCache SVG缓存结构
|
// PNCache SVG缓存结构
|
||||||
type PNCache struct {
|
type PNCache struct {
|
||||||
Options CacheOptions
|
Options CacheOptions
|
||||||
@@ -58,7 +119,7 @@ type PNCache struct {
|
|||||||
func NewCache(options CacheOptions) *PNCache {
|
func NewCache(options CacheOptions) *PNCache {
|
||||||
cache := &PNCache{
|
cache := &PNCache{
|
||||||
Options: options,
|
Options: options,
|
||||||
Items: make(map[CacheKey]*list.Element),
|
Items: make(map[CacheKey]*list.Element, options.Size),
|
||||||
EvictionList: list.New(),
|
EvictionList: list.New(),
|
||||||
Hits: 0,
|
Hits: 0,
|
||||||
Misses: 0,
|
Misses: 0,
|
||||||
@@ -103,6 +164,11 @@ func (c *PNCache) Get(key CacheKey) (string, bool) {
|
|||||||
// 删除过期项
|
// 删除过期项
|
||||||
c.EvictionList.Remove(element)
|
c.EvictionList.Remove(element)
|
||||||
delete(c.Items, key)
|
delete(c.Items, key)
|
||||||
|
|
||||||
|
// 将缓存项归还到对象池
|
||||||
|
cacheItem.Reset()
|
||||||
|
cacheItemPool.Put(cacheItem)
|
||||||
|
|
||||||
c.Misses++
|
c.Misses++
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -178,15 +244,15 @@ func (c *PNCache) Set(key CacheKey, svg string) {
|
|||||||
c.evictItem()
|
c.evictItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的缓存项
|
// 从对象池获取新的缓存项
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
cacheItem := &CacheItem{
|
cacheItem := cacheItemPool.Get().(*CacheItem)
|
||||||
SVG: svg,
|
cacheItem.Reset()
|
||||||
Compressed: compressed,
|
cacheItem.SVG = svg
|
||||||
IsCompressed: isCompressed,
|
cacheItem.Compressed = compressed
|
||||||
CreatedAt: now,
|
cacheItem.IsCompressed = isCompressed
|
||||||
LastUsed: now,
|
cacheItem.CreatedAt = now
|
||||||
}
|
cacheItem.LastUsed = now
|
||||||
|
|
||||||
// 添加到链表和映射
|
// 添加到链表和映射
|
||||||
element := c.EvictionList.PushFront(cacheItem)
|
element := c.EvictionList.PushFront(cacheItem)
|
||||||
@@ -214,6 +280,9 @@ func (c *PNCache) evictItem() {
|
|||||||
// 从链表中移除
|
// 从链表中移除
|
||||||
c.EvictionList.Remove(element)
|
c.EvictionList.Remove(element)
|
||||||
|
|
||||||
|
// 获取缓存项并归还到对象池
|
||||||
|
cacheItem := element.Value.(*CacheItem)
|
||||||
|
|
||||||
// 从映射中找到并删除对应的键
|
// 从映射中找到并删除对应的键
|
||||||
for k, v := range c.Items {
|
for k, v := range c.Items {
|
||||||
if v == element {
|
if v == element {
|
||||||
@@ -221,6 +290,10 @@ func (c *PNCache) evictItem() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置并归还缓存项
|
||||||
|
cacheItem.Reset()
|
||||||
|
cacheItemPool.Put(cacheItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +302,14 @@ func (c *PNCache) Clear() {
|
|||||||
c.Mutex.Lock()
|
c.Mutex.Lock()
|
||||||
defer c.Mutex.Unlock()
|
defer c.Mutex.Unlock()
|
||||||
|
|
||||||
c.Items = make(map[CacheKey]*list.Element)
|
// 归还所有缓存项到对象池
|
||||||
|
for e := c.EvictionList.Front(); e != nil; e = e.Next() {
|
||||||
|
item := e.Value.(*CacheItem)
|
||||||
|
item.Reset()
|
||||||
|
cacheItemPool.Put(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Items = make(map[CacheKey]*list.Element, c.Options.Size)
|
||||||
c.EvictionList = list.New()
|
c.EvictionList = list.New()
|
||||||
c.Hits = 0
|
c.Hits = 0
|
||||||
c.Misses = 0
|
c.Misses = 0
|
||||||
@@ -277,6 +357,9 @@ func (c *PNCache) RemoveExpired() int {
|
|||||||
c.EvictionList.Remove(element)
|
c.EvictionList.Remove(element)
|
||||||
// 从映射中删除
|
// 从映射中删除
|
||||||
delete(c.Items, key)
|
delete(c.Items, key)
|
||||||
|
// 归还缓存项到对象池
|
||||||
|
cacheItem.Reset()
|
||||||
|
cacheItemPool.Put(cacheItem)
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,11 +440,18 @@ func (c *PNCache) DeleteItem(key CacheKey) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取缓存项
|
||||||
|
cacheItem := element.Value.(*CacheItem)
|
||||||
|
|
||||||
// 从链表中移除
|
// 从链表中移除
|
||||||
c.EvictionList.Remove(element)
|
c.EvictionList.Remove(element)
|
||||||
|
|
||||||
// 从映射中删除
|
// 从映射中删除
|
||||||
delete(c.Items, key)
|
delete(c.Items, key)
|
||||||
|
|
||||||
|
// 重置并归还缓存项
|
||||||
|
cacheItem.Reset()
|
||||||
|
cacheItemPool.Put(cacheItem)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
219
cache/compress.go
vendored
219
cache/compress.go
vendored
@@ -5,60 +5,95 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompressOptions 压缩选项
|
var (
|
||||||
|
// 字符串构建器对象池
|
||||||
|
optimizeBuilderPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(strings.Builder)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gzip Writer对象池
|
||||||
|
gzipWriterPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
writer, _ := gzip.NewWriterLevel(nil, gzip.BestCompression)
|
||||||
|
return writer
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gzip Reader对象池
|
||||||
|
gzipReaderPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(gzip.Reader)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字节缓冲区对象池
|
||||||
|
bytesBufferPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompressOptions 压缩配置选项
|
||||||
type CompressOptions struct {
|
type CompressOptions struct {
|
||||||
Enabled bool // 是否启用压缩
|
Enabled bool // 是否启用压缩
|
||||||
Level int // 压缩级别 (1-9),1为最快压缩,9为最佳压缩
|
Level int // 压缩级别,范围从-2(不压缩)到9(最高压缩)
|
||||||
MinSizeBytes int // 最小压缩大小,小于此大小的数据不进行压缩
|
MinSize int // 最小压缩大小,小于此大小的SVG不压缩
|
||||||
Ratio float64 // 压缩比阈值,压缩后大小/原始大小,小于此值才保存压缩结果
|
CompressionRatio float64 // 最小压缩比率,压缩后大小/原始大小,小于此比率才使用压缩结果
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCompressOptions 默认压缩选项
|
// DefaultCompressOptions 默认压缩配置
|
||||||
var DefaultCompressOptions = CompressOptions{
|
var DefaultCompressOptions = CompressOptions{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Level: 6, // 默认压缩级别为6,平衡压缩率和性能
|
Level: 9, // 默认使用最高压缩级别
|
||||||
MinSizeBytes: 100, // 默认最小压缩大小为100字节
|
MinSize: 1024, // 默认最小压缩大小为1KB
|
||||||
Ratio: 0.9, // 默认压缩比阈值为0.9,即至少要压缩到原始大小的90%以下才保存压缩结果
|
CompressionRatio: 0.8, // 默认最小压缩比率为0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompressSVG 压缩SVG数据
|
// CompressSVG 压缩SVG字符串
|
||||||
// 返回压缩后的数据和是否进行了压缩
|
|
||||||
func CompressSVG(svg string, options CompressOptions) ([]byte, bool) {
|
func CompressSVG(svg string, options CompressOptions) ([]byte, bool) {
|
||||||
if !options.Enabled || len(svg) < options.MinSizeBytes {
|
if !options.Enabled {
|
||||||
return []byte(svg), false
|
return []byte(svg), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个bytes.Buffer来存储压缩数据
|
// 如果SVG太小,不进行压缩
|
||||||
var buf bytes.Buffer
|
if len(svg) < options.MinSize {
|
||||||
|
|
||||||
// 创建一个gzip.Writer,设置压缩级别
|
|
||||||
writer, err := gzip.NewWriterLevel(&buf, options.Level)
|
|
||||||
if err != nil {
|
|
||||||
return []byte(svg), false
|
return []byte(svg), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入SVG数据
|
// 从对象池获取缓冲区
|
||||||
_, err = writer.Write([]byte(svg))
|
buf := bytesBufferPool.Get().(*bytes.Buffer)
|
||||||
if err != nil {
|
buf.Reset()
|
||||||
|
defer bytesBufferPool.Put(buf)
|
||||||
|
|
||||||
|
// 从对象池获取gzip写入器
|
||||||
|
writer := gzipWriterPool.Get().(*gzip.Writer)
|
||||||
|
writer.Reset(buf)
|
||||||
|
defer gzipWriterPool.Put(writer)
|
||||||
|
|
||||||
|
// 写入数据
|
||||||
|
if _, err := writer.Write([]byte(svg)); err != nil {
|
||||||
return []byte(svg), false
|
return []byte(svg), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭writer,确保所有数据都被写入
|
// 关闭写入器
|
||||||
err = writer.Close()
|
if err := writer.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return []byte(svg), false
|
return []byte(svg), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取压缩后的数据
|
// 获取压缩后的数据
|
||||||
compressed := buf.Bytes()
|
compressed := make([]byte, buf.Len())
|
||||||
|
copy(compressed, buf.Bytes())
|
||||||
|
|
||||||
// 计算压缩比
|
// 计算压缩比率
|
||||||
ratio := float64(len(compressed)) / float64(len(svg))
|
ratio := float64(len(compressed)) / float64(len(svg))
|
||||||
|
if ratio > options.CompressionRatio {
|
||||||
// 如果压缩比不理想,返回原始数据
|
// 压缩效果不好,返回原始数据
|
||||||
if ratio >= options.Ratio {
|
|
||||||
return []byte(svg), false
|
return []byte(svg), false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +112,12 @@ func DecompressSVG(data []byte, isCompressed bool) (string, error) {
|
|||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个gzip.Reader
|
// 获取缓冲区
|
||||||
|
buf := bytesBufferPool.Get().(*bytes.Buffer)
|
||||||
|
buf.Reset()
|
||||||
|
defer bytesBufferPool.Put(buf)
|
||||||
|
|
||||||
|
// 创建Reader
|
||||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return string(data), err
|
return string(data), err
|
||||||
@@ -85,12 +125,11 @@ func DecompressSVG(data []byte, isCompressed bool) (string, error) {
|
|||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
// 读取解压缩后的数据
|
// 读取解压缩后的数据
|
||||||
decompressed, err := io.ReadAll(reader)
|
if _, err := io.Copy(buf, reader); err != nil {
|
||||||
if err != nil {
|
|
||||||
return string(data), err
|
return string(data), err
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(decompressed), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isGzipped 检查数据是否为gzip格式
|
// isGzipped 检查数据是否为gzip格式
|
||||||
@@ -101,6 +140,11 @@ func isGzipped(data []byte) bool {
|
|||||||
|
|
||||||
// OptimizeSVG 优化SVG字符串,移除不必要的空白和注释
|
// OptimizeSVG 优化SVG字符串,移除不必要的空白和注释
|
||||||
func OptimizeSVG(svg string) string {
|
func OptimizeSVG(svg string) string {
|
||||||
|
// 如果SVG太小,不进行优化
|
||||||
|
if len(svg) < 100 {
|
||||||
|
return svg
|
||||||
|
}
|
||||||
|
|
||||||
// 移除XML注释
|
// 移除XML注释
|
||||||
svg = removeXMLComments(svg)
|
svg = removeXMLComments(svg)
|
||||||
|
|
||||||
@@ -110,33 +154,100 @@ func OptimizeSVG(svg string) string {
|
|||||||
return svg
|
return svg
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeXMLComments 移除XML注释
|
// removeXMLComments 移除XML注释 - 优化版本
|
||||||
func removeXMLComments(svg string) string {
|
func removeXMLComments(svg string) string {
|
||||||
for {
|
// 获取构建器
|
||||||
start := strings.Index(svg, "<!--")
|
sb := optimizeBuilderPool.Get().(*strings.Builder)
|
||||||
if start == -1 {
|
sb.Reset()
|
||||||
break
|
sb.Grow(len(svg))
|
||||||
}
|
defer optimizeBuilderPool.Put(sb)
|
||||||
end := strings.Index(svg[start:], "-->") + start
|
|
||||||
if end > start {
|
for i := 0; i < len(svg); {
|
||||||
svg = svg[:start] + svg[end+3:]
|
// 检查是否遇到注释开始
|
||||||
|
if i+3 < len(svg) && svg[i] == '<' && svg[i+1] == '!' && svg[i+2] == '-' && svg[i+3] == '-' {
|
||||||
|
// 查找注释结束
|
||||||
|
end := i + 4
|
||||||
|
for end+2 <= len(svg) {
|
||||||
|
if svg[end] == '-' && svg[end+1] == '-' && svg[end+2] == '>' {
|
||||||
|
end += 3
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
// 跳过注释
|
||||||
|
i = end
|
||||||
} else {
|
} else {
|
||||||
break
|
// 添加非注释字符
|
||||||
|
sb.WriteByte(svg[i])
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return svg
|
|
||||||
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeExtraWhitespace 移除多余的空白
|
// removeExtraWhitespace 移除多余的空白 - 优化版本
|
||||||
func removeExtraWhitespace(svg string) string {
|
func removeExtraWhitespace(svg string) string {
|
||||||
// 替换多个空白字符为单个空格
|
// 获取构建器
|
||||||
svg = strings.Join(strings.Fields(svg), " ")
|
sb := optimizeBuilderPool.Get().(*strings.Builder)
|
||||||
|
sb.Reset()
|
||||||
|
sb.Grow(len(svg))
|
||||||
|
defer optimizeBuilderPool.Put(sb)
|
||||||
|
|
||||||
// 优化常见的SVG标签周围的空白
|
inTag := false
|
||||||
svg = strings.ReplaceAll(svg, "> <", "><")
|
inQuote := false
|
||||||
svg = strings.ReplaceAll(svg, " />", "/>")
|
quoteChar := byte(0)
|
||||||
svg = strings.ReplaceAll(svg, " =", "=")
|
lastWasSpace := false
|
||||||
svg = strings.ReplaceAll(svg, "= ", "=")
|
|
||||||
|
|
||||||
return svg
|
for i := 0; i < len(svg); i++ {
|
||||||
|
c := svg[i]
|
||||||
|
|
||||||
|
// 处理引号内的内容
|
||||||
|
if inQuote {
|
||||||
|
sb.WriteByte(c)
|
||||||
|
if c == quoteChar {
|
||||||
|
inQuote = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签
|
||||||
|
if c == '<' {
|
||||||
|
inTag = true
|
||||||
|
lastWasSpace = false
|
||||||
|
sb.WriteByte(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '>' {
|
||||||
|
inTag = false
|
||||||
|
lastWasSpace = false
|
||||||
|
sb.WriteByte(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理引号开始
|
||||||
|
if (c == '"' || c == '\'') && inTag {
|
||||||
|
inQuote = true
|
||||||
|
quoteChar = c
|
||||||
|
sb.WriteByte(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理空白字符
|
||||||
|
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
||||||
|
// 压缩连续空白
|
||||||
|
if !lastWasSpace && (inTag || i > 0 && svg[i-1] != '>') {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
lastWasSpace = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理普通字符
|
||||||
|
lastWasSpace = false
|
||||||
|
sb.WriteByte(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
180
pixelnebula.go
180
pixelnebula.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/landaiqing/go-pixelnebula/animation"
|
"github.com/landaiqing/go-pixelnebula/animation"
|
||||||
@@ -22,7 +23,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
hashLength = 12
|
hashLength = 12
|
||||||
keyFactor = 0.47
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -30,6 +30,32 @@ var (
|
|||||||
numberRegex = regexp.MustCompile(`[0-9]`)
|
numberRegex = regexp.MustCompile(`[0-9]`)
|
||||||
// 使用非贪婪模式并优化颜色匹配模式
|
// 使用非贪婪模式并优化颜色匹配模式
|
||||||
colorRegex = regexp.MustCompile(`#([^;]*);`)
|
colorRegex = regexp.MustCompile(`#([^;]*);`)
|
||||||
|
// 使用字节池减少内存分配
|
||||||
|
hashBufPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
return &buf
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// 缓存键计算结果缓存
|
||||||
|
keyCache = make(map[string][2]int)
|
||||||
|
keyCacheMux sync.RWMutex
|
||||||
|
// 使用对象池减少内存分配
|
||||||
|
builderPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &strings.Builder{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mapPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make(map[string]string, 6)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
keyMapPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make(map[string][2]int, 6)
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type PNOptions struct {
|
type PNOptions struct {
|
||||||
@@ -119,15 +145,16 @@ func (pn *PixelNebula) hashToNum(hash []string) int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将哈希字符串数组连接成一个字符串
|
// 使用位运算直接计算哈希值,减少内存分配和计算复杂度
|
||||||
var result int64
|
var result int64
|
||||||
for _, h := range hash {
|
for _, h := range hash {
|
||||||
num, err := strconv.ParseInt(h, 10, 64)
|
// 优化:直接处理字符,避免ParseInt的开销
|
||||||
if err != nil {
|
for i := 0; i < len(h); i++ {
|
||||||
continue
|
if h[i] >= '0' && h[i] <= '9' {
|
||||||
|
// 使用位运算进行计算: result = result*10 + (h[i] - '0')
|
||||||
|
result = (result << 3) + (result << 1) + int64(h[i]-'0')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 使用位运算和加法组合多个数字
|
|
||||||
result = (result << 3) + (result << 1) + num // result * 8 + result * 2 + num
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保结果为正数
|
// 确保结果为正数
|
||||||
@@ -138,14 +165,37 @@ func (pn *PixelNebula) hashToNum(hash []string) int64 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCacheKey 生成缓存键的哈希表示
|
||||||
|
func (pn *PixelNebula) getCacheKey(id string, hash []string, index int, opts *PNOptions) string {
|
||||||
|
// 构造一个唯一的键字符串
|
||||||
|
var key string
|
||||||
|
if opts != nil && opts.StyleIndex >= 0 && opts.ThemeIndex >= 0 {
|
||||||
|
key = fmt.Sprintf("%s_%d_%d_%d", id, index, opts.StyleIndex, opts.ThemeIndex)
|
||||||
|
} else if len(hash) > 0 {
|
||||||
|
key = fmt.Sprintf("%s_%d_%s", id, index, strings.Join(hash, ""))
|
||||||
|
} else {
|
||||||
|
key = fmt.Sprintf("%s_%d", id, index)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
// calcKey 计算主题和部分的键值
|
// calcKey 计算主题和部分的键值
|
||||||
func (pn *PixelNebula) calcKey(hash []string, opts *PNOptions) [2]int {
|
func (pn *PixelNebula) calcKey(hash []string, opts *PNOptions) [2]int {
|
||||||
// 只有当明确设置了主题和风格索引时才使用固定值
|
// 检查是否使用固定值
|
||||||
if opts != nil && opts.StyleIndex >= 0 && opts.ThemeIndex >= 0 {
|
if opts != nil && opts.StyleIndex >= 0 && opts.ThemeIndex >= 0 {
|
||||||
return [2]int{opts.StyleIndex, opts.ThemeIndex}
|
return [2]int{opts.StyleIndex, opts.ThemeIndex}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接使用哈希值,不进行 keyFactor 转换
|
// 尝试从缓存中获取结果
|
||||||
|
cacheKey := strings.Join(hash, "")
|
||||||
|
keyCacheMux.RLock()
|
||||||
|
if result, ok := keyCache[cacheKey]; ok {
|
||||||
|
keyCacheMux.RUnlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
keyCacheMux.RUnlock()
|
||||||
|
|
||||||
|
// 计算哈希值
|
||||||
hashNum := pn.hashToNum(hash)
|
hashNum := pn.hashToNum(hash)
|
||||||
|
|
||||||
// 获取可用的风格数量
|
// 获取可用的风格数量
|
||||||
@@ -154,14 +204,11 @@ func (pn *PixelNebula) calcKey(hash []string, opts *PNOptions) [2]int {
|
|||||||
return [2]int{0, 0}
|
return [2]int{0, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用哈希值计算风格索引
|
// 使用位运算优化取模操作
|
||||||
styleIndex := int(hashNum % int64(styleCount))
|
styleIndex := int(hashNum % int64(styleCount))
|
||||||
if styleIndex < 0 {
|
if styleIndex < 0 {
|
||||||
styleIndex = -styleIndex
|
styleIndex = -styleIndex
|
||||||
}
|
}
|
||||||
if styleIndex >= styleCount {
|
|
||||||
styleIndex = styleCount - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取该风格下的主题数量
|
// 获取该风格下的主题数量
|
||||||
themeCount := pn.themeManager.ThemeCount(styleIndex)
|
themeCount := pn.themeManager.ThemeCount(styleIndex)
|
||||||
@@ -170,16 +217,18 @@ func (pn *PixelNebula) calcKey(hash []string, opts *PNOptions) [2]int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用哈希值的不同部分计算主题索引
|
// 使用哈希值的不同部分计算主题索引
|
||||||
// 使用更简单的计算方式
|
|
||||||
themeIndex := int(hashNum % int64(themeCount))
|
themeIndex := int(hashNum % int64(themeCount))
|
||||||
if themeIndex < 0 {
|
if themeIndex < 0 {
|
||||||
themeIndex = -themeIndex
|
themeIndex = -themeIndex
|
||||||
}
|
}
|
||||||
if themeIndex >= themeCount {
|
|
||||||
themeIndex = themeCount - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return [2]int{styleIndex, themeIndex}
|
// 将结果存入缓存
|
||||||
|
result := [2]int{styleIndex, themeIndex}
|
||||||
|
keyCacheMux.Lock()
|
||||||
|
keyCache[cacheKey] = result
|
||||||
|
keyCacheMux.Unlock()
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCache 设置缓存选项
|
// WithCache 设置缓存选项
|
||||||
@@ -584,10 +633,14 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算avatarId的哈希值
|
// 使用对象池获取缓冲区
|
||||||
|
hashBuf := hashBufPool.Get().(*[]byte)
|
||||||
|
defer hashBufPool.Put(hashBuf)
|
||||||
|
|
||||||
|
// 计算avatarId的哈希值 - 优化版本
|
||||||
pn.hasher.Reset()
|
pn.hasher.Reset()
|
||||||
pn.hasher.Write([]byte(id))
|
pn.hasher.Write([]byte(id))
|
||||||
sum := pn.hasher.Sum(nil)
|
sum := pn.hasher.Sum((*hashBuf)[:0])
|
||||||
s := hex.EncodeToString(sum)
|
s := hex.EncodeToString(sum)
|
||||||
hashStr := numberRegex.FindAllString(s, -1)
|
hashStr := numberRegex.FindAllString(s, -1)
|
||||||
if len(hashStr) < hashLength {
|
if len(hashStr) < hashLength {
|
||||||
@@ -595,9 +648,17 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
}
|
}
|
||||||
hashStr = hashStr[0:hashLength]
|
hashStr = hashStr[0:hashLength]
|
||||||
|
|
||||||
// 预分配map容量以提高性能
|
// 从对象池获取映射
|
||||||
var p = make(map[string][2]int, 6)
|
p := keyMapPool.Get().(map[string][2]int)
|
||||||
|
defer func() {
|
||||||
|
// 清空并归还对象池
|
||||||
|
for k := range p {
|
||||||
|
delete(p, k)
|
||||||
|
}
|
||||||
|
keyMapPool.Put(p)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 计算各部分的键值
|
||||||
p[string(style.TypeEnv)] = pn.calcKey(hashStr[:2], opts)
|
p[string(style.TypeEnv)] = pn.calcKey(hashStr[:2], opts)
|
||||||
p[string(style.TypeClo)] = pn.calcKey(hashStr[2:4], opts)
|
p[string(style.TypeClo)] = pn.calcKey(hashStr[2:4], opts)
|
||||||
p[string(style.TypeHead)] = pn.calcKey(hashStr[4:6], opts)
|
p[string(style.TypeHead)] = pn.calcKey(hashStr[4:6], opts)
|
||||||
@@ -605,8 +666,17 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
p[string(style.TypeEyes)] = pn.calcKey(hashStr[8:10], opts)
|
p[string(style.TypeEyes)] = pn.calcKey(hashStr[8:10], opts)
|
||||||
p[string(style.TypeTop)] = pn.calcKey(hashStr[10:], opts)
|
p[string(style.TypeTop)] = pn.calcKey(hashStr[10:], opts)
|
||||||
|
|
||||||
// 预分配map容量
|
// 获取结果映射
|
||||||
var final = make(map[string]string, 6)
|
final := mapPool.Get().(map[string]string)
|
||||||
|
defer func() {
|
||||||
|
// 清空并归还对象池
|
||||||
|
for k := range final {
|
||||||
|
delete(final, k)
|
||||||
|
}
|
||||||
|
mapPool.Put(final)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 获取并处理各部分的SVG
|
||||||
for k, v := range p {
|
for k, v := range p {
|
||||||
// 获取主题颜色
|
// 获取主题颜色
|
||||||
themePart, err := pn.themeManager.GetTheme(v[0], v[1])
|
themePart, err := pn.themeManager.GetTheme(v[0], v[1])
|
||||||
@@ -627,8 +697,10 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
}
|
}
|
||||||
|
|
||||||
match := colorRegex.FindAllStringSubmatch(svgPart, -1)
|
match := colorRegex.FindAllStringSubmatch(svgPart, -1)
|
||||||
// 使用strings.Builder提高字符串处理性能
|
|
||||||
var sb strings.Builder
|
// 从对象池获取Builder
|
||||||
|
sb := builderPool.Get().(*strings.Builder)
|
||||||
|
sb.Reset()
|
||||||
sb.Grow(len(svgPart) + 50) // 预分配足够的容量
|
sb.Grow(len(svgPart) + 50) // 预分配足够的容量
|
||||||
|
|
||||||
lastIndex := 0
|
lastIndex := 0
|
||||||
@@ -653,14 +725,21 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
}
|
}
|
||||||
// 添加剩余部分
|
// 添加剩余部分
|
||||||
sb.WriteString(svgPart[lastIndex:])
|
sb.WriteString(svgPart[lastIndex:])
|
||||||
|
|
||||||
final[k] = sb.String()
|
final[k] = sb.String()
|
||||||
|
|
||||||
|
// 归还Builder到对象池
|
||||||
|
builderPool.Put(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用strings.Builder构建最终SVG
|
// 使用对象池获取主Builder来构建最终SVG
|
||||||
var builder strings.Builder
|
builder := builderPool.Get().(*strings.Builder)
|
||||||
|
builder.Reset()
|
||||||
// 预估SVG大小,避免多次内存分配
|
// 预估SVG大小,避免多次内存分配
|
||||||
builder.Grow(1024 * 2)
|
builder.Grow(2048) // 2KB 应该足够容纳大多数SVG
|
||||||
builder.WriteString(pn.getSvgStart()) // 使用动态生成的svgStart
|
|
||||||
|
// 添加SVG开始标签
|
||||||
|
builder.WriteString(pn.getSvgStart())
|
||||||
|
|
||||||
// 获取动画定义
|
// 获取动画定义
|
||||||
animations := pn.animManager.GenerateSVGAnimations()
|
animations := pn.animManager.GenerateSVGAnimations()
|
||||||
@@ -668,32 +747,31 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
builder.WriteString(animations)
|
builder.WriteString(animations)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有旋转动画并获取旋转动画的SVG代码
|
// 构建和处理旋转动画 - 使用对象池
|
||||||
rotateAnimations := make(map[string]bool)
|
rotateAnimations := make(map[string]bool)
|
||||||
rotateAnimationSVGs := make(map[string]string)
|
rotateAnimationSVGs := make(map[string]string)
|
||||||
|
|
||||||
|
// 收集旋转动画
|
||||||
for _, anim := range pn.animManager.GetAnimations() {
|
for _, anim := range pn.animManager.GetAnimations() {
|
||||||
// 检查是否为旋转动画
|
|
||||||
if rotateAnim, ok := anim.(*animation.RotateAnimation); ok {
|
if rotateAnim, ok := anim.(*animation.RotateAnimation); ok {
|
||||||
rotateAnimations[anim.GetTargetID()] = true
|
targetID := anim.GetTargetID()
|
||||||
// 获取旋转动画的SVG代码(只提取animateTransform部分)
|
rotateAnimations[targetID] = true
|
||||||
|
|
||||||
|
// 提取animateTransform部分
|
||||||
svgCode := rotateAnim.GenerateSVG()
|
svgCode := rotateAnim.GenerateSVG()
|
||||||
// 提取animateTransform标签
|
|
||||||
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
|
if start := strings.Index(svgCode, "<animateTransform"); start != -1 {
|
||||||
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
|
if end := strings.Index(svgCode[start:], "/>"); end != -1 {
|
||||||
rotateAnimationSVGs[anim.GetTargetID()] = svgCode[start : start+end+2]
|
rotateAnimationSVGs[targetID] = svgCode[start : start+end+2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理元素,如果元素有旋转动画,则包裹在g标签中并添加animateTransform
|
// 处理环境
|
||||||
// 只有当不是无环境模式时才添加环境
|
|
||||||
if !sansEnv {
|
if !sansEnv {
|
||||||
if _, hasRotate := rotateAnimations["env"]; hasRotate {
|
if _, hasRotate := rotateAnimations["env"]; hasRotate {
|
||||||
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
|
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
|
||||||
builder.WriteString(final["env"])
|
builder.WriteString(final["env"])
|
||||||
// 添加animateTransform标签
|
|
||||||
if animSVG, ok := rotateAnimationSVGs["env"]; ok {
|
if animSVG, ok := rotateAnimationSVGs["env"]; ok {
|
||||||
builder.WriteString(animSVG)
|
builder.WriteString(animSVG)
|
||||||
}
|
}
|
||||||
@@ -705,34 +783,28 @@ func (pn *PixelNebula) generateSVG(id string, sansEnv bool, opts *PNOptions) (sv
|
|||||||
|
|
||||||
// 处理其他元素
|
// 处理其他元素
|
||||||
elements := []string{"head", "clo", "top", "eyes", "mouth"}
|
elements := []string{"head", "clo", "top", "eyes", "mouth"}
|
||||||
|
|
||||||
// 单独处理每个元素
|
|
||||||
for _, elem := range elements {
|
for _, elem := range elements {
|
||||||
if _, hasRotate := rotateAnimations[elem]; hasRotate {
|
if _, hasRotate := rotateAnimations[elem]; hasRotate {
|
||||||
// 如果元素有旋转动画,则包裹在g标签中
|
|
||||||
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
|
builder.WriteString("<g style=\"transform-box: fill-box; transform-origin: center;\">\n")
|
||||||
builder.WriteString(final[elem])
|
builder.WriteString(final[elem])
|
||||||
|
|
||||||
// 添加animateTransform标签
|
|
||||||
if animSVG, ok := rotateAnimationSVGs[elem]; ok {
|
if animSVG, ok := rotateAnimationSVGs[elem]; ok {
|
||||||
// 提取animateTransform标签部分
|
builder.WriteString(animSVG)
|
||||||
if start := strings.Index(animSVG, "<animateTransform"); start != -1 {
|
|
||||||
if end := strings.Index(animSVG[start:], "/>"); end != -1 {
|
|
||||||
builder.WriteString(animSVG[start : start+end+2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
builder.WriteString("</g>\n")
|
builder.WriteString("</g>\n")
|
||||||
} else {
|
} else {
|
||||||
// 如果元素没有旋转动画,直接添加
|
|
||||||
builder.WriteString(final[elem])
|
builder.WriteString(final[elem])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.WriteString(pn.svgEnd)
|
builder.WriteString(pn.svgEnd)
|
||||||
svg = builder.String()
|
svg = builder.String()
|
||||||
|
|
||||||
|
// 将生成的SVG存储到实例中
|
||||||
pn.imgData = []byte(svg)
|
pn.imgData = []byte(svg)
|
||||||
|
|
||||||
|
// 归还Builder到对象池
|
||||||
|
builderPool.Put(builder)
|
||||||
|
|
||||||
// 如果启用了缓存,将结果存入缓存
|
// 如果启用了缓存,将结果存入缓存
|
||||||
if pn.cache != nil {
|
if pn.cache != nil {
|
||||||
cacheKey := cache.CacheKey{
|
cacheKey := cache.CacheKey{
|
||||||
@@ -881,7 +953,7 @@ func (pn *PixelNebula) DeleteCacheItem(key string) bool {
|
|||||||
id := parts[0]
|
id := parts[0]
|
||||||
sansEnv := parts[1] == "true"
|
sansEnv := parts[1] == "true"
|
||||||
|
|
||||||
theme, err := strconv.Atoi(parts[2])
|
themeItem, err := strconv.Atoi(parts[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("pixelnebula: 解析theme失败: %v", err)
|
log.Printf("pixelnebula: 解析theme失败: %v", err)
|
||||||
return false
|
return false
|
||||||
@@ -897,7 +969,7 @@ func (pn *PixelNebula) DeleteCacheItem(key string) bool {
|
|||||||
cacheKey := cache.CacheKey{
|
cacheKey := cache.CacheKey{
|
||||||
Id: id,
|
Id: id,
|
||||||
SansEnv: sansEnv,
|
SansEnv: sansEnv,
|
||||||
Theme: theme,
|
Theme: themeItem,
|
||||||
Part: part,
|
Part: part,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -144,7 +144,7 @@ func TestDemo(t *testing.T) {
|
|||||||
pn := NewPixelNebula()
|
pn := NewPixelNebula()
|
||||||
|
|
||||||
// 设置风格和尺寸
|
// 设置风格和尺寸
|
||||||
pn.WithStyle(style.MechStyle)
|
pn.WithStyle(style.MetaStyle)
|
||||||
pn.WithTheme(1)
|
pn.WithTheme(1)
|
||||||
pn.WithSize(231, 231)
|
pn.WithSize(231, 231)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user