diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 088e79c705..9edbb5184a 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -4,6 +4,7 @@ #### Improvements 🧹 +- performance: reduce repeated work in Dagre ranking, text measurement, SVG preparation, and PNG encoding - performance: reduce repeated work in selective imports, dynamic grid layout, and PNG connection rendering - exports: reduce PNG memory usage and support images above the previous 67-megapixel limit - performance: SVG rendering is approximately 10× faster across the E2E corpus. Real-world diagrams compile, lay out, and render approximately 3× faster at the median. diff --git a/d2cli/raster_png_encoder.go b/d2cli/raster_png_encoder.go index 8a04a1d775..157365bc44 100644 --- a/d2cli/raster_png_encoder.go +++ b/d2cli/raster_png_encoder.go @@ -481,6 +481,10 @@ func rasterPNGUpFilter(destination, raw, prior []byte) int { _ = prior[len(raw)-1] destination[0] = 2 destination = destination[1:] + if bytes.Equal(raw, prior) { + clear(destination[:len(raw)]) + return 0 + } cost := 0 for index, value := range raw { destination[index] = value - prior[index] diff --git a/d2cli/raster_png_filter_runs_test.go b/d2cli/raster_png_filter_runs_test.go new file mode 100644 index 0000000000..b974d5c400 --- /dev/null +++ b/d2cli/raster_png_filter_runs_test.go @@ -0,0 +1,160 @@ +package d2cli + +import ( + "bytes" + "math" + "math/rand" + "testing" +) + +// Compare the actual scores and scratch writes against the pre-optimization +// scalar filters, including early termination and runs crossing pixel boundaries. +func TestRasterPNGFilterRunsMatchScalar(t *testing.T) { + random := rand.New(rand.NewSource(2879)) + for _, channels := range []int{3, 4} { + for _, width := range []int{1, 2, 5, 6, 7, 8, 10, 15, 16, 17, 31, 32, 33, 63, 64, 65, 257} { + for pattern := 0; pattern < 9; pattern++ { + raw, prior := make([]byte, width*channels), make([]byte, width*channels) + random.Read(raw) + random.Read(prior) + switch pattern { + case 1: + copy(raw, prior) + case 2, 3: + for i := range raw { + raw[i] = byte(i%channels*53 + 1) + prior[i] = raw[i] + if pattern == 3 { + prior[i] += 19 + } + } + case 4: + copy(raw, prior) + for i := channels - 1; i < len(raw); i += 16 { + raw[i] ^= 0x81 + } + case 5: + for i := range raw { + raw[i] = 255 + prior[i] = 255 + if i%32 == 15 { + raw[i] = byte(i) + } + } + case 6: + for i := 0; i < min(64, len(raw)); i++ { + raw[i] = 255 + prior[i] = 255 + } + case 7: + for i := 16; i+16 <= len(raw); i += 32 { + copy(raw[i:i+16], prior[i:i+16]) + } + case 8: + clear(raw) + clear(prior) + } + thresholds := []int{0, 1, 2, 7, 255, 65535, math.MaxInt} + scratch := make([]byte, len(raw)+1) + for _, fullCost := range []int{ + referenceRasterPNGUpFilter(scratch, raw, prior), + referenceRasterPNGSubFilter(scratch, raw, math.MaxInt, channels), + referenceRasterPNGPaethFilter(scratch, raw, prior, math.MaxInt, channels), + } { + thresholds = append(thresholds, max(0, fullCost-1), fullCost, fullCost+1) + } + for _, stopAt := range thresholds { + for filter := 0; filter < 3; filter++ { + got, want := bytes.Repeat([]byte{0xa5}, len(raw)+1), bytes.Repeat([]byte{0xa5}, len(raw)+1) + var gotCost, wantCost int + switch filter { + case 0: + gotCost = rasterPNGUpFilter(got, raw, prior) + wantCost = referenceRasterPNGUpFilter(want, raw, prior) + case 1: + gotCost = rasterPNGSubFilter(got, raw, stopAt, channels) + wantCost = referenceRasterPNGSubFilter(want, raw, stopAt, channels) + case 2: + gotCost = rasterPNGPaethFilter(got, raw, prior, stopAt, channels) + wantCost = referenceRasterPNGPaethFilter(want, raw, prior, stopAt, channels) + } + if gotCost != wantCost || !bytes.Equal(got, want) { + t.Fatalf("channels=%d width=%d pattern=%d stop=%d filter=%d: cost %d != %d or scratch differs", channels, width, pattern, stopAt, filter, gotCost, wantCost) + } + } + } + } + } + } +} + +// Frozen scalar implementations from 70affc0e. +func referenceRasterPNGUpFilter(destination, raw, prior []byte) int { + _ = prior[len(raw)-1] + destination[0] = 2 + destination = destination[1:] + cost := 0 + for index, value := range raw { + destination[index] = value - prior[index] + cost += rasterPNGAbs8(destination[index]) + } + return cost +} + +func referenceRasterPNGSubFilter(destination, raw []byte, stopAt, channels int) int { + destination[0] = 1 + destination = destination[1:] + cost := 0 + for index := 0; index < channels; index++ { + destination[index] = raw[index] + cost += rasterPNGAbs8(destination[index]) + if cost >= stopAt { + return cost + } + } + for index := channels; index < len(raw); index++ { + destination[index] = raw[index] - raw[index-channels] + cost += rasterPNGAbs8(destination[index]) + if cost >= stopAt { + break + } + } + return cost +} + +func referenceRasterPNGPaethFilter(destination, raw, prior []byte, stopAt, channels int) int { + _ = prior[len(raw)-1] + destination[0] = 4 + destination = destination[1:] + cost := 0 + for index := 0; index < channels; index++ { + destination[index] = raw[index] - prior[index] + cost += rasterPNGAbs8(destination[index]) + if cost >= stopAt { + return cost + } + } + for index := channels; index < len(raw); index++ { + left := raw[index-channels] + upperLeft := prior[index-channels] + upper := prior[index] + leftDistance := rasterPNGByteDistance(upper, upperLeft) + upperDistance := rasterPNGByteDistance(left, upperLeft) + diagonalDistance := int(left) + int(upper) - 2*int(upperLeft) + if diagonalDistance < 0 { + diagonalDistance = -diagonalDistance + } + predictor := upperLeft + if leftDistance <= upperDistance && leftDistance <= diagonalDistance { + predictor = left + } else if upperDistance <= diagonalDistance { + predictor = upper + } + destination[index] = raw[index] - predictor + cost += rasterPNGAbs8(destination[index]) + if cost >= stopAt { + break + } + } + return cost +} diff --git a/d2cli/raster_png_mixed_content_test.go b/d2cli/raster_png_mixed_content_test.go new file mode 100644 index 0000000000..369fe6f8e5 --- /dev/null +++ b/d2cli/raster_png_mixed_content_test.go @@ -0,0 +1,104 @@ +package d2cli + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "math/rand" + "testing" +) + +// These fixtures exercise unchanged prefixes followed by varied content. +// Fixture generation is outside timed runs. +func rasterPNGMixedContentImages() []struct { + name string + img *image.NRGBA +} { + var cases []struct { + name string + img *image.NRGBA + } + for _, kind := range []string{"noise", "gradient"} { + for _, margin := range []int{8, 64} { + img := image.NewNRGBA(image.Rect(0, 0, 512, 512)) + random := rand.New(rand.NewSource(0xD2)) + for y := 0; y < 512; y++ { + for x := 0; x < 512; x++ { + offset := img.PixOffset(x, y) + // Consume one value for every pixel so both margin widths + // have the same underlying noise at matching coordinates. + value := random.Uint32() + red, green, blue := byte(value), byte(value>>8), byte(value>>16) + if kind == "gradient" { + // Match the existing encoder benchmark's diagonal + // gradient: colors vary both within and between rows. + red, green, blue = byte(x), byte(y), byte(x+y) + } + if x < margin { + red, green, blue = 0xff, 0xff, 0xff + } + img.Pix[offset], img.Pix[offset+1], img.Pix[offset+2], img.Pix[offset+3] = red, green, blue, 0xff + } + } + cases = append(cases, struct { + name string + img *image.NRGBA + }{name: fmt.Sprintf("%s/margin-%d", kind, margin), img: img}) + } + } + return cases +} + +var benchmarkRasterPNGMixedBytes []byte + +// BenchmarkRasterPNGMixedContent uses the native subbenchmark lifecycle from +// BenchmarkRasterPNGEncoder: one encoder per subbenchmark, retained across +// iterations and closed afterward. Each encode allocates its output normally. +func BenchmarkRasterPNGMixedContent(b *testing.B) { + for _, benchmark := range rasterPNGMixedContentImages() { + b.Run(benchmark.name+"/native", func(b *testing.B) { + var encoder rasterPNGEncoder + defer encoder.close() + b.ReportAllocs() + for range b.N { + encoded, err := encoder.encode(context.Background(), benchmark.img) + if err != nil { + b.Fatal(err) + } + benchmarkRasterPNGMixedBytes = encoded + } + }) + } +} + +func TestRasterPNGMixedContentMatchesStdlib(t *testing.T) { + var encoder rasterPNGEncoder + defer encoder.close() + for _, test := range rasterPNGMixedContentImages() { + t.Run(test.name, func(t *testing.T) { + before := bytes.Clone(test.img.Pix) + var expected bytes.Buffer + stdlib := png.Encoder{CompressionLevel: png.BestSpeed} + if err := stdlib.Encode(&expected, test.img); err != nil { + t.Fatal(err) + } + // Both inputs are opaque NRGBA, so native encoding must emit + // the same truecolor PNG bytes and filter choices as image/png. + // Repeat to check retained encoder state as well as first use. + for pass := 0; pass < 2; pass++ { + got, err := encoder.encode(context.Background(), test.img) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, expected.Bytes()) { + t.Fatalf("pass %d: native PNG differs from image/png BestSpeed", pass) + } + if !bytes.Equal(test.img.Pix, before) { + t.Fatalf("pass %d: encoding mutated source pixels", pass) + } + } + }) + } +} diff --git a/d2layouts/d2dagrelayout/real_world_benchmark_test.go b/d2layouts/d2dagrelayout/real_world_benchmark_test.go new file mode 100644 index 0000000000..e1d98b32d1 --- /dev/null +++ b/d2layouts/d2dagrelayout/real_world_benchmark_test.go @@ -0,0 +1,21 @@ +package d2dagrelayout + +import ( + "os" + "path/filepath" + "testing" +) + +// Keep layout measurements separate from parsing and font measurement, using +// the same real-world sources as the D2 end-to-end output compatibility suite. +func BenchmarkDagreRealWorld(b *testing.B) { + for _, name := range []string{"tpmjs_architecture", "spyre_encoder", "mocha_soc", "fulcro_rad", "queue_workers"} { + b.Run(name, func(b *testing.B) { + source, err := os.ReadFile(filepath.Join("..", "..", "e2etests", "testdata", "files", name+".d2")) + if err != nil { + b.Fatal(err) + } + benchmarkDagreLayout(b, string(source)) + }) + } +} diff --git a/d2target/corpus_hash_test.go b/d2target/corpus_hash_test.go new file mode 100644 index 0000000000..c17b09fc76 --- /dev/null +++ b/d2target/corpus_hash_test.go @@ -0,0 +1,130 @@ +package d2target + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "net/url" + "strings" + "testing" +) + +func TestHashJSONWithoutIconsMatchesNormalization(t *testing.T) { + labels := []string{ + "", `quoted "icon":{"Scheme":"fake"}`, `escaped \\"icon":{`, + "