【问题标题】:Reading a large batch of objects taking way too long读取大量对象花费的时间太长
【发布时间】:2018-03-28 14:46:03
【问题描述】:

我正在尝试在 JXA 中编写一批 OmniFocus 任务的脚本,但遇到了一些重大的速度问题。我不认为这个问题是 OmniFocus 或 JXA 所特有的。相反,我认为这是对获取对象如何工作的更普遍的误解——我希望它像单个 SQL 查询一样工作,将所有对象加载到内存中,但它似乎是按需执行每个操作。

这是一个简单的例子 - 让我们获取所有未完成任务的名称(存储在后端的 SQLite DB 中):

var tasks = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})
var totalTasks = tasks.length
for (var i = 0; i < totalTasks; i++) {
    tasks[i].name()
}

[Finished in 46.68s]

实际上,获取 900 个任务的列表大约需要 7 秒 - 已经很慢了 - 但是循环和读取基本属性又需要 40 秒,大概是因为它正在为每个任务访问数据库。 (另外,tasks 的行为不像数组 - 它似乎在每次访问时都会重新计算。)

有什么方法可以快速做到这一点 - 一次将一批对象及其所有属性读入内存?

【问题讨论】:

    标签: applescript javascript-automation


    【解决方案1】:

    简介

    使用 AppleEvents,即用于自动化的 JavaScript (JXA) 所基于的 IPC 技术,您从另一个应用程序请求信息的方式是向它发送一个“对象说明符”,它的工作原理有点像用于访问对象属性的点表示法,有点像 SQL 或 GraphQL 查询。

    接收应用程序评估对象说明符并确定它引用哪些对象(如果有)。然后它返回一个表示引用对象的值。如果引用的对象是对象的集合,则返回的值可以是值列表。对象说明符也可以指对象的属性。返回的值可能是字符串、数字,甚至是新的对象说明符。

    对象说明符

    用 AppleScript 编写的完全限定对象说明符的示例是:

    a reference to the name of the first window of application "Safari"
    

    在 JXA 中,将表示相同的对象说明符:

    Application("Safari").windows[0].name
    

    要向 Safari 发送 IPC 请求以要求它评估此对象说明符并以值响应,您可以在对象说明符上调用 .get() 函数:

    Application("Safari").windows[0].name.get()
    

    作为.get()函数的简写,您可以直接调用对象说明符:

    Application("Safari").windows[0].name()
    

    向 Safari 发送单个请求,并返回单个值(在本例中为字符串)。

    通过这种方式,对象说明符在访问对象属性时有点像点符号。但是对象说明符比这更强大。

    收藏

    您可以有效地对集合执行映射或理解。在 AppleScript 中,这看起来像:

    get the name of every window of Application "Safari"
    

    在 JXA 中它看起来像:

    Application("Safari").windows.name.get()
    

    即使这请求多个值,它也只需要向 Safari 发送一个请求,然后它会遍历自己的窗口,收集每个窗口的名称,然后发回一个包含所有名称的列表值字符串。不管 Safari 打开了多少个窗口,这个语句只会产生一个请求/响应。

    For 循环反模式

    将该方法与 for 循环反模式进行对比:

    var nameOfEveryWindow = []
    var everyWindowSpecifier = Application("Safari").windows
    var numberOfWindows = everyWindowSpecifier.length
    for (var i = 0; i < numberOfWindows; i++) {
        var windowNameSpecifier = everyWindowSpecifier[i].name
        var windowName = windowNameSpecifier.get()
        nameOfEveryWindow.push(windowName)
    }
    

    这种方法可能需要更长的时间,因为它需要length+1 次请求才能获取名称集合。

    (请注意,集合对象说明符的 length 属性经过特殊处理,因为 JXA 中的集合对象说明符试图表现得像原生 JavaScript 数组。在长度属性上不需要(或不允许)调用 .get()。)

    过滤,以及为什么您的代码示例很慢

    AppleEvents 真正有趣的部分是所谓的“谁的子句”。这允许您提供标准来过滤从中返回值的对象。

    在您的问题中包含的代码中,tasks 是一个对象说明符,它指的是已使用 who 子句过滤为仅包含未完成任务的对象集合。请注意,此时这仍然只是参考;直到你在对象说明符上调用.get(),它只是一个指向某个东西的指针,而不是这个东西本身。

    然后您包含的代码实现了 for-loop 反模式,这可能就是您观察到的性能如此缓慢的原因。您正在向 OmniFocus 发送 length+1 请求。每次调用 .name() 都会导致另一个 AppleEvent。

    此外,您每次都要求 OmniFocus 重新过滤任务集合,因为您每次发送的对象说明符都包含 who 子句。

    试试这个:

    var taskNames = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false}).name.get()
    

    这应该向 OmniFocus 发送一个请求,并返回每个未完成任务的名称数组。

    另一种尝试的方法是让 OmniFocus 评估“whose 子句”一次,并返回一个对象说明符数组:

    var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
    

    遍历返回的对象指定数组并在每个对象上调用 .name.get() 可能会比原来的方法更快。

    回答

    虽然 JXA 可以获取对象集合的单个属性的数组,但似乎由于作者的疏忽,JXA 不支持获取集合中所有对象的所有属性。

    因此,要回答您的实际问题,使用 JXA,没有办法一次将一批对象及其所有属性读入内存。

    也就是说,AppleScript 确实支持它:

    tell app "OmniFocus" to get the properties of every flattened task of default document whose completed is false
    

    使用 JXA,如果您真的想要对象的所有属性,则必须回退到 for-loop 反模式,但我们可以通过将其评估拉到 for 之外来避免多次评估 who 子句循环:

    var tasks = []
    var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
    var totalTasks = taskSpecifiers.length
    for (var i = 0; i < totalTasks; i++) {
        tasks[i] = taskSpecifiers[i].properties()
    }
    

    最后,应该注意的是,AppleScript 还允许您请求特定的属性集:

    get the {name, zoomable} of every window of application "Safari"
    

    但是 JXA 无法发送对一个对象或对象集合的多个属性的单个请求。

    【讨论】:

    • 完美而彻底的答案!这里有很多有用的信息。太糟糕了,OP似乎是MIA。 @zambezi,如果你在外面,你应该把它标记为正确答案!
    【解决方案2】:

    尝试类似:

    tell app "OmniFocus"
      tell default document
        get name of every flattened task whose completed is false
      end tell
    end tell
    

    Apple 事件 IPC 不是 OOP,它是 RPC + 简单的一级关系查询。 AppleScript 混淆了这一点,而 JXA 不仅混淆了它甚至更糟,而且还削弱了它;但是一旦你学会看穿虚假的 OO 句法废话,它就会变得更有意义。 Thisthis 可能会提供更多见解。

    [ETA:Omni 最近在其应用程序中实现了自己的嵌入式基于 JavaScriptCore 的脚本支持;如果 JS 是你的菜,你可能会发现这是一个更好的选择。]

    【讨论】:

    • 太棒了,这些链接看起来真的很有帮助,我会研究它们 - 一直在寻找对正在发生的事情的更高层次的理解。需要明确的是 - 这些 JS 示例是否使用 nodeautomation 而不是 JXA?两者有什么区别?
    • JXA 充斥着设计缺陷和缺失/损坏的功能,并且失败得如此严重,以至于整个 MacAutomation 团队解散/解雇。 NodeAutomation 源自 appscript,它是除了 AppleScript(可能还有 UserTalk)之外唯一能正确处理 Apple 事件的 Apple 事件桥。 Appscript/NA 文档不是很好,但它仍然比 Apple 提供的任何东西都要好。两者都不再被开发或支持,这就是我建议坚持使用 AppleScript 的原因。虽然整个 AE/OSA/AS 堆栈现在无论如何都注定要失败,但它至少应该还能再使用几年。
    • 一个更有用的链接:AppleScript history and motivation 由其原设计师提供。
    • 谢谢@has 这是有用的颜色。为什么你认为整个堆栈注定要失败/你认为什么会取代它?
    • AE/OSA/AS 技术非常成熟,并没有很好地发展或老化(特别是在安全方面),在 iOS 上不存在(这是所有资金的所在对于现在的苹果来说),它的开发团队已经走了,而且它一直在进行维护模式,没有杂音。真正的问题是 Apple 有 500,000,000 名客户(!),我怀疑其中甚至有 50,000 名使用 AppleScript。 AS 的增长和兴趣似乎在 2010 年之后就消失了。Apple 没有立即杀死它的必要,但它已经有机会赢得新用户和建立新市场,但完全未能实现。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-06
    相关资源
    最近更新 更多