Skip to content

Commit cfa1a27

Browse files
committed
Improve convert interface
Creating a converter and calling converter.Convert() feels too complicated for no reason. We need to keep state during the conversion, but the user does not need to care about the internals. Since we already made incompatible changes and clients like lima need to change, this is an opportunity to get the interface right so future changes can be backward compatible. Changes: - `Convert()` is now a function accepting `Options`, saving one step for the caller. - The progress argument is now a `Progress` option, so users do not need to pass nil to disable progress. - The size argument was removed since we can use the size of the image. If we want to support a byte range we can add start and length options without changing the function signature. - `Converter` renamed to `conversion`, created inside `Convert()` - Since we create a new `conversion` for each call, we don't need `reset()` - Adding future options will not change the function signature Example usage using defaults: convert.Convert(target, img, convert.Options{}) Example usage with progress bar: convert.Convert(target, img, convert.Options{Progress: bar}) Signed-off-by: Nir Soffer <[email protected]>
1 parent 0473ecd commit cfa1a27

File tree

3 files changed

+37
-61
lines changed

3 files changed

+37
-61
lines changed

cmd/go-qcow2reader-example/convert.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,12 @@ func cmdConvert(args []string) error {
7373
return err
7474
}
7575

76-
c, err := convert.New(options)
77-
if err != nil {
78-
return err
79-
}
80-
8176
bar := newProgressBar(img.Size())
8277
bar.Start()
8378
defer bar.Finish()
79+
options.Progress = bar
8480

85-
if err := c.Convert(t, img, img.Size(), bar); err != nil {
81+
if err := convert.Convert(t, img, options); err != nil {
8682
return err
8783
}
8884

convert/convert.go

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const SegmentSize = 32 * BufferSize
3232
// results with lima default Ubuntu image.
3333
const Workers = 8
3434

35+
// Updater is an interface for tracking conversion progress.
36+
type Updater interface {
37+
// Called from multiple goroutines after a byte range of length was converted.
38+
// If the conversion is successfu, the total number of bytes will be the image
39+
// virtual size.
40+
Update(n int64)
41+
}
42+
3543
type Options struct {
3644
// SegmentSize in bytes. Must be aligned to BufferSize. If not set, use the
3745
// default value (32 MiB).
@@ -43,6 +51,9 @@ type Options struct {
4351
// Workers is the number of goroutines copying buffers in parallel. If not set
4452
// use the default value (8).
4553
Workers int
54+
55+
// If set, update progress during conversion.
56+
Progress Updater
4657
}
4758

4859
// Validate validates options and set default values. Returns an error for
@@ -78,44 +89,21 @@ func (o *Options) Validate() error {
7889
return nil
7990
}
8091

81-
// Updater is an interface for tracking conversion progress.
82-
type Updater interface {
83-
// Called from multiple goroutines after a byte range of length was converted.
84-
// If the conversion is successfu, the total number of bytes will be the image
85-
// virtual size.
86-
Update(n int64)
87-
}
88-
89-
type Converter struct {
90-
// Read only after starting.
92+
type conversion struct {
93+
// Read only.
9194
size int64
9295
segmentSize int64
93-
bufferSize int
94-
workers int
9596

96-
// State modified during Convert, protected by the mutex.
97+
// Modified during Convert, protected by the mutex.
9798
mutex sync.Mutex
9899
offset int64
99100
err error
100101
}
101102

102-
// New returns a new converter initialized from options.
103-
func New(opts Options) (*Converter, error) {
104-
if err := opts.Validate(); err != nil {
105-
return nil, err
106-
}
107-
c := &Converter{
108-
segmentSize: opts.SegmentSize,
109-
bufferSize: opts.BufferSize,
110-
workers: opts.Workers,
111-
}
112-
return c, nil
113-
}
114-
115103
// nextSegment returns the next segment to process and stop flag. The stop flag
116104
// is true if there is no more work, or if another workers has failed and set
117105
// the error.
118-
func (c *Converter) nextSegment() (int64, int64, bool) {
106+
func (c *conversion) nextSegment() (int64, int64, bool) {
119107
c.mutex.Lock()
120108
defer c.mutex.Unlock()
121109

@@ -134,37 +122,33 @@ func (c *Converter) nextSegment() (int64, int64, bool) {
134122

135123
// setError keeps the first error set. Setting the error signal other workers to
136124
// abort the operation.
137-
func (c *Converter) setError(err error) {
125+
func (c *conversion) setError(err error) {
138126
c.mutex.Lock()
139127
if c.err == nil {
140128
c.err = err
141129
}
142130
c.mutex.Unlock()
143131
}
144132

145-
func (c *Converter) reset(size int64) {
146-
c.size = size
147-
c.err = nil
148-
c.offset = 0
149-
}
150-
151-
// Convert copy size bytes from image to io.WriterAt. Unallocated extents in the
152-
// source image or read data which is all zeros are converted to unallocated
153-
// byte range in the target image. The target image must be new empty file or a
154-
// file full of zeroes. To get a sparse target image, the image must be a new
155-
// empty file, since Convert does not punch holes for zero ranges even if the
156-
// underlying file system supports hole punching.
157-
func (c *Converter) Convert(wa io.WriterAt, img image.Image, size int64, progress Updater) error {
158-
c.reset(size)
159-
160-
zero := make([]byte, c.bufferSize)
133+
// Convert copy image to io.WriterAt. Unallocated extents in the image or read
134+
// data which is all zeros are converted to unallocated byte range in the target
135+
// image. The target image must be new empty file or a file full of zeroes. To
136+
// get a sparse target image, the image must be a new empty file, since Convert
137+
// does not punch holes for zero ranges even if the underlying file system
138+
// supports hole punching.
139+
func Convert(wa io.WriterAt, img image.Image, opts Options) error {
140+
if err := opts.Validate(); err != nil {
141+
return err
142+
}
143+
c := conversion{size: img.Size(), segmentSize: opts.SegmentSize}
144+
zero := make([]byte, opts.BufferSize)
161145
var wg sync.WaitGroup
162146

163-
for i := 0; i < c.workers; i++ {
147+
for i := 0; i < opts.Workers; i++ {
164148
wg.Add(1)
165149
go func() {
166150
defer wg.Done()
167-
buf := make([]byte, c.bufferSize)
151+
buf := make([]byte, opts.BufferSize)
168152
for {
169153
// Get next segment to copy.
170154
start, end, stop := c.nextSegment()
@@ -181,8 +165,8 @@ func (c *Converter) Convert(wa io.WriterAt, img image.Image, size int64, progres
181165
}
182166
if extent.Zero {
183167
start += extent.Length
184-
if progress != nil {
185-
progress.Update(extent.Length)
168+
if opts.Progress != nil {
169+
opts.Progress.Update(extent.Length)
186170
}
187171
continue
188172
}
@@ -223,8 +207,8 @@ func (c *Converter) Convert(wa io.WriterAt, img image.Image, size int64, progres
223207
}
224208
}
225209

226-
if progress != nil {
227-
progress.Update(int64(nr))
210+
if opts.Progress != nil {
211+
opts.Progress.Update(int64(nr))
228212
}
229213

230214
extent.Length -= int64(nr)

qcow2reader_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -805,11 +805,7 @@ func benchmarkConvert(b *testing.B, filename string) {
805805
b.Fatal(err)
806806
}
807807
defer dst.Close()
808-
c, err := convert.New(convert.Options{})
809-
if err != nil {
810-
b.Fatal(err)
811-
}
812-
err = c.Convert(dst, img, img.Size(), nil)
808+
err = convert.Convert(dst, img, convert.Options{})
813809
if err != nil {
814810
b.Fatal(err)
815811
}

0 commit comments

Comments
 (0)