Skip to content

Commit cfb0a11

Browse files
tetujinclaude
andcommitted
Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit cfb0a11

8 files changed

Lines changed: 725 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Sensor CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
schedule:
8+
# Every Monday at 02:00 UTC
9+
- cron: '0 2 * * 1'
10+
11+
jobs:
12+
test:
13+
runs-on: macos-15
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Show Xcode and simulator info
20+
run: |
21+
xcodebuild -version
22+
swift --version
23+
xcrun simctl list runtimes
24+
xcrun simctl list devices available
25+
26+
- name: Resolve package dependencies
27+
run: swift package resolve
28+
29+
- name: Detect scheme and simulator
30+
run: |
31+
SCHEME=$(swift package dump-package | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])")
32+
echo "SCHEME=$SCHEME" >> "$GITHUB_ENV"
33+
34+
# Find the latest available iPhone simulator name (name-based, more robust than UDID)
35+
SIM_NAME=$(xcrun simctl list devices available --json | python3 -c "
36+
import json, sys
37+
devs = json.load(sys.stdin)['devices']
38+
iphones = [
39+
(rt, d['name'])
40+
for rt, dl in devs.items()
41+
for d in dl
42+
if 'iOS' in rt and 'iPhone' in d['name'] and d.get('isAvailable', True)
43+
]
44+
iphones.sort(reverse=True)
45+
print(iphones[0][1] if iphones else 'iPhone 16')
46+
")
47+
echo "SIM_NAME=$SIM_NAME" >> "$GITHUB_ENV"
48+
echo "Detected scheme: $SCHEME"
49+
echo "Detected simulator: $SIM_NAME"
50+
51+
- name: Build
52+
id: build
53+
run: |
54+
xcodebuild build \
55+
-scheme "$SCHEME" \
56+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
57+
-sdk iphonesimulator \
58+
2>&1 | tee build_output.txt; exit "${PIPESTATUS[0]}"
59+
continue-on-error: true
60+
61+
- name: Test
62+
id: test
63+
if: steps.build.outcome == 'success'
64+
run: |
65+
xcodebuild test \
66+
-scheme "$SCHEME" \
67+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
68+
-sdk iphonesimulator \
69+
-configuration Debug \
70+
-enableCodeCoverage YES \
71+
2>&1 | tee test_output.txt; exit "${PIPESTATUS[0]}"
72+
continue-on-error: true
73+
74+
- name: Create or update issue on failure
75+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
76+
uses: actions/github-script@v7
77+
with:
78+
github-token: ${{ secrets.GITHUB_TOKEN }}
79+
script: |
80+
const fs = require('fs');
81+
const failed = '${{ steps.build.outcome }}' === 'failure' ? 'build' : 'test';
82+
const logFile = failed === 'build' ? 'build_output.txt' : 'test_output.txt';
83+
84+
let raw = '';
85+
try { raw = fs.readFileSync(logFile, 'utf8'); } catch (_) {}
86+
87+
const errors = raw.split('\n')
88+
.filter(l => /error:|FAILED|\*\* BUILD FAILED/.test(l))
89+
.slice(0, 60)
90+
.join('\n');
91+
92+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
93+
const title = `[CI] ${failed} failure detected`;
94+
const body = [
95+
`## :x: Automated CI ${failed} failure`,
96+
'',
97+
`| | |`,
98+
`|---|---|`,
99+
`| **Workflow run** | [View logs](${runUrl}) |`,
100+
`| **Trigger** | \`${context.eventName}\` |`,
101+
`| **Ref** | \`${context.ref}\` |`,
102+
`| **Commit** | \`${context.sha.slice(0,7)}\` |`,
103+
'',
104+
`## Error summary`,
105+
'```',
106+
errors || '(no error lines captured — see full logs above)',
107+
'```',
108+
].join('\n');
109+
110+
// Ensure ci-failure label exists
111+
try {
112+
await github.rest.issues.createLabel({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
name: 'ci-failure',
116+
color: 'd73a4a',
117+
description: 'Automated CI test failure',
118+
});
119+
} catch (_) { /* label already exists */ }
120+
121+
const { data: issues } = await github.rest.issues.listForRepo({
122+
owner: context.repo.owner,
123+
repo: context.repo.repo,
124+
state: 'open',
125+
labels: 'ci-failure',
126+
});
127+
128+
if (issues.length > 0) {
129+
await github.rest.issues.createComment({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
issue_number: issues[0].number,
133+
body: `### :warning: New failure (${new Date().toISOString().split('T')[0]})\n\n${body}`,
134+
});
135+
} else {
136+
await github.rest.issues.create({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
title,
140+
body,
141+
labels: ['ci-failure'],
142+
});
143+
}
144+
145+
- name: Close ci-failure issue on success
146+
if: steps.build.outcome == 'success' && steps.test.outcome == 'success'
147+
uses: actions/github-script@v7
148+
with:
149+
github-token: ${{ secrets.GITHUB_TOKEN }}
150+
script: |
151+
const { data: issues } = await github.rest.issues.listForRepo({
152+
owner: context.repo.owner,
153+
repo: context.repo.repo,
154+
state: 'open',
155+
labels: 'ci-failure',
156+
});
157+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
158+
for (const issue of issues) {
159+
await github.rest.issues.createComment({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
issue_number: issue.number,
163+
body: `:white_check_mark: All tests are passing again. See [run ${context.runId}](${runUrl}).`,
164+
});
165+
await github.rest.issues.update({
166+
owner: context.repo.owner,
167+
repo: context.repo.repo,
168+
issue_number: issue.number,
169+
state: 'closed',
170+
});
171+
}
172+
173+
- name: Fail job if build or tests failed
174+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
175+
run: |
176+
echo "Build outcome: ${{ steps.build.outcome }}"
177+
echo "Test outcome: ${{ steps.test.outcome }}"
178+
exit 1
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>com.awareframework.ios.sensor.processor.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>30</integer>
11+
</dict>
12+
</dict>
13+
</dict>
14+
</plist>

Package.resolved

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "com.awareframework.ios.sensor.processor",
7+
platforms: [.iOS(.v13), .macOS(.v10_15)],
8+
products: [
9+
.library(
10+
name: "com.awareframework.ios.sensor.processor",
11+
targets: [
12+
"com.awareframework.ios.sensor.processor"
13+
]
14+
),
15+
],
16+
dependencies: [
17+
.package(url: "https://github.com/awareframework/com.awareframework.ios.core.git", from: "1.3.0")
18+
],
19+
targets: [
20+
.target(
21+
name: "com.awareframework.ios.sensor.processor",
22+
dependencies: [
23+
.product(name: "com.awareframework.ios.core", package: "com.awareframework.ios.core")
24+
],
25+
path: "Sources/com.awareframework.ios.sensor.processor"
26+
),
27+
.testTarget(
28+
name: "com.awareframework.ios.sensor.processorTests",
29+
dependencies: ["com.awareframework.ios.core", "com.awareframework.ios.sensor.processor"]
30+
)
31+
],
32+
swiftLanguageModes: [.v5]
33+
)

README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# AWARE Processor Sensor
2+
3+
The processor sensor samples app process resource usage and device processor state on iOS.
4+
It is designed for periodic, low-overhead monitoring of the AwareClient process, not for
5+
system-wide CPU profiling.
6+
7+
Records are stored in `ios_processor` under the `aware_processor.sqlite` database.
8+
9+
## Installation
10+
11+
Add the package to your app target:
12+
13+
```text
14+
https://github.com/awareframework/com.awareframework.ios.sensor.processor.git
15+
```
16+
17+
Then import the module:
18+
19+
```swift
20+
import com_awareframework_ios_sensor_processor
21+
```
22+
23+
## Usage
24+
25+
```swift
26+
let sensor = ProcessorSensor(ProcessorSensor.Config().apply { config in
27+
config.interval = 60 // seconds
28+
})
29+
30+
sensor.start()
31+
```
32+
33+
To stop collection:
34+
35+
```swift
36+
sensor.stop()
37+
```
38+
39+
## Configuration
40+
41+
`ProcessorSensor.Config` extends the common AWARE `SensorConfig`.
42+
43+
| Property | Type | Default | Description |
44+
| --- | --- | --- | --- |
45+
| `interval` | `Int` | `60` | Sampling interval in seconds. Values less than `1` are ignored. |
46+
| `sensorObserver` | `ProcessorObserver?` | `nil` | Callback for live processor samples. |
47+
| `dbPath` | `String` | `aware_processor` | SQLite database path stem. |
48+
| `dbTableName` | `String?` | `ios_processor` in AwareClient | Active database table name. |
49+
50+
## Data Model
51+
52+
`ProcessorData` stores the following fields:
53+
54+
| Field | Type | Description |
55+
| --- | --- | --- |
56+
| `timestamp` | `Int64` | Unix timestamp in milliseconds. |
57+
| `deviceId` | `String` | AWARE device identifier. |
58+
| `label` | `String` | Optional sensor label. |
59+
| `appCpuUsage` | `Double` | CPU usage percentage for the current app process. |
60+
| `activeProcessorCount` | `Int` | Number of active logical processors. |
61+
| `processorCount` | `Int` | Total number of logical processors. |
62+
| `residentMemory` | `Int64` | Current app memory footprint in bytes. |
63+
| `physicalMemory` | `Int64` | Device physical memory in bytes. |
64+
| `memoryUsage` | `Double` | App memory footprint as a percentage of physical memory. |
65+
| `systemUptime` | `Double` | System uptime in seconds. |
66+
| `thermalState` | `Int` | `0` nominal, `1` fair, `2` serious, `3` critical, `-1` unknown. |
67+
| `lowPowerMode` | `Int` | `1` when Low Power Mode is enabled, otherwise `0`. |
68+
69+
## Notifications
70+
71+
| Notification | Description |
72+
| --- | --- |
73+
| `actionAwareProcessorStart` | Posted when the sensor starts. |
74+
| `actionAwareProcessorStop` | Posted when the sensor stops. |
75+
| `actionAwareProcessor` | Posted after each processor sample. |
76+
| `actionAwareProcessorSync` | Posted when sync starts. |
77+
| `actionAwareProcessorSyncCompletion` | Posted when sync completes. |
78+
| `actionAwareProcessorSetLabel` | Posted when the label changes. |
79+
80+
## Observer
81+
82+
```swift
83+
final class Observer: ProcessorObserver {
84+
func onProcessorChanged(data: ProcessorData) {
85+
print(data.toDictionary())
86+
}
87+
}
88+
89+
let config = ProcessorSensor.Config().apply { config in
90+
config.interval = 30
91+
config.sensorObserver = Observer()
92+
}
93+
94+
let sensor = ProcessorSensor(config)
95+
sensor.start()
96+
```

0 commit comments

Comments
 (0)