在使用gtk.FileChooserDialog选择大量文件时的平台依赖性能问题

5 投票
2 回答
833 浏览
提问于 2025-04-17 13:00

我有一个用pygtk写的程序,设计可以在Windows和Ubuntu上运行。这个程序是用Python 2.7和gtk2写的,使用的是静态绑定(也就是说没有使用gobject introspection)。我遇到的问题是在Ubuntu上出现,而在Windows上没有。

我的程序应该能够处理大量文件(这里我测试的是大约200个文件),但每个文件的处理量其实不大。我是按文件逐个排队处理,并向用户展示进度。

问题是,在用gtk.FileChooserDialog选择文件后(按Control+A可以全选),程序会卡住一段时间,gtk事件也无法处理——即使我的回调函数已经返回。在这段时间里,所有核心的CPU使用率都在80%左右,iotop显示我的进程正在以每秒大约20MB的速度写入磁盘,其他应用程序也会偶尔变得无响应——像Chrome、Xorg、compiz、banshee和gedit的CPU使用率都很高(在选择文件之前它们的使用率都很低)。

这里有一些示例代码。要重现这个问题,点击按钮,从某个地方选择大约200个文件(大约需要十个屏幕的Shift+向下选择),然后点击确定。文件的内容不重要——因为程序并没有对它们做任何处理。

import gtk,gobject,time

def print_how_long_it_was_frozen():
    print time.time() - start_time

def button_clicked(button):
    dialog = gtk.FileChooserDialog(
                'Select files to add', w, gtk.FILE_CHOOSER_ACTION_OPEN,
                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
    dialog.set_select_multiple(True)
    dialog.set_default_response(gtk.RESPONSE_OK)
    response = dialog.run()
    files = dialog.get_filenames()
    dialog.destroy()
    for i, f in enumerate(files):
        print i

    global start_time
    start_time = time.time()
    gobject.idle_add(print_how_long_it_was_frozen)


w = gtk.Window() 
b = gtk.Button('Select files')
w.add(b)
b.connect('clicked', button_clicked)
w.show_all()
gtk.main()

这导致在回调结束后大约会卡住60秒,在这段时间里,除了处理对话框的销毁之外,应该没有其他事情发生(销毁是在卡住的过程中进行的)。

这是在Ubuntu 11.10上发生的。在Windows上,卡住的时间不到一秒。

我怀疑这可能是由于某些Gnome或Unity的“最近文件”功能,或者其他活动跟踪的原因。在卡住期间,进程zeitgeist-daemon的CPU使用率也很高,虽然杀掉它并不能解决问题。禁用Zeitgeist活动日志管理器的日志记录也没有用。即使可以禁用Zeitgeist,我也不能指望我的用户去禁用它。

有没有人知道如何禁用gtk应用程序的最近文件报告,或者知道还有什么其他原因可能导致这个问题?

对于需要处理的极大量文件,可能需要通过“选择文件夹”对话框来添加,但对于较少数量的文件,卡住的时间似乎是每个文件大约半秒,这对于一个本应响应迅速的应用程序来说并不可接受。

(测试是在32位的Windows 7和64位的Ubuntu 11.10上进行的。两个系统上都是Python 2.7和pygtk 2.24)

2 个回答

2

这可能不算是个答案,但或许能帮到你。

我在研究为什么gtk2中的文件选择对话框打开得这么慢时发现,gtk.FileChooserDialog其实不是轻量级的对象。

你不应该为了单次使用而创建一个,然后再销毁它。你应该尽量重用这些对话框,因为你可以用.hide()把它隐藏起来,当你再次调用.run()时,它会重新出现。

需要注意的是,使用dialog.set_current_folder(dialog.get_current_folder())可以强制刷新文件列表。

另外要注意的是,当对话框被隐藏时选中的项目会在对话框重新出现时保持选中状态,除非文件列表被刷新或者文件不再存在。


如果我把你的代码改成这样,就变成了:

import gtk,gobject,time

def print_how_long_it_was_frozen():
    print time.time() - start_time

def button_clicked(button):
    response = dialog.run()
    files = dialog.get_filenames()
    dialog.hide()
    for i, f in enumerate(files):
        print i

    global start_time
    start_time = time.time()
    gobject.idle_add(print_how_long_it_was_frozen)


w = gtk.Window() 
b = gtk.Button('Select files')
w.add(b)
b.connect('clicked', button_clicked)
w.show_all()

dialog = gtk.FileChooserDialog(
            'Select files to add', w, gtk.FILE_CHOOSER_ACTION_OPEN,
            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                     gtk.STOCK_OPEN, gtk.RESPONSE_OK))
dialog.set_select_multiple(True)
dialog.set_default_response(gtk.RESPONSE_OK)

gtk.main()
dialog.destroy()
6

这个慢的原因是因为 gtk.FileChooser 这个组件会自动把你选择的所有文件放到最近使用的文件列表里(通过 gtk.RecentManager.add_item() 实现)。

在示例代码中,如果把这个功能放到一个单独的线程里运行(而且在卡住的时候似乎也能顺利获取 gtk 锁),你会发现(如果让它运行一整夜)每添加一个文件的延迟会随着最近文件数量的增加而增加:

def log_n_recent_files():
    manager = gtk.recent_manager_get_default()
    manager.purge_items()
    while True:
        time.sleep(1)
        with gtk.gdk.lock:
            items = manager.get_items()
        with open('log.log','a') as f:
            f.write('%f %d\n'%(time.time(), len(items)))

这里有两张图,第一张显示的是随着时间推移添加的文件数量,第二张显示的是文件添加的速度:

添加文件数量随时间变化 文件添加速度

因为没有办法一次性把多个文件添加到 RecentManager,所以只能一个一个地添加。

每添加一个文件,其他的 gtk 应用程序就会收到通知,告诉它们最近文件列表(存储在 ~/.local/share/recently-used.xbel)发生了变化。它们会解析这个文件,并循环查看其中的项目,寻找最近的 n 个项目(n 是应用程序特定的),然后显示出来。在判断哪些文件是最近的时,每个项目都会进行一次系统时间的调用。

问题还因为 recently-used.xbel 可以无限增长而变得更糟。如果你的 recently-used.xbel 里有 5000 个项目,而你选择了 200 个文件,那么每个正在运行的 gtk 应用程序就会进行大约 100 万次的系统时间调用。

gtk.Settings 中有一些属性可以让你的应用程序在历史记录中查找更少的文件,比如 gtk-recent-files-limitgtk-recent-files-max-age,但这些并不能阻止 ~/.local/share/recently-used.xbel 被写入。

如果想要阻止 recently-used.xbel 被写入,可以给它加上写保护,或者用一个文件夹替代。在这种情况下,gtk 仍然会尝试添加所有文件,但每次尝试都会失败。每 200 个文件的延迟大约是 1 秒——我想尝试的开销还是很大的。

因为似乎没有办法关闭 gtk.FileChooser 的这个行为,唯一的办法就是使用其他的文件选择组件。即使有 30000 个文件,使用已经不推荐的 gtk.FileSelection 组件时也不会有明显的延迟。

虽然这个组件看起来很丑,但我想我还是得用它,并且提交一个错误报告或功能请求,希望能实现关闭 gtk.FileChooser 最近文件记录的功能。

撰写回答