Skip to content

Commit fde5cfd

Browse files
committed
fix: optimize simctl list for online devices
1 parent 7a4b420 commit fde5cfd

File tree

3 files changed

+78
-32
lines changed

3 files changed

+78
-32
lines changed

devices/common.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package devices
22

33
import (
44
"fmt"
5+
"time"
56

67
"github.com/mobile-next/mobilecli/devices/wda"
78
"github.com/mobile-next/mobilecli/types"
@@ -58,47 +59,66 @@ type ControllableDevice interface {
5859
func GetAllControllableDevices(includeOffline bool) ([]ControllableDevice, error) {
5960
var allDevices []ControllableDevice
6061

62+
startTotal := time.Now()
63+
6164
// get Android devices
65+
startAndroid := time.Now()
6266
androidDevices, err := GetAndroidDevices()
67+
androidDuration := time.Since(startAndroid).Milliseconds()
68+
androidCount := 0
6369
if err != nil {
6470
utils.Verbose("Warning: Failed to get Android devices: %v", err)
6571
} else {
72+
androidCount = len(androidDevices)
6673
allDevices = append(allDevices, androidDevices...)
6774
}
6875

6976
// get offline Android emulators if requested
77+
offlineAndroidCount := 0
78+
offlineAndroidDuration := int64(0)
7079
if includeOffline {
80+
startOfflineAndroid := time.Now()
7181
// build map of online device IDs for quick lookup
7282
onlineDeviceIDs := make(map[string]bool)
7383
for _, device := range androidDevices {
7484
onlineDeviceIDs[device.ID()] = true
7585
}
7686

7787
offlineEmulators, err := getOfflineAndroidEmulators(onlineDeviceIDs)
88+
offlineAndroidDuration = time.Since(startOfflineAndroid).Milliseconds()
7889
if err != nil {
7990
utils.Verbose("Warning: Failed to get offline Android emulators: %v", err)
8091
} else {
92+
offlineAndroidCount = len(offlineEmulators)
8193
allDevices = append(allDevices, offlineEmulators...)
8294
}
8395
}
8496

8597
// get iOS real devices
98+
startIOS := time.Now()
8699
iosDevices, err := ListIOSDevices()
100+
iosDuration := time.Since(startIOS).Milliseconds()
101+
iosCount := 0
87102
if err != nil {
88103
utils.Verbose("Warning: Failed to get iOS real devices: %v", err)
89104
} else {
105+
iosCount = len(iosDevices)
90106
for _, device := range iosDevices {
91107
allDevices = append(allDevices, &device)
92108
}
93109
}
94110

95111
// get iOS simulator devices (all simulators, not just booted ones)
112+
startSimulators := time.Now()
96113
sims, err := GetSimulators()
114+
simulatorsDuration := time.Since(startSimulators).Milliseconds()
115+
simulatorsCount := 0
97116
if err != nil {
98117
utils.Verbose("Warning: Failed to get iOS simulators: %v", err)
99118
} else {
100119
// filter to only include simulators that have been booted at least once
101120
filteredSims := filterSimulatorsByDownloadsDirectory(sims)
121+
simulatorsCount = len(filteredSims)
102122
for _, sim := range filteredSims {
103123
allDevices = append(allDevices, &SimulatorDevice{
104124
Simulator: sim,
@@ -107,6 +127,17 @@ func GetAllControllableDevices(includeOffline bool) ([]ControllableDevice, error
107127
}
108128
}
109129

130+
totalDuration := time.Since(startTotal).Milliseconds()
131+
132+
// log all timing stats in one verbose message
133+
if includeOffline {
134+
utils.Verbose("GetAllControllableDevices completed in %dms: android=%dms (%d devices), offline_android=%dms (%d devices), ios=%dms (%d devices), simulators=%dms (%d devices)",
135+
totalDuration, androidDuration, androidCount, offlineAndroidDuration, offlineAndroidCount, iosDuration, iosCount, simulatorsDuration, simulatorsCount)
136+
} else {
137+
utils.Verbose("GetAllControllableDevices completed in %dms: android=%dms (%d devices), ios=%dms (%d devices), simulators=%dms (%d devices)",
138+
totalDuration, androidDuration, androidCount, iosDuration, iosCount, simulatorsDuration, simulatorsCount)
139+
}
140+
110141
return allDevices, nil
111142
}
112143

devices/simulator.go

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"os/exec"
9+
"path/filepath"
910
"regexp"
1011
"strconv"
1112
"strings"
@@ -15,6 +16,7 @@ import (
1516
"github.com/mobile-next/mobilecli/devices/wda"
1617
"github.com/mobile-next/mobilecli/devices/wda/mjpeg"
1718
"github.com/mobile-next/mobilecli/utils"
19+
"howett.net/plist"
1820
)
1921

2022
const (
@@ -31,6 +33,14 @@ type AppInfo struct {
3133
CFBundleVersion string `json:"CFBundleVersion"`
3234
}
3335

36+
// devicePlist represents the structure of device.plist
37+
type devicePlist struct {
38+
UDID string `plist:"UDID"`
39+
Name string `plist:"name"`
40+
Runtime string `plist:"runtime"`
41+
State int `plist:"state"`
42+
}
43+
3444
// Simulator represents an iOS simulator device
3545
type Simulator struct {
3646
Name string `json:"name"`
@@ -110,53 +120,58 @@ func runSimctl(args ...string) ([]byte, error) {
110120
return output, nil
111121
}
112122

113-
// getSimulators executes 'xcrun simctl list --json' and returns the parsed response
123+
// getSimulators reads simulator information from the filesystem
114124
func GetSimulators() ([]Simulator, error) {
115-
output, err := runSimctl("list", "--json")
125+
homeDir, err := os.UserHomeDir()
116126
if err != nil {
117-
return nil, fmt.Errorf("failed to execute xcrun simctl list: %w", err)
118-
}
119-
120-
var simulators map[string]interface{}
121-
if err := json.Unmarshal(output, &simulators); err != nil {
122-
return nil, fmt.Errorf("failed to parse simulator list JSON: %w", err)
127+
return nil, fmt.Errorf("failed to get user home directory: %w", err)
123128
}
124129

125-
devices, ok := simulators["devices"].(map[string]interface{})
126-
if !ok {
127-
return nil, fmt.Errorf("unexpected format in simulator list: devices not found or not a map")
130+
devicesPath := filepath.Join(homeDir, "Library", "Developer", "CoreSimulator", "Devices")
131+
entries, err := os.ReadDir(devicesPath)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to read devices directory: %w", err)
128134
}
129135

130-
var filteredDevices []Simulator
136+
var simulators []Simulator
131137

132-
for runtimeName, deviceList := range devices {
133-
deviceArray, ok := deviceList.([]interface{})
134-
if !ok {
138+
for _, entry := range entries {
139+
if !entry.IsDir() {
135140
continue
136141
}
137142

138-
for _, device := range deviceArray {
139-
deviceMap, ok := device.(map[string]interface{})
140-
if !ok {
141-
continue
142-
}
143+
plistPath := filepath.Join(devicesPath, entry.Name(), "device.plist")
144+
data, err := os.ReadFile(plistPath)
145+
if err != nil {
146+
// skip devices without device.plist
147+
continue
148+
}
143149

144-
name, _ := deviceMap["name"].(string)
145-
udid, _ := deviceMap["udid"].(string)
146-
state, _ := deviceMap["state"].(string)
150+
var device devicePlist
151+
if _, err := plist.Unmarshal(data, &device); err != nil {
152+
// skip devices with invalid plist
153+
continue
154+
}
147155

148-
simulator := Simulator{
149-
Name: name,
150-
UDID: udid,
151-
State: state,
152-
Runtime: runtimeName,
153-
}
156+
// convert state integer to string
157+
// state 1 = Shutdown (offline)
158+
// state 3 = Booted (online)
159+
stateStr := "Shutdown"
160+
if device.State == 3 {
161+
stateStr = "Booted"
162+
}
154163

155-
filteredDevices = append(filteredDevices, simulator)
164+
simulator := Simulator{
165+
Name: device.Name,
166+
UDID: device.UDID,
167+
State: stateStr,
168+
Runtime: device.Runtime,
156169
}
170+
171+
simulators = append(simulators, simulator)
157172
}
158173

159-
return filteredDevices, nil
174+
return simulators, nil
160175
}
161176

162177
// filterSimulatorsByDownloadsDirectory filters simulators that have been booted at least once

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/spf13/cobra v1.9.1
99
github.com/stretchr/testify v1.10.0
1010
gopkg.in/ini.v1 v1.67.0
11+
howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5
1112
)
1213

1314
replace github.com/quic-go/quic-go => github.com/quic-go/quic-go v0.49.1
@@ -44,6 +45,5 @@ require (
4445
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
4546
gopkg.in/yaml.v3 v3.0.1 // indirect
4647
gvisor.dev/gvisor v0.0.0-20240405191320-0878b34101b5 // indirect
47-
howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 // indirect
4848
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
4949
)

0 commit comments

Comments
 (0)