@@ -185,7 +185,7 @@ type SaveOpts struct {
185185}
186186
187187// SaveTo writes f's state to the given stream.
188- func (f * MemoryFile ) SaveTo (ctx context.Context , w io.Writer , opts * SaveOpts ) error {
188+ func (f * MemoryFile ) SaveTo (ctx context.Context , w io.Writer , opts * SaveOpts ) ( retErr error ) {
189189 if err := f .AwaitLoadAll (); err != nil {
190190 return fmt .Errorf ("previous async page loading failed: %w" , err )
191191 }
@@ -373,6 +373,33 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
373373 return maseg
374374 }
375375
376+ var hostData * hostFileDataIterator
377+ if ! f .opts .DiskBackedFile {
378+ fileSize := f .TotalSize ()
379+ fd := int (f .file .Fd ())
380+ backingFileUsageBytes , err := f .TotalUsage ()
381+ if err != nil {
382+ log .Debugf ("MemoryFile(%p): falling back to page scanning because backing file usage is unavailable: %v" , f , err )
383+ } else if accountedBytes := uint64 (f .memAcct .Span ()); hostFileUsageShowsEnoughHoles (backingFileUsageBytes , accountedBytes ) {
384+ off , err := unix .Seek (fd , 0 , unix .SEEK_CUR )
385+ if err != nil {
386+ log .Debugf ("MemoryFile(%p): falling back to page scanning because the backing file offset is unavailable: %v" , f , err )
387+ } else {
388+ hostData = & hostFileDataIterator {
389+ size : fileSize ,
390+ seek : func (offset int64 , whence int ) (int64 , error ) {
391+ return unix .Seek (fd , offset , whence )
392+ },
393+ }
394+ defer func () {
395+ if _ , err := unix .Seek (fd , off , unix .SEEK_SET ); retErr == nil && err != nil {
396+ retErr = fmt .Errorf ("failed to restore host file offset: %w" , err )
397+ }
398+ }()
399+ }
400+ }
401+ }
402+
376403 zeroPage := make ([]byte , hostarch .PageSize )
377404 // f.mu is unlocked below, allowing concurrent calls to f.UpdateUsage() to
378405 // observe pages that we transiently commit (for comparisons to zero) or
@@ -389,30 +416,7 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
389416 f .commitSeq = 0
390417 maseg := f .memAcct .FirstSegment ()
391418 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- }
419+ scanRange := func (fr memmap.FileRange , wasCommitted bool ) {
416420 f .forEachChunk (fr , func (chunk * chunkInfo , chunkFR memmap.FileRange ) bool {
417421 bs := chunk .sliceAt (chunkFR )
418422 for pgoff := 0 ; pgoff < len (bs ); pgoff += hostarch .PageSize {
@@ -444,6 +448,61 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
444448 f .mu .Lock ()
445449 return true
446450 })
451+ }
452+ markHostHoleUncommitted := func (fr memmap.FileRange , wasCommitted bool ) {
453+ if wasCommitted {
454+ newUncommittedBytes += fr .Length ()
455+ } else {
456+ alreadyUncommittedBytes += fr .Length ()
457+ }
458+ maseg = updateAddRange (maseg , fr , wasCommitted , false /* nowCommitted */ )
459+ }
460+ for maseg .Ok () {
461+ ma := maseg .ValuePtr ()
462+ if ma .wasteOrReleasing {
463+ // This shouldn't be possible since we waited for memory release
464+ // above, and f shouldn't be mutated during saving.
465+ panic (fmt .Sprintf ("found waste or releasing pages %v during pgalloc.MemoryFile.SaveTo()" , maseg .Range ()))
466+ }
467+ fr := maseg .Range ()
468+ if fr .Start < unscannedStart {
469+ fr .Start = unscannedStart
470+ }
471+ unscannedStart = fr .End
472+ allocatedBytes += fr .Length ()
473+ ma .commitSeq = 0
474+ wasCommitted := ma .knownCommitted
475+ if ! opts .ExcludeCommittedZeroPages && wasCommitted {
476+ alreadyCommittedBytes += fr .Length ()
477+ maseg = updateAddRange (maseg , fr , true /* wasCommitted */ , true /* nowCommitted */ )
478+ } else if hostData != nil {
479+ off := fr .Start
480+ for off < fr .End {
481+ dataFR , ok , err := hostData .rangeAtOrAfter (off )
482+ if err != nil {
483+ log .Debugf ("MemoryFile(%p): scanning remaining pages because backing file extents are unavailable: %v" , f , err )
484+ hostData = nil
485+ scanRange (memmap.FileRange {Start : off , End : fr .End }, wasCommitted )
486+ off = fr .End
487+ break
488+ }
489+ if ! ok || dataFR .Start >= fr .End {
490+ break
491+ }
492+ dataStart := max (off , dataFR .Start )
493+ if off < dataStart {
494+ markHostHoleUncommitted (memmap.FileRange {Start : off , End : dataStart }, wasCommitted )
495+ }
496+ dataEnd := min (fr .End , dataFR .End )
497+ scanRange (memmap.FileRange {Start : dataStart , End : dataEnd }, wasCommitted )
498+ off = dataEnd
499+ }
500+ if off < fr .End {
501+ markHostHoleUncommitted (memmap.FileRange {Start : off , End : fr .End }, wasCommitted )
502+ }
503+ } else {
504+ scanRange (fr , wasCommitted )
505+ }
447506 // We need to flush batched updates to f.memAcct whenever potentially
448507 // reaching the end of a segment, in order to maintain the invariant
449508 // that updatePendingFR corresponds to a single segment.
@@ -523,6 +582,61 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
523582 return nil
524583}
525584
585+ const extentScanMinHoleFractionDivisor = 8
586+
587+ // hostFileUsageShowsEnoughHoles reports whether the host file allocation proves
588+ // that more than one eighth of the accounted MemoryFile ranges are holes.
589+ // Finding holes in a dense tmpfs file may scan the same pages as SaveTo. File
590+ // allocation outside the accounted ranges can only prevent this optimization.
591+ func hostFileUsageShowsEnoughHoles (backingFileUsageBytes , accountedBytes uint64 ) bool {
592+ if accountedBytes == 0 {
593+ return false
594+ }
595+ return backingFileUsageBytes < accountedBytes - accountedBytes / extentScanMinHoleFractionDivisor
596+ }
597+
598+ // hostFileDataIterator iterates over page-aligned ranges reported by SEEK_DATA.
599+ // SaveTo still scans these ranges because they may contain zero pages. The
600+ // iterator holds at most one range regardless of file fragmentation.
601+ type hostFileDataIterator struct {
602+ size uint64
603+ current memmap.FileRange
604+ done bool
605+ seek func (offset int64 , whence int ) (int64 , error )
606+ }
607+
608+ func (it * hostFileDataIterator ) rangeAtOrAfter (off uint64 ) (memmap.FileRange , bool , error ) {
609+ for it .current .End <= off {
610+ it .current = memmap.FileRange {}
611+ if it .done || off >= it .size {
612+ return memmap.FileRange {}, false , nil
613+ }
614+ data , err := it .seek (int64 (off ), unix .SEEK_DATA )
615+ if err == unix .ENXIO {
616+ it .done = true
617+ return memmap.FileRange {}, false , nil
618+ }
619+ if err != nil {
620+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_DATA from %#x: %w" , off , err )
621+ }
622+ if data < int64 (off ) || uint64 (data ) >= it .size {
623+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_DATA from %#x returned %#x for file size %#x" , off , data , it .size )
624+ }
625+ hole , err := it .seek (data , unix .SEEK_HOLE )
626+ if err != nil {
627+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_HOLE from %#x: %w" , data , err )
628+ }
629+ if hole <= data || uint64 (hole ) > it .size {
630+ return memmap.FileRange {}, false , fmt .Errorf ("SEEK_HOLE from %#x returned %#x for file size %#x" , data , hole , it .size )
631+ }
632+ it .current = memmap.FileRange {
633+ Start : hostarch .PageRoundDown (uint64 (data )),
634+ End : hostarch .MustPageRoundUp (uint64 (hole )),
635+ }.Intersect (memmap.FileRange {End : it .size })
636+ }
637+ return it .current , true , nil
638+ }
639+
526640// AsyncPagesFileSave holds async page saving state for a single pages file.
527641type AsyncPagesFileSave struct {
528642 mu apfsMutex
0 commit comments