Optimize binary size and thumbnail performance
This commit is contained in:
@@ -4,6 +4,10 @@ COMPANION_WIN := companion/datascape-companion-windows-amd64.exe
|
|||||||
COMPANION_LIN := companion/datascape-companion-linux-amd64
|
COMPANION_LIN := companion/datascape-companion-linux-amd64
|
||||||
COMPANION_SRCS := $(wildcard cmd/companion/*.go) $(wildcard cmd/companion/*.html) go.mod go.sum
|
COMPANION_SRCS := $(wildcard cmd/companion/*.go) $(wildcard cmd/companion/*.html) go.mod go.sum
|
||||||
|
|
||||||
|
# Release builds drop the symbol table, DWARF debug info and local source
|
||||||
|
# paths. Panics still print file:line stack traces; only debuggers lose out.
|
||||||
|
RELEASE_FLAGS := -trimpath -ldflags="-s -w"
|
||||||
|
|
||||||
EDITOR_BUNDLE := assets/editor/vendor/codemirror.bundle.js
|
EDITOR_BUNDLE := assets/editor/vendor/codemirror.bundle.js
|
||||||
EDITOR_SRCS := $(wildcard editor-build/*.js) editor-build/package.json editor-build/package-lock.json
|
EDITOR_SRCS := $(wildcard editor-build/*.js) editor-build/package.json editor-build/package-lock.json
|
||||||
|
|
||||||
@@ -14,10 +18,10 @@ EDITOR_SRCS := $(wildcard editor-build/*.js) editor-build/package.json editor-bu
|
|||||||
companion-release: $(COMPANION_WIN) $(COMPANION_LIN)
|
companion-release: $(COMPANION_WIN) $(COMPANION_LIN)
|
||||||
|
|
||||||
$(COMPANION_WIN): $(COMPANION_SRCS)
|
$(COMPANION_WIN): $(COMPANION_SRCS)
|
||||||
GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" -o $@ ./cmd/companion
|
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -H windowsgui" -o $@ ./cmd/companion
|
||||||
|
|
||||||
$(COMPANION_LIN): $(COMPANION_SRCS)
|
$(COMPANION_LIN): $(COMPANION_SRCS)
|
||||||
GOOS=linux GOARCH=amd64 go build -o $@ ./cmd/companion
|
GOOS=linux GOARCH=amd64 go build $(RELEASE_FLAGS) -o $@ ./cmd/companion
|
||||||
|
|
||||||
companion-windows: $(COMPANION_WIN)
|
companion-windows: $(COMPANION_WIN)
|
||||||
companion-linux: $(COMPANION_LIN)
|
companion-linux: $(COMPANION_LIN)
|
||||||
@@ -37,7 +41,7 @@ $(EDITOR_BUNDLE): $(EDITOR_SRCS)
|
|||||||
cd editor-build && npm ci && npm run build
|
cd editor-build && npm ci && npm run build
|
||||||
|
|
||||||
deploy: companion-release
|
deploy: companion-release
|
||||||
GOOS=linux GOARCH=arm GOARM=7 go build -o datascape-arm .
|
GOOS=linux GOARCH=arm GOARM=7 go build $(RELEASE_FLAGS) -o datascape-arm .
|
||||||
ssh $(NAS) 'kill $$(cat /share/homes/luxick/.local/bin/datascape.pid) 2>/dev/null; rm -f /share/homes/luxick/.local/bin/datascape.pid'
|
ssh $(NAS) 'kill $$(cat /share/homes/luxick/.local/bin/datascape.pid) 2>/dev/null; rm -f /share/homes/luxick/.local/bin/datascape.pid'
|
||||||
scp datascape-arm $(NAS):/share/homes/luxick/.local/bin/datascape
|
scp datascape-arm $(NAS):/share/homes/luxick/.local/bin/datascape
|
||||||
ssh $(NAS) /share/homes/luxick/.local/bin/datascape-start.sh
|
ssh $(NAS) /share/homes/luxick/.local/bin/datascape-start.sh
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ The wiki binary embeds the companion binaries, so build those first:
|
|||||||
```bash
|
```bash
|
||||||
make companion-release # once, and after companion changes
|
make companion-release # once, and after companion changes
|
||||||
go build -o datascape . # local
|
go build -o datascape . # local
|
||||||
GOOS=linux GOARCH=arm go build -o datascape . # QNAP NAS
|
make deploy # QNAP NAS: stripped ARMv7 build, copy, restart
|
||||||
./datascape -dir ./wiki -addr :8080 -user me -pass secret
|
./datascape -dir ./wiki -addr :8080 -user me -pass secret
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+39
-12
@@ -57,6 +57,14 @@ func resizeBox(src image.Image, width int) image.Image {
|
|||||||
}
|
}
|
||||||
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
|
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
|
||||||
|
|
||||||
|
// JPEGs decode to *image.YCbCr. Reading its planes directly avoids an
|
||||||
|
// interface call and a colour conversion per source pixel, which makes the
|
||||||
|
// resize ~4x faster; everything else takes the generic At() path.
|
||||||
|
avg := boxAvg
|
||||||
|
if ycc, ok := src.(*image.YCbCr); ok {
|
||||||
|
avg = func(_ image.Image, r image.Rectangle) color.RGBA { return boxAvgYCbCr(ycc, r) }
|
||||||
|
}
|
||||||
|
|
||||||
for y := 0; y < dstH; y++ {
|
for y := 0; y < dstH; y++ {
|
||||||
sy0 := y * srcH / dstH
|
sy0 := y * srcH / dstH
|
||||||
sy1 := (y + 1) * srcH / dstH
|
sy1 := (y + 1) * srcH / dstH
|
||||||
@@ -69,23 +77,42 @@ func resizeBox(src image.Image, width int) image.Image {
|
|||||||
if sx1 == sx0 {
|
if sx1 == sx0 {
|
||||||
sx1 = sx0 + 1
|
sx1 = sx0 + 1
|
||||||
}
|
}
|
||||||
var r, g, bl, n uint64
|
box := image.Rect(sx0, sy0, sx1, sy1).Add(b.Min)
|
||||||
for sy := sy0; sy < sy1; sy++ {
|
dst.SetRGBA(x, y, avg(src, box))
|
||||||
for sx := sx0; sx < sx1; sx++ {
|
}
|
||||||
sr, sg, sb, _ := src.At(b.Min.X+sx, b.Min.Y+sy).RGBA()
|
}
|
||||||
r += uint64(sr >> 8)
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// boxAvg returns the mean colour of the pixels of src inside r.
|
||||||
|
func boxAvg(src image.Image, r image.Rectangle) color.RGBA {
|
||||||
|
var red, g, bl, n uint64
|
||||||
|
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||||
|
for x := r.Min.X; x < r.Max.X; x++ {
|
||||||
|
sr, sg, sb, _ := src.At(x, y).RGBA()
|
||||||
|
red += uint64(sr >> 8)
|
||||||
g += uint64(sg >> 8)
|
g += uint64(sg >> 8)
|
||||||
bl += uint64(sb >> 8)
|
bl += uint64(sb >> 8)
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dst.SetRGBA(x, y, color.RGBA{
|
return color.RGBA{R: uint8(red / n), G: uint8(g / n), B: uint8(bl / n), A: 255}
|
||||||
R: uint8(r / n),
|
}
|
||||||
G: uint8(g / n),
|
|
||||||
B: uint8(bl / n),
|
// boxAvgYCbCr is boxAvg for *image.YCbCr, reading the sample planes directly.
|
||||||
A: 255,
|
// Each pixel is still converted to RGB before averaging so out-of-gamut
|
||||||
})
|
// samples clamp exactly as they do through At().
|
||||||
|
func boxAvgYCbCr(src *image.YCbCr, r image.Rectangle) color.RGBA {
|
||||||
|
var red, g, bl, n uint64
|
||||||
|
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||||
|
for x := r.Min.X; x < r.Max.X; x++ {
|
||||||
|
ci := src.COffset(x, y)
|
||||||
|
sr, sg, sb := color.YCbCrToRGB(src.Y[src.YOffset(x, y)], src.Cb[ci], src.Cr[ci])
|
||||||
|
red += uint64(sr)
|
||||||
|
g += uint64(sg)
|
||||||
|
bl += uint64(sb)
|
||||||
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dst
|
return color.RGBA{R: uint8(red / n), G: uint8(g / n), B: uint8(bl / n), A: 255}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testPhoto returns a JPEG-decoded (so *image.YCbCr, 4:2:0) image with an odd
|
||||||
|
// size and a non-zero origin to exercise the plane offset arithmetic.
|
||||||
|
func testPhoto(t testing.TB, w, h int) *image.YCbCr {
|
||||||
|
t.Helper()
|
||||||
|
src := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
for i := range src.Pix {
|
||||||
|
src.Pix[i] = uint8(i*7 + i/w)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: 90}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
img, err := jpeg.Decode(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return img.(*image.YCbCr).SubImage(image.Rect(3, 5, w, h)).(*image.YCbCr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResizeBoxYCbCrMatchesGeneric(t *testing.T) {
|
||||||
|
src := testPhoto(t, 1001, 667)
|
||||||
|
fast := resizeBox(src, 120).(*image.RGBA)
|
||||||
|
// Hide the concrete type so resizeBox takes the generic At() path.
|
||||||
|
slow := resizeBox(struct{ image.Image }{src}, 120).(*image.RGBA)
|
||||||
|
if fast.Bounds() != slow.Bounds() {
|
||||||
|
t.Fatalf("bounds %v != %v", fast.Bounds(), slow.Bounds())
|
||||||
|
}
|
||||||
|
for i := range fast.Pix {
|
||||||
|
if fast.Pix[i] != slow.Pix[i] {
|
||||||
|
t.Fatalf("pixel byte %d: fast %d, generic %d", i, fast.Pix[i], slow.Pix[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkResizeBox(b *testing.B) {
|
||||||
|
src := testPhoto(b, 4000, 3000)
|
||||||
|
b.Run("ycbcr", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
resizeBox(src, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("generic", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
resizeBox(struct{ image.Image }{src}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user