mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
all: add vm_data_size_bytes
metrics for easy monitoring of on-disk data size and on-disk inverted index size
This commit is contained in:
parent
8d83dcf332
commit
56c154f45b
9 changed files with 70 additions and 13 deletions
|
@ -45,8 +45,9 @@ func Init() {
|
||||||
partsCount := tm.SmallPartsCount + tm.BigPartsCount
|
partsCount := tm.SmallPartsCount + tm.BigPartsCount
|
||||||
blocksCount := tm.SmallBlocksCount + tm.BigBlocksCount
|
blocksCount := tm.SmallBlocksCount + tm.BigBlocksCount
|
||||||
rowsCount := tm.SmallRowsCount + tm.BigRowsCount
|
rowsCount := tm.SmallRowsCount + tm.BigRowsCount
|
||||||
logger.Infof("successfully opened storage %q in %s; partsCount: %d; blocksCount: %d; rowsCount: %d",
|
sizeBytes := tm.SmallSizeBytes + tm.BigSizeBytes
|
||||||
*DataPath, time.Since(startTime), partsCount, blocksCount, rowsCount)
|
logger.Infof("successfully opened storage %q in %s; partsCount: %d; blocksCount: %d; rowsCount: %d; sizeBytes: %d",
|
||||||
|
*DataPath, time.Since(startTime), partsCount, blocksCount, rowsCount, sizeBytes)
|
||||||
|
|
||||||
registerStorageMetrics(Storage)
|
registerStorageMetrics(Storage)
|
||||||
}
|
}
|
||||||
|
@ -340,6 +341,16 @@ func registerStorageMetrics(strg *storage.Storage) {
|
||||||
return float64(idbm().BlocksCount)
|
return float64(idbm().BlocksCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
metrics.NewGauge(`vm_data_size_bytes{type="storage/big"}`, func() float64 {
|
||||||
|
return float64(tm().BigSizeBytes)
|
||||||
|
})
|
||||||
|
metrics.NewGauge(`vm_data_size_bytes{type="storage/small"}`, func() float64 {
|
||||||
|
return float64(tm().SmallSizeBytes)
|
||||||
|
})
|
||||||
|
metrics.NewGauge(`vm_data_size_bytes{type="indexdb"}`, func() float64 {
|
||||||
|
return float64(idbm().SizeBytes)
|
||||||
|
})
|
||||||
|
|
||||||
metrics.NewGauge(`vm_rows{type="storage/big"}`, func() float64 {
|
metrics.NewGauge(`vm_rows{type="storage/big"}`, func() float64 {
|
||||||
return float64(tm().BigRowsCount)
|
return float64(tm().BigRowsCount)
|
||||||
})
|
})
|
||||||
|
|
12
lib/fs/fs.go
12
lib/fs/fs.go
|
@ -185,6 +185,18 @@ func MustClose(f *os.File) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustFileSize returns file size for the given path.
|
||||||
|
func MustFileSize(path string) uint64 {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Panicf("FATAL: cannot stat %q: %s", path, err)
|
||||||
|
}
|
||||||
|
if fi.IsDir() {
|
||||||
|
logger.Panicf("FATAL: %q must be a file, not a directory", path)
|
||||||
|
}
|
||||||
|
return uint64(fi.Size())
|
||||||
|
}
|
||||||
|
|
||||||
// IsPathExist returns whether the given path exists.
|
// IsPathExist returns whether the given path exists.
|
||||||
func IsPathExist(path string) bool {
|
func IsPathExist(path string) bool {
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
|
|
@ -84,13 +84,18 @@ func (ip *inmemoryPart) Init(ib *inmemoryBlock) {
|
||||||
// It is unsafe re-using ip while the returned part is in use.
|
// It is unsafe re-using ip while the returned part is in use.
|
||||||
func (ip *inmemoryPart) NewPart() *part {
|
func (ip *inmemoryPart) NewPart() *part {
|
||||||
ph := ip.ph
|
ph := ip.ph
|
||||||
p, err := newPart(&ph, "", ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
|
size := ip.size()
|
||||||
|
p, err := newPart(&ph, "", size, ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Panicf("BUG: cannot create a part from inmemoryPart: %s", err)
|
logger.Panicf("BUG: cannot create a part from inmemoryPart: %s", err)
|
||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ip *inmemoryPart) size() uint64 {
|
||||||
|
return uint64(len(ip.metaindexData.B) + len(ip.indexData.B) + len(ip.itemsData.B) + len(ip.lensData.B))
|
||||||
|
}
|
||||||
|
|
||||||
func getInmemoryPart() *inmemoryPart {
|
func getInmemoryPart() *inmemoryPart {
|
||||||
v := ipPool.Get()
|
v := ipPool.Get()
|
||||||
if v == nil {
|
if v == nil {
|
||||||
|
|
|
@ -48,6 +48,8 @@ type part struct {
|
||||||
|
|
||||||
path string
|
path string
|
||||||
|
|
||||||
|
size uint64
|
||||||
|
|
||||||
mrs []metaindexRow
|
mrs []metaindexRow
|
||||||
|
|
||||||
indexFile fs.ReadAtCloser
|
indexFile fs.ReadAtCloser
|
||||||
|
@ -71,6 +73,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot open %q: %s", metaindexPath, err)
|
return nil, fmt.Errorf("cannot open %q: %s", metaindexPath, err)
|
||||||
}
|
}
|
||||||
|
metaindexSize := fs.MustFileSize(metaindexPath)
|
||||||
|
|
||||||
indexPath := path + "/index.bin"
|
indexPath := path + "/index.bin"
|
||||||
indexFile, err := fs.OpenReaderAt(indexPath)
|
indexFile, err := fs.OpenReaderAt(indexPath)
|
||||||
|
@ -78,6 +81,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
metaindexFile.MustClose()
|
metaindexFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open %q: %s", indexPath, err)
|
return nil, fmt.Errorf("cannot open %q: %s", indexPath, err)
|
||||||
}
|
}
|
||||||
|
indexSize := fs.MustFileSize(indexPath)
|
||||||
|
|
||||||
itemsPath := path + "/items.bin"
|
itemsPath := path + "/items.bin"
|
||||||
itemsFile, err := fs.OpenReaderAt(itemsPath)
|
itemsFile, err := fs.OpenReaderAt(itemsPath)
|
||||||
|
@ -86,6 +90,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
indexFile.MustClose()
|
indexFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open %q: %s", itemsPath, err)
|
return nil, fmt.Errorf("cannot open %q: %s", itemsPath, err)
|
||||||
}
|
}
|
||||||
|
itemsSize := fs.MustFileSize(itemsPath)
|
||||||
|
|
||||||
lensPath := path + "/lens.bin"
|
lensPath := path + "/lens.bin"
|
||||||
lensFile, err := fs.OpenReaderAt(lensPath)
|
lensFile, err := fs.OpenReaderAt(lensPath)
|
||||||
|
@ -95,11 +100,13 @@ func openFilePart(path string) (*part, error) {
|
||||||
itemsFile.MustClose()
|
itemsFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open %q: %s", lensPath, err)
|
return nil, fmt.Errorf("cannot open %q: %s", lensPath, err)
|
||||||
}
|
}
|
||||||
|
lensSize := fs.MustFileSize(lensPath)
|
||||||
|
|
||||||
return newPart(&ph, path, metaindexFile, indexFile, itemsFile, lensFile)
|
size := metaindexSize + indexSize + itemsSize + lensSize
|
||||||
|
return newPart(&ph, path, size, metaindexFile, indexFile, itemsFile, lensFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser, indexFile, itemsFile, lensFile fs.ReadAtCloser) (*part, error) {
|
func newPart(ph *partHeader, path string, size uint64, metaindexReader filestream.ReadCloser, indexFile, itemsFile, lensFile fs.ReadAtCloser) (*part, error) {
|
||||||
var errors []error
|
var errors []error
|
||||||
mrs, err := unmarshalMetaindexRows(nil, metaindexReader)
|
mrs, err := unmarshalMetaindexRows(nil, metaindexReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -109,6 +116,7 @@ func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser,
|
||||||
|
|
||||||
p := &part{
|
p := &part{
|
||||||
path: path,
|
path: path,
|
||||||
|
size: size,
|
||||||
mrs: mrs,
|
mrs: mrs,
|
||||||
|
|
||||||
indexFile: indexFile,
|
indexFile: indexFile,
|
||||||
|
|
|
@ -156,7 +156,8 @@ func newTestPart(blocksCount, maxItemsPerBlock int) (*part, []string, error) {
|
||||||
if itemsMerged != uint64(len(items)) {
|
if itemsMerged != uint64(len(items)) {
|
||||||
return nil, nil, fmt.Errorf("unexpected itemsMerged; got %d; want %d", itemsMerged, len(items))
|
return nil, nil, fmt.Errorf("unexpected itemsMerged; got %d; want %d", itemsMerged, len(items))
|
||||||
}
|
}
|
||||||
p, err := newPart(&ip.ph, "partName", ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
|
size := ip.size()
|
||||||
|
p, err := newPart(&ip.ph, "partName", size, ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot create part: %s", err)
|
return nil, nil, fmt.Errorf("cannot create part: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,8 +161,8 @@ func OpenTable(path string) (*Table, error) {
|
||||||
|
|
||||||
var m TableMetrics
|
var m TableMetrics
|
||||||
tb.UpdateMetrics(&m)
|
tb.UpdateMetrics(&m)
|
||||||
logger.Infof("table %q has been opened in %s; partsCount: %d; blocksCount: %d, itemsCount: %d",
|
logger.Infof("table %q has been opened in %s; partsCount: %d; blocksCount: %d, itemsCount: %d; sizeBytes: %d",
|
||||||
path, time.Since(startTime), m.PartsCount, m.BlocksCount, m.ItemsCount)
|
path, time.Since(startTime), m.PartsCount, m.BlocksCount, m.ItemsCount, m.SizeBytes)
|
||||||
|
|
||||||
return tb, nil
|
return tb, nil
|
||||||
}
|
}
|
||||||
|
@ -242,6 +242,7 @@ type TableMetrics struct {
|
||||||
|
|
||||||
BlocksCount uint64
|
BlocksCount uint64
|
||||||
ItemsCount uint64
|
ItemsCount uint64
|
||||||
|
SizeBytes uint64
|
||||||
|
|
||||||
DataBlocksCacheSize uint64
|
DataBlocksCacheSize uint64
|
||||||
DataBlocksCacheRequests uint64
|
DataBlocksCacheRequests uint64
|
||||||
|
@ -274,6 +275,7 @@ func (tb *Table) UpdateMetrics(m *TableMetrics) {
|
||||||
|
|
||||||
m.BlocksCount += p.ph.blocksCount
|
m.BlocksCount += p.ph.blocksCount
|
||||||
m.ItemsCount += p.ph.itemsCount
|
m.ItemsCount += p.ph.itemsCount
|
||||||
|
m.SizeBytes += p.size
|
||||||
|
|
||||||
m.DataBlocksCacheSize += p.ibCache.Len()
|
m.DataBlocksCacheSize += p.ibCache.Len()
|
||||||
m.DataBlocksCacheRequests += p.ibCache.Requests()
|
m.DataBlocksCacheRequests += p.ibCache.Requests()
|
||||||
|
@ -727,6 +729,7 @@ func (tb *Table) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isOuterP
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
|
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
|
||||||
}
|
}
|
||||||
|
newPSize := newP.size
|
||||||
newPW := &partWrapper{
|
newPW := &partWrapper{
|
||||||
p: newP,
|
p: newP,
|
||||||
refCount: 1,
|
refCount: 1,
|
||||||
|
@ -761,7 +764,7 @@ func (tb *Table) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isOuterP
|
||||||
|
|
||||||
d := time.Since(startTime)
|
d := time.Since(startTime)
|
||||||
if d > 10*time.Second {
|
if d > 10*time.Second {
|
||||||
logger.Infof("merged %d items in %s at %d items/sec to %q", outItemsCount, d, int(float64(outItemsCount)/d.Seconds()), dstPartPath)
|
logger.Infof("merged %d items in %s at %d items/sec to %q; bytesSize: %d", outItemsCount, d, int(float64(outItemsCount)/d.Seconds()), dstPartPath, newPSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -51,7 +51,8 @@ func (mp *inmemoryPart) InitFromRows(rows []rawRow) {
|
||||||
// It is unsafe re-using mp while the returned part is in use.
|
// It is unsafe re-using mp while the returned part is in use.
|
||||||
func (mp *inmemoryPart) NewPart() (*part, error) {
|
func (mp *inmemoryPart) NewPart() (*part, error) {
|
||||||
ph := mp.ph
|
ph := mp.ph
|
||||||
return newPart(&ph, "", mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
|
size := uint64(len(mp.timestampsData.B) + len(mp.valuesData.B) + len(mp.indexData.B) + len(mp.metaindexData.B))
|
||||||
|
return newPart(&ph, "", size, mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInmemoryPart() *inmemoryPart {
|
func getInmemoryPart() *inmemoryPart {
|
||||||
|
|
|
@ -36,6 +36,9 @@ type part struct {
|
||||||
// Empty for in-memory part.
|
// Empty for in-memory part.
|
||||||
path string
|
path string
|
||||||
|
|
||||||
|
// Total size in bytes of part data.
|
||||||
|
size uint64
|
||||||
|
|
||||||
timestampsFile fs.ReadAtCloser
|
timestampsFile fs.ReadAtCloser
|
||||||
valuesFile fs.ReadAtCloser
|
valuesFile fs.ReadAtCloser
|
||||||
indexFile fs.ReadAtCloser
|
indexFile fs.ReadAtCloser
|
||||||
|
@ -59,6 +62,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot open timestamps file: %s", err)
|
return nil, fmt.Errorf("cannot open timestamps file: %s", err)
|
||||||
}
|
}
|
||||||
|
timestampsSize := fs.MustFileSize(timestampsPath)
|
||||||
|
|
||||||
valuesPath := path + "/values.bin"
|
valuesPath := path + "/values.bin"
|
||||||
valuesFile, err := fs.OpenReaderAt(valuesPath)
|
valuesFile, err := fs.OpenReaderAt(valuesPath)
|
||||||
|
@ -66,6 +70,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
timestampsFile.MustClose()
|
timestampsFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open values file: %s", err)
|
return nil, fmt.Errorf("cannot open values file: %s", err)
|
||||||
}
|
}
|
||||||
|
valuesSize := fs.MustFileSize(valuesPath)
|
||||||
|
|
||||||
indexPath := path + "/index.bin"
|
indexPath := path + "/index.bin"
|
||||||
indexFile, err := fs.OpenReaderAt(indexPath)
|
indexFile, err := fs.OpenReaderAt(indexPath)
|
||||||
|
@ -74,6 +79,7 @@ func openFilePart(path string) (*part, error) {
|
||||||
valuesFile.MustClose()
|
valuesFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open index file: %s", err)
|
return nil, fmt.Errorf("cannot open index file: %s", err)
|
||||||
}
|
}
|
||||||
|
indexSize := fs.MustFileSize(indexPath)
|
||||||
|
|
||||||
metaindexPath := path + "/metaindex.bin"
|
metaindexPath := path + "/metaindex.bin"
|
||||||
metaindexFile, err := filestream.Open(metaindexPath, true)
|
metaindexFile, err := filestream.Open(metaindexPath, true)
|
||||||
|
@ -83,15 +89,17 @@ func openFilePart(path string) (*part, error) {
|
||||||
indexFile.MustClose()
|
indexFile.MustClose()
|
||||||
return nil, fmt.Errorf("cannot open metaindex file: %s", err)
|
return nil, fmt.Errorf("cannot open metaindex file: %s", err)
|
||||||
}
|
}
|
||||||
|
metaindexSize := fs.MustFileSize(metaindexPath)
|
||||||
|
|
||||||
return newPart(&ph, path, metaindexFile, timestampsFile, valuesFile, indexFile)
|
size := timestampsSize + valuesSize + indexSize + metaindexSize
|
||||||
|
return newPart(&ph, path, size, metaindexFile, timestampsFile, valuesFile, indexFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPart returns new part initialized with the given arguments.
|
// newPart returns new part initialized with the given arguments.
|
||||||
//
|
//
|
||||||
// The returned part calls MustClose on all the files passed to newPart
|
// The returned part calls MustClose on all the files passed to newPart
|
||||||
// when calling part.MustClose.
|
// when calling part.MustClose.
|
||||||
func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
|
func newPart(ph *partHeader, path string, size uint64, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
|
||||||
var errors []error
|
var errors []error
|
||||||
metaindex, err := unmarshalMetaindexRows(nil, metaindexReader)
|
metaindex, err := unmarshalMetaindexRows(nil, metaindexReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -102,6 +110,7 @@ func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser,
|
||||||
p := &part{
|
p := &part{
|
||||||
ph: *ph,
|
ph: *ph,
|
||||||
path: path,
|
path: path,
|
||||||
|
size: size,
|
||||||
timestampsFile: timestampsFile,
|
timestampsFile: timestampsFile,
|
||||||
valuesFile: valuesFile,
|
valuesFile: valuesFile,
|
||||||
indexFile: indexFile,
|
indexFile: indexFile,
|
||||||
|
|
|
@ -282,6 +282,9 @@ type partitionMetrics struct {
|
||||||
SmallIndexBlocksCacheRequests uint64
|
SmallIndexBlocksCacheRequests uint64
|
||||||
SmallIndexBlocksCacheMisses uint64
|
SmallIndexBlocksCacheMisses uint64
|
||||||
|
|
||||||
|
BigSizeBytes uint64
|
||||||
|
SmallSizeBytes uint64
|
||||||
|
|
||||||
BigRowsCount uint64
|
BigRowsCount uint64
|
||||||
SmallRowsCount uint64
|
SmallRowsCount uint64
|
||||||
|
|
||||||
|
@ -326,6 +329,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
|
||||||
m.BigIndexBlocksCacheMisses += p.ibCache.Misses()
|
m.BigIndexBlocksCacheMisses += p.ibCache.Misses()
|
||||||
m.BigRowsCount += p.ph.RowsCount
|
m.BigRowsCount += p.ph.RowsCount
|
||||||
m.BigBlocksCount += p.ph.BlocksCount
|
m.BigBlocksCount += p.ph.BlocksCount
|
||||||
|
m.BigSizeBytes += p.size
|
||||||
m.BigPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
m.BigPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,6 +341,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
|
||||||
m.SmallIndexBlocksCacheMisses += p.ibCache.Misses()
|
m.SmallIndexBlocksCacheMisses += p.ibCache.Misses()
|
||||||
m.SmallRowsCount += p.ph.RowsCount
|
m.SmallRowsCount += p.ph.RowsCount
|
||||||
m.SmallBlocksCount += p.ph.BlocksCount
|
m.SmallBlocksCount += p.ph.BlocksCount
|
||||||
|
m.SmallSizeBytes += p.size
|
||||||
m.SmallPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
m.SmallPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1013,12 +1018,14 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPW *partWrapper
|
var newPW *partWrapper
|
||||||
|
var newPSize uint64
|
||||||
if len(dstPartPath) > 0 {
|
if len(dstPartPath) > 0 {
|
||||||
// Open the merged part if it is non-empty.
|
// Open the merged part if it is non-empty.
|
||||||
newP, err := openFilePart(dstPartPath)
|
newP, err := openFilePart(dstPartPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
|
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
|
||||||
}
|
}
|
||||||
|
newPSize = newP.size
|
||||||
newPW = &partWrapper{
|
newPW = &partWrapper{
|
||||||
p: newP,
|
p: newP,
|
||||||
refCount: 1,
|
refCount: 1,
|
||||||
|
@ -1057,7 +1064,7 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
|
||||||
|
|
||||||
d := time.Since(startTime)
|
d := time.Since(startTime)
|
||||||
if d > 10*time.Second {
|
if d > 10*time.Second {
|
||||||
logger.Infof("merged %d rows in %s at %d rows/sec to %q", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath)
|
logger.Infof("merged %d rows in %s at %d rows/sec to %q; sizeBytes: %d", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath, newPSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in a new issue