mirror of https://github.com/aria2/aria2
				
				
				
			Write data in 4K aligned offset in write with disk cache enabled
This greatly reduces disk activity especially on Win + NTFS. Not so much difference on Linux.pull/36/head
							parent
							
								
									911851debb
								
							
						
					
					
						commit
						9ed8502e74
					
				| 
						 | 
				
			
			@ -38,6 +38,8 @@
 | 
			
		|||
#include "DiskWriter.h"
 | 
			
		||||
#include "FileEntry.h"
 | 
			
		||||
#include "TruncFileAllocationIterator.h"
 | 
			
		||||
#include "WrDiskCacheEntry.h"
 | 
			
		||||
#include "LogFactory.h"
 | 
			
		||||
#ifdef HAVE_SOME_FALLOCATE
 | 
			
		||||
# include "FallocFileAllocationIterator.h"
 | 
			
		||||
#endif // HAVE_SOME_FALLOCATE
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +83,51 @@ ssize_t AbstractSingleDiskAdaptor::readData
 | 
			
		|||
  return diskWriter_->readData(data, len, offset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AbstractSingleDiskAdaptor::writeCache(const WrDiskCacheEntry* entry)
 | 
			
		||||
{
 | 
			
		||||
  // Write cached data in 4KiB aligned offset. This reduces disk
 | 
			
		||||
  // activity especially on Windows 7 NTFS. In this code, we assume
 | 
			
		||||
  // that maximum length of DataCell data is 16KiB to simplify the
 | 
			
		||||
  // code.
 | 
			
		||||
  unsigned char buf[16*1024];
 | 
			
		||||
  int64_t start = 0;
 | 
			
		||||
  size_t buflen = 0;
 | 
			
		||||
  size_t buffoffset = 0;
 | 
			
		||||
  const WrDiskCacheEntry::DataCellSet& dataSet = entry->getDataSet();
 | 
			
		||||
  for(WrDiskCacheEntry::DataCellSet::const_iterator i = dataSet.begin(),
 | 
			
		||||
        eoi = dataSet.end(); i != eoi; ++i) {
 | 
			
		||||
    if(start+static_cast<ssize_t>(buflen) < (*i)->goff) {
 | 
			
		||||
      A2_LOG_DEBUG(fmt("Cache flush goff=%"PRId64", len=%lu",
 | 
			
		||||
                       start, static_cast<unsigned long>(buflen)));
 | 
			
		||||
      writeData(buf+buffoffset, buflen-buffoffset, start);
 | 
			
		||||
      start = (*i)->goff;
 | 
			
		||||
      buflen = buffoffset = 0;
 | 
			
		||||
    }
 | 
			
		||||
    if(buflen == 0 && ((*i)->goff & 0xfff) == 0 && ((*i)->len & 0xfff) == 0) {
 | 
			
		||||
      // Already aligned. Write it without copy.
 | 
			
		||||
      writeData((*i)->data + (*i)->offset, (*i)->len, start);
 | 
			
		||||
      start += (*i)->len;
 | 
			
		||||
    } else {
 | 
			
		||||
      if(buflen == 0) {
 | 
			
		||||
        buflen = buffoffset = (*i)->goff & 0xfff;
 | 
			
		||||
      }
 | 
			
		||||
      size_t wlen = std::min(sizeof(buf) - buflen, (*i)->len);
 | 
			
		||||
      memcpy(buf+buflen, (*i)->data+(*i)->offset, wlen);
 | 
			
		||||
      buflen += wlen;
 | 
			
		||||
      if(buflen == sizeof(buf)) {
 | 
			
		||||
        A2_LOG_DEBUG(fmt("Cache flush goff=%"PRId64", len=%lu",
 | 
			
		||||
                         start, static_cast<unsigned long>(buflen)));
 | 
			
		||||
        writeData(buf+buffoffset, buflen-buffoffset, start);
 | 
			
		||||
        memcpy(buf, (*i)->data + (*i)->offset + wlen, (*i)->len - wlen);
 | 
			
		||||
        start += sizeof(buf) - buffoffset;
 | 
			
		||||
        buflen = (*i)->len - wlen;
 | 
			
		||||
        buffoffset = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  writeData(buf+buffoffset, buflen-buffoffset, start);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AbstractSingleDiskAdaptor::fileExists()
 | 
			
		||||
{
 | 
			
		||||
  return File(getFilePath()).exists();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,6 +65,8 @@ public:
 | 
			
		|||
 | 
			
		||||
  virtual ssize_t readData(unsigned char* data, size_t len, int64_t offset);
 | 
			
		||||
 | 
			
		||||
  virtual void writeCache(const WrDiskCacheEntry* entry);
 | 
			
		||||
 | 
			
		||||
  virtual bool fileExists();
 | 
			
		||||
 | 
			
		||||
  virtual int64_t size();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,7 @@ namespace aria2 {
 | 
			
		|||
 | 
			
		||||
class FileEntry;
 | 
			
		||||
class FileAllocationIterator;
 | 
			
		||||
class WrDiskCacheEntry;
 | 
			
		||||
 | 
			
		||||
class DiskAdaptor:public BinaryStream {
 | 
			
		||||
public:
 | 
			
		||||
| 
						 | 
				
			
			@ -105,6 +106,9 @@ public:
 | 
			
		|||
  // successfully changed.
 | 
			
		||||
  virtual size_t utime(const Time& actime, const Time& modtime) = 0;
 | 
			
		||||
 | 
			
		||||
  // Writes cached data to the underlying disk.
 | 
			
		||||
  virtual void writeCache(const WrDiskCacheEntry* entry) = 0;
 | 
			
		||||
 | 
			
		||||
  void setFileAllocationMethod(FileAllocationMethod method)
 | 
			
		||||
  {
 | 
			
		||||
    fileAllocationMethod_ = method;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@
 | 
			
		|||
#include "Logger.h"
 | 
			
		||||
#include "LogFactory.h"
 | 
			
		||||
#include "SimpleRandomizer.h"
 | 
			
		||||
#include "WrDiskCacheEntry.h"
 | 
			
		||||
 | 
			
		||||
namespace aria2 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -419,6 +420,103 @@ ssize_t MultiDiskAdaptor::readData
 | 
			
		|||
  return totalReadLength;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MultiDiskAdaptor::writeCache(const WrDiskCacheEntry* entry)
 | 
			
		||||
{
 | 
			
		||||
  // Write cached data in 4KiB aligned offset. This reduces disk
 | 
			
		||||
  // activity especially on Windows 7 NTFS.
 | 
			
		||||
  unsigned char buf[16*1024];
 | 
			
		||||
  size_t buflen = 0;
 | 
			
		||||
  size_t buffoffset = 0;
 | 
			
		||||
  const WrDiskCacheEntry::DataCellSet& dataSet = entry->getDataSet();
 | 
			
		||||
  if(dataSet.empty()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  DiskWriterEntries::const_iterator dent =
 | 
			
		||||
    findFirstDiskWriterEntry(diskWriterEntries_, (*dataSet.begin())->goff),
 | 
			
		||||
    eod = diskWriterEntries_.end();
 | 
			
		||||
  WrDiskCacheEntry::DataCellSet::const_iterator i = dataSet.begin(),
 | 
			
		||||
    eoi = dataSet.end();
 | 
			
		||||
  size_t celloff = 0;
 | 
			
		||||
  for(; dent != eod; ++dent) {
 | 
			
		||||
    int64_t lstart = 0, lp = 0;
 | 
			
		||||
    const SharedHandle<FileEntry>& fent = (*dent)->getFileEntry();
 | 
			
		||||
    for(; i != eoi;) {
 | 
			
		||||
      if(std::max(fent->getOffset(),
 | 
			
		||||
                  static_cast<int64_t>((*i)->goff + celloff)) <
 | 
			
		||||
         std::min(fent->getLastOffset(),
 | 
			
		||||
                  static_cast<int64_t>((*i)->goff + (*i)->len))) {
 | 
			
		||||
        openIfNot(*dent, &DiskWriterEntry::openFile);
 | 
			
		||||
        if(!(*dent)->isOpen()) {
 | 
			
		||||
          throwOnDiskWriterNotOpened(*dent, (*i)->goff + celloff);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        A2_LOG_DEBUG(fmt("%s Cache flush loff=%"PRId64", len=%lu",
 | 
			
		||||
                         fent->getPath().c_str(),
 | 
			
		||||
                         lstart,
 | 
			
		||||
                         static_cast<unsigned long>(buflen-buffoffset)));
 | 
			
		||||
        (*dent)->getDiskWriter()->
 | 
			
		||||
          writeData(buf + buffoffset, buflen - buffoffset, lstart);
 | 
			
		||||
        buflen = buffoffset = 0;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      int64_t loff = fent->gtoloff((*i)->goff + celloff);
 | 
			
		||||
      if(static_cast<int64_t>(lstart + buflen) < loff) {
 | 
			
		||||
        A2_LOG_DEBUG(fmt("%s Cache flush loff=%"PRId64", len=%lu",
 | 
			
		||||
                         fent->getPath().c_str(),
 | 
			
		||||
                         lstart,
 | 
			
		||||
                         static_cast<unsigned long>(buflen-buffoffset)));
 | 
			
		||||
        (*dent)->getDiskWriter()->
 | 
			
		||||
          writeData(buf + buffoffset, buflen - buffoffset, lstart);
 | 
			
		||||
        lstart = lp = loff;
 | 
			
		||||
        buflen = buffoffset = 0;
 | 
			
		||||
      }
 | 
			
		||||
      // If the position of the cache data is not aligned, offset
 | 
			
		||||
      // buffer so that next write can be aligned.
 | 
			
		||||
      if(buflen == 0) {
 | 
			
		||||
        buflen = buffoffset = loff & 0xfff;
 | 
			
		||||
      }
 | 
			
		||||
      assert((*i)->len > celloff);
 | 
			
		||||
      for(;;) {
 | 
			
		||||
        size_t wlen = std::min(static_cast<int64_t>((*i)->len - celloff),
 | 
			
		||||
                               fent->getLength() - lp);
 | 
			
		||||
        wlen = std::min(wlen, sizeof(buf) - buflen);
 | 
			
		||||
        memcpy(buf + buflen, (*i)->data + (*i)->offset + celloff, wlen);
 | 
			
		||||
        buflen += wlen;
 | 
			
		||||
        celloff += wlen;
 | 
			
		||||
        lp += wlen;
 | 
			
		||||
        if(lp == fent->getLength() || buflen == sizeof(buf)) {
 | 
			
		||||
          A2_LOG_DEBUG(fmt("%s Cache flush loff=%"PRId64", len=%lu",
 | 
			
		||||
                           fent->getPath().c_str(),
 | 
			
		||||
                           lstart,
 | 
			
		||||
                           static_cast<unsigned long>(buflen-buffoffset)));
 | 
			
		||||
          (*dent)->getDiskWriter()->
 | 
			
		||||
            writeData(buf + buffoffset, buflen - buffoffset, lstart);
 | 
			
		||||
          lstart += buflen - buffoffset;
 | 
			
		||||
          lp = lstart;
 | 
			
		||||
          buflen = buffoffset = 0;
 | 
			
		||||
        }
 | 
			
		||||
        if(lp == fent->getLength() || celloff == (*i)->len) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if(celloff == (*i)->len) {
 | 
			
		||||
        ++i;
 | 
			
		||||
        celloff = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if(i == eoi) {
 | 
			
		||||
      A2_LOG_DEBUG(fmt("%s Cache flush loff=%"PRId64", len=%lu",
 | 
			
		||||
                       fent->getPath().c_str(),
 | 
			
		||||
                       lstart,
 | 
			
		||||
                       static_cast<unsigned long>(buflen - buffoffset)));
 | 
			
		||||
      (*dent)->getDiskWriter()->
 | 
			
		||||
        writeData(buf + buffoffset, buflen - buffoffset, lstart);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  assert(i == eoi);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool MultiDiskAdaptor::fileExists()
 | 
			
		||||
{
 | 
			
		||||
  return std::find_if(getFileEntries().begin(), getFileEntries().end(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,6 +135,8 @@ public:
 | 
			
		|||
 | 
			
		||||
  virtual ssize_t readData(unsigned char* data, size_t len, int64_t offset);
 | 
			
		||||
 | 
			
		||||
  virtual void writeCache(const WrDiskCacheEntry* entry);
 | 
			
		||||
 | 
			
		||||
  virtual bool fileExists();
 | 
			
		||||
 | 
			
		||||
  virtual int64_t size();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,15 +75,9 @@ void WrDiskCacheEntry::writeToDisk()
 | 
			
		|||
{
 | 
			
		||||
  DataCellSet::iterator i = set_.begin(), eoi = set_.end();
 | 
			
		||||
  try {
 | 
			
		||||
    for(; i != eoi; ++i) {
 | 
			
		||||
      A2_LOG_DEBUG(fmt("WrDiskCacheEntry flush goff=%"PRId64", len=%lu",
 | 
			
		||||
                       (*i)->goff, static_cast<unsigned long>((*i)->len)));
 | 
			
		||||
      diskAdaptor_->writeData((*i)->data+(*i)->offset, (*i)->len,
 | 
			
		||||
                              (*i)->goff);
 | 
			
		||||
    }
 | 
			
		||||
    diskAdaptor_->writeCache(this);
 | 
			
		||||
  } catch(RecoverableException& e) {
 | 
			
		||||
    A2_LOG_ERROR(fmt("WrDiskCacheEntry flush error goff=%"PRId64", len=%lu",
 | 
			
		||||
                     (*i)->goff, static_cast<unsigned long>((*i)->len)));
 | 
			
		||||
    A2_LOG_ERROR("WrDiskCacheEntry flush error");
 | 
			
		||||
    error_ = CACHE_ERR_ERROR;
 | 
			
		||||
    errorCode_ = e.getErrorCode();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,8 @@
 | 
			
		|||
#include "Exception.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
#include "TestUtil.h"
 | 
			
		||||
#include "ByteArrayDiskWriter.h"
 | 
			
		||||
#include "WrDiskCacheEntry.h"
 | 
			
		||||
 | 
			
		||||
namespace aria2 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +16,7 @@ class DirectDiskAdaptorTest:public CppUnit::TestFixture {
 | 
			
		|||
 | 
			
		||||
  CPPUNIT_TEST_SUITE(DirectDiskAdaptorTest);
 | 
			
		||||
  CPPUNIT_TEST(testCutTrailingGarbage);
 | 
			
		||||
  CPPUNIT_TEST(testWriteCache);
 | 
			
		||||
  CPPUNIT_TEST_SUITE_END();
 | 
			
		||||
public:
 | 
			
		||||
  void setUp() {}
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +24,7 @@ public:
 | 
			
		|||
  void tearDown() {}
 | 
			
		||||
 | 
			
		||||
  void testCutTrailingGarbage();
 | 
			
		||||
  void testWriteCache();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,4 +54,30 @@ void DirectDiskAdaptorTest::testCutTrailingGarbage()
 | 
			
		|||
                       File(entry->getPath()).size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DirectDiskAdaptorTest::testWriteCache()
 | 
			
		||||
{
 | 
			
		||||
  SharedHandle<DirectDiskAdaptor> adaptor(new DirectDiskAdaptor());
 | 
			
		||||
  SharedHandle<ByteArrayDiskWriter> dw(new ByteArrayDiskWriter());
 | 
			
		||||
  adaptor->setDiskWriter(dw);
 | 
			
		||||
  WrDiskCacheEntry cache(adaptor);
 | 
			
		||||
  std::string data1(4096, '1'), data2(4094, '2');
 | 
			
		||||
  cache.cacheData(createDataCell(5, data1.c_str()));
 | 
			
		||||
  cache.cacheData(createDataCell(5+data1.size(), data2.c_str()));
 | 
			
		||||
  adaptor->writeCache(&cache);
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(data1+data2, dw->getString().substr(5));
 | 
			
		||||
 | 
			
		||||
  cache.clear();
 | 
			
		||||
  dw->setString("");
 | 
			
		||||
  cache.cacheData(createDataCell(4096, data1.c_str()));
 | 
			
		||||
  adaptor->writeCache(&cache);
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(data1, dw->getString().substr(4096));
 | 
			
		||||
 | 
			
		||||
  cache.clear();
 | 
			
		||||
  dw->setString("???????");
 | 
			
		||||
  cache.cacheData(createDataCell(0, "abc"));
 | 
			
		||||
  cache.cacheData(createDataCell(4, "efg"));
 | 
			
		||||
  adaptor->writeCache(&cache);
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(std::string("abc?efg"), dw->getString());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace aria2
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@
 | 
			
		|||
#include "array_fun.h"
 | 
			
		||||
#include "TestUtil.h"
 | 
			
		||||
#include "DiskWriter.h"
 | 
			
		||||
#include "WrDiskCacheEntry.h"
 | 
			
		||||
 | 
			
		||||
namespace aria2 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +25,7 @@ class MultiDiskAdaptorTest:public CppUnit::TestFixture {
 | 
			
		|||
  CPPUNIT_TEST(testSize);
 | 
			
		||||
  CPPUNIT_TEST(testUtime);
 | 
			
		||||
  CPPUNIT_TEST(testResetDiskWriterEntries);
 | 
			
		||||
  CPPUNIT_TEST(testWriteCache);
 | 
			
		||||
  CPPUNIT_TEST_SUITE_END();
 | 
			
		||||
private:
 | 
			
		||||
  SharedHandle<MultiDiskAdaptor> adaptor;
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +41,7 @@ public:
 | 
			
		|||
  void testSize();
 | 
			
		||||
  void testUtime();
 | 
			
		||||
  void testResetDiskWriterEntries();
 | 
			
		||||
  void testWriteCache();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -453,4 +456,46 @@ void MultiDiskAdaptorTest::testUtime()
 | 
			
		|||
                 File(entries[2]->getPath()).getModifiedTime().getTime());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MultiDiskAdaptorTest::testWriteCache()
 | 
			
		||||
{
 | 
			
		||||
  std::string storeDir =
 | 
			
		||||
    A2_TEST_OUT_DIR"/aria2_MultiDiskAdaptorTest_testWriteCache";
 | 
			
		||||
  SharedHandle<FileEntry> entries[] = {
 | 
			
		||||
    SharedHandle<FileEntry>(new FileEntry(storeDir+"/file1", 16385, 0)),
 | 
			
		||||
    SharedHandle<FileEntry>(new FileEntry(storeDir+"/file2", 4098, 16385))
 | 
			
		||||
  };
 | 
			
		||||
  for(int i = 0; i < 2; ++i) {
 | 
			
		||||
    File(entries[i]->getPath()).remove();
 | 
			
		||||
  }
 | 
			
		||||
  SharedHandle<MultiDiskAdaptor> adaptor(new MultiDiskAdaptor());
 | 
			
		||||
  adaptor->setFileEntries(vbegin(entries), vend(entries));
 | 
			
		||||
  WrDiskCacheEntry cache(adaptor);
 | 
			
		||||
  std::string data1(16383, '1'), data2(100, '2'), data3(4000, '3');
 | 
			
		||||
  cache.cacheData(createDataCell(0, data1.c_str()));
 | 
			
		||||
  cache.cacheData(createDataCell(data1.size(), data2.c_str()));
 | 
			
		||||
  cache.cacheData(createDataCell(data1.size()+data2.size(), data3.c_str()));
 | 
			
		||||
  adaptor->openFile();
 | 
			
		||||
  adaptor->writeCache(&cache);
 | 
			
		||||
  for(int i = 0; i < 2; ++i) {
 | 
			
		||||
    CPPUNIT_ASSERT_EQUAL(entries[i]->getLength(),
 | 
			
		||||
                         File(entries[i]->getPath()).size());
 | 
			
		||||
  }
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(data1+data2.substr(0, 2),
 | 
			
		||||
                       readFile(entries[0]->getPath()));
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(data2.substr(2)+data3,
 | 
			
		||||
                       readFile(entries[1]->getPath()));
 | 
			
		||||
 | 
			
		||||
  adaptor->closeFile();
 | 
			
		||||
  for(int i = 0; i < 2; ++i) {
 | 
			
		||||
    File(entries[i]->getPath()).remove();
 | 
			
		||||
  }
 | 
			
		||||
  cache.clear();
 | 
			
		||||
  cache.cacheData(createDataCell(123, data2.c_str()));
 | 
			
		||||
  adaptor->openFile();
 | 
			
		||||
  adaptor->writeCache(&cache);
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL((int64_t)(123+data2.size()),
 | 
			
		||||
                       File(entries[0]->getPath()).size());
 | 
			
		||||
  CPPUNIT_ASSERT_EQUAL(data2, readFile(entries[0]->getPath()).substr(123));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace aria2
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ WrDiskCacheEntry::DataCell* createDataCell(int64_t goff,
 | 
			
		|||
  cell->data = new unsigned char[len];
 | 
			
		||||
  memcpy(cell->data, data, len);
 | 
			
		||||
  cell->offset = offset;
 | 
			
		||||
  cell->len = len;
 | 
			
		||||
  cell->len = len - offset;
 | 
			
		||||
  return cell;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue