-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathbatch.go
More file actions
166 lines (150 loc) · 3.81 KB
/
Copy pathbatch.go
File metadata and controls
166 lines (150 loc) · 3.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package fennec
import (
"context"
"fmt"
"runtime"
"sync"
)
// BatchItem represents one file to compress in a batch operation.
type BatchItem struct {
// Src is the input file path.
Src string
// Dst is the output file path.
Dst string
// Opts are the per-item compression options. If nil, BatchOptions.DefaultOpts is used.
Opts *Options
}
// BatchResult holds the result for a single item in a batch.
type BatchResult struct {
// Item is the original batch item.
Item BatchItem
// Result is the compression result (nil if Err is non-nil).
Result *Result
// Err is any error that occurred.
Err error
// Index is the position in the original input slice.
Index int
}
// BatchOptions configures batch compression behavior.
type BatchOptions struct {
// Workers is the number of concurrent workers. 0 = runtime.NumCPU().
Workers int
// DefaultOpts is used for any BatchItem where Opts is nil.
DefaultOpts Options
// OnItem is called after each item completes (for progress reporting).
// It receives the item index and total count.
OnItem func(completed, total int)
}
// CompressBatch compresses multiple image files concurrently using a worker pool.
// Results are returned in the same order as the input items.
// The context can be used to cancel the entire batch \u2014 in-flight items will
// finish but no new items will be started.
//
// Example:
//
// items := []fennec.BatchItem{
// {Src: "photo1.jpg", Dst: "out1.jpg"},
// {Src: "photo2.png", Dst: "out2.jpg"},
// }
// results := fennec.CompressBatch(ctx, items, fennec.BatchOptions{
// Workers: 4,
// DefaultOpts: fennec.DefaultOptions(),
// })
func CompressBatch(ctx context.Context, items []BatchItem, batchOpts BatchOptions) []BatchResult {
if len(items) == 0 {
return nil
}
workers := batchOpts.Workers
if workers <= 0 {
workers = runtime.NumCPU()
}
if workers > len(items) {
workers = len(items)
}
results := make([]BatchResult, len(items))
workCh := make(chan int, len(items))
var wg sync.WaitGroup
var completed int
var completedMu sync.Mutex
// Feed work.
for i := range items {
workCh <- i
}
close(workCh)
// Start workers.
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for idx := range workCh {
// Check cancellation before starting new work.
select {
case <-ctx.Done():
results[idx] = BatchResult{
Item: items[idx],
Err: ctx.Err(),
Index: idx,
}
continue
default:
}
item := items[idx]
opts := batchOpts.DefaultOpts
if item.Opts != nil {
opts = *item.Opts
}
result, err := CompressFile(ctx, item.Src, item.Dst, opts)
results[idx] = BatchResult{
Item: item,
Result: result,
Err: err,
Index: idx,
}
if batchOpts.OnItem != nil {
completedMu.Lock()
completed++
c := completed
completedMu.Unlock()
batchOpts.OnItem(c, len(items))
}
}
}()
}
wg.Wait()
return results
}
// BatchSummary provides aggregate statistics for a batch operation.
type BatchSummary struct {
Total int
Succeeded int
Failed int
TotalSaved int64
AvgSSIM float64
}
// Summarize computes aggregate statistics from batch results.
func Summarize(results []BatchResult) BatchSummary {
s := BatchSummary{Total: len(results)}
var ssimSum float64
for _, r := range results {
if r.Err != nil {
s.Failed++
continue
}
s.Succeeded++
if r.Result != nil {
s.TotalSaved += r.Result.OriginalSize - r.Result.CompressedSize
ssimSum += r.Result.SSIM
}
}
if s.Succeeded > 0 {
s.AvgSSIM = ssimSum / float64(s.Succeeded)
}
return s
}
// String returns a human-readable batch summary.
func (s BatchSummary) String() string {
return fmt.Sprintf(
"Batch: %d/%d succeeded | %s saved | Avg SSIM: %.4f",
s.Succeeded, s.Total, humanBytes(s.TotalSaved), s.AvgSSIM,
)
}