Skip to content

Commit cceab71

Browse files
committed
prototype for perf swiftUI trace
1 parent d4d341d commit cceab71

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
#if canImport(SwiftUI)
15+
import FirebasePerformance
16+
import Foundation
17+
import SwiftUI
18+
19+
// foregroud trace, screen render, refresh times
20+
// background trace, duration
21+
class PerfTraceViewModel {
22+
enum TraceType: String {
23+
case forground
24+
case background
25+
}
26+
27+
let name: String
28+
var trace: Trace?
29+
var traceType: TraceType?
30+
31+
var refreshCount: Int64 = 0
32+
33+
lazy var displayLink: CADisplayLink = .init(target: self, selector: #selector(displayLinkStep))
34+
var previousTimestamp: CFTimeInterval = -1.0
35+
var slowFrameCount: Int64 = 0
36+
var totalFrameCount: Int64 = 0
37+
38+
init(name: String) {
39+
self.name = name
40+
}
41+
42+
func startScreenRenderMonitoring() {
43+
displayLink.add(to: .main, forMode: .common)
44+
}
45+
46+
func stopScreenRenderMonitoring() {
47+
displayLink.remove(from: .main, forMode: .common)
48+
}
49+
50+
// calling perf to start the trace
51+
func startTrace(_ traceType: TraceType) {
52+
if trace != nil {
53+
print("there is active trace already, can't start another one")
54+
}
55+
self.traceType = traceType
56+
trace = Performance().trace(name: "\(name)_\(traceType.rawValue)")
57+
print("trace start \(name)")
58+
trace?.start()
59+
if self.traceType == .forground {
60+
startScreenRenderMonitoring()
61+
}
62+
}
63+
64+
// calling perf to end the trace
65+
func endTrace() {
66+
print("trace end")
67+
if traceType == .forground {
68+
stopScreenRenderMonitoring()
69+
trace?.setValue(totalFrameCount, forMetric: "totalFrameCount")
70+
trace?.setValue(slowFrameCount, forMetric: "slowFrameCount")
71+
}
72+
trace?.stop()
73+
trace = nil
74+
traceType = nil
75+
refreshCount = 0
76+
}
77+
78+
func logChanges() {
79+
refreshCount = refreshCount + 1
80+
print("refresh count changed to: \(refreshCount)")
81+
trace?.setValue(refreshCount, forMetric: "refreshCount")
82+
}
83+
84+
@objc
85+
func displayLinkStep() {
86+
let currentTimestamp = displayLink.timestamp
87+
if previousTimestamp > 0 {
88+
let frameDuration = currentTimestamp - previousTimestamp
89+
if frameDuration > 1.0 / 59.0 {
90+
slowFrameCount += 1
91+
}
92+
}
93+
totalFrameCount += 1
94+
previousTimestamp = currentTimestamp
95+
}
96+
}
97+
98+
public struct PerfTracedView<Content: View>: View {
99+
@State private var viewModel: PerfTraceViewModel
100+
101+
let content: () -> Content
102+
103+
// Init through view builder
104+
public init(_ viewName: String, @ViewBuilder content: @escaping () -> Content) {
105+
self.content = content
106+
// print("Type of this content is: \(content()) ")
107+
viewModel = PerfTraceViewModel(name: viewName)
108+
}
109+
110+
@Environment(\.scenePhase) var scenePhase
111+
112+
public var body: some View {
113+
viewModel.logChanges()
114+
115+
return content()
116+
.onAppear {
117+
print("\(viewModel.name) On appear \(Date().timeIntervalSince1970)")
118+
viewModel.startTrace(.forground)
119+
}
120+
.onDisappear {
121+
print("\(viewModel.name) On disaappear \(Date().timeIntervalSince1970)")
122+
viewModel.endTrace()
123+
}
124+
.onChange(of: scenePhase) { oldValue, newPhase in
125+
if newPhase == .active {
126+
print("\(viewModel.name) Active \(Date().timeIntervalSince1970)")
127+
if viewModel.traceType == .forground {
128+
print("foreground trace already running")
129+
} else {
130+
viewModel.endTrace()
131+
viewModel.startTrace(.forground)
132+
}
133+
} else if newPhase == .inactive {
134+
print("\(viewModel.name) Inactive \(Date().timeIntervalSince1970)")
135+
viewModel.endTrace()
136+
viewModel.startTrace(.background)
137+
} else if newPhase == .background {
138+
print("\(viewModel.name) Background \(Date().timeIntervalSince1970)")
139+
}
140+
}
141+
}
142+
}
143+
144+
public extension View {
145+
func perfTrace(_ viewName: String) -> some View {
146+
return PerfTracedView(viewName) {
147+
self
148+
}
149+
}
150+
}
151+
#endif

Package.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,8 @@ let package = Package(
924924
.target(
925925
name: "FirebasePerformanceTarget",
926926
dependencies: [.target(name: "FirebasePerformance",
927+
condition: .when(platforms: [.iOS, .tvOS, .visionOS])),
928+
.target(name: "FirebasePerformanceSwift",
927929
condition: .when(platforms: [.iOS, .tvOS, .visionOS]))],
928930
path: "SwiftPM-PlatformExclude/FirebasePerformanceWrap"
929931
),
@@ -943,7 +945,12 @@ let package = Package(
943945
.product(name: "GULUserDefaults", package: "GoogleUtilities"),
944946
.product(name: "nanopb", package: "nanopb"),
945947
],
946-
path: "FirebasePerformance/Sources",
948+
path: "FirebasePerformance",
949+
source: [
950+
"Sources/",
951+
],
952+
exclude:
953+
["Sources/SwiftUI/"],
947954
publicHeadersPath: "Public",
948955
cSettings: [
949956
.headerSearchPath("../../"),
@@ -957,6 +964,11 @@ let package = Package(
957964
.linkedFramework("QuartzCore", .when(platforms: [.iOS, .tvOS])),
958965
]
959966
),
967+
.tartget(
968+
name: "FirebasePerformanceSwift",
969+
dependencies: ["FirebasePerformance"],
970+
path: "FirebasePerformance/Sources/SwiftUI/"
971+
),
960972
.testTarget(
961973
name: "PerformanceUnit",
962974
dependencies: [

0 commit comments

Comments
 (0)