@@ -177,15 +177,16 @@ type SaveOpts struct {
177177 // ExcludeCommittedZeroPages is false, SaveTo() will scan only
178178 // possibly-committed pages to find zero pages.
179179 //
180- // Enabling ExcludeCommittedZeroPages will usually increase the time taken
181- // by SaveTo() (due to the larger number of pages that must be scanned),
182- // but may instead improve SaveTo() and LoadFrom() time, and checkpoint
183- // size, if the application has many committed zero pages.
180+ // Enabling ExcludeCommittedZeroPages may increase the time taken by
181+ // SaveTo() when committed pages must be scanned, but may instead improve
182+ // SaveTo() and LoadFrom() time, and checkpoint size, if the application has
183+ // many committed zero pages. SaveTo() avoids scanning host file holes when
184+ // the backing file is sufficiently sparse.
184185 ExcludeCommittedZeroPages bool
185186}
186187
187188// SaveTo writes f's state to the given stream.
188- func (f * MemoryFile ) SaveTo (ctx context.Context , w io.Writer , opts * SaveOpts ) error {
189+ func (f * MemoryFile ) SaveTo (ctx context.Context , w io.Writer , opts * SaveOpts ) ( retErr error ) {
189190 if err := f .AwaitLoadAll (); err != nil {
190191 return fmt .Errorf ("previous async page loading failed: %w" , err )
191192 }
@@ -373,6 +374,33 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
373374 return maseg
374375 }
375376
377+ var hostData * hostFileDataIterator
378+ if ! f .opts .DiskBackedFile {
379+ fileSize := f .TotalSize ()
380+ fd := int (f .file .Fd ())
381+ backingFileUsageBytes , err := f .TotalUsage ()
382+ if err != nil {
383+ log .Debugf ("MemoryFile(%p): falling back to page scanning because backing file usage is unavailable: %v" , f , err )
384+ } else if accountedBytes := uint64 (f .memAcct .Span ()); hostFileUsageShowsEnoughHoles (backingFileUsageBytes , accountedBytes ) {
385+ off , err := unix .Seek (fd , 0 , unix .SEEK_CUR )
386+ if err != nil {
387+ log .Debugf ("MemoryFile(%p): falling back to page scanning because the backing file offset is unavailable: %v" , f , err )
388+ } else {
389+ hostData = & hostFileDataIterator {
390+ size : fileSize ,
391+ seek : func (offset int64 , whence int ) (int64 , error ) {
392+ return unix .Seek (fd , offset , whence )
393+ },
394+ }
395+ defer func () {
396+ if _ , err := unix .Seek (fd , off , unix .SEEK_SET ); retErr == nil && err != nil {
397+ retErr = fmt .Errorf ("failed to restore host file offset: %w" , err )
398+ }
399+ }()
400+ }
401+ }
402+ }
403+
376404 zeroPage := make ([]byte , hostarch .PageSize )
377405 // f.mu is unlocked below, allowing concurrent calls to f.UpdateUsage() to
378406 // observe pages that we transiently commit (for comparisons to zero) or
@@ -389,30 +417,7 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
389417 f .commitSeq = 0
390418 maseg := f .memAcct .FirstSegment ()
391419 unscannedStart := uint64 (0 )
392- for maseg .Ok () {
393- ma := maseg .ValuePtr ()
394- if ma .wasteOrReleasing {
395- // This shouldn't be possible since we waited for memory release
396- // above, and f shouldn't be mutated during saving.
397- panic (fmt .Sprintf ("found waste or releasing pages %v during pgalloc.MemoryFile.SaveTo()" , maseg .Range ()))
398- }
399- fr := maseg .Range ()
400- if fr .Start < unscannedStart {
401- fr .Start = unscannedStart
402- }
403- unscannedStart = fr .End
404- allocatedBytes += fr .Length ()
405- ma .commitSeq = 0
406- wasCommitted := ma .knownCommitted
407- if ! opts .ExcludeCommittedZeroPages && wasCommitted {
408- alreadyCommittedBytes += fr .Length ()
409- maseg = updateAddRange (maseg , fr , true /* wasCommitted */ , true /* nowCommitted */ )
410- maseg = updateFlush (maseg )
411- if maseg .End () == unscannedStart {
412- maseg = maseg .NextSegment ()
413- }
414- continue
415- }
420+ scanRange := func (fr memmap.FileRange , wasCommitted bool ) {
416421 f .forEachChunk (fr , func (chunk * chunkInfo , chunkFR memmap.FileRange ) bool {
417422 bs := chunk .sliceAt (chunkFR )
418423 for pgoff := 0 ; pgoff < len (bs ); pgoff += hostarch .PageSize {
@@ -444,6 +449,61 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
444449 f .mu .Lock ()
445450 return true
446451 })
452+ }
453+ markHostHoleUncommitted := func (fr memmap.FileRange , wasCommitted bool ) {
454+ if wasCommitted {
455+ newUncommittedBytes += fr .Length ()
456+ } else {
457+ alreadyUncommittedBytes += fr .Length ()
458+ }
459+ maseg = updateAddRange (maseg , fr , wasCommitted , false /* nowCommitted */ )
460+ }
461+ for maseg .Ok () {
462+ ma := maseg .ValuePtr ()
463+ if ma .wasteOrReleasing {
464+ // This shouldn't be possible since we waited for memory release
465+ // above, and f shouldn't be mutated during saving.
466+ panic (fmt .Sprintf ("found waste or releasing pages %v during pgalloc.MemoryFile.SaveTo()" , maseg .Range ()))
467+ }
468+ fr := maseg .Range ()
469+ if fr .Start < unscannedStart {
470+ fr .Start = unscannedStart
471+ }
472+ unscannedStart = fr .End
473+ allocatedBytes += fr .Length ()
474+ ma .commitSeq = 0
475+ wasCommitted := ma .knownCommitted
476+ if ! opts .ExcludeCommittedZeroPages && wasCommitted {
477+ alreadyCommittedBytes += fr .Length ()
478+ maseg = updateAddRange (maseg , fr , true /* wasCommitted */ , true /* nowCommitted */ )
479+ } else if hostData != nil {
480+ off := fr .Start
481+ for off < fr .End {
482+ dataFR , ok , err := hostData .rangeAtOrAfter (off )
483+ if err != nil {
484+ log .Debugf ("MemoryFile(%p): scanning remaining pages because backing file extents are unavailable: %v" , f , err )
485+ hostData = nil
486+ scanRange (memmap.FileRange {Start : off , End : fr .End }, wasCommitted )
487+ off = fr .End
488+ break
489+ }
490+ if ! ok || dataFR .Start >= fr .End {
491+ break
492+ }
493+ dataStart := max (off , dataFR .Start )
494+ if off < dataStart {
495+ markHostHoleUncommitted (memmap.FileRange {Start : off , End : dataStart }, wasCommitted )
496+ }
497+ dataEnd := min (fr .End , dataFR .End )
498+ scanRange (memmap.FileRange {Start : dataStart , End : dataEnd }, wasCommitted )
499+ off = dataEnd
500+ }
501+ if off < fr .End {
502+ markHostHoleUncommitted (memmap.FileRange {Start : off , End : fr .End }, wasCommitted )
503+ }
504+ } else {
505+ scanRange (fr , wasCommitted )
506+ }
447507 // We need to flush batched updates to f.memAcct whenever potentially
448508 // reaching the end of a segment, in order to maintain the invariant
449509 // that updatePendingFR corresponds to a single segment.
@@ -523,6 +583,69 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
523583 return nil
524584}
525585
586+ const extentScanMinHoleFractionDivisor = 8
587+
588+ // hostFileUsageShowsEnoughHoles reports whether the host file allocation proves
589+ // that more than one eighth of the accounted MemoryFile ranges are holes.
590+ // Finding holes in a dense tmpfs file may scan the same pages as SaveTo. File
591+ // allocation outside the accounted ranges can only prevent this optimization.
592+ func hostFileUsageShowsEnoughHoles (backingFileUsageBytes , accountedBytes uint64 ) bool {
593+ if accountedBytes == 0 {
594+ return false
595+ }
596+ return backingFileUsageBytes < accountedBytes - accountedBytes / extentScanMinHoleFractionDivisor
597+ }
598+
599+ // hostFileDataIterator iterates over page-aligned ranges reported by SEEK_DATA.
600+ // SaveTo still scans these ranges because they may contain zero pages. The
601+ // iterator holds at most one range regardless of file fragmentation.
602+ type hostFileDataIterator struct {
603+ size uint64
604+ next uint64
605+ current memmap.FileRange
606+ done bool
607+ seek func (offset int64 , whence int ) (int64 , error )
608+ }
609+
610+ func (it * hostFileDataIterator ) rangeAtOrAfter (off uint64 ) (memmap.FileRange , bool , error ) {
611+ if it .size % hostarch .PageSize != 0 {
612+ return memmap.FileRange {}, false , fmt .Errorf ("host file size %#x is not page-aligned" , it .size )
613+ }
614+ for it .current .End <= off {
615+ it .current = memmap.FileRange {}
616+ if it .next < off {
617+ it .next = off
618+ }
619+ if it .done || it .next >= it .size {
620+ return memmap.FileRange {}, false , nil
621+ }
622+ data , err := it .seek (int64 (it .next ), unix .SEEK_DATA )
623+ if err == unix .ENXIO {
624+ it .done = true
625+ return memmap.FileRange {}, false , nil
626+ }
627+ if err != nil {
628+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_DATA from %#x: %w" , it .next , err )
629+ }
630+ if data < int64 (it .next ) || uint64 (data ) >= it .size {
631+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_DATA from %#x returned %#x for file size %#x" , it .next , data , it .size )
632+ }
633+ hole , err := it .seek (data , unix .SEEK_HOLE )
634+ if err != nil {
635+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_HOLE from %#x: %w" , data , err )
636+ }
637+ if hole <= data || uint64 (hole ) > it .size {
638+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_HOLE from %#x returned %#x for file size %#x" , data , hole , it .size )
639+ }
640+ it .current = memmap.FileRange {
641+ Start : hostarch .PageRoundDown (uint64 (data )),
642+ End : hostarch .MustPageRoundUp (uint64 (hole )),
643+ }.Intersect (memmap.FileRange {End : it .size })
644+ it .next = uint64 (hole )
645+ }
646+ return it .current , true , nil
647+ }
648+
526649// AsyncPagesFileSave holds async page saving state for a single pages file.
527650type AsyncPagesFileSave struct {
528651 mu apfsMutex
0 commit comments