From ab3bbb2a7f1082220d3c3a149628ceb98c1158ce Mon Sep 17 00:00:00 2001 From: cmux reload-cloud Date: Tue, 28 Jul 2026 15:42:53 -0700 Subject: [PATCH 1/4] Add failing memory attribution aggregation tests --- cmux.xcodeproj/project.pbxproj | 16 ++- cmuxTests/CMUXCLIMemoryAttributionTests.swift | 36 +++++++ cmuxTests/CmuxTopMemoryAttributionTests.swift | 100 ++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 cmuxTests/CMUXCLIMemoryAttributionTests.swift create mode 100644 cmuxTests/CmuxTopMemoryAttributionTests.swift diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 07346a32966d..b8f2a0565398 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -603,6 +603,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources D1FF52000000000000000001 /* CMUXCLI+TypedDiffViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FF52000000000000000002 /* CMUXCLI+TypedDiffViewer.swift */; }; 06CC2F6C1340C7424D1C7E0A /* CMUXCLI+WorkspaceTodo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E5D35CC62B0F252F9EC2AD1 /* CMUXCLI+WorkspaceTodo.swift */; }; C0DE31390000000000000105 /* CMUXCLIErrorOutputRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE31390000000000000106 /* CMUXCLIErrorOutputRegressionTests.swift */; }; + 906900000000000000000004 /* CMUXCLIMemoryAttributionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906900000000000000000003 /* CMUXCLIMemoryAttributionTests.swift */; }; A72C9F4179B54DF38E99A021 /* CmuxCLIPathInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */; }; C12985000000000000000004 /* CMUXCLISentryTelemetryRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12985000000000000000003 /* CMUXCLISentryTelemetryRegressionTests.swift */; }; C0DE64950000000000000009 /* CMUXCLISessionsListForkDiagnosticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE6495000000000000000A /* CMUXCLISessionsListForkDiagnosticsTests.swift */; }; @@ -724,6 +725,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources C750500000000000000000A3 /* CmuxTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = C750500000000000000000A2 /* CmuxTerminal */; }; C750200000000000000000A3 /* CmuxTerminalCore in Frameworks */ = {isa = PBXBuildFile; productRef = C750200000000000000000A2 /* CmuxTerminalCore */; }; CFF12E0000000000000000A3 /* CmuxTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CFF12E0000000000000000A2 /* CmuxTestSupport */; }; + 906900000000000000000002 /* CmuxTopMemoryAttributionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906900000000000000000001 /* CmuxTopMemoryAttributionTests.swift */; }; C7A510000000000000000002 /* CmuxTopMemoryDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A510000000000000000001 /* CmuxTopMemoryDiagnostics.swift */; }; B35750000000000000000004 /* CmuxTopProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35750000000000000000003 /* CmuxTopProcessArguments.swift */; }; C7A50C000000000000000003 /* CmuxTopProcessArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50C000000000000000004 /* CmuxTopProcessArgumentsTests.swift */; }; @@ -3046,6 +3048,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D1FF52000000000000000002 /* CMUXCLI+TypedDiffViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+TypedDiffViewer.swift"; sourceTree = ""; }; 7E5D35CC62B0F252F9EC2AD1 /* CMUXCLI+WorkspaceTodo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+WorkspaceTodo.swift"; sourceTree = ""; }; C0DE31390000000000000106 /* CMUXCLIErrorOutputRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLIErrorOutputRegressionTests.swift; sourceTree = ""; }; + 906900000000000000000003 /* CMUXCLIMemoryAttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLIMemoryAttributionTests.swift; sourceTree = ""; }; 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxCLIPathInstaller.swift; sourceTree = ""; }; C12985000000000000000003 /* CMUXCLISentryTelemetryRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLISentryTelemetryRegressionTests.swift; sourceTree = ""; }; C0DE6495000000000000000A /* CMUXCLISessionsListForkDiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLISessionsListForkDiagnosticsTests.swift; sourceTree = ""; }; @@ -3116,6 +3119,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C10D00040000000000000004 /* CmuxSurfaceTabBarBuiltInAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxSurfaceTabBarBuiltInAction.swift; sourceTree = ""; }; 0A1107110000000000000017 /* CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift"; sourceTree = ""; }; F1000002A1B2C3D4E5F60718 /* cmuxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = cmuxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 906900000000000000000001 /* CmuxTopMemoryAttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopMemoryAttributionTests.swift; sourceTree = ""; }; C7A510000000000000000001 /* CmuxTopMemoryDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopMemoryDiagnostics.swift; sourceTree = ""; }; B35750000000000000000003 /* CmuxTopProcessArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessArguments.swift; sourceTree = ""; }; C7A50C000000000000000004 /* CmuxTopProcessArgumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessArgumentsTests.swift; sourceTree = ""; }; @@ -7141,8 +7145,10 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D78976000000000000000002 /* GhosttyCurrentDirectoryActionDispatcherTests.swift */, C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */, C7A5090000000000000005A1 /* CmuxTopSnapshotScopeCacheTests.swift */, + 906900000000000000000001 /* CmuxTopMemoryAttributionTests.swift */, C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */, C7A50C000000000000000004 /* CmuxTopProcessArgumentsTests.swift */, + 906900000000000000000003 /* CMUXCLIMemoryAttributionTests.swift */, D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */, B804D0010000000000000001 /* SidebarWorkspaceTableTests.swift */, B804D0020000000000000002 /* SidebarSelectionCoalescerTests.swift */, @@ -9598,6 +9604,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 8295A0058295A0058295A005 /* CmuxAlertContentTests.swift in Sources */, 0CB4E9797AD54D3BB9CF06F9 /* CMUXCLI+AutoNaming.swift in Sources */, C0DE31390000000000000105 /* CMUXCLIErrorOutputRegressionTests.swift in Sources */, + 906900000000000000000004 /* CMUXCLIMemoryAttributionTests.swift in Sources */, C12985000000000000000004 /* CMUXCLISentryTelemetryRegressionTests.swift in Sources */, C0DE64950000000000000009 /* CMUXCLISessionsListForkDiagnosticsTests.swift in Sources */, C0DE64970000000000000001 /* CMUXCLISessionsListOpenCodeTrustTests.swift in Sources */, @@ -9622,10 +9629,11 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DE31390000000000000111 /* CMUXOpenHTMLFocusTests.swift in Sources */, 7837E0017837E0017837E001 /* CmuxSocketEventMapperTests.swift in Sources */, C3677001000000000000001 /* CmuxSSHURLRequestTests.swift in Sources */, - C7A50C000000000000000003 /* CmuxTopProcessArgumentsTests.swift in Sources */, - C7A50C000000000000000002 /* CmuxTopProcessCPUTests.swift in Sources */, - C7A5090000000000000005A2 /* CmuxTopSnapshotScopeCacheTests.swift in Sources */, - C7A509000000000000000002 /* CmuxTopSnapshotScopeTests.swift in Sources */, + 906900000000000000000002 /* CmuxTopMemoryAttributionTests.swift in Sources */, + C7A50C000000000000000003 /* CmuxTopProcessArgumentsTests.swift in Sources */, + C7A50C000000000000000002 /* CmuxTopProcessCPUTests.swift in Sources */, + C7A5090000000000000005A2 /* CmuxTopSnapshotScopeCacheTests.swift in Sources */, + C7A509000000000000000002 /* CmuxTopSnapshotScopeTests.swift in Sources */, D7C0DE00000000000000A103 /* CmuxWebViewContextMenuLinkCaptureTests.swift in Sources */, D0B1000CA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift in Sources */, C0DE58990000000000000004 /* CmuxWebViewKeyDownReentryTests.swift in Sources */, diff --git a/cmuxTests/CMUXCLIMemoryAttributionTests.swift b/cmuxTests/CMUXCLIMemoryAttributionTests.swift new file mode 100644 index 000000000000..3ea03396917e --- /dev/null +++ b/cmuxTests/CMUXCLIMemoryAttributionTests.swift @@ -0,0 +1,36 @@ +import Foundation +import Testing + +extension CMUXCLIErrorOutputRegressionTests { + @Test func testMemoryCommandLabelsMultiWorkspaceGroupWithoutSingleOwner() throws { + let cliPath = try bundledCLIPath() + let socketPath = "/tmp/cmux-memory-attribution-\(UUID().uuidString.prefix(8)).sock" + let responder = try UnixSocketResponder( + path: socketPath, + response: Self.multiWorkspaceMemoryResponse + ) + defer { responder.stop() } + + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["--socket", socketPath, "memory", "--all"], + environment: environment, + timeout: 5 + ) + + #expect(!result.timedOut, Comment(rawValue: result.stdout)) + #expect(result.status == 0, Comment(rawValue: result.stdout)) + #expect(result.stdout.contains("2 workspaces"), Comment(rawValue: result.stdout)) + #expect(!result.stdout.contains("workspace workspace:1"), Comment(rawValue: result.stdout)) + } + + private static let multiWorkspaceMemoryResponse = #""" + {"ok":true,"result":{"memory_diagnostic":{"summary":"100 MB app footprint + 23 MB child RSS; top child group: cmux 23 MB","app":{"pid":100,"name":"cmux","physical_footprint_bytes":104857600,"resident_bytes":52428800},"children":{"root_pid":100,"recursive_rss_bytes":24117248,"process_count":2,"pids":[101,102],"groups":[{"id":"cmux","name":"cmux","rss_bytes":24117248,"resident_bytes":24117248,"process_count":2,"pids":[101,102],"group_attribution":{"kind":"multiple","owner":null,"workspace_count":2,"owner_count":2,"attributed_process_count":2,"unattributed_process_count":0},"top_attribution":{"workspace_id":"11111111-1111-1111-1111-111111111111","workspace_ref":"workspace:1","pane_id":null,"pane_ref":null,"surface_id":null,"surface_ref":null,"surface_type":null,"reason":"surface-process-tree","rss_bytes":12582912,"resident_bytes":12582912,"process_count":1,"pids":[101]},"attributions":[{"workspace_id":"11111111-1111-1111-1111-111111111111","workspace_ref":"workspace:1","pane_id":null,"pane_ref":null,"surface_id":null,"surface_ref":null,"surface_type":null,"reason":"surface-process-tree","rss_bytes":12582912,"resident_bytes":12582912,"process_count":1,"pids":[101]},{"workspace_id":"22222222-2222-2222-2222-222222222222","workspace_ref":"workspace:2","pane_id":null,"pane_ref":null,"surface_id":null,"surface_ref":null,"surface_type":null,"reason":"surface-process-tree","rss_bytes":11534336,"resident_bytes":11534336,"process_count":1,"pids":[102]}]}]}}}} + """# +} diff --git a/cmuxTests/CmuxTopMemoryAttributionTests.swift b/cmuxTests/CmuxTopMemoryAttributionTests.swift new file mode 100644 index 000000000000..10e9bccd3267 --- /dev/null +++ b/cmuxTests/CmuxTopMemoryAttributionTests.swift @@ -0,0 +1,100 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +struct CmuxTopMemoryAttributionTests { + private let firstWorkspaceID = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! + private let secondWorkspaceID = UUID(uuidString: "22222222-2222-2222-2222-222222222222")! + + @Test func commandGroupSpanningWorkspacesHasNoSingleOwner() throws { + let payload = memoryDiagnosticPayload() + let children = try #require(payload["children"] as? [String: Any]) + let groups = try #require(children["groups"] as? [[String: Any]]) + let group = try #require(groups.first) + let groupAttribution = try #require(group["group_attribution"] as? [String: Any]) + + #expect(groupAttribution["kind"] as? String == "multiple") + #expect(groupAttribution["workspace_count"] as? Int == 2) + #expect(groupAttribution["owner"] is NSNull) + } + + @Test func taskManagerDoesNotNavigateMultiWorkspaceGroupToTopMember() throws { + let snapshot = CmuxTaskManagerSnapshot(payload: [ + "memory_diagnostic": memoryDiagnosticPayload() + ]) + let row = try #require(snapshot.childMemoryRows.first) + + #expect(row.workspaceId == nil) + #expect(row.surfaceId == nil) + #expect(!row.detail.contains(firstWorkspaceID.uuidString)) + #expect(!row.detail.contains(secondWorkspaceID.uuidString)) + } + + private func memoryDiagnosticPayload() -> [String: Any] { + let appPID = 100 + let firstHelperPID = 101 + let secondHelperPID = 102 + let snapshot = CmuxTopProcessSnapshot( + processes: [ + process(pid: appPID, parentPID: 1, name: "cmux", residentBytes: 32 * 1024 * 1024), + process(pid: firstHelperPID, parentPID: appPID, name: "cmux", residentBytes: 12 * 1024 * 1024), + process(pid: secondHelperPID, parentPID: appPID, name: "cmux", residentBytes: 11 * 1024 * 1024) + ], + sampledAt: Date(timeIntervalSince1970: 0), + includesProcessDetails: true + ) + + return snapshot.memoryDiagnosticPayload( + appPID: appPID, + attributionByPID: [ + firstHelperPID: attribution(workspaceID: firstWorkspaceID, workspaceRef: "workspace:1"), + secondHelperPID: attribution(workspaceID: secondWorkspaceID, workspaceRef: "workspace:2") + ] + ) + } + + private func process( + pid: Int, + parentPID: Int, + name: String, + residentBytes: Int64 + ) -> CmuxTopProcessInfo { + CmuxTopProcessInfo( + pid: pid, + parentPID: parentPID, + name: name, + path: "/Applications/cmux.app/Contents/Resources/bin/cmux", + ttyDevice: nil, + cmuxWorkspaceID: nil, + cmuxSurfaceID: nil, + cmuxAttributionReason: nil, + processGroupID: nil, + terminalProcessGroupID: nil, + cpuPercent: 0, + residentBytes: residentBytes, + virtualBytes: residentBytes, + threadCount: 1 + ) + } + + private func attribution( + workspaceID: UUID, + workspaceRef: String + ) -> CmuxTopProcessAttribution { + CmuxTopProcessAttribution( + workspaceID: workspaceID, + workspaceRef: workspaceRef, + paneID: nil, + paneRef: nil, + surfaceID: nil, + surfaceRef: nil, + surfaceType: nil, + reason: "surface-process-tree" + ) + } +} From 921d783eb7d1ee0899ff4bfc12340528647445c7 Mon Sep 17 00:00:00 2001 From: cmux reload-cloud Date: Tue, 28 Jul 2026 15:51:53 -0700 Subject: [PATCH 2/4] Fix aggregate memory ownership reporting --- CLI/CMUXCLI+Memory.swift | 46 +----- CLI/CMUXCLI+MemoryAttribution.swift | 79 ++++++++++ Resources/Localizable.xcstrings | 51 +++++++ .../CmuxTaskManagerMemoryAttribution.swift | 67 +++++++++ Sources/CmuxTaskManagerMemoryGroup.swift | 30 ++++ ...muxTaskManagerMemoryGroupAttribution.swift | 66 +++++++++ ...xTopMemoryDiagnosticGroupAccumulator.swift | 129 ++++++++++++++++ Sources/CmuxTopMemoryDiagnostics.swift | 110 +------------- Sources/CmuxTopProcessAttribution.swift | 71 +++++++++ Sources/CmuxTopProcessOwner.swift | 138 ++++++++++++++++++ Sources/TaskManagerSnapshot.swift | 33 +---- Sources/TaskManagerTypes.swift | 63 -------- Sources/TerminalControllerTopSupport.swift | 66 ++------- cmux.xcodeproj/project.pbxproj | 28 ++++ cmuxTests/CmuxTopMemoryAttributionTests.swift | 43 ++++++ 15 files changed, 719 insertions(+), 301 deletions(-) create mode 100644 CLI/CMUXCLI+MemoryAttribution.swift create mode 100644 Sources/CmuxTaskManagerMemoryAttribution.swift create mode 100644 Sources/CmuxTaskManagerMemoryGroup.swift create mode 100644 Sources/CmuxTaskManagerMemoryGroupAttribution.swift create mode 100644 Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift create mode 100644 Sources/CmuxTopProcessAttribution.swift create mode 100644 Sources/CmuxTopProcessOwner.swift diff --git a/CLI/CMUXCLI+Memory.swift b/CLI/CMUXCLI+Memory.swift index f9587327bde9..a45cdb2a9f0d 100644 --- a/CLI/CMUXCLI+Memory.swift +++ b/CLI/CMUXCLI+Memory.swift @@ -165,7 +165,7 @@ extension CMUXCLI { let processCount = padLeft(String(topInt(group["process_count"]) ?? 0), width: 5) let name = topLabelText(group["name"] as? String) let command = name.padding(toLength: 26, withPad: " ", startingAt: 0) - let attribution = memoryAttributionText(group["top_attribution"], idFormat: idFormat) + let attribution = memoryGroupAttributionText(group, idFormat: idFormat) lines.append("\(rss) \(processCount) \(command) \(attribution)") } @@ -182,48 +182,4 @@ extension CMUXCLI { ) } - private func memoryAttributionText(_ raw: Any?, idFormat: CLIIDFormat) -> String { - guard let attribution = raw as? [String: Any] else { - return String(localized: "cli.memory.output.unattributed", defaultValue: "unattributed") - } - - var parts: [String] = [] - if let workspace = memoryAttributionHandle(attribution, prefix: "workspace", idFormat: idFormat) { - parts.append(String.localizedStringWithFormat( - String(localized: "cli.memory.output.workspaceAttribution", defaultValue: "workspace %@"), - workspace - )) - } - if let pane = memoryAttributionHandle(attribution, prefix: "pane", idFormat: idFormat) { - parts.append(String.localizedStringWithFormat( - String(localized: "cli.memory.output.paneAttribution", defaultValue: "pane %@"), - pane - )) - } - if let surface = memoryAttributionHandle(attribution, prefix: "surface", idFormat: idFormat) { - parts.append(String.localizedStringWithFormat( - String(localized: "cli.memory.output.surfaceAttribution", defaultValue: "surface %@"), - surface - )) - } - return parts.isEmpty ? String(localized: "cli.memory.output.unattributed", defaultValue: "unattributed") : parts.joined(separator: " / ") - } - - private func memoryAttributionHandle( - _ attribution: [String: Any], - prefix: String, - idFormat: CLIIDFormat - ) -> String? { - let ref = topLabelText(attribution["\(prefix)_ref"] as? String) - let id = topLabelText(attribution["\(prefix)_id"] as? String) - switch idFormat { - case .refs: - return ref.isEmpty ? (id.isEmpty ? nil : id) : ref - case .uuids: - return id.isEmpty ? (ref.isEmpty ? nil : ref) : id - case .both: - let values = [ref, id].filter { !$0.isEmpty } - return values.isEmpty ? nil : values.joined(separator: " ") - } - } } diff --git a/CLI/CMUXCLI+MemoryAttribution.swift b/CLI/CMUXCLI+MemoryAttribution.swift new file mode 100644 index 000000000000..078e75094564 --- /dev/null +++ b/CLI/CMUXCLI+MemoryAttribution.swift @@ -0,0 +1,79 @@ +import Foundation + +extension CMUXCLI { + func memoryGroupAttributionText( + _ group: [String: Any], + idFormat: CLIIDFormat + ) -> String { + guard let groupAttribution = group["group_attribution"] as? [String: Any], + let kind = groupAttribution["kind"] as? String else { + return memoryAttributionText(group["top_attribution"], idFormat: idFormat) + } + switch kind { + case "common": + return memoryAttributionText(groupAttribution["owner"], idFormat: idFormat) + case "multiple": + let workspaceCount = topInt(groupAttribution["workspace_count"]) ?? 0 + if workspaceCount > 1 { + return String.localizedStringWithFormat( + String(localized: "memory.attribution.multipleWorkspaces", defaultValue: "%lld workspaces"), + workspaceCount + ) + } + return String(localized: "memory.attribution.multipleOwners", defaultValue: "multiple owners") + case "partial": + return String(localized: "memory.attribution.partial", defaultValue: "partially attributed") + case "unattributed": + return String(localized: "cli.memory.output.unattributed", defaultValue: "unattributed") + default: + return memoryAttributionText(group["top_attribution"], idFormat: idFormat) + } + } + + private func memoryAttributionText(_ raw: Any?, idFormat: CLIIDFormat) -> String { + guard let attribution = raw as? [String: Any] else { + return String(localized: "cli.memory.output.unattributed", defaultValue: "unattributed") + } + + var parts: [String] = [] + if let workspace = memoryAttributionHandle(attribution, prefix: "workspace", idFormat: idFormat) { + parts.append(String.localizedStringWithFormat( + String(localized: "cli.memory.output.workspaceAttribution", defaultValue: "workspace %@"), + workspace + )) + } + if let pane = memoryAttributionHandle(attribution, prefix: "pane", idFormat: idFormat) { + parts.append(String.localizedStringWithFormat( + String(localized: "cli.memory.output.paneAttribution", defaultValue: "pane %@"), + pane + )) + } + if let surface = memoryAttributionHandle(attribution, prefix: "surface", idFormat: idFormat) { + parts.append(String.localizedStringWithFormat( + String(localized: "cli.memory.output.surfaceAttribution", defaultValue: "surface %@"), + surface + )) + } + return parts.isEmpty + ? String(localized: "cli.memory.output.unattributed", defaultValue: "unattributed") + : parts.joined(separator: " / ") + } + + private func memoryAttributionHandle( + _ attribution: [String: Any], + prefix: String, + idFormat: CLIIDFormat + ) -> String? { + let ref = topLabelText(attribution["\(prefix)_ref"] as? String) + let id = topLabelText(attribution["\(prefix)_id"] as? String) + switch idFormat { + case .refs: + return ref.isEmpty ? (id.isEmpty ? nil : id) : ref + case .uuids: + return id.isEmpty ? (ref.isEmpty ? nil : ref) : id + case .both: + let values = [ref, id].filter { !$0.isEmpty } + return values.isEmpty ? nil : values.joined(separator: " ") + } + } +} diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 719cc4018166..324dde695602 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -115711,6 +115711,57 @@ } } }, + "memory.attribution.multipleOwners": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "multiple owners" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "複数の帰属先" + } + } + } + }, + "memory.attribution.multipleWorkspaces": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld workspaces" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%lld ワークスペース" + } + } + } + }, + "memory.attribution.partial": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "partially attributed" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "一部のみ帰属" + } + } + } + }, "memoryDiagnostic.summary.base": { "extractionState": "manual", "localizations": { diff --git a/Sources/CmuxTaskManagerMemoryAttribution.swift b/Sources/CmuxTaskManagerMemoryAttribution.swift new file mode 100644 index 000000000000..d4ba51cf4dfc --- /dev/null +++ b/Sources/CmuxTaskManagerMemoryAttribution.swift @@ -0,0 +1,67 @@ +import Foundation + +/// Identifies a Task Manager process owner decoded from a diagnostic payload. +struct CmuxTaskManagerMemoryAttribution: Sendable { + let workspaceId: UUID? + let workspaceRef: String? + let paneId: UUID? + let paneRef: String? + let surfaceId: UUID? + let surfaceRef: String? + let surfaceType: String? + + init?(_ payload: [String: Any]?) { + guard let payload else { return nil } + self.workspaceId = Self.uuid(payload["workspace_id"]) + self.workspaceRef = CmuxTaskManagerMemoryDiagnostic.string(payload["workspace_ref"]) + self.paneId = Self.uuid(payload["pane_id"]) + self.paneRef = CmuxTaskManagerMemoryDiagnostic.string(payload["pane_ref"]) + self.surfaceId = Self.uuid(payload["surface_id"]) + self.surfaceRef = CmuxTaskManagerMemoryDiagnostic.string(payload["surface_ref"]) + self.surfaceType = CmuxTaskManagerMemoryDiagnostic.string(payload["surface_type"]) + if workspaceId == nil, + workspaceRef == nil, + paneId == nil, + paneRef == nil, + surfaceId == nil, + surfaceRef == nil, + surfaceType == nil { + return nil + } + } + + var localizedDescription: String { + var parts: [String] = [] + if let workspace = workspaceRef ?? workspaceId?.uuidString { + parts.append(String.localizedStringWithFormat( + String(localized: "taskManager.memory.workspace", defaultValue: "Workspace %@"), + workspace + )) + } + if let pane = paneRef ?? paneId?.uuidString { + parts.append(String.localizedStringWithFormat( + String(localized: "taskManager.memory.pane", defaultValue: "Pane %@"), + pane + )) + } + if let surface = surfaceRef ?? surfaceId?.uuidString { + parts.append(String.localizedStringWithFormat( + String(localized: "taskManager.memory.surface", defaultValue: "Surface %@"), + surface + )) + } + return parts.isEmpty + ? String(localized: "taskManager.memory.unattributed", defaultValue: "Unattributed") + : parts.joined(separator: " / ") + } + + private static func uuid(_ raw: Any?) -> UUID? { + if let value = raw as? UUID { + return value + } + guard let value = CmuxTaskManagerMemoryDiagnostic.string(raw) else { + return nil + } + return UUID(uuidString: value) + } +} diff --git a/Sources/CmuxTaskManagerMemoryGroup.swift b/Sources/CmuxTaskManagerMemoryGroup.swift new file mode 100644 index 000000000000..e3e2bca2e3da --- /dev/null +++ b/Sources/CmuxTaskManagerMemoryGroup.swift @@ -0,0 +1,30 @@ +import Foundation + +/// Decodes one command-name aggregate from the memory diagnostic payload. +struct CmuxTaskManagerMemoryGroup: Sendable { + let id: String + let name: String + let rssBytes: Int64 + let processCount: Int + let processIds: [Int] + let attribution: CmuxTaskManagerMemoryGroupAttribution + + init?(_ payload: [String: Any]) { + guard let name = CmuxTaskManagerMemoryDiagnostic.string(payload["name"]) else { + return nil + } + let processCount = CmuxTaskManagerMemoryDiagnostic.int(payload["process_count"]) ?? 0 + guard processCount > 0 else { return nil } + self.id = CmuxTaskManagerMemoryDiagnostic.string(payload["id"]) ?? name.lowercased() + self.name = name + self.rssBytes = CmuxTaskManagerMemoryDiagnostic.int64(payload["rss_bytes"]) + self.processCount = processCount + self.processIds = CmuxTaskManagerMemoryDiagnostic.intArray(payload["pids"]) + let topAttribution = CmuxTaskManagerMemoryAttribution(payload["top_attribution"] as? [String: Any]) + self.attribution = CmuxTaskManagerMemoryGroupAttribution( + payload["group_attribution"] as? [String: Any] + ) + ?? topAttribution.map(CmuxTaskManagerMemoryGroupAttribution.common) + ?? .unattributed + } +} diff --git a/Sources/CmuxTaskManagerMemoryGroupAttribution.swift b/Sources/CmuxTaskManagerMemoryGroupAttribution.swift new file mode 100644 index 000000000000..175cb348b9fb --- /dev/null +++ b/Sources/CmuxTaskManagerMemoryGroupAttribution.swift @@ -0,0 +1,66 @@ +import Foundation + +/// Describes whether a memory group has one common owner, several owners, or incomplete attribution. +enum CmuxTaskManagerMemoryGroupAttribution: Sendable { + case common(CmuxTaskManagerMemoryAttribution) + case multiple(workspaceCount: Int, ownerCount: Int) + case partial(attributedProcessCount: Int, unattributedProcessCount: Int) + case unattributed + + init?(_ payload: [String: Any]?) { + guard let payload, + let kind = CmuxTaskManagerMemoryDiagnostic.string(payload["kind"]) else { + return nil + } + switch kind { + case "common": + guard let owner = CmuxTaskManagerMemoryAttribution(payload["owner"] as? [String: Any]) else { + return nil + } + self = .common(owner) + case "multiple": + self = .multiple( + workspaceCount: CmuxTaskManagerMemoryDiagnostic.int(payload["workspace_count"]) ?? 0, + ownerCount: CmuxTaskManagerMemoryDiagnostic.int(payload["owner_count"]) ?? 0 + ) + case "partial": + self = .partial( + attributedProcessCount: CmuxTaskManagerMemoryDiagnostic.int( + payload["attributed_process_count"] + ) ?? 0, + unattributedProcessCount: CmuxTaskManagerMemoryDiagnostic.int( + payload["unattributed_process_count"] + ) ?? 0 + ) + case "unattributed": + self = .unattributed + default: + return nil + } + } + + var commonOwner: CmuxTaskManagerMemoryAttribution? { + guard case .common(let owner) = self else { return nil } + return owner + } + + var localizedDescription: String { + switch self { + case .common(let owner): + owner.localizedDescription + case .multiple(let workspaceCount, _): + if workspaceCount > 1 { + String.localizedStringWithFormat( + String(localized: "memory.attribution.multipleWorkspaces", defaultValue: "%lld workspaces"), + workspaceCount + ) + } else { + String(localized: "memory.attribution.multipleOwners", defaultValue: "multiple owners") + } + case .partial: + String(localized: "memory.attribution.partial", defaultValue: "partially attributed") + case .unattributed: + String(localized: "taskManager.memory.unattributed", defaultValue: "Unattributed") + } + } +} diff --git a/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift new file mode 100644 index 000000000000..66c33eaf2045 --- /dev/null +++ b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift @@ -0,0 +1,129 @@ +import Foundation + +/// Builds one command-name memory group and its aggregate ownership classification. +struct CmuxTopMemoryDiagnosticGroupAccumulator { + private struct AttributionAccumulator { + let attribution: CmuxTopProcessAttribution + var rssBytes: Int64 = 0 + var processIDs: [Int] = [] + + var displayKey: String { + [ + attribution.workspaceRef, + attribution.paneRef, + attribution.surfaceRef, + attribution.workspaceID?.uuidString, + attribution.paneID?.uuidString, + attribution.surfaceID?.uuidString + ] + .compactMap { $0 } + .joined(separator: "/") + } + + mutating func append(process: CmuxTopProcessInfo) { + rssBytes = CmuxTopProcessSnapshot.clampedAdd(rssBytes, process.residentBytes) + processIDs.append(process.pid) + } + + func payload() -> [String: Any] { + var payload = attribution.payload() + let sortedProcessIDs = processIDs.sorted() + payload["rss_bytes"] = rssBytes + payload["resident_bytes"] = rssBytes + payload["process_count"] = sortedProcessIDs.count + payload["pids"] = sortedProcessIDs + return payload + } + } + + let id: String + let name: String + var rssBytes: Int64 = 0 + private var processIDs: [Int] = [] + private var attributions: [CmuxTopProcessAttribution: AttributionAccumulator] = [:] + private var attributedProcessCount = 0 + private var commonOwner: CmuxTopProcessOwner? + private var ownerIdentityKeys: Set = [] + private var workspaceIdentityKeys: Set = [] + + mutating func append( + process: CmuxTopProcessInfo, + attribution: CmuxTopProcessAttribution? + ) { + rssBytes = CmuxTopProcessSnapshot.clampedAdd(rssBytes, process.residentBytes) + processIDs.append(process.pid) + guard let attribution else { return } + attributedProcessCount += 1 + let owner = attribution.owner + if let existingCommonOwner = commonOwner { + commonOwner = existingCommonOwner.commonOwner(with: owner) + } else if attributedProcessCount == 1 { + commonOwner = owner + } + if let identityKey = owner.identityKey { + ownerIdentityKeys.insert(identityKey) + } + if let workspaceIdentityKey = owner.workspaceIdentityKey { + workspaceIdentityKeys.insert(workspaceIdentityKey) + } + if attributions[attribution] == nil { + attributions[attribution] = AttributionAccumulator(attribution: attribution) + } + attributions[attribution]?.append(process: process) + } + + func payload() -> [String: Any] { + let sortedProcessIDs = processIDs.sorted() + let attributionPayloads = attributions.values + .sorted { + if $0.rssBytes != $1.rssBytes { + return $0.rssBytes > $1.rssBytes + } + return $0.displayKey < $1.displayKey + } + .map { $0.payload() } + let topAttribution: Any = attributionPayloads.first.map { $0 as Any } ?? NSNull() + return [ + "id": id, + "name": name, + "rss_bytes": rssBytes, + "resident_bytes": rssBytes, + "process_count": sortedProcessIDs.count, + "pids": sortedProcessIDs, + "group_attribution": groupAttributionPayload(), + "top_attribution": topAttribution, + "attributions": attributionPayloads + ] + } + + private func groupAttributionPayload() -> [String: Any] { + let processCount = processIDs.count + let unattributedProcessCount = max(0, processCount - attributedProcessCount) + let kind: String + let ownerPayload: Any + if attributedProcessCount == 0 { + kind = "unattributed" + ownerPayload = NSNull() + } else if unattributedProcessCount > 0 { + kind = "partial" + ownerPayload = NSNull() + } else if let commonOwner { + kind = "common" + ownerPayload = CmuxTopProcessAttribution( + owner: commonOwner, + reason: "common-command-group-owner" + ).payload() + } else { + kind = "multiple" + ownerPayload = NSNull() + } + return [ + "kind": kind, + "owner": ownerPayload, + "workspace_count": workspaceIdentityKeys.count, + "owner_count": ownerIdentityKeys.count, + "attributed_process_count": attributedProcessCount, + "unattributed_process_count": unattributedProcessCount + ] + } +} diff --git a/Sources/CmuxTopMemoryDiagnostics.swift b/Sources/CmuxTopMemoryDiagnostics.swift index 4342454cb73b..437fd1e72d7f 100644 --- a/Sources/CmuxTopMemoryDiagnostics.swift +++ b/Sources/CmuxTopMemoryDiagnostics.swift @@ -3,30 +3,6 @@ import Foundation nonisolated let cmuxTopMemoryDiagnosticDefaultGroupLimit = 12 -struct CmuxTopProcessAttribution: Hashable, Sendable { - let workspaceID: UUID? - let workspaceRef: String? - let paneID: UUID? - let paneRef: String? - let surfaceID: UUID? - let surfaceRef: String? - let surfaceType: String? - let reason: String - - func payload() -> [String: Any] { - [ - "workspace_id": workspaceID?.uuidString as Any? ?? NSNull(), - "workspace_ref": workspaceRef as Any? ?? NSNull(), - "pane_id": paneID?.uuidString as Any? ?? NSNull(), - "pane_ref": paneRef as Any? ?? NSNull(), - "surface_id": surfaceID?.uuidString as Any? ?? NSNull(), - "surface_ref": surfaceRef as Any? ?? NSNull(), - "surface_type": surfaceType as Any? ?? NSNull(), - "reason": reason - ] - } -} - extension CmuxTopProcessSnapshot { func memoryDiagnosticPayload( appPID: Int = Int(Darwin.getpid()), @@ -72,97 +48,19 @@ extension CmuxTopProcessSnapshot { ] } - private struct MemoryDiagnosticGroupAccumulator { - let id: String - let name: String - var rssBytes: Int64 = 0 - var processIDs: [Int] = [] - var attributions: [CmuxTopProcessAttribution: MemoryDiagnosticAttributionAccumulator] = [:] - - mutating func append( - process: CmuxTopProcessInfo, - attribution: CmuxTopProcessAttribution? - ) { - rssBytes = CmuxTopProcessSnapshot.clampedAdd(rssBytes, process.residentBytes) - processIDs.append(process.pid) - guard let attribution else { return } - if attributions[attribution] == nil { - attributions[attribution] = MemoryDiagnosticAttributionAccumulator(attribution: attribution) - } - attributions[attribution]?.append(process: process) - } - - func payload() -> [String: Any] { - let sortedProcessIDs = processIDs.sorted() - let attributionPayloads = attributions.values - .sorted { - if $0.rssBytes != $1.rssBytes { - return $0.rssBytes > $1.rssBytes - } - return $0.displayKey < $1.displayKey - } - .map { $0.payload() } - let topAttribution: Any = attributionPayloads.first.map { $0 as Any } ?? NSNull() - return [ - "id": id, - "name": name, - "rss_bytes": rssBytes, - "resident_bytes": rssBytes, - "process_count": sortedProcessIDs.count, - "pids": sortedProcessIDs, - "top_attribution": topAttribution, - "attributions": attributionPayloads - ] - } - } - - private struct MemoryDiagnosticAttributionAccumulator { - let attribution: CmuxTopProcessAttribution - var rssBytes: Int64 = 0 - var processIDs: [Int] = [] - - var displayKey: String { - [ - attribution.workspaceRef, - attribution.paneRef, - attribution.surfaceRef, - attribution.workspaceID?.uuidString, - attribution.paneID?.uuidString, - attribution.surfaceID?.uuidString - ] - .compactMap { $0 } - .joined(separator: "/") - } - - mutating func append(process: CmuxTopProcessInfo) { - rssBytes = CmuxTopProcessSnapshot.clampedAdd(rssBytes, process.residentBytes) - processIDs.append(process.pid) - } - - func payload() -> [String: Any] { - var payload = attribution.payload() - let sortedProcessIDs = processIDs.sorted() - payload["rss_bytes"] = rssBytes - payload["resident_bytes"] = rssBytes - payload["process_count"] = sortedProcessIDs.count - payload["pids"] = sortedProcessIDs - return payload - } - } - private func memoryDiagnosticGroups( for pids: Set, topGroupLimit: Int, attributionByPID: [Int: CmuxTopProcessAttribution] ) -> [[String: Any]] { - var groups: [String: MemoryDiagnosticGroupAccumulator] = [:] + var groups: [String: CmuxTopMemoryDiagnosticGroupAccumulator] = [:] for pid in pids.sorted() { guard let process = processesByPID[pid] else { continue } let name = process.name.trimmingCharacters(in: .whitespacesAndNewlines) let displayName = name.isEmpty ? "pid-\(pid)" : name let key = displayName.lowercased() if groups[key] == nil { - groups[key] = MemoryDiagnosticGroupAccumulator(id: key, name: displayName) + groups[key] = CmuxTopMemoryDiagnosticGroupAccumulator(id: key, name: displayName) } groups[key]?.append( process: process, @@ -234,7 +132,9 @@ extension CmuxTopProcessSnapshot { name, Self.formatDiagnosticBytes(rssBytes) ) - if let attribution = topGroup["top_attribution"] as? [String: Any], + if let groupAttribution = topGroup["group_attribution"] as? [String: Any], + groupAttribution["kind"] as? String == "common", + let attribution = groupAttribution["owner"] as? [String: Any], let workspace = attribution["workspace_ref"] as? String ?? attribution["workspace_id"] as? String, !workspace.isEmpty { summary += String.localizedStringWithFormat( diff --git a/Sources/CmuxTopProcessAttribution.swift b/Sources/CmuxTopProcessAttribution.swift new file mode 100644 index 000000000000..895b9e0e4d5d --- /dev/null +++ b/Sources/CmuxTopProcessAttribution.swift @@ -0,0 +1,71 @@ +import Foundation + +/// Records a process owner together with the evidence used to infer it. +struct CmuxTopProcessAttribution: Hashable, Sendable { + let workspaceID: UUID? + let workspaceRef: String? + let paneID: UUID? + let paneRef: String? + let surfaceID: UUID? + let surfaceRef: String? + let surfaceType: String? + let reason: String + + init( + workspaceID: UUID?, + workspaceRef: String?, + paneID: UUID?, + paneRef: String?, + surfaceID: UUID?, + surfaceRef: String?, + surfaceType: String?, + reason: String + ) { + self.workspaceID = workspaceID + self.workspaceRef = workspaceRef + self.paneID = paneID + self.paneRef = paneRef + self.surfaceID = surfaceID + self.surfaceRef = surfaceRef + self.surfaceType = surfaceType + self.reason = reason + } + + init(owner: CmuxTopProcessOwner, reason: String) { + self.init( + workspaceID: owner.workspaceID, + workspaceRef: owner.workspaceRef, + paneID: owner.paneID, + paneRef: owner.paneRef, + surfaceID: owner.surfaceID, + surfaceRef: owner.surfaceRef, + surfaceType: owner.surfaceType, + reason: reason + ) + } + + var owner: CmuxTopProcessOwner { + CmuxTopProcessOwner( + workspaceID: workspaceID, + workspaceRef: workspaceRef, + paneID: paneID, + paneRef: paneRef, + surfaceID: surfaceID, + surfaceRef: surfaceRef, + surfaceType: surfaceType + ) + } + + func payload() -> [String: Any] { + [ + "workspace_id": workspaceID?.uuidString as Any? ?? NSNull(), + "workspace_ref": workspaceRef as Any? ?? NSNull(), + "pane_id": paneID?.uuidString as Any? ?? NSNull(), + "pane_ref": paneRef as Any? ?? NSNull(), + "surface_id": surfaceID?.uuidString as Any? ?? NSNull(), + "surface_ref": surfaceRef as Any? ?? NSNull(), + "surface_type": surfaceType as Any? ?? NSNull(), + "reason": reason + ] + } +} diff --git a/Sources/CmuxTopProcessOwner.swift b/Sources/CmuxTopProcessOwner.swift new file mode 100644 index 000000000000..ce317e329830 --- /dev/null +++ b/Sources/CmuxTopProcessOwner.swift @@ -0,0 +1,138 @@ +import Foundation + +/// Identifies the narrowest workspace, pane, or surface that owns a process. +struct CmuxTopProcessOwner: Hashable, Sendable { + let workspaceID: UUID? + let workspaceRef: String? + let paneID: UUID? + let paneRef: String? + let surfaceID: UUID? + let surfaceRef: String? + let surfaceType: String? + + var specificity: Int { + if surfaceID != nil || surfaceRef != nil { + return 3 + } + if paneID != nil || paneRef != nil { + return 2 + } + if workspaceID != nil || workspaceRef != nil { + return 1 + } + return 0 + } + + var identityKey: String? { + if let surface = Self.identifier(id: surfaceID, ref: surfaceRef) { + return "surface:\(surface)" + } + if let pane = Self.identifier(id: paneID, ref: paneRef) { + return "pane:\(pane)" + } + if let workspace = workspaceIdentityKey { + return "workspace:\(workspace)" + } + return nil + } + + var workspaceIdentityKey: String? { + Self.identifier(id: workspaceID, ref: workspaceRef) + } + + func commonOwner(with other: CmuxTopProcessOwner) -> CmuxTopProcessOwner? { + guard Self.identifiersMatch( + lhsID: workspaceID, + lhsRef: workspaceRef, + rhsID: other.workspaceID, + rhsRef: other.workspaceRef + ) else { + return nil + } + + let workspaceID = workspaceID ?? other.workspaceID + let workspaceRef = workspaceRef ?? other.workspaceRef + if Self.identifiersMatch( + lhsID: surfaceID, + lhsRef: surfaceRef, + rhsID: other.surfaceID, + rhsRef: other.surfaceRef + ), Self.identifiersAreCompatible( + lhsID: paneID, + lhsRef: paneRef, + rhsID: other.paneID, + rhsRef: other.paneRef + ) { + return CmuxTopProcessOwner( + workspaceID: workspaceID, + workspaceRef: workspaceRef, + paneID: paneID ?? other.paneID, + paneRef: paneRef ?? other.paneRef, + surfaceID: surfaceID ?? other.surfaceID, + surfaceRef: surfaceRef ?? other.surfaceRef, + surfaceType: surfaceType ?? other.surfaceType + ) + } + if Self.identifiersMatch( + lhsID: paneID, + lhsRef: paneRef, + rhsID: other.paneID, + rhsRef: other.paneRef + ) { + return CmuxTopProcessOwner( + workspaceID: workspaceID, + workspaceRef: workspaceRef, + paneID: paneID ?? other.paneID, + paneRef: paneRef ?? other.paneRef, + surfaceID: nil, + surfaceRef: nil, + surfaceType: nil + ) + } + return CmuxTopProcessOwner( + workspaceID: workspaceID, + workspaceRef: workspaceRef, + paneID: nil, + paneRef: nil, + surfaceID: nil, + surfaceRef: nil, + surfaceType: nil + ) + } + + private static func identifier(id: UUID?, ref: String?) -> String? { + id?.uuidString ?? ref + } + + private static func identifiersMatch( + lhsID: UUID?, + lhsRef: String?, + rhsID: UUID?, + rhsRef: String? + ) -> Bool { + if let lhsID, let rhsID { + return lhsID == rhsID + } + if let lhsRef, let rhsRef { + return lhsRef == rhsRef + } + return false + } + + private static func identifiersAreCompatible( + lhsID: UUID?, + lhsRef: String?, + rhsID: UUID?, + rhsRef: String? + ) -> Bool { + let lhsExists = lhsID != nil || lhsRef != nil + let rhsExists = rhsID != nil || rhsRef != nil + guard lhsExists, rhsExists else { return true } + return identifiersMatch( + lhsID: lhsID, + lhsRef: lhsRef, + rhsID: rhsID, + rhsRef: rhsRef + ) + } +} diff --git a/Sources/TaskManagerSnapshot.swift b/Sources/TaskManagerSnapshot.swift index b85e302d0fff..f329f5a43c35 100644 --- a/Sources/TaskManagerSnapshot.swift +++ b/Sources/TaskManagerSnapshot.swift @@ -99,13 +99,13 @@ struct CmuxTaskManagerSnapshot { private static func childMemoryRows(from diagnostic: CmuxTaskManagerMemoryDiagnostic?) -> [CmuxTaskManagerRow] { guard let diagnostic else { return [] } return diagnostic.groups.map { group in - let attribution = group.topAttribution + let attribution = group.attribution.commonOwner let workspaceId = attribution?.workspaceId let surfaceId = attribution?.surfaceId let surfaceType = attribution?.surfaceType?.lowercased() let detailParts = [ processCountDetail(group.processCount), - attributionDetail(attribution) + group.attribution.localizedDescription ].compactMap { $0 } return CmuxTaskManagerRow( id: "childMemoryAggregate:\(group.id)", @@ -132,35 +132,6 @@ struct CmuxTaskManagerSnapshot { } } - private static func attributionDetail(_ attribution: CmuxTaskManagerMemoryAttribution?) -> String? { - guard let attribution else { - return String(localized: "taskManager.memory.unattributed", defaultValue: "Unattributed") - } - var parts: [String] = [] - if let workspace = attribution.workspaceRef ?? attribution.workspaceId?.uuidString { - parts.append(String(format: String( - localized: "taskManager.memory.workspace", - defaultValue: "Workspace %@" - ), workspace)) - } - if let pane = attribution.paneRef ?? attribution.paneId?.uuidString { - parts.append(String(format: String( - localized: "taskManager.memory.pane", - defaultValue: "Pane %@" - ), pane)) - } - if let surface = attribution.surfaceRef ?? attribution.surfaceId?.uuidString { - parts.append(String(format: String( - localized: "taskManager.memory.surface", - defaultValue: "Surface %@" - ), surface)) - } - if parts.isEmpty { - return String(localized: "taskManager.memory.unattributed", defaultValue: "Unattributed") - } - return parts.joined(separator: " / ") - } - private static func agentRows(from payloads: [[String: Any]]) -> [CmuxTaskManagerRow] { payloads.compactMap { payload in guard let id = nonEmptyString(payload["id"]), diff --git a/Sources/TaskManagerTypes.swift b/Sources/TaskManagerTypes.swift index ef6c55a27334..c63ff32a6676 100644 --- a/Sources/TaskManagerTypes.swift +++ b/Sources/TaskManagerTypes.swift @@ -429,69 +429,6 @@ struct CmuxTaskManagerMemoryDiagnostic: Sendable { } } -struct CmuxTaskManagerMemoryGroup: Sendable { - let id: String - let name: String - let rssBytes: Int64 - let processCount: Int - let processIds: [Int] - let topAttribution: CmuxTaskManagerMemoryAttribution? - - init?(_ payload: [String: Any]) { - guard let name = CmuxTaskManagerMemoryDiagnostic.string(payload["name"]) else { - return nil - } - let processCount = CmuxTaskManagerMemoryDiagnostic.int(payload["process_count"]) ?? 0 - guard processCount > 0 else { return nil } - self.id = CmuxTaskManagerMemoryDiagnostic.string(payload["id"]) ?? name.lowercased() - self.name = name - self.rssBytes = CmuxTaskManagerMemoryDiagnostic.int64(payload["rss_bytes"]) - self.processCount = processCount - self.processIds = CmuxTaskManagerMemoryDiagnostic.intArray(payload["pids"]) - self.topAttribution = CmuxTaskManagerMemoryAttribution(payload["top_attribution"] as? [String: Any]) - } -} - -struct CmuxTaskManagerMemoryAttribution: Sendable { - let workspaceId: UUID? - let workspaceRef: String? - let paneId: UUID? - let paneRef: String? - let surfaceId: UUID? - let surfaceRef: String? - let surfaceType: String? - - init?(_ payload: [String: Any]?) { - guard let payload else { return nil } - self.workspaceId = Self.uuid(payload["workspace_id"]) - self.workspaceRef = CmuxTaskManagerMemoryDiagnostic.string(payload["workspace_ref"]) - self.paneId = Self.uuid(payload["pane_id"]) - self.paneRef = CmuxTaskManagerMemoryDiagnostic.string(payload["pane_ref"]) - self.surfaceId = Self.uuid(payload["surface_id"]) - self.surfaceRef = CmuxTaskManagerMemoryDiagnostic.string(payload["surface_ref"]) - self.surfaceType = CmuxTaskManagerMemoryDiagnostic.string(payload["surface_type"]) - if workspaceId == nil, - workspaceRef == nil, - paneId == nil, - paneRef == nil, - surfaceId == nil, - surfaceRef == nil, - surfaceType == nil { - return nil - } - } - - private static func uuid(_ raw: Any?) -> UUID? { - if let value = raw as? UUID { - return value - } - guard let value = CmuxTaskManagerMemoryDiagnostic.string(raw) else { - return nil - } - return UUID(uuidString: value) - } -} - enum CmuxTaskManagerFormat { private static let isoFormatter = ISO8601DateFormatter() private static let timeFormatter: DateFormatter = { diff --git a/Sources/TerminalControllerTopSupport.swift b/Sources/TerminalControllerTopSupport.swift index 1b05d8b6ac90..11d6ce4a6faf 100644 --- a/Sources/TerminalControllerTopSupport.swift +++ b/Sources/TerminalControllerTopSupport.swift @@ -400,9 +400,15 @@ extension TerminalController { let commonOwnerSourceSpecificity = commonOwnerSourceSpecificityByPID[pid] let existingSourceSpecificity = commonOwnerSourceSpecificity ?? existingSpecificity let mergedSourceSpecificity = max(existingSourceSpecificity, newSpecificity) - if let commonOwner = v2TopMemoryAttributionCommonOwner(existing, attribution), + if let commonOwner = existing.owner.commonOwner(with: attribution.owner), commonOwnerSourceSpecificity != nil || newSpecificity == existingSourceSpecificity { - result[pid] = commonOwner + let sharedReason: String + switch commonOwner.specificity { + case 3: sharedReason = "shared-surface-process-tree" + case 2: sharedReason = "shared-pane-process-tree" + default: sharedReason = "shared-workspace-process-tree" + } + result[pid] = CmuxTopProcessAttribution(owner: commonOwner, reason: sharedReason) commonOwnerSourceSpecificityByPID[pid] = mergedSourceSpecificity } else if newSpecificity > existingSourceSpecificity { result[pid] = attribution @@ -417,62 +423,8 @@ extension TerminalController { } } - private nonisolated func v2TopMemoryAttributionCommonOwner( - _ lhs: CmuxTopProcessAttribution, - _ rhs: CmuxTopProcessAttribution - ) -> CmuxTopProcessAttribution? { - guard let workspaceID = lhs.workspaceID, workspaceID == rhs.workspaceID else { - return nil - } - let workspaceRef = lhs.workspaceRef ?? rhs.workspaceRef - if let paneID = lhs.paneID, paneID == rhs.paneID { - let paneRef = lhs.paneRef ?? rhs.paneRef - if let surfaceID = lhs.surfaceID, surfaceID == rhs.surfaceID { - return CmuxTopProcessAttribution( - workspaceID: workspaceID, - workspaceRef: workspaceRef, - paneID: paneID, - paneRef: paneRef, - surfaceID: surfaceID, - surfaceRef: lhs.surfaceRef ?? rhs.surfaceRef, - surfaceType: lhs.surfaceType ?? rhs.surfaceType, - reason: "shared-surface-process-tree" - ) - } - return CmuxTopProcessAttribution( - workspaceID: workspaceID, - workspaceRef: workspaceRef, - paneID: paneID, - paneRef: paneRef, - surfaceID: nil, - surfaceRef: nil, - surfaceType: nil, - reason: "shared-pane-process-tree" - ) - } - return CmuxTopProcessAttribution( - workspaceID: workspaceID, - workspaceRef: workspaceRef, - paneID: nil, - paneRef: nil, - surfaceID: nil, - surfaceRef: nil, - surfaceType: nil, - reason: "shared-workspace-process-tree" - ) - } - private nonisolated func v2TopMemoryAttributionSpecificity(_ attribution: CmuxTopProcessAttribution) -> Int { - if attribution.surfaceID != nil { - return 3 - } - if attribution.paneID != nil { - return 2 - } - if attribution.workspaceID != nil { - return 1 - } - return 0 + attribution.owner.specificity } nonisolated func v2AttachTopApplicationProcess( diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index b8f2a0565398..b50a4619b8a2 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -560,6 +560,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources C51A75000000000000000001 /* CMUXCLI+JSONOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51A75000000000000000002 /* CMUXCLI+JSONOutput.swift */; }; 489F4CF9B768C42D87B5EB2F /* CMUXCLI+KimiHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3D532FF0A00E20DA31667F /* CMUXCLI+KimiHooks.swift */; }; B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */; }; + 906902000000000000000002 /* CMUXCLI+MemoryAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906902000000000000000001 /* CMUXCLI+MemoryAttribution.swift */; }; 812600000000000000000005 /* CMUXCLI+MoshTerminalTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812600000000000000000006 /* CMUXCLI+MoshTerminalTransport.swift */; }; D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */; }; B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */; }; @@ -722,17 +723,23 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources C5A1FED100000000000000D1 /* CmuxSwiftRenderUI in Frameworks */ = {isa = PBXBuildFile; productRef = C5A1FED100000000000000D3 /* CmuxSwiftRenderUI */; }; C5A1FED100000000000000D2 /* CmuxSwiftRenderUI in Frameworks */ = {isa = PBXBuildFile; productRef = C5A1FED100000000000000D3 /* CmuxSwiftRenderUI */; }; 0A1107110000000000000016 /* CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1107110000000000000017 /* CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift */; }; + 906901000000000000000004 /* CmuxTaskManagerMemoryAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906901000000000000000003 /* CmuxTaskManagerMemoryAttribution.swift */; }; + 906901000000000000000006 /* CmuxTaskManagerMemoryGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906901000000000000000005 /* CmuxTaskManagerMemoryGroup.swift */; }; + 90690100000000000000000C /* CmuxTaskManagerMemoryGroupAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90690100000000000000000B /* CmuxTaskManagerMemoryGroupAttribution.swift */; }; C750500000000000000000A3 /* CmuxTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = C750500000000000000000A2 /* CmuxTerminal */; }; C750200000000000000000A3 /* CmuxTerminalCore in Frameworks */ = {isa = PBXBuildFile; productRef = C750200000000000000000A2 /* CmuxTerminalCore */; }; CFF12E0000000000000000A3 /* CmuxTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CFF12E0000000000000000A2 /* CmuxTestSupport */; }; 906900000000000000000002 /* CmuxTopMemoryAttributionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906900000000000000000001 /* CmuxTopMemoryAttributionTests.swift */; }; + 906901000000000000000008 /* CmuxTopMemoryDiagnosticGroupAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906901000000000000000007 /* CmuxTopMemoryDiagnosticGroupAccumulator.swift */; }; C7A510000000000000000002 /* CmuxTopMemoryDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A510000000000000000001 /* CmuxTopMemoryDiagnostics.swift */; }; B35750000000000000000004 /* CmuxTopProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35750000000000000000003 /* CmuxTopProcessArguments.swift */; }; C7A50C000000000000000003 /* CmuxTopProcessArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50C000000000000000004 /* CmuxTopProcessArgumentsTests.swift */; }; + 906901000000000000000002 /* CmuxTopProcessAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906901000000000000000001 /* CmuxTopProcessAttribution.swift */; }; C7A50C000000000000000002 /* CmuxTopProcessCPUTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */; }; C7A50D000000000000000002 /* CmuxTopProcessCPUTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50D000000000000000001 /* CmuxTopProcessCPUTracker.swift */; }; C7A50E000000000000000002 /* CmuxTopProcessDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50E000000000000000001 /* CmuxTopProcessDetails.swift */; }; C7A511000000000000000002 /* CmuxTopProcessEnumeration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A511000000000000000001 /* CmuxTopProcessEnumeration.swift */; }; + 90690100000000000000000A /* CmuxTopProcessOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906901000000000000000009 /* CmuxTopProcessOwner.swift */; }; 0A1107110000000000000018 /* CmuxTopProcessSnapshot+PromptAgentDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1107110000000000000019 /* CmuxTopProcessSnapshot+PromptAgentDetection.swift */; }; C7A512000000000000000002 /* CmuxTopProcessSnapshotCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A512000000000000000001 /* CmuxTopProcessSnapshotCache.swift */; }; C7A501000000000000000002 /* CmuxTopSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A501000000000000000001 /* CmuxTopSnapshot.swift */; }; @@ -3005,6 +3012,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C51A75000000000000000002 /* CMUXCLI+JSONOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+JSONOutput.swift"; sourceTree = ""; }; 7A3D532FF0A00E20DA31667F /* CMUXCLI+KimiHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+KimiHooks.swift"; sourceTree = ""; }; B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+Memory.swift"; sourceTree = ""; }; + 906902000000000000000001 /* CMUXCLI+MemoryAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MemoryAttribution.swift"; sourceTree = ""; }; 812600000000000000000006 /* CMUXCLI+MoshTerminalTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MoshTerminalTransport.swift"; sourceTree = ""; }; D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+OmpExtension.swift"; sourceTree = ""; }; @@ -3118,15 +3126,21 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DE43000000000000000004 /* CmuxSurfaceTabBarBuiltInAction+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxSurfaceTabBarBuiltInAction+Codable.swift"; sourceTree = ""; }; C10D00040000000000000004 /* CmuxSurfaceTabBarBuiltInAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxSurfaceTabBarBuiltInAction.swift; sourceTree = ""; }; 0A1107110000000000000017 /* CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift"; sourceTree = ""; }; + 906901000000000000000003 /* CmuxTaskManagerMemoryAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTaskManagerMemoryAttribution.swift; sourceTree = ""; }; + 906901000000000000000005 /* CmuxTaskManagerMemoryGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTaskManagerMemoryGroup.swift; sourceTree = ""; }; + 90690100000000000000000B /* CmuxTaskManagerMemoryGroupAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTaskManagerMemoryGroupAttribution.swift; sourceTree = ""; }; F1000002A1B2C3D4E5F60718 /* cmuxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = cmuxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 906900000000000000000001 /* CmuxTopMemoryAttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopMemoryAttributionTests.swift; sourceTree = ""; }; + 906901000000000000000007 /* CmuxTopMemoryDiagnosticGroupAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopMemoryDiagnosticGroupAccumulator.swift; sourceTree = ""; }; C7A510000000000000000001 /* CmuxTopMemoryDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopMemoryDiagnostics.swift; sourceTree = ""; }; B35750000000000000000003 /* CmuxTopProcessArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessArguments.swift; sourceTree = ""; }; C7A50C000000000000000004 /* CmuxTopProcessArgumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessArgumentsTests.swift; sourceTree = ""; }; + 906901000000000000000001 /* CmuxTopProcessAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessAttribution.swift; sourceTree = ""; }; C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessCPUTests.swift; sourceTree = ""; }; C7A50D000000000000000001 /* CmuxTopProcessCPUTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessCPUTracker.swift; sourceTree = ""; }; C7A50E000000000000000001 /* CmuxTopProcessDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessDetails.swift; sourceTree = ""; }; C7A511000000000000000001 /* CmuxTopProcessEnumeration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessEnumeration.swift; sourceTree = ""; }; + 906901000000000000000009 /* CmuxTopProcessOwner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessOwner.swift; sourceTree = ""; }; 0A1107110000000000000019 /* CmuxTopProcessSnapshot+PromptAgentDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTopProcessSnapshot+PromptAgentDetection.swift"; sourceTree = ""; }; C7A512000000000000000001 /* CmuxTopProcessSnapshotCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessSnapshotCache.swift; sourceTree = ""; }; C7A501000000000000000001 /* CmuxTopSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshot.swift; sourceTree = ""; }; @@ -5768,6 +5782,9 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C7A505000000000000000001 /* TerminalControllerTopSupport.swift */, C7A501000000000000000001 /* CmuxTopSnapshot.swift */, C7A510000000000000000001 /* CmuxTopMemoryDiagnostics.swift */, + 906901000000000000000007 /* CmuxTopMemoryDiagnosticGroupAccumulator.swift */, + 906901000000000000000001 /* CmuxTopProcessAttribution.swift */, + 906901000000000000000009 /* CmuxTopProcessOwner.swift */, C7A511000000000000000001 /* CmuxTopProcessEnumeration.swift */, C7A512000000000000000001 /* CmuxTopProcessSnapshotCache.swift */, B35750000000000000000003 /* CmuxTopProcessArguments.swift */, @@ -5806,6 +5823,9 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C7A506000000000000000001 /* TaskManagerView.swift */, C7A503000000000000000001 /* TaskManagerSnapshot.swift */, C7A504000000000000000001 /* TaskManagerTypes.swift */, + 906901000000000000000003 /* CmuxTaskManagerMemoryAttribution.swift */, + 906901000000000000000005 /* CmuxTaskManagerMemoryGroup.swift */, + 90690100000000000000000B /* CmuxTaskManagerMemoryGroupAttribution.swift */, B79500130000000000000002 /* AgentPortPublicationHistory.swift */, B79500120000000000000002 /* AgentPortRootIdentity.swift */, B79500050000000000000002 /* AgentPortScanPublication.swift */, @@ -6612,6 +6632,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C05555010000000000000008 /* CMUXCLI+PiExtensionSourcePart2.swift */, C08672010000000000000008 /* PiCompactedFeedEventExpander.swift */, B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */, + 906902000000000000000001 /* CMUXCLI+MemoryAttribution.swift */, C51A75000000000000000002 /* CMUXCLI+JSONOutput.swift */, C51A73000000000000000002 /* CMUXCLI+Simulator.swift */, C51A73B20000000000000001 /* IOSScreenshotCommandResultBox.swift */, @@ -8119,11 +8140,17 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DE43000000000000000003 /* CmuxSurfaceTabBarBuiltInAction+Codable.swift in Sources */, C10D00030000000000000003 /* CmuxSurfaceTabBarBuiltInAction.swift in Sources */, 0A1107110000000000000016 /* CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift in Sources */, + 906901000000000000000004 /* CmuxTaskManagerMemoryAttribution.swift in Sources */, + 906901000000000000000006 /* CmuxTaskManagerMemoryGroup.swift in Sources */, + 90690100000000000000000C /* CmuxTaskManagerMemoryGroupAttribution.swift in Sources */, + 906901000000000000000008 /* CmuxTopMemoryDiagnosticGroupAccumulator.swift in Sources */, C7A510000000000000000002 /* CmuxTopMemoryDiagnostics.swift in Sources */, B35750000000000000000004 /* CmuxTopProcessArguments.swift in Sources */, + 906901000000000000000002 /* CmuxTopProcessAttribution.swift in Sources */, C7A50D000000000000000002 /* CmuxTopProcessCPUTracker.swift in Sources */, C7A50E000000000000000002 /* CmuxTopProcessDetails.swift in Sources */, C7A511000000000000000002 /* CmuxTopProcessEnumeration.swift in Sources */, + 90690100000000000000000A /* CmuxTopProcessOwner.swift in Sources */, 0A1107110000000000000018 /* CmuxTopProcessSnapshot+PromptAgentDetection.swift in Sources */, C7A512000000000000000002 /* CmuxTopProcessSnapshotCache.swift in Sources */, C7A501000000000000000002 /* CmuxTopSnapshot.swift in Sources */, @@ -9310,6 +9337,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C51A75000000000000000001 /* CMUXCLI+JSONOutput.swift in Sources */, 489F4CF9B768C42D87B5EB2F /* CMUXCLI+KimiHooks.swift in Sources */, B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */, + 906902000000000000000002 /* CMUXCLI+MemoryAttribution.swift in Sources */, 812600000000000000000005 /* CMUXCLI+MoshTerminalTransport.swift in Sources */, D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */, B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */, diff --git a/cmuxTests/CmuxTopMemoryAttributionTests.swift b/cmuxTests/CmuxTopMemoryAttributionTests.swift index 10e9bccd3267..b72538f76630 100644 --- a/cmuxTests/CmuxTopMemoryAttributionTests.swift +++ b/cmuxTests/CmuxTopMemoryAttributionTests.swift @@ -35,6 +35,34 @@ struct CmuxTopMemoryAttributionTests { #expect(!row.detail.contains(secondWorkspaceID.uuidString)) } + @Test func commonOwnerPreservesMatchingSurfaceWithoutPaneMetadata() throws { + let surfaceID = UUID(uuidString: "33333333-3333-3333-3333-333333333333")! + let first = owner(workspaceID: firstWorkspaceID, surfaceID: surfaceID) + let second = owner(workspaceID: firstWorkspaceID, surfaceID: surfaceID) + + let common = try #require(first.commonOwner(with: second)) + + #expect(common.workspaceID == firstWorkspaceID) + #expect(common.surfaceID == surfaceID) + } + + @Test func commonOwnerWidensDifferentSurfacesToWorkspace() throws { + let first = owner( + workspaceID: firstWorkspaceID, + surfaceID: UUID(uuidString: "33333333-3333-3333-3333-333333333333")! + ) + let second = owner( + workspaceID: firstWorkspaceID, + surfaceID: UUID(uuidString: "44444444-4444-4444-4444-444444444444")! + ) + + let common = try #require(first.commonOwner(with: second)) + + #expect(common.workspaceID == firstWorkspaceID) + #expect(common.paneID == nil) + #expect(common.surfaceID == nil) + } + private func memoryDiagnosticPayload() -> [String: Any] { let appPID = 100 let firstHelperPID = 101 @@ -97,4 +125,19 @@ struct CmuxTopMemoryAttributionTests { reason: "surface-process-tree" ) } + + private func owner( + workspaceID: UUID, + surfaceID: UUID + ) -> CmuxTopProcessOwner { + CmuxTopProcessOwner( + workspaceID: workspaceID, + workspaceRef: nil, + paneID: nil, + paneRef: nil, + surfaceID: surfaceID, + surfaceRef: nil, + surfaceType: "terminal" + ) + } } From 1e55a1016682084dfbfb72b736b66361bccf78b3 Mon Sep 17 00:00:00 2001 From: cmux reload-cloud Date: Tue, 28 Jul 2026 16:06:12 -0700 Subject: [PATCH 3/4] Expose memory group accumulator initializer --- Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift index 66c33eaf2045..faa286513cfd 100644 --- a/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift +++ b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift @@ -46,6 +46,11 @@ struct CmuxTopMemoryDiagnosticGroupAccumulator { private var ownerIdentityKeys: Set = [] private var workspaceIdentityKeys: Set = [] + init(id: String, name: String) { + self.id = id + self.name = name + } + mutating func append( process: CmuxTopProcessInfo, attribution: CmuxTopProcessAttribution? From b34c4c3af078db12cff7ce8ecce57479197e9f55 Mon Sep 17 00:00:00 2001 From: cmux reload-cloud Date: Tue, 28 Jul 2026 16:19:31 -0700 Subject: [PATCH 4/4] Address memory attribution review findings --- Sources/CmuxTaskManagerMemoryGroup.swift | 2 +- ...muxTaskManagerMemoryGroupAttribution.swift | 5 +- ...xTopMemoryDiagnosticGroupAccumulator.swift | 8 +-- cmuxTests/CMUXCLIMemoryAttributionTests.swift | 6 +- cmuxTests/CmuxTopMemoryAttributionTests.swift | 70 +++++++++++++++++-- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/Sources/CmuxTaskManagerMemoryGroup.swift b/Sources/CmuxTaskManagerMemoryGroup.swift index e3e2bca2e3da..61cef570a9db 100644 --- a/Sources/CmuxTaskManagerMemoryGroup.swift +++ b/Sources/CmuxTaskManagerMemoryGroup.swift @@ -22,7 +22,7 @@ struct CmuxTaskManagerMemoryGroup: Sendable { self.processIds = CmuxTaskManagerMemoryDiagnostic.intArray(payload["pids"]) let topAttribution = CmuxTaskManagerMemoryAttribution(payload["top_attribution"] as? [String: Any]) self.attribution = CmuxTaskManagerMemoryGroupAttribution( - payload["group_attribution"] as? [String: Any] + payload["group_attribution"] as? [String: Any] ?? [:] ) ?? topAttribution.map(CmuxTaskManagerMemoryGroupAttribution.common) ?? .unattributed diff --git a/Sources/CmuxTaskManagerMemoryGroupAttribution.swift b/Sources/CmuxTaskManagerMemoryGroupAttribution.swift index 175cb348b9fb..29e922ef86e6 100644 --- a/Sources/CmuxTaskManagerMemoryGroupAttribution.swift +++ b/Sources/CmuxTaskManagerMemoryGroupAttribution.swift @@ -7,9 +7,8 @@ enum CmuxTaskManagerMemoryGroupAttribution: Sendable { case partial(attributedProcessCount: Int, unattributedProcessCount: Int) case unattributed - init?(_ payload: [String: Any]?) { - guard let payload, - let kind = CmuxTaskManagerMemoryDiagnostic.string(payload["kind"]) else { + init?(_ payload: [String: Any]) { + guard let kind = CmuxTaskManagerMemoryDiagnostic.string(payload["kind"]) else { return nil } switch kind { diff --git a/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift index faa286513cfd..af9c0544b80f 100644 --- a/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift +++ b/Sources/CmuxTopMemoryDiagnosticGroupAccumulator.swift @@ -40,7 +40,7 @@ struct CmuxTopMemoryDiagnosticGroupAccumulator { let name: String var rssBytes: Int64 = 0 private var processIDs: [Int] = [] - private var attributions: [CmuxTopProcessAttribution: AttributionAccumulator] = [:] + private var attributions: [CmuxTopProcessOwner: AttributionAccumulator] = [:] private var attributedProcessCount = 0 private var commonOwner: CmuxTopProcessOwner? private var ownerIdentityKeys: Set = [] @@ -71,10 +71,10 @@ struct CmuxTopMemoryDiagnosticGroupAccumulator { if let workspaceIdentityKey = owner.workspaceIdentityKey { workspaceIdentityKeys.insert(workspaceIdentityKey) } - if attributions[attribution] == nil { - attributions[attribution] = AttributionAccumulator(attribution: attribution) + if attributions[owner] == nil { + attributions[owner] = AttributionAccumulator(attribution: attribution) } - attributions[attribution]?.append(process: process) + attributions[owner]?.append(process: process) } func payload() -> [String: Any] { diff --git a/cmuxTests/CMUXCLIMemoryAttributionTests.swift b/cmuxTests/CMUXCLIMemoryAttributionTests.swift index 3ea03396917e..b39bb87e0f52 100644 --- a/cmuxTests/CMUXCLIMemoryAttributionTests.swift +++ b/cmuxTests/CMUXCLIMemoryAttributionTests.swift @@ -16,6 +16,10 @@ extension CMUXCLIErrorOutputRegressionTests { environment.removeValue(forKey: key) } environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + environment["AppleLanguages"] = "(en)" + environment["AppleLocale"] = "en_US" + environment["LANG"] = "en_US.UTF-8" + environment["LC_ALL"] = "en_US.UTF-8" let result = runProcess( executablePath: cliPath, @@ -27,7 +31,7 @@ extension CMUXCLIErrorOutputRegressionTests { #expect(!result.timedOut, Comment(rawValue: result.stdout)) #expect(result.status == 0, Comment(rawValue: result.stdout)) #expect(result.stdout.contains("2 workspaces"), Comment(rawValue: result.stdout)) - #expect(!result.stdout.contains("workspace workspace:1"), Comment(rawValue: result.stdout)) + #expect(!result.stdout.contains("workspace workspace:"), Comment(rawValue: result.stdout)) } private static let multiWorkspaceMemoryResponse = #""" diff --git a/cmuxTests/CmuxTopMemoryAttributionTests.swift b/cmuxTests/CmuxTopMemoryAttributionTests.swift index b72538f76630..da36a7b78686 100644 --- a/cmuxTests/CmuxTopMemoryAttributionTests.swift +++ b/cmuxTests/CmuxTopMemoryAttributionTests.swift @@ -35,6 +35,45 @@ struct CmuxTopMemoryAttributionTests { #expect(!row.detail.contains(secondWorkspaceID.uuidString)) } + @Test func commandGroupCombinesDifferentReasonsForSameOwner() throws { + let appPID = 200 + let firstHelperPID = 201 + let secondHelperPID = 202 + let snapshot = CmuxTopProcessSnapshot( + processes: [ + process(pid: appPID, parentPID: 1, name: "cmux", residentBytes: 32 * 1024 * 1024), + process(pid: firstHelperPID, parentPID: appPID, name: "cmux", residentBytes: 12 * 1024 * 1024), + process(pid: secondHelperPID, parentPID: appPID, name: "cmux", residentBytes: 11 * 1024 * 1024) + ], + sampledAt: Date(timeIntervalSince1970: 0), + includesProcessDetails: true + ) + let payload = snapshot.memoryDiagnosticPayload( + appPID: appPID, + attributionByPID: [ + firstHelperPID: attribution( + workspaceID: firstWorkspaceID, + workspaceRef: "workspace:1", + reason: "surface-process-tree" + ), + secondHelperPID: attribution( + workspaceID: firstWorkspaceID, + workspaceRef: "workspace:1", + reason: "cmux-process-scope" + ) + ] + ) + let children = try #require(payload["children"] as? [String: Any]) + let groups = try #require(children["groups"] as? [[String: Any]]) + let group = try #require(groups.first) + let attributions = try #require(group["attributions"] as? [[String: Any]]) + let combined = try #require(attributions.first) + + #expect(attributions.count == 1) + #expect(combined["process_count"] as? Int == 2) + #expect(combined["pids"] as? [Int] == [firstHelperPID, secondHelperPID]) + } + @Test func commonOwnerPreservesMatchingSurfaceWithoutPaneMetadata() throws { let surfaceID = UUID(uuidString: "33333333-3333-3333-3333-333333333333")! let first = owner(workspaceID: firstWorkspaceID, surfaceID: surfaceID) @@ -63,6 +102,23 @@ struct CmuxTopMemoryAttributionTests { #expect(common.surfaceID == nil) } + @Test func commonOwnerCarriesMetadataWhenIdentifiersOverlap() throws { + let complete = owner( + workspaceID: firstWorkspaceID, + workspaceRef: "workspace:1" + ) + let idOnly = owner(workspaceID: firstWorkspaceID) + let refOnly = owner(workspaceRef: "workspace:1") + + let commonByID = try #require(idOnly.commonOwner(with: complete)) + let commonByRef = try #require(refOnly.commonOwner(with: complete)) + + #expect(commonByID.workspaceID == firstWorkspaceID) + #expect(commonByID.workspaceRef == "workspace:1") + #expect(commonByRef.workspaceID == firstWorkspaceID) + #expect(commonByRef.workspaceRef == "workspace:1") + } + private func memoryDiagnosticPayload() -> [String: Any] { let appPID = 100 let firstHelperPID = 101 @@ -112,7 +168,8 @@ struct CmuxTopMemoryAttributionTests { private func attribution( workspaceID: UUID, - workspaceRef: String + workspaceRef: String, + reason: String = "surface-process-tree" ) -> CmuxTopProcessAttribution { CmuxTopProcessAttribution( workspaceID: workspaceID, @@ -122,22 +179,23 @@ struct CmuxTopMemoryAttributionTests { surfaceID: nil, surfaceRef: nil, surfaceType: nil, - reason: "surface-process-tree" + reason: reason ) } private func owner( - workspaceID: UUID, - surfaceID: UUID + workspaceID: UUID? = nil, + workspaceRef: String? = nil, + surfaceID: UUID? = nil ) -> CmuxTopProcessOwner { CmuxTopProcessOwner( workspaceID: workspaceID, - workspaceRef: nil, + workspaceRef: workspaceRef, paneID: nil, paneRef: nil, surfaceID: surfaceID, surfaceRef: nil, - surfaceType: "terminal" + surfaceType: surfaceID == nil ? nil : "terminal" ) } }