vdr-checkts + vdrnfofs

Message ID 20111003160811.160940@gmx.net
State New
Headers

Commit Message

Ed Hein Oct. 3, 2011, 4:08 p.m. UTC
  Hi,

> I tried to set up a dlna server with an earlier version of vdr-nfofs a few
> months ago. It kinda worked but there were some performance problems in
> regard to file system operations. I hacked a file descriptor cache into the
> python code which helped a bit. Tobi might have included this in the new
> version.

Looks like my patches haven't been accepted for 0.7. You can try my patchset but I haven't worked on that for some months now. Patch is not minimal and not cleanly separated, so you should pick the tidbits you like.

Cya, Ed
  

Comments

Tobias Grimm Oct. 3, 2011, 5:26 p.m. UTC | #1
On 03.10.2011 18:08, Ed Hein wrote:

> Looks like my patches haven't been accepted for 0.7.

No they haven't. Sorry! But it's on the todo list. Just need to fix the
merge conflicts first, because the patch doesn't apply to 0.7.

Tobias
  
Tobias Grimm Oct. 10, 2011, 8:36 p.m. UTC | #2
On 03.10.2011 19:26, Tobi wrote:

>> Looks like my patches haven't been accepted for 0.7.
> 
> No they haven't. Sorry! But it's on the todo list. Just need to fix the
> merge conflicts first, because the patch doesn't apply to 0.7.

I have a new version 0.8 out with the following changes:

  - Use cStringIO instead of string concatenation - about 3 times faster
    (Patch provided by Ed Hein)
  - Fixed license header in source files - it's the BSD licence now!
  - Set mtime of file nodes to recording time (parsed from *.rec)
  - Set mtime of dir nodes to original directories mtime
  - Added homepage http://projects.vdr-developer.org/projects/vdrnfofs
  - Some micro optimizations
  - Cache the file system nodes for get_stat() (nodes used for
    reading are not cached)
  - For FUSE file nodes' uid/gid is taken from the *.rec dir, for dir
    nodes from the original dir

The file system should now be *much* faster.

http://projects.vdr-developer.org/projects/vdrnfofs
http://projects.vdr-developer.org/git/vdrnfofs.git/

@Ed Hein:

I've taken the cStringIO from your patch, which works pretty well, but
did a different approach with the caching. Only the nodes used for
tree walking/stat() are cached (max. 10 seconds).
I've also set multithreaded=False instead of locking the file reads -
threading (especially with locks around the reads) doesn't seem to make
things faster anyways.

Tobias
  

Patch

diff -ur vdrnfofs-0.6/vdrnfofs/concatenated_file_reader.py vdrnfofs-0.6.tyger1//vdrnfofs/concatenated_file_reader.py
--- vdrnfofs-0.6/vdrnfofs/concatenated_file_reader.py	2011-04-14 23:59:21.000000000 +0200
+++ vdrnfofs-0.6.tyger1//vdrnfofs/concatenated_file_reader.py	2011-04-18 22:35:43.776907385 +0200
@@ -20,32 +20,37 @@ 
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 import os
+from threading import Lock
+from cStringIO import StringIO
 
 class ConcatenatedFileReader:
     def __init__(self, filenames):
         self.files = [(f, os.path.getsize(f)) for f in filenames]
         self.current_filename = None
         self.current_file = None
+        self.rwlock = Lock()
 
     def read(self, offset, size):
-        buffer = ""
+        buffer = StringIO()
         ptr = offset
-        while (len(buffer) < size):
-            (filename, file_offset) = self.filename_from_offset(ptr)
-            if filename:
-                if (self.current_filename != filename):
-                    if self.current_file:
-                        self.current_file.close()
-                    self.current_filename = filename
-                    self.current_file = open(filename, 'r')
-                self.current_file.seek(file_offset)
-                buffer += self.current_file.read(size - len(buffer))
-                ptr = offset + len(buffer)
-            else:
-                break
-        return buffer
+        with self.rwlock:
+            while (buffer.tell() < size):
+                (filename, file_offset) = self.filename_from_offset(ptr)
+                if filename:
+                    if (self.current_filename != filename):
+                        if self.current_file:
+                            self.current_file.close()
+                        self.current_filename = filename
+                        self.current_file = open(filename, 'r')
+                    self.current_file.seek(file_offset)
+                    buffer.write(self.current_file.read(size - buffer.tell()))
+                    ptr = offset + buffer.tell()
+                else:
+                    break
+        return buffer.getvalue()
 
-    def release(self):
+    def __del__(self):
+        # print "CFR::DEL"
         if self.current_file:
             self.current_file.close()
 
Only in vdrnfofs-0.6.tyger1//vdrnfofs: concatenated_file_reader.py~
diff -ur vdrnfofs-0.6/vdrnfofs/filesystemnodes.py vdrnfofs-0.6.tyger1//vdrnfofs/filesystemnodes.py
--- vdrnfofs-0.6/vdrnfofs/filesystemnodes.py	2011-04-14 23:27:02.000000000 +0200
+++ vdrnfofs-0.6.tyger1//vdrnfofs/filesystemnodes.py	2011-04-18 22:39:19.540905997 +0200
@@ -26,6 +26,7 @@ 
 
 from concatenated_file_reader import *
 from vdr import *
+from threading import Lock
 
 class NodeAttributes(fuse.Stat):
     def __init__(self):
@@ -40,40 +41,90 @@ 
         self.st_mtime = 0
         self.st_ctime = 0
 
+
 class MpgNode:
+    cachesize = 20
+    cachelock = Lock()
+    cache = []
+
+    @classmethod
+    def create(cls, path, cache):
+        if not cache:
+            return cls(path)
+        with cls.cachelock:
+            index = next((i for i in xrange(len(cls.cache)-1, -1, -1) if cls.cache[i].key == path), None)
+            if index is not None:
+                # print "%s HIT" % cls.__name__
+                node = cls.cache.pop(index)
+            else:
+                # print "%s MISS" % cls.__name__
+                node = cls(path)
+                if len(cls.cache) > cls.cachesize:
+                    del cls.cache[0]
+            cls.cache.append(node)
+            # print "%s cache size: %d" %(cls.__name__, len(cls.cache))
+            return node
+
     def __init__(self, path):
+        # print "MpgNode::init %s" % path
+        self.key = path
         self.path = os.path.normpath(path)
         self.mpeg_files = glob.glob(path + '/[0-9]*.vdr')
         if not self.mpeg_files:
             self.mpeg_files = glob.glob(path + '/[0-9]*.ts')
         self.mpeg_files.sort()
-        self.file_system_name = os.path.basename(os.path.abspath(path + '/..')) + '_' + os.path.basename(path) + '.mpg'
+        self.file_system_name ="%s_%s.mpg" % (os.path.basename(os.path.abspath(path + '/..')), os.path.basename(path)) 
         self.reader = ConcatenatedFileReader(self.mpeg_files)
-
-    def size(self):
         size = 0
         for file in self.mpeg_files:
             size += os.path.getsize(file)
-        return size
+        self.filesize = size
 
-    def read(self, offset, size):
-         return self.reader.read(offset, size)
+    def size(self):
+        # print "MpgNode::size"
+        return self.filesize
 
-    def release(self):
-         return self.reader.release()
+    def read(self, offset, size):
+        # print "MpgNode::read %d" % offset
+        return self.reader.read(offset, size)
 
     def get_stat(self):
+        # print "MpgNode::get_stat"
         attr = NodeAttributes()
-        attr.st_mode = stat.S_IFREG | 644
+        attr.st_mode = stat.S_IFREG | 0444
         attr.st_nlink = 1
-        attr.st_size = self.size()
+        attr.st_size = self.filesize
         return attr
 
 
 class NfoNode:
+    cachesize = 20
+    cachelock = Lock()
+    cache = []
+
+    @classmethod
+    def create(cls, path, cache):
+        if not cache:
+            return cls(path)
+        with cls.cachelock:
+            index = next((i for i in xrange(len(cls.cache)-1, -1, -1) if cls.cache[i].key == path), None)
+            if index is not None:
+                # print "%s HIT" % cls.__name__
+                node = cls.cache.pop(index)
+            else:
+                # print "%s MISS" % cls.__name__
+                node = cls(path)
+                if len(cls.cache) > cls.cachesize:
+                    del cls.cache[0]
+            cls.cache.append(node)
+            # print "%s cache size: %d" %(cls.__name__, len(cls.cache))
+            return node
+
     def __init__(self, path):
+        # print "NfoNode::init %s" % path
+        self.key = path
         self.path = os.path.normpath(path)
-        self.file_system_name = os.path.basename(os.path.abspath(path + '/..')) + '_' + os.path.basename(path) + '.nfo'
+        self.file_system_name ="%s_%s.nfo" % (os.path.basename(os.path.abspath(path + '/..')), os.path.basename(path)) 
         if os.path.exists(path + '/info.vdr'):
             info_vdr = InfoVdr(path + '/info.vdr')
         elif os.path.exists(path + '/info'):
@@ -86,39 +137,75 @@ 
   <plot>%s</plot>
 </movie>
 """ % (info_vdr['T'], info_vdr['D'])
+        self.contentsize = len(self.nfo_content)
 
     def size(self):
-        return len(self.nfo_content)
+        return self.contentsize
 
     def read(self, offset, size):
+       # print "NfoNode::read %s" % offset
        return self.nfo_content[offset:offset+size]
 
     def get_stat(self):
+        # print "NfoNode::get_stat"
         attr = NodeAttributes()
-        attr.st_mode = stat.S_IFREG | 644
+        attr.st_mode = stat.S_IFREG | 0444
         attr.st_nlink = 1
-        attr.st_size = self.size()
+        attr.st_size = self.contentsize
         return attr
 
 
 class DirNode:
+    cachesize = 100
+    cachelock = Lock()
+    cache = []
+
+    @classmethod
+    def create(cls, path, cache):
+        if not cache:
+            return cls(path)
+        with cls.cachelock:
+            index = next((i for i in xrange(len(cls.cache)-1, -1, -1) if cls.cache[i].key == path), None)
+            if index is not None:
+                # print "%s HIT" % cls.__name__
+                node = cls.cache.pop(index)
+            else:
+                # print "%s MISS" % cls.__name__
+                node = cls(path)
+                if len(cls.cache) > cls.cachesize:
+                    del cls.cache[0]
+            cls.cache.append(node)
+            # print "%s cache size: %d" %(cls.__name__, len(cls.cache))
+            return node
+
     def __init__(self, path):
+        # print "DirNode::init %s" % path
+        self.key = path
         self.path = os.path.normpath(path)
         self.file_system_name = os.path.basename(path)
         self.cache = []
+        self.mysize = None
 
     def content(self):
+        # print "DirNode::content %s" % self.path
         if not self.cache:
+            # print "without cache"
             for entry in os.listdir(self.path):
-                entry = self.path + '/' + entry
+                entry = "/".join((self.path, entry))
                 if self.is_sub_folder(entry):
-                    self.cache.append(DirNode(entry))
+                    self.cache.append(DirNode.create(entry, True))
                 for recording in glob.glob(entry + '/*.rec'):
                     if os.path.exists(recording + '/info.vdr') or os.path.exists(recording + '/info'):
-                        self.cache.append(MpgNode(recording))
-                        self.cache.append(NfoNode(recording))
+                        self.cache.append(MpgNode(recording)) # DONT CACHE!
+                        self.cache.append(NfoNode(recording)) # DONT CACHE!
         return self.cache
 
+    def size(self):
+        # print "DirNode::size"
+        if not self.mysize:
+            self.mysize = len(self.content())
+        return self.mysize
+
     def is_sub_folder(self, dir):
         if not os.path.isdir(dir):
             return False
@@ -130,7 +217,8 @@ 
         return False
 
     def get_stat(self):
+        # print "DirNode::get_stat"
         attr = NodeAttributes()
-        attr.st_mode = stat.S_IFDIR | 0755
-        attr.st_nlink = 2 + len(self.content())
+        attr.st_mode = stat.S_IFDIR | 0555
+        attr.st_nlink = 2 + self.size()
         return attr
Only in vdrnfofs-0.6.tyger1//vdrnfofs: filesystemnodes.py~
diff -ur vdrnfofs-0.6/vdrnfofs/vdrnfofs.py vdrnfofs-0.6.tyger1//vdrnfofs/vdrnfofs.py
--- vdrnfofs-0.6/vdrnfofs/vdrnfofs.py	2011-04-14 23:35:57.000000000 +0200
+++ vdrnfofs-0.6.tyger1//vdrnfofs/vdrnfofs.py	2011-04-18 22:38:40.540905398 +0200
@@ -29,30 +29,32 @@ 
 from concatenated_file_reader import *
 from vdr import *
 from filesystemnodes import *
+from traceback import format_exc
 
 fuse.fuse_python_api = (0, 2)
 
-def get_node(video, path):
+def get_node(video, path, cache=False):
     virtual_path, virtual_file_extension = os.path.splitext(path)
     if virtual_file_extension in ['.mpg', '.nfo']:
         p = virtual_path.rfind('_')
         if p > 0:
-            video_path = video + '/' + virtual_path[0:p] + '/' + virtual_path[p+1:]
+            video_path = "%s/%s/%s" % (video, virtual_path[0:p], virtual_path[p+1:])
             if not os.path.isdir(video_path):
                return None
             elif virtual_file_extension == '.mpg':
-                return MpgNode(video_path)
+                return MpgNode.create(video_path, cache)
             elif virtual_file_extension == '.nfo':
-                return NfoNode(video_path)
+                return NfoNode.create(video_path, cache)
     else:
-        if os.path.isdir(video + '/' + path):
-            return DirNode(video + path)
+        dir_path = video + path
+        if os.path.isdir(dir_path):
+            return DirNode.create(dir_path, cache)
     return None
 
 class VdrNfoFsFile:
     def __init__(self, path, flags, *mode):
         self.path = path
-        self.node = get_node(VdrNfoFsFile.video_root, path)
+        self.node = get_node(VdrNfoFsFile.video_root, path, True)
 
     def read(self, size, offset):
         try:
@@ -60,10 +62,10 @@ 
                 return -errno.ENOENT
             return self.node.read(offset, size)
         except:
-            syslog.syslog('VdrFuseFs: Unexpected error for read(%s)' % self.path)
+            syslog.syslog('VdrFuseFs: Unexpected error for read(%s): ' % (self.path, format_exc()))
 
-    def release(self, flags):
-        self.node.release()
+#    def release(self, flags):
+        # print "VdrNfoFsFile::release %s" % self.node.key
 
 #    def write(self, buf, offset):
 #        return 0
@@ -90,24 +92,26 @@ 
         self.video = ""
 
     def getattr(self, path):
+        # print "VNF::getattr"
         try:
-            node = get_node(self.video, path)
+            node = get_node(self.video, path, False)
             if node:
+                # print "got node %s" % node.key
                 return node.get_stat()
             return -errno.ENOENT
         except:
-            syslog.syslog('VdrFuseFs: Unexpected error for getattr(%s): %s' % path)
+            syslog.syslog('VdrFuseFs: Unexpected error for getattr(%s): %s' % (path, format_exc()))
 
     def readdir(self, path, offset):
         try:
             yield fuse.Direntry('.')
             yield fuse.Direntry('..')
-            node = get_node(self.video, path)
+            node = get_node(self.video, path, True)
             if node:
                 for item in node.content():
                     yield fuse.Direntry(item.file_system_name)
         except:
-            syslog.syslog('VdrFuseFs: Unexpected error for readdir(%s)' % path)
+            syslog.syslog('VdrFuseFs: Unexpected error for readdir(%s): %s' % (path, format_exc()))
 
     def main(self, *a, **kw):
         VdrNfoFsFile.video_root = self.video
Only in vdrnfofs-0.6.tyger1//vdrnfofs: vdrnfofs.py~
diff -ur vdrnfofs-0.6/vdrnfofs/vdr.py vdrnfofs-0.6.tyger1//vdrnfofs/vdr.py
--- vdrnfofs-0.6/vdrnfofs/vdr.py	2011-04-14 23:27:14.000000000 +0200
+++ vdrnfofs-0.6.tyger1//vdrnfofs/vdr.py	2011-04-18 16:50:09.040905717 +0200
@@ -23,10 +23,10 @@ 
     def __init__(self, filename = None):
         self.values = {'T' : 'Unknown', 'D': 'No Description'}
         if filename:
-            file = open(filename, 'r')
-            for line in file:
-                line = line.rstrip("\r\n")
-                self.values[line[0]] = line[2:]
+            with open(filename, 'r') as file:
+                for line in file:
+                    line = line.rstrip("\r\n")
+                    self.values[line[0]] = line[2:]
 
     def __getitem__(self, key):
         return self.values[key] if self.values.has_key(key) else ''
Only in vdrnfofs-0.6.tyger1//vdrnfofs: vdr.py~