Skip to content
Merged
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
64 changes: 53 additions & 11 deletions cmuxTests/TerminalAndGhosttyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4314,7 +4314,13 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
let sampler = TaskVMInfoMemoryPressureFootprintSampler()
let sampleNoiseAllowance: UInt64 = 8 * 1_024 * 1_024

func settledFootprint(_ description: String) throws -> UInt64 {
func sampleFootprint(
_ description: String
) throws -> (median: UInt64, settled: Bool) {
// Freed malloc pages stay in the physical footprint until the
// allocator returns them. That is allocator caching, not renderer
// retention, so return them before every measurement.
_ = malloc_zone_pressure_relief(nil, 0)
let deadline = ProcessInfo.processInfo.systemUptime + 4
var recent: [UInt64] = []
while ProcessInfo.processInfo.systemUptime < deadline {
Expand All @@ -4334,20 +4340,21 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
let minimum = recent.min(),
let maximum = recent.max(),
maximum - minimum <= sampleNoiseAllowance {
return recent.sorted()[recent.count / 2]
return (recent.sorted()[recent.count / 2], true)
}
}
guard !recent.isEmpty else {
throw XCTSkip("task_vm_info did not provide a physical footprint for \(description)")
}
let minimum = recent.min() ?? 0
let maximum = recent.max() ?? 0
XCTAssertLessThanOrEqual(
maximum - minimum,
sampleNoiseAllowance,
"Physical footprint did not settle for \(description)"
)
return recent.sorted()[recent.count / 2]
return (recent.sorted()[recent.count / 2], maximum - minimum <= sampleNoiseAllowance)
}

func settledFootprint(_ description: String) throws -> UInt64 {
let sample = try sampleFootprint(description)
XCTAssertTrue(sample.settled, "Physical footprint did not settle for \(description)")
return sample.median
}

let hiddenSurfaces = Array(surfaces.dropFirst())
Expand Down Expand Up @@ -4393,8 +4400,42 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
"Cycle \(cycle) must leave only the visible tab's renderer realized"
)

let targetFootprint = try settledFootprint("cycle \(cycle) one-renderer target")
let realizedDelta = fiveRendererPeak - oneRendererBaseline
let retentionLimit = 0.45
let allowedTarget = oneRendererBaseline + sampleNoiseAllowance
+ UInt64(Double(realizedDelta) * retentionLimit)

// `releaseRenderer()` only publishes an unrealize request. The
// renderer thread applies it later, drains outstanding frame
// leases, and keeps compositor-owned IOSurfaces alive until the
// queued layer clear finishes (docs/ghostty-fork.md). On a loaded
// headless CI runner a plateau of not-yet-released memory can hold
// still for the whole seven-sample settle window, which read as
// retention: cycle 2 targets of 241-243 MB over a 206-210 MB
// baseline, with the next cycle's target back down to 225 MB.
//
// Keep sampling while the target is unsettled or over the limit,
// for a bounded window, and judge the lowest settled footprint.
// Only settled windows count, so a transient dip cannot pass the
// test. Memory the renderers still hold after the window is real
// retention and still fails.
let targetDescription = "cycle \(cycle) one-renderer target"
let reclaimStart = ProcessInfo.processInfo.systemUptime
let reclaimDeadline = reclaimStart + 20
let firstTarget = try sampleFootprint(targetDescription)
var targetFootprint = firstTarget.median
var targetSettled = firstTarget.settled
while !targetSettled || targetFootprint > allowedTarget,
ProcessInfo.processInfo.systemUptime < reclaimDeadline {
let sample = try sampleFootprint(targetDescription)
guard sample.settled else { continue }
targetFootprint = targetSettled
? min(targetFootprint, sample.median)
: sample.median
targetSettled = true
}
XCTAssertTrue(targetSettled, "Physical footprint did not settle for \(targetDescription)")
let reclaimWait = ProcessInfo.processInfo.systemUptime - reclaimStart

let retainedDelta = targetFootprint > oneRendererBaseline
? targetFootprint - oneRendererBaseline
Expand All @@ -4407,13 +4448,14 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
"renderer-memory cycle=\(cycle) one=\(oneRendererBaseline) " +
"five=\(fiveRendererPeak) target=\(targetFootprint) " +
"realized_delta=\(realizedDelta) noise_allowance=\(sampleNoiseAllowance) " +
"reclaim_wait=\(String(format: "%.2f", reclaimWait))s " +
"retained_ratio=\(normalizedRetainedRatio)"
)
XCTAssertLessThanOrEqual(
normalizedRetainedRatio,
0.45,
retentionLimit,
"Cycle \(cycle) cumulative retention above the one-renderer baseline "
+ "exceeds 45% of the five-renderer delta"
+ "exceeds \(Int(retentionLimit * 100))% of the five-renderer delta"
)
}

Expand Down
Loading