This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 045c40537 perf(io): avoid copying MemFS contents on open (#2233)
045c40537 is described below

commit 045c4053772fab695fe73f84bb6253dd7c64e010
Author: Minh Vu <[email protected]>
AuthorDate: Mon Oct 26 18:03:32 2026 +0100

    perf(io): avoid copying MemFS contents on open (#2233)
    
    * perf(io): avoid copying MemFS contents on open
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * Merge branch 'main' into perf/memfs-zero-copy-open-20261020
    
    Pass a context to MemFS.Open/Remove in the new tests after #2067 added ctx 
to IO.Open and IO.Remove.
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
    Co-authored-by: Matt Topol <[email protected]>
---
 io/mem.go                    |   8 +-
 io/mem_open_bench_test.go    |  54 ++++++++++++
 io/mem_open_snapshot_test.go | 193 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 251 insertions(+), 4 deletions(-)

diff --git a/io/mem.go b/io/mem.go
index ecc5541d7..ef46346ba 100644
--- a/io/mem.go
+++ b/io/mem.go
@@ -58,7 +58,7 @@ func init() {
 // Use the "mem://bucket/path" URI scheme to access files.
 type MemFS struct {
        mu    sync.RWMutex
-       files map[string][]byte // keyed by full URI
+       files map[string][]byte // immutable snapshots, keyed by full URI
 }
 
 // NewMemFS creates an initialized MemFS ready for use.
@@ -73,10 +73,10 @@ func (m *MemFS) Open(_ context.Context, name string) (File, 
error) {
        if !ok {
                return nil, &fs.PathError{Op: "open", Path: name, Err: 
fs.ErrNotExist}
        }
-       cp := make([]byte, len(data))
-       copy(cp, data)
 
-       return &memFile{data: cp, name: path.Base(name)}, nil
+       // WriteFile stores a copy that is never mutated. Replacing or removing
+       // a path does not change the bytes held by an already-open reader.
+       return &memFile{data: data, name: path.Base(name)}, nil
 }
 
 func (m *MemFS) Remove(_ context.Context, name string) error {
diff --git a/io/mem_open_bench_test.go b/io/mem_open_bench_test.go
new file mode 100644
index 000000000..a3e3db10f
--- /dev/null
+++ b/io/mem_open_bench_test.go
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package io_test
+
+import (
+       "fmt"
+       "testing"
+
+       icebergio "github.com/apache/iceberg-go/io"
+)
+
+func BenchmarkMemFSOpen(b *testing.B) {
+       for _, sizeMiB := range []int{1, 16} {
+               b.Run(fmt.Sprintf("%dMiB", sizeMiB), func(b *testing.B) {
+                       memFS := icebergio.NewMemFS()
+                       name := "mem://bucket/data.parquet"
+                       size := sizeMiB << 20
+                       if err := memFS.WriteFile(name, make([]byte, size)); 
err != nil {
+                               b.Fatal(err)
+                       }
+
+                       var lastByte [1]byte
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for b.Loop() {
+                               file, err := memFS.Open(b.Context(), name)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               if _, err := file.ReadAt(lastByte[:], 
int64(size-1)); err != nil {
+                                       b.Fatal(err)
+                               }
+                               if err := file.Close(); err != nil {
+                                       b.Fatal(err)
+                               }
+                       }
+               })
+       }
+}
diff --git a/io/mem_open_snapshot_test.go b/io/mem_open_snapshot_test.go
new file mode 100644
index 000000000..3e011dd87
--- /dev/null
+++ b/io/mem_open_snapshot_test.go
@@ -0,0 +1,193 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package io_test
+
+import (
+       "bytes"
+       "errors"
+       "io"
+       "io/fs"
+       "sync"
+       "testing"
+
+       icebergio "github.com/apache/iceberg-go/io"
+)
+
+func TestMemFSOpenPreservesSnapshots(t *testing.T) {
+       memFS := icebergio.NewMemFS()
+       name := "mem://bucket/data.parquet"
+       input := []byte("original")
+       if err := memFS.WriteFile(name, input); err != nil {
+               t.Fatal(err)
+       }
+
+       oldFile, err := memFS.Open(t.Context(), name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer oldFile.Close()
+
+       // The caller's input must not alias a published file.
+       input[0] = 'X'
+       if err := memFS.WriteFile(name, []byte("replacement")); err != nil {
+               t.Fatal(err)
+       }
+       newFile, err := memFS.Open(t.Context(), name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer newFile.Close()
+
+       if err := memFS.Remove(t.Context(), name); err != nil {
+               t.Fatal(err)
+       }
+       if _, err := memFS.Open(t.Context(), name); !errors.Is(err, 
fs.ErrNotExist) {
+               t.Fatalf("open after remove: got %v, want fs.ErrNotExist", err)
+       }
+
+       for _, test := range []struct {
+               file icebergio.File
+               want string
+       }{
+               {oldFile, "original"},
+               {newFile, "replacement"},
+       } {
+               got, err := io.ReadAll(test.file)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if string(got) != test.want {
+                       t.Fatalf("read after replacement/removal: got %q, want 
%q", got, test.want)
+               }
+               if _, err := test.file.Seek(0, io.SeekStart); err != nil {
+                       t.Fatal(err)
+               }
+               at := make([]byte, len(test.want))
+               if _, err := test.file.ReadAt(at, 0); err != nil {
+                       t.Fatal(err)
+               }
+               if string(at) != test.want {
+                       t.Fatalf("readat after replacement/removal: got %q, 
want %q", at, test.want)
+               }
+               info, err := test.file.Stat()
+               if err != nil || info.Size() != int64(len(test.want)) {
+                       t.Fatalf("stat after replacement/removal: info=%v 
err=%v", info, err)
+               }
+       }
+}
+
+func TestMemFSOpenPreservesSnapshotAcrossCreate(t *testing.T) {
+       memFS := icebergio.NewMemFS()
+       name := "mem://bucket/created.parquet"
+
+       firstWriter, err := memFS.Create(name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if _, err := firstWriter.Write([]byte("first")); err != nil {
+               t.Fatal(err)
+       }
+       if err := firstWriter.Close(); err != nil {
+               t.Fatal(err)
+       }
+
+       oldFile, err := memFS.Open(t.Context(), name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer oldFile.Close()
+
+       secondWriter, err := memFS.Create(name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if _, err := secondWriter.Write([]byte("second")); err != nil {
+               t.Fatal(err)
+       }
+       if err := secondWriter.Close(); err != nil {
+               t.Fatal(err)
+       }
+
+       got, err := io.ReadAll(oldFile)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if string(got) != "first" {
+               t.Fatalf("open reader changed after Create replacement: got 
%q", got)
+       }
+
+       current, err := memFS.Open(t.Context(), name)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer current.Close()
+       got, err = io.ReadAll(current)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if string(got) != "second" {
+               t.Fatalf("new reader did not see Create replacement: got %q", 
got)
+       }
+}
+
+func TestMemFSOpenConcurrentReplacement(t *testing.T) {
+       memFS := icebergio.NewMemFS()
+       name := "mem://bucket/data.parquet"
+       a := bytes.Repeat([]byte{'a'}, 4096)
+       b := bytes.Repeat([]byte{'b'}, 4096)
+       if err := memFS.WriteFile(name, a); err != nil {
+               t.Fatal(err)
+       }
+
+       var wg sync.WaitGroup
+       wg.Go(func() {
+               for i := range 300 {
+                       value := a
+                       if i%2 != 0 {
+                               value = b
+                       }
+                       if err := memFS.WriteFile(name, value); err != nil {
+                               t.Errorf("write: %v", err)
+
+                               return
+                       }
+               }
+       })
+
+       for range 300 {
+               file, err := memFS.Open(t.Context(), name)
+               if err != nil {
+                       t.Errorf("open: %v", err)
+
+                       break
+               }
+               data, readErr := io.ReadAll(file)
+               closeErr := file.Close()
+               if readErr != nil || closeErr != nil {
+                       t.Errorf("read: %v, close: %v", readErr, closeErr)
+
+                       break
+               }
+               if !bytes.Equal(data, a) && !bytes.Equal(data, b) {
+                       t.Errorf("file changed while open")
+
+                       break
+               }
+       }
+       wg.Wait()
+}

Reply via email to