Files
luma/Sources/LumaCore/Models/TracerConfig.swift
Ole André Vadla Ravnås de4c469393 itrace: Give each trace its own buffer
Drop the shared-buffer rendezvous. Each trace now owns its
buffer, created before Stalker is armed and announced to Luma
via a token-keyed start/ack handshake. The ack carries the
drain verdict: Luma remaps the buffer out-of-process while the
agent blocks, so an early crash still yields data; on
task_for_pid denial (cached per target) the agent drains and
pushes chunks itself.

Fold all itrace into the tracer instrument. Function-arm traces
and thread traces share one Trace registry; thread traces become
threadTraces config entries on a find-or-created tracer instance.
Core no longer exports itrace, and the drain agent keys its
readers by session so concurrent traces stay separate.
2026-05-31 15:12:17 +02:00

262 lines
7.4 KiB
Swift

import Foundation
public enum TracerHookKind: String, Codable, Sendable {
case instruction
case function
}
public struct ITraceArming: Codable, Equatable, Sendable {
public static let defaultMaxInvocations: Int = 5
public static let defaultMaxBytesPerInvocation: Int = 1_000_000
public var maxInvocations: Int
public var maxBytesPerInvocation: Int
public init(
maxInvocations: Int = ITraceArming.defaultMaxInvocations,
maxBytesPerInvocation: Int = ITraceArming.defaultMaxBytesPerInvocation
) {
self.maxInvocations = maxInvocations
self.maxBytesPerInvocation = maxBytesPerInvocation
}
}
public struct TracerConfig: Codable, Equatable, Sendable {
public struct Hook: Codable, Equatable, Identifiable, Sendable {
public enum State: String, Codable, Sendable, Equatable {
case enabled
case disabled
}
public var id: UUID
public var displayName: String
public var addressAnchor: AddressAnchor
public var kind: TracerHookKind
public var state: State
public var code: String
public var itraceArming: ITraceArming?
public var lastEditedAt: Date?
public init(
id: UUID = UUID(),
displayName: String,
addressAnchor: AddressAnchor,
kind: TracerHookKind,
state: State = .enabled,
code: String,
itraceArming: ITraceArming? = nil,
lastEditedAt: Date? = .now
) {
self.id = id
self.displayName = displayName
self.addressAnchor = addressAnchor
self.kind = kind
self.state = state
self.code = code
self.itraceArming = itraceArming
self.lastEditedAt = lastEditedAt
}
public mutating func updateCode(_ newCode: String, at date: Date = .now) {
code = newCode
lastEditedAt = date
}
}
public struct ThreadTrace: Codable, Equatable, Identifiable, Sendable {
public var id: UUID
public var threadID: UInt
public var threadName: String?
public init(id: UUID = UUID(), threadID: UInt, threadName: String? = nil) {
self.id = id
self.threadID = threadID
self.threadName = threadName
}
}
public var hooks: [Hook]
public var threadTraces: [ThreadTrace]
public init(hooks: [Hook] = [], threadTraces: [ThreadTrace] = []) {
self.hooks = hooks
self.threadTraces = threadTraces
}
enum CodingKeys: String, CodingKey {
case hooks
case threadTraces
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
hooks = try container.decode([Hook].self, forKey: .hooks)
threadTraces = try container.decodeIfPresent([ThreadTrace].self, forKey: .threadTraces) ?? []
}
public func hooksByMostRecentlyEdited() -> [Hook] {
return hooks.enumerated()
.map { (originalIndex: $0.offset, hook: $0.element) }
.sorted(by: hookRecencyComesFirst)
.map(\.hook)
}
public static func decode(from data: Data) throws -> TracerConfig {
try JSONDecoder().decode(TracerConfig.self, from: data)
}
public func encode() -> Data {
try! JSONEncoder().encode(self)
}
public func toJSON() -> JSONObject {
[
"hooks": hooks.map { hook in
var dict: JSONObject = [
"id": hook.id.uuidString,
"displayName": hook.displayName,
"addressAnchor": hook.addressAnchor.toJSON(),
"kind": hook.kind.rawValue,
"state": hook.state.rawValue,
"code": hook.code,
]
if let arming = hook.itraceArming {
dict["itraceArming"] = [
"maxInvocations": arming.maxInvocations,
"maxBytesPerInvocation": arming.maxBytesPerInvocation,
] as JSONObject
}
return dict
}
]
}
}
private func hookRecencyComesFirst(
_ lhs: (originalIndex: Int, hook: TracerConfig.Hook),
_ rhs: (originalIndex: Int, hook: TracerConfig.Hook)
) -> Bool {
if let l = lhs.hook.lastEditedAt, let r = rhs.hook.lastEditedAt, l != r {
return l > r
}
let lhsHasTimestamp = lhs.hook.lastEditedAt != nil
let rhsHasTimestamp = rhs.hook.lastEditedAt != nil
if lhsHasTimestamp != rhsHasTimestamp {
return lhsHasTimestamp
}
let nameComparison = lhs.hook.displayName.localizedCaseInsensitiveCompare(rhs.hook.displayName)
if nameComparison != .orderedSame {
return nameComparison == .orderedAscending
}
return lhs.originalIndex < rhs.originalIndex
}
public func defaultTracerCode(kind: TracerHookKind, anchor: AddressAnchor, displayName: String) -> String {
switch kind {
case .instruction:
return instructionStub(displayName: displayName)
case .function:
switch anchor {
case .objcMethod:
return objcMethodStub(displayName: displayName)
case .swiftFunc:
return swiftFuncStub(displayName: displayName)
case .javaMethod:
return javaMethodStub(displayName: displayName)
case .absolute, .moduleOffset, .moduleExport, .debugSymbol:
return nativeFunctionStub(displayName: displayName)
}
}
}
private func nativeFunctionStub(displayName: String) -> String {
return """
defineHandler({
onEnter(log, args) {
log(`\(displayName)(args[0]=${args[0]})`);
},
onLeave(log, retval) {
}
});
"""
}
private func instructionStub(displayName: String) -> String {
return """
defineHandler(function (log, args) {
log(`\(displayName) hit! sp=${this.context.sp}`);
});
"""
}
private func objcMethodStub(displayName: String) -> String {
return """
defineHandler({
onEnter(log, args) {
log(`\(objcLogPattern(selector: displayName))`);
},
onLeave(log, retval) {
}
});
"""
}
private func swiftFuncStub(displayName: String) -> String {
return """
defineHandler({
onEnter(log, args) {
log(`\(displayName)`);
},
onLeave(log, retval) {
}
});
"""
}
private func javaMethodStub(displayName: String) -> String {
return """
defineHandler({
onEnter(log, args) {
log(`\(displayName)(${args.map(JSON.stringify).join(', ')})`);
},
onLeave(log, retval) {
if (retval !== undefined) {
log(`<= ${JSON.stringify(retval)}`);
}
}
});
"""
}
private func objcLogPattern(selector: String) -> String {
var argIndex = 2
var result = ""
for character in selector {
if character == ":" {
result += ":${args[\(argIndex)]} "
argIndex += 1
} else {
result.append(character)
}
}
if result.hasSuffix(" ]") {
result.removeLast(2)
result += "]"
}
return result
}