ios_core/services/instruments/
graphics.rs1use tokio::io::{AsyncRead, AsyncWrite};
2
3use crate::services::dtx::codec::{DtxConnection, DtxError};
4use crate::services::dtx::primitive_enc::archived_object;
5use crate::services::dtx::types::{DtxMessage, DtxPayload};
6
7#[derive(Debug, Clone, PartialEq, serde::Serialize)]
8pub struct GraphicsSample {
9 pub payload: serde_json::Value,
10}
11
12pub struct GraphicsMonitorClient<S> {
13 conn: DtxConnection<S>,
14 channel_code: i32,
15}
16
17impl<S: AsyncRead + AsyncWrite + Unpin + Send> GraphicsMonitorClient<S> {
18 pub async fn connect(stream: S) -> Result<Self, DtxError> {
19 let mut conn = DtxConnection::new(stream);
20 let channel_code = conn.request_channel(super::GRAPHICS_MONITOR_SVC).await?;
21 conn.method_call(
22 channel_code,
23 "startSamplingAtTimeInterval:",
24 &[archived_object(
25 crate::proto::nskeyedarchiver_encode::archive_float(0.0),
26 )],
27 )
28 .await?;
29 Ok(Self { conn, channel_code })
30 }
31
32 pub async fn next_sample(&mut self) -> Result<GraphicsSample, DtxError> {
33 loop {
34 let msg = self.conn.recv().await?;
35 if msg.expects_reply {
36 self.conn.send_ack(&msg).await?;
37 }
38 if let Some(sample) = parse_graphics_message(&msg)? {
39 return Ok(sample);
40 }
41 }
42 }
43
44 pub async fn stop(&mut self) -> Result<(), DtxError> {
45 self.conn
46 .method_call_async(self.channel_code, "stopSampling", &[])
47 .await
48 }
49}
50
51fn parse_graphics_message(msg: &DtxMessage) -> Result<Option<GraphicsSample>, DtxError> {
52 let payload = match &msg.payload {
53 DtxPayload::Response(value) => value.clone(),
54 DtxPayload::Raw(bytes) => match super::unarchive_raw_payload(bytes) {
55 Some(value) => value,
56 None => return Ok(None),
57 },
58 DtxPayload::MethodInvocation { args, .. } if !args.is_empty() => {
59 let payload = if args.len() == 1 {
60 super::nsobject_to_json(&args[0])
61 } else {
62 serde_json::Value::Array(args.iter().map(super::nsobject_to_json).collect())
63 };
64 if is_capabilities_payload(&payload) {
65 return Ok(None);
66 }
67 return Ok(Some(GraphicsSample { payload }));
68 }
69 _ => return Ok(None),
70 };
71
72 let payload = super::nsobject_to_json(&payload);
73 if is_capabilities_payload(&payload) {
74 return Ok(None);
75 }
76
77 Ok(Some(GraphicsSample { payload }))
78}
79
80fn is_capabilities_payload(payload: &serde_json::Value) -> bool {
81 if let Some(object) = payload.as_object() {
82 return object.keys().any(|key| key.starts_with("com.apple."));
83 }
84 let Some(items) = payload.as_array() else {
85 return false;
86 };
87 let Some(first) = items.first().and_then(serde_json::Value::as_object) else {
88 return false;
89 };
90 first.keys().any(|key| key.starts_with("com.apple."))
91}
92
93#[cfg(test)]
94mod tests {
95 use indexmap::IndexMap;
96
97 use super::*;
98 use crate::services::dtx::types::NSObject;
99
100 #[test]
101 fn parses_graphics_response_payload() {
102 let msg = DtxMessage {
103 identifier: 1,
104 conversation_idx: 0,
105 channel_code: 9,
106 expects_reply: false,
107 payload: DtxPayload::Response(NSObject::Dict(IndexMap::from_iter([
108 ("fps".to_string(), NSObject::Double(59.8)),
109 ("renderer".to_string(), NSObject::String("AGX".into())),
110 ]))),
111 };
112
113 let sample = parse_graphics_message(&msg).unwrap().unwrap();
114 assert_eq!(sample.payload["fps"], serde_json::json!(59.8));
115 assert_eq!(sample.payload["renderer"], serde_json::json!("AGX"));
116 }
117}