From ce7da4311848e7d83fe50f818cdbd9c706700456 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:27:46 +0000 Subject: [PATCH 1/2] Optimize readNativeFrame performance via chunked I/O reads - Reduced I/O overhead by implementing a 4KB chunk buffer instead of per-sample reads. - Flattened nested pixel/sample loops into a contiguous index space processing. - Eliminated boxing/unboxing and reflection overhead by moving from dynamically typed `any(...).(I)` to generic numeric conversion `I(bo.Uint...)`. - Ensured unsupported BitsAllocated constraints (<8 or not a multiple of 8) exit cleanly before divisions. Co-authored-by: suyashkumar <6299853+suyashkumar@users.noreply.github.com> --- read.go | 70 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/read.go b/read.go index 5366c332..e0fd58cc 100644 --- a/read.go +++ b/read.go @@ -546,36 +546,52 @@ func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRe } bo := rawReader.ByteOrder() - for pixel := 0; pixel < pixelsPerFrame; pixel++ { - for value := 0; value < samplesPerPixel; value++ { - _, err := io.ReadFull(rawReader, pixelBuf) - if err != nil { - return frame.Frame{}, bytesToRead, - fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err) + if bitsAllocated < 8 || bitsAllocated%8 != 0 { + return frame.Frame{}, bytesToRead, fmt.Errorf("readNativeFrame unsupported bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) + } + + bytesPerSample := bitsAllocated / 8 + + // Use a 4KB chunk buffer to minimize io.ReadFull calls + chunkSize := 4096 + // ensure chunkSize is a multiple of bytesPerSample + chunkSize = (chunkSize / bytesPerSample) * bytesPerSample + chunkBuf := make([]byte, chunkSize) + + totalSamples := pixelsPerFrame * samplesPerPixel + samplesRead := 0 + + for samplesRead < totalSamples { + samplesLeft := totalSamples - samplesRead + bytesToReadChunk := samplesLeft * bytesPerSample + if bytesToReadChunk > chunkSize { + bytesToReadChunk = chunkSize + } + + _, err := io.ReadFull(rawReader, chunkBuf[:bytesToReadChunk]) + if err != nil { + return frame.Frame{}, bytesToRead, + fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err) + } + + samplesInChunk := bytesToReadChunk / bytesPerSample + switch bitsAllocated { + case 8: + for i := 0; i < samplesInChunk; i++ { + nativeFrame.RawData[samplesRead+i] = I(chunkBuf[i]) } - switch bitsAllocated { - case 8: - v, ok := any(pixelBuf[0]).(I) - if !ok { - return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) - } - nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - case 16: - v, ok := any(bo.Uint16(pixelBuf)).(I) - if !ok { - return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) - } - nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - case 32: - v, ok := any(bo.Uint32(pixelBuf)).(I) - if !ok { - return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) - } - nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - default: - return frame.Frame{}, bytesToRead, fmt.Errorf("readNativeFrame unsupported bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) + case 16: + for i := 0; i < samplesInChunk; i++ { + nativeFrame.RawData[samplesRead+i] = I(bo.Uint16(chunkBuf[i*2 : i*2+2])) } + case 32: + for i := 0; i < samplesInChunk; i++ { + nativeFrame.RawData[samplesRead+i] = I(bo.Uint32(chunkBuf[i*4 : i*4+4])) + } + default: + return frame.Frame{}, bytesToRead, fmt.Errorf("readNativeFrame unsupported bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) } + samplesRead += samplesInChunk } return currentFrame, bytesToRead, nil } From efd3a0eb8db46afdf597563c3b5a6b38fe46da6f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 04:00:51 +0000 Subject: [PATCH 2/2] Optimize readNativeFrame performance via chunked I/O reads - Reduced I/O overhead by implementing a 4KB chunk buffer instead of per-sample reads. - Flattened nested pixel/sample loops into a contiguous index space processing. - Eliminated boxing/unboxing and reflection overhead by moving from dynamically typed `any(...).(I)` to generic numeric conversion `I(bo.Uint...)`. - Ensured unsupported BitsAllocated constraints (<8 or not a multiple of 8) exit cleanly before divisions. - Fixed a benchmark bug in `read_test.go` that previously suppressed validation errors regarding pixel sizes vs calculated sizes. Co-authored-by: suyashkumar <6299853+suyashkumar@users.noreply.github.com> --- read_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/read_test.go b/read_test.go index e4a84641..8303ccb7 100644 --- a/read_test.go +++ b/read_test.go @@ -1032,8 +1032,15 @@ func BenchmarkReadNativeFrames(b *testing.B) { dataset, rawReader := buildReadNativeFramesInput(c.Rows, c.Cols, c.NumFrames, c.SamplesPerPixel, b) r := &reader{rawReader: rawReader} b.ResetTimer() + // BitsAllocated is hardcoded to 16, meaning 2 bytes per sample. + bytesAllocated := 2 + vl := uint32(c.Rows * c.Cols * c.NumFrames * c.SamplesPerPixel * bytesAllocated) + for i := 0; i < b.N; i++ { - _, _, _ = r.readNativeFrames(dataset, nil, uint32(c.Rows*c.Cols*c.NumFrames)) + _, _, err := r.readNativeFrames(dataset, nil, vl) + if err != nil { + b.Fatalf("readNativeFrames error: %v", err) + } } }) } @@ -1064,7 +1071,8 @@ func buildReadNativeFramesInput(rows, cols, numFrames, samplesPerPixel int, b *t } } - return &dataset, dicomio.NewReader(bufio.NewReader(&dcmdata), binary.LittleEndian, int64(dcmdata.Len())) + rawReader := dicomio.NewReader(bufio.NewReader(&dcmdata), binary.LittleEndian, int64(2*numFrames*rows*cols*samplesPerPixel)) + return &dataset, rawReader } func buildTagData(t *testing.T, tg tag.Tag) []byte {