package host import ( "context" "fmt" "log" "os" "path/filepath" "strconv" "time" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/moves" ) // measureDiskSynced is the disk measurement without O_DIRECT: write, fsync, // then read measureDiskSize. The read may be answered from the page cache, // so it overstates the read rate; it is the fallback where O_DIRECT is // unavailable (movesmeasure_other.go, and tmpfs on linux). func measureDiskSynced(dir string, now func() time.Time) (moves.MeasuredRate, error) { path := filepath.Join(dir, "disk-synced") f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600) if err != nil { return moves.MeasuredRate{}, err } defer f.Close() buf := make([]byte, 1<<20) start := now() for n := 0; n < measureDiskSize; n += len(buf) { if _, err := f.Write(buf); err != nil { return moves.MeasuredRate{}, err } } if err := f.Sync(); err != nil { return moves.MeasuredRate{}, err } if _, err := f.Seek(0, 0); err != nil { return moves.MeasuredRate{}, err } for n := 0; n < measureDiskSize; n += len(buf) { if _, err := f.Read(buf); err != nil { return moves.MeasuredRate{}, err } } return moves.RateOf(2*measureDiskSize, now().Sub(start), now().UTC()), nil } // measureS3 times a 16 MiB put and get of the probe object (moves.ProbeKey, // outside moves/, F11) in this box's own bucket with its own credentials, // and deletes it. func measureS3(mirror func(ctx context.Context) (lease.MirrorStore, error), bucket string, now func() time.Time) func(context.Context, string) (moves.MeasuredRate, error) { return func(ctx context.Context, key string) (moves.MeasuredRate, error) { m, err := mirror(ctx) if err != nil { return moves.MeasuredRate{}, err } const size = 16 << 20 body := make([]byte, size) start := now() if err := m.Put(ctx, bucket, key, body); err != nil { return moves.MeasuredRate{}, err } got, err := m.Get(ctx, bucket, key) elapsed := now().Sub(start) if derr := m.Delete(ctx, bucket, key); derr != nil { log.Printf("moves: delete the S3 probe object %s: %v", key, derr) } if err != nil { return moves.MeasuredRate{}, err } if len(got) != size { return moves.MeasuredRate{}, fmt.Errorf("the probe object read back %d bytes of %d", len(got), size) } return moves.RateOf(2*size, elapsed, now().UTC()), nil } } // measureDiskIn measures this box's disk under /.moves (the // platform-specific write and read are movesmeasure_*.go). func measureDiskIn(dataDir string, now func() time.Time) func(context.Context) (moves.MeasuredRate, moves.FilesRate, error) { return func(ctx context.Context) (moves.MeasuredRate, moves.FilesRate, error) { dir, err := os.MkdirTemp(filepath.Join(dataDir, ".moves"), "measure-") if err != nil { if err := os.MkdirAll(filepath.Join(dataDir, ".moves"), 0o755); err != nil { return moves.MeasuredRate{}, moves.FilesRate{}, err } if dir, err = os.MkdirTemp(filepath.Join(dataDir, ".moves"), "measure-"); err != nil { return moves.MeasuredRate{}, moves.FilesRate{}, err } } defer os.RemoveAll(dir) disk, err := measureDiskRate(dir, now) if err != nil { return moves.MeasuredRate{}, moves.FilesRate{}, err } files, err := measureFsyncRate(dir, now) if err != nil { return moves.MeasuredRate{}, moves.FilesRate{}, err } return disk, files, nil } } // measureFsyncFiles and measureFsyncWorkers are the per-file sync probe: // a few hundred small files synced by the extract's 16-worker pattern // (ruling T7-budget). const ( measureFsyncFiles = 512 measureFsyncWorkers = 16 ) func measureFsyncRate(dir string, now func() time.Time) (moves.FilesRate, error) { jobs := make(chan int) errs := make(chan error, measureFsyncWorkers) start := now() for i := 0; i < measureFsyncWorkers; i++ { go func() { var first error for n := range jobs { if first != nil { continue } f, err := os.OpenFile(filepath.Join(dir, "f"+strconv.Itoa(n)), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err == nil { _, err = f.Write(make([]byte, 4096)) if err == nil { err = f.Sync() } if cerr := f.Close(); err == nil { err = cerr } } first = err } errs <- first }() } for n := 0; n < measureFsyncFiles; n++ { jobs <- n } close(jobs) for i := 0; i < measureFsyncWorkers; i++ { if err := <-errs; err != nil { return moves.FilesRate{}, err } } d := now().Sub(start) if d <= 0 { d = time.Microsecond } return moves.FilesRate{PerS: measureFsyncFiles / d.Seconds(), At: now().UTC(), Files: measureFsyncFiles}, nil }