diff --git a/device.go b/device.go index 6d3d83e..b59de94 100644 --- a/device.go +++ b/device.go @@ -10,11 +10,12 @@ import ( "strings" "time" + uuid "github.com/satori/go.uuid" + "howett.net/plist" + "github.com/electricbubble/gidevice/pkg/ipa" "github.com/electricbubble/gidevice/pkg/libimobiledevice" "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" - uuid "github.com/satori/go.uuid" - "howett.net/plist" ) const LockdownPort = 62078 @@ -47,6 +48,7 @@ type device struct { springBoard SpringBoard crashReportMover CrashReportMover pcapd Pcapd + perfd []Perfd } func (d *device) Properties() DeviceProperties { @@ -315,14 +317,19 @@ func (d *device) InstallationProxyLookup(opts ...InstallationProxyOption) (looku return d.installationProxy.Lookup(opts...) } +func (d *device) newInstrumentsService() (instruments Instruments, err error) { + // NOTICE: each instruments service should have individual connection, otherwise it will be blocked + if _, err = d.lockdownService(); err != nil { + return + } + return d.lockdown.InstrumentsService() +} + func (d *device) instrumentsService() (instruments Instruments, err error) { if d.instruments != nil { return d.instruments, nil } - if _, err = d.lockdownService(); err != nil { - return nil, err - } - if d.instruments, err = d.lockdown.InstrumentsService(); err != nil { + if d.instruments, err = d.newInstrumentsService(); err != nil { return nil, err } instruments = d.instruments @@ -570,6 +577,102 @@ func (d *device) PcapStop() { d.pcapd.Stop() } +func (d *device) PerfStart(opts ...PerfOption) (data <-chan []byte, err error) { + perfOptions := defaulPerfOption() + for _, fn := range opts { + fn(perfOptions) + } + + // wait until get pid for bundle id + if perfOptions.BundleID != "" { + instruments, err := d.newInstrumentsService() + if err != nil { + fmt.Printf("get pid by bundle id failed: %v\n", err) + os.Exit(1) + } + + for { + pid, err := instruments.getPidByBundleID(perfOptions.BundleID) + if err != nil { + time.Sleep(1 * time.Second) + continue + } + perfOptions.Pid = pid + break + } + } + + // processAttributes must contain pid, or it can't get process info, reason unknown + if !containString(perfOptions.ProcessAttributes, "pid") { + perfOptions.ProcessAttributes = append(perfOptions.ProcessAttributes, "pid") + } + + outCh := make(chan []byte, 100) + + if perfOptions.SysCPU || perfOptions.SysMem || perfOptions.SysDisk || + perfOptions.SysNetwork { + perfd, err := d.newPerfdSysmontap(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + if perfOptions.Network { + perfd, err := d.newPerfdNetworking(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + if perfOptions.FPS || perfOptions.gpu { + perfd, err := d.newPerfdGraphicsOpengl(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + return outCh, nil +} + +func (d *device) PerfStop() { + if d.perfd == nil { + return + } + for _, p := range d.perfd { + p.Stop() + } +} + func (d *device) crashReportMoverService() (crashReportMover CrashReportMover, err error) { if d.crashReportMover != nil { return d.crashReportMover, nil @@ -783,7 +886,6 @@ func (d *device) XCTest(bundleID string, opts ...XCTestOption) (out <-chan strin } // time.Sleep(time.Second) close(_out) - return }() return _out, cancelFunc, err diff --git a/idevice.go b/idevice.go index 26c5df1..2ff71b4 100644 --- a/idevice.go +++ b/idevice.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log" "time" "github.com/electricbubble/gidevice/pkg/libimobiledevice" @@ -77,6 +76,9 @@ type Device interface { springBoardService() (springBoard SpringBoard, err error) GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) GetInterfaceOrientation() (orientation OrientationState, err error) + + PerfStart(opts ...PerfOption) (data <-chan []byte, err error) + PerfStop() } type DeviceProperties = libimobiledevice.DeviceProperties @@ -143,11 +145,13 @@ type Instruments interface { AppList(opts ...AppListOption) (apps []Application, err error) DeviceInfo() (devInfo *DeviceInfo, err error) + getPidByBundleID(bundleID string) (pid int, err error) appProcess(bundleID string) (err error) startObserving(pid int) (err error) notifyOfPublishedCapabilities() (err error) requestChannel(channel string) (id uint32, err error) + call(channel, selector string, auxiliaries ...interface{}) (result *libimobiledevice.DTXMessageResult, err error) // sysMonSetConfig(cfg ...interface{}) (err error) // SysMonStart(cfg ...interface{}) (_ interface{}, err error) @@ -217,6 +221,11 @@ type Pcapd interface { Stop() } +type Perfd interface { + Start() (data <-chan []byte, err error) + Stop() +} + type DiagnosticsRelay interface { Reboot() error Shutdown() error @@ -469,5 +478,5 @@ func debugLog(msg string) { if !debugFlag { return } - log.Println(fmt.Sprintf("[go-iDevice-debug] %s", msg)) + fmt.Printf("[go-iDevice-debug] %s\n", msg) } diff --git a/instruments.go b/instruments.go index a07f09d..b18147b 100644 --- a/instruments.go +++ b/instruments.go @@ -3,9 +3,26 @@ package giDevice import ( "encoding/json" "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" ) +// instruments services +const ( + instrumentsServiceDeviceInfo = "com.apple.instruments.server.services.deviceinfo" + instrumentsServiceProcessControl = "com.apple.instruments.server.services.processcontrol" + instrumentsServiceDeviceApplictionListing = "com.apple.instruments.server.services.device.applictionListing" + instrumentsServiceGraphicsOpengl = "com.apple.instruments.server.services.graphics.opengl" // 获取 GPU/FPS + instrumentsServiceSysmontap = "com.apple.instruments.server.services.sysmontap" // 获取 CPU/Mem/Disk/Network 性能数据 + instrumentsServiceNetworking = "com.apple.instruments.server.services.networking" // 获取所有网络详情数据 + instrumentsServiceMobileNotifications = "com.apple.instruments.server.services.mobilenotifications" // 监控应用状态 +) + +const ( + instrumentsServiceXcodeNetworkStatistics = "com.apple.xcode.debug-gauge-data-providers.NetworkStatistics" // 获取单进程网络数据 + instrumentsServiceXcodeEnergyStatistics = "com.apple.xcode.debug-gauge-data-providers.Energy" // 获取功耗数据 +) + var _ Instruments = (*instruments)(nil) func newInstruments(client *libimobiledevice.InstrumentsClient) *instruments { @@ -27,6 +44,53 @@ func (i *instruments) requestChannel(channel string) (id uint32, err error) { return i.client.RequestChannel(channel) } +func (i *instruments) call(channel, selector string, auxiliaries ...interface{}) ( + result *libimobiledevice.DTXMessageResult, err error) { + + chanID, err := i.requestChannel(channel) + if err != nil { + return nil, err + } + + args := libimobiledevice.NewAuxBuffer() + for _, aux := range auxiliaries { + if err = args.AppendObject(aux); err != nil { + return nil, err + } + } + + return i.client.Invoke(selector, args, chanID, true) +} + +func (i *instruments) getPidByBundleID(bundleID string) (pid int, err error) { + apps, err := i.AppList() + if err != nil { + fmt.Printf("get app list error: %v\n", err) + return 0, err + } + + mapper := make(map[string]interface{}) + for _, app := range apps { + mapper[app.ExecutableName] = app.CFBundleIdentifier + } + + processes, err := i.AppRunningProcesses() + if err != nil { + fmt.Printf("get running app processes error: %v\n", err) + return 0, err + } + for _, proc := range processes { + b, ok := mapper[proc.Name] + if ok && bundleID == b { + fmt.Printf("get pid %d by bundleId %s\n", proc.Pid, bundleID) + return proc.Pid, nil + } + } + + fmt.Printf("can't find pid by bundleID: %s\n", bundleID) + return 0, fmt.Errorf("can't find pid by bundleID: %s", bundleID) +} + func (i *instruments) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) { opt := new(appLaunchOption) opt.appPath = "" @@ -41,7 +105,7 @@ func (i *instruments) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid i } var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { return 0, err } @@ -77,7 +141,7 @@ func (i *instruments) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid i func (i *instruments) appProcess(bundleID string) (err error) { var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { return err } @@ -96,7 +160,7 @@ func (i *instruments) appProcess(bundleID string) (err error) { func (i *instruments) startObserving(pid int) (err error) { var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { return err } @@ -119,7 +183,7 @@ func (i *instruments) startObserving(pid int) (err error) { func (i *instruments) AppKill(pid int) (err error) { var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { return err } @@ -138,7 +202,7 @@ func (i *instruments) AppKill(pid int) (err error) { func (i *instruments) AppRunningProcesses() (processes []Process, err error) { var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.deviceinfo"); err != nil { + if id, err = i.requestChannel(instrumentsServiceDeviceInfo); err != nil { return nil, err } @@ -187,7 +251,7 @@ func (i *instruments) AppList(opts ...AppListOption) (apps []Application, err er } var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.device.applictionListing"); err != nil { + if id, err = i.requestChannel(instrumentsServiceDeviceApplictionListing); err != nil { return nil, err } @@ -232,7 +296,7 @@ func (i *instruments) AppList(opts ...AppListOption) (apps []Application, err er func (i *instruments) DeviceInfo() (devInfo *DeviceInfo, err error) { var id uint32 - if id, err = i.requestChannel("com.apple.instruments.server.services.deviceinfo"); err != nil { + if id, err = i.requestChannel(instrumentsServiceDeviceInfo); err != nil { return nil, err } diff --git a/perfd.go b/perfd.go new file mode 100644 index 0000000..995e40d --- /dev/null +++ b/perfd.go @@ -0,0 +1,892 @@ +package giDevice + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "net" + "strconv" + "time" + + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +type PerfOptions struct { + // system + SysCPU bool `json:"sys_cpu,omitempty" yaml:"sys_cpu,omitempty"` + SysMem bool `json:"sys_mem,omitempty" yaml:"sys_mem,omitempty"` + SysDisk bool `json:"sys_disk,omitempty" yaml:"sys_disk,omitempty"` + SysNetwork bool `json:"sys_network,omitempty" yaml:"sys_network,omitempty"` + gpu bool + FPS bool `json:"fps,omitempty" yaml:"fps,omitempty"` + Network bool `json:"network,omitempty" yaml:"network,omitempty"` + // process + BundleID string `json:"bundle_id,omitempty" yaml:"bundle_id,omitempty"` + Pid int `json:"pid,omitempty" yaml:"pid,omitempty"` + // config + OutputInterval int `json:"output_interval,omitempty" yaml:"output_interval,omitempty"` // ms + SystemAttributes []string `json:"system_attributes,omitempty" yaml:"system_attributes,omitempty"` + ProcessAttributes []string `json:"process_attributes,omitempty" yaml:"process_attributes,omitempty"` +} + +func defaulPerfOption() *PerfOptions { + return &PerfOptions{ + SysCPU: true, // default on + SysMem: true, // default on + SysDisk: false, + SysNetwork: false, + gpu: false, + FPS: false, + Network: false, + OutputInterval: 1000, // default 1000ms + SystemAttributes: []string{ + // disk + "diskBytesRead", + "diskBytesWritten", + "diskReadOps", + "diskWriteOps", + // memory + "vmCompressorPageCount", + "vmExtPageCount", + "vmFreeCount", + "vmIntPageCount", + "vmPurgeableCount", + "vmWireCount", + "vmUsedCount", + "__vmSwapUsage", + // network + "netBytesIn", + "netBytesOut", + "netPacketsIn", + "netPacketsOut", + }, + ProcessAttributes: []string{ + "pid", + "cpuUsage", + }, + } +} + +type PerfOption func(*PerfOptions) + +func WithPerfSystemCPU(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysCPU = b + } +} + +func WithPerfSystemMem(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysMem = b + } +} + +func WithPerfSystemDisk(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysDisk = b + } +} + +func WithPerfSystemNetwork(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysNetwork = b + } +} + +func WithPerfBundleID(bundleID string) PerfOption { + return func(opt *PerfOptions) { + opt.BundleID = bundleID + } +} + +func WithPerfPID(pid int) PerfOption { + return func(opt *PerfOptions) { + opt.Pid = pid + } +} + +func WithPerfGPU(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.gpu = b + } +} + +func WithPerfFPS(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.FPS = b + } +} + +func WithPerfNetwork(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.Network = b + } +} + +func WithPerfOutputInterval(intervalMilliseconds int) PerfOption { + return func(opt *PerfOptions) { + opt.OutputInterval = intervalMilliseconds + } +} + +func WithPerfProcessAttributes(attrs ...string) PerfOption { + return func(opt *PerfOptions) { + opt.ProcessAttributes = attrs + } +} + +func WithPerfSystemAttributes(attrs ...string) PerfOption { + return func(opt *PerfOptions) { + opt.SystemAttributes = attrs + } +} + +type perfdClient struct { + options *PerfOptions + i Instruments + stop chan struct{} // used to stop perf client + cancel context.CancelFunc // used to cancel all iterators +} + +func (d *device) newPerfdSysmontap(options *PerfOptions) (*perfdSysmontap, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdSysmontap{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanSysCPU: make(chan []byte, 10), + chanSysMem: make(chan []byte, 10), + chanSysDisk: make(chan []byte, 10), + chanSysNetwork: make(chan []byte, 10), + chanProcess: make(chan []byte, 10), + }, nil +} + +type perfdSysmontap struct { + perfdClient + chanSysCPU chan []byte // system cpu channel + chanSysMem chan []byte // system mem channel + chanSysDisk chan []byte // system disk channel + chanSysNetwork chan []byte // system network channel + chanProcess chan []byte // process channel +} + +func (c *perfdSysmontap) Start() (data <-chan []byte, err error) { + + // set config + config := map[string]interface{}{ + "bm": 0, + "cpuUsage": true, + "sampleInterval": time.Second * 1, // 1s + "ur": c.options.OutputInterval, // 输出频率 + "procAttrs": c.options.ProcessAttributes, // process performance + "sysAttrs": c.options.SystemAttributes, // system performance + } + if _, err = c.i.call( + instrumentsServiceSysmontap, + "setConfig:", + config, + ); err != nil { + return nil, err + } + + // start + if _, err = c.i.call( + instrumentsServiceSysmontap, + "start", + ); err != nil { + return nil, err + } + + // register listener + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceSysmontap, "stop") + return + default: + dataArray, ok := m.Obj.([]interface{}) + if !ok || len(dataArray) != 2 { + return + } + + if c.options.Pid != 0 { + c.parseProcessData(dataArray) + } else { + c.parseSystemData(dataArray) + } + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case cpuBytes, ok := <-c.chanSysCPU: + if ok { + outCh <- cpuBytes + } + case memBytes, ok := <-c.chanSysMem: + if ok { + outCh <- memBytes + } + case diskBytes, ok := <-c.chanSysDisk: + if ok { + outCh <- diskBytes + } + case networkBytes, ok := <-c.chanSysNetwork: + if ok { + outCh <- networkBytes + } + case processBytes, ok := <-c.chanProcess: + if ok { + outCh <- processBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdSysmontap) Stop() { + close(c.stop) +} + +func (c *perfdSysmontap) parseProcessData(dataArray []interface{}) { + // dataArray example: + // [ + // map[ + // CPUCount:2 + // EnabledCPUs:2 + // PerCPUUsage:[ + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:3.6363636363636402 CPU_UserLoad:-1] + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:2.7272727272727195 CPU_UserLoad:-1] + // ] + // System:[36408520704 6897049600 3031160 773697 15596 61940 1297 26942 588 17020 127346 1835008 119718056 107009899 174046 103548] + // SystemCPUUsage:map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:6.36363636363636 CPU_UserLoad:-1] + // StartMachAbsTime:5896602132889 + // EndMachAbsTime:5896628486761 + // Type:41 + // ] + // map[ + // Processes:map[ + // 0:[1.3582834340402803 0] + // 124:[0.011456702068519481 124] + // 136:[0.05468332721703649 136] + // ] + // StartMachAbsTime:5896602295095 + // EndMachAbsTime:5896628780514 + // Type:5 + // ] + // ] + + processData := make(map[string]interface{}) + processData["type"] = "process" + processData["timestamp"] = time.Now().Unix() + processData["pid"] = c.options.Pid + + defer func() { + processBytes, _ := json.Marshal(processData) + c.chanProcess <- processBytes + }() + + systemInfo := dataArray[0].(map[string]interface{}) + processInfo := dataArray[1].(map[string]interface{}) + if _, ok := systemInfo["System"]; !ok { + systemInfo, processInfo = processInfo, systemInfo + } + + var targetProcessValue []interface{} + processList := processInfo["Processes"].(map[string]interface{}) + for pid, v := range processList { + if pid != strconv.Itoa(c.options.Pid) { + continue + } + targetProcessValue = v.([]interface{}) + } + + if targetProcessValue == nil { + processData["msg"] = fmt.Sprintf("process %d not found", c.options.Pid) + return + } + + processAttributesMap := make(map[string]interface{}) + for idx, value := range c.options.ProcessAttributes { + processAttributesMap[value] = targetProcessValue[idx] + } + processData["proc_perf"] = processAttributesMap + + systemAttributesValue := systemInfo["System"].([]interface{}) + systemAttributesMap := make(map[string]int64) + for idx, value := range c.options.SystemAttributes { + systemAttributesMap[value] = convert2Int64(systemAttributesValue[idx]) + } + processData["sys_perf"] = systemAttributesMap +} + +func (c *perfdSysmontap) parseSystemData(dataArray []interface{}) { + timestamp := time.Now().Unix() + var systemInfo map[string]interface{} + data1 := dataArray[0].(map[string]interface{}) + data2 := dataArray[1].(map[string]interface{}) + if _, ok := data1["SystemCPUUsage"]; ok { + systemInfo = data1 + } else { + systemInfo = data2 + } + + // systemInfo example: + // map[ + // CPUCount:2 + // EnabledCPUs:2 + // PerCPUUsage:[ + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:3.9215686274509807 CPU_UserLoad:-1] + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:11.650485436893206 CPU_UserLoad:-1]] + // ] + // System:[704211 35486281728 6303789056 3001119 1001 11033 52668 1740 40022 2114 17310 126903 1835008 160323 107909856 95067 95808179] + // SystemCPUUsage:map[ + // CPU_NiceLoad:0 + // CPU_SystemLoad:-1 + // CPU_TotalLoad:15.572054064344186 + // CPU_UserLoad:-1 + // ] + // StartMachAbsTime:5339240248449 + // EndMachAbsTime:5339264441260 + // Type:41 + // ] + + if c.options.SysCPU { + sysCPUUsage := systemInfo["SystemCPUUsage"].(map[string]interface{}) + sysCPUInfo := SystemCPUData{ + PerfDataBase: PerfDataBase{ + Type: "sys_cpu", + TimeStamp: timestamp, + }, + NiceLoad: sysCPUUsage["CPU_NiceLoad"].(float64), + SystemLoad: sysCPUUsage["CPU_SystemLoad"].(float64), + TotalLoad: sysCPUUsage["CPU_TotalLoad"].(float64), + UserLoad: sysCPUUsage["CPU_UserLoad"].(float64), + } + cpuBytes, _ := json.Marshal(sysCPUInfo) + c.chanSysCPU <- cpuBytes + } + + systemAttributesValue := systemInfo["System"].([]interface{}) + systemAttributesMap := make(map[string]int64) + for idx, value := range c.options.SystemAttributes { + systemAttributesMap[value] = convert2Int64(systemAttributesValue[idx]) + } + + if c.options.SysMem { + kernelPageSize := int64(1) // why 16384 ? + appMemory := (systemAttributesMap["vmIntPageCount"] - systemAttributesMap["vmPurgeableCount"]) * kernelPageSize + cachedFiles := (systemAttributesMap["vmExtPageCount"] - systemAttributesMap["vmPurgeableCount"]) * kernelPageSize + compressed := systemAttributesMap["vmCompressorPageCount"] * kernelPageSize + usedMemory := (systemAttributesMap["vmUsedCount"] - systemAttributesMap["vmExtPageCount"]) * kernelPageSize + wiredMemory := systemAttributesMap["vmWireCount"] * kernelPageSize + swapUsed := systemAttributesMap["__vmSwapUsage"] + freeMemory := systemAttributesMap["vmFreeCount"] * kernelPageSize + + sysMemInfo := SystemMemData{ + PerfDataBase: PerfDataBase{ + Type: "sys_mem", + TimeStamp: timestamp, + }, + AppMemory: appMemory, + UsedMemory: usedMemory, + WiredMemory: wiredMemory, + FreeMemory: freeMemory, + CachedFiles: cachedFiles, + Compressed: compressed, + SwapUsed: swapUsed, + } + memBytes, _ := json.Marshal(sysMemInfo) + c.chanSysMem <- memBytes + } + + if c.options.SysDisk { + diskBytesRead := systemAttributesMap["diskBytesRead"] + diskBytesWritten := systemAttributesMap["diskBytesWritten"] + diskReadOps := systemAttributesMap["diskReadOps"] + diskWriteOps := systemAttributesMap["diskWriteOps"] + + sysDiskInfo := SystemDiskData{ + PerfDataBase: PerfDataBase{ + Type: "sys_disk", + TimeStamp: timestamp, + }, + DataRead: diskBytesRead, + DataWritten: diskBytesWritten, + ReadOps: diskReadOps, + WriteOps: diskWriteOps, + } + diskBytes, _ := json.Marshal(sysDiskInfo) + c.chanSysDisk <- diskBytes + } + + if c.options.SysNetwork { + netBytesIn := systemAttributesMap["netBytesIn"] + netBytesOut := systemAttributesMap["netBytesOut"] + netPacketsIn := systemAttributesMap["netPacketsIn"] + netPacketsOut := systemAttributesMap["netPacketsOut"] + + sysNetworkInfo := SystemNetworkData{ + PerfDataBase: PerfDataBase{ + Type: "sys_network", + TimeStamp: timestamp, + }, + BytesIn: netBytesIn, + BytesOut: netBytesOut, + PacketsIn: netPacketsIn, + PacketsOut: netPacketsOut, + } + networkBytes, _ := json.Marshal(sysNetworkInfo) + c.chanSysNetwork <- networkBytes + } +} + +type SystemCPUData struct { + PerfDataBase // system cpu + NiceLoad float64 `json:"nice_load"` + SystemLoad float64 `json:"system_load"` + TotalLoad float64 `json:"total_load"` + UserLoad float64 `json:"user_load"` +} + +type SystemMemData struct { + PerfDataBase // mem + AppMemory int64 `json:"app_memory"` + FreeMemory int64 `json:"free_memory"` + UsedMemory int64 `json:"used_memory"` + WiredMemory int64 `json:"wired_memory"` + CachedFiles int64 `json:"cached_files"` + Compressed int64 `json:"compressed"` + SwapUsed int64 `json:"swap_used"` +} + +type SystemDiskData struct { + PerfDataBase // disk + DataRead int64 `json:"data_read"` + DataWritten int64 `json:"data_written"` + ReadOps int64 `json:"reads_in"` + WriteOps int64 `json:"writes_out"` +} + +type SystemNetworkData struct { + PerfDataBase // network + BytesIn int64 `json:"bytes_in"` + BytesOut int64 `json:"bytes_out"` + PacketsIn int64 `json:"packets_in"` + PacketsOut int64 `json:"packets_out"` +} + +func (d *device) newPerfdNetworking(options *PerfOptions) (*perfdNetworking, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdNetworking{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanNetwork: make(chan []byte, 10), + }, nil +} + +type perfdNetworking struct { + perfdClient + chanNetwork chan []byte // network channel +} + +func (c *perfdNetworking) Start() (data <-chan []byte, err error) { + + if _, err = c.i.call( + instrumentsServiceNetworking, + "replayLastRecordedSession", + ); err != nil { + return nil, err + } + + if _, err = c.i.call( + instrumentsServiceNetworking, + "startMonitoring", + ); err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceNetworking, "stopMonitoring") + return + default: + c.parseNetworking(m.Obj) + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case networkBytes, ok := <-c.chanNetwork: + if ok { + outCh <- networkBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdNetworking) Stop() { + close(c.stop) +} + +func (c *perfdNetworking) parseNetworking(data interface{}) { + raw, ok := data.([]interface{}) + if !ok || len(raw) != 2 { + fmt.Printf("invalid networking data: %v\n", data) + return + } + + var netBytes []byte + msgType := raw[0].(uint64) + msgValue := raw[1].([]interface{}) + if msgType == 0 { + // interface-detection + // ['InterfaceIndex', "Name"] + // e.g. [0, [14, 'en0']] + netData := NetworkDataInterfaceDetection{ + PerfDataBase: PerfDataBase{ + Type: "network-interface-detection", + TimeStamp: time.Now().Unix(), + }, + InterfaceIndex: convert2Int64(msgValue[0]), + Name: msgValue[1].(string), + } + netBytes, _ = json.Marshal(netData) + } else if msgType == 1 { + // connection-detected + // ['LocalAddress', 'RemoteAddress', 'InterfaceIndex', 'Pid', + // 'RecvBufferSize', 'RecvBufferUsed', 'SerialNumber', 'Kind'] + // e.g. [1 [[16 2 211 158 192 168 100 101 0 0 0 0 0 0 0 0] + // [16 2 0 53 183 221 253 100 0 0 0 0 0 0 0 0] + // 14 -2 786896 0 133 2]] + + localAddr, err := parseSocketAddr(msgValue[0].([]byte)) + if err != nil { + fmt.Printf("parse local socket address err: %v\n", err) + } + remoteAddr, err := parseSocketAddr(msgValue[1].([]byte)) + if err != nil { + fmt.Printf("parse remote socket address err: %v\n", err) + } + netData := NetworkDataConnectionDetected{ + PerfDataBase: PerfDataBase{ + Type: "network-connection-detected", + TimeStamp: time.Now().Unix(), + }, + LocalAddress: localAddr, + RemoteAddress: remoteAddr, + InterfaceIndex: convert2Int64(msgValue[2]), + Pid: convert2Int64(msgValue[3]), + RecvBufferSize: convert2Int64(msgValue[4]), + RecvBufferUsed: convert2Int64(msgValue[5]), + SerialNumber: convert2Int64(msgValue[6]), + Kind: convert2Int64(msgValue[7]), + } + netBytes, _ = json.Marshal(netData) + } else if msgType == 2 { + // connection-update + // ['RxPackets', 'RxBytes', 'TxPackets', 'TxBytes', + // 'RxDups', 'RxOOO', 'TxRetx', 'MinRTT', 'AvgRTT', 'ConnectionSerial'] + // e.g. [2, [21, 1708, 22, 14119, 309, 0, 5830, 0.076125, 0.076125, 54, -1]] + netData := NetworkDataConnectionUpdate{ + PerfDataBase: PerfDataBase{ + Type: "network-connection-update", + TimeStamp: time.Now().Unix(), + }, + RxBytes: convert2Int64(msgValue[0]), + RxPackets: convert2Int64(msgValue[1]), + TxBytes: convert2Int64(msgValue[2]), + TxPackets: convert2Int64(msgValue[3]), + } + if value, ok := msgValue[4].(uint64); ok { + netData.RxDups = int64(value) + } + if value, ok := msgValue[5].(uint64); ok { + netData.RxOOO = int64(value) + } + if value, ok := msgValue[6].(uint64); ok { + netData.TxRetx = int64(value) + } + if value, ok := msgValue[7].(uint64); ok { + netData.MinRTT = int64(value) + } + if value, ok := msgValue[8].(uint64); ok { + netData.AvgRTT = int64(value) + } + if value, ok := msgValue[9].(uint64); ok { + netData.ConnectionSerial = int64(value) + } + + netBytes, _ = json.Marshal(netData) + } + c.chanNetwork <- netBytes +} + +func parseSocketAddr(data []byte) (string, error) { + len := data[0] // length of address + _ = data[1] // family + port := binary.BigEndian.Uint16(data[2:4]) // port + + // network, data[4:4+len] + if len == 0x10 { + // IPv4, 4 bytes + ip := net.IP(data[4:8]) + return fmt.Sprintf("%s:%d", ip, port), nil + } else if len == 0x1c { + // IPv6, 16 bytes + ip := net.IP(data[4:20]) + return fmt.Sprintf("%s:%d", ip, port), nil + } + return "", fmt.Errorf("invalid socket address: %v", data) +} + +type PerfDataBase struct { + Type string `json:"type"` + TimeStamp int64 `json:"timestamp"` + Msg string `json:"msg,omitempty"` // message for invalid data +} + +// network-interface-detection +type NetworkDataInterfaceDetection struct { + PerfDataBase + InterfaceIndex int64 `json:"interface_index"` // 0 + Name string `json:"name"` // 1 +} + +// network-connection-detected +type NetworkDataConnectionDetected struct { + PerfDataBase + LocalAddress string `json:"local_address"` // 0 + RemoteAddress string `json:"remote_address"` // 1 + InterfaceIndex int64 `json:"interface_index"` // 2 + Pid int64 `json:"pid"` // 3 + RecvBufferSize int64 `json:"recv_buffer_size"` // 4 + RecvBufferUsed int64 `json:"recv_buffer_used"` // 5 + SerialNumber int64 `json:"serial_number"` // 6 + Kind int64 `json:"kind"` // 7 +} + +// network-connection-update +type NetworkDataConnectionUpdate struct { + PerfDataBase + RxBytes int64 `json:"rx_bytes"` // 0 + RxPackets int64 `json:"rx_packets"` // 1 + TxBytes int64 `json:"tx_bytes"` // 2 + TxPackets int64 `json:"tx_packets"` // 3 + RxDups int64 `json:"rx_dups,omitempty"` // 4 + RxOOO int64 `json:"rx_000,omitempty"` // 5 + TxRetx int64 `json:"tx_retx,omitempty"` // 6 + MinRTT int64 `json:"min_rtt,omitempty"` // 7 + AvgRTT int64 `json:"avg_rtt,omitempty"` // 8 + ConnectionSerial int64 `json:"connection_serial"` // 9 +} + +func (d *device) newPerfdGraphicsOpengl(options *PerfOptions) (*perfdGraphicsOpengl, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdGraphicsOpengl{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanGPU: make(chan []byte, 10), + chanFPS: make(chan []byte, 10), + }, nil +} + +type perfdGraphicsOpengl struct { + perfdClient + chanGPU chan []byte // gpu channel + chanFPS chan []byte // fps channel +} + +func (c *perfdGraphicsOpengl) Start() (data <-chan []byte, err error) { + + if _, err = c.i.call( + instrumentsServiceGraphicsOpengl, + "setSamplingRate:", + float64(c.options.OutputInterval)/100, + ); err != nil { + return nil, err + } + + if _, err = c.i.call( + instrumentsServiceGraphicsOpengl, + "startSamplingAtTimeInterval:", + 0, + ); err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceGraphicsOpengl, "stopSampling") + return + default: + c.parseData(m.Obj) + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case gpuBytes, ok := <-c.chanGPU: + if ok { + outCh <- gpuBytes + } + case fpsBytes, ok := <-c.chanFPS: + if ok { + outCh <- fpsBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdGraphicsOpengl) Stop() { + close(c.stop) +} + +func (c *perfdGraphicsOpengl) parseData(data interface{}) { + // data example: + // map[ + // Alloc system memory:50167808 + // Allocated PB Size:1179648 + // CoreAnimationFramesPerSecond:0 // fps from GPU + // Device Utilization %:0 // device + // IOGLBundleName:Built-In + // In use system memory:10633216 + // Renderer Utilization %:0 // renderer + // SplitSceneCount:0 + // TiledSceneBytes:0 + // Tiler Utilization %:0 // tiler + // XRVideoCardRunTimeStamp:1010679 + // recoveryCount:0 + // ] + + gpuInfo := GPUData{ + PerfDataBase: PerfDataBase{ + Type: "gpu", + TimeStamp: time.Now().Unix(), + }, + } + fpsInfo := FPSData{ + PerfDataBase: PerfDataBase{ + Type: "fps", + TimeStamp: time.Now().Unix(), + }, + } + + defer func() { + if c.options.gpu { + gpuBytes, _ := json.Marshal(gpuInfo) + c.chanGPU <- gpuBytes + } + if c.options.FPS { + fpsBytes, _ := json.Marshal(fpsInfo) + c.chanFPS <- fpsBytes + } + }() + + raw, ok := data.(map[string]interface{}) + if !ok { + gpuInfo.Msg = fmt.Sprintf("invalid graphics.opengl data: %v", data) + return + } + + // gpu + gpuInfo.DeviceUtilization = convert2Int64(raw["Device Utilization %"]) + gpuInfo.TilerUtilization = convert2Int64(raw["Tiler Utilization %"]) + gpuInfo.RendererUtilization = convert2Int64(raw["Renderer Utilization %"]) + + // fps + fpsInfo.FPS = int(convert2Int64(raw["CoreAnimationFramesPerSecond"])) +} + +type GPUData struct { + PerfDataBase // gpu + TilerUtilization int64 `json:"tiler_utilization"` // 处理顶点的 GPU 时间占比 + DeviceUtilization int64 `json:"device_utilization"` // 设备利用率 + RendererUtilization int64 `json:"renderer_utilization"` // 渲染器利用率 +} + +type FPSData struct { + PerfDataBase // fps + FPS int `json:"fps"` +} + +func convert2Int64(num interface{}) int64 { + switch value := num.(type) { + case int64: + return value + case uint64: + return int64(value) + case uint32: + return int64(value) + case uint16: + return int64(value) + case uint8: + return int64(value) + case uint: + return int64(value) + } + fmt.Printf("convert2Int64 failed: %v, %T\n", num, num) + return -1 +} + +func containString(ss []string, s string) bool { + for _, v := range ss { + if s == v { + return true + } + } + return false +} diff --git a/perfd_test.go b/perfd_test.go new file mode 100644 index 0000000..14cf607 --- /dev/null +++ b/perfd_test.go @@ -0,0 +1,159 @@ +package giDevice + +import ( + "fmt" + "testing" + "time" +) + +func TestPerfSystemMonitor(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(true), + WithPerfSystemMem(true), + WithPerfSystemDisk(true), + WithPerfSystemNetwork(true), + WithPerfOutputInterval(1000), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfProcessMonitor(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfProcessAttributes("cpuUsage", "memAnon"), + WithPerfOutputInterval(1000), + WithPerfPID(100), + WithPerfBundleID("com.apple.mobilesafari"), // higher priority than pid + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfGPU(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfGPU(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfFPS(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfFPS(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfNetwork(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfNetwork(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfAll(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(true), + WithPerfSystemMem(true), + WithPerfSystemDisk(true), + WithPerfSystemNetwork(true), + WithPerfNetwork(true), + WithPerfFPS(true), + WithPerfGPU(true), + WithPerfBundleID("com.apple.mobilesafari"), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} diff --git a/pkg/libimobiledevice/auxbuffer.go b/pkg/libimobiledevice/auxbuffer.go index 966d45f..a48e77c 100644 --- a/pkg/libimobiledevice/auxbuffer.go +++ b/pkg/libimobiledevice/auxbuffer.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "errors" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" ) diff --git a/pkg/libimobiledevice/client_dtxmessage.go b/pkg/libimobiledevice/client_dtxmessage.go index cf09ada..067446a 100644 --- a/pkg/libimobiledevice/client_dtxmessage.go +++ b/pkg/libimobiledevice/client_dtxmessage.go @@ -5,12 +5,13 @@ import ( "context" "encoding/hex" "fmt" - "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" "io" "strings" "sync" "time" "unsafe" + + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" ) const ( @@ -112,7 +113,7 @@ func (c *dtxMessageClient) SendDTXMessage(selector string, aux []byte, channelCo func (c *dtxMessageClient) ReceiveDTXMessage() (result *DTXMessageResult, err error) { bufPayload := new(bytes.Buffer) - header := new(dtxMessageHeaderPacket) + var header *dtxMessageHeaderPacket = nil var needToReply *dtxMessageHeaderPacket = nil for { @@ -242,7 +243,6 @@ func (c *dtxMessageClient) ReceiveDTXMessage() (result *DTXMessageResult, err er c.mu.Lock() c.resultMap[sk] = result c.mu.Unlock() - } return diff --git a/pkg/libimobiledevice/keyedarchiver.go b/pkg/libimobiledevice/keyedarchiver.go index 1cb2e52..f942c5c 100644 --- a/pkg/libimobiledevice/keyedarchiver.go +++ b/pkg/libimobiledevice/keyedarchiver.go @@ -2,6 +2,7 @@ package libimobiledevice import ( "reflect" + "strconv" "time" "howett.net/plist" @@ -202,9 +203,17 @@ func (ka *NSKeyedArchiver) convertValue(v interface{}) interface{} { values := m["NS.objects"].([]interface{}) for i := 0; i < len(keys); i++ { - key := ka.objRefVal[keys[i].(plist.UID)].(string) + var keyValue string + key := ka.objRefVal[keys[i].(plist.UID)] + switch key.(type) { + case uint64: + keyValue = strconv.Itoa(int(key.(uint64))) + break + default: + keyValue = key.(string) + } val := ka.convertValue(ka.objRefVal[values[i].(plist.UID)]) - ret[key] = val + ret[keyValue] = val } return ret case NSMutableArrayClass.Classes[0], NSArrayClass.Classes[0]: diff --git a/pkg/libimobiledevice/lib.go b/pkg/libimobiledevice/lib.go index 950981e..39e8c93 100644 --- a/pkg/libimobiledevice/lib.go +++ b/pkg/libimobiledevice/lib.go @@ -2,7 +2,6 @@ package libimobiledevice import ( "bytes" - "fmt" "log" ) @@ -25,5 +24,5 @@ func debugLog(msg string) { if !debugFlag { return } - log.Println(fmt.Sprintf("[%s-debug] %s", ProgramName, msg)) + log.Printf("[%s-debug] %s\n", ProgramName, msg) } diff --git a/testmanagerd.go b/testmanagerd.go index d4a34c9..5ba78c9 100644 --- a/testmanagerd.go +++ b/testmanagerd.go @@ -36,7 +36,8 @@ func (t *testmanagerd) newXCTestManagerDaemon() (xcTestManager XCTestManagerDaem return } -func (t *testmanagerd) invoke(selector string, args *libimobiledevice.AuxBuffer, channelCode uint32, expectsReply bool) (result *libimobiledevice.DTXMessageResult, err error) { +func (t *testmanagerd) invoke(selector string, args *libimobiledevice.AuxBuffer, channelCode uint32, expectsReply bool) ( + result *libimobiledevice.DTXMessageResult, err error) { return t.client.Invoke(selector, args, channelCode, expectsReply) }