如何匹配自定义 Instruments 启动模式中的字符串?

How do you match strings in custom Instruments start-pattern?

我在 Xcode 的 Instruments 中匹配 start-pattern in a custom instrument (see WWDC 2018 video Creating Custom Instruments) 中的字符串文字时遇到问题。

例如,这个 start-pattern 有效...

<start-pattern>
    <message>?name ?value</message>
</start-pattern>

<column>
    <mnemonic>name</mnemonic>
    <title>Name</title>
    <type>string</type>
    <expression>?name</expression>
</column>
<column>
    <mnemonic>value</mnemonic>
    <title>Value</title>
    <type>uint64</type>
    <expression>?value</expression>
</column>

...用这个 OSSignpost:

let id = OSSignpostID(log: log)

os_signpost(.begin, log: log, name: "Interval", signpostID: id, "Foo %d", 42)
...
os_signpost(.end, log: log, name: "Interval", signpostID: id)

?name ?value 模式有效,捕获 "Foo" 作为 name42 作为 value。很好。

但是当我尝试在消息中使用字符串文字时,它不起作用,例如这个start-pattern...

<start-pattern>
    <message>"Name:" ?name ",Value:" ?value</message>
</start-pattern>

... 有了这个 os_signpost ...

let id = OSSignpostID(log: log)

os_signpost(.begin, log: log, name: "Interval", signpostID: id, "Name:Foo,Value:%d", 42)
...
os_signpost(.end, log: log, name: "Interval", signpostID: id)

这个 "Name:" ?name ",Value:" ?value 模式不起作用,尽管 the documentation 建议它应该。

我做错了什么?

如果在 start-pattern 中使用字符串文字,则必须使用 printf-style 格式字符串。

因此,这将不起作用:

os_signpost(.begin, 
            log: log, 
            name: "Interval", 
            signpostID: id, 
            "Name:Foo,Value:%d", 
            42)

但是如果我们将 "Foo" 值移出格式字符串,并将其作为参数,它将起作用:

os_signpost(.begin, 
            log: log, 
            name: "Interval", 
            signpostID: id, 
            "Name:%{public}@,Value:%d", 
            "Foo", 
            42)

问题在于 os_signpost 调用中格式字符串的特殊细节。人们可能假设 start-pattern/message 解析了 os_signpost 最终输出的结果,但它似乎(至少在 message 键中使用字符串文字时)它实际上是在解析格式字符串本身。


FWIW,这是我最后的,公认的基本音程乐器:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- Instruments Developer Help: https://help.apple.com/instruments/developer/mac/current/ -->
<package>
    <id>com.robertmryan.CustomInterval</id>
    <version>0.1</version>
    <title>Custom OS Signpost Interval</title>
    <owner>
        <name>Robert Ryan</name>
    </owner>

    <import-schema>os-signpost</import-schema>

    <!-- See https://help.apple.com/instruments/developer/mac/current/#/dev536412616 -->
    <os-signpost-interval-schema>
        <id>custom-interval-schema</id>
        <title>Interval</title>
        <owner>
            <name>Robert Ryan</name>
        </owner>
        <purpose>Provide mechanism for multicolored intervals posted by `os_signpost`; The string generated by `os_signpost` must be in form of "Label:%d,Concept:%{public}@", where "Label" is string that will control what text appears in the interval, and "Concept" is one of the strings listed in https://help.apple.com/instruments/developer/mac/current/#/dev66257045 that dictates the color of the interval. No spaces after the commas within this string.</purpose>
        <note>That message must use that printf-style format, not embedding the values in the format string literal.</note>

        <!-- you can constrain this to a particular subsystem if you'd like:
        <subsystem>"com.domain.MyApp"</subsystem>
        -->
        <category>"Interval"</category>
        <name>?name</name>

        <start-pattern>
            <message>"Label:" ?label ",Concept:" ?concept</message>
        </start-pattern>

        <column>
            <mnemonic>name</mnemonic>
            <title>Name</title>
            <type>string</type>
            <expression>?name</expression>
        </column>
        <column>
            <mnemonic>label</mnemonic>
            <title>Label</title>
            <type>string</type>
            <expression>?label</expression>
        </column>
        <column>
            <mnemonic>concept</mnemonic>
            <title>Concept</title>
            <type>event-concept</type>
            <expression>?concept</expression>
        </column>
    </os-signpost-interval-schema>

    <instrument>
        <id>com.robertmryan.CustomInterval.instrument</id>
        <title>Custom OS Signpost Interval</title>
        <category>Behavior</category>
        <purpose>Provide multi-colored intervals as dictated by the "event-concept" parsed from the `start-pattern` string.</purpose>
        <icon>Generic</icon>

        <create-table>
            <id>custom-interval-table</id>
            <schema-ref>custom-interval-schema</schema-ref>
        </create-table>

        <graph>
            <title>Custom Interval Graph</title>
            <lane>
                <title>Interval</title>
                <table-ref>custom-interval-table</table-ref>
                <plot-template>
                    <instance-by>name</instance-by>
                    <label-format>%s</label-format>
                    <value-from>name</value-from>
                    <color-from>concept</color-from>
                    <label-from>label</label-from>
                    <qualified-by>layout-qualifier</qualified-by>
                </plot-template>
            </lane>
        </graph>

        <list>
            <title>Custom Interval List</title>
            <table-ref>custom-interval-table</table-ref>
            <column>name</column>
            <column>label</column>
            <column>concept</column>
            <column>start</column>
            <column>duration</column>
        </list>
    </instrument>
</package>

以及我为该工具发布自定义间隔的类型:

//
//  InstrumentsInterval.swift
//
//  Created by Robert Ryan on 6/5/21.
//

import Foundation
import os.log

/// EventConcept enumeration
///
/// This is used to dictate the color of the intervals in our custom instrument.
/// See [Event Concept Engineering Type](https://help.apple.com/instruments/developer/mac/current/#/dev66257045).

enum EventConcept: String {
    case success = "Success"
    case failure = "Failure"

    case fault = "Fault"
    case critical = "Critical"
    case error = "Error"
    case debug = "Debug"
    case pedantic = "Pedantic"
    case info = "Info"

    case signpost = "Signpost"

    case veryLow = "Very Low"
    case low = "Low"
    case moderate = "Moderate"
    case high = "High"

    case red = "Red"
    case orange = "Orange"
    case blue = "Blue"
    case purple = "Purple"
    case green = "Green"
}

/// Interval to be shown in custom instrument when profiling app

struct InstrumentsInterval {
    static let category = "Interval"

    let name: StaticString
    let label: String
    let concept: EventConcept
    let log: OSLog
    let id: OSSignpostID

    init(name: StaticString, label: String, concept: EventConcept = .debug, log: OSLog) {
        self.name = name
        self.concept = concept
        self.label = label
        self.log = log
        self.id = OSSignpostID(log: log)
    }

    /// Block based interval
    func perform<T>(block: () throws -> T) rethrows -> T {
        begin()
        defer { end() }
        return try block()
    }

    /// Manually begin an interval
    func begin() {
        os_signpost(.begin, log: log, name: name, signpostID: id, "Label:%{public}@,Concept:%{public}@", label, concept.rawValue)
    }

    /// Manually end an interval
    func end() {
        os_signpost(.end, log: log, name: name, signpostID: id)
    }
}

然后你可以像这样使用:

let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: InstrumentsInterval.category)

let interval = InstrumentsInterval(name: "Foo", label: "1", concept: .red, log: log)

interval.perform {
    ...
}

这可以在 Instruments 中产生以下结果(在这个例子中,我将应用程序限制为同时执行四个并发任务):