Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,13 @@
A5001540 /* PortScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001541 /* PortScanner.swift */; };
C7A501000000000000000002 /* CmuxTopSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A501000000000000000001 /* CmuxTopSnapshot.swift */; };
C7A508000000000000000002 /* CmuxTopSnapshotScopeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A508000000000000000001 /* CmuxTopSnapshotScopeCache.swift */; };
C7A50D000000000000000002 /* CmuxTopProcessCPUTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50D000000000000000001 /* CmuxTopProcessCPUTracker.swift */; };
C7A50E000000000000000002 /* CmuxTopProcessDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50E000000000000000001 /* CmuxTopProcessDetails.swift */; };
C7A502000000000000000002 /* TaskManagerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A502000000000000000001 /* TaskManagerWindowController.swift */; };
C7A506000000000000000002 /* TaskManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A506000000000000000001 /* TaskManagerView.swift */; };
C7A507000000000000000002 /* TaskManagerResourcesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A507000000000000000001 /* TaskManagerResourcesTests.swift */; };
C7A509000000000000000002 /* CmuxTopSnapshotScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */; };
C7A50C000000000000000002 /* CmuxTopProcessCPUTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */; };
C7A503000000000000000002 /* TaskManagerSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A503000000000000000001 /* TaskManagerSnapshot.swift */; };
C7A504000000000000000002 /* TaskManagerTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A504000000000000000001 /* TaskManagerTypes.swift */; };
C7A505000000000000000002 /* TerminalControllerTopSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A505000000000000000001 /* TerminalControllerTopSupport.swift */; };
Expand Down Expand Up @@ -595,10 +598,13 @@
C7A50B000000000000000001 /* TerminalControllerV2ParamParsingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerV2ParamParsingSupport.swift; sourceTree = "<group>"; };
C7A501000000000000000001 /* CmuxTopSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshot.swift; sourceTree = "<group>"; };
C7A508000000000000000001 /* CmuxTopSnapshotScopeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshotScopeCache.swift; sourceTree = "<group>"; };
C7A50D000000000000000001 /* CmuxTopProcessCPUTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessCPUTracker.swift; sourceTree = "<group>"; };
C7A50E000000000000000001 /* CmuxTopProcessDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessDetails.swift; sourceTree = "<group>"; };
C7A502000000000000000001 /* TaskManagerWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManagerWindowController.swift; sourceTree = "<group>"; };
C7A506000000000000000001 /* TaskManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManagerView.swift; sourceTree = "<group>"; };
C7A507000000000000000001 /* TaskManagerResourcesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManagerResourcesTests.swift; sourceTree = "<group>"; };
C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshotScopeTests.swift; sourceTree = "<group>"; };
C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopProcessCPUTests.swift; sourceTree = "<group>"; };
C7A503000000000000000001 /* TaskManagerSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManagerSnapshot.swift; sourceTree = "<group>"; };
C7A504000000000000000001 /* TaskManagerTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManagerTypes.swift; sourceTree = "<group>"; };
C7A505000000000000000001 /* TerminalControllerTopSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerTopSupport.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1081,6 +1087,8 @@
C7A505000000000000000001 /* TerminalControllerTopSupport.swift */,
C7A501000000000000000001 /* CmuxTopSnapshot.swift */,
C7A508000000000000000001 /* CmuxTopSnapshotScopeCache.swift */,
C7A50D000000000000000001 /* CmuxTopProcessCPUTracker.swift */,
C7A50E000000000000000001 /* CmuxTopProcessDetails.swift */,
C7A502000000000000000001 /* TaskManagerWindowController.swift */,
C7A506000000000000000001 /* TaskManagerView.swift */,
C7A503000000000000000001 /* TaskManagerSnapshot.swift */,
Expand Down Expand Up @@ -1305,6 +1313,7 @@
51D800000000000000000002 /* SidebarIdentifierFormattingTests.swift */,
C7A507000000000000000001 /* TaskManagerResourcesTests.swift */,
C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */,
C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */,
D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */,
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */,
9C1BEA3D2E6F49709A71C021 /* TerminalControllerSocketWriteTests.swift */,
Expand Down Expand Up @@ -1668,6 +1677,8 @@
C7A505000000000000000002 /* TerminalControllerTopSupport.swift in Sources */,
C7A501000000000000000002 /* CmuxTopSnapshot.swift in Sources */,
C7A508000000000000000002 /* CmuxTopSnapshotScopeCache.swift in Sources */,
C7A50D000000000000000002 /* CmuxTopProcessCPUTracker.swift in Sources */,
C7A50E000000000000000002 /* CmuxTopProcessDetails.swift in Sources */,
C7A502000000000000000002 /* TaskManagerWindowController.swift in Sources */,
C7A506000000000000000002 /* TaskManagerView.swift in Sources */,
C7A503000000000000000002 /* TaskManagerSnapshot.swift in Sources */,
Expand Down Expand Up @@ -1932,6 +1943,7 @@
51D800000000000000000001 /* SidebarIdentifierFormattingTests.swift in Sources */,
C7A507000000000000000002 /* TaskManagerResourcesTests.swift in Sources */,
C7A509000000000000000002 /* CmuxTopSnapshotScopeTests.swift in Sources */,
C7A50C000000000000000002 /* CmuxTopProcessCPUTests.swift in Sources */,
D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */,
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */,
9C1BEA3D2E6F49709A71C020 /* TerminalControllerSocketWriteTests.swift in Sources */,
Expand Down
125 changes: 125 additions & 0 deletions Sources/CmuxTopProcessCPUTracker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Darwin
import Foundation
import os

nonisolated struct CmuxTopProcessCPUSample: Sendable {
let totalTimeTicks: UInt64
let sampledAtNanoseconds: UInt64
}

private nonisolated struct CmuxTopProcessCPUTrackerState: Sendable {
var samples: [CmuxTopProcessScopeCacheKey: CmuxTopProcessCPUSample] = [:]
var latestPrunedAtNanoseconds: UInt64 = 0
}

private nonisolated final class CmuxTopProcessCPUTracker: @unchecked Sendable {
private let state = OSAllocatedUnfairLock(initialState: CmuxTopProcessCPUTrackerState())

// Snapshot capture is synchronous for the v2 socket path, so an actor would
// force that caller to block on async state. Keep OS sampling outside this
// owner and serialize only the CPU history read/compute/write transaction.
func cpuPercentages(
for currentSamples: [CmuxTopProcessScopeCacheKey: CmuxTopProcessCPUSample],
activeKeys: Set<CmuxTopProcessScopeCacheKey>,
sampledAtNanoseconds: UInt64
) -> [CmuxTopProcessScopeCacheKey: Double] {
state.withLock { state in
var percentages: [CmuxTopProcessScopeCacheKey: Double] = [:]
percentages.reserveCapacity(currentSamples.count)

for (key, sample) in currentSamples {
let existing = state.samples[key]
if let existing,
existing.sampledAtNanoseconds > sample.sampledAtNanoseconds {
continue
}

percentages[key] = CmuxTopProcessSnapshot.cpuPercent(
current: sample,
previous: existing
)
state.samples[key] = sample
}

// Overlapping captures can finish out of sample-time order; only
// the newest completed capture is allowed to evict inactive keys.
if sampledAtNanoseconds >= state.latestPrunedAtNanoseconds {
state.latestPrunedAtNanoseconds = sampledAtNanoseconds
state.samples = state.samples.filter { entry in
activeKeys.contains(entry.key)
}
}

return percentages
}
}
}

private nonisolated let cmuxTopProcessCPUTracker = CmuxTopProcessCPUTracker()
private nonisolated let cmuxTopAbsoluteTimeNanosecondsRatio: Double? = {
var info = mach_timebase_info_data_t()
guard mach_timebase_info(&info) == KERN_SUCCESS, info.denom > 0 else {
return nil
}
return Double(info.numer) / Double(info.denom)
}()

nonisolated extension CmuxTopProcessSnapshot {
static func cpuSampleClockNanoseconds() -> UInt64 {
clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
}

static func cpuPercentages(
for samples: [CmuxTopProcessScopeCacheKey: CmuxTopProcessCPUSample],
activeKeys: Set<CmuxTopProcessScopeCacheKey>,
sampledAtNanoseconds: UInt64
) -> [CmuxTopProcessScopeCacheKey: Double] {
cmuxTopProcessCPUTracker.cpuPercentages(
for: samples,
activeKeys: activeKeys,
sampledAtNanoseconds: sampledAtNanoseconds
)
}

static func cpuSample(
from taskInfo: proc_taskinfo,
sampledAtNanoseconds: UInt64
) -> CmuxTopProcessCPUSample {
CmuxTopProcessCPUSample(
totalTimeTicks: clampedCPUTimeTicks(taskInfo.pti_total_user, taskInfo.pti_total_system),
sampledAtNanoseconds: sampledAtNanoseconds
)
}

static func cpuPercent(
current: CmuxTopProcessCPUSample,
previous: CmuxTopProcessCPUSample?
) -> Double {
guard let previous,
current.sampledAtNanoseconds > previous.sampledAtNanoseconds,
current.totalTimeTicks >= previous.totalTimeTicks,
current.totalTimeTicks != UInt64.max,
previous.totalTimeTicks != UInt64.max else {
return 0
}

let cpuDelta = current.totalTimeTicks - previous.totalTimeTicks
let wallDeltaNanoseconds = current.sampledAtNanoseconds - previous.sampledAtNanoseconds
guard wallDeltaNanoseconds > 0 else { return 0 }

guard let cpuNanoseconds = absoluteTimeNanoseconds(cpuDelta) else { return 0 }
let wallNanoseconds = Double(wallDeltaNanoseconds)

return max(0, cpuNanoseconds / wallNanoseconds * 100.0)
}

private static func clampedCPUTimeTicks(_ user: UInt64, _ system: UInt64) -> UInt64 {
let (sum, overflow) = user.addingReportingOverflow(system)
return overflow ? UInt64.max : sum
}
Comment thread
lawrencecchen marked this conversation as resolved.

private static func absoluteTimeNanoseconds(_ ticks: UInt64) -> Double? {
guard let ratio = cmuxTopAbsoluteTimeNanosecondsRatio else { return nil }
return Double(ticks) * ratio
}
}
17 changes: 17 additions & 0 deletions Sources/CmuxTopProcessDetails.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ObjectiveC.runtime
import WebKit

enum CmuxWebContentProcessIdentifier {
@MainActor
static func pid(for webView: WKWebView) -> Int? {
let selector = NSSelectorFromString("_webProcessIdentifier")
guard let method = class_getInstanceMethod(WKWebView.self, selector) else {
return nil
}

typealias WebProcessIdentifierFn = @convention(c) (AnyObject, Selector) -> Int32
let implementation = method_getImplementation(method)
let pid = unsafeBitCast(implementation, to: WebProcessIdentifierFn.self)(webView, selector)
return pid > 0 ? Int(pid) : nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Loading
Loading