mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-21 08:17:34 +00:00
first look at making region writes reuse old space
This commit is contained in:
parent
da42c8d020
commit
6a7b77fee2
154
src/pocketmine/level/format/io/region/RegionGarbageMap.php
Normal file
154
src/pocketmine/level/format/io/region/RegionGarbageMap.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\level\format\io\region;
|
||||
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function end;
|
||||
use function ksort;
|
||||
use function time;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
final class RegionGarbageMap{
|
||||
|
||||
/** @var RegionLocationTableEntry[] */
|
||||
private $entries = [];
|
||||
/** @var bool */
|
||||
private $clean = false;
|
||||
|
||||
/**
|
||||
* @param RegionLocationTableEntry[] $entries
|
||||
*/
|
||||
public function __construct(array $entries){
|
||||
foreach($entries as $entry){
|
||||
$this->entries[$entry->getFirstSector()] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RegionLocationTableEntry[]|null[] $locationTable
|
||||
*/
|
||||
public static function buildFromLocationTable(array $locationTable) : self{
|
||||
/** @var RegionLocationTableEntry[] $usedMap */
|
||||
$usedMap = [];
|
||||
foreach($locationTable as $entry){
|
||||
if($entry === null){
|
||||
continue;
|
||||
}
|
||||
if(isset($usedMap[$entry->getFirstSector()])){
|
||||
throw new AssumptionFailedError("Overlapping entries detected");
|
||||
}
|
||||
$usedMap[$entry->getFirstSector()] = $entry;
|
||||
}
|
||||
|
||||
ksort($usedMap, SORT_NUMERIC);
|
||||
|
||||
/** @var RegionLocationTableEntry[] $garbageMap */
|
||||
$garbageMap = [];
|
||||
|
||||
/** @var RegionLocationTableEntry|null $prevEntry */
|
||||
$prevEntry = null;
|
||||
foreach($usedMap as $firstSector => $entry){
|
||||
$expectedStart = ($prevEntry !== null ? $prevEntry->getLastSector() + 1 : RegionLoader::FIRST_SECTOR);
|
||||
$actualStart = $entry->getFirstSector();
|
||||
if($expectedStart < $actualStart){
|
||||
//found a gap in the table
|
||||
$garbageMap[$expectedStart] = new RegionLocationTableEntry($expectedStart, $actualStart - $expectedStart, 0);
|
||||
}
|
||||
$prevEntry = $entry;
|
||||
}
|
||||
|
||||
return new self($garbageMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegionLocationTableEntry[]
|
||||
* @phpstan-return array<int, RegionLocationTableEntry>
|
||||
*/
|
||||
public function getArray() : array{
|
||||
if(!$this->clean){
|
||||
ksort($this->entries, SORT_NUMERIC);
|
||||
|
||||
/** @var int|null $prevIndex */
|
||||
$prevIndex = null;
|
||||
foreach($this->entries as $k => $entry){
|
||||
if($prevIndex !== null and $this->entries[$prevIndex]->getLastSector() + 1 === $entry->getFirstSector()){
|
||||
//this SHOULD overwrite the previous index and not appear at the end
|
||||
$this->entries[$prevIndex] = new RegionLocationTableEntry(
|
||||
$this->entries[$prevIndex]->getFirstSector(),
|
||||
$this->entries[$prevIndex]->getSectorCount() + $entry->getSectorCount(),
|
||||
0
|
||||
);
|
||||
unset($this->entries[$k]);
|
||||
}else{
|
||||
$prevIndex = $k;
|
||||
}
|
||||
}
|
||||
$this->clean = true;
|
||||
}
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
public function add(RegionLocationTableEntry $entry) : void{
|
||||
if(isset($this->entries[$k = $entry->getFirstSector()])){
|
||||
throw new \InvalidArgumentException("Overlapping entry starting at " . $k);
|
||||
}
|
||||
$this->entries[$k] = $entry;
|
||||
$this->clean = false;
|
||||
}
|
||||
|
||||
public function remove(RegionLocationTableEntry $entry) : void{
|
||||
if(isset($this->entries[$k = $entry->getFirstSector()])){
|
||||
//removal doesn't affect ordering and shouldn't affect fragmentation
|
||||
unset($this->entries[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
public function end() : ?RegionLocationTableEntry{
|
||||
$array = $this->getArray();
|
||||
$end = end($array);
|
||||
return $end !== false ? $end : null;
|
||||
}
|
||||
|
||||
public function allocate(int $newSize) : ?RegionLocationTableEntry{
|
||||
foreach($this->getArray() as $start => $candidate){
|
||||
$candidateSize = $candidate->getSectorCount();
|
||||
if($candidateSize < $newSize){
|
||||
continue;
|
||||
}
|
||||
|
||||
$newLocation = new RegionLocationTableEntry($candidate->getFirstSector(), $newSize, time());
|
||||
$this->remove($candidate);
|
||||
|
||||
if($candidateSize > $newSize){ //we're not using the whole area, just take part of it
|
||||
$newGarbageStart = $candidate->getFirstSector() + $newSize;
|
||||
$newGarbageSize = $candidateSize - $newSize;
|
||||
$this->add(new RegionLocationTableEntry($newGarbageStart, $newGarbageSize, 0));
|
||||
}
|
||||
return $newLocation;
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@ class RegionLoader{
|
||||
public const MAX_SECTOR_LENGTH = 255 << 12; //255 sectors (~0.996 MiB)
|
||||
public const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps
|
||||
|
||||
private const FIRST_SECTOR = 2; //location table occupies 0 and 1
|
||||
public const FIRST_SECTOR = 2; //location table occupies 0 and 1
|
||||
|
||||
/** @var int */
|
||||
public static $COMPRESSION_LEVEL = 7;
|
||||
@ -82,6 +82,8 @@ class RegionLoader{
|
||||
protected $nextSector = self::FIRST_SECTOR;
|
||||
/** @var RegionLocationTableEntry[]|null[] */
|
||||
protected $locationTable = [];
|
||||
/** @var RegionGarbageMap */
|
||||
protected $garbageTable;
|
||||
/** @var int */
|
||||
public $lastUsed = 0;
|
||||
|
||||
@ -89,6 +91,7 @@ class RegionLoader{
|
||||
$this->x = $regionX;
|
||||
$this->z = $regionZ;
|
||||
$this->filePath = $filePath;
|
||||
$this->garbageTable = new RegionGarbageMap([]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -199,18 +202,49 @@ class RegionLoader{
|
||||
$newSize = (int) ceil(($length + 4) / 4096);
|
||||
$index = self::getChunkOffset($x, $z);
|
||||
|
||||
if($this->locationTable[$index] === null or $this->locationTable[$index]->getSectorCount() < $newSize){
|
||||
$offset = $this->nextSector;
|
||||
}else{
|
||||
$offset = $this->locationTable[$index]->getFirstSector(); //reuse old location - TODO: risk of corruption during power failure
|
||||
/*
|
||||
* look for an unused area big enough to hold this data
|
||||
* this is corruption-resistant (it leaves the old data intact if a failure occurs when writing new data), and
|
||||
* also allows the file to become more compact across consecutive writes without introducing a dedicated garbage
|
||||
* collection mechanism.
|
||||
*/
|
||||
$newLocation = $this->garbageTable->allocate($newSize);
|
||||
|
||||
/* if no gaps big enough were found, append to the end of the file instead */
|
||||
if($newLocation === null){
|
||||
$newLocation = new RegionLocationTableEntry($this->nextSector, $newSize, time());
|
||||
$this->bumpNextFreeSector($newLocation);
|
||||
}
|
||||
|
||||
$this->bumpNextFreeSector($this->locationTable[$index] = new RegionLocationTableEntry($offset, $newSize, time()));
|
||||
|
||||
fseek($this->filePointer, $offset << 12);
|
||||
/* write the chunk data into the chosen location */
|
||||
fseek($this->filePointer, $newLocation->getFirstSector() << 12);
|
||||
fwrite($this->filePointer, str_pad(Binary::writeInt($length) . chr(self::COMPRESSION_ZLIB) . $chunkData, $newSize << 12, "\x00", STR_PAD_RIGHT));
|
||||
|
||||
/*
|
||||
* update the file header - we do this after writing the main data, so that if a failure occurs while writing,
|
||||
* the header will still point to the old (intact) copy of the chunk, instead of a potentially broken new
|
||||
* version of the file (e.g. partially written).
|
||||
*/
|
||||
$oldLocation = $this->locationTable[$index];
|
||||
$this->locationTable[$index] = $newLocation;
|
||||
$this->writeLocationIndex($index);
|
||||
|
||||
if($oldLocation !== null){
|
||||
/* release the area containing the old copy to the garbage pool */
|
||||
$this->garbageTable->add($oldLocation);
|
||||
|
||||
$endGarbage = $this->garbageTable->end();
|
||||
$nextSector = $this->nextSector;
|
||||
for(; $endGarbage !== null and $endGarbage->getLastSector() + 1 === $nextSector; $endGarbage = $this->garbageTable->end()){
|
||||
$nextSector = $endGarbage->getFirstSector();
|
||||
$this->garbageTable->remove($endGarbage);
|
||||
}
|
||||
|
||||
if($nextSector !== $this->nextSector){
|
||||
$this->nextSector = $nextSector;
|
||||
ftruncate($this->filePointer, $this->nextSector << 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -289,6 +323,8 @@ class RegionLoader{
|
||||
|
||||
$this->checkLocationTableValidity();
|
||||
|
||||
$this->garbageTable = RegionGarbageMap::buildFromLocationTable($this->locationTable);
|
||||
|
||||
fseek($this->filePointer, 0);
|
||||
}
|
||||
|
||||
|
@ -42,8 +42,8 @@ class RegionLocationTableEntry{
|
||||
throw new \InvalidArgumentException("Start sector must be positive, got $firstSector");
|
||||
}
|
||||
$this->firstSector = $firstSector;
|
||||
if($sectorCount < 1 or $sectorCount > 255){
|
||||
throw new \InvalidArgumentException("Sector count must be in range 1...255, got $sectorCount");
|
||||
if($sectorCount < 1){
|
||||
throw new \InvalidArgumentException("Sector count must be positive, got $sectorCount");
|
||||
}
|
||||
$this->sectorCount = $sectorCount;
|
||||
$this->timestamp = $timestamp;
|
||||
|
Loading…
x
Reference in New Issue
Block a user